Guide

jq arrêtez de faire du grep sur du JSON

jq transforme, filtre et reformate du JSON en ligne de commande. select, map, group_by, --arg, @formats - pour traiter les sorties kubectl et les API.

Sommaire

En 2012, Stephen Dolan en avait assez de parser du JSON avec grep, sed et awk comme un animal préhistorique. Il a écrit jq en deux semaines, comme Git et SSH, apparemment deux semaines c'est le temps qu'il faut à un développeur légèrement irrité pour créer un outil que des millions de personnes utiliseront pendant des décennies en n'utilisant que le point.

Parce que le point, tout le monde le connaît. jq . pour formater du JSON, et hop, on se sent malin. C'est l'équivalent de savoir allumer une tronçonneuse et se prendre pour un bûcheron.

jq est un processeur JSON en ligne de commande. Il lit du JSON, il le transforme, il le filtre, il le reformate. C'est tout. Mais "c'est tout" cache une profondeur qui surprend tout le monde la première fois qu'on va au-delà du point. Ce guide parle de ce qui vient après. Si vous êtes déjà à l'aise avec tout ce qui suit, fermez cet onglet et allez faire quelque chose d'utile.

Les bases qu'on croit maîtriser

Le point - formatter du JSON

echo '{"name":"radar","version":"1.2.0","env":"prod"}' | jq .

# {
#   "name": "radar",
#   "version": "1.2.0",
#   "env": "prod"
# }

Le point seul reformate le JSON avec indentation. C'est la commande que 80% des gens connaissent et pensent être l'étendue de jq. C'est le bouton d'allumage de la tronçonneuse.

Accéder à un champ

echo '{"name":"radar","version":"1.2.0"}' | jq '.name'
# "radar"

# Sans les guillemets
echo '{"name":"radar"}' | jq -r '.name'
# radar

-r (raw output) supprime les guillemets autour des strings. Dans un script shell où vous assignez le résultat à une variable, vous voulez presque toujours -r. Si vous ne le mettez pas et que vous vous demandez pourquoi votre variable contient des guillemets, voilà pourquoi.

Accéder à un champ imbriqué

echo '{"meta":{"env":"prod","region":"eu-west-1"}}' | jq '.meta.region'
# "eu-west-1"

Accéder à un tableau

echo '["kubernetes","docker","terraform"]' | jq '.[0]'
# "kubernetes"

echo '["kubernetes","docker","terraform"]' | jq '.[]'
# "kubernetes"
# "docker"
# "terraform"

echo '["kubernetes","docker","terraform"]' | jq 'length'
# 3

Ce qui échoue silencieusement - et c'est le vrai problème

echo '{"name":"radar"}' | jq '.version'
# null

Si le champ n'existe pas, jq retourne null sans erreur et sans remords. Il ne vous dit rien. Il vous laisse propager ce null dans vos scripts jusqu'à ce que quelque chose explose 40 lignes plus loin dans un contexte complètement différent, pendant que vous vous demandez ce que vous avez fait de mal dans une vie antérieure.

jq -e retourne un code d'erreur non-zéro si le résultat est null ou false - utilisez-le dans les scripts :

version=$(echo '{"name":"radar"}' | jq -e -r '.version') || {
  echo "Champ version manquant"
  exit 1
}

Construire et transformer

Construire un nouvel objet

echo '{"name":"radar","version":"1.2.0","internal_id":"abc123"}' | \
  jq '{nom: .name, ver: .version}'

# {
#   "nom": "radar",
#   "ver": "1.2.0"
# }

Vous choisissez les champs que vous voulez, vous les renommez, vous en ignorez d'autres. Utile pour extraire ce qui compte d'une réponse API qui vous renvoie 47 champs dont vous n'avez besoin que de trois.

Ajouter ou modifier un champ

echo '{"name":"radar","version":"1.2.0"}' | \
  jq '. + {"env":"prod","deployed":true}'

Supprimer un champ

echo '{"name":"radar","secret":"abc123","version":"1.2.0"}' | \
  jq 'del(.secret)'

del est particulièrement utile pour nettoyer des réponses API avant de les loguer. Parce que loguer des tokens en clair dans Elasticsearch c'est le genre de découverte qu'on fait un vendredi soir.

select - filtrer les éléments

select garde les éléments pour lesquels l'expression est vraie. C'est le WHERE de jq, mais sans le SQL autour pour vous donner l'illusion que vous faites quelque chose de sérieux.

# Filtrer les pods qui ne sont pas Running
kubectl get pods -o json | \
  jq '.items[] | select(.status.phase != "Running") | .metadata.name'

# Filtrer les conteneurs avec plus de 2 restarts
kubectl get pods -o json | \
  jq '.items[].status.containerStatuses[] | 
      select(.restartCount > 2) | 
      {name: .name, restarts: .restartCount}'

