awk est un langage de script particulièrement intéressant dans l'analyse et le traitement de fichier texte ainsi que dans la génération de rapports. Nous présentons dans cette partie ces principales caractéristiques sur des exemples.
On suppose vouloir extraire du fichier
/etc/passwd les uids,
gids et nom d'utilisateur, le tout trié en majeur
sur l'uid et en mineur sur le
gid. Bien entendu, les lignes de commentaires ne
doivent pas apparaître. Voilà typiquement un travail pour
awk.
EXEMPLE :
$ awk -F":" '$0 !~ /^#/ { print $3"\t"$4"\t"$1 }' /etc/passwd | sort -n -k 1,2
0 0 root
0 0 toor
1 1 daemon
2 5 operator
3 7 bin
4 65533 tty
5 65533 kmem
7 13 games
8 8 news
9 9 man
22 22 sshd
25 25 smmsp
26 26 mailnull
53 53 bind
66 66 uucp
67 67 xten
68 6 pop
70 70 pgsql
80 80 www
88 88 mysql
666 666 pascal
65534 65534 nobody
L'option -F de l'interpréteur
awk permet de spécifier le séparateur de champ, ici
le caractère : . Le reste dit que si la ligne dénotée par
$0 ne commence pas par le caractère # (c'est la syntaxe de
csh, i.e. du langage
C) alors on imprime sur la sortie standard les
trois champs dénotés $3, $4 puis $1 (désignant respectivement
l'uid, le gid et le nom
d'utilisateur), lesquels sont repris en entrée de la commande
sort qui trie numériquement (option
-n) respectivement sur les champs positionnels 1 et 2
(i.e. l'uid et le
gid). Noter enfin, l'utilisation des quotes qui
permet d'empêcher l'évaluation des variables $0, $1, $3 et $4 par le
shell.
L'utilisation de awk ne se limite pas aux scripts à une ligne (les fameux one-liners), c'est un véritable langage de programmation doté de structures de contrôles et manipulant des variables.
Si le code du script awk devient complexe et/ou
s'étend sur plusieurs lignes, il est possible de le mettre dans un
fichier indépendant. Pour l'invoquer ensuite il suffit de passer
l'option -f à l'interpréteur, comme suit :
EXEMPLE :
$ awk -f myscript.awk <fichier_à _traiter>
La structure typique d'un script awk est composé de trois blocs :
STRUCTURE D'UN SCRIPT awk :
BEGIN {
# bloc spécial d'initialisation, facultatif, exécuté avant que awk ne commence
# à traiter le fichier d'entrée <fichier_à _traiter>
}
{
# bloc de code exécuté à chaque ligne du fichier <fichier_à _traiter>
}
END {
# bloc spécial de terminaison, facultatif, exécuté après que awk ait terminé
# le traitement du fichier d'entrée <fichier_à _traiter>
}
Toute ce qui suit le caractère # est considéré comme un commentaire qui est ignoré par l'interprète awk.
Si nous reprenons sous forme de script externe, le one-liners précédent (c.f. Section 4.1, « Premier exemple »), cela peut donner :
CODE : # ~/bin/myscript1.awk BEGIN { # block d'initialisation FS=":" # Field Separator : positionné à ":" OFS="\t" # Output Field Separator : positionné à "\t" } { # block d'execution if ( $0 !~ /^#/ ) { print $3,$4,$1 } } END { # block final } UTILISATION : $ awk -f ~/bin/myscript1.awk /etc/passwd | sort -n -k 1,2
Exemple 2. Autre exemple, trouver l'uid maximum dans
le fichier /etc/passwd et l'afficher ainsi que
le nom de login et le gid du groupe
correspondant.
CODE : # ~/bin/myscript2.awk BEGIN { # block d'initialisation FS=":" # Field Separator : positionné à ":" OFS="\t" # Output Field Separator : positionné à "\t" uid=0 gid=0 name="" } { # block d'execution - recherche de l'uid et du gid maximun if ( $0 !~ /^#/ ) { if ( $3 > uid ) { uid=$3 gid=$4 name=$1 } } } END { # block final print "nom = ", name, "uid max = ", uid, "gid = ", gid } UTILISATION : $ awk -f ~/bin/myscript2.awk /etc/passwd nom = nobody uid max = 65534 gid = 65534
Example 3. La commande ps aux permet sur un système BSD d'obtenir par processus, entre autre, la taille de la mémoire virtuelle utilisée (champs vsz) ainsi que la taille de la mémoire réelle utilisée (champs rss), l'unité étant le kilo-octet. On désire avoir le total pour ces deux paramètres tous processus confondus.
CODE : # ~/bin/mypstot.awk BEGIN { # block d'initialisation FS=" " totvsz = 0 totrss = 0 } { # block d'execution if ( $0 !~ /^USER/ ) { totvsz += $5 totrss += $6 } } END { # block final printf("Total VSZ = %6d\t\ttotal RSS = %6d\n", totvsz, totrss) } UTILISATION : $ ps aux | awk -f ~/bin/mypstot.awk Total VSZ = 876712 total RSS = 701040
awk permet l'exécution conditionelle d'un bloc. Il suffit pour cela de préfixer le bloc à exécuter par une expression à valeur booléenne, i.e. la chaîne vide et l'entier 0 sont interprétés comme la valeur booléenne faux , le reste est vrai .
Les expressions régulières (ou rationnelles).
EXEMPLES :
/^root/ { print } # n'imprime que les lignes commençant par le motif root
/csh$/ { print } # n'imprime que les lignes finissant par le motif csh
/[0-9]+:[A-Za-z]*/ { print }
# n'imprime que les lignes contenant un motif structuré
# ainsi entier puis ":" puis chaîne de caractères
! /root/ { print } # n'imprime que les lignes ne contenant pas le motif root
Les expressions conditionnelles.
EXEMPLES :
$1 == "pascal" { print } # (1) imprime la ligne si le premier champ est égale à
# "pascal" (égalité de chaîne de caractères)
$5 !~ /root/ { print } # (2) imprime la ligne si le 5e champ ne contient pas l
# motif root
( $1 == "foo" ) && ( $3 > 666 ) { print }
# (3) imprime la ligne corresp. si le 1er champ est une
# chaîne égale à "foo" et le 3e champ un entier plus
# grand strictement à 666
La structure conditionnelles. awk offre l'instruction if. Tous les exemples pécédents peuvent ainsi être réécrit.
EXEMPLES :
{ # reprise du (1) précédent
if ( $1 == "pascal" ) {
print
}
}
{ # reprise du (2)
if ( $5 !~ /root/ ) {
print
}
}
{ # reprise du (3)
if ( ( $1 == "foo" ) && ( $3 > 666 ) ) {
print
}
}
boucle while.
EXEMPLE : # # ~/bin/test.awk # BEGIN { FS=":" ORS="\t" nbfield=0 } { x = 1 while ( x < NF ) { # NF var. spéciale donnant le nombre de champs (Number of print $x # Fields) de la ligne courante x++ } print NF, " champs lus \n" nbfield += NF } END { print "\ttotal : ", nbfield, " champs lus \n" } UTILISATION : $ awk -f ~/bin/test.awk /etc/group # $FreeBSD src/etc/group,v 1.19.2.3 2002/06/30 17 57 4 champs lus 1 champs lus wheel * 0 4 champs lus daemon * 1 4 champs lus kmem * 2 4 champs lus sys * 3 4 champs lus tty * 4 4 champs lus operator * 5 4 champs lus mail * 6 4 champs lus bin * 7 4 champs lus news * 8 4 champs lus man * 9 4 champs lus staff * 20 4 champs lus sshd * 22 4 champs lus smmsp * 25 4 champs lus ... total : 137 champs lus
boucle do ... while. Ici, l'évaluation de la condition se fait après la fin de la boucle.
EXEMPLE : # # ~/bin/test2.awk # BEGIN { FS=":" ORS="\t" nbfield=0 } { x = 1 do { print $x x++ } while ( x < NF ) print NF, " champs lus \n" nbfield += NF } END { print "\ttotal : ", nbfield, " champs lus \n" } UTILISATION : $ awk -f ~/bin/test2.awk /etc/group # $FreeBSD src/etc/group,v 1.19.2.3 2002/06/30 17 57 4 champs lus # 1 champs lus wheel * 0 4 champs lus daemon * 1 4 champs lus kmem * 2 4 champs lus sys * 3 4 champs lus tty * 4 4 champs lus operator * 5 4 champs lus mail * 6 4 champs lus bin * 7 4 champs lus news * 8 4 champs lus man * 9 4 champs lus ... total : 117 champs lus
boucle for.
EXEMPLE : # # ~/bin/test3.awk # BEGIN { FS=":" ORS="\t" nbfield=0 } { for ( x = 1; x < NF; x++ ) { print $x } print NF, " champs lus \n" nbfield += NF } END { print "\ttotal : ", nbfield, " champs lus \n" } UTILISATION : $ awk -f ~/bin/test3.awk /etc/group # $FreeBSD src/etc/group,v 1.19.2.3 2002/06/30 17 57 4 champs lus # 1 champs lus wheel * 0 4 champs lus daemon * 1 4 champs lus kmem * 2 4 champs lus sys * 3 4 champs lus tty * 4 4 champs lus operator * 5 4 champs lus mail * 6 4 champs lus bin * 7 4 champs lus news * 8 4 champs lus man * 9 4 champs lus ... total : 117 champs lus
instructions break et continue. Ces deux intructions permmettent de rompre le déroulement classique d'une boucle. La première permet de sortir (inconditionnellement quand elle est rencontrée) du corps de la boucle la plus interne. La seconde, quant à elle, relance une nouvelle itération en shuntant littérallement le reste du code de la boucle.
EXEMPLE : # # ~/bin/test4.awk # { i = 1 while (1) { # boucle infinie # 'continue' conduit ici if ( ( i % 4 ) == 0 ) { i++ continue } print "iteration " i if ( i > 20 ) { break } i++ } # 'break' conduit là print "termine : " i } UTILISATION : $ awk -f ~/bin/test4.awk <ENTREE> iteration 1 iteration 2 iteration 3 iteration 5 iteration 6 iteration 7 iteration 9 iteration 10 iteration 11 iteration 13 iteration 14 iteration 15 iteration 17 iteration 18 iteration 19 iteration 21 termine : 21 ^C
Il existe deux types scalaires : les entiers et les chaines de caractères (vus dans les exemples précédents) et un type structuré : tableau.
Attention, contrairement au langage C une chaîne de caractères n'est pas un tableau de caractères.
AWK met à la disposition du programmeur des fonctions standard prédéfinies, dont voici quelques unes :
length(chaine) : donne la longueur de son argument, par défaut $0
substr(chaine_1,int_1,int_2) : donne la sous chaine de chaine_1 commençant à la position int_1 et de longueur inférieure ou égale à int_2
split(chaine, tab, car): éclate la chaine, les sous chaine sont récupérées en tab[1], tab[2]... Le caractère car sert à délimmiter les différents morceaux.
index(chaine_1, chaine_2): donne la position de la premiere occurence de chaine_2 dans chaine_1.
Il existe des variables prédéfinies dans awk, les voici :
FS : séparateur de champ dans le fichier (par défaut espace ou tabulation), un nombre quelconque de séparateur étant équivalent à un seul. L'option -F permet de modifier à l'appel de la commande la valeur de ce séparateur.
RS : séparateur d'enregistrement en entrée dans le fichier (par défaut le caractère de fin de ligne, un enregistrement étant alors une ligne)
OFS : séparateur de champ en sortie (par défaut espace)
ORS : séparateur d'enregistrement en sortie (par défaut le caractère fin de ligne)
NF : nombre de champ de l'enregistrement courant
NR : numéro de l'enregistrement en cours de traitement
FILENAME : nom du fichier en cours de traitement
Ce type structuré permet de stocker différents éléments (de type différents dans une structure naturellement indéxée par des chaînes de caractères. Tous les tableaux awk sont donc des tableaux associatifs, y compris ceux indexé par des entiers, puisqu'en awk un entier est représenté par sa chaîne.
La simple déclaration suivante, permet de définir un tableau :
EXEMPLE :
myarr[0] = "World"
myarr[1] = 72
myarr[2] = "Hello"
L'origine de l'indexation à 0 ou à 1 est laissé au libre choix du programmeur, awk ne pose aucun problème. La construction suivante permet d'itérer sur le contenu du tableau :
EXEMPLE :
for ( i in myarr ) {
print myarr[i]
}
Toutefois, il n'y a aucune garantie sur l'ordre d'impression.
Suppression [delete] d'éléments dans un tableau :
EXEMPLE :
delete myarr[1] # suppression de l'éléments indexé par l'entier 1
appartenance d'un élément à un tableau [in] :
EXEMPLE :
{
elem=72
if ( elem in myarr ) {
...
}
else {
...
}
...
}
On dispose de différents fichiers à la structure identique : une première ligne non pertinente qui pourra être ignorée, suivi d'un ensemble de lignes structurées en 13 champs séparés soit par des caractères espaces, soit par des caractères de tabulation. Voici un exemple de ligne :
str1 int2 int3 int4 int5 int6 int7 int8 int9
int10 int11 int12 int13
Travail à faire. On souhaiterait faire la somme des colonnes de chacun des
fichiers et écrire les résultats obtenus dans un fichier résultat,
au format suivant : file_i tot2 tot3 tot4 tot5 tot6
tot7 tot8 tot9 tot10 tot11 tot12 tot13. On pourra
supposer que chaque fichier dispose d'une marque explicite de fin
(ici <<EOF>>).
Réponse :
CODE : BEGIN { # Proposition de Correction - Les fichiers possèdent une marque spéciale de # fin : <<EOF>> FS= " " # Field Separator OFS = "\t" ORS = "" RT = "^D" MAX=12 for ( i=0; i < MAX; i++ ) { mysum[i] = 0 mytot[i] = 0 } } { if ( $0 ~ /^<<EOF>>$/ ) { # Afficher les totaux des stats du fichier print FILENAME, " " for ( i=0; i < MAX; i++ ) { printf("%5s", mysum[i]) } print "\n"; # (re)initialiser le tableau for ( i=0; i < MAX; i++ ) { mysum[i] = 0 } } else { # totaliser for ( i=0; i < MAX; i++ ) { mysum[i] += $(i+2) mytot[i] += $(i+2) } } } END { print "TOTAL", " " for ( i=0; i < MAX; i++ ) { printf("%5s", mytot[i]) } print "\n"; } UTILISATION : $ awk -f sumstat.awk stat1 stat2 stat3 stat1 5 6 5 6 5 6 6 7 6 5 4 6 stat2 5 6 5 6 5 6 6 7 6 5 4 6 stat3 14 15 5 6 5 15 6 7 6 5 13 6 TOTAL 24 27 15 18 15 27 18 21 18 15 21 18
Travail à faire. Récupérer l'adresse IP et MAC des interfaces réseaux de votre machine en utilisant les valeurs données par la commande ifconfig
Travail à faire. Simuler (en partie) avec un script awk la commande wc (word count), qui sans option supplémentaire donne respectivement le nombre de ligne(s), de mot(s) (relativement à un séparateur prédéfini), d'octet(s) du fichier qui lui est passé en argument.
Réponse : Simuler wc.
CODE : # ~/bin/wc.awk BEGIN { # block d'initialisation RS="\n" # Record Separator OFS="\t" # Output Field Separator line=0 # var. utilisateur word=0 byte=0 } { # block d'execution - comptabiliser le nombre de ligne, mot, octet line++ word += NF byte += length($0) + 1 # donne la longueur de la ligne au complet } END { # block final print "", line, word, byte, FILENAME } UTILISATION : $ wc /etc/passwd # la commande originale 29 79 1589 /etc/passwd $ awk -f ~/bin/wc.awk /etc/passwd 29 79 1589 /etc/passwd $ wc /etc/motd # la commande originale 24 154 1074 /etc/motd $ awk -f ~/bin/wc.awk /etc/motd 24 154 1074 /etc/motd