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.
Ce document n'est pas activement maintenu.

Awk par l'exemple, 3e partie

Daniel Robbins  Auteur
Christophe Lefebvre  Traducteur

Dernière mise à jour le 31 octobre 2005
Cette traduction n'est plus maintenue

1.  Fonctions de chaînes de caractères et... livre de tenue de comptes ?

Formater l'affichage du résultat

Bien que la directive d'affichage « print » d'Awk fait généralement son travail correctement, il est possible que nous ayons besoin d'encore mieux dans certains cas. Pour ces situations, Awk offre deux bons vieux amis appelés « printf() » et « sprintf() ». Oui, ces fonctions comme beaucoup d'autres d'Awk, sont identiques à leurs homologues en C. « printf() » affiche une chaîne de caractères formatée, tandis que « sprintf() » retourne une chaîne de caractères qui peut être affectée à une variable. Si vous n'êtes pas familier avec « printf() » et « sprintf() », un guide d'introduction au langage C vous présentera rapidement ces deux fonctions essentielles d'affichage. Vous pouvez voir la page du manuel de « printf() » en tapant man 3 printf sur votre système Linux.

Voici quelques exemples de code utilisant les fonctions « sprintf() » et « printf() » d'Awk. Comme vous pouvez le voir, cela ressemble beaucoup au C :

Exemple de code 1.1 : Exemple de code Awk avec « sprintf() » et « printf() »

x=1
b="foo"
printf("%s a eu %d à son dernier devoir\n","Jim",83)
myout=("%s-%d",b,x)
print myout

Ce code affichera :

Exemple de code 1.2 : Résultat affiché par le code

Jim a eu 83 à son dernier devoir
foo-1

Fonctions de chaînes de caractères

Awk a une pléthore de fonctions de chaînes de caractères et c'est une bonne chose. Dans Awk, les fonctions de traitement des chaînes de caractères sont vraiment importantes puisque vous ne pouvez pas traiter une chaîne comme un tableau de caractères comme dans d'autres langages tels que le C, le C++ et Python. Par exemple, si vous exécutez le code suivant :

Exemple de code 1.3 : Exemple de code

mystring="How are you doing today?"
print mystring[3]

Vous aurez une erreur qui ressemblera à quelque chose comme ça :

Exemple de code 1.4 : Erreur générée par l'exemple de code

awk: string.gawk:59: fatal: attempt to use scalar as array

Oh, d'accord. Bien que pas aussi pratiques que les fonctions de type itérable en Python, les fonctions Awk de chaînes de caractères font leur travail. Jetons-y coup d'œil.

Tout d'abord, nous avons la fonction classique « length() » qui retourne la longueur d'une chaîne de caractères. Voici comment vous pouvez l'utiliser :

Exemple de code 1.5 : Exemple de la fonction length()

print length(mystring)

Ce code va afficher la valeur :

Exemple de code 1.6 : Valeur affichée

24

Bien, continuons. La prochaine fonction de chaînes de caractères est appelée « index() » et retourne la position de l'occurrence d'une sous-chaîne de caractères dans une autre chaîne de caractères ou bien elle retournera « 0 » si la chaîne de caractères n'est pas trouvée. En utilisant la variable « mystring », nous pouvons utiliser cette fonction de cette façon :

Exemple de code 1.7 : Exemple de la fonction index()

print index(mystring,"you")

Awk affiche :

Exemple de code 1.8 : Résultat affiché

9

Nous allons passer à deux fonctions plus faciles : « tolower() » et « toupper() ». Comme vous pouvez le deviner, ces fonctions retournent la chaîne de caractères avec tous les caractères convertis respectivement en minuscules ou en majuscules. Notez que « tolower() » et « toupper() » retournent la nouvelle chaîne de caractères et ne modifient pas celle d'origine. Ce code :

Exemple de code 1.9 : Convertir des chaînes en minuscules ou en majuscules

print tolower(mystring)
print toupper(mystring)
print mystring