Les opérateurs de comparaison

select(.age > 30)
select(.name == "radar")
select(.name != "legacy")
select(.active)
select(.active | not)
select(.tags | length > 0)

select avec test pour les regex

kubectl get pods -o json | \
  jq '.items[] | select(.metadata.name | test("radar")) | .metadata.name'

map - transformer chaque élément

map(expr) applique une transformation à chaque élément d'un tableau. C'est le SELECT de jq, et non ce n'est pas la même chose que select, oui c'est confusant, bienvenue dans jq.

kubectl get pods -o json | \
  jq '.items | map(.metadata.name)'

# Transformer chaque élément
echo '[1,2,3,4,5]' | jq 'map(. * 2)'
# [2,4,6,8,10]

# Combiner map et select
kubectl get pods -o json | \
  jq '.items | map(select(.status.phase == "Running")) | map(.metadata.name)'

group_by et sort_by - agréger et trier

# Grouper les pods par namespace
kubectl get pods -A -o json | \
  jq '.items | group_by(.metadata.namespace) | 
      map({namespace: .[0].metadata.namespace, count: length})'

# Trouver le namespace avec le plus de pods
kubectl get pods -A -o json | \
  jq '.items | 
      group_by(.metadata.namespace) | 
      map({ns: .[0].metadata.namespace, count: length}) | 
      sort_by(.count) | 
      reverse | 
      .[0]'

unique et unique_by

# Lister les images uniques dans le cluster
kubectl get pods -A -o json | \
  jq '[.items[].spec.containers[].image] | unique | .[]' -r

Les cas d'usage DevOps concrets

kubectl - l'usage le plus courant

# Tous les pods qui ne sont pas Running
kubectl get pods -A -o json | jq -r '
  .items[] | 
  select(.status.phase != "Running") |
  [.metadata.namespace, .metadata.name, .status.phase] | 
  @tsv'

# Pods en CrashLoopBackOff - parce que ça arrive toujours à 23h
kubectl get pods -A -o json | jq -r '
  .items[] | 
  select(.status.containerStatuses[]?.state.waiting.reason == "CrashLoopBackOff") |
  "\(.metadata.namespace)/\(.metadata.name)"'

# Images utilisées dans le cluster
kubectl get pods -A -o json | jq -r '
  .items[].spec.containers[].image' | sort | uniq -c | sort -rn

docker inspect

# Variables d'environnement d'un conteneur
# Utile pour vérifier que votre secret n'est pas "password123"
docker inspect mon-conteneur | \
  jq '.[0].Config.Env[]' -r

# Ports exposés
docker inspect mon-conteneur | \
  jq '.[0].NetworkSettings.Ports'

Logs JSON

# Filtrer les erreurs
cat app.log | jq 'select(.level == "error")'

# Compter les erreurs par type
# Pour le post-mortem du lundi matin
cat app.log | jq -r 'select(.level == "error") | .error_code' | \
  sort | uniq -c | sort -rn

# Erreurs des 5 dernières minutes
cat app.log | jq --arg since "$(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%S)" \
  'select(.level == "error" and .timestamp > $since)'

Réponses API

# Construire une commande shell depuis une réponse API
# Parce que parfois c'est vraiment la bonne solution
curl -s "https://api.example.com/servers" | \
  jq -r '.servers[] | "ssh \(.user)@\(.ip) -p \(.port)"'

Combiner avec curl - le duo parfait

# Extraire un token et l'utiliser immédiatement
TOKEN=$(curl -s -X POST "https://auth.example.com/token" \
  -d '{"user":"cyril","pass":"..."}' | jq -r '.access_token')

curl -H "Authorization: Bearer $TOKEN" \
  "https://api.example.com/data" | jq '.results[]'

# Pipeline complet : fetch → filter → retry les déploiements en échec
# Trois commandes qui font ce qu'une interface graphique ferait en 15 clics
curl -s "https://api.example.com/deployments" | \
  jq -r '.[] | select(.status == "failed") | .id' | \
  xargs -I{} curl -s -X POST "https://api.example.com/deployments/{}/retry"

--arg et --argjson - injecter des variables shell proprement

Le piège classique : essayer d'interpoler une variable shell dans un filtre jq comme un cowboy. Ça marche jusqu'au jour où la variable contient un guillemet, un espace, ou pire - du JSON. Ce jour-là vous découvrirez l'injection dans vos propres scripts, ce qui est une expérience humiliante et formatrice.

# NE FAITES PAS ÇA
ENV="prod"
jq "select(.env == \"$ENV\")"

# Faites ça
ENV="prod"
jq --arg env "$ENV" 'select(.env == $env)'

--arg injecte une string. --argjson injecte une valeur JSON :

