cClaude.rocks ☕ Le blog

L'informatique et les nouvelles technologies

Menu
Ce billet a été édité le : 2019-10-24

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 flux JSON 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: .attribut1, attribut2: .attribut2 } ]' : 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.

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 '
    [
        .[] |
        {
            interface: .ifname, mac: .address, addr_info: [
                .addr_info[] | 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 '[ .[] | { interface: .ifname, mac: .address, ipv4: .addr_info[] | select ( .family == "inet" ) | .local } ]'

Du coup, pour IPv6, c’est ?

ip -j address | jq '[ .[] | { 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 '[ .[] |
   {
        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 appriori 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") ) ]'

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 '[ .[] | { 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 celle qui sont actuellement actives)
ip -j address | jq -r '[ .[] | { interface: .ifname, mac: .address, ipv4: .addr_info[] | select ( .family == "inet" ) | .local } ] | .[] | select ( .ipv4 | contains( "127.0.0." ) | not ) | .interface'

En savoir +

ᦿ


ℹ 2006 - 2020 | 🕸 Retour à l'accueil du domaine | 🏡 Retour à l'accueil du blog