Ce code affiche le résultat suivant :

Exemple de code 1.10 : Résultat affiché

how are you doing today?
HOW ARE YOU DOING TODAY?
How are you doing today?

C'est très bien, mais comment pouvons-nous extraire exactement une sous-chaîne de caractères ou même un simple caractère d'une chaîne de caractères ? C'est là que « substr() » intervient. Voici comment utiliser « substr() » :

Exemple de code 1.11 : Exemple de la fonction substr()

mysub=substr(mystring,startpos,maxlen)

« mystring » doit être soit une variable de type chaîne de caractères ou une chaîne de caractères à partir de laquelle vous souhaitez extraire une sous-chaîne de caractères. « startpos » correspond à la position du caractère de départ et « maxlen » à la longueur maximale de la chaîne de caractères que vous souhaitez extraire. Notez que j'ai dit longueur maximale : si « length(mystring) » est plus petit que « startpos + maxlen », votre résultat sera tronqué. « substr() » ne modifie pas la chaîne de caractères d'origine mais retourne la sous-chaîne de caractères à la place. Voici un exemple :

Exemple de code 1.12 : Un autre exemple

print substr(mystring,9,3)

Awk va afficher :

Exemple de code 1.13 : Ce qu'Awk va afficher

you

Si vous avec l'habitude de programmer dans un langage qui utilise des index de tableau pour accéder à des parties d'une chaîne de caractères (ce qu'Awk ne fait pas), mettez vous dans la tête que « substr() » s'y substitue en Awk. Vous aurez besoin de l'utiliser pour extraire de simples caractères et des sous-chaînes de caractères parce qu'Awk est un langage basé sur des chaînes de caractères, vous utiliserez donc souvent cette fonction.

Maintenant, nous allons passer à des fonctions plus puissantes, la première d'entre elles est « match() ». « match() » ressemble beaucoup à « index() » à l'exception près qu'au lieu de rechercher une sous-chaîne de caractères comme « index() », elle recherche une expression régulière. La fonction « match() » retourne la position de départ de la correspondance ou zéro s'il n'a pas trouvé de correspondance. De plus, « match() » renseignera deux variables appelées RSTART et RLENGTH. RSTART contient la valeur de retour (la position de la première correspondance) et RLENGTH contient la longueur de la chaîne de caractères trouvée (ou -1 si aucune correspondance n'a été trouvée). En utilisant RSTART, RLENGTH, « substr() » et une petite boucle, vous pouvez facilement itérer toutes les correspondances trouvées dans votre chaîne de caractères. Voici un exemple de l'utilisation de la fonction « match() » :

Exemple de code 1.14 : Exemple d'utilisation de match()

print match(mystring,/you/), RSTART, RLENGTH

Awk affiche :

Exemple de code 1.15 : Résultat affiché par la fonction précédente

9 9 3

Substitution d'une chaîne de caractères

Maintenant, nous allons nous intéresser à un couple de fonctions de substitution de chaînes de caractères : « sub » et « gsub() ». Celles-ci diffèrent légèrement des fonctions que nous avons étudiées jusqu'à présent puisqu'elles modifient la chaîne de caractères d'origine. Voici comment appeler la fonction « sub() » :

Exemple de code 1.16 : Modèle d'utilisation de la fonction sub()

sub(regexp,replstring,mystring)

Quand vous utilisez la fonction « sub », celle-ci trouve la première séquence de caractères dans la chaîne de caractères « mystring » qui correspond à l'expression régulière « regexp » et remplace cette séquence avec la chaîne de caractères « replstring ». « sub() » et « gsub() » utilise les mêmes arguments. Leur seule différence est que « sub » remplace la première expression régulière correspondante (s'il y en a une) et « gsub() » réalise un remplacement global, remplaçant ainsi toutes les correspondances rencontrées dans la chaîne de caractères. Voici un exemple d'utilisation des fonctions « sub() » et « gsub() »:

