Home

Awesome

jaq

Build status Crates.io Documentation Rust 1.65+

jaq (pronounced /ʒaːk/, like Jacques1) is a clone of the JSON data processing tool jq. jaq aims to support a large subset of jq's syntax and operations.

You can try jaq online on the jaq playground. Instructions for the playground can be found here.

jaq focuses on three goals:

I drew inspiration from another Rust program, namely jql. However, unlike jql, jaq aims to closely imitate jq's syntax and semantics. This should allow users proficient in jq to easily use jaq.

Installation

Binaries

You can download binaries for Linux, Mac, and Windows on the releases page. On a Linux system, you can download it using the following commands:

$ curl -fsSL https://github.com/01mf02/jaq/releases/latest/download/jaq-$(uname -m)-unknown-linux-musl -o jaq && chmod +x jaq
$ upx -d jaq # decompress binary for shorter startup time (optional step)

You may also install jaq using homebrew on macOS or Linux:

$ brew install jaq
$ brew install --HEAD jaq # latest development version

Or using scoop on Windows:

$ scoop install main/jaq

From Source

To compile jaq, you need a Rust toolchain. See https://rustup.rs/ for instructions. (Note that Rust compilers shipped with Linux distributions may be too outdated to compile jaq.)

Any of the following commands install jaq:

$ cargo install --locked jaq
$ cargo install --locked --git https://github.com/01mf02/jaq # latest development version

On my system, both commands place the executable at ~/.cargo/bin/jaq.

If you have cloned this repository, you can also build jaq by executing one of the commands in the cloned repository:

$ cargo build --release # places binary into target/release/jaq
$ cargo install --locked --path jaq # installs binary

jaq should work on any system supported by Rust. If it does not, please file an issue.

Examples

The following examples should give an impression of what jaq can currently do. You should obtain the same outputs by replacing jaq with jq. If not, your filing an issue would be appreciated. :) The syntax is documented in the jq manual.

Access a field:

$ echo '{"a": 1, "b": 2}' | jaq '.a'
1

Add values:

$ echo '{"a": 1, "b": 2}' | jaq 'add'
3

Construct an array from an object in two ways and show that they are equal:

$ echo '{"a": 1, "b": 2}' | jaq '[.a, .b] == [.[]]'
true

Apply a filter to all elements of an array and filter the results:

$ echo '[0, 1, 2, 3]' | jaq 'map(.*2) | [.[] | select(. < 5)]'
[0, 2, 4]

Read (slurp) input values into an array and get the average of its elements:

$ echo '1 2 3 4' | jaq -s 'add / length'
2.5

Repeatedly apply a filter to itself and output the intermediate results:

$ echo '0' | jaq '[recurse(.+1; . < 3)]'
[0, 1, 2]

Lazily fold over inputs and output intermediate results:

$ seq 1000 | jaq -n 'foreach inputs as $x (0; . + $x)'
1 3 6 10 15 [...]

Performance

The following evaluation consists of several benchmarks that allow comparing the performance of jaq, jq, and gojq. The empty benchmark runs n times the filter empty with null input, serving to measure the startup time. The bf-fib benchmark runs a Brainfuck interpreter written in jq, interpreting a Brainfuck script that produces n Fibonacci numbers. The other benchmarks evaluate various filters with n as input; see bench.sh for details.

I generated the benchmark data with bench.sh target/release/jaq jq-1.7.1 gojq-0.12.16 | tee bench.json on a Linux system with an AMD Ryzen 5 5500U.2 I then processed the results with a "one-liner" (stretching the term and the line a bit):

jq -rs '.[] | "|`\(.name)`|\(.n)|" + ([.time[] | min | (.*1000|round)? // "N/A"] | min as $total_min | map(if . == $total_min then "**\(.)**" else "\(.)" end) | join("|"))' bench.json

(Of course, you can also use jaq here instead of jq.) Finally, I concatenated the table header with the output and piped it through pandoc -t gfm.

Table: Evaluation results in milliseconds ("N/A" if error or more than 10 seconds).

Benchmarknjaq-2.0jq-1.7.1gojq-0.12.16
empty512300500230
bf-fib134401230570
defs10000060N/A1020
upto81920470460
reduce-update16384105501340
reverse104857640690280
sort1048576110530630
group-by104857650019201500
min-max1048576210320260
add10485764606301300
kv131072110150230
kv-update131072130540470
kv-entries1310725701150730
ex-implode10485765201110580
reduce1048576770890N/A
try-catch1048576290320370
repeat1048576140840530
from10485763201010590
last104857640240110
pyramid524288340350480
tree-contains2370610210
tree-flatten1778036010
tree-update177009701340
tree-paths17440280870
to-fromjson6553640360110
ack75207101220
range-prop128360320230
cumsum1048576280380450
cumsum-xy1048576430470710

The results show that jaq-2.0 is fastest on 25 benchmarks, whereas jq-1.7.1 is fastest on 1 benchmark and gojq-0.12.16 is fastest on 3 benchmarks. gojq is much faster on tree-flatten because it implements the filter flatten natively instead of by definition.

