jq by Example: A Tutorial That Walks the Whole Manual
jq is a command-line JSON processor. You feed it JSON, give it a small program called a filter, and it prints the result. The official manual is complete but terse, and its examples are deliberately abstract (.foo, {"a": 1}). This post walks that manual in the same order, section by section, but every example runs against one realistic sample file so you can see how the pieces fit together.
Every command below was executed against jq 1.8.2 and the output shown is what it printed. If your output differs, check jq --version first. Several builtins used here are newer than the jq 1.6 that some distributions still ship; those are called out where they appear.
The jq manual is licensed CC-BY-3.0 by the jq contributors. The explanations and examples here are my own, written after reading it; where I paraphrase a definition, the manual is the source.
Contents
- Setup
- 1. Invoking jq
- 2. Basic filters
- 3. Types and values
- 4. Builtin operators and functions
- 5. Conditionals and comparisons
- 6. Regular expressions
- 7. Advanced features
- 8. Math
- 9. I/O
- 10. Streaming
- 11. Assignment
- 12. Comments
- 13. Modules
- 14. Colors
- Putting it together
Setup
Install jq with your package manager (brew install jq, sudo apt install jq, sudo pacman -S jq), or download a static binary from the releases page.
All examples use this file. Save it as inventory.json in your working directory, or download it here.
{
"site": "homelab",
"updated": "2026-09-20T18:30:00Z",
"hosts": [
{
"name": "pve1",
"ip": "10.0.0.11",
"os": "proxmox",
"cpu": 8,
"mem_gb": 64,
"tags": ["hypervisor", "core"],
"services": [
{"name": "pve-web", "port": 8006},
{"name": "ssh", "port": 22}
],
"up": true
},
{
"name": "nas",
"ip": "10.0.0.20",
"os": "truenas",
"cpu": 4,
"mem_gb": 32,
"tags": ["storage", "core"],
"services": [
{"name": "smb", "port": 445},
{"name": "nfs", "port": 2049},
{"name": "ssh", "port": 22}
],
"up": true
},
{
"name": "pinetab",
"ip": "10.0.0.42",
"os": "arch",
"cpu": 4,
"mem_gb": 4,
"tags": ["mobile"],
"services": [],
"up": false,
"note": null,
"last-seen": "2026-09-19T07:12:00Z"
},
{
"name": "router",
"ip": "10.0.0.1",
"os": "openwrt",
"cpu": 2,
"mem_gb": 0.5,
"tags": ["network", "core"],
"services": [
{"name": "dns", "port": 53},
{"name": "ssh", "port": 22}
],
"up": true
}
]
}
How to read the examples. Lines beginning with $ are commands you type; everything under them is the output. Lines beginning with > are continuation lines of a multi-line command. Most examples use -c (compact output) so results fit on one line. Examples that start with jq -n need no input file.
One habit to adopt now: always single-quote the jq program. Characters like $, |, *, [ and " mean things to your shell too.
1. Invoking jq
jq reads a stream of whitespace-separated JSON values from stdin or from files named on the command line, runs the filter on each one, and prints each result on its own line. The simplest filter is ., the identity: it prints its input, pretty-printed.
$ jq '.site' inventory.json
"homelab"
Output flags
-r (--raw-output) prints strings without quotes, which is what you want when feeding other tools.
$ jq -r '.site' inventory.json
homelab
-c (--compact-output) puts each result on one line.
$ jq -c '.hosts[0].tags' inventory.json
["hypervisor","core"]
-j (--join-output) is -r without the trailing newline. The echo afterwards just ends the line.
$ jq -j '.hosts[].name' inventory.json; echo
pve1naspinetabrouter
--raw-output0 is -r with a NUL byte after each value instead of a newline, for piping into xargs -0. Here tr makes the NULs visible.
$ jq --raw-output0 '.hosts[].name' inventory.json | tr '\0' '|'; echo
pve1|nas|pinetab|router|
-a (--ascii-output) escapes everything outside ASCII.
$ jq -n '"μ"'
"μ"
$ jq -na '"μ"'
"\u03bc"
-S (--sort-keys) sorts object keys on output.
$ jq -nSc '{zebra: 1, apple: 2}'
{"apple":2,"zebra":1}
--indent n and --tab control pretty-printing.
$ jq --indent 4 '.hosts[0].services[0]' inventory.json
{
"name": "pve-web",
"port": 8006
}
$ jq --tab '.hosts[0].services[0]' inventory.json
{
"name": "pve-web",
"port": 8006
}
--seq wraps output in the application/json-seq framing: an ASCII record separator (0x1E) before each value. cat -v shows that byte as ^^. Note that --seq also changes how input is parsed: a plain JSON file without record separators is reported as an unfinished text, so the example generates its value with -n.
$ jq -n --seq '"homelab"' | cat -v
^^"homelab"
-C forces colour and -M disables it; see the Colors section.
Input flags
-n (--null-input) runs the filter once with null as input. This turns jq into a calculator or a JSON generator.
$ jq -n '1 + 2'
3
$ jq -nc '{name: "new", ports: [22, 80]}'
{"name":"new","ports":[22,80]}
-R (--raw-input) feeds each input line to the filter as a string instead of parsing JSON.
$ printf 'alpha\nbeta\n' | jq -R '.'
"alpha"
"beta"
-s (--slurp) reads all inputs into one array and runs the filter once.
$ echo '1 2 3' | jq -sc '.'
[1,2,3]
Combined, -Rs gives you the entire input as one string.
$ printf 'alpha\nbeta\n' | jq -Rs '.'
"alpha\nbeta\n"
--stream parses input incrementally as [path, leaf] events. It is covered in the Streaming section.
Passing values in
--arg name value binds $name to a string. --argjson name json parses the value as JSON first.
$ jq -c --arg os arch '.hosts[] | select(.os == $os) | .name' inventory.json
"pinetab"
$ jq -nc --arg n 5 --argjson m 5 '[$n, $m, ($n | type), ($m | type)]'
["5",5,"string","number"]
Both kinds also appear in $ARGS.named. Anything after --args becomes a positional string in $ARGS.positional; --jsonargs does the same but parses each one as JSON.
$ jq -nc --arg a 1 '$ARGS' --args x y
{"positional":["x","y"],"named":{"a":"1"}}
$ jq -nc '$ARGS.positional' --jsonargs 1 '{"a":2}' null
[1,{"a":2},null]
--slurpfile var file binds $var to an array of every JSON value in the file. --rawfile var file binds the file’s text as one string.
$ jq -nc --slurpfile inv inventory.json '($inv | length), $inv[0].site'
1
"homelab"
$ jq -nc --rawfile txt inventory.json '$txt | [type, .[0:1]]'
["string","{"]
-f file (--from-file) reads the program from a file. Handy once a filter outgrows one line.
$ echo '.hosts | length' > count.jq
$ jq -f count.jq inventory.json
4
Exit status
Normally jq exits 0 when the program ran, 2 for usage or system errors, and 3 for a compile error. With -e (--exit-status) the exit code also reflects the last output: 1 if it was false or null, 4 if there was no output at all. That makes jq usable in shell conditionals.
$ jq -e '.hosts[0].up' inventory.json; echo "exit $?"
true
exit 0
$ jq -e '.hosts[2].up' inventory.json; echo "exit $?"
false
exit 1
$ jq -e '.hosts[] | select(.name == "nope")' inventory.json; echo "exit $?"
exit 4
The rest
-L dir adds a module search path (Modules). --unbuffered flushes after every output, for slow pipelines. -b matters only for native Windows builds. --run-tests file runs jq’s own test file format, which is a nice way to keep a regression suite for your filters: program line, input line, expected output lines, blank line.
$ cat > examples.test <<'EOF'
> # program, then input, then expected output(s), then a blank line
> .a + 1
> {"a": 1}
> 2
>
> [.[] | . * 2]
> [1,2]
> [2,4]
> EOF
$ jq --run-tests examples.test | head -3
Test #1: '.a + 1' at line number 2
Test #2: '[.[] | . * 2]' at line number 6
2 of 2 tests passed (0 malformed, 0 skipped)
After the summary line, jq 1.8.2 goes on to print a handful of its own internal self-tests (Test jq_state: ...), which is why the example is trimmed with head.
2. Basic filters
Identity .
. outputs its input unchanged. Because jq pretty-prints by default, jq . is the standard way to format JSON from curl or a log file.
$ echo '{"a":1,"b":[1,2]}' | jq '.'
{
"a": 1,
"b": [
1,
2
]
}
Object identifier-index .foo, .foo.bar
.foo returns the value at key foo, or null if the key is missing. Chaining works as you would expect.
$ jq '.hosts[0].name' inventory.json
"pve1"
$ jq '.nothere' inventory.json
null
The bare .foo form only works for identifier-like keys (letters, digits, underscore, not starting with a digit). For anything else, quote the key or use bracket syntax.
$ jq '.hosts[2]."last-seen"' inventory.json
"2026-09-19T07:12:00Z"
$ jq '.hosts[2]["last-seen"]' inventory.json
"2026-09-19T07:12:00Z"
Optional .foo?
Indexing something that is not an object is an error. ? suppresses it and produces no output instead. The line number in the error is where the input value ended, so it will differ if you reformatted the file.
$ jq '.site.name' inventory.json
jq: error (at inventory.json:58): Cannot index string with string ("name")
$ jq '.site.name?' inventory.json
# no output, no error
Array index .[n] and slices .[a:b]
Arrays are zero-based. Negative indices count from the end. Slices are inclusive of the start and exclusive of the end, and either bound may be omitted. Slices also work on strings.
$ jq -c '.hosts[1].tags[0], .hosts[1].tags[-1]' inventory.json
"storage"
"core"
$ jq -c '.hosts[1:3] | map(.name)' inventory.json
["nas","pinetab"]
$ jq -c '[.hosts[].name] | .[-2:]' inventory.json
["pinetab","router"]
$ jq '.site[:4]' inventory.json
"home"
Iterator .[]
.[] emits every element of an array, or every value of an object, as separate outputs. This is the single most important idea in jq: a filter can produce many results, and everything downstream runs once per result.
$ jq '.hosts[].name' inventory.json
"pve1"
"nas"
"pinetab"
"router"
$ jq -c '.hosts[0].services[0] | .[]' inventory.json
"pve-web"
8006
.[]? is the error-suppressing form.
$ jq '.site[]' inventory.json
jq: error (at inventory.json:58): Cannot iterate over string ("homelab")
$ jq '.site[]?' inventory.json
# no output, no error
Comma ,
a, b feeds the same input to both filters and concatenates their outputs.
$ jq -c '.hosts[0] | .name, .ip, .up' inventory.json
"pve1"
"10.0.0.11"
true
$ jq -c '.hosts[0].tags[1, 0]' inventory.json
"core"
"hypervisor"
Pipe |
a | b runs b on each output of a. . on the right-hand side means “whatever the left side produced”.
$ jq '.hosts[] | .services[] | .port' inventory.json
8006
22
445
2049
22
53
22
Parentheses
Parentheses group, exactly as in arithmetic.
$ jq '.hosts[0] | (.cpu + .mem_gb) * 2' inventory.json
144
3. Types and values
jq has the JSON types: null, boolean, number, string, array, object. Literals are filters too: 42 ignores its input and outputs 42.
$ jq -nc '[null, true, 1.5, "s", [1], {"k": 1}] | map(type)'
["null","boolean","number","string","array","object"]
Numbers are IEEE 754 doubles once you do arithmetic on them, but jq 1.7 and later preserve the original text of a number literal that is passed through untouched.
$ echo '100000000000000000000000000001' | jq '.'
100000000000000000000000000001
$ echo '100000000000000000000000000001' | jq '. + 0'
1e+29
Array construction [...]
[f] collects every output of f into one array. [1,2,3] is really [] applied to the generator 1,2,3.
$ jq -c '[.hosts[].name]' inventory.json
["pve1","nas","pinetab","router"]
$ jq -c '[.site, .hosts[0].ip]' inventory.json
["homelab","10.0.0.11"]
Object construction {...}
Keys can be bare identifiers or strings; values are arbitrary filters run against the same input. {name} is shorthand for {name: .name}.
$ jq -c '.hosts[] | {name, up}' inventory.json
{"name":"pve1","up":true}
{"name":"nas","up":true}
{"name":"pinetab","up":false}
{"name":"router","up":true}
$ jq -c '.hosts[0] | {host: .name, ports: [.services[].port]}' inventory.json
{"host":"pve1","ports":[8006,22]}
If a value expression produces several outputs, you get several objects (a cartesian product).
$ jq -c '.hosts[0] | {name, port: .services[].port}' inventory.json
{"name":"pve1","port":8006}
{"name":"pve1","port":22}
A parenthesised key is evaluated as an expression. A variable used as a key contributes both its name and value.
$ jq -c '.hosts[0] | {(.name): .ip}' inventory.json
{"pve1":"10.0.0.11"}
$ jq -c '.site as $site | {$site}' inventory.json
{"site":"homelab"}
Recursive descent ..
.. emits the input and then every value nested inside it, at every depth. It pairs well with ? and the type selectors described later.
$ jq -c '[.. | .port? // empty]' inventory.json
[8006,22,445,2049,22,53,22]
$ jq -c '[.. | numbers]' inventory.json
[8,64,8006,22,4,32,445,2049,22,4,4,2,0.5,53,22]
4. Builtin operators and functions
Arithmetic: +, -, *, /, %
+ is overloaded by type: numbers add, strings and arrays concatenate, objects merge (right side wins on duplicate keys), and null + x is x.
$ jq -nc '1 + 2, "a" + "b", [1] + [2], {a: 1} + {b: 2}, null + 5'
3
"ab"
[1,2]
{"a":1,"b":2}
5
$ jq -nc '{a: 1, b: 1} + {b: 2}'
{"a":1,"b":2}
- subtracts numbers, or removes every occurrence of the right array’s elements from the left array.
$ jq -c '.hosts[0].tags - ["core"]' inventory.json
["hypervisor"]
* multiplies numbers, repeats a string, and deep-merges objects (compare +, which is shallow). / divides numbers or splits a string. % is modulo.
$ jq -nc '"ab" * 3, "a,b,c" / ",", 7 % 3, 7 / 2'
"ababab"
["a","b","c"]
1
3.5
$ jq -n '"x" * 0'
""
$ jq -nc '{a: {x: 1}} + {a: {y: 2}}'
{"a":{"y":2}}
$ jq -nc '{a: {x: 1}} * {a: {y: 2}}'
{"a":{"x":1,"y":2}}
Division by zero is an error; ? turns it into “skip this one”.
$ echo '[2, 0, 5]' | jq -c '[.[] | (10 / .)?]'
[5,2]
abs, length, utf8bytelength
length means: characters of a string, elements of an array, keys of an object, absolute value of a number, and 0 for null. abs is documented from jq 1.7.
$ jq -nc '[-3, 2.5] | map(abs)'
[3,2.5]
$ jq -c '.hosts[] | [.name, (.tags | length), (.services | length)]' inventory.json
["pve1",2,2]
["nas",2,3]
["pinetab",1,0]
["router",2,2]
$ jq -nc '"homelab", [1,2,3], {a: 1}, null, -7 | length'
7
3
1
0
7
$ jq -n '"μ" | length, utf8bytelength'
1
2
keys, keys_unsorted, has, in
keys sorts by codepoint; keys_unsorted keeps insertion order. On arrays, keys returns the indices. has(k) asks whether the input has key or index k; in(obj) asks the reverse question of the input.
$ jq -c '.hosts[0] | keys' inventory.json
["cpu","ip","mem_gb","name","os","services","tags","up"]
$ jq -c '.hosts[0] | keys_unsorted' inventory.json
["name","ip","os","cpu","mem_gb","tags","services","up"]
$ jq -c '.hosts[0].tags | keys' inventory.json
[0,1]
$ jq -c '[.hosts[] | has("note")]' inventory.json
[false,false,true,false]
$ jq -c '.hosts[0].tags | has(1), has(2)' inventory.json
true
false
$ jq -nc '"a", "z" | in({a: 1})'
true
false
map(f), map_values(f)
map(f) is [.[] | f]: apply f to each element and collect into an array. map_values(f) is .[] |= f: it keeps the container type, so an object stays an object, and it uses only the first output of f per value. If f produces nothing for a key, the key is dropped.
$ jq -c '.hosts | map(.name)' inventory.json
["pve1","nas","pinetab","router"]
$ jq -c '.hosts[0].services[0] | map_values(tostring)' inventory.json
{"name":"pve-web","port":"8006"}
$ jq -c '.hosts[2] | map_values(select(. != null))' inventory.json
{"name":"pinetab","ip":"10.0.0.42","os":"arch","cpu":4,"mem_gb":4,"tags":["mobile"],"services":[],"up":false,"last-seen":"2026-09-19T07:12:00Z"}
The manual’s idiom for dropping nulls is map_values(. // empty), but that would also drop "up": false. select(. != null) keeps it.
pick(pathexps)
pick (jq 1.7+) builds a new value containing only the given paths, preserving structure.
$ jq -c '.hosts[0] | pick(.name, .services[0].port)' inventory.json
{"name":"pve1","services":[{"port":8006}]}
Paths: path, paths, getpath, setpath, delpaths, del
A path is an array of keys and indices, like ["hosts",0,"name"]. path(f) converts a path expression to that form; paths lists every path in the input; paths(f) lists the paths whose values satisfy f.
$ jq -c 'path(.hosts[0].services[1].port)' inventory.json
["hosts",0,"services",1,"port"]
$ jq -nc '{a: [1, {b: 2}]} | [paths]'
[["a"],["a",0],["a",1],["a",1,"b"]]
$ jq -c '[paths(type == "boolean")]' inventory.json
[["hosts",0,"up"],["hosts",1,"up"],["hosts",2,"up"],["hosts",3,"up"]]
getpath and setpath read and write by path array. delpaths takes an array of paths; del takes path expressions.
$ jq 'getpath(["hosts", 3, "name"])' inventory.json
"router"
$ jq 'setpath(["hosts", 2, "up"]; true) | .hosts[2].up' inventory.json
true
$ jq -c '.hosts[0] | delpaths([["tags"], ["services"]])' inventory.json
{"name":"pve1","ip":"10.0.0.11","os":"proxmox","cpu":8,"mem_gb":64,"up":true}
$ jq -c '.hosts[2] | del(.note, ."last-seen") | keys_unsorted' inventory.json
["name","ip","os","cpu","mem_gb","tags","services","up"]
$ jq -c '.hosts[1].services | del(.[0, 2])' inventory.json
[{"name":"nfs","port":2049}]
to_entries, from_entries, with_entries(f)
These convert an object to and from an array of {key, value} pairs, which lets you use array tools on objects. with_entries(f) is to_entries | map(f) | from_entries. from_entries also accepts name/Name/Key/Value as field names.
$ jq -c '.hosts[0].services[0] | to_entries' inventory.json
[{"key":"name","value":"pve-web"},{"key":"port","value":8006}]
$ jq -nc '[{key: "a", value: 1}, {name: "b", Value: 2}] | from_entries'
{"a":1,"b":2}
$ jq -c '.hosts[0].services[0] | with_entries(.key |= ascii_upcase)' inventory.json
{"NAME":"pve-web","PORT":8006}
$ jq -c '.hosts[0] | with_entries(select(.value | type == "number"))' inventory.json
{"cpu":8,"mem_gb":64}
A very common real-world use is turning a list into a lookup object:
$ jq -c '.hosts[1].services | map({key: .name, value: .port}) | from_entries' inventory.json
{"smb":445,"nfs":2049,"ssh":22}
select(f) and the type selectors
select(f) passes its input through if f is truthy, otherwise produces nothing. arrays, objects, iterables, booleans, numbers, normals, finites, strings, nulls, values (non-null), and scalars (non-iterable) are shorthand selects on type.
$ jq '.hosts[] | select(.up) | .name' inventory.json
"pve1"
"nas"
"router"
$ jq '.hosts[] | select(.mem_gb >= 32) | .name' inventory.json
"pve1"
"nas"
$ jq -c '[.hosts[2][] | strings]' inventory.json
["pinetab","10.0.0.42","arch","2026-09-19T07:12:00Z"]
$ jq -c '[.hosts[2][] | numbers], [.hosts[2][] | iterables], [.hosts[2][] | nulls]' inventory.json
[4,4]
[["mobile"],[]]
[null]
$ jq -c '[.hosts[2][] | values] | length' inventory.json
9
empty
empty produces no output at all. It is how you say “nothing” in a branch or drop an element.
$ jq -nc '[1, empty, 2]'
[1,2]
$ jq -c '[.hosts[] | if .up then .name else empty end]' inventory.json
["pve1","nas","router"]
error, try/catch, halt, halt_error
error(msg) raises an error carrying any JSON value. try f catch g runs g with the error value if f fails. halt stops the program immediately with exit code 0. halt_error prints its input to stderr (strings raw, everything else as JSON) and exits with the given code, default 5.
$ jq -n 'try error("boom") catch "caught: \(.)"'
"caught: boom"
$ jq -n 'try error({code: 7}) catch .code'
7
$ jq -n '"partial", halt, "never printed"'
"partial"
$ jq -n '"fatal: disk on fire\n" | halt_error(3)'; echo "exit $?"
fatal: disk on fire
exit 3
$ jq -n '{err: "bad"} | halt_error'; echo "exit $?"
{"err":"bad"}
exit 5
$__loc__ gives the file and line of the program text where it appears, useful in error messages from longer scripts.
$ jq -nc '$__loc__'
{"file":"<top-level>","line":1}
add, any, all
add folds an array with +, so it sums numbers, concatenates strings and arrays, and merges objects. add(f) (jq 1.8) folds a generator instead. any and all test booleans, with one- and two-argument forms taking a condition or a generator plus condition.
$ jq '[.hosts[].mem_gb] | add' inventory.json
100.5
$ jq 'add(.hosts[].cpu)' inventory.json
18
$ jq -c '[.hosts[].tags] | add | unique' inventory.json
["core","hypervisor","mobile","network","storage"]
$ jq -nc '[] | add'
null
$ jq '[.hosts[].up] | any, all' inventory.json
true
false
$ jq 'any(.hosts[]; .os == "arch")' inventory.json
true
$ jq '.hosts | all(.cpu >= 2)' inventory.json
true
flatten, range
$ jq -c '[.hosts[].tags] | flatten' inventory.json
["hypervisor","core","storage","core","mobile","network","core"]
$ jq -nc '[1, [2, [3, [4]]]] | flatten(1), flatten'
[1,2,[3,[4]]]
[1,2,3,4]
$ jq -nc '[range(5)], [range(2; 5)], [range(0; 1; 0.25)], [range(5; 0; -2)]'
[0,1,2,3,4]
[2,3,4]
[0,0.25,0.5,0.75]
[5,3,1]
Number and type conversion: floor, sqrt, tonumber, toboolean, tostring, type
toboolean is new in jq 1.8. The full list of math functions is in the Math section.
$ jq -nc '[3.7 | floor, ceil, round], (16 | sqrt)'
[3,4,4]
4
$ jq -nc '["42", 42] | map(tonumber)'
[42,42]
$ jq -nc '["true", false] | map(toboolean)'
[true,false]
$ jq -nc '[1, "1", [1], {a: 1}, null] | map(tostring)'
["1","1","[1]","{\"a\":1}","null"]
$ jq -c '.hosts[2] | map_values(type)' inventory.json
{"name":"string","ip":"string","os":"string","cpu":"number","mem_gb":"number","tags":"array","services":"array","up":"boolean","note":"null","last-seen":"string"}
infinite, nan, isinfinite, isnan, isnormal
Infinity prints as the largest double and NaN prints as null, because JSON has neither.
$ jq -nc '[infinite, -infinite, nan] | map(isinfinite), map(isnan), .'
[true,true,false]
[false,false,true]
[1.7976931348623157e+308,-1.7976931348623157e+308,null]
Sorting and grouping: sort, sort_by, group_by, min/max, unique, reverse
jq’s total order is: null, false, true, numbers, strings, arrays, objects. sort_by(f) sorts by the outputs of f; multiple outputs act as tie-breakers.
$ jq -nc '[3, "a", null, true, [1], {a: 1}, false, 1.5] | sort'
[null,false,true,1.5,3,"a",[1],{"a":1}]
$ jq -c '.hosts | sort_by(.mem_gb) | map(.name)' inventory.json
["router","pinetab","nas","pve1"]
$ jq -c '.hosts | sort_by(.up, .cpu) | map(.name)' inventory.json
["pinetab","router","nas","pve1"]
group_by(f) returns an array of arrays, sorted by the grouping key. Pair it with map to summarise each group.
$ jq -c '.hosts | group_by(.up) | map(map(.name))' inventory.json
[["pinetab"],["pve1","nas","router"]]
$ jq -c '.hosts | group_by(.cpu) | map({cpu: .[0].cpu, hosts: map(.name)})' inventory.json
[{"cpu":2,"hosts":["router"]},{"cpu":4,"hosts":["nas","pinetab"]},{"cpu":8,"hosts":["pve1"]}]
$ jq '[.hosts[].mem_gb] | min, max' inventory.json
0.5
64
$ jq '.hosts | max_by(.cpu).name, min_by(.mem_gb).name' inventory.json
"pve1"
"router"
$ jq -c '[.hosts[].services[].name] | unique' inventory.json
["dns","nfs","pve-web","smb","ssh"]
$ jq -c '.hosts | unique_by(.cpu) | map(.name)' inventory.json
["router","nas","pve1"]
$ jq -c '[.hosts[].name] | reverse' inventory.json
["router","pinetab","nas","pve1"]
Searching: contains, inside, indices, index, rindex, startswith, endswith
contains is recursive and, for strings, means substring. That is a well-known gotcha: ["hypervisor"] contains ["hyper"]. inside is contains with the arguments swapped.
$ jq '.hosts[0].tags | contains(["core"])' inventory.json
true
$ jq '.hosts[0] | contains({tags: ["hyper"]})' inventory.json
true
$ jq -n '"lab" | inside("homelab")'
true
To test for an exact element, use index, any, or IN (shown in the SQL-style operators section).
$ jq -nc '"a-b-c" | indices("-"), index("-"), rindex("-")'
[1,3]
1
3
$ jq -nc '[1,2,1,2,1] | indices(1), indices([1,2])'
[0,2,4]
[0,2]
$ jq '[.hosts[].ip | startswith("10.0.0.")] | all' inventory.json
true
$ jq -c '[.hosts[].name | select(endswith("1"))]' inventory.json
["pve1"]
String trimming: ltrimstr, rtrimstr, trimstr, trim, ltrim, rtrim
The *str functions remove a specific prefix or suffix if present. trim, ltrim, rtrim and trimstr remove whitespace or a given string and are documented from jq 1.8.
$ jq -nc '"pve-web" | ltrimstr("pve-"), rtrimstr("-web")'
"web"
"pve"
$ jq -nc '"--x--" | trimstr("--")'
"x"
$ jq -nc '" padded\t" | trim, ltrim, rtrim'
"padded"
"padded\t"
" padded"
split, join, explode, implode, ascii_downcase, ascii_upcase, combinations
join converts numbers and booleans to strings and treats null as empty.
$ jq -c '.hosts[0].ip | split(".")' inventory.json
["10","0","0","11"]
$ jq -r '[.hosts[].name] | join(", ")' inventory.json
pve1, nas, pinetab, router
$ jq -nc '[1, null, "x", true] | join("-")'
"1--x-true"
$ jq -nc '"jq" | explode, (explode | implode)'
[106,113]
"jq"
$ jq -nc '"MiXed é" | ascii_downcase, ascii_upcase'
"mixed é"
"MIXED é"
$ jq -nc '[["a","b"], [1,2]] | [combinations]'
[["a",1],["a",2],["b",1],["b",2]]
$ jq -nc '[0, 1] | [combinations(2)]'
[[0,0],[0,1],[1,0],[1,1]]
Loops: while, until, repeat, recurse, walk
There is no loop statement in jq. These builtins are generators built from recursion. while(cond; update) emits . and keeps applying update while cond holds; until(cond; next) applies next until cond holds and emits only the final value.
$ jq -nc '[1 | while(. < 50; . * 3)]'
[1,3,9,27]
$ jq -nc '1 | until(. > 100; . * 2)'
128
repeat(f) is different from what the name suggests: it applies f to the same input over and over, forever, until f raises an error. The input never advances, so repeat(. * 2) on 1 yields 2, 2, 2, … Its real use is with filters that have side effects or eventually fail, like reading inputs until they run out.
$ jq -nc '[limit(3; 1 | repeat(. * 2))]'
[2,2,2]
$ echo '1 2 3' | jq -nc '[repeat(input)?]'
[1,2,3]
recurse(f) emits the input, then f of it, then f of that, and so on, following every branch when f is a generator. It is the tool for trees. recurse with no argument is ... The three-argument form stops when the condition fails.
$ jq -nc '{name: "/", children: [{name: "/etc", children: [{name: "/etc/hosts", children: []}]}]}
> | [recurse(.children[]) | .name]'
["/","/etc","/etc/hosts"]
$ jq -nc '[{a: [1]} | recurse]'
[{"a":[1]},[1],1]
$ jq -nc '[1 | recurse(. * 2; . < 40)]'
[1,2,4,8,16,32]
walk(f) applies f bottom-up to every value in the structure. It is how you transform every string, or rename keys everywhere, in one pass.
$ jq -c '.hosts[0] | walk(if type == "string" then ascii_upcase else . end)' inventory.json
{"name":"PVE1","ip":"10.0.0.11","os":"PROXMOX","cpu":8,"mem_gb":64,"tags":["HYPERVISOR","CORE"],"services":[{"name":"PVE-WEB","port":8006},{"name":"SSH","port":22}],"up":true}
$ jq -c 'walk(if type == "object" then with_entries(.key |= gsub("-"; "_")) else . end)
> | .hosts[2] | keys_unsorted' inventory.json
["name","ip","os","cpu","mem_gb","tags","services","up","note","last_seen"]
$ENV, env
$ FOO=bar jq -n '$ENV.FOO, env.FOO'
"bar"
"bar"
have_literal_numbers and have_decnum (jq 1.8) and $JQ_BUILD_CONFIGURATION (jq 1.7) report how your binary was compiled; their output depends on the build.
$ jq -n 'have_literal_numbers, have_decnum'
# (output varies)
transpose, bsearch
bsearch(x) on a sorted array returns the index of x, or -1 - insertion_point if it is absent.
$ jq -nc '[[1,2,3], [4,5]] | transpose'
[[1,4],[2,5],[3,null]]
$ jq -nc '[1,3,5,7] | bsearch(5), bsearch(4)'
2
-3
String interpolation \(...)
Inside a string, \(f) splices in the output of f. With -r this is the fastest way to produce human-readable reports.
$ jq -r '.hosts[] | "\(.name) has \(.cpu) cores and \(.mem_gb) GB"' inventory.json
pve1 has 8 cores and 64 GB
nas has 4 cores and 32 GB
pinetab has 4 cores and 4 GB
router has 2 cores and 0.5 GB
tojson, fromjson
tojson serialises any value to a JSON string; fromjson parses one. tostring differs in that it leaves strings alone.
$ jq -nc '{a: [1, "x"]} | tojson, (tojson | fromjson)'
"{\"a\":[1,\"x\"]}"
{"a":[1,"x"]}
$ jq -nc '"[1,2]" | tostring, tojson, fromjson'
"[1,2]"
"\"[1,2]\""
[1,2]
Format strings: @csv, @tsv, @html, @uri, @urid, @sh, @base64, @base64d, @json, @text
Each @format is a filter that escapes or encodes its input. @csv and @tsv want an array per row. Use -r so the result is not itself JSON-quoted.
$ jq -r '.hosts[] | [.name, .ip, .up] | @csv' inventory.json
"pve1","10.0.0.11",true
"nas","10.0.0.20",true
"pinetab","10.0.0.42",false
"router","10.0.0.1",true
$ jq -r '.hosts[] | [.name, .os] | @tsv' inventory.json
pve1 proxmox
nas truenas
pinetab arch
router openwrt
$ jq -nr '"<b>&\"hi\"</b>" | @html'
<b>&"hi"</b>
$ jq -nr '"a b&c=d" | @uri, (@uri | @urid)'
a%20b%26c%3Dd
a b&c=d
$ jq -nr '["rm", "my file", "x"] | @sh'
'rm' 'my file' 'x'
$ jq -nr '"homelab" | @base64, (@base64 | @base64d)'
aG9tZWxhYg==
homelab
@urid is new in jq 1.8. A format can also prefix a string literal: the literal text is left alone and only the interpolated parts are escaped. That is exactly what you want for URLs and shell commands.
$ jq -r '.hosts[0] | @uri "https://\(.ip):\(.services[0].port)/?q=\(.name + " status")"' inventory.json
https://10.0.0.11:8006/?q=pve1%20status
$ jq -r '.hosts[0] | @sh "ssh -p \(.services[1].port) admin@\(.ip)"' inventory.json
ssh -p 22 admin@'10.0.0.11'
$ jq -r '.hosts[0] | @json "payload=\(.services[0])"' inventory.json
payload={"name":"pve-web","port":8006}
Dates
Everything is UTC unless you use the local variants. fromdate/todate convert between ISO 8601 strings and Unix seconds. strptime(fmt) parses to a “broken-down time” array (year, zero-based month, day, hours, minutes, seconds, weekday, zero-based day of year); mktime turns that back into seconds; strftime(fmt) formats seconds or a broken-down time. now is the current time in seconds.
$ jq '.updated | fromdate' inventory.json
1789929000
$ jq -r '.updated | fromdate | todate' inventory.json
2026-09-20T18:30:00Z
$ jq -c '.updated | strptime("%Y-%m-%dT%H:%M:%SZ")' inventory.json
[2026,8,20,18,30,0,0,262]
$ jq -r '.updated | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime | strftime("%A, %d %B %Y")' inventory.json
Sunday, 20 September 2026
$ jq -c '0 | gmtime' inventory.json
[1970,0,1,0,0,0,4,0]
$ jq -n 'now | type'
"number"
Time arithmetic is just arithmetic on seconds:
$ jq '((.updated | fromdate) - (.hosts[2]."last-seen" | fromdate)) / 3600 | floor' inventory.json
35
localtime and strflocaltime use the TZ environment variable.
$ TZ=America/Denver jq -r '.updated | fromdate | strflocaltime("%Y-%m-%d %H:%M %Z")' inventory.json
2026-09-20 12:30 MDT
The manual notes that strptime’s %u and %j specifiers are not supported on macOS.
SQL-style operators: INDEX, JOIN, IN
INDEX(stream; f) builds an object keyed by f. IN(a, b, ...) tests membership in a stream, which is the exact-match test that contains is not. JOIN($idx; stream; key_expr; join_expr) looks each stream element up in an index and hands [element, match] to join_expr.
$ jq -c 'INDEX(.hosts[]; .name) | keys' inventory.json
["nas","pinetab","pve1","router"]
$ jq 'INDEX(.hosts[]; .name) | .nas.ip' inventory.json
"10.0.0.20"
$ jq -c '[.hosts[].os | IN("arch", "openwrt")]' inventory.json
[false,false,true,true]
$ jq 'IN(.hosts[].os; "arch", "windows")' inventory.json
true
$ jq -c --argjson loc '{"pve1":"rack-1","nas":"rack-1","router":"closet"}' \
> '[JOIN($loc; .hosts[]; .name; {name: .[0].name, location: .[1]})]' inventory.json
[{"name":"pve1","location":"rack-1"},{"name":"nas","location":"rack-1"},{"name":"pinetab","location":null},{"name":"router","location":"closet"}]
builtins
builtins lists every builtin as name/arity.
$ jq -nc '[builtins[] | select(startswith("split"))] | sort'
["split/1","split/2","splits/1","splits/2"]
5. Conditionals and comparisons
==, !=, <, <=, >, >=
Equality is strict (no type coercion) and structural (key order does not matter). Ordering uses the same total order as sort, so you can compare across types without errors.
$ jq -nc '1 == 1.0, "1" == 1, {a: 1, b: 2} == {b: 2, a: 1}, [1,2] != [2,1]'
true
false
true
true
$ jq -nc '[1 < 2, "a" < "b", null < false, [] < {}, "10" < "9"]'
[true,true,true,true,true]
if/elif/else/end
Only false and null are falsy. Empty strings, 0 and [] are all truthy, unlike in JavaScript or Python. else is optional and defaults to ..
$ jq -r '.hosts[] | if .up then "\(.name) is up" else "\(.name) is DOWN" end' inventory.json
pve1 is up
nas is up
pinetab is DOWN
router is up
$ jq -r '.hosts[] | "\(.name): " + (if .mem_gb >= 32 then "big" elif .mem_gb >= 4 then "medium" else "tiny" end)' inventory.json
pve1: big
nas: big
pinetab: medium
router: tiny
$ jq -nc '[1,2,3] | map(if . == 2 then "two" end)'
[1,"two",3]
$ jq -n '"" | if . then "truthy" else "falsy" end'
"truthy"
and, or, not
These return only true or false. not is a filter, so you pipe into it.
$ jq -c '[.hosts[] | select(.up and (.tags | index("core"))) | .name]' inventory.json
["pve1","nas","router"]
$ jq -nc '[(null and true), (0 or false), ("" | not), (false | not)]'
[false,true,false,true]
Alternative operator //
a // b yields the outputs of a that are not false or null; if there are none, it yields b. It is the idiom for defaults, with one trap: a legitimate false also triggers the default.
$ jq -c '[.hosts[] | .note // "n/a"]' inventory.json
["n/a","n/a","n/a","n/a"]
$ jq -nc '(false, null, 3, 4) // 9'
3
4
$ jq -nc 'empty // 9'
9
$ jq -nc '{up: false} | .up // true'
true
try/catch and ?
f? is try f with no handler: errors simply produce no output. Use catch to see the message.
$ jq -nc '["1", "x", "3"] | [.[] | tonumber?]'
[1,3]
$ jq -nc '["1", "x", "3"] | [.[] | try tonumber catch "bad: \(.)"]'
[1,"bad: string (\"x\") cannot be parsed as a number",3]
label/break
label $name | ... sets a point that break $name can jump back to, ending that generator early. The first(f) builtin is implemented this way.
$ jq -nc '[label $out | range(10) | if . > 3 then break $out else . end]'
[0,1,2,3]
$ jq 'label $stop | .hosts[] | select(.up) | .name, break $stop' inventory.json
"pve1"
6. Regular expressions
jq uses the Oniguruma library with Perl-style syntax and named groups. Because the regex is a JSON string, backslashes are doubled: \d is written "\\d". Flags are a second argument: g (all matches), i (case-insensitive), x (extended, ignore whitespace), n (ignore empty matches), s, m, p, l. Any regex filter also accepts a single [regex, flags] array.
test
$ jq -c '[.hosts[].name | test("^p")]' inventory.json
[true,false,true,false]
$ jq '.hosts[] | select(.ip | test("\\.1$")) | .name' inventory.json
"router"
$ jq -nc '"Host: NAS" | test("nas"), test("nas"; "i"), test(["nas", "i"])'
false
true
true
$ jq -nc '"10.0.0.1" | test("^ \\d+ \\. \\d+ \\. \\d+ \\. \\d+ $ # dotted quad"; "x")'
true
match, capture, scan
match returns detailed match objects. capture returns just the named groups as an object. scan returns matched strings (or arrays of groups) for every match.
$ jq -c '.hosts[0].ip | match("(\\d+)\\.(\\d+)$")' inventory.json
{"offset":5,"length":4,"string":"0.11","captures":[{"offset":5,"length":1,"string":"0","name":null},{"offset":7,"length":2,"string":"11","name":null}]}
$ jq -c '[.hosts[0].ip | match("\\d+"; "g").string]' inventory.json
["10","0","0","11"]
$ jq -c '.hosts[0].ip | capture("^(?<a>\\d+)\\.(?<b>\\d+)\\.(?<c>\\d+)\\.(?<d>\\d+)$")' inventory.json
{"a":"10","b":"0","c":"0","d":"11"}
$ jq -nc '"ports: 22, 443, 8006" | [scan("\\d+")]'
["22","443","8006"]
$ jq -nc '"ports: 22, 443, 8006" | [scan("(\\d)(\\d+)")]'
[["2","2"],["4","43"],["8","006"]]
split and splits with a regex
split(str) with one argument splits on a literal string. With two arguments (the second may be null) it splits on a regex. splits streams the pieces instead of returning an array.
$ jq -nc '"a1b22c333" | split("\\d+"; "g")'
["a","b","c",""]
$ jq -nc '"a, b;c" | split("[,;] *"; null)'
["a","b","c"]
$ jq -nc '"a1b22c333" | [splits("\\d+")]'
["a","b","c",""]
sub, gsub
sub replaces the first match, gsub every match. The replacement is a string with interpolation, and named captures are available in it as .name.
$ jq -r '.hosts[0].ip | sub("\\.\\d+$"; ".0/24")' inventory.json
10.0.0.0/24
$ jq -r '.updated | sub("^(?<d>[^T]+)T(?<t>[^Z]+)Z$"; "\(.d) at \(.t) UTC")' inventory.json
2026-09-20 at 18:30:00 UTC
$ jq -r '.hosts[0].os | gsub("o"; "0")' inventory.json
pr0xm0x
$ jq -nc '"a b c" | gsub("\\s+"; "_")'
"a_b_c"
$ jq -nc '"2026-09-20" | gsub("(?<n>\\d+)"; "<\(.n)>")'
"<2026>-<09>-<20>"
7. Advanced features
Variables: f as $x | ...
jq rarely needs variables because every filter already has an input. They earn their place when you need a value from elsewhere in the document while you are deep inside it. f as $x | rest runs rest once for each output of f, with the original input and $x bound.
$ jq -c '.site as $s | .hosts[] | {site: $s, name}' inventory.json
{"site":"homelab","name":"pve1"}
{"site":"homelab","name":"nas"}
{"site":"homelab","name":"pinetab"}
{"site":"homelab","name":"router"}
$ jq '(.hosts | length) as $n | [.hosts[].cpu] | add / $n' inventory.json
4.5
Variables are lexically scoped and immutable. Rebinding the same name shadows it.
$ jq -nc '[1 as $x | 2 as $x | $x]'
[2]
A lookup table passed with --argjson is a typical use:
$ jq -r --argjson owners '{"pve1":"nick","nas":"nick","router":"isp"}' \
> '.hosts[] | "\(.name): \($owners[.name] // "unknown")"' inventory.json
pve1: nick
nas: nick
pinetab: unknown
router: isp
Destructuring
The pattern after as can be an object or array shape. {$name} binds $name to .name; array patterns bind by position and give null for missing elements.
$ jq -c '.hosts[0] as {name: $n, services: [$first]} | {$n, $first}' inventory.json
{"n":"pve1","first":{"name":"pve-web","port":8006}}
$ jq '.hosts[0].services[] as {$name, $port} | "\($name)=\($port)"' inventory.json
"pve-web=8006"
"ssh=22"
$ jq -c '.hosts[] as {$name, tags: [$primary]} | {$name, $primary}' inventory.json
{"name":"pve1","primary":"hypervisor"}
{"name":"nas","primary":"storage"}
{"name":"pinetab","primary":"mobile"}
{"name":"router","primary":"network"}
Destructuring alternative ?//
When the input can take one of several shapes, list patterns separated by ?//. jq tries each in turn until one matches without error. Variables that appear only in a pattern that did not match are null. Here svc is sometimes a single object and sometimes an array, a common quirk of APIs converted from XML.
$ jq -nc '({host: "x", svc: {port: 22}}, {host: "y", svc: [{port: 80}, {port: 443}]})
> | . as {$host, svc: [{$port}]} ?// {$host, svc: {$port}}
> | {$host, $port}'
{"host":"x","port":22}
{"host":"y","port":80}
Defining functions: def
def name: body; defines a filter. Parameters are filters, not values: inside the body, f is re-evaluated against whatever . is at that point. Write def name($x) when you want a plain value instead. Functions may be recursive.
$ jq -c 'def up_names: map(select(.up) | .name); .hosts | up_names' inventory.json
["pve1","nas","router"]
$ jq -c 'def with_tag(t): map(select(.tags | index(t))); .hosts | with_tag("core") | map(.name)' inventory.json
["pve1","nas","router"]
$ jq -n 'def twice(f): f | f; 3 | twice(. * 2)'
12
$ jq -nc 'def add_all($x): map(. + $x); [1, 2] | add_all(10)'
[11,12]
$ jq -n 'def fact: if . <= 1 then 1 else . * (. - 1 | fact) end; 5 | fact'
120
Both functions and variables are visible only “to the right” of their definition, and only inside the parentheses that enclose them.
Generator utilities: isempty, limit, skip, first, last, nth
These take a generator and pick from it. skip is new in jq 1.8. Without an argument, first, last and nth(n) operate on an array input instead.
$ jq 'isempty(.hosts[] | select(.os == "windows"))' inventory.json
true
$ jq -c '[limit(2; .hosts[].name)]' inventory.json
["pve1","nas"]
$ jq -c '[skip(2; .hosts[].name)]' inventory.json
["pinetab","router"]
$ jq 'first(.hosts[] | select(.up | not) | .name)' inventory.json
"pinetab"
$ jq 'last(.hosts[].name), nth(2; .hosts[].name)' inventory.json
"router"
"pinetab"
$ jq '[.hosts[].name] | first, last, nth(1)' inventory.json
"pve1"
"router"
"nas"
reduce
reduce GEN as $x (INIT; UPDATE) starts with INIT as ., then for each output of GEN runs UPDATE with $x bound, feeding the result forward. It is jq’s fold, and the way to build counts and aggregates.
$ jq 'reduce .hosts[] as $h (0; . + $h.cpu)' inventory.json
18
$ jq -c 'reduce .hosts[] as $h ({}; .[$h.os] = $h.name)' inventory.json
{"proxmox":"pve1","truenas":"nas","arch":"pinetab","openwrt":"router"}
$ jq -c 'reduce .hosts[].tags[] as $t ({}; .[$t] += 1)' inventory.json
{"hypervisor":1,"core":3,"storage":1,"mobile":1,"network":1}
$ jq -c 'reduce .hosts[] as {$name, $up} ({up: [], down: []};
> if $up then .up += [$name] else .down += [$name] end)' inventory.json
{"up":["pve1","nas","router"],"down":["pinetab"]}
foreach
foreach is reduce that emits every intermediate state. An optional third expression transforms each state before it is emitted.
$ jq -c '[foreach .hosts[].mem_gb as $m (0; . + $m)]' inventory.json
[64,96,100,100.5]
$ jq -c 'foreach .hosts[] as $h (0; . + 1; {n: ., name: $h.name})' inventory.json
{"n":1,"name":"pve1"}
{"n":2,"name":"nas"}
{"n":3,"name":"pinetab"}
{"n":4,"name":"router"}
Recursion and generators
Any function can call itself, and the comma operator turns recursion into a generator. Tail calls are optimised when the recursive call is the last thing that produces output.
$ jq -nc 'def countdown: if . < 0 then empty else ., (. - 1 | countdown) end; [5 | countdown]'
[5,4,3,2,1,0]
$ jq -nc 'def fib: [0, 1] | recurse([.[1], add]) | .[0]; [limit(10; fib)]'
[0,1,1,2,3,5,8,13,21,34]
8. Math
jq exposes most of the C math library. One-argument C functions take their argument from . (2 | sqrt); two- and three-argument functions take jq arguments and ignore . (pow(2; 8)). All arithmetic is IEEE 754 double precision, with the usual consequences.
$ jq -nc '[2 | exp2, log2, sqrt], [pow(2; 8), fmin(3; 9), fmax(3; 9), fmod(10; 3)], (1 | atan * 4)'
[4,1,1.4142135623730951]
[256,3,9,1]
3.141592653589793
$ jq -nc '[-2.5 | floor, ceil, round, trunc, fabs]'
[-3,-2,-3,-2,2.5]
$ jq -nc '8 | frexp'
[0.5,4]
$ jq -nc '3.25 | modf'
[0.25,3]
$ jq -n '0.1 + 0.2'
0.30000000000000004
$ jq -n '3.14159 * 100 | round / 100'
3.14
Functions missing from your platform’s libm are still defined but raise an error when called.
9. I/O
jq reads inputs for you, one per filter run. input and inputs let the program pull inputs itself, which is how you fold across many inputs or pair them up. Use -n so jq does not consume the first one before your program starts.
$ echo '1 2 3 4' | jq -c '[., input]'
[1,2]
[3,4]
$ echo '1 2 3' | jq -n 'reduce inputs as $i (0; . + $i)'
6
$ printf 'alpha\nbeta\n' | jq -nRc '[inputs]'
["alpha","beta"]
debug passes its input through unchanged and prints it to stderr, wrapped in ["DEBUG:", ...]. debug(msg) (jq 1.7) prints a custom message instead. stderr prints its input raw to stderr with no newline.
$ echo '{"a":1}' | jq -c 'debug | .a'
["DEBUG:",{"a":1}]
1
$ echo '5' | jq 'debug("value is \(.)") | . * 2'
["DEBUG:","value is 5"]
10
$ jq -n '"to stderr\n" | stderr | empty'
to stderr
input_filename and input_line_number report where the current input came from.
$ jq -c '[input_filename, input_line_number]' inventory.json
["inventory.json",58]
10. Streaming
With --stream, jq emits [path, leaf] for every scalar (and empty container), and [path] when a container closes, without waiting to parse the whole document. tostream produces the same events from an in-memory value; fromstream reassembles them.
$ echo '{"a":[1,{"b":2}]}' | jq -c --stream '.'
[["a",0],1]
[["a",1,"b"],2]
[["a",1,"b"]]
[["a",1]]
[["a"]]
$ echo '{"a":[1,{"b":2}]}' | jq -c 'fromstream(tostream)'
{"a":[1,{"b":2}]}
Stream events make it trivial to flatten a document into dotted keys:
$ echo '{"a":[1,{"b":2}]}' | jq -c --stream 'select(length == 2) | {(.[0] | map(tostring) | join(".")): .[1]}'
{"a.0":1}
{"a.1.b":2}
The main use is processing a huge array one element at a time. truncate_stream(depth) strips the leading path components, so the elements of .hosts become top-level values that fromstream emits one by one. Memory use stays constant no matter how many hosts there are.
$ jq -nc --stream 'fromstream(2 | truncate_stream(inputs)) | .name' inventory.json
"pve1"
"nas"
"pinetab"
"router"
11. Assignment
Assignment never mutates. The left side is a path expression, jq computes a new value with those paths changed, and outputs it. The original is untouched, which the second output here proves.
$ jq -nc '{a: 1} | (.a = 2), .'
{"a":2}
{"a":1}
Update |= and arithmetic updates +=, -=, *=, /=, %=, //=
path |= f replaces each value at path with f applied to that value. a += b is a |= . + b, and so on.
$ jq -c '.hosts[0].services[0].port |= . + 1 | .hosts[0].services[0]' inventory.json
{"name":"pve-web","port":8007}
$ jq -c '.hosts[].cpu += 2 | [.hosts[].cpu]' inventory.json
[10,6,6,4]
$ jq -c '.hosts[].mem_gb *= 1024 | [.hosts[].mem_gb]' inventory.json
[65536,32768,4096,512]
$ jq -c '.hosts[].note //= "none" | [.hosts[].note]' inventory.json
["none","none","none","none"]
$ jq -c '.hosts[].tags |= sort | .hosts[0].tags' inventory.json
["core","hypervisor"]
If the right side of |= produces empty, the path is deleted. If it produces several values, only the first is used.
$ jq '.hosts[2].note |= empty | .hosts[2] | has("note")' inventory.json
false
$ jq '.hosts[0].name |= (., "ignored") | .hosts[0].name' inventory.json
"pve1"
Plain assignment =
path = f evaluates f against the whole input (not the value at the path), and if f yields several values you get several outputs. Missing intermediate objects and array slots are created.
$ jq -nc '{a: {b: 10}, b: 20} | (.a = .b), (.a |= .b)'
{"a":20,"b":20}
{"a":10,"b":20}
$ jq -nc '{} | (.a, .b) = (1, 2)'
{"a":1,"b":1}
{"a":2,"b":2}
$ jq -nc '{} | .a.b.c = 1'
{"a":{"b":{"c":1}}}
$ jq -nc '[1, 2] | .[4] = 9'
[1,2,null,null,9]
Complex assignments
Any path expression works on the left, including ones that use select or .., so you can update just the matching parts of a document in place.
$ jq -c '(.hosts[] | select(.name == "pinetab") | .up) = true | [.hosts[].up]' inventory.json
[true,true,true,true]
$ jq -c '(.hosts[] | select(.os == "arch") | .tags) += ["arm"] | .hosts[2].tags' inventory.json
["mobile","arm"]
$ jq -c '(.. | numbers) |= . * 2 | [.. | numbers] | .[:3]' inventory.json
[16,128,16012]
$ jq 'del(.hosts[] | select(.up | not)) | .hosts | length' inventory.json
3
12. Comments
# starts a comment to end of line, which makes multi-line programs readable. A backslash at the end of a comment line continues the comment onto the next line (documented in the 1.8 manual; check that your version supports it). The manual uses that to build a shebang line that both sh and jq accept.
$ jq -c '
> # keep only hosts that are up
> .hosts
> | map(select(.up)) # drop the down ones
> | map(.name)
> ' inventory.json
["pve1","nas","router"]
$ cat > up-hosts <<'EOF'
> #!/bin/sh --
> # up-hosts: print the names of hosts that are up
> # \
> exec jq -rf -- "$0" "$@"
> .hosts[] | select(.up) | .name
> EOF
$ chmod +x up-hosts
$ ./up-hosts inventory.json
pve1
nas
router
sh runs the exec line (a backslash inside a shell comment means nothing), and jq sees that same line as the continuation of the comment above it.
13. Modules
A module is a .jq file. import "path" as name; makes its definitions available as name::def; include "path"; pulls them into the current namespace. import "path" as $name; loads a .json file as data. The search path is set with -L; by default jq looks in ~/.jq, $ORIGIN/../lib/jq and $ORIGIN/../lib. If ~/.jq is a file, it is sourced into every program automatically.
$ mkdir -p lib
$ cat > lib/inv.jq <<'EOF'
> module {name: "inv", version: "1.0"};
> def up: map(select(.up));
> def names: map(.name);
> EOF
$ cat > lib/ports.json <<'EOF'
> {"ssh": 22, "dns": 53}
> EOF
$ jq -c -L lib 'import "inv" as inv; .hosts | inv::up | inv::names' inventory.json
["pve1","nas","router"]
$ jq -c -L lib 'include "inv"; .hosts | up | names' inventory.json
["pve1","nas","router"]
$ jq -nc -L lib 'import "ports" as $p; $p::p'
[{"ssh":22,"dns":53}]
Note that a JSON module is bound to an array of every value in the file. modulemeta reports a module’s metadata, dependencies and definitions.
$ jq -nc -L lib '"inv" | modulemeta'
{"name":"inv","version":"1.0","deps":[],"defs":["up/0","names/0"]}
14. Colors
When stdout is a terminal jq colours its output; -C forces it through a pipe and -M turns it off. Setting NO_COLOR to a non-empty value disables colour by default. JQ_COLORS is a colon-separated list of ANSI attribute pairs for, in order: null, false, true, numbers, strings, arrays, objects, object keys. The default is 0;90:0;39:0;39:0;39:0;32:1;39:1;39:1;34.
$ JQ_COLORS="0;31" jq -Cn 'null' | cat -v
^[[0;31mnull^[[0m
$ jq -Cc '.hosts[0].services[0]' inventory.json | cat -v | cut -c1-40
^[[1;39m{^[[0m^[[1;34m"name"^[[0m^[[1;39
Putting it together
A few one-liners that combine the sections above, of the kind you end up typing every day.
Tab-separated report of hosts that are up, aligned with column:
$ jq -r '.hosts[] | select(.up) | [.name, .ip, .os] | @tsv' inventory.json | column -t
pve1 10.0.0.11 proxmox
nas 10.0.0.20 truenas
router 10.0.0.1 openwrt
CSV with a header row, by putting the header and the rows through the same @csv:
$ jq -r '["name", "ports"], (.hosts[] | [.name, ([.services[].port] | join(" "))]) | @csv' inventory.json
"name","ports"
"pve1","8006 22"
"nas","445 2049 22"
"pinetab",""
"router","53 22"
Which hosts expose which service, inverted from the host-centric layout:
$ jq -c 'reduce (.hosts[] | {name, svc: .services[].name}) as $r ({}; .[$r.svc] += [$r.name])' inventory.json
{"pve-web":["pve1"],"ssh":["pve1","nas","router"],"smb":["nas"],"nfs":["nas"],"dns":["router"]}
Merge several JSON documents (from separate files or a stream) into one:
$ echo '{"a":1,"b":{"x":1}} {"b":{"y":2}}' | jq -sc 'reduce .[] as $d ({}; . * $d)'
{"a":1,"b":{"x":1,"y":2}}
Canonicalise a file for diffing: sorted keys, stable formatting.
$ jq -S '.hosts[0].services[0]' inventory.json
{
"name": "pve-web",
"port": 8006
}
That covers every section of the jq 1.8 manual. Keep it open alongside this page; once the model of “filters that produce streams of values” clicks, the rest of the manual reads as a catalogue rather than a puzzle.