jq — Aggregation
Slurp Mode (-s)
Collect line-delimited JSON into an array
printf '{"a":1}\n{"a":2}\n{"a":3}\n' | jq -s '.'
# Output:
# [
# {"a": 1},
# {"a": 2},
# {"a": 3}
# ]
Without -s, jq processes each line independently. With -s, it reads all input into a single array first. This is mandatory for any cross-object operation (sorting, grouping, reducing).
Slurp + sum
printf '{"bytes":1024}\n{"bytes":2048}\n{"bytes":512}\n' | jq -s 'map(.bytes) | add'
# Output: 3584
Slurp + count
printf '{"state":"UP"}\n{"state":"DOWN"}\n{"state":"UP"}\n' | jq -s 'length'
# Output: 3
sort_by
Sort objects by a field
echo '[{"name":"wlan0","mtu":1500},{"name":"lo","mtu":65536},{"name":"eth0","mtu":1500}]' | \
jq 'sort_by(.name)'
# Output:
# [
# {"name": "eth0", "mtu": 1500},
# {"name": "lo", "mtu": 65536},
# {"name": "wlan0", "mtu": 1500}
# ]
Sort descending — reverse after sort
echo '[{"name":"eth0","mtu":1500},{"name":"lo","mtu":65536},{"name":"docker0","mtu":1500}]' | \
jq 'sort_by(.mtu) | reverse'
# Output:
# [
# {"name": "lo", "mtu": 65536},
# {"name": "eth0", "mtu": 1500},
# {"name": "docker0", "mtu": 1500}
# ]
Sort by multiple keys
echo '[{"state":"UP","name":"wlan0"},{"state":"DOWN","name":"eth0"},{"state":"UP","name":"eth0"}]' | \
jq 'sort_by(.state, .name)'
# Output:
# [
# {"state": "DOWN", "name": "eth0"},
# {"state": "UP", "name": "eth0"},
# {"state": "UP", "name": "wlan0"}
# ]
group_by
Group objects by a field value
echo '[{"name":"eth0","state":"UP"},{"name":"lo","state":"UNKNOWN"},{"name":"wlan0","state":"UP"},{"name":"docker0","state":"DOWN"}]' | \
jq 'group_by(.state)'
# Output:
# [
# [{"name": "docker0", "state": "DOWN"}],
# [{"name": "lo", "state": "UNKNOWN"}],
# [{"name": "eth0", "state": "UP"}, {"name": "wlan0", "state": "UP"}]
# ]
group_by(.field) sorts by the field, then partitions into sub-arrays of equal values. This is jq’s GROUP BY.
Group + count — frequency table
echo '[{"name":"eth0","state":"UP"},{"name":"lo","state":"UNKNOWN"},{"name":"wlan0","state":"UP"},{"name":"docker0","state":"DOWN"}]' | \
jq 'group_by(.state) | map({state: .[0].state, count: length})'
# Output:
# [
# {"state": "DOWN", "count": 1},
# {"state": "UNKNOWN", "count": 1},
# {"state": "UP", "count": 2}
# ]
Group + aggregate — names per state
echo '[{"name":"eth0","state":"UP"},{"name":"lo","state":"UNKNOWN"},{"name":"wlan0","state":"UP"},{"name":"docker0","state":"DOWN"}]' | \
jq 'group_by(.state) | map({state: .[0].state, interfaces: [.[].name]})'
# Output:
# [
# {"state": "DOWN", "interfaces": ["docker0"]},
# {"state": "UNKNOWN", "interfaces": ["lo"]},
# {"state": "UP", "interfaces": ["eth0", "wlan0"]}
# ]
unique and unique_by
Remove duplicate values from an array
echo '["UP","DOWN","UP","UNKNOWN","UP","DOWN"]' | jq 'unique'
# Output: ["DOWN", "UNKNOWN", "UP"]
Unique by a field — deduplicate objects
echo '[{"state":"UP","name":"eth0"},{"state":"UP","name":"wlan0"},{"state":"DOWN","name":"docker0"}]' | \
jq 'unique_by(.state)'
# Output:
# [
# {"state": "DOWN", "name": "docker0"},
# {"state": "UP", "name": "eth0"}
# ]
unique_by keeps the first object per unique value. If you need all objects, use group_by instead.
min_by / max_by
Find the object with the largest value
echo '[{"name":"eth0","mtu":1500},{"name":"lo","mtu":65536},{"name":"docker0","mtu":1500}]' | \
jq 'max_by(.mtu)'
# Output: {"name": "lo", "mtu": 65536}
Find the object with the smallest value
echo '[{"name":"eth0","mtu":1500},{"name":"lo","mtu":65536},{"name":"docker0","mtu":1500}]' | \
jq 'min_by(.mtu)'
# Output: {"name": "eth0", "mtu": 1500}
Top N by value
echo '[{"name":"a","size":100},{"name":"b","size":500},{"name":"c","size":250},{"name":"d","size":50}]' | \
jq 'sort_by(.size) | reverse | .[:2]'
# Output:
# [
# {"name": "b", "size": 500},
# {"name": "c", "size": 250}
# ]
add
Sum an array of numbers
echo '[1500, 65536, 1500, 1500]' | jq 'add'
# Output: 70036
Sum a field across objects
echo '[{"name":"eth0","errors":5},{"name":"wlan0","errors":12},{"name":"lo","errors":0}]' | \
jq 'map(.errors) | add'
# Output: 17
Merge an array of objects
echo '[{"dns":"53"},{"http":"80"},{"ssh":"22"}]' | jq 'add'
# Output:
# {
# "dns": "53",
# "http": "80",
# "ssh": "22"
# }
add on an array of objects merges them. Combined with map({(key): value}), this builds objects dynamically.
Concatenate arrays
echo '[[1,2],[3,4],[5,6]]' | jq 'add'
# Output: [1, 2, 3, 4, 5, 6]
length
Count elements
echo '[{"a":1},{"a":2},{"a":3}]' | jq 'length'
# Output: 3
Count after filtering
echo '[{"name":"eth0","state":"UP"},{"name":"lo","state":"UNKNOWN"},{"name":"wlan0","state":"UP"}]' | \
jq '[.[] | select(.state == "UP")] | length'
# Output: 2
Count keys in an object
echo '{"name":"eth0","state":"UP","mtu":1500,"mac":"aa:bb:cc:dd:ee:ff"}' | jq 'length'
# Output: 4
limit and first/last
Take first N results
echo '[1,2,3,4,5,6,7,8,9,10]' | jq '.[:3]'
# Output: [1, 2, 3]
first and last
echo '[{"name":"a"},{"name":"b"},{"name":"c"}]' | jq 'first'
# Output: {"name": "a"}
echo '[{"name":"a"},{"name":"b"},{"name":"c"}]' | jq 'last'
# Output: {"name": "c"}
Limit with early exit — nth
echo '[10,20,30,40,50]' | jq 'nth(2)'
# Output: 30
Practical: Aggregation Pipeline
Complete aggregation example — process list summary
ps aux --no-headers | awk '{print "{\"user\":\""$1"\",\"cpu\":"$3",\"mem\":"$4",\"cmd\":\""$11"\"}"}' | \
jq -s 'group_by(.user) | map({
user: .[0].user,
processes: length,
total_cpu: (map(.cpu) | add),
total_mem: (map(.mem) | add)
}) | sort_by(.total_cpu) | reverse | .[:5]'
# Output: top 5 users by CPU, with process count and memory totals
This is the full pattern: shell command produces JSON lines, -s collects them, then group/aggregate/sort/slice.