Why this lesson
select and @tsv answer "which ones". Real questions go further: "how many per group?", "what are its labels?", "change this one number and save a copy", "fail my script if any entry is on a latest version". This lesson is the rest of jq you will actually use.
What you need to know already: JSON, .key, .[], select, -r and --arg (7.11, 7.12); sort f > f truncating the file (6.17); if on an exit status (6.5).
Looking at an unknown document
keys lists an object's keys (sorted); join(",") glues an array of strings with commas; has("k") is true if the key exists:
$ cd ~/labs/data
$ jq -r '.items[0] | keys | join(",")' pods.json
apiVersion,kind,metadata,spec,status
$ jq '.items | length' pods.json
12
$ jq '.items[0].metadata | has("labels")' pods.json
true
keys (sorted) and length are how you explore: what fields are there, how many elements. jq -c '.items[0]' prints one element compactly on a line.
Objects as lists: to_entries
$ jq -c '.items[0].metadata.labels | to_entries' pods.json
[{"key":"app","value":"web"}]
$ jq -c '.items[0].metadata.labels | to_entries | from_entries' pods.json
{"app":"web"}
$ jq -r '.items[0].metadata.labels | to_entries[] | "\(.key)=\(.value)"' pods.json
app=web
to_entries turns an object into [{key, value}] so you can iterate, filter or format it; from_entries turns it back; with_entries(f) does both around f (with_entries(select(.key | startswith("app"))) keeps some keys).
Labels are free-form key: value tags people attach to things (app: web, team: payments). You do not know their keys in advance - which is exactly when you need to_entries.
Counting and grouping
$ jq -c '[.items[] | {ns: .metadata.namespace}] | group_by(.ns) | map({ns: .[0].ns, n: length})' pods.json
[{"ns":"default","n":2},{"ns":"kube-system","n":3},{"ns":"monitoring","n":3},{"ns":"payments","n":4}]
Read it right to left from the end: group_by(.ns) sorts and splits the array into arrays of equal .ns; map({...}) turns each group into one object; .[0].ns is the group's key and length its size. It is sort | uniq -c for JSON.
jq '[.items[].status.containerStatuses[]?.restartCount] | add' sum: 14
jq '.items | map(.status.phase) | unique' distinct values
jq '.items | max_by(.status.containerStatuses[0].restartCount) | .metadata.name'
jq 'reduce .items[] as $p (0; . + ($p.spec.containers | length))' a fold
max_by(f) picks the element with the largest f. reduce ITEMS as $x (START; UPDATE) is a fold: start with a value, and update it once per item - here, add up how many packaged programs (the containers list, 7.11) each entry runs.
add sums numbers, concatenates strings and arrays, and merges objects. It returns null for an empty array, so add // 0 when that matters.
Passing values in
jq --arg ns payments '.items[] | select(.metadata.namespace == $ns) | .metadata.name'
jq --argjson n 2 '.items[] | select(.status.containerStatuses[0].restartCount > $n)'
--arg always passes a string. --arg n 2 then > $n compares a number with the string "2" - and in jq every number is less than every string, so it is always false. Numbers and booleans go in with --argjson.
Changing a document
$ jq '.items[0].metadata.labels.app |= ascii_upcase | .items[0].metadata.labels' pods.json
{
"app": "WEB"
}
A path here is the chain of keys and indexes to a value (.items[0].metadata.labels.app). |= updates the value at a path and returns the whole document. = assigns, += adds. ascii_upcase upper-cases a string. jq never edits a file: write to a new file and move it into place.
jq '.spec.replicas = 3' app.json > app.json.new && mv app.json.new app.json
jq ... > app.json on the same file truncates it before jq reads it - the same trap as sort f > f (6.17).
Several documents: -s and -n
$ echo '{"a":1}{"a":2}' | jq -s 'map(.a) | add'
3
-s (slurp) reads every input document into one array - how you aggregate over newline-delimited JSON: one JSON object per line, a common format for application logs. -n starts with null and no input; with --arg it builds JSON safely from shell values: jq -n --arg u "$USER" '{user: $u}'.
Matching strings
jq '.items[] | select(.metadata.name | test("^web")) | .metadata.name'
jq '.items[] | select(.metadata.name | startswith("refund"))'
jq -r '.items[].spec.containers[].image | split(":") | .[1]' the tag
test() takes a PCRE-style regex (7.3) (test("^web-\\d"), note the doubled backslash inside a jq string inside shell quotes). split(":") cuts a string into an array at each : - so .[1] is the version after the colon. join, ascii_downcase, ltrimstr / rtrimstr (remove a prefix / suffix) cover most other string work.
Exit status and errors
$ jq -e '.items[] | select(.metadata.name=="nope")' pods.json; echo $?
4
$ jq '.items.metadata' pods.json
jq: error (at pods.json:545): Cannot index array with string "metadata"
$ jq '.items[' pods.json
jq: error: syntax error, unexpected end of file at <top-level>, line 1:
.items[
jq: 1 compile error
-esets the exit status from the last output: 0 if it was truthy, 1 iffalse/null, 4 if there was no output at all. That makes jq usable inif:if jq -e '.ready' status.json >/dev/null; then ....Cannot index array with string "metadata"means you asked an array for a field - you forgot[]. The fix is.items[].metadata.Cannot index stringmeans you went one level too deep.- Exit 5 is a runtime error, 3 a compile error (the filter itself is invalid, as in the
.items[example), 2 an unreadable input.
What you can now do
- Explore an unknown document with
keys,lengthandhas. - Group and count with
group_by+map, and walk labels withto_entries. - Change a value with
|=into a new file, and usejq -ein anif.