cClaude.rocks ☕ Le blog

[Nouvelles technologies, sciences et coups de gueule…]

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 - 2022 | 🏠 Accueil du domaine | 🏡 Accueil du blog