Exemple de code 1.17 : Exemple d'utilisation des fonctions sub() et gsub()

sub(/o/,"O",mystring)
print mystring
mystring="How are you doing today?"
gsub(/o/,"O",mystring)
print mystring

Nous avons dû réaffecter la variable « mystring » à sa valeur initiale car le premier appel de la fonction « sub() » a modifié directement la variable « mystring ». Une fois exécuté, ce code affiche le résultat suivant :

Exemple de code 1.18 : Résultat affiché

HOw are you doing today?
HOw are yOu dOing tOday?

Bien sûr, des expressions régulières plus complexes sont possibles. Je vous laisse tester des expressions régulières plus compliquées.

Nous terminerons cette découverte de fonctions de chaînes de caractères en vous présentant une fonction appelée « split() ». Le rôle de « split() » est de « découper » une chaîne de caractères et d'en placer les différentes parties dans un tableau indexé par des nombres entiers. Voici un exemple de l'utilisation de la fonction « split() » :

Exemple de code 1.19 : Exemple d'utilisation de la fonction split()

numelements=split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",mymonths,",")

Lors de l'appel de la fonction « split() », le premier argument contient directement la chaîne de caractères ou la variable à « découper ». Pour le deuxième argument, vous devez spécifier le nom du tableau dans lequel « split() » stockera les différentes parties de la chaîne de caractères découpée. Pour le troisième argument, vous spécifiez le séparateur qui sera utiliser pour « découper » les chaînes de caractères. Une fois exécuté, « split() » retourne le nombre de chaînes de caractères issu du « découpage ». « split() » affecte chacune de ces chaînes de caractères à un tableau d'index commençant à « 1 », donc le code suivant :

Exemple de code 1.20 : Exemple de code

print mymonths[1],mymonths[numelements]

... affichera :

Exemple de code 1.21 : Exemple d'affichage

Jan Dec

Particularité de certaines fonctions

N.B. : quand vous utilisez la fonction « length() », « sub() » ou « gsub() », vous pouvez omettre le dernier argument ; Awk appliquera alors l'appel de la fonction à « $0 » (la ligne courante entière). Pour afficher la longueur de chaque ligne d'un fichier, utilisez ce script Awk :

Exemple de code 1.22 : Le code affichant la longueur de chaque ligne d'un fichier

{
    print length()
}

Amusement financier

Il y a quelques semaines, j'ai décidé d'écrire mon propre programme de tenue de compte en Awk. Je voulais un fichier texte délimité par des tabulations dans lequel je pouvais saisir mes derniers dépôts et retraits d'argent. L'idée était de traiter ces données par un script Awk qui additionnerait automatiquement tous les montants et m'en donnerait la balance. Voici comment j'ai enregistré toutes mes transactions dans mon « livre de tenue de comptes ASCII » :

Exemple de code 1.23 : Livre de tenue de comptes ASCII pour enregistrer les transactions


23 Aug 2000    food    -    -    Y    Jimmy's Buffet    30.25

Chaque champ dans ce fichier est séparé par une ou plusieurs tabulations. Après la date (champ 1, $1), il y a deux champs appelés « rubrique de dépense » et « rubrique de revenu ». Quand je saisis une dépense comme sur la ligne ci-dessus, je mets un nom de quatre lettres dans le champs de la rubrique de dépense et un « - » (entrée vierge) dans le champ de rubrique de revenu. Cela signifie que cet enregistrement correspond à une « dépense de nourriture » :) Voici à quoi un dépôt ressemblerait :

Exemple de code 1.24 : Exemple d'une saisie d'un dépôt


23 Aug 2000    -    inco    -    Y    Boss Man        2001.00

Dans ce cas, je mets un « - » dans la rubrique de dépense et « inco » dans la rubrique de revenu. « inco » est un nom pour les revenus génériques (virement de paye par exemple). En utilisant des noms de rubriques, cela me permet de produire un bilan de mes revenus et de mes dépenses par rubrique. Tous les autres champs sont assez explicites. Le champ « "Y" ou "N" » indique si la transaction a été enregistrée sur mon compte. Ensuite, les derniers champs sont la description de la transaction et le montant (positif) de la transaction.

