mercredi 17 février 2010

awk (introduction)

Introduction à awk

Awk est une commande qui agit sur les différentes lignes d'un fichier textes, les unes après les autres, chaque ligne étant considérée comme un enregistrement formé de différents champs séparés par un espace (par défaut).

La syntaxe est la suivante:

awk 'condition {action}[;condition {action}]' fichier

L'ensemble condition {action} constitue une instruction.

L'action est exécutée si le condition est vérifiée. L'un ou l'autre peuvent être absents. L'action par défaut est print.

La totalité de la ligne en cours de traitement se trouve dans la variable $0, et les différents champs dans des variables $i (i=1, 2, ......NF). NF (Number of Field) est une variable interne qui contient le nombre de champ de la ligne lue.

Nous allons maintenant sur quelques exemples montrer quelques manipulations simples qui peuvent être effectuées avec awk.

Afficher le contenu d'un fichier

$ awk '1' toto

Je m'appelle Toto.

Je vais à l'école avec Tata

Nous allons à pied car ce n'est pas loin.

Elle m'aime et ça c'est cool!

Et oui Tata aime Toto et Toto aime Tata.

La vie est belle car Tata habite près de Toto.

Ils se voient tous les jours!

La condition 1 est toujours vérifiée et toutes les lignes du fichier sont donc imprimées (action par défaut). Nous avons émulé la commande cat.

Imprimer la ou les lignes contenant une expression régulière

$ awk '/vais/' toto

Je vais à l'école avec Tata

La condition est cette fois la présence d'une expression régulière dans l'input, expression régulière qui ici est une expression régulière constante:  le mot « vais ». Nous avons émulé grep.

Imprimer la ou les lignes dont un champ contient une expression régulière

$ awk '$6 ~ /ta/' toto

Je vais à l'école avec Tata

La vie est belle car Tata habite près de Toto.

On a sélectionné les lignes dont le champ 6 (le sixième mot) contient « ta ».

Imprimer des lignes sur base d'une condition de départ et d'une condition d'arrêt

$ awk '/vais/,/aime/' toto

Je vais à l'école avec Tata

Nous allons à pied car ce n'est pas loin.

Elle m'aime et ça c'est cool!

Quand une ligne contenant « vais » est rencontrée, elle est imprimée et toutes les lignes suivantes jusqu'au moment où une ligne contenant « aime » est rencontrée. Cette ligne est encore imprimée mais plus les suivantes

Imprimer les lignes sur base d'une condition de départ et d'une condition d'arrêt, mais en excluant les lignes vérifiant ces conditions

awk '/aime/ {p=0} ; p ; /vais/ {p=1}' toto

Nous allons à pied car ce n'est pas loin.

Il y a cette fois 3 instructions séparées par des points-virgules  et qui sont exécutées de gauche à droite pour chacune des lignes du fichier. L'action de la 3ième instruction consiste à donner à p la valeur 1. Cette action est exécutée à la lecture de la 2ième ligne du fichier. Les deux autres instructions ont été jusqu'alors inopérantes. Lors de la lecture de la 3ième ligne, p vaut 1, la condition p de la 2ième instruction est vérifiée, ce qui déclenche l'action par défaut print de cette instruction. A la lecture de la ligne suivante, p est remis à zéro et la 2ième instruction, celle qui imprime, devient inopérante.

Imprimer la première ligne vérifiant une condition et toutes les suivantes  jusqu'à la fin du fichier

$ awk '/oui/ {p=1} ; p' toto

Et oui Tata aime Toto et Toto aime Tata.

La vie est belle car Tata habite près de Toto.

Ils se voient tous les jours!

On a deux instructions séparées par un point-virgule. Lorsque « oui » est rencontré, p se voit assigné la valeur 1, ce qui rend vraie la condition p de l'instruction suivante et provoque l'impression de  la ligne avec « oui » et de toutes les suivantes puisque p n'est plus changé.

Ou:

$ awk '/oui/,0' toto

Et oui Tata aime Toto et Toto aime Tata.

La vie est belle car Tata habite près de Toto.

Ils se voient tous les jours!

La condition 0 est toujours fausse!

Imprimer une partie d'un fichier sur base des numéros de lignes

$ awk 'NR>=5' toto

Et oui Tata aime Toto et Toto aime Tata.

La vie est belle car Tata habite près de Toto.

Ils se voient tous les jours!

NR (Number of Record) est une variable qui indique le nombre d'enregistrements lus, c'est-à-dire le numéro de ligne. Nous avons imprimé les lignes dont le numéro est plus grand ou égal à 5.

$ awk 'NR==3,NR==5 {print NR-2,$0}' toto

1 Nous allons à pied car ce n'est pas loin.

2 Elle m'aime et ça c'est cool!

3 Et oui Tata aime Toto et Toto aime Tata.

