GAWK

De Lea Linux
Révision datée du 2 septembre 2005 à 18:06 par PingouinMigrateur (discussion | contributions) (conversion de la documentation originale de Léa par HTML::WikiConverter)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigation Aller à la recherche

Introduction à (g)awk

Introduction à (g)awk
Cet article va vous permettre de vous familiariser avec (g)awk, langage de programmation permettant de faire très rapidement des traitements intéressants sur des données diverses. Après la présentation générale, on réalisera deux filtres inspirés de filtres ayant été réellement développés.

Introduction

Présentation

Vous vous demandez surement pourquoi ce (g) avant awk, non ? Ce n'est pas le g de gnome, ni celui de gtk+ mais celui de gnu. Extrait de la page de manuel (man gawk ou man awk) :
"Gawk est l'implémentation du projet GNU du langage de programmation AWK, lequel se conforme à la définition du langage dans la norme POSIX1003.2 Command Language and Utilities Standard. Cette version est basée, elle, sur la description d'AWK de Aho, Kernighan and Weinberger, plus les spécificités supplémentaires se trouvant dans la version de awk de l'UNIX System V version 4. Gawk ajoute également des extensions plus récentes des Laboratoires Bell ainsi que des extensions spécifiques à GNU."
Je ne mets pas ce pavé pour remplir de l'espace mais pour que les personnes à la base de choses que je ne fais que présenter soient reconnues pour leur travail et que l'on sache de quoi on parle.

Nous allons pouvoir commencer:

Qu'est ce que c'est

