jq est un outil permet de manipuler des données JSON
depuis la ligne de commande et câest lâoutil parfait pour vos scripts.
Cet outil, trĂšs lĂ©ger, nâa pas de dĂ©pendance et permet de remplacer avantageusement les lignes de sed
, de awk
, de cut
et de grep
pour toute manipulation de données JSON
. Il permet de filtrer, découper, transformer et grouper des données avec une grande simplicité.
Le format JSON
a été adopté par la plupart des services web
, lâautomatisation des taches dâadministration de ces services passe donc par de grosse manipulation de donnĂ©es JSON
.
De plus en plus de commandes Linux offrent la possibilitĂ© dâavoir le rĂ©sultat au format JSON
, câest le cas par exemple de la commande ip
qui replace notamment ifconfig
.
Installation
jq est généralement disponible dans les dépÎts connus de votre systÚme (sauf bien-sur pour RedHat
)
Un exemple pour vous convaincre
Prenant par exemple :
ip link
On obtient un résultat du type :
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:d8:61:05:ad:fc brd ff:ff:ff:ff:ff:ff
3: wlo1: <BROADCAST,MULTICAST> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000
link/ether 48:a4:72:84:3c:d0 brd ff:ff:ff:ff:ff:ff
Qui, il faut le reconnaĂźtre nâest pas vraiment simple Ă dĂ©couperâŠ
ip -j link
Qui donne, un résultat certes moins lisible :
[{"ifindex":1,"ifname":"lo","flags":["LOOPBACK","UP","LOWER_UP"],"mtu":65536,"qdisc":"noqueue","operstate":"UNKNOWN","linkmode":"DEFAULT","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00"},{"ifindex":2,"ifname":"enp3s0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"mq","operstate":"UP","linkmode":"DEFAULT","group":"default","txqlen":1000,"link_type":"ether","address":"00:d8:61:05:ad:fc","broadcast":"ff:ff:ff:ff:ff:ff"},{"ifindex":3,"ifname":"wlo1","flags":["BROADCAST","MULTICAST"],"mtu":1500,"qdisc":"mq","operstate":"DOWN","linkmode":"DEFAULT","group":"default","txqlen":1000,"link_type":"ether","address":"48:a4:72:84:3c:d0","broadcast":"ff:ff:ff:ff:ff:ff"}]
Mais avec un petit jq, cela devient déjà beaucoup mieux :
ip -j link | jq .
Résultat :
[
{
"ifindex": 1,
"ifname": "lo",
"flags": [
"LOOPBACK",
"UP",
"LOWER_UP"
],
"mtu": 65536,
"qdisc": "noqueue",
"operstate": "UNKNOWN",
"linkmode": "DEFAULT",
"group": "default",
"txqlen": 1000,
"link_type": "loopback",
"address": "00:00:00:00:00:00",
"broadcast": "00:00:00:00:00:00"
},
{
"ifindex": 2,
"ifname": "enp3s0",
"flags": [
"BROADCAST",
"MULTICAST",
"UP",
"LOWER_UP"
],
"mtu": 1500,
"qdisc": "mq",
"operstate": "UP",
"linkmode": "DEFAULT",
"group": "default",
"txqlen": 1000,
"link_type": "ether",
"address": "00:d8:61:05:ad:fc",
"broadcast": "ff:ff:ff:ff:ff:ff"
},
{
"ifindex": 3,
"ifname": "wlo1",
"flags": [
"BROADCAST",
"MULTICAST"
],
"mtu": 1500,
"qdisc": "mq",
"operstate": "DOWN",
"linkmode": "DEFAULT",
"group": "default",
"txqlen": 1000,
"link_type": "ether",
"address": "48:a4:72:84:3c:d0",
"broadcast": "ff:ff:ff:ff:ff:ff"
}
]
Et obtenir la liste des adresses mac, par exemple, est finalement assez simple :
ip -j link | jq -r .[].address
ou pour la liste des interfaces :
ip -j link | jq -r .[].ifname
ĂlĂ©ment de base pour lâutilisation de la commande jq
Un rappel rapide de la syntaxe de jq pour commencer :
Syntaxe :
jq [options] filtre [fichier_d_entrée]
Les principales options :
-c
 : affiche les données de maniÚres compacte (sur une ligne).-r
 : affiche les chaßnes de maniÚre brute (raw), attention ne produit pas une sortie JSON.
Effet de lâoption « -r »
ip -j link | jq .[]
"lo"
"enp3s0"
"wlo1"
ip -j link | jq -r .[].ifname
lo
enp3s0
wlo1
Effet de lâoption « -c »
ip -j link | jq '[.[] | { ifname: .ifname, address: .address } ]'
[
{
"ifname": "lo",
"address": "00:00:00:00:00:00"
},
{
"ifname": "enp3s0",
"address": "00:d8:61:05:ad:fc"
},
{
"ifname": "wlo1",
"address": "48:a4:72:84:3c:d0"
}
]
ip -j link | jq -c '[.[] | { ifname: .ifname, address: .address } ]'
[{"ifname":"lo","address":"00:00:00:00:00:00"},{"ifname":"enp3s0","address":"00:d8:61:05:ad:fc"},{"ifname":"wlo1","address":"48:a4:72:84:3c:d0"}]
RÚgles élémentaires sur le filtre jq
Cette section ne prĂ©tend pas ĂȘtre exhaustive, pour cela je vous renvoie au manuel de jq mais elle vous donnera de bonne bases.
Voici les filtres utilisés plus haut :
-
.
 : formate et colorise le flux JSON (pretty print). Il faut comprendre le point comme correspondant Ă la racine du flux JSON sur lequel on ne fait rien. Câest donc le flux dâorigine qui est affichĂ©. A priori sans utilitĂ©, mais trĂšs pratique pour avoir un affichage lisible. -
.[]
 : ici on prend le flux et on dĂ©coupe le tableau se trouvant Ă la racine. La commande produira autant de flux JSON quâil y a dâentrĂ©e dans le tableau. Cela suppose que le fluxJSON
soit un tableau Ă sa racine.
Par exemple :
ip -j link | jq -r '.[]'
[ .[] ]
 : Ce filtre est identique Ă.
si le flux JSON à un tableau à sa racine. Mais on passe par deux étapes, la premiÚre consistant à décomposer le tableau, la seconde à le recomposer. Le premier[
est poussé dans le flux de retour, ensuite est traité.[]
comme décris ci-dessus et on referme le tableau avec le]
final. Du coup, jq considĂšre cela comme un tableau et remet les virgules entre les Ă©lĂ©ments Ă lâaffichage :
ip -j link | jq -r '[ .[] ]'
.[].attribut
 : ici on prend le flux et on dĂ©coupe le tableau se trouvant Ă la racine, puis on ne garde que lâattribut attribut.
On peut bien-sur utiliser la syntaxe :
ip -j link | jq '.[].address'
Mais lâutilisation de lâoption « -r » semble dans ce cas requise :
ip -j link | jq -r '.[].address'
Ă moins que ce que lâon souhaite câest un autre flux JSON
valide et dans ce cas on utilisera un autre filtre, permettant de reconstruire un tableau, comme vu précédemment :
ip -j link | jq '[ .[].address ]'
- '[ .[] | { attribut1: .attribut1valeur, attribut2: .attribut2valeur } ]' : Avec lâopĂ©rateur
|
il est possible de reconstruire des Ă©lĂ©ments diffĂ©rents du tableau dâorigine. Voir mĂȘme de les renommer.
Par exemple :
ip -j link | jq '[ .[] | { interface: .ifname, mac: .address } ]'
On obtient alors :
[
{
"interface": "lo",
"mac": "00:00:00:00:00:00"
},
{
"interface": "enp3s0",
"mac": "00:d8:61:05:ad:fc"
},
{
"interface": "wlo1",
"mac": "48:a4:72:84:3c:d0"
}
]
On notera que la valeur dâune entrĂ©e, peut ĂȘtre un Ă©lĂ©ment composĂ©Â :
ip -j address | jq '[ .[] | { interface: .ifname, mac: .address, addr_info: .addr_info } ]'
Dans ce dernier exemple, .addr_info est un tableau, ce que je vous laisse vérifier.
LâĂ©criture [ .[] | xxx ]
s'Ă©crit plus simplement map( xxx )
 :
ip -j address | jq 'map( { interface: .ifname, mac: .address, addr_info: .addr_info } )'
La fonction map applique ce qui décrit dans ces parenthÚses à chacun des éléments du tableau en entrée.
Comment filtrer des donnĂ©es JSON en fonction dâune valeur ?
Lâobjectif ici est dâobtenir pour chaque interface rĂ©seau de lâordinateur, le nom de lâinterface, son adresse mac et son adresse ip (disons IPv4) et rien dâautre.
La premiĂšre Ă©tape, pour comprendre, consiste Ă reprendre le dernier exemple et dâajouter un filtre en fonction de la famille (« family ») de lâinterface :
ip -j address | jq '[ .[] | { interface: .ifname, mac: .address, addr_info: [ .addr_info[] | select ( .family == "inet" ) ] } ]'
Que lâon peut Ă©galement Ă©crire :
ip -j address | jq '
map(
{
interface: .ifname,
mac: .address,
addr_info: [
.addr_info[] | select ( .family == "inet" )
]
}
)'
ou encore avec deux fois l'adresse map :
ip -j address | jq '
map(
{
interface: .ifname,
mac: .address,
addr_info: (
.addr_info | map(
select( .family == "inet" )
)
)
}
)'
Et hop, les informations relatives Ă IPv6 ont disparues.
Pour lâattribut addr_info:
met comme valeur associée : [ .addr_info[] | select ( .family == "inet" ) ] } ]
. En gros, on reconstruit un tableau, mais en ayant gardé que les entrées ayant comme valeur inet
pour le champ family
.
Cependant, il reste encore beaucoup de bruit compte tenu, en fait, on a pas besoin ici de reconstruire de tableau, puisquâen rĂ©alitĂ© seule lâadresse IP nous intĂ©resse, et donc seul lâattribut local
est pertinent.
ip -j address | jq 'map( { interface: .ifname, mac: .address, ipv4: .addr_info[] | select ( .family == "inet" ) | .local } )'
Du coup, pour IPv6, câest ?
ip -j address | jq 'map( { interface: .ifname, mac: .address, ipv6: .addr_info[] | select ( .family == "inet6" ) | .local } )'
Pas vraiment, car la commande ip
remonte plusieurs familles dâadresse avec IPv6. Il nous faut donc un peu plus dâinformation.
Pour comprendre comment résoudre cela, il faut donc repartir de :
ip -j address | jq '[ .[] | { interface: .ifname, mac: .address, addr_info: [ .addr_info[] | select ( .family == "inet6" ) ] } ]'
Ou en version plus lisible :
ip -j address | jq 'map(
{
interface: .ifname,
mac: .address,
addr_info: [
.addr_info[] | select ( .family == "inet6" )
]
}
)'
Il existe dâautre de nombreux « sĂ©lecteurs » par exemple contains()
:
Pour valeur de type chaßne de caractÚre (« string ») :
jq -c '.[] | select( .<key> | contains("<value>"))' <filename.json>
Pour valeur de type entier :
jq -c '.[] | select( .<key> | contains( <value> ))' <filename.json>
Lorsqu'on est certain de n'avoir qu'un résultat, on peut simplement écrire:
echo '[{"id":1,"value":"str1"},{"id":2,"value":"str2"}]' | jq -c '.[] | select( .id == 2 )'
Car ici le champ id est a priori unique (câest lâidĂ©e dâun identificateur).
Mais avec contains()
, il est préférable de reconstruire un tableau :
echo '[{"id":1,"value":"str1"},{"id":2,"value":"str2"},{"id":3,"value":"truc"}]' | jq -c '[ .[] | select( .value | contains("str") ) ]'
ou en utilisant map :
echo '[{"id":1,"value":"str1"},{"id":2,"value":"str2"},{"id":3,"value":"truc"}]' | jq -c 'map( select( .value | contains("str") ) )'
Cela permet dâavoir un rĂ©sultat qui est encore un flux JSON
bien formé.
Cependant dans certain car on souhaitera faire un traitement pour chaque résultat :
while read -r json ; do echo "FAIRE: '${json}'" ; done < <( echo '[{"id":1,"value":"str1"},{"id":2,"value":"str2"},{"id":3,"value":"truc"}]' | jq -c '.[] | select( .value | contains("str") )' )
ou encore :
while read -r json
do
echo "FAIRE: '${json}'" # Un traitement avec lâextrait au format JSON
done < <(
echo '[{"id":1,"value":"str1"},{"id":2,"value":"str2"},{"id":3,"value":"truc"}]' |
jq -c '.[] | select( .value | contains("str") )' # ICI le « -c » est obligatoire !
)
Recomposer le flux
Regardons ce que font les lignes suivantes :
echo '[{"a1":"v1","a2":"v2"},{"aa1":"vv1","aa2":"vv2"},{"aaa1":"vvv1","aaa2":"vvv2"}]' | jq 'to_entries'
qui retourne :
[{"key":0,"value":{"a1":"v1","a2":"v2"}},{"key":1,"value":{"aa1":"vv1","aa2":"vv2"}},{"key":2,"value":{"aaa1":"vvv1","aaa2":"vvv2"}}]
et
echo '{"a1":"v1","a2":"v2"}' | jq 'to_entries'
qui retourne :
[{"key":"a1","value":"v1"},{"key":"a2","value":"v2"}]
On comprend comment to_entries
converti les couples (clĂ©,valeur) en crĂ©ant pour chacune dâelle un entrĂ© key
contenant le nom de la clé et entrés value
contenant la valeur associée.
Ensuite Ă lâaide de la fonction map()
on peut par exemple crĂ©er un tableau Ă©lĂ©ments, dont chacun de nos Ă©lĂ©ments est lâinversion des couples dâorigines.
echo '{"a1":"v1","a2":"v2","a3":"v3"}' | jq 'to_entries | map( {(.value|tostring) : .key } )'
A savoir :
[{"v1":"a1"},{"v2":"a2"},{"v3":"a3"}]
A notĂ© que pour lâinstant on a un tableau comme Ă©lĂ©ment racine et si lâon souhaite retrouver une structure similaire Ă celle du dĂ©part, il faut ajouter un appel Ă add
comme suit :
echo '{"a1":"v1","a2":"v2","a3":"v3"}' | jq 'to_entries | map( {(.value|tostring) : .key } ) | add'
qui produit :
{"v1":"a1","v2":"a2","v3":"a3"}
Attention, cependant si les valeurs dâorigine ne sont pas toutes diffĂ©rentes, il nâest pas possible dâavoir une bijection.
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq 'to_entries | map( {(.value|tostring) : .key } ) | add'
Et lĂ , il manque une entrĂ©e puisquâelle a Ă©tĂ© Ă©crasĂ©e lors du dernier add
.
{"v1":"a1","v2":"a3"}
Pour ne pas perdre de valeur, on peut utiliser quelque chose comme :
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq 'to_entries | map( {(.value) : {(.key):null} } ) | reduce .[] as $item ({}; . * $item) | to_entries | map({key:.key, value:(.value|keys)}) | from_entries'
de maniÚre plus lisible :
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq '
to_entries |
map( {(.value) : {(.key):null} } ) |
reduce .[] as $item ({}; . * $item) |
to_entries |
map({key:.key, value:(.value|keys)}) |
from_entries'
qui donne :
{"v1":["a1"],"v2":["a2","a3"]}
Je ne vais pas rentrer dans le détail du fonctionnement de ce dernier exemple, qui introduit les fonctions reduce
et from_entries
, lâusage de null
et utilise pas mal dâastuces.
Mais je vous propose dâessayer le code ci-dessous pour voir les diffĂ©rentes Ă©tapes :
(
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq -c 'to_entries'
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq -c 'to_entries | map( {(.value) : {(.key):null} } )'
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq -c 'to_entries | map( {(.value) : {(.key):null} } ) | reduce .[] as $item ({}; . * $item)'
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq -c 'to_entries | map( {(.value) : {(.key):null} } ) | reduce .[] as $item ({}; . * $item) | to_entries'
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq -c 'to_entries | map( {(.value) : {(.key):null} } ) | reduce .[] as $item ({}; . * $item) | to_entries | map({key:.key, value:(.value|keys)})'
echo '{"a1":"v1","a2":"v2","a3":"v2"}' | jq -c 'to_entries | map( {(.value) : {(.key):null} } ) | reduce .[] as $item ({}; . * $item) | to_entries | map({key:.key, value:(.value|keys)}) | from_entries'
)
Les trucs stupides
Inverser les clĂ©s et valeurs nâa pas de sens si au moins une valeur est un tableau, mais cela ne produit pas dâerreur :
echo '[{"a1":"v1","a2":"v2"},{"aa1":"vv1","aa2":"vv2"},{"aaa1":"vvv1","aaa2":"vvv2"}]' | jq 'to_entries | map( {(.value|tostring) : .key } ) | add'
Commandes utiles
- Afficher le nom de lâinterface, lâadresse mac et lâadresse IPv4 des interfaces actives :
ip -j address | jq 'map( { interface: .ifname, mac: .address, ipv4: .addr_info[] | select ( .family == "inet" ) | .local } | select ( .ipv4 | contains( "127.0.0." ) | not ) )'
- Liste des adresses IPv4 actives de la machine (sauf la boucle locale)
ip -j address | jq -r '.[].addr_info[] | select( .family == "inet" ) | select( .local | contains( "127.0.0." ) | not ) | .local'
- RĂ©cupĂ©rer lâadresse IPv4 dâune interface donnĂ©e
iface=enp3s0 # dĂ©fini le nom de lâinterface
ip -j add | jq -r ".[] | select( .ifname == \"${iface}\" ) | .addr_info[] | select ( .family == \"inet\" ) | .local"
Pour les anciennes versions de ip
, on doit bricoler avec quelque chose comme :
iface=enp3s0 # dĂ©fini le nom de lâinterface
ip addr show "${iface}" 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -f1 -d'/'
- Liste des interfaces de la machine (toutes)
ip -j link | jq -r .[].ifname
- Liste des interfaces de la machine (toutes sauf la boucle locale)
ip -j link | jq -r '.[] | select( .ifname == "lo" | not ) | .ifname'
- Liste des interfaces de la machine (uniquement celles qui sont actuellement actives)
ip -j address | jq -r 'map( { interface: .ifname, mac: .address, ipv4: .addr_info[] | select ( .family == "inet" ) | .local } )[] | select ( .ipv4 | contains( "127.0.0." ) | not ) | .interface'
En savoir +
- Un article sur JSON avec RedHat qui décris en particulier comment compiler
jq
, ce qui dans un environnement professionnel du fait des restrictions de sĂ©curitĂ© peut ĂȘtre la seule solution pour installer cet outil, - Le code source de jq,
- Le manuel de jq,
- Documentation sur le remodelage de
JSON
avec jq
኿