Nous avons cette fois sélectionné les lignes dont le numéro est compris entre 3 et 5 (limites incluses)  et nous avons remplacé l'action par défaut par l'impression du numéro de ligne diminué de 2 suivi de la ligne lue (dont le contenu est dans la variable $0)

Imprimer sous forme de tableau certains champs (mots) d'un fichier

awk 'BEGIN {OFS="|";print "Champ 3\t","champ 2";print "=======\t","======="} ; {print $3"\t",$2}' toto

Champ 3 |champ 2

======= |=======

Toto... |m'appelle

à...... |vais

à...... |allons

et..... |m'aime

Tata... |oui

est.... |vie

voient. |se

La première instruction comprend la condition spéciale BEGIN qui est VRAIE tant que la lecture du fichier n'a pas débuté. Dans cette instruction figurent plusieurs actions dont la première consiste à donner à la variable interne OFS (Output Field Separator) une valeur différente celle par défaut (normalement un espace). Ensuite nous imprimons les entêtes. Il est fait usage du caractère de contrôle tabulation représenté par \t. L'instruction suivante imprime les champs choisis.

Compter les mots dans un fichier

$ awk ' {n = n + NF} ; END {print n}' toto
49

L'action de la 2ième instruction est liée à la condition spéciale END qui est portée à VRAIE lorsque la totalité du fichier a été lue.

Chercher et remplacer

$ awk '{gsub(/Tata/,"Titine"); print}' toto
Je m'appelle Toto.
Je vais à l'école avec Titine

Nous allons à pied car ce n'est pas loin.

Elle m'aime et ça c'est cool!

Et oui Titine aime Toto et Toto aime Titine.

La vie est belle car Titine habite près de Toto.

Ils se voient tous les jours!

La commande awk est suivie d'une seule instruction qui comprend deux actions successives: remplacer « Tata » par «Titine » et ensuite imprimer la ligne.

Et si on ne met pas le print?

$ awk '{gsub(/Tata/,"Titine")}' toto

$

Il n'y a pas d'output: l'instruction awk comprend déjà une action et il n'y a donc pas lieu de considérer une action par défaut.

Supprimons les accolades:

$ awk 'gsub(/Tata/,"Titine")' toto

Je vais à l'école avec Titine

Et oui Titine aime Toto et Toto aime Titine.

La vie est belle car Titine habite près de Toto.

gsub(/Tata/,"Titine") n'est plus considéré comme une action mais comme une condition qui est VRAIE lorsqu'il y a effectivement une substitution (dans ce cas gsub retourne 1), ce qui déclenche l'action par défaut (impression). Donc seules sont imprimées les lignes du fichier qui ont été modifiées.

Si on utilise sub au lieu de gsub, on remplace seulement la première occurrence de Tata:

$ awk 'sub(/Tata/,"Titine")' toto

Je vais à l'école avec Titine

Et oui Titine aime Toto et Toto aime Tata.

La vie est belle car Titine habite près de Toto.

(Le 2ième Tata a survécu).

Pour remplacer une occurrence particulière, on doit utiliser gensub. Contrairement à sub ou gsub, gensub ne renvoie pas 1 ou 0, mais la ligne, modifiée ou pas, reçue en input, l'original restant inchangé. Il convient donc d'assigner à $0 le retour de gensub:

$ awk '{x=$0 ; $0=gensub(/Tata/,"Titine",2)}; !(x==$0) {print}' toto                                                               

Et oui Tata aime Toto et Toto aime Titine.    

Nous sauvegardons la valeur initiale de $0 dans x, et nous imprimons seulement si $0 n'a pas changé: condition !(x==$0) de la 2ième instruction.

Conversion des fins de ligne Unix au format DOS

Dans un système Unix, les lignes se terminent par le caractère de contrôle nouvelle ligne (0A en hexadécimal). Dans un système DOS ce caractère nouvelle ligne est précédé du caractère de contrôle retour chariot (OD en hexadécimal).

Nous disposons d'un fichier salut qui contient le seul mot Salut.

$ cat salut

Salut

Au lieu d'envoyer directement le résultat de la commande  vers l'écran, faisons le transiter par la commande od afin d'avoir, par suite des options fournies à cette commande, un output en hexadécimal:

$ cat salut | od -An -tx1

53 61 6c 75 74 0a

Nous voyons apparaître en dernier le fameux caractère de contrôle.

Essayons ceci:

$ awk 'BEGIN {ORS="\r\n"};1' salut | od -An -tx1
53 61 6c 75 74 0d 0a

Et voilà! Nous avons une fin de ligne au format DOS.

Lorsque awk lis une ligne, il la débarrasse de son caractère de contrôle final. L'action print remet par défaut à la fin de la ligne un caractère de contrôle nouvelle ligne. Ce comportement peut-être modifié en jouant sur le contenu de la variable interne ORS (Output Record Separator). Sachant que \r  représente le caractère de contrôle retour chariot et \n le caractère de contrôle nouvelle ligne, on voit que la valeur assignée à ORS, est bien celle qui convient.

