Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 11 – Text processing: the Unix toolbox

jq: query and reshape JSON from the shell

Build jq filters with ., .key, .[], select, map and group_by; pass shell values with --arg; produce raw text, CSV or TSV from a shell loop.

  • Intermediate
  • 15 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

What you will learn

  • Build jq filters from ., .key, .[], the pipe and the comma, and read how -r, -c, -s and -n change input and output
  • Filter arrays with select, transform with map and group with group_by, sort_by and unique_by on the lesson's orders file
  • Pass shell values into jq with --arg and --argjson, and explain the paste-into-filter trap
  • Format shell-friendly output with @csv, @tsv, @sh, -r and the while IFS loop
  • Read jq's exit status in scripts, including -e, invalid JSON input and the pipefail interaction

Before you start

What you’ll learn · Before you start

What you will learn

  • Build jq filters from ., .key, .[], the pipe and the comma, and read how -r, -c, -s and -n change input and output
  • Filter arrays with select, transform with map and group with group_by, sort_by and unique_by on the lesson's orders file
  • Pass shell values into jq with --arg and --argjson, and explain the paste-into-filter trap
  • Format shell-friendly output with @csv, @tsv, @sh, -r and the while IFS loop
  • Read jq's exit status in scripts, including -e, invalid JSON input and the pipefail interaction

Before you start

On this page

One line of jq pulls three paid orders out of a four-order JSON blob and sums their amounts: jq '[.orders[] | select(.status == "paid") | .amount] | add' orders.json prints 1870. The same work in a hand-written script is a loop, a parser and an accumulator. jq ships that pipeline as its everyday idiom and runs it on a stream, one value at a time.

You will build filters out of five building blocks (., .key, .[], the pipe | and the comma ,); use select, map, group_by, sort_by and unique_by to filter, transform and summarise an orders file; pass shell values in with --arg and --argjson instead of pasting them into the filter; format output with @csv, @tsv, @sh and -r so a bash loop can read it; and read jq’s exit status in a script, including the special -e flag. The toolchain pins jq 1.8.2, with its filters and format strings documented in the manual sections cited below.

A filter is a pipeline of generators

A jq filter: A jq program; a chain of operators that reads JSON on standard input and writes JSON on standard output, one value at a time. is a short program that reads JSON on standard input and writes JSON on standard output. Five building blocks make almost every filter: the identity ., the object identifier-index .key or .["key"], the array index .[0] and iterator .[], the pipe | and the comma ,. The jq manual says each one on its own, but the lesson’s examples use them together, which is how a real filter looks.

., .key, .[0], .[], the pipe and the comma on an orders file Python · basics_session.py
"""A filter is a pipeline of generators: ., .key, .[], the pipe and the comma.

The session shows:
1. The identity filter . reads JSON on standard input and pretty-prints it on standard output.
2. .orders is the object identifier-index; .orders[0] indexes an array; .orders[].id iterates it.
3. The pipe | feeds the output of the filter on its left into the one on its right (jq manual, Pipe).
4. The comma , runs two filters on the same input and prints both streams in order.
5. -c (--compact-output) prints each result on one line; -r (--raw-output) drops the quotes on a string.
6. .foo? uses the optional operator so a type mismatch produces no output instead of an error.
"""
from session import run

run("""
# A tiny JSON file of three orders with id, city, items, amount and status
printf '%s\\n' '{"orders":[{"id":"o1","city":"Pune","items":2,"amount":450,"status":"paid"},{"id":"o2","city":"Chennai","items":1,"amount":220,"status":"pending"},{"id":"o3","city":"Pune","items":4,"amount":880,"status":"paid"}]}' > orders.json
jq --version

# . is the identity filter: it reads the input and pretty-prints it
jq . orders.json

# .orders is an object index; it picks one field of the object
jq '.orders' orders.json

# .[0] indexes an array; combine with .orders to pick the first order
jq '.orders[0]' orders.json

# .[] iterates the array and emits each element as its own value
jq '.orders[].id' orders.json

# The pipe | combines two filters: the output of the left feeds the right
jq '.orders[] | .amount' orders.json

# The comma , runs two filters on the same input and concatenates their outputs
jq '.orders[0] | .id, .amount' orders.json

# -c prints each result on one line (compact); good for a wc -l count
jq -c '.orders[]' orders.json

# -r drops the surrounding quotes on a string result; integers print as themselves
jq -r '.orders[].city' orders.json

# A mixed array holds numbers and objects; .[].city then raises an error on the number
printf '%s\\n' '[{"city":"Pune"},42,{"city":"Chennai"}]' > mixed.json
!jq '.[].city' mixed.json

# .foo? is the optional operator: a type error gives no output and the stream continues
jq '.[].city?' mixed.json
""")

Output

# A tiny JSON file of three orders with id, city, items, amount and status
$ printf '%s\n' '{"orders":[{"id":"o1","city":"Pune","items":2,"amount":450,"status":"paid"},{"id":"o2","city":"Chennai","items":1,"amount":220,"status":"pending"},{"id":"o3","city":"Pune","items":4,"amount":880,"status":"paid"}]}' > orders.json
$ jq --version
jq-1.8.2
# . is the identity filter: it reads the input and pretty-prints it
$ jq . orders.json
{
  "orders": [
    {
      "id": "o1",
      "city": "Pune",
      "items": 2,
      "amount": 450,
      "status": "paid"
    },
    {
      "id": "o2",
      "city": "Chennai",
      "items": 1,
      "amount": 220,
      "status": "pending"
    },
    {
      "id": "o3",
      "city": "Pune",
      "items": 4,
      "amount": 880,
      "status": "paid"
    }
  ]
}
# .orders is an object index; it picks one field of the object
$ jq '.orders' orders.json
[
  {
    "id": "o1",
    "city": "Pune",
    "items": 2,
    "amount": 450,
    "status": "paid"
  },
  {
    "id": "o2",
    "city": "Chennai",
    "items": 1,
    "amount": 220,
    "status": "pending"
  },
  {
    "id": "o3",
    "city": "Pune",
    "items": 4,
    "amount": 880,
    "status": "paid"
  }
]
# .[0] indexes an array; combine with .orders to pick the first order
$ jq '.orders[0]' orders.json
{
  "id": "o1",
  "city": "Pune",
  "items": 2,
  "amount": 450,
  "status": "paid"
}
# .[] iterates the array and emits each element as its own value
$ jq '.orders[].id' orders.json
"o1"
"o2"
"o3"
# The pipe | combines two filters: the output of the left feeds the right
$ jq '.orders[] | .amount' orders.json
450
220
880
# The comma , runs two filters on the same input and concatenates their outputs
$ jq '.orders[0] | .id, .amount' orders.json
"o1"
450
# -c prints each result on one line (compact); good for a wc -l count
$ jq -c '.orders[]' orders.json
{"id":"o1","city":"Pune","items":2,"amount":450,"status":"paid"}
{"id":"o2","city":"Chennai","items":1,"amount":220,"status":"pending"}
{"id":"o3","city":"Pune","items":4,"amount":880,"status":"paid"}
# -r drops the surrounding quotes on a string result; integers print as themselves
$ jq -r '.orders[].city' orders.json
Pune
Chennai
Pune
# A mixed array holds numbers and objects; .[].city then raises an error on the number
$ printf '%s\n' '[{"city":"Pune"},42,{"city":"Chennai"}]' > mixed.json
$ jq '.[].city' mixed.json
jq: error (at mixed.json:1): Cannot index number with string ("city")
"Pune"
[exit status 5]
# .foo? is the optional operator: a type error gives no output and the stream continues
$ jq '.[].city?' mixed.json
"Pune"
"Chennai"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 basics_session.py

