cClaude.rocks ☕ Le blog

[Nouvelles technologies du libre, sciences et coups de gueule…]

Menu
đŸ˜€ Ce billet a Ă©tĂ© Ă©ditĂ© le : 2024-02-09

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: .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 +

኿


â„č 2006 - 2024 | 🏠 Accueil du domaine | 🏡 Accueil du blog