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.

vendredi 12 février 2010

diffdate

Dates passées et futures.

La commande date affiche la date du jour:

$ date

sam. févr.  6 10:28:38 CET 2010

On peut agir sur le format de l'output via des options:

$ date "+%A %d %B %Y"

samedi 06 février 2010

Cette commande permet également de fournir une réponse à des questions du genre:

Quelle date serons nous dans 25 jours?

[michel@rigel ~]$ date "+%A %d %B %Y" -d "25 days"

mercredi 03 mars 2010

Quelle était la date il y a 25 jours?

$ date "+%A %d %B %Y" -d "-25 days"

mardi 12 janvier 2010

Quelle était la date 25 jours après le 3 décembre 2009?

$ date "+%A %d %B %Y" -d "20091203 25 days"

lundi 28 décembre 2009

On n'a pas d'emprise sur le format de la date fournie en input.

Différents formats sont automatiquement reconnus, par exemple pour le 3 décembre 2009, on aurait pu mettre au lieu de 20091203:

091203

2009/12/03

2009-12-03

12/03/2009

....

mais pas 03/12/2009  qui aurait été reconnu comme le 12 mars 2009.

Pour utiliser ce dernier format, on doit transformer l'input, avec par exemple awk, comme ciaprès:

$ echo 03/12/2009 | awk -F '/' '{print $3$2$1}'

20091203

D'autre part, on sait que $(...) représente le résultat d'une commande, comme dans cet exemple:

$ echo  Nous sommes $(date +%A)

Nous sommes samedi

Dans la commande qui donne la date 25 jours après le 3 décembre 2009, remplaçons 20091203 par $(la commande dont le résultat est 20091203):

$ date "+%A %d %B %Y" -d "$(echo 03/12/2009 | awk -F '/' '{print $3$2$1}') 25 days"

lundi 28 décembre 2009

Définissons la fonction addjours:

$ addjours() { date "+%A %d %B %Y" -d "$(echo $2 | awk -F '/' '{print $3$2$1}') $1 days" ;}

(En rouge, $1 et $2, les paramètres transmis à la fonction, remplacent 25 et 20091203; ne pas les confondre avec les arguments de l'action print de awk qui correspondent aux 3 champs de la date fournie en input)

Vérifions que la fonction est opérationnelle:

$ addjours -60 29/01/2010

lundi 30 novembre 2009

Et voilà le travail!

Il nous est loisible de ne pas fournir une date. La date prise en compte est alors la date du jour:

addjours 45

mardi 23 mars 2010

Pour garder de manière permanente la définition de cette fonction, il suffit de la mettre dans son  ~/.bashrc

Nombre de jours entre deux dates

L'option +%s permet d'afficher un date sous forme du nombre de secondes écoulées depuis le 1ier janvier 1970:

$ date +%s -d "2010/02/01"

1264978800

$ date +%s -d "2009/12/03"

1259794800

On sait que $((...)) représente le résultat d'une opération arithmétique, comme ici:

$ echo $((1264978800 - 1259794800))

5184000

5184000 est le nombre de secondes entres les deux dates.

Pour avoir le nombre de jours, il faut encore diviser par 86400:

$ echo $(((1264978800 - 1259794800)/86400))

60

Soit en remplaçant 1264978800 et1259794800 par $(la commande qui fournit ces nombres):

$ echo $((($(date +%s -d "2010/02/01") - $(date +%s -d "2009/12/03"))/86400))

60

Définissons la fonction diffdate:

$ diffdate() { echo $((($(date +%s -d $1) - $(date +%s -d $2))/86400)) ;}

($1 et $2 sont les paramètres correspondant aux dates à transmettre à la fonction)

Vérifions en l'usage;

$ diffdate 2010/02/01 2009/11/25

68

Pour utilisation du format de date habituel, il nous est loisible de passer comme précédemment  par l'intermédiaire de awk, et donc d'utiliser une fonction diffdate définie comme suit:

$ diffdate() { echo $((($(date +%s -d "$(echo  $1 | awk -F '/' '{print $3$2$1}')") - $(date +%s -d "$(echo  $2 | awk -F '/' '{print $3$2$1}')"))/86400)) ;}

Vérifions que tout est OK:

$ diffdate 03/02/2010 03/12/2009

62

Pour que la définition de diffdate soit permanente il suffit de l'ajouter à son fichier ~/.bashrc.