Features

Here is an overview that summarises:

Contributions to extend jaq are highly welcome.

Basics

Paths

Operators

Definitions

Core filters

Standard filters

These filters are defined via more basic filters. Their definitions are at std.jq.

Numeric filters

jaq imports many filters from libm and follows their type signature.

<details><summary>Full list of numeric filters defined in jaq</summary>

Zero-argument filters:

Two-argument filters that ignore .:

Three-argument filters that ignore .:

</details>

Modules

Advanced features

jaq currently does not aim to support several features of jq, such as:

Differences between jq and jaq

Numbers

jq uses 64-bit floating-point numbers (floats) for any number. By contrast, jaq interprets numbers such as 0 or -42 as machine-sized integers and numbers such as 0.0 or 3e8 as 64-bit floats. Many operations in jaq, such as array indexing, check whether the passed numbers are indeed integer. The motivation behind this is to avoid rounding errors that may silently lead to wrong results. For example:

$ jq  -n '[0, 1, 2] | .[1.0000000000000001]'
1
$ jaq -n '[0, 1, 2] | .[1.0000000000000001]'
Error: cannot use 1.0 as integer
$ jaq -n '[0, 1, 2] | .[1]'
1

The rules of jaq are:

Examples:

$ jaq -n '1 + 2'
3
$ jaq -n '10 / 2'
5.0
$ jaq -n '1.0 + 2'
3.0

You can convert an integer to a floating-point number e.g. by adding 0.0, by multiplying with 1.0, or by dividing with 1. You can convert a floating-point number to an integer by round, floor, or ceil:

$ jaq -n '1.2 | [floor, round, ceil]'
[1, 1, 2]

NaN and infinity

In jq, division by 0 yields an error, whereas in jaq, n / 0 yields nan if n == 0, infinite if n > 0, and -infinite if n < 0. jaq's behaviour is closer to the IEEE standard for floating-point arithmetic (IEEE 754).

Assignments

Like jq, jaq allows for assignments of the form p |= f. However, jaq interprets these assignments differently. Fortunately, in most cases, the result is the same.

In jq, an assignment p |= f first constructs paths to all values that match p. Only then, it applies the filter f to these values.

In jaq, an assignment p |= f applies f immediately to any value matching p. Unlike in jq, assignment does not explicitly construct paths.

jaq's implementation of assignment likely yields higher performance, because it does not construct paths. Furthermore, this allows jaq to use multiple outputs of the right-hand side, whereas jq uses only the first. For example, 0 | (., .) |= (., .+1) yields 0 1 1 2 in jaq, whereas it yields only 0 in jq. However, {a: 1} | .a |= (2, 3) yields {"a": 2} in both jaq and jq, because an object can only associate a single value with any given key, so we cannot use multiple outputs in a meaningful way here.

Because jaq does not construct paths, it does not allow some filters on the left-hand side of assignments, for example first, last, limit: For example, [1, 2, 3] | first(.[]) |= .-1 yields [0, 2, 3] in jq, but is invalid in jaq. Similarly, [1, 2, 3] | limit(2; .[]) |= .-1 yields [0, 1, 3] in jq, but is invalid in jaq. (Inconsequentially, jq also does not allow for last.)

Folding

jq and jaq provide filters reduce xs as $x (init; update), foreach xs as $x (init; update), and foreach xs as $x (init; update; project), where foreach xs as $x (init; update) is equivalent to foreach xs as $x (init; update; .).

In jaq, the output of these filters is defined very simply: Assuming that xs evaluates to x0, x1, ..., xn, reduce xs as $x (init; update) evaluates to

init
| x0 as $x | update
| ...
| xn as $x | update

and foreach xs as $x (init; update; project) evaluates to

init |
( x0 as $x | update | project,
( ...
( xn as $x | update | project,
( empty )...)

The interpretation of reduce/foreach in jaq has the following advantages over jq:

Miscellaneous

Contributing

Contributions to jaq are welcome. Please make sure that after your change, cargo test runs successfully.

Acknowledgements

This project was funded through the <a href="https://nlnet.nl/entrust">NGI0 Entrust</a> Fund, a fund established by <a href="https://nlnet.nl">NLnet</a> with financial support from the European Commission's <a href="https://ngi.eu">Next Generation Internet</a> programme, under the aegis of <a href="https://commission.europa.eu/about-european-commission/departments-and-executive-agencies/communications-networks-content-and-technology_en">DG Communications Networks, Content and Technology</a> under grant agreement N<sup>o</sup> 101069594.

jaq has also profited from:

Footnotes

  1. I wanted to create a tool that should be discreet and obliging, like a good waiter. And when I think of a typical name for a (French) waiter, to my mind comes "Jacques". Later, I found out about the old French word jacquet, meaning "squirrel", which makes for a nice ex post inspiration for the name.

  2. The binaries for jq-1.7.1 and gojq-0.12.16 were retrieved from their GitHub release pages.