Awk comparé aux outils Unix et Python

Magazine
Marque
GNU/Linux Magazine
Numéro
273
Mois de parution
janvier 2025
Spécialité(s)


Résumé

Vous connaissez probablement déjà les outils classiques installés sur toutes les distributions GNU/Linux, et Python, langage populaire aujourd’hui. Découvrez Awk, un (vieux) langage qui pourrait vous servir lorsque les premiers sont trop limités et qu’un script Python vous semble surdimensionné.


Body

Un script shell permet de réaliser des traitements basiques sur des lignes de texte grâce à un ensemble d'outils Unix (cut, grep, sort, wc, tr...). Cependant, lorsque les traitements deviennent plus complexes, un script shell avoue ses limites : faire des calculs est laborieux, le code est généralement peu structuré, etc. Dans ce cas, un script dans un langage plus évolué (comme Python) sera plus adapté. Cet article présente une solution intermédiaire : Awk, un langage créé par Alfred Aho, Peter Weinberger et Brian Kernighan. La version initiale date de 1977, révisée dans les années 80. Bien que toujours disponible, son heure de gloire semble passée. Il est spécialisé dans le traitement de lignes et leur transformation, ce qui le rapproche du comportement des enchaînements de filtres dans un terminal.

L'article présente Awk, en le comparant aux outils Unix classiques et à du code Python.

1. Disponibilité

En général, plusieurs interpréteurs Awk sont disponibles dans une distribution GNU/Linux (souvent gawk et mawk) et il est bien possible qu'il soit déjà installé. Tapez juste awk dans un terminal pour vérifier.

Par exemple, si mawk est installé, le début de la sortie sera :

$ awk
Usage: mawk [Options] [Program] [file ...]
 
Program:
    The -f option value is the name of a file containing program text.
[...]

Dans le cas contraire, vous pouvez en installer un depuis les paquets de votre distribution. Ils sont nommés mawk ou gawk sous Debian et Arch Linux (mawk est dans les dépôts AUR). D'autres interpréteurs plus récents existent (nawk, goawk).

2. Utilisation

Awk a été conçu pour l'écriture de one-liners, mais il est aussi possible d'écrire un programme Awk pour l'exécuter.

L'usage en tant que one-liner est :

$ awk ‘le_programme’ les_fichiers_à_traiter

L'usage avec un programme Awk écrit dans un fichier est :

$ awk -f programme.awk les_fichiers_à_traiter

Dans le cas où le programme est écrit sur la ligne de commande, il faut l'entourer de guillemets simples, car le programme va utiliser le caractère dollar (par exemple, la variable $0). Si ce sont des guillemets doubles qui sont utilisés, le programme ne fonctionnera pas correctement, car le shell va tenter d'interpréter les variables préfixées par le dollar comme des variables du shell avant d'exécuter le programme.

Ceci est un comportement classique du shell et n’est donc pas une spécificité liée à Awk.

Pour la suite de l’article, nous allons utiliser un fichier nommé distribs.txt contenant les données suivantes :

Raspbian linux 2012
Archlinux linux 2002
Debian linux 1993
FreeBSD bsd 1993
pfSense bsd 2004

2.1 Imiter cat

Par exemple, pour faire l'équivalent de cat distribs.txt :

$ awk '{print $0}' distribs.txt

Comme le montre la figure 1, $0 représente l'enregistrement total (qu'on peut assimiler ici à la ligne). $1, le contenu du premier champ, $2 le contenu du deuxième champ, etc. Le contenu du dernier champ est $NF. NF signifie Number of Fields. On peut donc obtenir astucieusement le contenu de l'avant-dernier champ avec $(NF-1).

awk compare fig 01-s

Fig. 1 : Représentation des données accessibles d’un enregistrement.

La plupart des langages de programmation démarrent les listes à partir de 0 (comme C, Java, Lisp, etc.) ; Python fait aussi partie de cette catégorie. Quelques langages, comme Awk, commencent à partir de 1. $0 représentant l'enregistrement complet, le premier élément ne peut pas lui aussi commencer à 0. Parmi les autres langages commençant aussi à 1, R ou Lua sont probablement les plus connus. Julia permet de choisir n'importe quel entier, mais utilise 1 comme index par défaut.

2.2 Imiter cut

Afficher le premier et le troisième champ avec cut avec les paramètres longs :

