====== Avant propos ====== Cette page ne se veut certainement pas complète, ni une référence, ni un cours, ni quoi que ce soit. Ce n'est qu'une très modeste introduction, juste pour donner envie et faire découvrir le potentiel de cette commande. ====== Qu'est ce que awk ? ======= AWK est tout à la fois un langage et un programme portant ce même nom. Le contexte permet en général de savoir de quoi on parle. ====== À quoi sert awk ? ====== Awk est une sorte de grep programmable; donc ** awk traite des fichiers textes **. Mais en plus, de sélectionner simplement des lignes, awk peut aussi travailler sur les colonnes très simplement. C'est même **l'outil idéal quand on veut travailler sur les colonnes** d'un fichier texte. Mais awk est bien plus que ça, comme il y a un langage assez riche, on peut faire des calculs, avant de commencer à lire les fichiers texte (initialiser tout un tas de trucs et de machins), faire des calculs après avoir lu toutes les lignes de tous les fichiers, pour générer un rapport par exemple. Si awk est très complexe -- plusieurs livres lui sont consacrés -- il peut aussi être infiniment simple et **pratique** pour un travail en ligne de commande au quotidien. Les exemples que nous donnons ci après ne sont qu'une microscopique partie de la partie émergée de l'iceberg. ====== Un peu d'histoire ====== Aho Weinberger Kernighan se réunissent dans un bateau et tombent à l'eau Que reste-t-il ? AWK. On distinguera awk, la commande originale, encore trouvable sur des systèmes dont l'historique est plus long que celui des jeunes et souvent plus familiers GNU/Linux, tel que Solaris par exemple, du new awk (nawk sur ces systèmes), (version normalisée POSIX ?) arrivée un peu plus tard sur le marché. Les implémentations GNU/Linux de awk, sont en fait des new awk. La présente page ne parle que de new awk. Conclusion, si vous désirez essayer les exemples, essayez d'abord avec une hypothétique commande nawk, si nawk n'existe pas, essayez avec awk (ce sera sans doute un new awk). Dernière petite chose. On trouve en général la commande awk dans /usr/bin. Certains systèmes GNU/linux ont eu la mauvaise idée de la mettre dans /bin. Pas très grave, pensez vous car en général, les deux sont dans le **PATH**... Oui, mais tout de même, on peut faire des scripts en awk, et le shebang (!# /usr/bin/awk -f) devient faux, et le script inutilisable si le binaire n'est pas la où on l'attend. ====== Les implémentations de awk ====== Tout dépend, et, pour reprendre la section précédente, on peut avoir à faire au awk d'origine ou à un nawk. il y a donc : * awk * nawk (POSIX ?) * mawk : implémentation petite et rapide de nawk, utilisée par défaut sur les systèmes [[http://www.debian.org|Debian GNU/Linux]], contient des bugs sévères non corrigés. * gawk : encore une [[http://www.gnu.org|GNU]]rie. D'aucuns lui trouvent certains avantages, la possibilité de ne pas tenir compte de la casse (majuscule/minuscule), internationalisation etc... Pleins d'extensions "non standard", gros, lent, mais certainement innovant. (en particulier il est le seul a bien vouloir gerer les '\0') * sans doute pleins d'autres, faites nous parvenir vos découvertes. ====== Les rapports aux "autres" programmes de manipulations de données textes ====== ===== sed / awk ===== * Si vous voulez traiter des colonnes, ou des paragraphes entiers, utilisez awk. * Si vous voulez juste remplacer une chaine par une autre dans un texte, la coutume est d'utiliser sed. * Si vous voulez extraire une ligne ou deux et faire un peu de modification de texte : * Si le critère de sélection de ligne est immédiat (présence d'un marqueur) utilisez sed. * Si le critère est complexe (la somme des nombres situés en colonne 4 et 7 est supérieure à 25), utilisez awk. d'une manière général, sed n'a qu'une seule variable -- et seuls quelques vieux sages savent s'en servir --, donc, si les critères font intervenir une mémorisation quelconque, ou un calcul, utilisez awk. ===== grep ===== En utilisation courante, c'est égal. grep est plus direct et est l'outil classique. Disons que //bla | grep toto// indique clairement ce qu'on fait, alors que //bla | nawk /toto/ ...// nécessite qu'on réfléchisse à ce qui est fait par awk (car il peut faire n'importe quoi) La version [[http://www.gnu/org|GNU]] de grep a bien des avantages, comme la coloration de la chaine recherchée. De plus grep peut afficher facilement des lignes de contexte avant et après la ligne recherchée (options -B -A et -C), ce qui nécessite un programme complet avec awk. ===== Perl ===== Perl est une //monster application//. Son initialisation est très longue, et très couteuse (ouverture de nombreux fichiers, recherche de modules dans un grand nombre de répertoires différents, etc...) Si c'est pour remplacer trois lettres dans une ligne, utiliser sed. Si c'est pour extraire trois colonnes, utiliser awk. Sérieusement, perl n'est pas comparable et ne joue pas dans la même cour. Cela étant dit, dans un domaine bien particulier, et selon les dire du grand Larry lui même, awk est supérieur à perl. Selon la page de manuel de perlvar : Remember: the value of $/ is a string, not a regex. awk has to be better for something. :-) ======Exécuter awk ====== Non, nous ne parlons pas ici de mise à mort, mais de la manière de se servir de awk. L'invocation standard est : awk 'commande awk' fichier fichier... et cela signifie : exécuter la 'commande awk' pour l'ensemble des fichiers.\\ exemple : awk '{ print NR, $0 }' *.txt Si aucun fichier n'est donné, awk lit son entrée standard, ce qui fait qu'on peut l'utiliser dans les pipes, en filtre.\\ exemple : ls -l | awk '{ print ; s += $5 } END { print "total =", s }' Enfin, on peut définir des variables awk avant d'exécuter quoi que ce soit. Les deux lignes d'exemple ci dessous sont equivallentes. ls | awk -v x=coucou '{ print x, $0 }' ls | awk 'BEGIN { x="coucou" } { print x, $0 }' Si votre programme en awk est tellement complexe et long qu'il ne tient plus sur la ligne de commande, vous le mettrez peut-être dans un fichier. À ce moment là, vous exécuterez votre script de la manière suivante ; awk -f fichier-script fichier-a-traiter... Vous pouvez également rendre votre fichier-script exécutable grâce à //chmod 755 fichier-script// par exemple, et y ajouter à la première ligne le shabang idoine. Vous pourrez alors exécuter le script comme toute autre commande : en tapant son nom. \\ Voilà un exemple de script en awk, pas très utile cependant : #! /usr/bin/awk -f BEGIN { x="coucou" } { print x, $0 } ===== Des exemples simples ==== Ces exemples simples permettent d'approcher doucement awk. L'idée de awk est que les lignes sont composées de colonnes; la définition de la colonne étant la définition naturelle. * extraire une ligne contenant un modèle donné (une expression rationnelle) grep 'regexp' awk '/regexp/' * extraire la colonne 4 awk '{ print $4 }' * extraire la dernière colonne awk '{ print $NF }' * extraire la colonne 7 et 5, si le nombre en colonne 3 est plus grand que pi awk '$3 > 3.141592654 { print $7, $5 }' * lignes paires : awk '!(FNR % 2)' * lignes impaires : awk 'FNR % 2' Si votre fichier a des colonnes séparées par autre chose que des blancs, par exemple des : comme le fichier /etc/passwd, utiliser l'option -F exemple : awk -F : '$3 > 150 { print $1, $3, $4, $7 }' /etc/passwd Mais pourquoi utilise-t-on des { } de temps en temps mais pas toujours ? C'est parce qu'un script awk est composé de tests et d'actions. Les actions son entourées de { } , pas les tests. Cela est expliqué dans la section suivante. ====== Langage awk ====== Entrons enfin dans le vif du sujet. Grosso modo le langage awk est du C (kernighan oblige), mais interprété et où les variables peuvent être de n'importe quel type. (entier, flottant ou chaine) on note les chaines entre "", comme dans x="coucou" les entiers et les flottants s'écrivent normalement : * x=4 * pi=3.14159 * z=1.5e12 **Attention à vos réglages internationaux** Si votre variable LC_ALL ou LC_NUMERIC vaut fr_FR ou un truc du genre, vous allez vous retrouver avec des nombres à ****virgule**** oui, à virgule, pas a point (.), et cela sera sans doute la cause de bien des maux. awk lit des enregistrements (des lignes) qu'il découpe en champs (en colonnes). la ligne de base est aussi conservée sans modification. On peut changer ce réglage grâce à des variables internes -- dont les noms sont toujours en majuscule -- Ici il s'agit des variable RS (Record Separator) et FS (Field Separator). Pour lire des paragraphes entiers et découper en ligne, il faut que RS soit vide et que FS contienne un retour chariot. Exemple : awk 'BEGIN { RS="" ; FS = "\n" } { printf "enregistrement %3d, nombre de champs %d\n, %s\n", NR, NF,\ $0 }' NB : On utilisera maintenant le terme ligne pour enregistrement et le terme colonne pour champ, car c'est ce qu'on fait le plus souvent. Ne perdez pas de vue cependant que cela peut être changé. ===== Structure d'un script ===== Un script en awk est une suite de paire : ''test { action }'' Pour chaque ligne, un test est effectué, et s'il réussit, l'action associée est exécutée. Si l'action précédente n'a pas provoqué la fin du programme (exit) ou le passage à une nouvelle ligne (next), le test suivant est évalué et son action associée est exécutée s'il a réussi, etc... * Ne pas mettre de test est considéré comme un test qui réussit toujours * Ne pas mettre d'action provoque l'action par défaut si le test réussit : l'affichage de la ligne. * omettre un test et une action (faire un script vide) ne fait rien. * le test ''BEGIN'' est vrai avant de commencer la lecture de quoi que ce soit on l'utilise pour initialiser. on peut mettre plusieurs tests ''BEGIN'' dans un script, leurs actions seront toutes exécutées avant de commencer le traitement des données. * le test ''END'' est vrai après que la dernière ligne soit lue. on l'utilise habituellement pour générer des rapports, afficher des résultats lentement collectés au cours de la lecture... On peut mettre plusieurs tests ''END'' dans un script, comme pour les tests ''BEGIN'' les actions seront toutes exécutées, sauf si l'une d'elles a recours à la commande ''exit'' de awk, bien sûr. Les tests peuvent être n'importe quelle expression qui s'évalue numériquement ou "chainement" ou une expression rationnelle entre ''/'' ou toute combinaison de tout cela grâce aux opérateurs et-logique (''&&''), ou-logique (''||'') etc... * une expression numérique est "vraie" si elle est non nulle, * une expression chaine est "vraie" si elle est non vide, * une /expression rationnelle/ est "vraie" si elle concorde avec la ligne en cours de traitement, ainsi les scripts suivants affichent toutes les lignes de leur entrée : awk 1 awk '"a"' et ceux là n'affichent rien : awk 0 awk '""' Voilà quelques exemples de test possible et leur signification : /toto/ { print "la ligne\n\t" $0 "\ncontient 'toto'" } $4 == "toto" { print "dans\n\t", $0 "\nle 4eme mot est toto" } $3 ~ /toto/ { print "dans\n\t", $0 "\nle 3eme mot contient 'toto'" } $3 ~ /^toto/ { print "dans\n\t", $0, "\nle 3eme mot commence par 'toto'" } NF > 1 && $(NF - 1) !~ /toto$/ { print "dans\n\t", $0, "\nl avant dernier mot ne se termine PAS par 'toto'" } NF > 4 && $(NF - 4) { print "dans la ligne\n\t" $0 \ "\nil y a au moins 5 colonnes", "\nle quatrieme avant dernier mot est non nul (", $(NF - 4), ")" } # Les chaines sont plus grandes que les nombres, donc # ce test n est correct que si on est certain de ne rencontrer que des # nombres dans les colonnes 3, 5, 7, 8 /xyzzy/ || $5 > 3 && ( $3 < $5 || $7 >sin( 0.732 * $8 )) { print "SURPRENEMENT, dans \n\t" $0 print "\tsoit il y a 'xyzzy'" print "\tsoit le nombre en colonne 5 est plus grand que 3 ET" print "\t\tle nombre en colonne 3 est plus petit que le nombre en colonne 5" print "\t\tou bien" print "\t\tle nombre en colonne 7 est plus grand que sinus( 0,732 * le nombre en colonne 8 )" } # On peut ruser un peu en multipliant chaque colonne par 1.0, car # une chaine * un nombre = 0. Malgre tout, si seule la colonne 8 # contient une chaine mais pas un nombre, ce test peut reussir tout # de meme si les autres conditions sont remplies. /xyzzy/ || $5 * 1.0 > 3 && ( $3 < $5 || $7 * 1.0 >sin( 0.732 * $8 )) { print "PRESQUE CORRECTEMENT, dans \n\t" $0 print "\tsoit il y a 'xyzzy'" print "\tsoit le nombre en colonne 5 est plus grand que 3 ET" print "\t\tle nombre en colonne 3 est plus petit que le nombre en colonne 5" print "\t\tou bien" print "\t\tle nombre en colonne 7 est plus grand que sinus( 0,732 * le nombre en colonne 8 )" } ===== La magie de awk ===== une part importante de la magie de awk réside dans son opérateur $. Opérateur qu'il ne faut pas confondre avec le $ du shell. D'ailleurs les scripts en awk se mettent presque toujours entre ' ' pas entre " " Cet opérateur est le sélecteur de colonne, et il attend un entier. $0 correspond à la ligne telle qu'elle a été lue. exemple : afficher le dernier mot de la ligne awk '{ print $NF }' NF est une autre variable magique de awk, elle contient le nombre de colonnes, et est mise à jour à la lecture de chaque nouvelle ligne. afficher l'avant dernier mot de la ligne : awk '{ print $(NF - 1) }' Une autre part de magie réside dans les tableaux. Les tableaux sont des tableaux associatifs (ce qu'on appelle dictionnaire en Postscript, Hash en perl). L'indice peut être n'importe quoi. cela permet d'associer une valeur à une clef, comme dans l'exemple suivant : awk ' # pour chaque ligne { # pour chaque mot for( i = 1; i <= NF; ++i ) { # incrementer la frequence du mot frequence[ $i ] ++ } # et maintenant, un petit rapport. END { for( i in frequence ) { printf "frequence[ %s ] = %s\n", i, frequence[i] } } ' *.txt autre exemple : nawk ' # Pour les lignes commençant par "From:" /^From:/ { de[$2]++ } # A la fin, on fait un petit rapport. END { for( i in de ) { printf "%s a ecrit %d messages\n", i , de[i] } } ' /var/mail/$USER ====== Awk facile dans le monde réel ====== Et maintenant on va faire simple \o/ \\ Comment résoudre des problèmes du monde réel avec awk (gawk dans mon cas) plutôt qu'a coup de grep | sed | cut | sort | ... ==== Petit résumé ==== * Un script awk se compose de 3 parties : * BEGIN { } : Ce qui sera fait avant de commencer a traiter le fichier donné. * Le corps du script * END { } : Ce qui sera fait après avoir traité la totalité du fichier * Mots clés et variables utiles : * FS (field separator) : Definit le separateur de champs. ex : BEGIN { FS="\t" } pour dire qu'on travaille sur un fichier tabulé. A ma connaissance, aucune implémentation n'accepte de regexp comme séparateur de champs. * RS (record separator) : Definit le separateur de ligne. (pareil, on le met dans le BEGIN { }) * NR : Nombre de champs dans la ligne courante * NF : Dernier champs d'une ligne. ex : print $NF affiche le dernier champs. Utile aussi dans les boucles for : for (i=1 ; i<=NF ; i++) * $0 : print $0 pour afficher toute la ligne * $1, $2, $n : premier champ, 2ème champ, nième champ. * next : dit a awk de sauter a la ligne suivante sans s'occuper du reste. du corps du script. Y'a des milliards d'autres trucs mais on a déjà le plus utile rien qu'avec ca. ==== De la pratique ==== Prenons un fichier dont les champs sont séparés par des ";" et les lignes par des "\n", les lignes commencants par "#" sont ignorées. #ceci est un commentaire 1;plop;coin 42;quatre;deux #mince, encore un commentaire 12;foo;bar etc ... * Première étape : travailler dans un script. Les oneliners c'est pour les barbares et les perlistes. * 2ème étape : définir certaines de nos variables dans BEGIN {} #!/usr/bin/awk -f BEGIN { FS=";" RS="\n" } * 3ème étape : ignorer les commentaires /^#/ { next } * maintenant il faut être un peu fun, et n'afficher par exemple que les lignes dont le 2ème et 3ème champs sont identiques. * en fait c'est surtout manière d'introduire la notion de fonction :) function are_equal(a,b) { return (a == b) } * Une fois qu'on a cette fonction écrite ce qui est presque trop facile, on n'a plus qu'a écrire la ligne awk qui va bien, qui est elle aussi presque trop facile... { if(are_equal($2,$3)) { print $0 } } Bon bon bon, je suis désolé mais c'est encore beaucoup trop facile. === Un exemple complet, utile et facile === On reprend le même fichier qu'au dessus, et on veut afficher et compter le nombre de ligne dont le champ 2 et 3 sont égaux (tout en ignorant les lisgnes #commentées. \\ Il se passe de discours, j'espère. #!/usr/bin/awk -f BEGIN { FS=";" RS="\n" mon_total = 0 } /^#/ { next } { if(are_equal($2,$3)) { print $0 mon_total++ } } function are_equal(a,b) { return (a == b) } END { print "mon total : " mon_total print "petit bonus, pourcentage de ligne avec les 2 champs egaux : " mon_total/NR*100 "%" } Trop facile \o/ presque ... il y a une petite subtilité : NR compte le nombre de ligne du fichier, commentaire ou pas. si on veut un pourcentage qui ne tiens pas compte des commentaire il faut rajouter un 2eme compteur (a initialiser dans BEGIN) et a incrementer pour chaque ligne qui ne sont pas un commentaire. \\ Bah, il faudrait faire un truc du genre : { if(are_equal($2,$3)) { print $0 mon_total++ } nocomment_total++ } et faire un mon_total/nocomment_total*100 ** A suivre, ... ** ====== Hadoop-pig : Le Awk taille adulte ====== Awk montre vite ses limites quand il s'agit de parser plusieurs Go de texte (des logs apache sur un site a forte audience), par exemple. Pour ça, il y a [[:filtre:HadoopPig|Hadoop Pig]]. Je ferai une doc la dessus, a l'occasion. Parce que c'est simple et ca marche(c)(r)(tm) (et ca fait beaucoup plus que awk bien sur, mais on peut l'utiliser aussi simplement qu'un awk).\\ En attendant, y'a http://hadoop.apache.org/pig/ ====== Historique du document ====== * Première version de Christophe Martin (10 Mars 2006) * Christophe Martin : Correction d'orthographe et ajout d'exemples commentés (19 avril 2009) * Awk facile dans le monde réel : Laurent "ker2x" Laborde (Juin-Juillet 2010)