New to jetro? jetro-book is the best place to start — guide, tour, and documentation for learning the expression language.
jetrocli is a terminal companion for jetro, a JSON expression language. It gives you two ways to work with JSON: an interactive TUI for exploring data and a command-line program for processing JSON in scripts, pipes, files, and NDJSON datasets.
Run it directly and it opens a split-pane JSON workbench: paste or load JSON, write a jetro expression, and see the result update live as you type. Pipe or redirect input and it skips the TUI, evaluates once, and prints the result like a regular Unix command.
For large files, jetrocli can memory map regular-file input. In --ndjson mode it scans one JSON document per line, supports reverse reads from the end of log-style files, and can stop early with --limit for bounded queries over very large datasets.
- Live evaluation — expression re-runs on every keystroke.
- Syntax-highlighted JSON result with pretty-print (strings, numbers, keys, booleans, null distinct).
- Structural folding in JSON editor. Fold any
{…}/[…]block, with gutter triangles (▾/▸) and inline⋯ N linesmarkers. - Schema-aware completion — suggests fields at the current path, auto-unwraps element fields inside array chains, filters builtins by receiver type.
- Inline docs pane next to completions — every jetro builtin ships with signature, summary, and example.
- Pipe / batch mode without TUI — when stdin is piped or redirected, jetrocli evaluates once and prints the result directly for shell workflows.
- File-backed large JSON reads — regular-file stdin is memory mapped for zero-copy loading instead of forcing the interactive path.
- Fast NDJSON scans —
--ndjsonevaluates one JSON document per line from-i <FILE>and emits one compact result per row. - Reverse NDJSON reads —
--ndjson -rscans from tail to head, useful for log-style files where the newest rows matter first. - Bounded NDJSON filters — combine
--ndjsonwith--limit <N>to stop after the firstNemitted rows, including reverse scans for "latest matching rows" queries. - Emacs-style bindings throughout (
C-a/C-e,C-f/C-b,M-f/M-b,C-n/C-p,C-g,C-cprefix chord). - Expression formatter — breaks long jetro chains onto indented lines (
C-c C-f).
brew tap mitghi/jetrocli https://github.com/mitghi/homebrew-jetrocli
brew install jetroclicurl --proto '=https' --tlsv1.2 -LsSf https://github.com/mitghi/jetrocli/releases/latest/download/jetrocli-installer.sh | shgit clone https://github.com/mitghi/jetrocli
cd jetrocli
cargo build --releaseBinary lands at target/release/jetrocli.
Default when stdin is a TTY.
jetrocli # sample document
jetrocli -i data.json # load from file
jetrocli -i data.json -e '$.users' # pre-fill expressionWhen stdin is piped or redirected, TUI is skipped — jetrocli evaluates the expression against stdin and prints the result with jq-style colorized JSON (ANSI dropped when stdout not a TTY; respects NO_COLOR and JETROCLI_COLOR=never).
echo '{"users":[{"name":"a"},{"name":"b"}]}' | jetrocli '$.users.name'
curl -s api.example.com/data | jetrocli '$.items.first()'With no expression (or empty string), jetrocli just pretty-prints stdin as JSON, like jq with no filter:
cat data.json | jetrocli
echo '{"a":1,"b":[2,3]}' | jetrocli ''When stdin is backed by a regular file (jetrocli EXPR < big.json), input is mmap'd instead of streamed — zero-copy load for large documents. Real pipes fall back to a buffered read.
--ndjson switches jetrocli into newline-delimited JSON batch mode: one JSON document per line in -i <FILE>, expression evaluated independently per row, one compact result per output line. File input only.
jetrocli --ndjson -i events.ndjson -e '$.user'
jetrocli --ndjson -i events.ndjson --limit 100 -e '$.level == "error"'
jetrocli --ndjson -i app.log -r -e '$.msg' # tail → head
jetrocli --ndjson -i app.log -r --limit 50 -e '$.msg' # last 50 matches
jetrocli --ndjson -i topic.log --payload-after '|' -e '$.id' # key|JSON payloadUse $.rows() when the expression should operate on the whole NDJSON file as one stream instead of running independently per line:
jetrocli --ndjson -i events.ndjson \
-e '$.rows().filter($.active).take(10).map({id: $.id, name: $.name})'For file inputs, $.rows().reverse() scans from the end of the file. This is useful for logs and Kafka compacted-topic dumps where the newest record for a key is last:
jetrocli --ndjson -i events.ndjson \
-e '$.rows().reverse().distinct_by($.id).take(100).map({id: $.id, ts: $.ts})'On the 1 GB benchmark described below, simple row-local projections are typically tens of times faster than jaq, and the best direct byte paths are near 100x faster. Whole-stream $.rows() queries keep the same mmap/direct-byte foundation, but performance depends on how much of the file must be scanned: take(...) and reverse latest-by-key style queries can stop early, while broad filter, distinct_by, or fallback expressions naturally pay for every row they inspect.
| Flag | Effect |
|---|---|
--ndjson |
Enable NDJSON mode. Requires -i <FILE> and a non-empty expression. |
-r, --reverse |
Read file from tail to head via mmap. Requires --ndjson. |
--limit <N> |
Stop after N emitted rows. Requires --ndjson and N ≥ 1. |
--payload-after <SEP> |
Treat each line as prefix<SEP>JSON and query only the JSON payload after the one-byte separator. Non-payload rows, empty payloads, and skipped null tombstones are ignored. |
--null-payload <skip|keep|error> |
Policy for framed null payloads when --payload-after is used. Defaults to skip. |
--max-line-bytes <BYTES> |
Per-line byte cap. Default 64 MiB. |
--reverse-chunk <BYTES> |
Reverse reader chunk size. Tune for very wide rows. |
NDJSON mode is built for file-backed batch scans. It memory maps the input file, evaluates the expression independently for each line, and emits compact JSON results without starting the interactive TUI.
The repository includes a reproducible benchmark in benchmark/:
rustc -O benchmark/gen_ndjson.rs -o /tmp/gen_ndjson
/tmp/gen_ndjson /tmp/big.ndjson 1000000000
benchmark/bench.shbenchmark/bench.sh compares only the jetrocli and jaq program. The generator writes roughly 1 GB of NDJSON shaped like:
{"id":1,"name":"user_1","attributes":[{"key":"k1","value":"v_1_1"}]}One run on an Apple M1 laptop over 4,764,404 rows produced:
| Query | jetro expression | jaq expression | jetrocli | jaq | Speedup |
|---|---|---|---|---|---|
| Project id | $.id |
.id |
0.72s | 27.89s | 38.7x |
| Project name | $.name |
.name |
0.30s | 28.99s | 96.6x |
| Attributes count | $.attributes.len() |
.attributes | length |
1.52s | 28.19s | 18.5x |
| Attribute keys list | $.attributes.map(@.key) |
.attributes | map(.key) |
2.06s | 39.98s | 19.4x |
| First attr value | $.attributes.first().value |
.attributes[0].value |
0.80s | 29.22s | 36.5x |
| Last attr value | $.attributes.last().value |
.attributes[-1].value |
1.61s | 29.25s | 18.2x |
| Uppercase name | $.name.upper() |
.name | ascii_upcase |
0.41s | 28.42s | 69.3x |
[key,value] pairs |
$.attributes.map([@.key, @.value]) |
.attributes | map([.key, .value]) |
2.99s | 54.26s | 18.1x |
Count attrs matching _3 |
$.attributes.filter(@.value.contains("_3")).len() |
[.attributes[] | select(.value | contains("_3"))] | length |
1.52s | 48.15s | 31.7x |
| Object keys | $.keys() |
keys |
1.05s | 28.48s | 27.1x |
In practice, expect NDJSON mode to be especially strong for field projection, string transforms, row-local indexing, and shallow array operations over large files. Queries that allocate larger derived arrays or inspect more nested values naturally move more bytes and take longer.
MIT
