Avertissement :
La version originale de cet article a été publiée sur IBM developerWorks
et est la propriété de Westtech Information Services. Ce document est une
traduction de la mise à jour de la version originale de l'article
réalisée par l'équipe de documentation Gentoo et contient quelques
améliorations proposées par l'équipe de documentation de Gentoo Linux. |
1. Enregistrements, boucles et tableaux
Les enregistrements sur plusieurs lignes
Awk est un excellent outil pour lire et traiter des données structurées, comme le fichier système /etc/passwd. /etc/passwd est la base de données UNIX des utilisateurs. C'est un fichier texte délimité par des ":", contenant beaucoup d'informations importantes, incluant entre autres tous les comptes et les ID des utilisateurs. Dans mon article précédent, je vous ai montré comment Awk pouvait facilement traiter les différents champs de ce fichier. Tout ce que nous avions à faire était d'assigner ":" à la variable FS (séparateur de champs).
En renseignant correctement la variable FS, Awk peut être configuré pour traiter les différents champs de n'importe quelle sorte de données structurées, tant qu'il y a un enregistrement par ligne. Cependant, FS ne sera pas suffisant si nous voulons traiter les différents champs d'un enregistrement qui sont sur plusieurs lignes. Dans ces situations, nous avons aussi besoin de modifier la variable séparatrice d'enregistrements RS (Record Separator). La variable RS indique à Awk quand l'enregistrement courant se termine et que le suivant commence.
Par exemple, regardez comment nous faisons pour traiter une liste d'adresses de participants du « Federal Witness Protection Program » :
Exemple de code 1.1 : Exemple de données de la liste des participants du « Federal Witness Protection Program » |
Jimmy the Weasel 100 Pleasant Drive San Francisco, CA 12345 Big Tony 200 Incognito Ave. Suburbia, WA 67890 |
Idéalement, nous souhaiterions qu'Awk reconnaisse chaque bloc de 3 adresses comme étant un enregistrement individuel, plutôt que 3 enregistrements séparés. Cela rendrait notre code un peu plus simple si Awk pouvait reconnaître la première ligne de l'enregistrement comme le premier champ ($1), les coordonnées de la rue comme le second champ ($2), la ville, l'état et le code postal comme troisième champ ($3). Le code suivant va simplement faire ce que nous voulons :
Exemple de code 1.2 : Réaliser l'initialisation d'un enregistrement de la liste d'adresse |
BEGIN {
FS="\n"
RS=""
}
|
Ci-dessus, assigner "\n" à FS indique à Awk que chaque champ apparaît sur une ligne séparée. En assignant "" à RS, nous lui indiquons également que chaque enregistrement d'adresse est séparé par une ligne vide. Une fois qu'Awk sait comment les données en entrée sont formatées, il peut faire tous les traitements d'affichage de ces enregistrements que nous souhaitons. Et le reste du script est simple à écrire. Regardez ce script complet qui traite cette liste d'adresse et affiche chaque enregistrement correspondant à une adresse sur une simple ligne, séparant chaque champ avec une virgule.
Exemple de code 1.3 : Script complet |
BEGIN {
FS="\n"
RS=""
}
{ print $1 ", " $2 ", " $3 }
|
Si ce script est sauvegardé en address.awk et que les données correspondantes aux adresses sont stockées dans un fichier appelé address.txt, vous pouvez exécuter ce script en tapant awk -f address.awk address.txt. Ce code produit l'affichage suivant :
Exemple de code 1.4 : Les données affichées par le script |
Jimmy the Weasel, 100 Pleasant Drive, San Francisco, CA 12345 Big Tony, 200 Incognito Ave., Suburbia, WA 67890 |
Dans la directive d'affichage « print » du fichier address.awk, vous pouvez voir qu'Awk concatène (joint) les chaînes de caractères qui sont les unes à côté des autres sur une ligne. Nous avons utilisé cette fonctionnalité pour insérer une virgule et une espace (", ") entre les trois champs d'adresse qui apparaissent sur la ligne. Bien que cette méthode fonctionne, elle n'est pas très élégante. Plutôt que d'insérer des ", " entre nos différents champs, Awk peut le faire pour nous grâce à une variable spéciale d'Awk appelée OFS, le séparateur des champs affichés « Output Field Separator ». Regardez cet extrait de code :
Exemple de code 1.5 : Exemple d'un extrait de code |
print "Hello", "there", "Jim!" |
Les virgules sur cette ligne ne font pas partie de notre chaîne de caractères d'origine. Au lieu de cela, elles indiquent à Awk que "Hello", "there" et "Jim!" sont des champs séparés et que la variable OFS doit être affichée entre chaque chaîne de caractères. Par défaut, Awk produit le résultat suivant :
Exemple de code 1.6 : Affichage produit par Awk |
Hello there Jim! |
Cela nous montre que par défaut, OFS vaut " ", une simple espace. Cependant, nous pouvons facilement redéfinir OFS, Awk insérera alors notre séparateur de champs favori. Voici une version modifiée de notre programme d'origine address.awk qui utilise OFS pour afficher ces caractères intermédiaires ", " :
Exemple de code 1.7 : Redéfinir OFS |
BEGIN {
FS="\n"
RS=""
OFS=", "
}
{ print $1, $2, $3 }
|
Awk dispose aussi une variable spéciale appelée ORS, appelée le « séparateur des enregistrements affichés » (Ouput Record Separator). En renseignant ORS, qui vaut par défaut le caractère de nouvelle ligne ("\n"), nous pouvons contrôler le caractère qui sera automatiquement affiché à la fin d'une ligne. La valeur par défaut ORS oblige Awk à afficher chaque directive « print » sur une nouvelle ligne. Si nous voulons que les résultats soient séparés par une double ligne, nous affecterons alors "\n\n" à ORS. Ou, si nous voulons que les enregistrements soient séparés par une simple espace (et non par une nouvelle ligne), nous devons mettre ORS à " ".
D'un enregistrement multiligne formaté en enregistrement avec des tabulations
Disons que nous avons écrit un script qui convertit notre liste d'adresses vers le format : une ligne par enregistrement en délimitant les champs par des tabulations afin de pouvoir l'importer dans une feuille de calcul. Après avoir utilisé une version légèrement modifiée de address.awk, cela devient clair que notre programme ne fonctionne que pour des adresses composées de trois lignes. Si Awk rencontre l'adresse suivante, la quatrième ligne sera rejetée et ne sera pas affichée :
Exemple de code 1.8 : Exemple d'enregistrement |
Cousin Vinnie Vinnie's Auto Shop 300 City Alley Sosueme, OR 76543 |
Pour gérer de telles situations, ce serait bien si notre code prenait le nombre de champs par enregistrements en compte, affichant chacun d'entre eux dans l'ordre. Pour l'instant, le code affiche seulement les trois premiers champs de l'adresse. Voici un code qui fait ce que nous voulons :
Exemple de code 1.9 : Code amélioré |
BEGIN {
FS="\n"
RS=""
ORS=""
}
{
x=1
while ( x<NF ) {
print $x "\t"
x++
}
print $NF "\n"
}
|
Tout d'abord, nous renseignons le séparateur de champs FS à "\n" et le séparateur d'enregistrements RS à "" afin qu'Awk analyse les adresses de plusieurs lignes correctement, comme auparavant. Alors, nous renseignons le séparateur d'affichage des enregistrements à "", ce qui demande à la directive « print » de ne pas afficher une nouvelle ligne à la fin de chaque appel. Cela signifie que si nous voulons qu'un texte commence sur une nouvelle ligne, nous avons besoin d'écrire explicitement « print "\n" ».
Dans le bloc de code principal, nous créons une variable appelée x qui conserve le numéro du champ actuel que nous traitons. Initialement, il vaut 1. Alors, nous utilisons une boucle « while » (une boucle d'Awk qui est construite de la même façon que celle que l'on trouve dans le langage C) pour tout parcourir sauf le dernier enregistrement, afficher l'enregistrement et une nouvelle ligne ; aussi, comme qu'ORS vaut "", « print » n'affiche plus de nouvelle ligne pour nous. Le résultat produit par le programme est le suivant, ce qui est exactement ce que nous voulons :
Exemple de code 1.10 : Notre résultat attendu. Pas très élégant, mais délimité par des tabulations pour une importation facile dans une feuille de calcul |
Jimmy the Weasel 100 Pleasant Drive San Francisco, CA 12345 Big Tony 200 Incognito Ave. Suburbia, WA 67890 Cousin Vinnie Vinnie's Auto Shop 300 City Alley Sosueme, OR 76543 |
Nous avons déjà vu le fonctionnement des boucles « while », qui est identique à son homologue en C. Awk a aussi une boucle « do...while » qui évalue la condition à la fin du bloc de code plutôt qu'au début comme une boucle « while ». C'est identique à une boucle « repeat...until » que l'on peut trouver dans d'autres langages. Voici un exemple :
Exemple de code 1.11 : Exemple « do...while » |
{
count=1
do {
print "Peu importe, je serais affiché au moins une fois"
} while ( count != 1 )
}
|
Parce que la condition est évaluée après le bloc de code, une boucle « do...while », à la différence d'une boucle normale « while », s'exécute toujours au moins une fois. Une boucle normale « while » ne s'exécutera jamais si sa condition est fausse à la première boucle.
Awk vous permet de créer des boucles, comme les boucles « while » sont identiques à leur homologue en C :
Exemple de code 1.12 : Exemple de boucle |
for ( assignement initial; comparaison; incrémentation ) {
bloc de code
}
|
Voici un exemple simple :
Exemple de code 1.13 : Exemple simple |
for ( x = 1; x <= 4; x++ ) {
print "iteration",x
}
|
Cet extrait affiche :
Exemple de code 1.14 : Résultat de l'extrait de code |
iteration 1 iteration 2 iteration 3 iteration 4 |
De plus, exactement comme en C, Awk fournit des directives « break » et « continue ». Ces directives fournissent un meilleur contrôle sur les différentes implémentations de boucles d'Awk. Voici un extrait de code qui a désespérément besoin d'une directive « break » :
Exemple de code 1.15 : Extrait de code ayant besoin d'une directive « break » |
while (1) {
print "pour toujours et toujours..."
}
|
Parce que 1 est toujours vrai, cette boucle se fera indéfiniment. Voici une boucle qui s'exécute seulement dix fois :
Exemple de code 1.16 : Boucle qui s'exécute seulement 10 fois |
x=1
while(1) {
print "iteration",x
if ( x == 10 ) {
break
}
x++
}
|
Ici, la directive « break » est utilisée pour arrêter la boucle la plus profonde. « break » termine donc immédiatement la boucle et l'exécution continue à la ligne après le bloc de code de la boucle.
La directive « continue » complète « break » et fonctionne de cette façon :
Exemple de code 1.17 : La directive « continue » complète la directive « break » |
x=1
while (1) {
if ( x == 4 ) {
x++
continue
}
print "iteration",x
if ( x > 20 ) {
break
}
x++
}
|
Ce code affichera de « iteration 1 » jusqu'à « iteration 21 », sauf « iteration 4 ». Si l'itération est égale à 4, x est incrémenté et la directive « continue » est appelée, ayant pour effet qu'Awk démarre alors la boucle suivante sans même exécuter le reste du bloc de code. La directive « continue » fonctionne pour tout type de boucle itérative d'Awk, comme le fait la directive « break ». Lorsqu'elle est utilisée à l'intérieur d'une boucle « for », la directive « continue » fait que la variable de contrôle de la boucle est automatiquement incrémentée. Voici un code équivalent pour la boucle :
Exemple de code 1.18 : Code équivalent au code précédent |
for ( x=1; x<=21; x++ ) {
if ( x == 4 ) {
continue
}
print "iteration",x
}
|
Il n'est pas nécessaire d'incrémenter « x » juste avant l'appel de « continue » comme c'était le cas dans notre boucle « while », puisque la boucle « for » incrémente « x » automatiquement.
Vous serez ravi d'apprendre qu'Awk a des tableaux. Cependant, sous Awk, il est habituel de démarrer les index de tableau à 1, plutôt que 0 :
Exemple de code 1.19 : Exemple de tableaux Awk |
myarray[1]="jim" myarray[2]=456 |
Quand Awk rencontre le premier assignement, myarray est créé et l'élément myarray[1] est initialisé à "jim". Après que le second assignement ait été évalué, le tableau a deux éléments.
Une fois défini, Awk a un moyen pratique pour parcourir les éléments d'un tableau :
Exemple de code 1.20 : Parcourir des tableaux |
for ( x in myarray ) {
print myarray[x]
}
|
Ce code affiche chaque élément du tableau myarray. Quand vous utilisez cette forme spéciale du « in » d'une boucle for, Awk assigne chaque index existant de myarray à x (la variable de contrôle de la boucle) à tour de rôle, exécutant le bloc de code de la boucle une fois après chaque assignement. Bien que ce soit une fonctionnalité d'Awk vraiment pratique, il a un inconvénient : quand Awk parcourt les index du tableau, il ne le fait pas suivant un ordre particulier. Cela signifie qu'il n'y a pas de moyen pour nous de savoir si le résultat du code précédent sera :
Exemple de code 1.21 : Résultat du code précédent |
jim 456 |
ou
Exemple de code 1.22 : Un autre résultat possible du même code |
456 jim |
Pour reprendre une phrase de Forrest Gump, parcourir le contenu d'un tableau est comme une boîte de chocolats : vous ne savez jamais sur quoi vous allez tomber. Cela a quelque chose à voir avec la nature « chaînes de caractères » des tableaux Awk, ce que nous allons voir à présent.
Les index de tableaux en « chaînes de caractères »
Dans mon article précédent, je vous ai montré qu'Awk conserve en fait les valeurs numériques en format de chaînes de caractères. Pendant qu'Awk réalise les conversions nécessaires pour faire son travail, il ouvre la porte pour du code un peu étrange :
Exemple de code 1.23 : Code un peu étrange |
a="1" b="2" c=a+b+3 |
Une fois ce code exécuté, « c » est égal à 6. Puisqu'Awk fonctionne avec des chaînes de caractères, ajouter les chaînes de caractères « 1 » et 2 est fonctionnellement la même chose que d'ajouter les nombres 1 et 2. Dans les deux cas, Awk réalisera correctement l'opération. La nature « chaînes de caractères » est assez intrigante : vous pouvez vous demander si nous utilisons des index de chaînes de caractères pour les tableaux. Par exemple, prenez le code suivant :
Exemple de code 1.24 : Exemple de code |
myarr["1"]="Mr. Whipple" print myarr["1"] |
Comme vous pouvez vous y attendre, ce code affichera « Mr. Whipple ». Mais que se passe-t-il si nous enlevons les guillemets autour du deuxième index "1" ?
Exemple de code 1.25 : En enlevant les guillemets du code |
myarr["1"]="Mr. Whipple" print myarr[1] |
Deviner le résultat de cet extrait de code est un peu plus difficile. Est-ce que Awk considère myarr["1"] et myarr[1] comme 2 éléments séparés du tableau, ou référencent-ils le même élément ? La réponse est qu'ils référencent le même élément, Awk affichera « Mr. Whipple » exactement comme dans le premier extrait de code. Bien que cela puisse sembler étrange, derrière le décor, Awk a utilisé des index de chaînes de caractères pour ses tableaux tout le temps !
Après avoir pris connaissance de cette curiosité, certains d'entre nous peuvent être tentés d'exécuter du code farfelu comme ça :
Exemple de code 1.26 : Code farfelu |
myarr["name"]="Mr. Whipple" print myarr["name"] |
Ce code ne génère pas d'erreur et fonctionne comme nos exemples précédents et affichera aussi "Mr. Whipple" ! Comme vous pouvez le voir, Awk ne nous limite pas à utiliser que des index de nombres entiers ; nous pouvons utiliser des index de chaînes de caractères si nous le voulons, sans que cela ne crée de problème. Chaque fois que nous utilisons des index de tableau n'étant pas des nombres entiers comme myarr["name"], nous utilisons des tableaux associatifs. Techniquement, Awk ne fait rien de différent quand nous utilisons un index de chaînes de caractères (même si vous utilisez un index de « nombre entier », Awk le traite encore comme une chaîne de caractères). Cependant, vous devriez utiliser ces tableaux associatifs : c'est plutôt cool et impressionnera votre patron. L'astuce d'index de chaînes de caractères sera notre petit secret. ;)
Quand il en vient aux tableaux, Awk nous donne beaucoup de flexibilité. Nous pouvons utiliser des chaînes de caractères comme index et nous ne sommes pas tenus d'avoir une suite continue de nombres d'index (par exemple, nous pouvons définir myarr[1] et myarr[1000] et laisser les autres éléments indéfinis). Bien que cela puisse être très utile, dans certaines circonstances, cela peut porter à confusion. Heureusement, Awk offre quelques fonctionnalités pratiques pour aider à rendre les tableaux plus facilement manipulables.
Tout d'abord, nous pouvons supprimer des éléments d'un tableau. Si vous voulez effacer l'élément 1 de votre tableau fooarray, tapez :
Exemple de code 1.27 : Supprimer des éléments du tableau |
delete fooarray[1] |
Et si vous voulez voir si un élément particulier du tableau existe, vous pouvez utiliser l'opérateur booléen spécial « in » comme décrit ci-dessous.
Exemple de code 1.28 : Vérifier si un élément particulier du tableau existe |
if ( 1 in fooarray ) {
print "Oui ! Il est ici."
} else {
print "Non ! Je ne le trouve pas."
}
|
Nous avons abordé pas mal de points dans cet article. La prochaine fois, je compléterai votre connaissance d'Awk en vous expliquant comment utiliser les fonctions mathématiques, les fonctions de chaînes de caractères et comment créer vos propres fonctions. Je vous parcourrai la création d'un programme d'équilibrage d'un carnet de chèques. D'ici là, je vous encourage à écrire quelques programmes Awk et de consulter les ressources suivantes.