DEV Community

Kengah Ireneaus
Kengah Ireneaus

Posted on

A Practical Guide to jq in Shell Scripts

A Practical Guide to jq in Shell Scripts

While building sykd (a system health monitor daemon for linux servers) i needed to collect system data as json so i could process easily in python on doing research if found out about the jq shell program for creating and parsing json in bash, but learning the commands was quite tedious and the man page is large and very difficult to read so i came out with this guide to ease and summarize the major commands you might need.

jq is a lightweight and flexible command-line JSON processor. It is one of the most useful tools for working with JSON data directly inside shell scripts and pipelines.

Installation

  • Debian / Ubuntu: sudo apt install jq
  • macOS: brew install jq
  • Other systems: Use your package manager or download binaries from the official site.

Basic Usage

jq [options] '<filter>' [file...]
Enter fullscreen mode Exit fullscreen mode

Input can come from a file, a pipe, or standard input. The filter expression selects or transforms the data.

Quick note: jq reads JSON from stdin by default. If you want to generate JSON inside jq without reading input, use -n.

Useful Options

Option Purpose
-r Raw output (no JSON quotes around strings)
-c Compact (single-line) output
-n Start with null input (useful for generating JSON)
-s Slurp entire input into an array
--arg Pass a string variable
--argjson Pass a JSON value (preferred for arrays/objects)

Other helpful runtime options:

Option Purpose
--rawfile name filename Read an entire file as a raw string into variable $name
--slurpfile name filename Read file and slurp into array $name (like -s but into variable)

Example: jq --slurpfile all files.json '.[0] + $all[0]'

Essential Filters