$ cut --delimiter " " --fields 1,3 distribs.txt

Avec les paramètres courts :

$ cut -d " " -f 1,3 distribs.txt

Avec Awk :

$ awk '{print $1, $3}' distribs.txt

Alors que cut ne fait que filtrer des champs en gardant le même ordre, Awk permet de changer facilement l'ordre des champs ou d'insérer du texte dans la ligne qui sera affichée :

$ awk '{print $3, "est l’année de création de", $1}' distribs.txt

cut permet de choisir le délimiteur de champ. Par défaut, Awk considère les espaces blancs comme délimiteur. Les plus courants sont l'espace et la tabulation. Il est possible de changer la valeur par défaut avec la valeur FS (pour Field Separator). Le paramétrage de FS se fait dans un bloc BEGIN qui sera exécuté avant les traitements et donc pris en compte pour chacun d’eux. C'est pratique lors de traitement de CSV (ayant la virgule pour séparateur) ou le fichier /etc/passwd (ayant le caractère « : » pour séparateur).

Ainsi :

$ awk 'BEGIN{FS=":"} {print $1}' /etc/passwd

est équivalent à :

$ cut -d":" -f1 /etc/passwd

En python, csv.reader() permet d'avoir un résultat équivalent avec le code suivant :

# le module csv permet de lire et écrire des données
# dans un format de type csv
import csv
with open("/etc/password") as f:
    reader = csv.reader(f, delimiter=":")
    for ligne in reader:
        print(ligne[0])

2.3 Imiter wc -l

La commande wc -l permet d’afficher le nombre de lignes d’un fichier (ou de l’entrée standard). Comparons avec une version minimale écrite en Awk :

$ wc -l distribs.txt
4 distribs.txt
$ awk 'END{print NR}' distribs.txt
4

Mais quel est ce prodige ? Le bloc END est l’équivalent du bloc BEGIN : il permet de faire un traitement sur des informations que l'on aura qu'une fois le traitement terminé. Les blocs BEGIN et END sont toujours facultatifs. Une fois parcouru l’ensemble des lignes du fichier, le bloc END est exécuté. Il affiche la valeur NR (signifiant Number of Records). Par défaut, le séparateur d’enregistrement est le retour à la ligne. C’est pourquoi, dans la plupart des cas, un enregistrement est équivalent à une ligne.

Si on souhaite simuler exactement la sortie de wc, il faut utiliser la valeur FILENAME qui est initialisée automatiquement par Awk :

$ awk 'END{print NR, FILENAME}' distribs.txt
4 distribs.txt

Un programme équivalent en Python serait :

NOM_FICHIER = "distribs.txt"
with open(NOM_FICHIER) as f:
    quantite = sum(1 for _ in f)
    print(f"{quantite} {NOM_FICHIER}")

2.4 Imiter head

Par défaut, head affiche les dix premières lignes d'un fichier.

Awk dispose de conditions, ce qui permet de réaliser un équivalent facilement :

$ awk 'NR <= 10 {print $0}' distribs.txt

À chaque ligne du fichier, si la condition ‘NR <= 10’ est vraie, alors la ligne sera affichée.

Une solution possible en Python serait :

with open("distribs.txt") as f:
    for index, ligne in enumerate(f.readlines()):
        if index < 10:
            # le paramètre end permet de définir la chaîne à ajouter à la fin
            # de la ligne. Ici, cela permet d’éviter d’avoir un second retour à la ligne
            print(ligne, end='')
        else:
            break

Les tests en début de bloc (comme le NR <= 10 précédent) permettent de vérifier facilement des égalités (ou inégalités) sur des nombres, des chaînes de caractères, etc., avec Awk. La syntaxe est celle que l'on retrouve dans la plupart des langages. Il n'y a pas de surprises de ce côté-là.

2.5 Imiter grep

La commande grep affiche les lignes correspondant à un motif passé en paramètre. Awk est conçu pour filtrer les enregistrements avec des conditions et exécuter les blocs de traitement lorsque la condition est remplie, donc réaliser un équivalent à grep est facile. La condition peut être une chaîne exacte :

$ awk '$2 == "linux" {print $0}' distribs.txt

Avec la commande précédente, trois lignes seront affichées : celles qui ont « linux » dans la deuxième colonne.

