Kapittelet Tekstbehandling er et verksted av linjeverktøy: grep, sed og awk leser en linje, bestemmer noe om den, og sender den videre. JSON nekter å komme i linjer: én post fyller et dusin linjer når den skrives ut formatert, og et mønster som matcher en nøkkel matcher like gjerne midt inne i en verdi. jq leser hele verdier i stedet for linjer.
Installere jq
Én linje, avhengig av distroen din, og jq --version som kvittering:
$ sudo apt install jq # Ubuntu, Debian
$ sudo dnf install jq # Fedora
$ sudo pacman -S jq # Arch
$ jq --version
jq-1.8.1
Ubuntu 26.04 og Fedora 44 leverer 1.8.1, Arch 1.8.2. Ingenting nedenfor bryr seg om forskjellen, med ett unntak som nevnes når det dukker opp.
Hvordan JSON ser ut
JSON har en kort rollebesetning: objekter i krøllparenteser, arrays i hakeparenteser, strenger, tall, boolske verdier og null. jq gjentar all inndata, formatert:
$ echo '{"name":"nethack"}' | jq .
{
"name": "nethack"
}
Hvorfor snubler linjeverktøyene? grep etter name i utdataen over, og
hver linje som inneholder ordet kommer tilbake, nøkler og verdier
sammen. Linjen er feil enhet for JSON. jq leser hele verdien og svarer
på spørsmål om struktur i stedet for tekst. Skallet behandler det du
taster først, derimot, og det er der kapittelet
Shell-grunnleggende
betyr noe: pipene der avgjør hva jq mottar.
De første filtrene
En jq-kommando er et filter: en kort beskrivelse av hva som skal
hentes fra JSON-en på standard inndata. Det tomme filteret ., vist
over, skriver ut verdien uendret, og det er jqs penutskrift. Det
enkleste navngitte filteret er et felt med et punktum foran:
$ echo '{"name":"nethack"}' | jq '.name'
"nethack"
Svaret bærer doble anførselstegn fordi verdien er en JSON-streng, og jq skriver ut JSON. Et felt som ikke finnes:
$ echo '{"name":"nethack"}' | jq '.nope'
null
null, og exit statusen er 0: et felt som mangler er et svar, ikke en
feil. Nøsting er bare flere punktum:
$ echo '{"a":{"b":42}}' | jq '.a.b'
42
Det er hele ideen. Resten av guiden kjører mot én realistisk fil, så
lagre tre poster fra en liten server som services.json: en
spillserver, en nedlastingsjobb og ssh:
[
{ "name": "nethack", "state": "running", "port": 24565 },
{ "name": "curl", "state": "stopped", "port": null },
{ "name": "sshd", "state": "running", "port": 22 }
]
En array med tre objekter, hvert med et navn, en tilstand og en port.
curl bærer null fordi den er stoppet, og noen her tar sitt nethack
på alvor.
Utdatakontroll
To flagg endrer det som kommer ut. -r skriver ut rå strenger, uten
JSON-anførselstegnene:
$ echo '{"name":"nethack"}' | jq -r '.name'
nethack
Når en verdi er på vei til en annen kommando, er -r det du vil ha.
-c klemmer hver verdi inn på én linje:
$ echo '{"name":"nethack","state":"running"}' | jq -c .
{"name":"nethack","state":"running"}
Kompakt utdata setter postene tilbake i linjeland. Et filter kan også
lese en fil direkte: gi filnavnet som siste argument, og ingen pipe
trengs. Alle eksempler herfra leser services.json på denne måten.
$ jq -c '.[]' services.json
{"name":"nethack","state":"running","port":24565}
{"name":"curl","state":"stopped","port":null}
{"name":"sshd","state":"running","port":22}
Gå gjennom arrays og objekter
Eksempelfila services.json er en array, og arrays indekseres fra
null:
$ echo '["alpha","beta","gamma"]' | jq '.[0]'
"alpha"
$ echo '["alpha","beta","gamma"]' | jq '.[]'
"alpha"
"beta"
"gamma"
.[0] plukker ett element. .[] er iteratoren: den produserer hvert
element etter tur, og det er derfor svarene kom på hver sin linje.
Før den inn i et felt, og du får det feltet fra hver post. Piped
tilhører jq her. Inne i anførselstegnene gir bash den videre urørt:
$ jq '.[] | .name' services.json
"nethack"
"curl"
"sshd"
length teller elementer i en array og tegn i en streng:
$ echo '[10,20,30]' | jq 'length'
3
$ echo '"hello"' | jq 'length'
5
keys lister et objekts nøkler, sortert:
$ echo '{"zebra":1,"apple":2,"mango":3}' | jq 'keys'
[
"apple",
"mango",
"zebra"
]
has() rapporterer om en nøkkel finnes, med en JSON-boolsk verdi:
$ echo '{"name":"nethack"}' | jq 'has("name")'
true
$ echo '{"name":"nethack"}' | jq 'has("nope")'
false
Velge og vrake
select() beholder verdiene som består en test, og treffene skrives
ut én etter én:
$ jq '.[] | select(.state == "running")' services.json
{
"name": "nethack",
"state": "running",
"port": 24565
}
{
"name": "sshd",
"state": "running",
"port": 22
}
jqs //-fallback utløses ved et manglende felt, en null og en
false-verdi. False biter: feltet .active under finnes helt ekte, og
likevel setter // inn "on".
$ echo '{"name":"nethack"}' | jq '.nickname // "none"'
"none"
$ echo '{"name":"nethack","nickname":null}' | jq '.nickname // "none"'
"none"
$ echo '{"nickname":"bob"}' | jq '.nickname // "none"'
"bob"
$ echo '{"active":false}' | jq '.active // "on"'
"on"
-e gjør utdata om til en exit status. Er siste utdata false eller
null, avslutter jq med 1, ellers 0:
$ echo '{"active":false}' | jq -e '.active'
false
$ echo $?
1
true avslutter med 0. Exit status 1 betyr at siste utdata var false
eller null, og null-inndata lander der også, fordi null inn gir null
ut. Exit status 4 betyr at filteret ikke produserte noe utdata i det
hele tatt, som en select uten treff. Det er et annet tilfelle for et
skript som forgrener seg på -e:
$ echo '{"a":1}' | jq -e '.a | select(. > 5)'
$ echo $?
4
Neste kjøring bruker --arg n nethack, som gir jq en variabel med
navnet n. Sammenligningen skriver ut false, og jq avslutter likevel
med 0. Uten -e rapporterer jq og lykkes uansett, så skript som
forgrener seg på svaret trenger -e.
$ echo '{"name":"curl"}' | jq --arg n nethack '.name == $n'
false
$ echo $?
0
Når selve JSON-en er ødelagt
$ echo '{"name":"nethack"' | jq .
jq: parse error: Unfinished JSON term at EOF at line 2, column 0
$ echo $?
5
Parsefeil gir exit status 5, ikke 2, med identisk ordlyd på 1.8.1 og 1.8.2.
Omforming
Å hente ut felt er halve jobben. Krøllparenteser inne i et filter bygger ny JSON av bitene:
$ echo '{"name":"nethack","state":"running"}' | jq '{user: .name}'
{
"user": "nethack"
}
Nøkkelen user er ny, verdien kommer fra .name, og det andre
feltet blir igjen. map() kjører et filter på hvert element i en
array og samler resultatene:
$ echo '[{"name":"nethack"},{"name":"curl"}]' | jq 'map(.name)'
[
"nethack",
"curl"
]
Sist kommer -s, slurp: noe inndata kommer som en strøm av
JSON-verdier i stedet for som ett dokument, og -s leser hele
strømmen inn i én array først:
$ printf '{"n":1}\n{"n":2}\n{"n":3}\n' | jq -s 'length'
3
Én ærlig begrensning: ekte jq-kode lenker disse konstruktene sammen
til lengre pipelines, der map(select(...)) mater videre til nye pipes.
Denne guiden holder seg bevisst til én idé per filter. Komposisjon er
der jq blir både kraftfull og uleselig, og det får ikke plass i det
som gjenstår.
JSON fra din egen maskin
To kommandoer på en vanlig Linux-boks sender ut JSON, i de to formene
denne guiden har holdt atskilt. Den første er ip fra kapittelet
Nettverk, som med -j skriver
ut grensesnittene sine som én JSON-array:
$ ip -j addr | jq -r '.[].ifname'
lo
eth0
Én array, ett objekt per grensesnitt, lo først her, og .[0] gir
deg det første objektet. Feltnavn som ifname tilhører iproute2.
Grensesnittene er ulike, så ikke forvent de samme feltene overalt.
Den andre er journalctl fra kapittelet
Tjenester og logger.
-o json skriver ut oppføringene som JSON Lines: journalctl -n 5 -o json
gir fem objekter på fem linjer, uten array rundt. Rekker du etter
.[0], klager jq én gang per objekt:
jq: error (at <stdin>:1): Cannot index object with number
Fem slike linjer, én per post, og exit code 5. Meldingen er nøyaktig:
objekter nekter numerisk indeksering. Archs jq 1.8.2 legger til et
(0)-suffiks på hver linje. Fiksen er -s, som slurper strømmen med
ett objekt per linje inn i én array først:
$ journalctl -n 5 -o json | jq -s 'length'
5
To forbehold. jq 'keys' uten -s skriver ut én keys-array per post,
ikke én for alt. Og .MESSAGE er en ren streng, så
journalctl -n 3 -o json | jq -r '.MESSAGE' skriver ut hver
oppførings meldingstekst, én per linje, med anførselstegnene fjernet.
--arg er broen fra skallverdier og inn i filtre. Skallet utvider
"$name" før jq i det hele tatt kjører:
$ name=nethack
$ echo '{"name":"nethack"}' | jq --arg n "$name" '.name == $n'
true
Peker du den mot {"name":"curl"} i stedet, er svaret false, igjen
med exit status 0. --arg n "$name" definerer $n inne i filteret,
fri for skallets utvidelse.
Så fellen denne guiden skylder deg. Enkle filtre overlever uten
anførselstegn, fordi et bart .name ikke rommer noe bash vil ha:
$ echo '{"name":"nethack"}' | jq .name
"nethack"
Det virket, exit status 0 og alt sammen. Legg til et tegn med sidejobb
for skallet, og versjonen uten anførselstegn faller fra hverandre på
tre måter. Piped deler kommandoen, og bash prøver å kjøre et program
med navnet .name:
$ echo '{"name":"nethack"}' | jq .[] | .name
bash: .name: command not found
Exit 127, og den feilen er bashs, ikke jqs. En // uten anførselstegn
ender med jq som rapporterer
jq: error: Could not open file none: No such file or directory og
exit status 2. Krøllparenteser uten anførselstegn gir jq halve
filteret, som returnerer en kompileringsfeil og exit status 3. Sett
enkelanførselstegn rundt filteret, alltid: det er den eneste formen
som aldri overrasker deg.
Avslutningsprosjekt — telle det som kjører
Alt ovenfor folder seg sammen til et lite skript som tar imot en JSON-fil, beholder postene som kjører, og teller dem:
#!/bin/bash
# running-count.sh -- count records in the running state
count=$(jq -c '.[] | select(.state == "running")' "$1" | wc -l)
echo "$1: $count running"
Her jobber to pipes: jqs egen, inne i anførselstegnene, mellom
iteratoren og select, og en pipe i skallet som sender jqs utdata
videre til wc -l fra kapittelet Tekstbehandling. -c legger hvert
treff på sin egen linje, og det er det som gjør wc -l til en
posttelling:
$ bash running-count.sh services.json
services.json: 2 running
Hvor du står nå
Du kan lese begge JSON-formene en Linux-maskin sender ut, og hente
felt og poster fra begge. Vanene: enkelanførselstegn rundt filtrene,
-r for rå tekst, -s for inndata med ett objekt per linje, og -e
når et skript trenger dommen som exit status. Dypere jq venter
utenfor denne guiden: reduce, def, streaming, jqplay, generering
av JSON og live-kall mot eksterne API-er er alle utenfor omfanget her,
og hvert eksempel over kjørte lokalt, så ingenting av det råtner.
Til skallsiden av dagens kommandoer har kapittelet Shell-grunnleggende pipene. Når det gjelder anførselstegn, se fellen ovenfor. To referanser dekker resten:
- jq-manualen — hele filterspråket og hvert flagg som er brukt her
- jq på GitHub — utgivelser, issue tracker og kilden