jq
(paquet logiciel du même nom) est un outil en ligne de commande pour manipuler du JSON (quelle idée !).
Il m'apparaît que le meilleur moyen d'apprendre à utiliser jq
, c'est de pratiquer. C'est ce que nous allons faire dans ce shaarli.
Fichiers utilisés dans les exemples ci-dessous : cables.json et cablesDefs.json.
L'usage le plus simple de jq
est de rendre lisible (indentation, coloration syntaxique, etc.) un bout de JSON qui ne l'est pas (on dit aussi prettifyer, forrmatter) : jq '.' cables.json
.
Afficher la valeur / le contenu de l'attribut « features » de l'entité / objet racine : jq '.features' cables.json
.
jq '.type' cables.json
, le retour serait une unique chaîne de caractères.
Afficher uniquement l'attribut « slug » de l'attribut « properties » de chaque élément du tableau features « features » : jq '.features[].properties.slug' cables.json
.
grep
.
Afficher l'attribut « name » de l'attribut « properties » de chaque élément du tableau « features ». Afficher « n/a » (not available) si l'attribut n'existe pas ou a « null » comme valeur : jq '.features[].properties.name // "n/a"'
.
Notons que jq
propose des paramètres intéressants.
Ce que nous avons mis entre apostrophes dans les exemples précédents se nomme un filtre. Il est possible d'enchaîner les filtres. Pour ce faire, deux opérateurs existent :
jq '.features[].properties.slug , .features[].properties.color' cables.json
;jq '.features[].properties.slug | .' cables.json
.
Tout peut être stocké dans une variable à tout moment : jq '.features[].properties as $props | .' cables.json
.
$props | <traitement_ici>
ou $props.<nom_attribut>
(exemple : « $props.color »).
Si un filtre retourne plusieurs résultats et qu'un ou plusieurs filtres lui sont chaînés, ces derniers filtres s'appliqueront / seront exécutés pour chaque résultat retourné par le premier filtre. Il est donc possible de travailler sur chaque élément d'un tableau. Nous le faisons depuis le début : jq '.features[].properties.slug' cables.json
= jq '.features[] | .properties.slug' cables.json
. Il est donc possible d'exécuter un traitement sur chaque élément du tableau, comme une sorte de grep | xargs
.
jq '.features[0] | .properties.slug' cables.json
(« [0] » demande à travailler sur le premier élément du tableau) avant de le généraliser ;jq '.features[] | .
, c'est-à-dire « affiche-moi chaque élément du tableau « features » », on se rend compte que l'on perd l'entête (« { "type": "FeatureCollection", "features": [ ») et le pied (« ] } ») de notre fichier. Il faut comprendre qu'on les retrouvera jamais, qu'on peut jamais remonter au niveau supérieur. Ce serait comme utiliser grep essai | xargs <maCommande>
et vouloir que maCommande accède à ce qui n'a pas été grep-é (retenu par grep
). Si l'on tente d'enregistrer la racine dans une variable (« . as $racine ») et que l'on tente de l'afficher dans une itération (exemple : jq '. as $racine | .features[] | .properties.slug | $racine' cables.json
), l'ensemble du fichier JSON sera affiché autant de fois qu'il y a d'éléments dans le tableau « features », ce qui n'est pas ce que l'on veut. Bref, n'essaye pas. On ne sort pas d'une itération, tout enchaînement de filtres après un filtre qui émet plusieurs éléments JSON sera exécuté autant de fois qu'il y a d'éléments ;
Recherche sur la valeur d'un attribut / afficher l'élément du tableau « features » dont la valeur de l'attribut « slug » est « adria-1 » : jq '.features[] | select(.properties.slug=="adria-1")' cables.json
.
jq '.features[] | select(.properties.slug=="adria-1") | .properties.color' cables.json
ou, plus simplement : jq '.features[] | select(.properties.slug=="adria-1").properties.color' cables.json
.jq '.features[] | select(.properties.slug | test("adria-1"))
. Il est possible de positionner des drapeaux pour des recherches gourmandes (greedy), pour des recherches insensibles à la case, etc. Exemple concret d'utilisation.
On peut mettre en forme /formater la sortie (afficher plusieurs attributs, ajouter du texte libre, etc.). Pour chaque élément du tableau « features », je veux afficher une ligne de la forme \*\*SLUG\*\*: <slug> ~ \*COLOR\*: <color>.
. Les « * » sont du formatage Markdown (**gras**, *italique*). Le texte en majuscule, le caractère « ~ » et le caractère final (« . ») sont des bouts de texte arbitraires que je souhaite afficher. jq '.features[] | "**SLUG**: \(.properties.slug) ~ *COLOR*: \(.properties.color)."' cables.json
.
jq
d'interpréter une expression située au milieu d'une chaîne de caractères.
Et si je veux utiliser le contenu d'une variable shell dans un filtre ?
jq
entre apostrophes afin que le shell n'interprète pas la chaîne de caractères. Mais, on peut aussi l'écrire entre guillemets afin quil l'interpète. Exemple : maVariable='{ "test": "toto" }' ; jq ".features[].properties += $maVariable" cables.json
. Cela signifie aussi que tu vas devoir échapper tout ce que le shell pourrait interpréter : le « $ » d'une variable interne à jq
, les guillemets, etc. Pour peu que ton enchaînement de filtres jq
soit un peu costaud, ça va le rendre illisible. Je déconseille ;jq
propose les arguments --arg <nom_variable_interne> <contenu>
(le contenu sera enregistré comme une chaîne de caractères) et --argjson <nom_variable_interne> <bout_de_JSON>
(attention : le bout de JSON doit avoir une syntaxe valide !). Reprise de l'exemple précédent : maVariable='{ "test": "toto" }' ; jq --argjson varInterne "$maVariable" '.features[].properties += $varInterne' cables.json
.
Et si l'on veut modifier le flux JSON ? Il existe les opérateurs « |= » et « += ». Le premier modifie la valeur d'un attribut en écrasant l'exisant. Le second ajoute un élément dans la valeur d'un attribut.
jq '.features[] | .properties.color |= "yellow"' cables.json
;jq '.features[] | .properties += { "toto": "titi" } ' cables.json
;jq '.features[] | .properties += { "toto": .properties.color } ' cables.json
;jq '.features[] | .properties += { "toto": { "color": .properties.color } }' cables.json
.
Normalement, si tu suis, tu me traites de tricheur : les deux dernières commandes du point précédent n'ont pas renommé / déplacé l'attribut, mais l'ont simplement copié. Pour finir le travail, jq
propose la fonction native « del() » qui, comme son nom l'indique, permet de supprimer un attribut. Démo sur les deux exemples précédents : jq '.features[] | .properties += { "toto": .properties.color } | del(.properties.color)' cables.json
et jq '.features[] | .properties += { "toto": { "color": .properties.color } } | del(.properties.color)' cables.json
.
jq
ne propose pas un argument (genre « -o ») permettant d'enregistrer le résultat de son traitement dans un fichier. Donc, si l'on veut enregistrer le résultat dans le fichier source / d'origine, il faut utiliser l'outil sponge (une redirection shell, >
, viderait le fichier avant d'exécuter jq
).
Il est possible de travailler sur plusieurs fichiers JSON en même temps. Soit en les concaténant (cat fichier1 fichier2 | jq '.'
). Oui, ça va générer un fichier JSON invalide (plusieurs entités racines…), mais jq
est tolérant. Soit en ayant un fichier principal et des fichiers secondaires accessibles via des variables. Cette deuxième possibilité est très pratique quand on veut agrémenter / enrichir un fichier principal avec des informations disponibles dans un autre fichier (sans polluer le fichier principal avec une concaténation, donc).
jq --slurpfile maVariable cablesDefs.json '.features[] | . as $cable | $maVariable[] | select(.id==$cable.properties.slug).name as $cableName | $cable | .properties += { "name": $cableName }' cables.json
.
Plus haut, j'écris que si l'on itère sur le contenu d'un attribut avec jq
, alors on perd les attributs qui entourent celui sur lequel on itère. Avec ce "mode multi-fichiers", on peut reconstruire le JSON d'origine (sauf si trop d'élements sont passés à la trappe, bien entendu). Exemple (je reprends le cas cité ci-dessus malgré son absence de représentativité) :
jq '.features[] | .' cables.json > /tmp/json.tmp # dans json.tmp, on aurait donc uniquement les éléments du tableau « features », tout le reste aura disparu
cat <<EOF > /tmp/minJSONStruct # dans ce fichier, on met l'entête et le pied du fichier d'origine (cables.json), précisement ce qui a disparu
{
"type": "FeatureCollection",
"features": [
]
}
EOF
jq --slurpfile featuresList /tmp/json.tmp '.features += $featuresList' /tmp/minJSONStruct > cables.json # on ajoute les ex-éléments du tableau « features » au tableau « features » de l'entête/pied JSON, donc on reconstitue notre fichier cables.json d'origine.
Oui, je préfère passer par un fichier temporaire que par une variable (avec « --arg ») afin d'éviter les problèmes d'échappement de caractères pour le shell.
On peut aussi préserver la structure JSON autour du tableau. Pour ce faire, il faut contenir l'itération sur le tableau au sein d'une affectation. Exemple : pour renommer l'attribut « properties.color » d'un objet membre d'un tableau en « properties.toto », il faut que j'itère sur ce tableau sinon je ne peux pas récupérer la valeur de l'attribut afin de la transvaser. Pour que cette opération ne soit pas destructrice, je l'encadre dans une affectation : jq '. as $racine | .featuresDup |= $racine.features | del(.features) | .features += [ .featuresDup[] | .properties += { "toto": .properties.color } | del(.properties.color) ] | del(.featuresDup)' cables.json
. Si je n'avais pas dupliqué l'attribut « features », alors ce tableau contiendrait deux fois chaque élément : l'original et la version modifiée. Cette copie me permet de vider l'attribut « features » et d'utiliser sa copie pour itérer dessus et remplir « features » avec les objets modifiés.
Appliquer le même traitement à tous les éléments d'un tableau. Pour chaque élément du fichier cablesDefs.json, je veux récupérer une unique chaîne de caractères contenant tous les éléments du tableau « landing_points » sous la forme * <nom1>: <latitude+longitude>\n * <nom2>: <latitude+longitude>\n […]
. Premier essai : jq '.landing_points[] | " * \(.name): \(.latlon)"' cablesDefs.json
. Bien vu, mais on ne peut pas obtenir une unique chaîne avec join() puisque ce qui ressort du filtre, c'est des chaînes de caractères et qu'un join() ne peut être appliqué sur ce type d'objets.
jq '.landing_points | map(" * \(.name) :\(.latlon)") | join("\n")' cablesDefs.json
.
Il reste encore beaucoup de choses à découvrir : les boucles, les conditions, les fonctions persos, etc. Voir le manuel officiel de jq. Pour ma part, j'ai pu réaliser ce que je voulais (construire une carte des câbles sous-marins d'Internet) donc je m'arrête là.