On this page

The text processing chapter is a workshop of line tools: grep, sed, and awk read a line, decide something about it, and pass it on. JSON refuses to come in lines: one record pretty-prints across a dozen of them, and a pattern that matches a key matches just as happily inside a value. jq reads whole values instead of lines.

Installing jq

One line, depending on your distro, and jq --version for the receipt:

$ sudo apt install jq        # Ubuntu, Debian
$ sudo dnf install jq        # Fedora
$ sudo pacman -S jq          # Arch
$ jq --version
jq-1.8.1

Ubuntu 26.04 and Fedora 44 ship 1.8.1, Arch 1.8.2. Nothing below cares about the difference, with one exception noted when it turns up.

What JSON looks like

JSON has a short cast: objects in braces, arrays in square brackets, strings, numbers, booleans, and null. jq echoes any input back, formatted:

$ echo '{"name":"nethack"}' | jq .
{
  "name": "nethack"
}

Why do the line tools stumble? grep for name over the output above and every line holding the word comes back, keys and values together. The line is the wrong unit for JSON. jq reads the complete value and answers questions about structure instead of text. The shell gets first pass at what you type, though, which is where the shell essentials chapter matters: its pipes decide what jq receives.

First filters

A jq command is a filter: a short description of what to pull from the JSON on standard input. The empty filter ., shown above, prints the value unchanged, which is jq’s pretty-print. The simplest named filter is a field with a dot in front:

$ echo '{"name":"nethack"}' | jq '.name'
"nethack"

The answer wears double quotes because the value is a JSON string, and jq prints JSON. A field that is not there:

$ echo '{"name":"nethack"}' | jq '.nope'
null

null, and the exit status is 0: a missing field is an answer, not an error. Nesting is just more dots:

$ echo '{"a":{"b":42}}' | jq '.a.b'
42

That is the whole idea. The rest of the guide runs against one realistic file, so save three records from a small server as services.json: a game server, a download job, and ssh:

[
  { "name": "nethack", "state": "running", "port": 24565 },
  { "name": "curl", "state": "stopped", "port": null },
  { "name": "sshd", "state": "running", "port": 22 }
]

An array of three objects, each with a name, a state, and a port. curl carries null because it is stopped, and somebody here takes their nethack seriously.

Output control

Two flags change what comes out. -r prints raw strings, without the JSON quotes:

$ echo '{"name":"nethack"}' | jq -r '.name'
nethack

When a value is headed for another command, -r is what you want. -c squeezes each value onto one line:

$ echo '{"name":"nethack","state":"running"}' | jq -c .
{"name":"nethack","state":"running"}

Compact output puts records back in line-land. A filter can also read a file directly: pass the filename as the last argument and no pipe is needed. Every example from here on reads services.json this way.

$ jq -c '.[]' services.json
{"name":"nethack","state":"running","port":24565}
{"name":"curl","state":"stopped","port":null}
{"name":"sshd","state":"running","port":22}

Walking arrays and objects

The services.json fixture is an array, and arrays index from zero:

$ echo '["alpha","beta","gamma"]' | jq '.[0]'
"alpha"
$ echo '["alpha","beta","gamma"]' | jq '.[]'
"alpha"
"beta"
"gamma"

.[0] picks one element. .[] is the iterator: it produces every element in turn, which is why those answers arrived on separate lines. Feed it into a field and you get that field from every record. The pipe belongs to jq here; inside the quotes, bash hands it over untouched:

$ jq '.[] | .name' services.json
"nethack"
"curl"
"sshd"

length counts elements in an array and characters in a string:

$ echo '[10,20,30]' | jq 'length'
3
$ echo '"hello"' | jq 'length'
5

keys lists an object’s keys, sorted:

$ echo '{"zebra":1,"apple":2,"mango":3}' | jq 'keys'
[
  "apple",
  "mango",
  "zebra"
]

has() reports whether a key exists, with a JSON boolean:

$ echo '{"name":"nethack"}' | jq 'has("name")'
true
$ echo '{"name":"nethack"}' | jq 'has("nope")'
false

Picking and choosing

select() keeps the values that pass a test, and the matches print one after another:

$ jq '.[] | select(.state == "running")' services.json
{
  "name": "nethack",
  "state": "running",
  "port": 24565
}
{
  "name": "sshd",
  "state": "running",
  "port": 22
}

jq’s // fallback fires for a missing field, a null, and a false value. False bites: the .active field below is real, yet // swaps in "on".

$ echo '{"name":"nethack"}' | jq '.nickname // "none"'
"none"
$ echo '{"name":"nethack","nickname":null}' | jq '.nickname // "none"'
"none"
$ echo '{"nickname":"bob"}' | jq '.nickname // "none"'
"bob"
$ echo '{"active":false}' | jq '.active // "on"'
"on"

-e turns output into an exit status. If the last output is false or null, jq exits 1, otherwise 0:

$ echo '{"active":false}' | jq -e '.active'
false
$ echo $?
1