awk (j'utiliserai awk par la suite, mais c'est de gawk que l'on parle) est un langage interprété, installé par défaut sur les distributions linux (au moins rh et mandrake) dans le répertoire /bin. Il permet de traiter des fichiers de données structurées.
Un programme awk est une suite de blocs de code compris entre {} (c'est pas très original) qui sont appliqués aux lignes (par défaut) de données du fichier si un "modèle" est vérifié. La page de manuel nous donne les différents types de modèles utilisables :

BEGIN Le bloc qui suit est exécuté au début du programme, une seule fois.
END Le bloc qui suit est exécuté à la fin du programme, une seule fois.
/expression régulière/ Le bloc qui suit est exécuté si les données en cours de traitement correspondent à l'expression régulière. Si vous ne connaissez pas les expressions régulières, lisez la page man egrep.
Un exemple : /^#/ Permet de selectionner les lignes commençant par un #.
expression relationnelle Le bloc qui suit est exécuté si l'expression est vraie.
Un exemple : maVariable==5
modèle && modèle Le bloc qui suit est exécuté si les deux modèles sont vérifiés.
modèle modèle Le bloc qui suit est exécuté si au moins un des modèles est vérifié.
modèle0 ? modèle1 : modèle2 Le bloc qui suit est exécuté si les modèles 0 et 1 sont vérifiés où si le modèle2 uniquement est vérifié.
(modèle) Permet de grouper les modèles.
Un exemple : (modèle0
modèle1) && modèle2
 ! modèle Le bloc qui suit est exécuté si le modèle n'est pas vérifié.
modèle1, modèle2 Le bloc qui suit est exécuté pour la partie des données en cours commençant par modèle1 et finissant par modèle2.
Un exemple : /\/\*/, /\*\// pour afficher les commentaires multilignes

Premiers pas

Les enregistrements

Pour awk, les blocs de données lues sont séparés par un caractère contenu dans la variable RS (Record Separator, retour à la ligne par défaut). On peut accéder à l'intégralité du bloc grâce à la variable $0. C'est ce qu'on appelle un enregistrement.

Un exemple : Tapez awk '{ print $0 }' un_fichier
Passionant, non ? vous venez de réinventer la commande cat ;-) !
Un autre exemple : Tapez awk '/\/\*/, /\*\// { print $0 }' un_fichier_c_avec_des_commentaires
C'est déjà plus sympa, hein ?
Un dernier exemple : Tapez awk '! (/^ *#/ || /^$/) { print $0 }' un_fichier_conf
Vous n'aviez jamais vu votre fichier de config d'Apache comme ça, si ?

Les champs

Ah, la nature ... Courir nus dans les champs en se tenant la main ... NON, je vous arrête tout de suite.
Chaque enregistrement est séparé en champs par un caractère contenu dans la variable FS (Field Separator, un espace par défaut). Les champs sont accessibles par les variables $1, $2, $3, etc ...
Pour afficher le 3ème champ de toutes les lignes de vos données à traiter, vous utilisez print $3.
Un exemple : awk -F ':' '{ print $1 " est " $5 }' /etc/passwd pour afficher la description des utilisateurs de votre machine.

Les variables

Il existe bon nombre de variables prédéfinies en awk. Je vais vous en donner la liste résumée, encore une fois, je n'invente rien et je ne créée rien, je ne fais que vous refaire une lecture de la page de manuel. D'ailleurs pour avoir une liste exhaustive des variables, consultez la.

ARGC Si vous avez fait du c, vous vous en doutez ! Sinon, je vous le dis. Cette variable contient le nombre d'arguments passés au programme. (sans les options passées à awk)
ARGV Un tableau contenant les ARGC paramètres, indéxé de 0 à ARGC-1. Ce sont tous les fichiers à traiter
CONVFMT Le format par défaut pour l'affichage des nombres, "%.6g" par défaut
ENVIRON Un tableau contenant les variables d'environnement. Exemple ENVIRON["PATH"] contient votre path.
FIELDWIDTHS Pour le cas de traitement de données non délimités mais contenant des champs de largeur fixe.
Cette variable est de la forme largeur_champ_1 largeur_champ_2 largeur_champ_3 etc....
FNR grep ^45: permet d'afficher la ligne 45 du fichier un_fichier. On fait appel à grep, ce n'est plus du 100% awk mais c'est beau la diversité, non ?
FS Le séparateur de champs. No comment !
IGNORECASE En gros, permet de ne pas prendre en compte la casse lors des comparaisons de chaines de caractères entre elles où avec des expressions régulières. Voir la page de manuel pour les détails.
NF Norme Française ... NON, Number of fields : C'est le nombre de champs de l'enregistrement en cours.
NR Number of Records : Le nombre d'enregistrements traités jusqu'à maintenant.
RS Le séparateur d'enregistrements. No comment !

Bien entendu, vous pouvez également définir les vôtres.

Exemples :
maVar=3 déclare et initialise la variable myVar à la valeur 3.
print maVar affiche le contenu de la variable maVar.
print $maVar affiche le champ n°maVar de l'enregistrement en cours (ici, équivaut à print $3).
print $(maVar-1) affiche le champ n°(maVar-1) de l'enregistrement en cours (ici, équivaut à print $2).
split("toto:tata:titi:tutu", arr, ":") initialise le tableau arr à ("toto","tata",...). On accède ensuite aux valeurs par arr[1], arr[2], ..., arr[4]

Les fonctions

Il y en a de nombreuses :

  • pour l'affichage formaté, de textes et de nombres. print printf ...
  • pour les opérations habituelles sur les chaines de caractères, concaténation, substitution, remplacement, recherche ... gensub substr tolower index split ...
  • pour les accés aux systèmes de fichiers et au système fflush close nextline system
  • pour les fonctions mathématiques rand sin exp log ...
  • pour la gestion du temps systime strftime

Les possibilités de formatage importantes à votre disposition sont définies dans la page de manuel de awk.

Et vous pouvez également définir vos fonctions grâce à la syntaxe : function nom_de_fonction(param1, param2, var_locale_1, var_locale_2) {
...
...
}
ou func nom_de_fonction(param1, param2 var_locale_1, var_locale_2) {
...
...
}
La déclaration des variables locales est étrange. Celà vient du fait que awk n'était pas prévu pour permettre la créaton de fonctions. Il a donc été décidé, de les ajouter à la liste des paramètres, mais séparées par des espaces supplémentaires. L'appel de la fonction, lui, ne contient que les paramètres. Exemple : nom_de_fonction(5,2). Les variables locales sont initialisées à chaine vide ou zéro.

Blocs

Merci à Jean-Louis pour m'avoir montré que je n'insistais pas assez sur le fonctionnement des blocs. C'est grâce à lui que cette partie existe

Utilisation des blocs

Prenons un exemple :

#!/usr/bin/awk -f
BEGIN {
print "début du script"
maVariable=5
nbLignes=0
nbCommentaires=0
nbCommentaires2=0
print "maVariable vaut " maVariable
}

( /^#/ || /^$/ ) {
nbCommentaires++
}

{
nbLignes++
printf "%04d:%s\n",FNR,$0
if ( $0 ~ /^#/ || $0 ~ /^$/ ) {
nbCommentaires2++
}
}

END {
print "fin du script maVariable vaut toujours " maVariable
print "Le script a traité " FNR " lignes dont " nbCommentaires "(=" nbCommentaires2 ") lignes vides ou commençant par un #"
}

Explications :

  • Le bloc BEGIN est exécuté une fois au début, avant le traitement des données. Il affiche "début du script", initialise les variables et affiche le contenu de maVariable.
  • Un bloc qui est exécuté pour chaque ligne commençant par un # ou vide, il incrémente la variable nbCommentaires.
  • Un Bloc exécuté pour toutes les lignes, il incrémente la variable nbLignes, affiche la ligne préfixée par son numéro. Le numéro est formaté pour occupper quattre caractères. Les lignes suivantes (à partir du if) montrent l'équivalence entre le bloc du dessus et un test if dans le bloc principal. La variable nbCommentaires2 est incrémentée pour chaque ligne commençant par un dièse ou vide.
  • Le bloc END est exécuté une fois à la fin, après le traitement des données. On voit que la variable maVariable est toujours définie et que son contenu n'a pas été altéré. Il indique également le nombre total de lignes traitées et le nombre de lignes de commentaires ou vides présentes dans le fichier.

Exécution

# chmod a+x ceScript.awk
# ./ceScript.awk unfichier.shell
début du script
maVariable vaut 5
0001:# Ce script affiche hello world
0002:
0003:echo "hello world"
0004:exit 0
fin du script maVariable vaut toujours 5
Le script a traité 4 lignes dont 2(=2) lignes vides ou commençant par un #

Divers

awk permet, bien sûr l'utilisation de :

  • if (condition) {instructions} else {instructions} : si alors sinon
  • while (condition) {instructions} : boucle tantque
  • do {instructions} while () : boucle répéter jusqu'à
  • for (expr1; expr2; expr3) {instructions} : boucle pour
  • for (var in tableau) : boucle pour toutes les valeur du tableau. Il existe aussi while (var ni tableau), if (var in tableau) ...

Utiliser awk

Par la ligne de commande

Il existe deux façons de se servir de awk en tapant la commande awk suivie d'arguments.
La première consiste à saisir le code à exécuter directement sur la ligne de commande, celà convient parfaitement à de petits scripts. La syntaxe est la suivante : awk -F séparateur_de_champ 'script awk' fichiers_a_traiter, par exemple awk -F ':' '{ print $1 " est " $5 }' /etc/passwd.
La seconde consiste à placer votre script dans un fichier et à l'appeler par la commande awk -f fichier_script fichier_a_traiter vous pouvez définir le séparateur de champ dans votre script, dans la section BEGIN ou le spécifier sur la ligne de commande grâce à l'option -F, comme ci-dessus. L'exemple ci-dessus deviendrait alors awk -f monScript /etc/passwd ou monScript serait : BEGIN { FS = ":"}
{ print $1 " est " $5 }
ou bien awk -F ':' -f monScript2 /etc/passwd avec monScript2 valant { print $1 " est " $5 }
Notez que dans le script on doit saisir FS = ":" et non FS = ':'. Vous êtes prévenus !

Comme d'habitude, pour les autres options de la ligne de commande, je vous renvoie à la page de manuel de awk.

Des scripts exécutables

Cette utilisation a vite quelque chose de rébarbatif quand même, on rend donc les scripts directement exécutables en plaçant en en-tête #!/bin/awk -f (le chemin doit être adapté à l'emplacement de awk). Créons donc un fichier monScript3 dans lequel nous plaçons : #!/bin/awk -f
BEGIN { FS = ":"}
{ print $1 " est " $5 }
Suite à cette saisie, on rend le script exécutable par chmod a+x et on exécute par ./monScript3 /etc/passwd.

A partir de maintenant, j'estime que vous avez les bases nécessaires pour réaliser vos scripts, si awk vous intéresse. Je vais maintenant vous soumettre des exemples qui vous aideront à mieux comprendre les choses qui sont peut être restées obscures lors de cette présentation sommaire. N'oubliez pas que le bon réflexe en cas de problème reste de lire la doc. En cas de gros pépin, postez un message dans le forum de lea-linux, je ne suis jamais loin ou envoyez moi un mail (j'y réponds dès que j'ai le temps).

Des filtres en exemple

Transformer le fichier hosts

Si vous êtes des habitués du site, ce premier exemple vous rapellera peut être quelque chose puisque je l'avais posté dans le forum en réponse à une personne qui souhaitait adapter le format des IP du fichier hosts à ses besoins, xxx.xxx.xxx.xxx.
Voici donc le code du premier filtre exemple :

#!/usr/bin/awk -f
 !(/^#/ || /^$/) {
split ($1, array_ip, ".")
printf "%03d.%03d.%03d.%03d\t%s\t%s\n", array_ip[1], array_ip[2], array_ip[3], array_ip[4],$2,$3
}

La première ligne signifie qu'on va chercher awk dans le répertoire /usr/bin/, ce qui est le bon chemin pour ma slack.
La deuxiême ligne signifie qu'on ne s'intéresse qu'aux lignes qui ne commencent pas par un # ni aux lignes vides. En effet en expressions régulières, ^ signifie début de chaîne et $, fin de chaîne.
Par la suite, on découpe le premier paramètre en prenant comme séparateur le ., on stocke le résultat dans un tableau et on affiche les données du tableau en les formattant puis les autres champs (nom_de_machine et nom_de_machine.domaine). Le formatage %03d signifie qu'on affiche des entiers sur trois caractères et que s'il manque des caractères, on précède ceux qui existent avec des 0.
Il faut rendre ce script exécutable : chmod a+x prepare_hosts.awk
Puis, on l'appelle comme ça : ./prepare_hosts.awk /etc/hosts > hosts.prepared pour récupérer le fichier traité dans un fichier hosts.prepared placé dans le répertoire en cours.

Filtre de GéoConcept© vers GRASS(GNU/GPL)

GéoConcept et GRASS sont deux systèmes d'information géographique (je dirais SIG par la suite). GéoConcept permet d'exporter des objets surfaces selon une syntaxe explicitée ci-dessous. Le fichier export de GéoConcept contient tous les objets exportés.

Bien entendu ce format n'est pas du tout celui de GRASS qui est explicité ci-après, j'ai donc du écrire un p'tit filtre. Il n'est pas universel puisqu'on ne sait pas combien de champs se trouvent avant les coordonnées avant de faire l'export dans GéoConcept. Je le donne ici à titre d'exemple et j'y ai fait de grosses découpes, ne l'utilisez pas tel quel. Si vous êtes arrivés ici en faisant une recherche sur les SIG et que ce script vous intéresse, je peux vous l'envoyer par mail.

Format GéoConcept :
//$SYSCOORD {Type:1}
//$UNIT Distance:m
//$FORMAT 1
28878 buffer comm TRITTELING 911561.09 2462669.95 69 0. 1.e-002 ...
28595 buffer comm LONGWY 848138.94 2509824.85 91 45.62 -171.88 ...
...
Soit en clair :

  • 3 lignes inutiles pour nous.
  • L'identifiant GéoConcept de l'objet
  • Le type d'objet
  • Le sous-type d'objet
  • Les champs qu'on a demandé à exporter, ici le nom de l'objet et c'est tout
  • Les coordonnées absolues x et y du premier point du contour de la surface
  • Le nombre de points décrivant le contour
  • Les coordonnées relatives entre chaque point
  • Le nombre de trous
  • Les coordonnées absolues x et y du premier point du contour du trou
  • Le nombre de points décrivant le contour du trou
  • Les coordonnées relatives entre chaque point du contour du trou

Format GRASS :

14 lignes d'en-tête comprenant des renseignements divers
...
A 69
2462669.95 911561.09
2462669.96 911561.09
...
Soit en clair :

  • 11 lignes inutiles pour nous, recollées directement d'un header.
  • A 25 : pour dire que c'est une surface (area) et que son contour comporte 25 points
  • Les coordonnées absolues y et x des points du contour de la surface
  • Les mêmes renseignements pour les trous

Les bouts intéressants du script BEGIN {
melange = "v.patch input="
exited = 0
getline h1 < "header"
getline h2 < "header"
...
}

 !/\/\// {
print h1 > ENVIRON["LOCATION"] "/dig_ascii/" $1
print h2 > ENVIRON["LOCATION"] "/dig_ascii/" $1
...

melange = melange $1 ".awkImport,"
...

beginObj = 5
if ( (beginObj + 2 + $(beginObj+2) * 2) < NF) {
nbTrou = $((beginObj+3) + $(beginObj+2) * 2)
}
else {
nbTrou = 0
}

for ( i=0 ; i < nbTrou+1 ; i++) {
actualx = $beginObj
actualy = $(beginObj+1)
print "A " ($(beginObj+2)+2) > ENVIRON["LOCATION"] "/dig_ascii/" $1
print " " actualy " " actualx > ENVIRON["LOCATION"] "/dig_ascii/" $1

for ( j=(beginObj+3) ; j<(beginObj+3)+$(beginObj+2)*2-1 ; j++ ) {
actualx += $j
actualy += $(++j)
print " " actualy " " actualx > ENVIRON["LOCATION"] "/dig_ascii/" $1
}
print " " $(beginObj+1) " " $beginObj > ENVIRON["LOCATION"] "/dig_ascii/" $1
...
}
...
fflush (ENVIRON["LOCATION"] "/dig_ascii/" $1)
close (ENVIRON["LOCATION"] "/dig_ascii/" $1)
...
if ( system("v.support map=" $1 ".awkImport option=build -s threshold=" thresh " >/dev/null")!=0 )
exit (1)
}