Conversion des fin de ligne DOS au format Unix

Soit salut.dos un fichier avec fin de ligne au format DOS

$ cat salut.dos | od -An -tx1

53 61 6c 75 74 0d 0a

Convertissons le au format Unix:

awk '{sub(/\r$/,"");print}' salut.dos | od -An -tx1

53 61 6c 75 74 0a

Sachant que $ est l'indicateur de fin de ligne, on voit que la commande sub remplace le caractère \r qui se trouve à la fin de la ligne (puisque \n a été enlevé à la lecture) par rien, c'est à dire le supprime. L'action print remet ensuite un \n à la fin.

Statistiques sur la fréquence des mots

Nous disposons d'un fichier prénoms qui se présente comme suit:

12345:Arthur

62345:Firmin

12445:Arthur:Motta:OK

12345:Isidore

72345:Arthur::KO

16545:Firmin

18345:Firmin

12345:Anatole:Dubois:KO

12645:Arthur:Dupont

12345:Arthur

12845:Anatole

14582:Grocanar

Nous aimerions établir une liste des prénoms classés suivant leur fréquence.

A moi awk!

$ awk -F ":" '{a[$2]++} ; END {for (x in a) print a[x]"\t" x | "sort -nr"}' prénoms

5       Arthur

3       Firmin

2       Anatole

1       Isidore

1       Grocanar

L'option -F fixe le séparateur de champ à « : ». D'autre part, awk peut travailler avec des tableaux dont l'indice n'est pas un nombre entier mais une chaîne de caractère. Ainsi l'instruction a[$2]++ crée d'abord l'élément a[Arthur] du tableau a avec la valeur 1, ensuite l'élément a[Firmin ] avec la même valeur 1, puis la valeur de a[Arthur] est incrémentée de 1 etc... En final, on se trouve avec le tableau:

a[Arthur]   = 5

a[Firmin]   = 3

a[Isidore]  = 1

a[Anatole]  = 2

a[Grocanar] = 1

Il reste à imprimer ce tableau, ce qui est réalisé dans l'instruction conditionnée par END. for (x in a)signifie que x parcourt toutes les valeurs de l'indice de a (tous les prénoms). L'argument de print est constitué de a[x] et x séparés par un caractère de tabulation \t. Le résultat des différentes commandes print est mis en cache pour être envoyé ensuite à la commande externe sort.

Considérons maintenant de nouveau le fichier toto. Nous souhaitons établir une liste des mots contenus dans ce fichier, classés suivant leur fréquence. Pour raccourcir la liste, nous éliminons les mots trop courts et ceux qui n'apparaissent qu'une seule fois:

$ awk '{gsub(/[.;,!?]/,""); gsub(/\47/," "); $0=tolower($0) ; for (i = 1; i <= NF; i++) if (length($i) > 2) a[$i]++} ; END {for (x in a) if (a[x] > 1) print a[x]"\t" x | "sort -nr"}' toto

4       toto

4       tata

3       est

3       aime

2       car

Bien plus qu'une simple commande, awk est un véritable langage de programmation et pour peu que les instructions soient nombreuses ou complexes, une ligne de commande telle que celle cidessus devient vite incompréhensible. Aussi est-il préférable de regrouper les instructions dans un fichier texte tel que celui-ci:

#!/usr/bin/awk -f

 BEGIN {

  print "Fréquence", "Mot"

  print "=========", "==="

}

{

  gsub(/[.;,!?]/,"")                                          

  # suppression de la ponctuation

  gsub(/\47/," ")

  # remplacement des apostrophes (47 en octal) par un espace

  $0=tolower($0)                                          

  # remplacement des majuscules par des minuscules

  for (i = 1; i <= NF; i++)

    if (length($i) > 2) a[$i]++              

}

END {

  for (x in a)

    if (a[x] > 1) {

    print a[x]"\t  " x | "sort -nr"

    }

}

Nous avons ajouté une instruction conditionnée par BEGIN, ainsi que des commentaires et appelé le fichier statmots. On peut maintenant procéder comme suit:

$ awk -f statmots toto

Fréquence Mot

========= ===

4         toto

4         tata

3         est

3         aime

2         car

L'option -f dit à awk de trouver ses instructions dans le fichier qui suit. La ligne shebang n'est pas nécessaire si on procède de cette façon. Cependant grâce à cette ligne shebang  et pour peu qu'on ai rendu statmots exécutable (chmod +x statmots), on peut également procéder comme ceci:

$ ./statmots toto
Fréquence Mot
========= ===
4         toto
4         tata

3         est

3         aime

2         car

Les quelques exemples donnés ici ne montrent  qu'un petit aperçu des nombreuses possibilités offertes par awk.