La condition peut aussi être une expression rationnelle. Dans ce cas, le motif à rechercher doit être encadré par le caractère /.

La syntaxe des expressions rationnelles Awk est celle qui est communément utilisée et qu’on retrouve en Perl, Python, etc. L’article ne peut s’attarder à expliquer les expressions rationnelles, car elles sont un sujet à part entière. Pour le besoin de l’article, on considérera uniquement le caractère $ qui signifie une fin de ligne.

Si on souhaite connaître tous les utilisateurs ayant Zsh comme shell par défaut, il faut afficher toutes les lignes du fichier /etc/password contenant zsh en fin de ligne : en effet, le shell utilisé est le dernier champ de ce fichier. grep utilise le paramètre -E pour activer les expressions rationnelles étendues, ce qui donne :

$ grep -E "zsh$" /etc/passwd

L’équivalent avec Awk peut être obtenu avec :

$ awk '/zsh$/ {print $0}' /etc/passwd

Un équivalent Python serait :

# re est le module dédié aux expressions rationnelles
# (re pour regular expression)
import re
 
with open("/etc/passwd") as f:
    for ligne in f.readlines():
        if re.search("zsh$", ligne):
            print(ligne)

Awk permet aussi un filtrage sur des champs spécifiques alors que grep filtre sur la ligne entière. grep va donc nécessiter une expression rationnelle plus compliquée pour obtenir un comportement équivalent. Le filtrage de Awk sur un champ s’obtient en préfixant l'expression rationnelle du numéro du champ souhaité. Par exemple, afficher uniquement le premier et le deuxième champ uniquement si le premier champ termine par « linux » :

$ awk '$1 ~ /linux/ {print $1, $2}' distribs.txt

Dans ce cas, seule la ligne concernant « Archlinux » sera affichée puisque c’est la seule avec le motif linux contenu dans la première colonne. Contrairement à l'exemple précédent, le terme « linux » présent dans le deuxième champ n’est pas pris en compte.

Un équivalent Python serait :

import csv
import re
 
with open("distribs.txt") as f:
    reader = csv.reader(f, delimiter=":")
    if re.search(".*linux", l[0]):
        print((l[0], l[1]))

2.6 Imiter grep --invert-match

L’option --invert-match (ou son équivalent court -v) permet d'afficher uniquement les lignes qui ne correspondent pas à l'expression rationnelle passée en paramètre. Le caractère ! permet de faire la même chose avec Awk.

Sur une ligne complète (donc seuls les systèmes BSD seront affichés) :

$ awk '! /linux/ {print $0}' distribs.txt

Uniquement sur le premier champ (donc toutes les lignes seront affichées sauf celle contenant « FreeBSD ») :

$ awk '$1 !~ /BSD/ {print $0}' distribs.txt

3. Transformer les données

3.1 Sur des chaînes

Il est possible de mettre en majuscule avec toupper() et en minuscule avec tolower().

$ head -n 3 /etc/passwd | awk '{ print toupper($0) }'
ROOT:X:0:0:ROOT:/ROOT:/BIN/BASH
DAEMON:X:1:1:DAEMON:/USR/SBIN:/USR/SBIN/NOLOGIN
BIN:X:2:2:BIN:/BIN:/USR/SBIN/NOLOGIN

C'est équivalent aux méthodes upper() et lower() disponibles en Python.

Il est possible de séparer des données à l'intérieur d'un champ avec split(), connaître la longueur d'un champ avec length(), trouver la première occurrence avec match(), etc.

3.2 Sur des nombres

Il est possible de faire des calculs sur des champs directement :

$ awk '{print $1, 2025 - $3}' distribs.txt

Cela affichera le premier champ et le résultat de la soustraction entre l’année actuelle et le troisième champ (c’est-à-dire l’âge de la première publication).

Awk embarque aussi des fonctions mathématiques comme int(), exp(), cos(), etc. Ce sont des fonctions équivalentes de celles présentes dans le module math de Python. Awk dispose aussi une fonction rand(). Son équivalent Python est random.random(), donc hors du module math, mais on ne va pas chipoter.

4. Boucle et condition

Nous avons vu précédemment comment Awk permet d’avoir une condition pour entrer dans un bloc. Il est aussi possible d’écrire des conditions à l’intérieur d’un bloc. Dans ce cas, la syntaxe de Awk est directement inspirée de celle du C. C’est aussi le cas pour les boucles for et while.