END {
print "Patching all files into composite.awkImport"
if ( system (melange " output=composite.awkImport" ) != 0 )
exit (1)
...
for (i=1;i<=FNR;i++) {
getline < ARGV[1]
print i ":" $4 > ENVIRON["LOCATION"] "/dig_cats/" mapName
}
system (effaceTempos " > /dev/null")
fflush ()
...
}

Nous allons ensemble analyser les morceaux intéressants de ce script.

Tout d'abord, je définis quelques variables dans la section BEGIN, qui restent accessibles jusqu'à la fin de l'exécution. Ensuite j'affecte des valeurs aux variables h1 et h2 en lisant les lignes dans un fichier du répertoire courant nommé header.

Pour chaque objet (tout ce qui ne commence pas par //), j'insère un entête GRASS, composé des variables h1, h2, etc... que je range dans un fichier avec le symbole de redirection > dont le chemin est la concaténation de la variable d'environnement LOCATION (ENVIRON["LOCATION"]), de la chaîne "/dig_ascii/" et de la première colonne de la ligne que je suis en train de lire ($1).

J'ajoute à la variable melange une chaine constituée de ma variable $1 et de la chaine ".awkImport"

Je dis que les coordonnées de l'objet commencent à la colonne 5. Ceci me dit (voir explications du format GC ci-dessus) que si il y a des trous, c'est à dire si la somme du double du nombre de points intermédiaires donné en colonne 7 ($(beginObj+2) équivaut à $7 car beginObj vaut 5)+ les cinq colonnes de départ + 2 pour les 2 premières coordonnées absolues est inférieur au nombre total de colonnes de la ligne contenu dans la variable NF, alors leur nombre me serra donné à la colonne suivant ce nombre de colonnes précalculé, d'où le nbTrous = $((beginObj+3) + $(beginObj+2) * 2).

Ensuite la reconstitution des coordonnées absolues et l'inscription dans le fichier utilisé plus haut donne l'occasion de voir la syntaxe des boucles for.

On flushe le buffer du fichier et on le ferme. Au début j'avais oublié, alors, je me suis retrouvé avec BEAUCOUP TROP de fichiers ouverts. Vous me direz, le fichier je l'ai pas ouvert. Bé, oui, c'est le côté expérimentation hasardeuse qui m'a fait comprendre. M'enfin, vous pourrez pas dire : "euh, ben, je comprends pas pourquoi mon linux me dit que le script il doit s'arrêter à cause que j'ai un problème de ressources".
Ceci dit, je peux me tromper aussi. Si j'ai tort envoyez moi un mail.

La ligne suivante nous donne un exemple d'utilisation de la commande system, avec récupération du code de retour et aussi de la commande exit. RAS.

La suite montre l'utilisation de variables telles que FNR, ARGV ainsi que des appels à la fonction système avec redirection et l'utilisation des variables déclarées dans le begin et que l'on a fait évoluer au cours du script.

Le mot de la fin, enfin ;-)

Voilà, j'espère vous avoir fait comprendre ce qu'était awk et à quoi il servait. Bien sûr on peut faire la même chose en Perl, PHP, C, Java. Je sais. Mais utiliser awk plutôt que Perl quand il est adapté c'est un peu comme aller dans un petit hôtel/restaurant pittoresque plutôt que d'aller au Club Med. En plus, awk est très léger et peut être un allié puissant dans les systèmes embarqués.

Nota : Je vous ai généré un pdf du manuel de awk accessible [awk.pdf ici] (64ko). Petit rappel en passant : la manipulation pour générer des pdf à partir des pages de manuel se trouve sur Léa section [../trucs Trucs et astuces], rubrique [/trucs/sommaire.php3?cat=6 Shell].

a+
Xavier

Cette page est issue de la documentation 'pré-wiki' de Léa a été convertie avec HTML::WikiConverter. Elle fut créée par Xavier GARREAU le 18/04/2001.