Skip to content

jq in Scripts and Pipelines

Everything so far has been interactive: type a filter, read the answer. The same filters earn their keep inside shell scripts and pipelines, where jq plays three roles — extracting values into shell variables, answering yes/no questions for if, and translating between programs that speak different shapes of JSON. Each role has one habit that makes it safe.

Values into the shell: -r and a loop

Chapter 4's raw output exists for this. Strings without quotes can feed the shell's own machinery:

$ jq -r '.models[] | .name' models.json | while read -r m; do echo "checking $m"; done
checking ember-2:7b
checking ember-2:32b
checking kestrel-1:8b
checking kestrel-1:1.5b
checking harbor-4:70b

One value, capture it into a variable with the usual $(…):

$ count=$(jq '.models | length' models.json); echo "count=$count"
count=5

Two cautions belong with the pattern. A value with spaces or newlines survives read -r only if the JSON values are one-per-line and line-shaped — a model name is; a free-text message may not be, and chapter 7's stream discipline (one value per line, -c when in doubt) is what makes the loop honest. And never build a jq filter by gluing shell variables into the filter text; chapter 8's --arg passes values as data, where stray quotes can't rewrite the filter.

Questions for if: jq -e

Chapter 7's -e is the scripting form of a query — the script doesn't read the answer, it reads the exit status:

$ if jq -e '.models | length > 0' models.json > /dev/null; then echo "models present"; fi
models present

The > /dev/null discards the printed answer (true) because the branch only needs the status. This is the pattern for "is the backup's status field ok," "did any event reach level error," and every other yes/no a JSON file can answer — with chapter 7's caveat in force: -e judges the last output, so the filter must end with the verdict you mean.

The pipeline trap: jq fails quietly in a pipe

Now the habit that matters most. jq's own exit status is only visible at the end of a pipeline if the shell is told to look. By default, a pipeline's status is its last command's status:

$ jq '.models[] | .capabilities[]' models.json 2>/dev/null | wc -l; echo "pipeline exit: $?"
5
pipeline exit: 0

That filter errors — chapter 10's iterate-over-null, on the fourth model. Five lines still reached wc, wc succeeded, and the pipeline reports exit 0. A script trusting $? here believes everything is fine with a partial result: five capability lines, silently missing the rest. (2>/dev/null hides the error text in this demo so the statuses stand out; in real life that text was your only clue.)

The fix is one shell line, set before the pipes that matter:

$ set -o pipefail; jq '.models[] | .capabilities[]' models.json 2>/dev/null | wc -l; echo "pipeline exit: $?"
5
pipeline exit: 5

With pipefail, the pipeline's status is the last non-zero status in it — jq's 5 surfaces, and the script can refuse the partial answer. Same five lines, honest bookkeeping. Any script that pipes jq into something else wants pipefail (and, usually, the []?/// [] discipline of chapter 10 so the error never happens in the first place).

jq after curl: the pipeline everyone meets

The most common jq pipeline in the world has an HTTP client on the left:

curl -s https://api.example.com/v1/models | jq '.models[] | .name'

This guide doesn't run that command — there's no API here to call — but nothing about it is new: it is jq '…' models.json with a different left side, because models.json is this shape of response, saved to a file. Everything learned against the file transfers untouched: selection, reshaping, -e for "did the API return what we hoped," pipefail for "or did it fail halfway." When a pipeline misbehaves, save the response to a file once (curl -s … > response.json) and debug the filter against the file; a filter debugged against fixed data stays debugged.

When jq is the wrong tool

Honest limits, to close. jq loads each input value whole before filtering it, so a single JSON value of many gigabytes — or a -s slurp of a huge stream — is a memory problem a streaming parser in a real language handles better. JSON with one object per line that isn't quite JSON (trailing commas, comments, unquoted keys) isn't jq's input at all until something forgiving has cleaned it. Multi-file joins with real logic, persistent state, date arithmetic beyond the basics (jq does have date builtins — strptime/strflocaltime and friends — but they are a garnish, not a library), and anything that needs to be a program rather than describe a transformation: those belong to Python or your language of choice, which will happily call the same shapes of questions with a debugger attached.

The line this guide has been drawing all along: jq is the best tool yet made for asking questions of JSON and reshaping it in flight. Learn the eleven chapters' worth, and you'll know both the questions and the moment to stop asking jq.