The session’s jq . orders.json pretty-printed the whole object; jq '.orders[0]' orders.json picked the first order out of the array; jq '.orders[].id' orders.json iterated the array and printed three ids, "o1", "o2" and "o3". The pipe | and the comma , are the two glue operators: .orders[] | .amount printed 450, 220 and 880 by feeding each order into .amount, while .orders[0] | .id, .amount ran the same input through both filters and printed "o1" and 450 in order. Each operator is a generator: .[] on an array of four values emits four values, and the next stage runs once per value.

Interactive · stage by stage A jq filter is a pipeline of generators Watch one input flow through .orders[] into select then into .amount, with each stage producing its own stream.
A jq filter is a pipeline of generators, step 5 of 5. Showing orders.json, .orders[] (four orders in stream), select(.status) (three paid orders left), .amount (450, 880, 540), output stream (450, 880, 540). In focus: output stream.orders.jsonone JSON object.orders[]four orders in streamselect(.status)three paid orders left.amount450, 880, 540output stream450, 880, 540

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. jq reads one value. jq parses orders.json into one JSON object with an orders array of four items (o1 Pune 450 paid, o2 Chennai 220 pending, o3 Pune 880 paid, o4 Kochi 540 paid). The identity . would print it as is; our program chains three filters after it.
  2. .orders[] fans out. The iterator .orders[] takes the array of four orders and emits each one as its own value, in order. The next filter runs once per emitted value, so the stage after this one sees four inputs, not one.
  3. select keeps the paid ones. select(.status == paid) runs on each order. When .status is paid it returns the order unchanged; when it is pending it returns nothing. For our file o1, o3 and o4 pass; o2 is dropped. select is the standard way to filter inside a stream.
  4. .amount reads one field. The last stage reads .amount from each paid order. o1 gives 450, o3 gives 880 and o4 gives 540. Because the pipe passes one value at a time, we never built an intermediate array; jq ran the whole program as a stream.
  5. The output stream is written. jq pretty-prints each result on its own line (-c keeps it to one line, -r drops the quotes on a string). Three numbers come out, in the order of the paid orders. The run read the file once and produced its answer without any intermediate file.

Four command-line flags change what jq reads and writes. -n runs the program once with null as input, so the filter builds JSON from the command line without reading anything (jq -n '{a:1}' prints {"a":1}). -s (slurp: jq's --slurp or -s flag, which reads every input JSON value into one array and runs the filter once on that array, instead of once per value.) reads every input value into one array first, so a filter sees it as a single value instead of a stream. -c prints each output on one line (good for wc -l); -r (raw output: jq's --raw-output or -r flag, which prints a string result without the enclosing double quotes, so the shell reads it as plain text.) drops the quotes on a string result, so jq -r '.orders[].city' printed Pune, Chennai, Pune (plain words, not JSON strings). The session’s run of jq --version confirmed jq 1.8.2 on the toolchain.

Key idea

Every stage of a jq filter is a generator: .[] fans out, | passes each value along, and , runs two stages on the same input and concatenates their outputs. -r turns the final strings into plain text for the shell.

The optional operator .foo? catches a type error and emits no output instead. In basics_session.py the mixed array [{"city":"Pune"},42,{"city":"Chennai"}] failed with jq '.[].city' (Cannot index number with string ("city"), exit 5) because the middle element was a number. The ? form jq '.[].city?' printed "Pune" and "Chennai" and skipped the error. Use it when a stream can hold mixed shapes.

One mistake the shell layer makes with jq programs

Wrong: jq .foo file.json with no quotes. The shell reads .foo without quotes and may still work for a plain identifier, but any *, (, | or [ in the filter is then expanded or interpreted by the shell. Right: always single-quote the whole jq program, as the manual says: jq '.foo' file.json. Double quotes break the inner "strings"; backticks run a subshell. One single-quoted string is the simple rule.

Checkpoint

Check: Filter pipeline: ., .[], | and ,

  1. Question 1 of 2 What does jq '.' orders.json do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Reads orders.json as JSON and pretty-prints every value it holds

    The basics session's first run of jq . orders.json pretty-printed the full object. The identity filter is the simplest valid program (jq manual, Identity).

  2. Question 2 of 2 On the orders file of three orders, what does jq '.orders[0] | .id, .amount' print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "o1" then 450, on two lines

    The basics session recorded "o1" and 450 as its output for exactly this command. The pipe binds tighter than the comma, so .orders[0] runs once and feeds both halves.

select, map and the toolkit of builtins

The filter pipeline: A chain of jq filters joined by |, so each value flows one at a time from the stage on its left to the stage on its right. gets short when a few builtins do the heavy lifting. select(cond) keeps a value when cond is true and drops it otherwise; map(f) applies f to every element of an array and returns a new array. The combination is the “filter a list and reshape it” everyday case, written as .orders | map(select(.status == "paid")). The manual’s “Builtin operators and functions” section lists every form; the lesson’s session uses the ones a daily script reaches for.

select, map, length, add, sort_by, group_by, unique_by, min_by and // Python · builtins_session.py
"""Builtins: select, map, add, length, group_by, sort_by, unique_by, min_by, to_entries and //.

The session shows:
1. select(cond) keeps a value in the stream only if cond is true; map(f) applies f to every array element.
2. [filter] wraps a stream of outputs back into an array; add then sums or concatenates the array.
3. length counts the elements of an array, the characters of a string or the keys of an object (jq manual).
4. group_by(.city) returns groups sorted by the key (jq manual), so a totals report is deterministic.
5. sort_by(.amount), unique_by(.city) and min_by/max_by take any expression as the sorting key.
6. The alternative operator a // b takes a when it is non-null and non-false, else b: a default without an if.
7. Object construction {id: .id, city: .city, total: .amount * .items} builds a new JSON value from fields.
"""
from session import run

run("""
# The orders file from the basics session, with one more order to make groups non-trivial
printf '%s\\n' '{"orders":[{"id":"o1","city":"Pune","items":2,"amount":450,"status":"paid"},{"id":"o2","city":"Chennai","items":1,"amount":220,"status":"pending"},{"id":"o3","city":"Pune","items":4,"amount":880,"status":"paid"},{"id":"o4","city":"Kochi","items":3,"amount":540,"status":"paid"}]}' > orders.json

# select(cond) keeps a value only when cond is true; .[] | select(...) is the filtering idiom
jq -c '.orders[] | select(.status == "paid")' orders.json

# map(f) applies f to every array element, then returns a new array
jq '.orders | map(.amount)' orders.json

# length counts the elements; works on arrays, strings and objects
jq '.orders | length' orders.json
jq '.orders[0] | length' orders.json
jq '.orders[0].city | length' orders.json

# add sums or concatenates the elements of an array; [filter] wraps a stream back into an array
jq '[.orders[].amount] | add' orders.json

# sort_by(.amount) sorts the array of orders by amount, ascending
jq -c '.orders | sort_by(.amount) | .[].id' orders.json

# group_by(.city) returns groups sorted by the key; map builds the totals report
jq -c '.orders | group_by(.city) | map({city: .[0].city, total: (map(.amount) | add)})' orders.json

# unique_by(.city) keeps one order per city, in sorted key order
jq -c '.orders | unique_by(.city) | .[].city' orders.json

# min_by and max_by take any expression as the key: here the smallest and largest amount
jq -c '[(.orders | min_by(.amount) | .id), (.orders | max_by(.amount) | .id)]' orders.json

# to_entries turns an object into a list of {key, value}; good when the keys are not fixed
printf '%s\\n' '{"Pune":1330,"Chennai":220,"Kochi":540}' | jq -c 'to_entries | map("\\(.key)=\\(.value)")'

# a // b falls back to b when a is null or false: a default without an if
jq -r '.orders[] | .discount // 0, .status // "unknown"' orders.json

# Object construction builds a new JSON value from a few fields of the input
jq -c '.orders[] | {id, city, total: (.amount * .items)}' orders.json
""")

Output

# The orders file from the basics session, with one more order to make groups non-trivial
$ printf '%s\n' '{"orders":[{"id":"o1","city":"Pune","items":2,"amount":450,"status":"paid"},{"id":"o2","city":"Chennai","items":1,"amount":220,"status":"pending"},{"id":"o3","city":"Pune","items":4,"amount":880,"status":"paid"},{"id":"o4","city":"Kochi","items":3,"amount":540,"status":"paid"}]}' > orders.json
# select(cond) keeps a value only when cond is true; .[] | select(...) is the filtering idiom
$ jq -c '.orders[] | select(.status == "paid")' orders.json
{"id":"o1","city":"Pune","items":2,"amount":450,"status":"paid"}
{"id":"o3","city":"Pune","items":4,"amount":880,"status":"paid"}
{"id":"o4","city":"Kochi","items":3,"amount":540,"status":"paid"}
# map(f) applies f to every array element, then returns a new array
$ jq '.orders | map(.amount)' orders.json
[
  450,
  220,
  880,
  540
]
# length counts the elements; works on arrays, strings and objects
$ jq '.orders | length' orders.json
4
$ jq '.orders[0] | length' orders.json
5
$ jq '.orders[0].city | length' orders.json
4
# add sums or concatenates the elements of an array; [filter] wraps a stream back into an array
$ jq '[.orders[].amount] | add' orders.json
2090
# sort_by(.amount) sorts the array of orders by amount, ascending
$ jq -c '.orders | sort_by(.amount) | .[].id' orders.json
"o2"
"o1"
"o4"
"o3"
# group_by(.city) returns groups sorted by the key; map builds the totals report
$ jq -c '.orders | group_by(.city) | map({city: .[0].city, total: (map(.amount) | add)})' orders.json
[{"city":"Chennai","total":220},{"city":"Kochi","total":540},{"city":"Pune","total":1330}]
# unique_by(.city) keeps one order per city, in sorted key order
$ jq -c '.orders | unique_by(.city) | .[].city' orders.json
"Chennai"
"Kochi"
"Pune"
# min_by and max_by take any expression as the key: here the smallest and largest amount
$ jq -c '[(.orders | min_by(.amount) | .id), (.orders | max_by(.amount) | .id)]' orders.json
["o2","o3"]
# to_entries turns an object into a list of {key, value}; good when the keys are not fixed
$ printf '%s\n' '{"Pune":1330,"Chennai":220,"Kochi":540}' | jq -c 'to_entries | map("\(.key)=\(.value)")'
["Pune=1330","Chennai=220","Kochi=540"]
# a // b falls back to b when a is null or false: a default without an if
$ jq -r '.orders[] | .discount // 0, .status // "unknown"' orders.json
0
paid
0
pending
0
paid
0
paid
# Object construction builds a new JSON value from a few fields of the input
$ jq -c '.orders[] | {id, city, total: (.amount * .items)}' orders.json
{"id":"o1","city":"Pune","total":900}
{"id":"o2","city":"Chennai","total":220}
{"id":"o3","city":"Pune","total":3520}
{"id":"o4","city":"Kochi","total":1620}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 builtins_session.py

The orders file holds four orders in three cities, with totals 450, 220, 880 and 540. The session’s jq -c '.orders[] | select(.status == "paid")' orders.json kept the three paid orders in a stream; jq '[.orders[].amount] | add' orders.json wrapped the stream of amounts back into an array and summed it to 2090. length is overloaded by input type: .orders | length is 4 (an array), .orders[0] | length is 5 (the keys of an object) and .orders[0].city | length is 4 (the characters of "Pune"), as the manual’s length entry describes.

sort_by(.amount) sorts the array ascending, so the ids came back as "o2", "o1", "o4", "o3". group_by(.city) returns groups ordered by the key (jq manual, group_by), which is why the recorded totals-by-city report starts with Chennai 220, then Kochi 540, then Pune 1330, deterministic across runs. unique_by(.city) is the one-order-per-city form, also in sorted key order. min_by(.amount) and max_by(.amount) on the lesson’s four orders returned "o2" and "o3".

The alternative operator // takes the left-hand value when it is neither null nor false, otherwise the right. .discount // 0 printed 0 for every order because no order has a discount, and .status // "unknown" printed each real status. The jq manual explains that a // b works on a stream too: it emits the non-null, non-false values of a and falls back to b only when there are none. Pairing // with an optional operator is how a filter stays robust against missing fields.

Two short forms show up repeatedly in reports. {id, city} is shorthand for {id: .id, city: .city} and is a cheap way to pick a few fields of an object; "\(.id): \(.amount)" is string interpolation: jq syntax: inside a string, \(expr) is replaced by the expression's value. @format "..." applies the format to every interpolation only.. The session’s final command built per-order totals with {id, city, total: (.amount * .items)}, which printed {"id":"o1","city":"Pune","total":900} for the first order.

One builtin you reach for next

to_entries turns an object into a list of {key, value}, which is the only way to map over an object whose keys are not fixed. to_entries | map("\(.key)=\(.value)") on {"Pune":1330,"Chennai":220,"Kochi":540} printed ["Pune=1330","Chennai=220","Kochi=540"]. with_entries(f) is a shorthand for to_entries | map(f) | from_entries when the result is an object again.

Checkpoint

Check: select, map, group_by

  1. Question 1 of 2 On the four-order file (o1 450 paid, o2 220 pending, o3 880 paid, o4 540 paid), what does jq '[.orders[] | select(.status == "paid") | .amount] | add' print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 1870

    The builtins session recorded add on the four amounts as 2090; the paid-only sum follows from 450 + 880 + 540, which is 1870.

  2. Question 2 of 2 The orders file holds orders from Pune, Chennai and Kochi, in that order. In what order does jq '.orders | group_by(.city) | .[].[0].city' print the three cities?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "Chennai", "Kochi", "Pune" (sorted alphabetically)

    The builtins session's group_by run on this file printed [{city: Chennai, total: 220}, {city: Kochi, total: 540}, {city: Pune, total: 1330}]. The sort is on the key.

Pass shell values with the arg option, not into the filter text

Pasting a shell variable into the filter text looks fine until a value contains a jq operator. The result is a two-stage parse: the shell expands the variable, and jq then parses whatever the shell produced. The right form is --arg name value, which the manual’s “Invoking jq” section documents as “a value that will be treated as a string” and bound to $name in the filter.

--arg, --argjson, --null-input and the paste-into-filter trap Python · arg_session.py
"""Pass shell values into jq with --arg and --argjson, not with string concatenation.

The session shows:
1. Pasting a shell variable into the filter text is a two-stage parse: the shell expands it, then jq
   parses the result. A value with a jq operator ("Pune\\" or \\"Chennai") reshapes the filter and
   the output is silently wrong, with no error for a script to catch.
2. --arg name value binds a shell string to a jq variable; its value is read exactly, so any shape of
   text stays a string ($city, $query).
3. --argjson name JSON parses the value as JSON, so a number comes through as a number and jq compares
   it as a number, not a string.
4. jq -n with --arg builds a JSON object from the command line, with no input file at all.
"""
from session import run

run("""
printf '%s\\n' '{"orders":[{"id":"o1","city":"Pune","amount":450,"status":"paid"},{"id":"o2","city":"Chennai","amount":220,"status":"pending"},{"id":"o3","city":"Pune","amount":880,"status":"paid"},{"id":"o4","city":"Kochi","amount":540,"status":"paid"}]}' > orders.json
city='Pune'

# Pasting the shell value into the filter text looks fine for a plain word
jq ".orders[] | select(.city == \\"$city\\")" orders.json

# The safe form: --arg city $city binds the shell string to $city inside jq, exactly as written
jq --arg city "$city" '.orders[] | select(.city == $city)' orders.json

# A value that holds a jq operator reshapes the pasted filter and the output is silently wrong
awkward='Pune" or "Chennai'
jq ".orders[] | select(.city == \\"$awkward\\")" orders.json | jq -s length

# The --arg form is unchanged by the awkward value, because jq reads it as a string, not as code
jq --arg city "$awkward" '.orders[] | select(.city == $city)' orders.json | jq -s length

# --argjson parses the value as JSON, so a number stays a number and the comparison is numeric
min=500
jq --argjson min "$min" '.orders[] | select(.amount >= $min) | .id' orders.json

# --arg with a number binds it as a STRING, so the comparison then fails: no match
jq --arg min "$min" '.orders[] | select(.amount >= $min) | .id' orders.json

# -n (--null-input) runs the filter once with null as input: build JSON from the command line only
jq -n --arg city "$city" --argjson amount 1330 '{report: {city: $city, total: $amount}}'
""")

Output

$ printf '%s\n' '{"orders":[{"id":"o1","city":"Pune","amount":450,"status":"paid"},{"id":"o2","city":"Chennai","amount":220,"status":"pending"},{"id":"o3","city":"Pune","amount":880,"status":"paid"},{"id":"o4","city":"Kochi","amount":540,"status":"paid"}]}' > orders.json
$ city='Pune'
# Pasting the shell value into the filter text looks fine for a plain word
$ jq ".orders[] | select(.city == \"$city\")" orders.json
{
  "id": "o1",
  "city": "Pune",
  "amount": 450,
  "status": "paid"
}
{
  "id": "o3",
  "city": "Pune",
  "amount": 880,
  "status": "paid"
}
# The safe form: --arg city $city binds the shell string to $city inside jq, exactly as written
$ jq --arg city "$city" '.orders[] | select(.city == $city)' orders.json
{
  "id": "o1",
  "city": "Pune",
  "amount": 450,
  "status": "paid"
}
{
  "id": "o3",
  "city": "Pune",
  "amount": 880,
  "status": "paid"
}
# A value that holds a jq operator reshapes the pasted filter and the output is silently wrong
$ awkward='Pune" or "Chennai'
$ jq ".orders[] | select(.city == \"$awkward\")" orders.json | jq -s length
4
# The --arg form is unchanged by the awkward value, because jq reads it as a string, not as code
$ jq --arg city "$awkward" '.orders[] | select(.city == $city)' orders.json | jq -s length
0
# --argjson parses the value as JSON, so a number stays a number and the comparison is numeric
$ min=500
$ jq --argjson min "$min" '.orders[] | select(.amount >= $min) | .id' orders.json
"o3"
"o4"
# --arg with a number binds it as a STRING, so the comparison then fails: no match
$ jq --arg min "$min" '.orders[] | select(.amount >= $min) | .id' orders.json
# -n (--null-input) runs the filter once with null as input: build JSON from the command line only
$ jq -n --arg city "$city" --argjson amount 1330 '{report: {city: $city, total: $amount}}'
{
  "report": {
    "city": "Pune",
    "total": 1330
  }
}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 arg_session.py

The session set city='Pune' and ran two filters that look alike: jq ".orders[] | select(.city == \"$city\")" orders.json and jq --arg city "$city" '.orders[] | select(.city == $city)' orders.json. Both returned the two Pune orders. Then it set awkward='Pune" or "Chennai' and ran the same two forms. The pasted form expanded to the filter .orders[] | select(.city == "Pune" or "Chennai"), which jq read as (.city == "Pune") or "Chennai"; because the string "Chennai" is neither false nor null, or is true for every row. jq -s length on the result printed 4: every order came back. The --arg form on the same awkward value printed 0: no city equals the awkward string, so the stream was empty. One form reshaped the program, the other kept it as written.

Key idea

--arg and --argjson are the only way a shell value should reach a jq filter. The paste-into-filter form fails silently when a value happens to contain a jq operator, which is what makes it dangerous: a wrong report is harder to find than a crash.

The two options differ in parsing. --arg min 500 binds $min to the string "500", so select(.amount >= $min) on the orders file matched nothing (jq’s sort order places numbers below strings, so 450 >= "500" is false; the session’s run printed nothing). --argjson min 500 binds $min to the number 500, so the same filter printed "o3" and "o4", the orders at or above 500. Use --arg for a string and --argjson for a value that must stay a number, a boolean or a parsed object. jq -n --arg city "$city" --argjson amount 1330 '{report: {city: $city, total: $amount}}' built the report {"report":{"city":"Pune","total":1330}} from the command line without reading any file.

The lesson of quoting-single-double-and-ansi-c covers the shell side of this: single-quote the jq program itself so the shell leaves $name, |, * and ( alone, and quote "$value" on --arg city "$value" so a space in the value does not split the word.

A pattern that looks like --arg but behaves like a paste

Wrong: jq ".orders[] | select(.city == \"$city\")" orders.json. The filter is one double-quoted string; the shell expands $city into the middle of it, so jq sees whatever text the shell wrote. Right: jq --arg city "$city" '.orders[] | select(.city == $city)' orders.json. The program is single-quoted (the shell changes nothing) and --arg city passes the value in as a string jq can read.

Checkpoint

Check: --arg and --argjson

  1. Question 1 of 2 A shell variable city holds the value Pune. Which jq command passes it in safely?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: jq --arg city "$city" '.orders[] | select(.city == $city)' orders.json

    The arg session's run of the --arg form on 'Pune' printed the two Pune orders; the pasted form broke silently when the value contained a jq operator.

  2. Question 2 of 2 A shell variable min holds the value 500. Why does jq --arg min "$min" '.orders[] | select(.amount >= $min)' orders.json print nothing, while --argjson prints the two orders of 540 and 880?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: --arg binds a string, so .amount >= $min compares a number to a string; jq's order puts numbers below strings and the comparison is false.

    The arg session recorded both runs on the same file; --arg min 500 printed nothing and --argjson min 500 printed "o3" and "o4". The difference is the type of $min inside jq.

Format strings and raw output for the shell

A jq filter that ends with a string can go straight into a bash loop, as long as its output follows a format the loop expects. -r (raw output) is the first flag: it drops the enclosing quotes on a string result. The @format family then shapes a value for a target syntax: @csv, @tsv, @sh, @uri, @html, @base64. The manual’s “Format strings and escaping” section lists the full set; the lesson’s session uses the four that come up in shell integration.

@csv, @tsv, @sh, @uri, string interpolation and the while IFS loop Python · output_session.py
"""Shell-friendly output: @csv, @tsv, @sh, @uri, raw strings and the while IFS read loop.

The session shows:
1. @csv takes an array and prints a CSV row that quotes strings and escapes a double quote by doubling it.
2. @tsv prints a tab-separated row; it escapes a tab, newline, carriage return and backslash with \\t \\n \\r \\\\.
3. @sh escapes a value for a POSIX shell: an apostrophe becomes '\\'' and the whole value is single-quoted.
4. String interpolation "\\(exp)" reads an expression inside a string; @format "..." applies the format to
   every interpolation only (jq manual, Format strings and escaping).
5. jq -r | while IFS=$'\\t' read -r is the standard way to iterate a jq result in a bash loop.
"""
from session import run

run("""
printf '%s\\n' '{"orders":[{"id":"o1","city":"Pune","amount":450,"status":"paid","note":"needs invoice"},{"id":"o2","city":"Chennai","amount":220,"status":"pending","note":"gift wrap"},{"id":"o3","city":"Pune","amount":880,"status":"paid","note":"split \\"pay\\""}]}' > orders.json

# @csv needs an array as input; strings get double quotes and an inner quote is doubled
jq -r '.orders[] | [.id, .city, .amount, .note] | @csv' orders.json

# @tsv is tab-separated; a tab, newline, CR or backslash in a value is written as a two-letter escape
jq -r '.orders[] | [.id, .city, .amount] | @tsv' orders.json

# @sh escapes each element for a POSIX shell: an apostrophe becomes '\\'' so the shell sees it as text
jq -r '[.orders[0].id, "O\\u0027Hara\\u0027s Ale"] | @sh' orders.json

# @uri "..." applies percent-encoding to every interpolation; the fixed text stays as written
jq -rn '@uri "https://example.test/search?q=\\("needs invoice")&city=\\("Pune")"'

# -r (raw-output) is a string without the outer quotes; the shell reads it with IFS set to a tab
jq -r '.orders[] | [.id, .city, .amount] | @tsv' orders.json | while IFS=$'\\t' read -r id city amount; do printf 'order=%s city=%s amount=%s\\n' "$id" "$city" "$amount"; done

# --raw-output0 (jq 1.7+; checked here on jq 1.8.2) prints a NUL after each value
jq --raw-output0 '.orders[].note' orders.json | tr '\\0' '|'
""")

Output

$ printf '%s\n' '{"orders":[{"id":"o1","city":"Pune","amount":450,"status":"paid","note":"needs invoice"},{"id":"o2","city":"Chennai","amount":220,"status":"pending","note":"gift wrap"},{"id":"o3","city":"Pune","amount":880,"status":"paid","note":"split \"pay\""}]}' > orders.json
# @csv needs an array as input; strings get double quotes and an inner quote is doubled
$ jq -r '.orders[] | [.id, .city, .amount, .note] | @csv' orders.json
"o1","Pune",450,"needs invoice"
"o2","Chennai",220,"gift wrap"
"o3","Pune",880,"split ""pay"""
# @tsv is tab-separated; a tab, newline, CR or backslash in a value is written as a two-letter escape
$ jq -r '.orders[] | [.id, .city, .amount] | @tsv' orders.json
o1	Pune	450
o2	Chennai	220
o3	Pune	880
# @sh escapes each element for a POSIX shell: an apostrophe becomes '\'' so the shell sees it as text
$ jq -r '[.orders[0].id, "O\u0027Hara\u0027s Ale"] | @sh' orders.json
'o1' 'O'\''Hara'\''s Ale'
# @uri "..." applies percent-encoding to every interpolation; the fixed text stays as written
$ jq -rn '@uri "https://example.test/search?q=\("needs invoice")&city=\("Pune")"'
https://example.test/search?q=needs%20invoice&city=Pune
# -r (raw-output) is a string without the outer quotes; the shell reads it with IFS set to a tab
$ jq -r '.orders[] | [.id, .city, .amount] | @tsv' orders.json | while IFS=$'\t' read -r id city amount; do printf 'order=%s city=%s amount=%s\n' "$id" "$city" "$amount"; done
order=o1 city=Pune amount=450
order=o2 city=Chennai amount=220
order=o3 city=Pune amount=880
# --raw-output0 (jq 1.7+; checked here on jq 1.8.2) prints a NUL after each value
$ jq --raw-output0 '.orders[].note' orders.json | tr '\0' '|'
needs invoice|gift wrap|split "pay"|

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 output_session.py

The session’s jq -r '.orders[] | [.id, .city, .amount, .note] | @csv' orders.json wrote one CSV row per order; the note on o3 contained a literal "pay", which @csv quoted by doubling the inner double quote ("split ""pay"""), as the RFC 4180 convention and jq manual’s @csv entry require. @tsv produced the same three fields separated by tabs. @sh on ["o1", "O'Hara's Ale"] wrote 'o1' 'O'\''Hara'\''s Ale': every element surrounded by single quotes, with an inner apostrophe written as the five characters '\'', which is what a POSIX shell reads back as one word with an apostrophe inside.

Interactive · stage by stage How jq picks its exit status Follow one run from input through compile and filter to the exit code jq returns to the shell.
How jq picks its exit status, step 5 of 5. Showing Read input, Compile program (exit 3 on a syntax error), Run the filter (writes every output), Last output check (0 value, 1 false, 4 none), Return exit code. In focus: Return exit code.exits 5exits 3Read inputparse JSON valuesCompile programexit 3 on a syntax errorRun the filterwrites every outputLast output check0 value, 1 false, 4 noneReturn exit codeto the shell in $?

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Read standard input or the file. jq reads JSON values. Invalid JSON ends the run with exit 5 at once; the default halt_error exit code is 5 (jq manual, halt_error). printf not-json | jq . prints a parse error and $? reads 5.
  2. Compile the filter. jq parses the program. A syntax error ends the run with exit 3 before any input is applied: jq 'this is not jq' orders.json prints "1 compile error" and $? reads 3. This is a different failure from any run-time error.
  3. Run the filter. jq applies the filter and writes each output. Without -e, this stage sets exit 0 whether the output is a value, null, false or nothing at all. The output is complete when the input is; no retries, no partial run.
  4. The -e flag reads the last value. With --exit-status (-e), jq remembers the last output. If it was neither false nor null, exit 0. If it was false or null, exit 1. If the filter produced no output at all, exit 4. Without -e the stage is a no-op and exit is 0.
  5. Return the exit code. jq exits with the code the check decided. The shell reads it from $?, and a script uses it in an if: if jq -e .amount response.json >/dev/null; then ...; fi tells "field is set and truthy" apart from "missing or false".

The @format "..." template form applies the format to every \(expr) interpolation only: @uri "https://example.test/search?q=\("needs invoice")&city=\("Pune")" wrote https://example.test/search?q=needs%20invoice&city=Pune, so the fixed https:// and the & stayed as they were, while the two interpolated values were percent-encoded. For a CSV template the input to each \(…) must still be a value @csv can quote; the simple form [.a, .b] | @csv is the one that always works.

A bash loop reads jq’s output with IFS set to the field separator. jq -r '.orders[] | [.id, .city, .amount] | @tsv' orders.json | while IFS=$'\t' read -r id city amount; do printf 'order=%s city=%s amount=%s\n' "$id" "$city" "$amount"; done printed order=o1 city=Pune amount=450 and two more lines, one per order. The same pattern works for CSV when values do not contain commas, which is why serious CSV work goes through @tsv first and uses the tab. The reading-files-line-by-line lesson covers the loop in detail. jq --raw-output0 (jq 1.7 and later, checked here on 1.8.2) writes a NUL byte between values instead of a newline, so a value that contains a newline can still be read by while IFS= read -r -d '' value.

Checkpoint

Check: Raw text and @format strings

  1. Question 1 of 2 On an array ["o3","Pune",880,"split \"pay\""], what does jq -r '@csv' print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "o3","Pune",880,"split ""pay"""

    The output session recorded this exact line as the third CSV row from the orders file: the inner quote in 'split "pay"' is doubled to form the correct CSV field.

  2. Question 2 of 2 A pipeline reads a jq -r '@tsv' stream in a bash while loop. Which IFS value should the loop set?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: IFS=$'\t' (a single tab)

    The output session's loop read id, city and amount with IFS=$'\\t' read -r and printed one line per order; a space-splitting IFS would break city names that hold a space.

Exit status in scripts, and when to use the e flag

jq’s exit status is where a script decides whether the data was there. The manual’s --exit-status entry gives the four values a script watches for: 0 when the last output was neither false nor null; 1 when the last output was false or null; 4 when the filter produced no output at all; and the generic ones a shell knows, 2 for a usage or system error and 3 for a program (compile) error. Invalid JSON input returns 5 by default, which is halt_error’s default exit status.

jq -e: 0 for a value, 1 for null, 4 for nothing, 5 for bad JSON Python · exit_status_session.py
"""jq's exit status: 0, 1, 4 and 5, and how a script reads them.

The session shows:
1. Without -e, jq exits 0 when the program compiles and runs, even if the last output is null or false.
2. With -e (--exit-status), jq exits 1 when the last output is false or null, and 4 when the filter
   produced no output. 0 means "the last value was neither false nor null" (jq manual, --exit-status).
3. Invalid JSON input raises a parse error; the default halt_error exit status is 5.
4. A missing path through .[] on a non-array raises a type error with the same exit 5.
5. jq -e inside an if is the idiom for "fail the script when the field is missing".
"""
from session import run

run("""
printf '%s\\n' '{"orders":[{"id":"o1","city":"Pune","amount":450,"status":"paid"},{"id":"o2","city":"Chennai","amount":220,"status":"pending"}]}' > orders.json
jq --version

# Without -e, a filter that produces null still exits 0: the run itself was fine
jq '.orders[2]' orders.json
echo "exit=$?"

# With -e, the same filter now exits 1 (last output was null)
!jq -e '.orders[2]' orders.json
echo "exit=$?"

# With -e, a filter that produces nothing at all exits 4 (no valid result was ever produced)
!jq -e '.orders[] | select(.status == "cancelled")' orders.json
echo "exit=$?"

# With -e, a filter whose last output is a non-null, non-false value exits 0
jq -e '.orders[0].id' orders.json
echo "exit=$?"

# Invalid JSON on standard input exits 5 (parse error)
!printf 'not json\\n' | jq .
echo "exit=$?"

# A compile error in the filter itself exits 3
!jq 'this is not jq' orders.json
echo "exit=$?"

# jq -e in an if is the usual "did the field show up?" test
if jq -e '.orders[0].city' orders.json >/dev/null; then echo "city is set"; else echo "city is missing"; fi
if jq -e '.orders[0].discount' orders.json >/dev/null; then echo "discount is set"; else echo "discount is missing"; fi
""")

Output

$ printf '%s\n' '{"orders":[{"id":"o1","city":"Pune","amount":450,"status":"paid"},{"id":"o2","city":"Chennai","amount":220,"status":"pending"}]}' > orders.json
$ jq --version
jq-1.8.2
# Without -e, a filter that produces null still exits 0: the run itself was fine
$ jq '.orders[2]' orders.json
null
$ echo "exit=$?"
exit=0
# With -e, the same filter now exits 1 (last output was null)
$ jq -e '.orders[2]' orders.json
null
[exit status 1]
$ echo "exit=$?"
exit=1
# With -e, a filter that produces nothing at all exits 4 (no valid result was ever produced)
$ jq -e '.orders[] | select(.status == "cancelled")' orders.json
[exit status 4]
$ echo "exit=$?"
exit=4
# With -e, a filter whose last output is a non-null, non-false value exits 0
$ jq -e '.orders[0].id' orders.json
"o1"
$ echo "exit=$?"
exit=0
# Invalid JSON on standard input exits 5 (parse error)
$ printf 'not json\n' | jq .
jq: parse error: Invalid numeric literal at line 1, column 4
[exit status 5]
$ echo "exit=$?"
exit=5
# A compile error in the filter itself exits 3
$ jq 'this is not jq' orders.json
jq: error: syntax error, unexpected IDENT, expecting end of file at <top-level>, line 1, column 6:
    this is not jq
         ^^
jq: 1 compile error
[exit status 3]
$ echo "exit=$?"
exit=3
# jq -e in an if is the usual "did the field show up?" test
$ if jq -e '.orders[0].city' orders.json >/dev/null; then echo "city is set"; else echo "city is missing"; fi
city is set
$ if jq -e '.orders[0].discount' orders.json >/dev/null; then echo "discount is set"; else echo "discount is missing"; fi
discount is missing

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exit_status_session.py

The session’s four jq runs showed the whole picture. jq '.orders[2]' orders.json printed null and exited 0: a plain run always exits 0 when it ran, regardless of what came out. jq -e '.orders[2]' orders.json on the same filter printed null and exited 1. jq -e '.orders[] | select(.status == "cancelled")' orders.json printed nothing and exited 4 (no value was ever produced). jq -e '.orders[0].id' orders.json printed "o1" and exited 0 (a real value). jq 'this is not jq' exited 3 with a syntax error, and printf 'not json\n' | jq . exited 5 with a parse error. Each exit number names a different failure, so a script that catches one does not catch another.

Key idea

Without -e, jq’s exit status says only “the run finished”; with -e, it says “the last output was a real value”, “it was null or false”, or “nothing came out”. A script that cares which of the three it saw adds -e; a script that uses jq only to transform data does not.

The -e flag is tailor-made for an if test. if jq -e '.orders[0].city' orders.json >/dev/null; then echo "city is set"; else echo "city is missing"; fi printed city is set; the same block on .orders[0].discount printed city is missing. The pipe discards jq’s output and keeps only its status, so the if reads just the yes/no. For a pipeline that ends with jq -e, pipelines-pipestatus-and-pipefail is the companion lesson: set -o pipefail makes the whole pipeline return the first non-zero status, so curl ... | jq -e .data.id reports a curl failure as well as a jq one, and ${PIPESTATUS[@]} names each stage.

jq -e cannot tell `false` from `null`

Wrong: writing a script that treats jq -e '.enabled' as “the field was missing” when it returned exit 1. jq returns exit 1 for both an output of null and an output of literal false, so a legitimate {"enabled": false} ends the script just as a missing field does. Right: pair -e with // for a default (.enabled // false) or test the value separately with jq '.enabled == null'.

Checkpoint

Check: Exit status in scripts

  1. One question A script runs jq -e '.orders[] | select(.status == "cancelled")' orders.json on the four-order file (none are cancelled). What exit status does jq return?

    Choose one answer.

    Show the answer: One question

    Answer: 4 (no valid result was ever produced)

    The exit-status session recorded this exact run: jq printed nothing and $? read 4. "No output" and "the last output was null" are distinct cases under -e.

Practise: pick the right jq for each ask

Try these at your own terminal, using an orders file like the one above. Each one is one or two jq commands.

  1. From orders.json, print the ids of orders whose amount is strictly less than 500. Hint: select with a numeric comparison; -r to drop the quotes.
  2. Print a totals-by-city report sorted by total descending. Hint: group_by(.city) | map(...) for the per-city object, then sort_by(-.total) to reverse.
  3. Build a CSV with a header line and three data lines, each with id, city and amount. Hint: ["id","city","amount"], (.orders[] | [.id, .city, .amount]) | @csv.
  4. From a shell variable limit=500, print the paid orders whose amount is at or above it, as a safe filter. Hint: --argjson limit "$limit" and select(.status == "paid" and .amount >= $limit).
  5. In a script, exit 4 when .orders holds no paid order; exit 0 otherwise. Hint: jq -e 'first(.orders[] | select(.status == "paid"))' orders.json >/dev/null.

The questions below use jq programs you have not seen. Every answer rests on a filter or flag from the sections above; settle on yours before you read the explanation:

Practice that adapts to you

Practise this lesson

9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 What does the identity filter . produce from its input?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: The input, unchanged (jq then pretty-prints it)

    The basics session's jq . orders.json printed the whole JSON object formatted on standard output. The identity is the simplest valid program.

  2. Question 2 of 9 How many outputs does jq '.orders[]' produce from an input whose .orders field holds three objects?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Three (one per element of the array)

    The basics session's jq '.orders[].id' orders.json printed "o1", "o2", "o3" on three lines, one per order. .[] is a generator of values.

  3. Question 3 of 9 On the string value "Pune", what does jq -r print?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Pune (no surrounding double quotes)

    The basics session's jq -r '.orders[].city' orders.json printed Pune, Chennai, Pune in plain words, with no quotes around them.

  4. Question 4 of 9 What does select(f) do on an input value?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: Emits the input unchanged when f is true, and nothing when f is false

    The builtins session's .orders[] | select(.status == "paid") kept three of the four orders; each one came out as it went in, without any change.

  5. Question 5 of 9 What does add return on the input [1, 2, 3]?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: 6

    The jq manual lists [1,2,3] | add = 6 as its first example. In the builtins session, [.orders[].amount] | add on the four amounts returned 2090 for the same reason.

  6. Question 6 of 9 What does --arg foo 123 bind $foo to inside the jq filter?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: The string "123"

    The jq manual's --arg entry: "value will be treated as a string, so --arg foo 123 will bind $foo to \"123\"." Use --argjson to pass a parsed number instead.

  7. Question 7 of 9 What does jq -r '@sh' print on the input of the string O-apostrophe-Hara?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: 'O'\''Hara' (the apostrophe is written as '\'' so the shell reads it as one word)

    The output session's @sh run on the string O-apostrophe-Hara-apostrophe-s-Ale produced 'O'\''Hara'\''s Ale', the standard POSIX shell quoting for a value with an apostrophe.

  8. Question 8 of 9 Without -e, what is jq's exit status after a filter whose last output was null?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: 0 (the run finished)

    The exit-status session recorded jq '.orders[2]' orders.json printing null and $? reading 0; the same filter under jq -e read $? = 1.

  9. Question 9 of 9 A script runs printf 'not json\n' | jq '.'. What is jq's exit status?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: 5 (parse error on standard input)

    The exit-status session recorded printf 'not json' | jq . exiting 5 with the message 'parse error: Invalid numeric literal'. 5 is the default halt_error status.

Interview questions

Warm-up: what does .[] do versus ., and when do you reach for each (fresher to 2–5 years)?

. is the identity filter: it emits its input unchanged, so jq '.' orders.json pretty-prints the whole object and nothing else. .[] is the array or object iterator: it emits each element of an array (or each value of an object) as its own value, so the next stage of a filter sees one element at a time instead of the whole collection. Reach for . when you only want jq to validate and pretty-print, or at the start of a filter where the input flows in as is. Reach for .[] whenever the next stage is “do something per item”: .orders[] | select(...), .orders[] | .id, or [.orders[] | .amount] | add to sum. The two forms combine: .orders[] is read as .orders | .[], and in the lesson’s session that pattern iterated the four orders in order. The manual’s “Array/Object Value Iterator” entry gives both forms side by side.

Before opening the answers, write down your own first move for each of the three scenarios the questions describe:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A nightly job runs jq ".orders[] | select(.city == \"$CITY\")" orders.json with $CITY read from a config. The usage report counted all 4,218 orders in the file; a hand-check says only 1,100 are in that city. Explain what the filter actually ran, and give the one-line fix that keeps the pipeline correct for any value of CITY.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A daily totals report runs jq -r '.orders | group_by(.city) | map("\(.[0].city) \((map(.amount) | add))") | .[]' expecting three lines, one per city. When Chennai has no orders the report drops to two; when a Mumbai order appears it has four. Rework so Chennai prints a zero and Mumbai still prints.

    Model answer, Pro. First step and follow-ups

  3. Fresher to mid

    An API-check runs if ! jq -e '.enabled' response.json; then echo "feature off"; exit 1; fi. On {"enabled": false} it prints feature off (correct). On a payload with .enabled missing it does the same, but the product owner needs "missing" (a rollout bug) kept apart from "set to false". Explain what jq -e saw and give a one-line fix.

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How do . and .[] differ in jq?

    . emits its input unchanged, so jq '.' pretty-prints a file. .[] emits each element of an array, or each value of an object, as its own value, so the next stage sees one at a time.

  2. What do | and , do between jq filters?

    | passes each value from the left filter into the right one; , runs two filters on the same input and joins their outputs.

  3. What do jq's -r, -c, -s and -n flags change?

    -r prints strings without their quotes, for the shell; -c prints each result on one line; -s reads every input value into one array; and -n runs the program once with null as input, so jq -n '{a:1}' builds JSON without reading anything.

  4. What do select, map and group_by do?

    select keeps the values for which a test is true, map transforms every element of an array, and group_by, like sort_by and unique_by, returns results sorted by the key.

  5. How should a shell value reach a jq filter?

    Through --arg name value, which gives a string, or --argjson name JSON, which gives a parsed value. Pasting the variable into the filter text breaks silently when the value contains jq syntax.

  6. What does jq -e change about the exit status?

    The status then describes the last output: 0 for a value that is neither null nor false, 1 for null or false, and 4 when nothing came out, which makes if jq -e '.field' file a presence test.

  7. Which jq format strings make output safe for the shell?

    @csv, @tsv, @sh and @uri format values as CSV, TSV, shell-quoted words and URL parts; with -r they come out as plain lines a loop can read.

Key takeaways

  • A jq filter is a chain of generators built from ., .key, .[], the pipe | and the comma ,; the flags -r, -c, -s and -n change how input and output are shaped.
  • select filters a stream, map transforms an array, and group_by with sort_by and unique_by returns results in a deterministic order sorted by the key.
  • Shell values reach jq through --arg name value (a string) or --argjson name JSON (a parsed value); pasting a shell variable into the filter text is a parse-reshape trap that fails silently.
  • @csv, @tsv, @sh, @uri and jq -r make jq’s output shell-friendly; a bash loop reads a @tsv stream with while IFS=$'\t' read -r.
  • jq’s exit status is 0 for a run that finished (and with -e for a last value that was neither null nor false), 1 for a last null or false value, 4 for no output, 3 for a compile error and 5 for invalid JSON input.

Terms in this lesson

filter pipeline
A chain of jq filters joined by |, so each value flows one at a time from the stage on its left to the stage on its right.
jq filter
A jq program; a chain of operators that reads JSON on standard input and writes JSON on standard output, one value at a time.
raw output
jq's --raw-output or -r flag, which prints a string result without the enclosing double quotes, so the shell reads it as plain text.
slurp
jq's --slurp or -s flag, which reads every input JSON value into one array and runs the filter once on that array, instead of once per value.
string interpolation
jq syntax: inside a string, \(expr) is replaced by the expression's value. @format "..." applies the format to every interpolation only.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.