L'algorithme utilisé pour calculer la balance n'est pas trop difficile. Awk a simplement besoin de lire chaque ligne, une par une. Si une dépense est renseignée et qu'il n'y a pas de revenu (c'est à dire « - ») alors le montant est un débit. Si un revenu est renseigné et qu'il n'y a pas de dépense (c'est à dire « - », alors le montant est une recette. Et si les deux (dépense et revenu) sont renseignés, alors ce montant est un « transfert » : c'est à dire que le montant sera soustrait de la dépense et ajouté au revenu. De plus, toutes ces rubriques sont virtuelles mais sont très utiles pour suivre les revenus et les dépenses et pour suivre son budget.

Le code

L'heure est venue de nous intéresser au code. Nous commençons par la première ligne, le bloc BEGIN et une définition de fonction :

Exemple de code 1.25 : La balance, 1ère partie

#!/usr/bin/env awk -f
BEGIN {
    FS="\t+"
    months="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
}

function monthdigit(mymonth) {
    return (index(months,mymonth)+3)/4
}

En ajoutant la première ligne « #!... » à n'importe quel script Awk, cela lui permet d'être directement exécuté à partir du shell, à condition que vous réalisiez au préalable un chmod +x monscript. Les lignes restantes définissent notre bloc BEGIN, qui sera exécuté avant qu'Awk commence à traiter le fichier de notre livre de tenue de comptes. Nous affectons FS « Field Separator », le séparateur de champs à « \t+ », qui indique à Awk que les champs sont séparés par une ou plusieurs tabulations. En plus, nous définissons une chaîne de caractères appelée months (mois) qui est utilisée par notre fonction « monthdigit() » que l'on retrouve ensuite.

Les trois dernières lignes vous montrent comment définir votre propre fonction en Awk. Le format est simple : Tapez « function » puis le nom de la fonction, puis les paramètres séparés par des virgules à l'intérieur de parenthèses. Après ça, un bloc de code « { } contient le code que votre fonction doit exécuter. Toutes les fonctions peuvent accéder à des variables globales (comme notre variable « months »). De plus, Awk fournit une directive « return » qui permet à la fonction de retourner une valeur et fonctionne de la même façon que la directive « return » que l'on retrouve en C, Python et autres langages. Cette fonction convertit un nom de mois dans un format de chaîne de caractères composée de 3 lettres en son équivalent numérique. Par exemple :

Exemple de code 1.26 : Exemple d'utilisation de la fonction monthdigit()

print monthdigit("Mar")

... Affichera :

Exemple de code 1.27 : Exemple d'affichage résultant de la fonction monthdigit()

3

Maintenant, passons à d'autres fonctions supplémentaires.

Fonctions financières

Voici encore trois fonctions supplémentaires qui vont nous réaliser des opérations comptables. Notre bloc de code principal, que nous verrons bientôt, traite séquentiellement chaque ligne du fichier du livre de tenue de comptes, appelant une de ces fonctions, stockant alors les transactions appropriées dans un tableau Awk. Il y a trois types de transactions :  un revenu (doincome), un débit (doexpense) et un transfert (dotransfer). Vous remarquerez que ces trois fonctions admettent un argument, appelé « mybalance ». « mybalance » correspond à un tableau à deux dimensions, que nous utiliserons comme argument. Jusqu'ici, nous n'avons pas vu les tableaux à deux dimensions. Cependant, comme vous pouvez le voir ci-dessous, la syntaxe est tout à fait simple : Séparez juste chaque dimension avec une virgule et c'est parti !

Nous enregistrons des informations dans « mybalance » comme suit : la première dimension du tableau varie de 0 à 12 et spécifie le mois ou zéro pour l'année entière. Notre seconde dimension est une rubrique de quatre lettres, comme « food » ou « inco » : c'est la rubrique réelle dont nous nous occupons. Donc, pour trouver la balance de l'année entière pour la rubrique « food », vous l'aurez avec « mybalance[0,"food"] ». Pour trouver les revenus de Juin, vous l'aurez avec « mybalance[6,"inco"].

Exemple de code 1.28 : Trouver des informations relatives aux revenus


function doincome(mybalance) {
    mybalance[curmonth,$3] += amount
    mybalance[0,$3] += amount
}

function doexpense(mybalance) {
    mybalance[curmonth,$2] -= amount
    mybalance[0,$2] -= amount
}

function dotransfer(mybalance) {
    mybalance[0,$2] -= amount
    mybalance[curmonth,$2] -= amount
    mybalance[0,$3] += amount
    mybalance[curmonth,$3] += amount
}

Quand « doincome() » ou n'importe quelle autre fonction est appelée, nous enregistrons la transaction à deux endroits : « mybalance[0,category] » et « mybalance[curmonth,category] » respectivement la balance de la rubrique pour l'année entière et la balance de la rubrique pour le mois en cours. Cela nous permet de générer ainsi facilement soit le bilan annuel ou mensuel des revenus/dépenses.

Si vous regardez ces fonctions, vous remarquerez que le tableau référencé par « mybalance » leur est passé en argument. De plus, nous faisons allusions à plusieurs variables globales : « curmonth » qui stocke la valeur numérique de l'enregistrement en cours, « $2 » (la rubrique de dépense), « $3 » (la rubrique de revenu) et le montant « $7 ». Quand les fonctions « doincome() » et les autres du même type sont appelées, toutes ces variables sont déjà renseignées correctement pour l'enregistrement (ligne) en cours de traitement.

Le bloc principal

Voici le bloc de code principal qui contient le code qui va traiter chaque ligne de données en entrée. Souvenez-vous, parce que nous avons renseigné la variable FS correctement, nous pouvons appeler le premier champ comme $1, le second champ comme $2, etc... Quand les fonctions « doincome() » et autres fonctions du même type sont appelées, les fonctions peuvent accéder aux valeurs de « curmonth », $2, $3 et le montant de l'intérieur de la fonction. Regardez ce code et voyons enseuite pour une explication.

Exemple de code 1.29 : La balance, 3ème partie


{
    curmonth=monthdigit(substr($1,4,3))
    amount=$7

    # Enregistre toutes les rubriques rencontrées
    if ( $2 != "-" )
        globcat[$2]="yes"
    if ( $3 != "-" )
        globcat[$3]="yes"

    # Comptabilise correctement la transaction
    if ( $2 == "-" ) {
        if ( $3 == "-" ) {
            print "Erreur: Ni inc, ni exp n'ont été renseignés !"
            exit 1
        } else {
            # C'est un revenu
            doincome(balance)
            if ( $5 == "Y" )
                doincome(balance2)
        }
    } else if ( $3 == "-" ) {
        # C'est une dépense
        doexpense(balance)
        if ( $5 == "Y" )
            doexpense(balance2)
    } else {
        # C'est un transfert
        dotransfer(balance)
        if ( $5 == "Y" )
            dotransfer(balance2)
    }
}

Dans le bloc principal, les deux premières lignes renseignent la variable « curmonth » avec un entier compris entre 1 et 12 et renseigne le montant au 7ème champ (pour rendre le code plus facile à comprendre). Ensuite, nous avons quatre lignes intéressantes où nous écrivons les valeurs dans un tableau appelé « globcat ». « globcat », le tableau des rubriques globales, est utilisé pour enregistrer toutes les rubriques rencontrées dans le fichier : « inco », « misc », « food », « util », etc... Par exemple, si « $2 == "inco" », nous renseignons « globcat["inco"] » à « yes ». Plus tard, nous pouvons itérer notre liste de rubriques avec une simple boucle « for (x in globcat) ».

Sur les vingt lignes suivantes environ, nous analysons les champs $2 et $3 et enregistrons la transaction appropriée. Si « $2 == "-" » et « $3 !="-" », nous sommes en présence d'un revenu donc nous appelons la fonction « doincome() ». Si la situation est inverse, nous appelons la fonction « doexpense() ». Et si $2 et $3 contiennent tous les deux une rubrique, nous appelons la fonction « dotranfer() ». Chaque fois, nous passons le tableau « balance » comme argument à ces fonctions pour que les données appropriées puissent y être enregistrées.

Vous remarquerez aussi plusieurs lignes qui disent que « si ( $5 == "Y" ) », la même transaction doit alors être enregistrée dans « balance2 ». Que faisons-nous exactement ici ? Vous vous rappelez que $5 contient soit un "Y" ou un "N" indiquant si la transaction a été réalisée dans le compte. Parce que nous enregistrons la transaction dans « balance2 » seulement si la transaction a été enregistrée, « balance2 » contient la balance actuelle du compte tandis que « balance » contient toutes les transactions, qu'elles aient été enregistrées ou non sur le compte. Vous pouvez utiliser « balance2 » pour vérifier votre entrée de données (il devrait correspondre à la balance de votre compte courant de votre banque et vous pouvez utiliser « balance » pour être sûr que vous n'êtes pas à découvert sur votre compte, étant donné qu'il tient compte par exemple des chèques qui n'ont pas encore été encaissés.

Générer le rapport

Après que le bloc principal ait répété le même processus pour chaque enregistrement en entrée, nous avons maintenant un enregistrement assez complet d'opérations au crédit et au débit du compte décomposées par rubrique et par mois. Maintenant, tout ce dont nous avons besoin de faire est de définir un bloc END qui va générer un report, en voici un modeste dans notre cas :

Exemple de code 1.30 : Générer le rapport final

END {
    bal=0
    bal2=0
    for (x in globcat) {
        bal=bal+balance[0,x]
        bal2=bal2+balance2[0,x]
    }
    printf("Vos fonds disponibles : %10.2f\n", bal)
    printf("La balance de votre compte : %10.2f\n", bal2)
}

Ce report affiche un résumé qui ressemble à quelque chose de ce genre :

Exemple de code 1.31 : Rapport final

Vos fonds disponibles :    1174.22
La balance de votre compte :    2399.33

Dans notre bloc END, nous utilisons la construction « for (x in globcat) » pour itérer à travers chaque rubrique, pour comptabiliser la balance principale basée sur toutes les transactions enregistrées. Nous comptabilisons en fait deux balances, l'une pour les fonds disponibles et une autre pour la balance du compte. Pour exécuter le programme et traiter vos données financières que vous avez saisies dans un fichier appelé mycheckbook.txt, mettez tout le code vu précédemment dans un fichier texte appelé balance et faîtes un chmod +x balance puis tapez ./balance mycheckbook.txt. Le script balance ajoutera alors toutes vos transactions et affichera pour vous un résumé de votre balance de deux lignes.

Possibilités d'évolution du programme

J'utilise une version plus avancée de ce programme pour gérer mes finances personnelles et professionnelles. Ma version (que je ne peux pas inclure ici par manque de place) affiche un bilan mensuel de mes revenus et de mes dépenses, incluant les totaux annuels, le revenu net et d'autres petites informations utiles. Encore mieux, il génère les données dans un format HTML, ce qui me permets de les consulter dans un navigateur Internet :) Si vous trouvez ce programme utile, je vous encourage à ajouter ces fonctionnalités. Vous n'aurez pas besoin d'enregistrer des informations supplémentaires. Toutes les informations dont vous avez besoin sont déjà dans « balance » et « balance2 ». Vous n'avez juste qu'à mettre à jour le bloc END et vous serez prêt pour les affaires !

J'espère que vous avez apprécié cette série d'articles. Pour plus d'informations sur Awk, consultez les ressources listées ci-dessous.

2.  Ressources