Exemple :

# itérer sur chaque champ et
# afficher ceux dont la longueur est supérieure à 5
for(i = 1 ; i <= NF ; i++) {
    if (length($i) > 5) {
        print $i
    }
}

Elle utilise donc des accolades pour définir des blocs, comme les scripts shell et le C (contrairement à Python qui utilise l'indentation).

5. Formater la sortie

Il est possible de changer le séparateur de sortie avec la constante OFS (pour Output File Separator) :

$ head -n 3 /etc/passwd | awk 'BEGIN{FS=":"; OFS="|"} { $1=$1; print $0 }'
root|x|0|0|root|/root|/bin/bash
daemon|x|1|1|daemon|/usr/sbin|/usr/sbin/nologin
bin|x|2|2|bin|/bin|/usr/sbin/nologin

Un formatage plus fin est réalisable grâce à la fonction printf(). Son usage est calqué sur la fonction éponyme en C. L'intérêt était probablement plus évident lorsque Awk servait à générer des rapports, mais je doute qu'il soit encore utilisé pour cela aujourd'hui. printf est aussi disponible comme commande shell, avec des capacités de formatage équivalentes en C ou Awk. L'équivalent en Python est print() (comme déjà vu dans les exemples précédents). Cependant, en Python, l'utilisation de f-strings est maintenant privilégiée au formatage façon C. Les f-strings ont l’avantage d’être bien plus lisibles.

6. Utilisation et limites du langage

Le mode de fonctionnement est très orienté filtre, comme un remplaçant puissant des outils Unix chaînés par des pipes. Chaque bloc de traitement peut avoir un filtre, chacun différent. On peut avoir une succession de traitements possibles et une ligne peut correspondre à plusieurs blocs (cf. figure 2).

awk compare fig 02-s 0

Fig. 2 : Ordre de traitement des blocs. Le bloc BEGIN (et END) est toujours exécuté au début (et à la fin), quel que soit l’endroit où ils sont déclarés. Il est cependant conseillé de les définir au début (et à la fin) du script.

Vouloir l'utiliser différemment oblige à tordre l'outil. Par exemple, il n'est pas fait pour trier les lignes puisque Awk les traite une par une. Dans ce cas, repasser par des commandes shell peut être une meilleure stratégie (typiquement avec la commande sort).

Selon Brian Kernighan [1], l'idée était d'avoir un outil pour faire des appels simples :

« Our model was that an invocation would be one or two lines long, typed in and used immediately. [...] choices that made it possible to write one-liners. »

Cela a poussé à certains choix comme la conversion implicite de types. Assez logiquement, cela se passe aussi mal que dans d'autres langages avec un typage faible :

Cas

Opération

Résultat

Commentaire

1

"hello" 2

hello2

concaténation de chaîne de caractères

2

"1" + "2"

3

addition (uniquement sur les éléments pouvant être convertis en nombre)

3

"hello" + "1" + 2

3

pas d'erreur

Ce genre de comportement n'arrivera pas en Python, puisqu'il lèvera une exception en cas de tentative de concaténation de chaînes et de nombres :

>>> "texte" + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

Les variables d'environnement de localisation (LANG, LC_...) modifient aussi le comportement de Awk.

Ainsi, dans le cas de l'addition, la variable d'environnement LANG modifie l'interprétation des données. Par exemple, avec LANG=C, le séparateur pour les nombres à virgule est le point et non la virgule.

Cas

Variable d'env.

Opération

Résultat

Commentaire

4

LANG=C

"1.3" + 2

3.3

avec séparateur "."

5

LANG=C

"1,3" + 2

3

avec séparateur ","

6

LANG=fr_FR.utf-8

"1,3" + 2

3,3

avec séparateur ","

Cette sensibilité aux variables d'environnement peut rendre le comportement d'un script non reproductible d'une machine à l'autre ou d'un utilisateur à l'autre.

Les conversions implicites de type sont un comportement général de Awk et se retrouvent donc aussi lors de comparaisons. Par exemple, toutes les conditions suivantes sont vraies, car converties en chaînes de caractères avant la comparaison :

Cas

Test

Commentaire

7

1 == "1"

 

8

"10" < 2

comparaison lexicographique entre "10" et "2"

9

"abc" > 1

idem, "a" précède "1"

En Python, le cas 7 serait évalué à « faux », car les types de données sont différents. Dans les cas 8 et 9, une exception serait levée en Python lors de l'exécution d'une comparaison de ce type :

>>> "10" < 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of ‘str' and 'int'

Enfin, le découpage des champs fait par Awk peut être problématique : par exemple, si le séparateur est l'espace et qu'il existe aussi des espaces dans les données, alors le découpage sera erroné. Par défaut, entourer la donnée de guillemets ne résoudra pas le problème. Les contournements possibles sont peu fiables ou lisibles. En 2023, Brian Kernighan a décidé d'ajouter une gestion spécifique du format CSV. Des implémentations comme gawk ou goawk disposent d'un paramètre --csv pour avoir un comportement équivalent.

Sur ce point, l'utilisation du module csv de Python permet d'avoir un découpage correct automatiquement (comme montré dans l'exemple donné dans la section 2.2).

Conclusion

De mon point de vue, Awk n'est pas réellement un concurrent des outils Unix ou de Python, mais bien un outil complémentaire lorsque les utilitaires Unix ne répondent pas directement au besoin et que le code ne sera pas intégré dans un ensemble plus grand, auquel cas Python permettrait d'avoir un code plus maintenable dans le futur. Quelqu'un connaissant Perl ne ressentirait pas le besoin d'utiliser Awk, étant donné que Perl permet aussi de faire des one-liners facilement.

Référence

[1] Alfred Aho, Brian Kernighan, Peter Weinberger, The AWK Programming Language, page 181.



Article rédigé par

Par le(s) même(s) auteur(s)

Réalisation de cartogrammes avec ScapeToad

Magazine
Marque
SysOps Pratique
Numéro
69
Mois de parution
janvier 2012
Spécialité(s)
Résumé
Qui n'a jamais vu une carte déformée en fonction d'une statistique pour mieux la mettre en évidence ? On pense typiquement aux cartes montrant la sous-nutrition ou le P.I.B par pays. Ces cartes volontairement déformées s'appellent des cartogrammes. Cet article montre comment il est possible d'en réaliser sous Linux. Pour cela, nous allons utiliser ScapeToad [ST], un logiciel libre, écrit en Java. Pour illustrer cette possibilité, nous allons représenter le pourcentage d'utilisateurs d'Internet par pays en 2010.

Processus de publication chez Debian et Ubuntu

Magazine
Marque
GNU/Linux Magazine
Numéro
118
Mois de parution
juillet 2009
Résumé
Debian a été créé en 1993. Depuis, de nombreuses distributions, à la durée de vie et au succès plus ou moins importants, en ont été dérivées. Ubuntu est l'une d'elles. C'est aujourd'hui une des distributions Linux les plus populaires et les plus médiatisées.Dire qu'une distribution est dérivée d'une autre signifie que le code source, les outils et le système de paquets de la distribution d'origine ont été réutilisés. La nouvelle distribution est libre de maintenir un lien avec la distribution d'origine ou de s'en éloigner définitivement. Ubuntu n'a jamais caché sa filiation et s'en est même servi pour asseoir sa crédibilité à ses débuts. Elle a d'ailleurs choisi de rester proche de Debian d'un point de vue technique. L'objectif de cet article est de montrer les différences entre les modes de publication de Debian et d'Ubuntu, puis de voir comment Ubuntu se base sur Debian.

Les derniers articles Premiums

Les derniers articles Premium

Bun.js : l’alternative à Node.js pour un développement plus rapide

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Dans l’univers du développement backend, Node.js domine depuis plus de dix ans. Mais un nouveau concurrent fait de plus en plus parler de lui, il s’agit de Bun.js. Ce runtime se distingue par ses performances améliorées, sa grande simplicité et une expérience développeur repensée. Peut-il rivaliser avec Node.js et changer les standards du développement JavaScript ?

PostgreSQL au centre de votre SI avec PostgREST

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Dans un système d’information, il devient de plus en plus important d’avoir la possibilité d’échanger des données entre applications. Ce passage au stade de l’interopérabilité est généralement confié à des services web autorisant la mise en œuvre d’un couplage faible entre composants. C’est justement ce que permet de faire PostgREST pour les bases de données PostgreSQL.

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 68 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous