jq: The Swiss Army Knife Your Shell Pipeline Was Missing

Every JSON API response is one pipe away from being actually readable. jq turns curl’s wall of braces into the two fields you wanted.

jq is a functional language for JSON that fits in a single binary — you feed it JSON on stdin, hand it a filter expression, and it streams back exactly the shape of data you asked for. That sounds modest until you realize how much of shell scripting is "fetch JSON, pull out a couple of values, maybe reshape it": API responses from curl, cloud CLI output, log files, package manifests, webhook payloads. Without jq you’re doing that with grep and a prayer; with it, one expression replaces twenty lines of sed. The mental shift is that jq is not a text tool — it treats input as structured values, so quoting, nesting, and type errors become explicit instead of silent. Master a handful of built-ins (.[], select(), map(), //) and you stop fearing any API that talks JSON.

install

macOS
# macOS
brew install jq
Debian/Ubuntu
# Debian/Ubuntu
sudo apt install jq
Fedora
# Fedora
sudo dnf install jq
Arch
# Arch
sudo pacman -S jq

examples

# Pretty-print an API response (the default is 2-space indented)
curl -s https://api.github.com/repos/stedolan/jq | jq
# Pull one field, raw — -r drops the quotes so you can actually use the value
curl -s https://api.github.com/repos/stedolan/jq | jq -r '.description'
# Iterate an array and filter it: .[] streams elements, select() keeps matches
jq '.[] | select(.state == "open") | .title' issues.json
# Rebuild objects: wrap fields you want in {} to emit a slimmed-down payload
jq '[.[] | {name, stars: .stargazers_count}]' repos.json
# // is the alternative operator — fallback when a field is null or missing
jq -r '.user.login // "unknown-user"' event.json
# -e sets the exit code from the output — null/false exit 1, so scripts can branch
jq -e '.result.ok' response.json || echo "request failed"
# --arg passes shell vars in safely (no string-interpolation injection nonsense)
jq --arg q "$query" '.[] | select(.name | contains($q))' data.json

pro tip

-S sorts object keys on output, which turns diffs between two similar JSON files from noise into signal — diff <(jq -S a.json) <(jq -S b.json) is a JSON diff with zero extra tooling. Also keep -r burned in: forgetting it is the #1 reason people think jq "adds quotes" to everything.

honest limits

jq is JSON-only. It will not parse YAML, XML, or your config file with comments (write yaml/jq forks exist, but they’re separate tools with their own quirks). Streaming huge files works but memory behavior on multi-GB inputs is not its calling — that’s what jq --stream or mlr are for. And don’t use jq to *generate* config files by hand-editing strings; that’s a job for a template. If your data is CSV, xsv/miller will make you happier. If you just need one nested value from a known path, grep -o is faster to type and you’ll survive.