true exits 0. Exit 1 means the last output was false or null, and an input of null lands there too, because null in gives null out. Exit 4 means the filter produced no output at all, like a select with no match. That is a different case for a script branching on -e:

$ echo '{"a":1}' | jq -e '.a | select(. > 5)'
$ echo $?
4

The next run uses --arg n nethack, which hands jq a variable named n; the comparison prints false and jq still exits 0. Without -e, jq reports and succeeds either way, so scripts that branch on its answer need -e.

$ echo '{"name":"curl"}' | jq --arg n nethack '.name == $n'
false
$ echo $?
0

When the JSON itself is broken

$ echo '{"name":"nethack"' | jq .
jq: parse error: Unfinished JSON term at EOF at line 2, column 0
$ echo $?
5

Parse errors exit 5, not 2, with identical wording on 1.8.1 and 1.8.2.

Reshaping

Pulling fields out is half the job. Braces inside a filter build new JSON from the pieces:

$ echo '{"name":"nethack","state":"running"}' | jq '{user: .name}'
{
  "user": "nethack"
}

The key user is new, the value comes from .name, and the other field stays behind. map() runs a filter on every element of an array and collects the results:

$ echo '[{"name":"nethack"},{"name":"curl"}]' | jq 'map(.name)'
[
  "nethack",
  "curl"
]

Last comes -s, slurp: some input arrives as a stream of JSON values rather than one document, and -s reads the whole stream into one array first:

$ printf '{"n":1}\n{"n":2}\n{"n":3}\n' | jq -s 'length'
3

One honest limit: real jq code chains these constructs into longer pipelines, map(select(...)) feeding further pipes. This guide stays at one idea per filter on purpose. Composition is where jq gets both powerful and unreadable, and it does not fit in the space left.

JSON from your own machine

Two commands on a normal Linux box emit JSON, in the two shapes this guide has kept apart. The first is ip from the networking chapter, which with -j prints its interfaces as one JSON array:

$ ip -j addr | jq -r '.[].ifname'
lo
eth0

One array, one object per interface, lo first here, and .[0] gets you the first object. Field names like ifname belong to iproute2. Interfaces differ, so do not expect the same fields everywhere.

The second is journalctl from the services and logs chapter. Its -o json prints entries as JSON Lines: journalctl -n 5 -o json gives five objects on five lines, no array around them. Reach for .[0] and jq complains once per object:

jq: error (at <stdin>:1): Cannot index object with number

Five of those lines, one per record, and exit code 5. The message is exact: objects refuse numeric indexing. Arch’s jq 1.8.2 adds a (0) suffix to each line. The fix is -s, which slurps the line-per-object stream into one array first:

$ journalctl -n 5 -o json | jq -s 'length'
5

Two cautions. jq 'keys' without -s prints one keys array per record, not one for everything. And .MESSAGE is a plain string, so journalctl -n 3 -o json | jq -r '.MESSAGE' prints each entry’s message text, one per line, quotes stripped.

--arg is the bridge from shell values into filters. The shell expands "$name" before jq ever runs:

$ name=nethack
$ echo '{"name":"nethack"}' | jq --arg n "$name" '.name == $n'
true

Point it at {"name":"curl"} instead and the answer is false, again with exit 0. --arg n "$name" defines $n inside the filter, free of shell expansion.

Then the pitfall this guide owes you. Simple filters survive unquoted, because a bare .name holds nothing bash wants:

$ echo '{"name":"nethack"}' | jq .name
"nethack"

It worked, exit 0 and all. Add a character with a shell side job and the unquoted version falls apart three ways. The pipe splits the command, and bash tries to run a program named .name:

$ echo '{"name":"nethack"}' | jq .[] | .name
bash: .name: command not found

Exit 127, and that error is bash’s, not jq’s. An unquoted // ends with jq reporting jq: error: Could not open file none: No such file or directory and exit 2. Unquoted braces hand jq half a filter, which returns a compile error and exit 3. Single-quote the filter, always: it is the one form that never surprises you.

Capstone — counting what is running

Everything above folds into a small script that takes a JSON file, keeps the running records, and counts them:

#!/bin/bash
# running-count.sh -- count records in the running state
count=$(jq -c '.[] | select(.state == "running")' "$1" | wc -l)
echo "$1: $count running"

Two pipes are in play: jq’s own, inside the quotes, between the iterator and select, and a shell pipe handing jq’s output to wc -l from the text processing chapter. -c puts each match on its own line, which is what makes wc -l a record count:

$ bash running-count.sh services.json
services.json: 2 running

Where you are now

You can read both JSON shapes a Linux machine emits, pull fields and records from either. The habits: single-quote filters, -r for raw text, -s for one-object-per-line input, and -e when a script needs the verdict as an exit status. Deeper jq waits beyond this guide: reduce, def, streaming, jqplay, generating JSON, and live external APIs are all out of scope here, and every example above ran locally, so none of it rots.

For the shell side of today’s typing, the shell essentials chapter has the pipes. For quoting, see the pitfall above. Two references cover the rest:

  • jq manual — the full filter language and every flag used here
  • jq on GitHub — releases, issue tracker, and the source