Goal Filter Example
Pretty-print .
Extract a field .name or .["name"]
Nested field .user.address.city
Array element .[0] or .[-1]
Iterate array .[]
Select matching objects `.[] \
Build a new object {% raw %}{name, age}
Transform every element map(.name)
Length length
List keys keys
Conditional if .active then "yes" else "no" end

You can define reusable functions inside jq using def. Example:

def greet: "Hello, " + .name + "!";
. | greet
Enter fullscreen mode Exit fullscreen mode

This will output "Hello, <name>!" for an object with a name key.

Practical Examples

Pretty-print a file

jq . data.json
Enter fullscreen mode Exit fullscreen mode

Extract a single value

jq -r '.version' package.json
Enter fullscreen mode Exit fullscreen mode

Filter an array of objects

jq '.[] | select(.status == "active") | .name' users.json
Enter fullscreen mode Exit fullscreen mode

Create a new structure

jq '{names: map(.name), total: length}' people.json
Enter fullscreen mode Exit fullscreen mode

Use in a pipeline

curl -s https://api.example.com/data | jq '.results[] | .id'
Enter fullscreen mode Exit fullscreen mode

Generate JSON from scratch

jq -n '{name: "example", version: 1}'
Enter fullscreen mode Exit fullscreen mode

Create multiple objects programmatically

jq -n '[range(3) | {index: ., squared: (. * .)}]'
Enter fullscreen mode Exit fullscreen mode

This produces:

[
{"index":0,"squared":0},
{"index":1,"squared":1},
{"index":2,"squared":4}
]

Passing a Bash Array to jq

Bash arrays cannot be passed directly. Convert the array to a JSON array first, then supply it with --argjson.

Recommended Method

my_array=("apple" "banana" "cherry")

# Convert bash array → JSON array
json_array=$(printf '%s\n' "${my_array[@]}" | jq -R . | jq -s .)

# Use it inside jq
jq --argjson arr "$json_array" '$arr'
Enter fullscreen mode Exit fullscreen mode

One-liner version:

jq --argjson arr "$(printf '%s\n' "${my_array[@]}" | jq -R . | jq -s .)" \
   '$arr[]'
Enter fullscreen mode Exit fullscreen mode

Numeric Arrays

numbers=(10 20 30)
json_numbers=$(printf '%s\n' "${numbers[@]}" | jq -s .)

jq --argjson nums "$json_numbers" '$nums | map(. * 2)'
Enter fullscreen mode Exit fullscreen mode

Feed as Input Instead

printf '%s\n' "${my_array[@]}" | jq -R . | jq -s .
Enter fullscreen mode Exit fullscreen mode

Tips for Reliable Shell Scripts

  • Always quote the filter when it contains spaces or special characters.
  • Prefer --argjson for arrays and objects; use --arg only for plain strings.
  • Test filters interactively before embedding them in scripts.
  • For complex logic, store the filter in a separate .jq file and call it with -f.

  • When embedding multi-line filters in shell variables, use a heredoc to keep quoting simple:

filter=$(cat <<'EOF'
.[] | select(.status=="active") | {id, name}
EOF
)

curl -s api | jq -f <(echo "$filter")
Enter fullscreen mode Exit fullscreen mode
  • Prefer --argjson when you need to pass non-string structured data; --arg always passes a string.

  • When values may contain newlines or special characters, convert them to JSON with jq -R . before passing.

  • For newline-delimited JSON (NDJSON/JSONL), use jq -c . to ensure compact output and --slurp if you need to combine records into an array.

  • Use try ... catch for safe extraction and defaults:

try .foo.bar catch "default"
Enter fullscreen mode Exit fullscreen mode
  • Use inputs to read multiple JSON documents from stdin (useful with -n):
# echo multiple JSON objects and process them with inputs
printf '%s
' '{"x":1}' '{"x":2}' | jq -n '[inputs | .x]'
Enter fullscreen mode Exit fullscreen mode

Additional Examples and Patterns

Selecting and Transforming

Extract and rename fields:

jq '.[] | {identifier: .id, user: .name, active: .status == "active"}' users.json
Enter fullscreen mode Exit fullscreen mode

Build an index (map id → object):

jq 'map({ (.id|tostring): . }) | add' users.json
Enter fullscreen mode Exit fullscreen mode

Group by a key and count per group:

jq 'group_by(.category) | map({category: .[0].category, count: length})' items.json
Enter fullscreen mode Exit fullscreen mode

Working with Strings

Trim and lowercase a field:

jq '.name | ascii_downcase | gsub("^ +| +$"; "")' person.json
Enter fullscreen mode Exit fullscreen mode

Join array of strings:

jq -r '.tags | join(",")' article.json
Enter fullscreen mode Exit fullscreen mode

Numeric operations

Sum field values:

jq '[.[] | .size] | add' files.json
Enter fullscreen mode Exit fullscreen mode

Find maximum:

jq 'max_by(.score) | .score' scores.json
Enter fullscreen mode Exit fullscreen mode

Advanced: reduce, recurse, and more

Use reduce to accumulate values:

jq 'reduce .[] as $item (0; . + $item.value)' metrics.json
Enter fullscreen mode Exit fullscreen mode

Recursively find all keys named id in nested structure:

jq '.. | objects | .id? // empty' nested.json
Enter fullscreen mode Exit fullscreen mode

Streaming large JSON (jq --stream)

For very large JSON documents use --stream to get path/value pairs; processing is more manual but uses much less memory:

jq --stream 'select(.[0][-1]=="targetKey") | .[1]' large.json
Enter fullscreen mode Exit fullscreen mode

Working with NDJSON / JSONL

To parse newline-delimited JSON and combine into an array:

jq -s '.' file.jsonl
Enter fullscreen mode Exit fullscreen mode

Process each line independently (streaming):

jq -c '. | {id: .id, ok: (.value > 0)}' file.jsonl
Enter fullscreen mode Exit fullscreen mode

Filters in separate .jq files

Save a complex filter to filters.jq:

def summarize:
   {total: (map(.value) | add), count: length};

.items | map({k: .key, v: .value}) | summarize
Enter fullscreen mode Exit fullscreen mode

Call it from shell:

jq -f filters.jq data.json
Enter fullscreen mode Exit fullscreen mode

Passing environment variables into jq

Pass a shell variable as JSON (string):

name="O'Reilly"
jq --arg name "$name" '. + {author: $name}' book.json
Enter fullscreen mode Exit fullscreen mode

Pass an environment variable that is already JSON:

json_obj='{"a":1}'
jq --argjson o "$json_obj" '. + $o' base.json
Enter fullscreen mode Exit fullscreen mode

Error handling and safe accesses

Use ? to avoid errors when a key may not exist:

jq '.maybe?.nested // "default"' data.json
Enter fullscreen mode Exit fullscreen mode

Use try to catch runtime errors:

jq 'try .items[] catch empty' data.json
Enter fullscreen mode Exit fullscreen mode

Common pitfalls

  • Forgetting to quote filters when they contain shell-sensitive characters — always wrap your filter in single quotes in shell.
  • Passing arrays with --arg (creates a JSON string, not an array) — use --argjson or jq -s conversion.
  • Assuming jq modifies files in-place — it writes to stdout; redirect output to a file to save changes.

Performance tips

  • Use --stream for very large JSON that can't fit in memory.
  • Avoid unnecessary map + add for simple reductions; reduce can be more efficient.
  • Use compiled filters in -f files for complex repeated transformations.

Examples: Real shell script snippets

Filter API result and extract IDs into a bash array:

readarray -t ids < <(curl -s "https://api.example.com/items" | jq -r '.items[].id')

for id in "${ids[@]}"; do
   echo "Got id: $id"
done
Enter fullscreen mode Exit fullscreen mode

Pass a bash array of words safely into jq and filter by membership:

words=(alpha beta "complex value")
json_words=$(printf '%s\n' "${words[@]}" | jq -R . | jq -s .)

curl -s dataset.json | jq --argjson words "$json_words" '[.[] | select(.name as $n | $words | index($n))]'
Enter fullscreen mode Exit fullscreen mode

Create a small portable CLI using jq and bash:

#!/usr/bin/env bash
data_file=${1:-data.json}
case $2 in
   list)
      jq -r '.items[] | "\(.id) - \(.name)"' "$data_file" ;;
   stats)
      jq '{total: (.items | length), sum: (.items | map(.value) | add)}' "$data_file" ;;
   *) echo "Usage: $0 <file> {list|stats}"; exit 2 ;;
esac
Enter fullscreen mode Exit fullscreen mode

Conclusion

With a few core filters and the correct technique for passing arrays, jq becomes a powerful and reliable tool inside any shell workflow. The patterns shown above cover the majority of day-to-day JSON processing needs.

Happy scripting!

Top comments (0)