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.
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:
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
|
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 ?
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.
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 |
Le numéro de l'enregistrement actuellement en cours de traitement. Un exemple : awk '{print FNR ": " $0}' un_fichier permet d'afficher le fichier avec les numéros de lignes.Un autre exemple : awk '{print FNR ": " $0}' un_fichier | 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.
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]
Il y en a de nombreuses :
print printf ...
gensub substr tolower index split ...
fflush close nextline system
rand sin exp log ...
systime strftime
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.
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 -fExplications :
maVariable
.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.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.awkawk permet, bien sûr l'utilisation de :
if (condition) {instructions} else {instructions}
: si alors sinonwhile (condition) {instructions}
: boucle tantquedo {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) ...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.
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).
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 :
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.
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 :
Format GRASS :
14 lignes d'en-tête comprenant des renseignements diversLes 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.
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 ici(64ko).
a+