# Injecter un nombre
jq --argjson threshold 5 'select(.restartCount > $threshold)'

# Injecter la date courante
jq --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
   '. + {deployed_at: $date}'

--slurp - lire plusieurs objets JSON comme un tableau

# Compter les erreurs dans plusieurs fichiers de logs
cat logs/*.json | jq -s 'map(select(.level == "error")) | length'

@formats - exporter dans d'autres formats

# TSV - pour piper vers awk, sort, ou votre collègue qui préfère Excel
kubectl get pods -o json | \
  jq -r '.items[] | [.metadata.name, .status.phase, .spec.nodeName] | @tsv'

# Décoder un secret Kubernetes
# Parce que base64 c'est pas du chiffrement mais tout le monde fait semblant
kubectl get secret mon-secret -o json | \
  jq -r '.data.password | @base64d'

Les pièges classiques

Les nulls qui se propagent silencieusement

echo '{"meta":{}}' | jq '.meta.version.tag'
# null

Utilisez // pour définir une valeur par défaut :

jq '.meta.version.tag // "unknown"'
jq '.tags // [] | length'

Les types qui ne correspondent pas

echo '{"count":"5"}' | jq '.count > 3'
# false - "5" string n'est pas > 3 number
# jq ne convertit pas silencieusement, contrairement à JavaScript
# ce qui est une qualité, même si ça surprend

jq '.count | tonumber | . > 3'
# true

? - ignorer les erreurs de type

kubectl get all -o json | jq '.items[].metadata.name?'

Les performances sur les gros fichiers

jq charge tout en mémoire. Sur un fichier de logs de plusieurs gigaoctets, votre serveur va regarder le plafond d'un air pensif avant de rendre l'âme. Pour les gros volumes, --stream traite le JSON en streaming sans tout charger :

jq --stream 'select(.[0][-1] == "level" and .[1] == "error")' huge-logs.json

C'est plus verbeux, moins lisible, et vous allez le détester. Mais votre serveur sera encore là le lendemain matin, ce qui est généralement l'objectif.

Débugger un filtre complexe

Ajoutez | debug n'importe où dans le pipe - jq affiche la valeur courante sur stderr sans interrompre le flux. C'est l'équivalent du console.log du pauvre, mais assumé, documenté, et disponible sans installer Node.js.

jq '.items[] | debug | select(.status.phase == "Running")'

À retenir

CommandePour quoi
jq .Formater du JSON
jq -r '.field'Extraire sans guillemets
jq -e '.field'Erreur si null
jq 'select(.x == "val")'Filtrer
jq '.items | map(.name)'Transformer chaque élément
jq 'sort_by(.count) | reverse'Trier
jq --arg k "$VAR" 'select(.x == $k)'Injecter une variable shell
jq -s '.'Plusieurs objets → tableau
jq 'del(.secret)'Supprimer un champ
jq '.field // "default"'Valeur par défaut si null

FAQ

Quelle différence entre `jq '.[]'` et `jq '.items[]'` ?

.[] itère sur tous les éléments d'un tableau ou toutes les valeurs d'un objet. .items[] itère sur le champ items spécifiquement. Si votre JSON est directement un tableau, .[]. Si c'est un objet avec un champ tableau, .monchamp[]. Si vous confondez les deux, jq vous le dira d'une manière peu aimable.

Comment débugger un filtre jq complexe ?

| debug n'importe où dans le pipe. jq affiche la valeur courante sur stderr. Ajoutez-en plusieurs, retirez-les quand ça marche, exactement comme vos console.log que vous commitez par accident.

jq vs python pour parser du JSON en shell ?

jq pour les opérations simples à moyennes. Python pour les transformations complexes qui nécessitent de la logique. La frontière est floue - quand votre filtre jq dépasse 3 pipes et que vous n'arrivez plus à le lire vous-même, c'est le moment de passer à Python et d'arrêter de vous battre contre l'outil.

Comment traiter du NDJSON ?

cat file.ndjson | jq '.' fonctionne directement - jq traite chaque objet JSON séparé par des newlines. Pas besoin de --slurp sauf si vous voulez les agréger dans un tableau.

`//` vs `?//` ?

// retourne la valeur de droite si la gauche est null ou false. ?// retourne la valeur de droite si la gauche produit une erreur de type. Pour les champs manquants, // suffit. Pour les erreurs de type, ?//. Si vous ne savez pas lequel utiliser, essayez // d'abord et ?// si ça casse encore.

Pour aller plus loin

  • jq manual - la référence complète, étonnamment bien écrite
  • jqplay - tester des filtres dans le navigateur, indispensable pour les filtres complexes
  • jq cookbook - recettes pour les cas courants
  • man jq - plus court que le manuel en ligne, bon pour une révision rapide avant un incident