filtern, transformieren, validieren
jq macht JSON in der Shell erstklassig handhabbar. Wer Filter, select(), map(), has() und type-Checks beherrscht, Variablen sicher mit --arg übergibt und Fehler mit try-catch behandelt, verarbeitet API-Antworten, Konfigurationsdateien und Log-Daten direkt in Bash – ohne Umweg über Python oder Node.
Inhaltsverzeichnis
- 1. Warum jq das JSON-Problem in der Shell löst
- 2. Grundfilter: Felder extrahieren und navigieren
- 3. Iteration mit .[] und Array-Operationen
- 4. Bedingte Filterung mit select()
- 5. Transformation mit map() und map_values()
- 6. Strukturprüfung mit has() und type
- 7. Bash-Variablen sicher mit --arg übergeben
- 8. Fehlerbehandlung mit try-catch und alternative
- 9. jq-Filter im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum jq das JSON-Problem in der Shell löst
jq ist ein Kommandozeilen-JSON-Prozessor, der in Bash-Skripten die Lücke schließt, die zwischen rohen API-Antworten und nutzbaren Daten klafft. Vor jq musste man JSON in der Shell entweder mit grep und sed parsen – fehleranfällig, nicht Unicode-safe und bei verschachtelten Strukturen unmöglich – oder Python mit einem import json-Einzeiler aufrufen, was einen separaten Prozess erfordert und in minimalen Umgebungen möglicherweise nicht verfügbar ist. jq ist ein einzelnes Binary, das in den meisten Linux-Distributionen über den Paketmanager verfügbar ist und eine vollständige JSON-Verarbeitungssprache mitbringt.
Die Stärke von jq liegt in seiner funktionalen Filter-Pipeline: Jeder Filter transformiert das Eingabe-JSON und gibt das Ergebnis an den nächsten Filter weiter. .|.items[].name ist eine Pipeline, die zuerst auf das Eingabeobjekt zugreift, dann das items-Array iteriert und von jedem Element den name-Wert extrahiert. Diese Notation macht komplexe JSON-Traversierungen lesbar und nachvollziehbar. Der Vergleich mit grep-basierten Ansätzen zeigt sofort die Überlegenheit: jq versteht die JSON-Struktur, während grep nur auf Text-Ebene arbeitet.
Ein praktischer Vorteil von jq in CI/CD-Pipelines: Es ist streaming-fähig. Sehr große JSON-Dateien oder kontinuierliche Streams können mit jq --stream verarbeitet werden, ohne dass die gesamte Datei in den Speicher geladen wird. Für typische API-Antworten im KB-Bereich ist das nicht notwendig, aber für Log-Dateien im GB-Bereich ist es ein entscheidender Faktor. jq ist kein Werkzeug für Gelegenheitsnutzung – wer es einmal gründlich gelernt hat, setzt es täglich in Shell-Skripten ein.
2. Grundfilter: Felder extrahieren und navigieren
Der einfachste jq-Filter ist .feldname: Er extrahiert den Wert des angegebenen Felds aus dem Eingabe-Objekt. Verschachtelte Felder werden mit Punkt-Notation navigiert: .metadata.labels.environment. Array-Elemente mit .items[0] (erstes Element), .items[-1] (letztes Element) oder .items[2:5] (Slice). Der Operator ? nach einem Feldnamen (.optional_field?) gibt null zurück statt einen Fehler, wenn das Feld nicht existiert – ein wichtiger Unterschied für defensive jq-Programmierung.
Der Identitäts-Filter . gibt das Eingabe-JSON unverändert zurück und wird häufig als Ausgangspunkt in Pipelines genutzt. jq '.' formatiert JSON lesbar (Pretty-Print). jq -c '.' gibt kompaktes JSON ohne Whitespace aus – ideal für die Weiterverarbeitung in Bash-Variablen, da kein Whitespace-Escaping nötig ist. jq -r (raw output) entfernt Anführungszeichen aus String-Ausgaben, was bei der Nutzung in Bash-Variablen unverzichtbar ist: name="$(jq -r '.name' <<< "$json")" ohne -r würde "myname" mit Anführungszeichen zuweisen.
#!/usr/bin/env bash
# jq_basics.sh — Core jq filter patterns for shell automation
set -euo pipefail
# Sample JSON (typical API response)
API_RESPONSE='{
"id": 42,
"name": "deploy-prod",
"status": "running",
"metadata": { "env": "production", "version": "2.3.1" },
"tags": ["release", "hotfix"],
"artifacts": [
{"name": "app.tar.gz", "size": 1024000, "checksum": "abc123"},
{"name": "db-migration.sql", "size": 4096, "checksum": "def456"}
]
}'
# Basic field extraction
name="$(jq -r '.name' <<< "$API_RESPONSE")"
env="$(jq -r '.metadata.env' <<< "$API_RESPONSE")"
first_tag="$(jq -r '.tags[0]' <<< "$API_RESPONSE")"
last_artifact="$(jq -r '.artifacts[-1].name' <<< "$API_RESPONSE")"
echo "Name: $name | Env: $env | First tag: $first_tag | Last artifact: $last_artifact"
# Multiple fields in one jq call — more efficient than separate calls
read -r id status version < <(jq -r '[.id, .status, .metadata.version] | @tsv' <<< "$API_RESPONSE")
echo "ID: $id | Status: $status | Version: $version"
# Optional field access — no error if field missing
optional="$(jq -r '.missing_field? // "default_value"' <<< "$API_RESPONSE")"
echo "Optional: $optional"
# Array length
artifact_count="$(jq '.artifacts | length' <<< "$API_RESPONSE")"
echo "Artifacts: $artifact_count"
# Extract all checksums as newline-separated values
jq -r '.artifacts[].checksum' <<< "$API_RESPONSE"
3. Iteration mit .[] und Array-Operationen
Der jq-Iterator .[] auf ein Array gibt jedes Element einzeln aus – als separates JSON-Dokument, nicht als Array. Das ist der Schlüsselunterschied für die Bash-Integration: In einer Bash-Schleife kann man die Ausgabe von jq '.items[]' Zeile für Zeile verarbeiten, wenn die Elemente einzeilige Werte sind. Für mehrzeilige oder verschachtelte Objekte empfiehlt sich jq -c '.items[]' (kompaktes JSON, ein Objekt pro Zeile) und das Lesen mit while IFS= read -r item.
Die Länge eines Arrays gibt length zurück: jq '.items | length'. Leere Arrays geben 0 zurück – das ist ein definiertes Verhalten, das in Bash-Checks direkt genutzt werden kann. Für das Prüfen ob ein Array mindestens ein Element hat: jq -e '.items | length > 0' – mit -e (exit-status) gibt Exit-Code 1 bei false zurück. first und last sind Abkürzungen für .[0] und .[-1]. nth(n; expr) gibt das n-te Element einer Stream-Ausdruck zurück. jq bietet eine vollständige Bibliothek für Array-Operationen, die extern kein Tool erfordert.
4. Bedingte Filterung mit select()
select() ist der zentrale Filteroperator in jq für bedingte Auswahl. select(cond) gibt das Element unverändert zurück, wenn cond true ist, und gibt nichts zurück wenn false. In Kombination mit .[] filtert das ein Array nach Bedingungen: .items[] | select(.status == "active") gibt alle aktiven Items aus. Mehrere Bedingungen kombinieren sich mit and und or: select(.status == "active" and .env == "production").
Für String-Matching in jq steht test(regex) zur Verfügung: select(.name | test("^feat-")) filtert alle Elemente, deren name mit "feat-" beginnt. startswith() und endswith() sind Alias-Funktionen für häufige String-Tests. contains(val) prüft, ob ein String einen Teilstring enthält oder ein Array ein Element enthält – in jq ist contains generisch für alle JSON-Typen definiert. Diese Ausdrücke lassen sich beliebig kombinieren und verschachteln, um komplexe Filterlogik prägnant auszudrücken.
#!/usr/bin/env bash
# jq_select_map.sh — Filtering and transformation with jq
set -euo pipefail
DEPLOYMENTS='[
{"id": 1, "name": "api-v2", "env": "production", "status": "success", "size": 2048},
{"id": 2, "name": "frontend", "env": "staging", "status": "failed", "size": 1024},
{"id": 3, "name": "worker", "env": "production", "status": "success", "size": 512},
{"id": 4, "name": "cron-job", "env": "staging", "status": "success", "size": 256}
]'
# Filter: production deployments only
echo "=== Production deployments ==="
jq -r '.[] | select(.env == "production") | "\(.id): \(.name) [\(.status)]"' <<< "$DEPLOYMENTS"
# Filter: failures across all environments
echo "=== Failed deployments ==="
jq -r '[.[] | select(.status == "failed")] | length' <<< "$DEPLOYMENTS"
# Combined filter: successful and larger than 500KB
echo "=== Large successful deployments ==="
jq -r '.[] | select(.status == "success" and .size > 500) | .name' <<< "$DEPLOYMENTS"
# map(): transform array elements
echo "=== Name + status pairs ==="
jq -c 'map({name: .name, ok: (.status == "success")})' <<< "$DEPLOYMENTS"
# map(select()): filter-map in one step
echo "=== Staging names only ==="
jq -r '[.[] | select(.env == "staging") | .name] | join(", ")' <<< "$DEPLOYMENTS"
# Group by environment — requires sort_by first for group_by
echo "=== Grouped by env ==="
jq 'group_by(.env) | map({env: .[0].env, count: length, names: map(.name)})' \
<<< "$DEPLOYMENTS"
# Count by status
jq 'group_by(.status) | map({status: .[0].status, count: length})' <<< "$DEPLOYMENTS"
5. Transformation mit map() und map_values()
map(expr) in jq ist das Äquivalent zu .[] | expr | [...] mit Klammern: Es wendet den Ausdruck auf jedes Element eines Arrays an und gibt das Ergebnis als Array zurück. Das ist die grundlegende Transformationsoperation in jq. map(.name) extrahiert den name-Wert aus jedem Objekt und gibt ein String-Array zurück. map(. * 2) verdoppelt jeden Zahlenwert. map(if .active then . else empty end) kombiniert Filter und Map in einem Schritt.
map_values(expr) wendet einen Ausdruck auf alle Werte eines Objekts an – nicht auf ein Array, sondern auf die Values einer Key-Value-Map. Das ist nützlich für die Transformation von Konfigurationsobjekten: map_values(. + "_v2") hängt an jeden Wert "_v2" an. to_entries und from_entries in jq ermöglichen die Transformation von Objekten als Array von Key-Value-Paaren: Mit to_entries | map(select(.value != null)) | from_entries werden alle Null-Werte aus einem Objekt entfernt. Diese Kombinationen ermöglichen komplexe Datenumformungen vollständig in jq, ohne externe Tools.
6. Strukturprüfung mit has() und type
Defensives jq-Programmieren bedeutet, vor dem Zugriff auf Felder zu prüfen, ob sie vorhanden sind und den erwarteten Typ haben. has("feldname") gibt true zurück wenn das Feld im Objekt vorhanden ist – auch wenn sein Wert null ist. in(objekt) ist das Inverse: "key" | in(objekt). Für Arrays gibt has(n) zurück, ob das Array mindestens n+1 Elemente hat. Diese Checks verhindern, dass jq-Skripte bei unerwarteten API-Antworten oder veralteten Schemas fehlschlagen.
Type-Checks mit type geben den Typ als String zurück: "null", "boolean", "number", "string", "array", "object". Kombiniert mit select: select(type == "array") filtert nur Array-Elemente aus einem gemischten Input. Die Funktionen arrays, objects, strings, numbers, booleans, nulls, values und scalars sind Abkürzungen für select(type == "..."). In jq-Skripten, die externe API-Antworten verarbeiten, sollte jeder Zugriff auf Pflichtfelder mit einem Type-Check oder einem Nullcheck abgesichert sein.
#!/usr/bin/env bash
# jq_validation.sh — Structural validation and safe field access with jq
set -euo pipefail
# Validate JSON structure before processing
validate_deployment_json() {
local json="$1"
# Check required fields exist and have correct types
local errors
errors="$(jq -r '
[
(if has("id") and (.id | type) == "number" then empty
else "Missing or invalid field: id" end),
(if has("name") and (.name | type) == "string" then empty
else "Missing or invalid field: name" end),
(if has("status") and (.status | strings | test("^(success|failed|running)$")) then empty
else "Missing or invalid field: status (must be success|failed|running)" end),
(if has("artifacts") and (.artifacts | type) == "array" then empty
else "Missing or invalid field: artifacts (must be array)" end)
] | join("\n")
' <<< "$json")"
if [[ -n "$errors" ]]; then
echo "[ERROR] JSON validation failed:" >&2
echo "$errors" >&2
return 1
fi
}
# Safe extraction with type check and default
safe_get() {
local json="$1" path="$2" expected_type="${3:-string}" default="${4:-}"
jq -r --arg type "$expected_type" --arg default "$default" \
'getpath($path | split(".")) |
if . == null then $default
elif type == $type then (if $type == "string" then . else tostring end)
else $default end' <<< "$json" 2>/dev/null || echo "$default"
}
# Type-aware field processing
process_config() {
local config="$1"
# Extract only string values from config object
echo "String config values:"
jq -r 'to_entries[] | select(.value | type == "string") | "\(.key)=\(.value)"' \
<<< "$config"
# Extract numbers and calculate total
echo "Numeric totals:"
jq '[to_entries[] | select(.value | type == "number") | .value] | add // 0' \
<<< "$config"
# Find nested arrays
echo "Array fields:"
jq -r 'to_entries[] | select(.value | type == "array") | .key' <<< "$config"
}
SAMPLE='{"timeout": 30, "env": "prod", "features": ["flag-a", "flag-b"], "debug": false}'
process_config "$SAMPLE"
7. Bash-Variablen sicher mit --arg übergeben
Das häufigste Sicherheitsproblem beim jq-Einsatz in Bash ist die direkte Interpolation von Bash-Variablen in jq-Filter-Strings. jq ".name == \"$user_input\"" bricht bei Anführungszeichen im Input, beim Backslash und bei bestimmten Sonderzeichen – und ist anfällig für jq-Injection. Die korrekte Methode ist immer --arg name value: Die Variable wird als typisierter JSON-String übergeben, jq übernimmt das korrekte Escaping, und der Filter referenziert sie als $name.
Neben --arg (String-Typ) gibt es --argjson name json_value für JSON-Werte (Zahlen, Booleans, Arrays, Objekte), --rawfile name file für Dateiinhalte als String und --slurpfile name file für JSON-Dateien als Array. --args und --jsonargs am Ende des Befehls übergeben $ARGS.positional als String- bzw. JSON-Array für dynamische Argumentlisten. Diese Methoden machen jq-Filter in Bash-Skripten sicher und portabel, unabhängig von Sonderzeichen in den verarbeiteten Daten.
#!/usr/bin/env bash
# jq_variables.sh — Safe variable passing and output formatting
set -euo pipefail
DEPLOYMENTS='[
{"id": 1, "name": "api", "env": "production", "version": "2.3.0"},
{"id": 2, "name": "frontend", "env": "staging", "version": "1.8.4"},
{"id": 3, "name": "worker", "env": "production", "version": "2.3.0"}
]'
# WRONG: variable interpolation — breaks with special chars, injection risk
# jq ".[] | select(.env == \"$target_env\")" <<< "$DEPLOYMENTS"
# RIGHT: --arg passes string safely, --argjson for numbers/booleans
target_env="production"
min_id=2
echo "=== Safe variable passing ==="
jq -r --arg env "$target_env" \
'.[] | select(.env == $env) | .name' <<< "$DEPLOYMENTS"
jq -r --argjson min_id "$min_id" \
'.[] | select(.id >= $min_id) | "\(.id): \(.name)"' <<< "$DEPLOYMENTS"
# Build JSON payload from Bash variables (never use string concatenation)
deploy_version="2.4.0"
deploy_env="production"
deploy_timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
payload="$(jq -n \
--arg version "$deploy_version" \
--arg env "$deploy_env" \
--arg ts "$deploy_timestamp" \
--argjson dry_run false \
'{version: $version, environment: $env, triggered_at: $ts, dry_run: $dry_run}')"
echo "=== Built payload ==="
echo "$payload"
# Format as CSV, TSV or key=value for downstream tools
echo "=== Tab-separated output for further processing ==="
jq -r '.[] | [.id, .name, .env, .version] | @tsv' <<< "$DEPLOYMENTS"
echo "=== CSV output ==="
jq -r '.[] | [.id, .name, .env, .version] | @csv' <<< "$DEPLOYMENTS"
8. Fehlerbehandlung mit try-catch und alternative
jq bietet zwei Mechanismen für die Fehlerbehandlung: den Alternative-Operator // und den try-catch-Ausdruck. Der Alternative-Operator expr // default gibt default zurück wenn expr null oder false ergibt – aber nicht bei Fehlern. Für echte Fehlerbehandlung (z. B. Type-Mismatch, ungültiger Zugriff) ist try expr catch "error: \(.)" der richtige Weg: Der catch-Block fängt den Fehler ab und kann ihn als String verarbeiten.
Ein wichtiger Aspekt bei der jq-Fehlerbehandlung in Bash-Skripten: jq gibt Exit-Code 0 zurück, auch wenn die Ausgabe null ist – sofern das JSON gültig ist. Mit -e (exit-status) gibt jq Exit-Code 1 zurück, wenn das letzte Ergebnis null oder false ist. Das ist die Grundlage für jq-Ausdrücke als Bash-Bedingungen. Auf der anderen Seite: Wenn die Eingabe kein valides JSON ist, schreibt jq eine Fehlermeldung nach stderr und gibt Exit-Code 5 zurück. Mit jq . <<< "$input" >/dev/null 2>&1 als Vorabcheck lässt sich das in Bash abfangen.
| jq-Ausdruck | Funktion | Bash-Äquivalent | Hinweis |
|---|---|---|---|
.field? // "default" |
Optionales Feld mit Fallback | ${var:-default} |
Kein Fehler wenn Feld fehlt |
select(.x == $v) |
Bedingte Filterung | grep/awk-Filter | Typsicher, kein Text-Parsen |
--arg key "$var" |
Bash-Var als String übergeben | String-Interpolation | Injection-sicher, Escaping automatisch |
jq -e 'expr' |
Exit 1 bei null/false | [ condition ] |
jq als Bash-Bedingung nutzbar |
try expr catch msg |
Fehler abfangen | || { handle; } |
Für Type-Mismatch und Zugriffsfehler |
9. Erweiterte Techniken: Streaming und Multi-Dokument
Im Shell-Alltag begegnet man jq auch in Szenarien, in denen die Eingabe kein einzelnes JSON-Dokument ist, sondern mehrere. jq mit -s (slurp) liest alle Eingabedokumente in ein Array. Ohne -s verarbeitet jq jedes JSON-Dokument separat. Das macht cat file1.json file2.json | jq -s '.' zu einem einfachen Merge-Operator für JSON-Dateien.
Für NDJSON (Newline-Delimited JSON) – ein Zeilenformat, das von vielen Logging-Systemen ausgegeben wird – ist jq direkt einsetzbar: Jede Zeile wird als separates JSON-Dokument verarbeitet. jq 'select(.level == "error")' < app.ndjson filtert nur Fehler-Logzeilen aus einem NDJSON-Log. Das ermöglicht effizientes Log-Analyse direkt in der Shell, ohne Import in eine Datenbank. Kombiniert mit grep für vorherige Zeilen-Filterung und jq für strukturierte Auswertung entsteht eine leistungsstarke Log-Analyse-Pipeline.
Mironsoft
Shell-Automatisierung, API-Integration und DevOps-Tooling
JSON-Verarbeitung, die in der Shell zuverlässig funktioniert?
Wir entwickeln robuste jq-basierte Bash-Automatisierungen für API-Integration, Konfigurationsverarbeitung und Log-Analyse – mit vollständiger Validierung, sicherer Variablenübergabe und Fehlerbehandlung.
API-Integration
curl + jq Automatisierungen für REST-APIs mit Validierung und Fehlerbehandlung
Konfigurationsverarbeitung
JSON-Konfigdateien transformieren, validieren und in CI-Pipelines nutzen
Log-Analyse
NDJSON-Log-Streams mit jq filtern, aggregieren und in Reports umwandeln
10. Zusammenfassung
jq im Shell-Alltag professionell einzusetzen bedeutet: Felder mit -r ohne Anführungszeichen extrahieren, Arrays mit .[] iterieren, mit select() filtern, mit map() transformieren, mit has() und type Strukturen prüfen, Bash-Variablen ausnahmslos mit --arg übergeben und mit -e (exit-status) jq-Ausdrücke als Bash-Bedingungen nutzen. Diese sechs Punkte decken neunzig Prozent der alltäglichen JSON-Verarbeitungsaufgaben in Shell-Skripten ab.
Die wichtigste Einzel-Regel: Niemals Bash-Variablen direkt in jq-Filter-Strings interpolieren. Das ist der häufigste Bug und ein Sicherheitsrisiko. --arg ist immer die richtige Methode. Danach folgt die Validierung der JSON-Struktur vor dem Zugriff auf Felder, besonders bei externen API-Antworten, die sich ändern können. Mit diesen Grundsätzen wird jq in Bash zu einem verlässlichen Bestandteil jeder API-Automatisierung, CI/CD-Pipeline und Log-Analyse.
jq für JSON im Shell-Alltag — Das Wichtigste auf einen Blick
Extraktion mit -r
jq -r '.field' gibt String ohne Anführungszeichen aus. -c für kompaktes JSON. Mehrere Felder in einem Call mit @tsv oder @csv.
Variablen mit --arg
Nie Bash-Variablen in Filter-Strings interpolieren. --arg key "$var" für Strings, --argjson key "$num" für JSON-Werte.
Validation mit -e
jq -e '.field' gibt Exit-Code 1 bei null/false. jq als Bash-Bedingung nutzen: jq -e 'has("id")' <<< "$json".
Sichere Defaults
.field? // "default" für optionale Felder. try expr catch msg für Fehler. JSON-Validierung vor Feldstextraktion.
11. FAQ: jq für JSON im Shell-Alltag
1Unterschied zwischen jq -r und jq -c?
-r entfernt Anführungszeichen für Bash-Variablen. -c gibt kompaktes JSON aus für JSON-Weiterverarbeitung. Für Bash-Variablen immer -r.2Warum keine Bash-Variablen in jq-Filter interpolieren?
--arg key "$var" lässt jq korrektes Escaping übernehmen.3Wie prüfe ich ob ein JSON-Feld vorhanden ist?
has("feld") gibt true zurück auch wenn Wert null ist. jq -e 'has("id")' als Bash-Bedingung. .feld? für fehlerfreien optionalen Zugriff.4JSON-Array in Bash-Schleife verarbeiten?
jq -c '.[]' gibt ein Element pro Zeile als kompaktes JSON. while IFS= read -r item; do … done < <(jq -c '.[]' …) für Element-weise Verarbeitung.5Was macht select() in jq?
empty. Kombiniert mit .[] filtert es Arrays: .[] | select(.active).6Wie gibt jq Exit-Code 1 bei null zurück?
jq -e '.field' – Flag -e (exit-status) gibt Exit-Code 1 bei null/false. Als Bash-Bedingung direkt nutzbar: if jq -e '…' <<< "$json"; then ….7JSON-Payloads sicher aus Bash-Variablen bauen?
jq -n --arg name "$name" '{name: $name}'. Nie String-Konkatenation für JSON. --argjson für Zahlen und Booleans. -n startet ohne Eingabe.8NDJSON mit jq verarbeiten?
-s als Array. --stream für speichereffizientes Processing großer Dateien.9JSON-Objekt mit map_values() transformieren?
map_values(expr) wendet Ausdruck auf alle Values eines Objekts an. to_entries | map(…) | from_entries für Key+Value-Zugriff gleichzeitig.10Fehler in jq-Ausdrücken behandeln?
try expr catch "msg" fängt Laufzeitfehler. .field? // "default" für null-sichere Defaults. Vorab-Validierung: jq . <<< "$input" >/dev/null 2>&1.