La boîte à outils du linuxien

Après avoir vu awk la fois dernière, je vais, pour continuer l'exploration de la boîte à outils de base du petit linuxien, vous présenter au travers de quelques exemples, d'autres outils pratiques trouvables dans toutes les distributions linux. Les cas résolus ici peuvent l'être de nombreuses façons, celles que je donne ne sont pas forcément les meilleures mais elles marchent.

sed et less, l'introduction

sed veut dire stream editor. C'est à dire que ce n'est pas un éditeur vous permettant de saisir des fichiers mais un outil vous permettant d'éditer du contenu existant dans des flux. Un flux est généralement obtenu en lisant un fichier mais peut également être le fruit d'un pipe (|, ou [AltGr]+6 pour les intimes). Par exemple, lorsque vous tapez ls -la dans un répertoire plein de fichiers et dossiers, le flux résultant vous remplit votre console, vous empêchant de lire le résultat. Un pipe vous permet d'envoyer le flux "dans" une autre commande, par exemple less, le pager par défaut des pages man. ls -al | less est une commande vous permettant de naviguer dans le résultat de ls -al grâce aux touches fléchées, d'y faire des recherches grâce aux touches '/', 'n' et [Shift]+'n'. On quitte ce mode en tapant 'q'. Tout de suite, ceux qui ont eu l'occasion de faire un dir sous DOS, se rendront compte du confort apporté par cette commande.

J'aimerais revenir plus amplement sur la fonctionnalité de recherche offerte par less, que l'on retrouve donc dans les pages man et lors de toute relecture d'un flux par less. Prenons l'exemple du listing de /etc, on va rechercher dans ce dernier, les occurences de la chaîne de caractère "host". On tape donc ls -al /etc | less. On peut alors naviguer dans le résultat en utilisant les touches fléchées et revenir à la console en tapant 'q'. Pour rechercher "host", on tape /host puis [Entrée]. L'affichage se modifie alors pour afficher la première occurence de host en surbrillance. Pour trouver la suivante, tapez 'n' et pour revenir à la précédente, [Shift]+'n'. Voilà, j'espère que ce truc vous sera utile et vous aura au passage permis d'appréhender la puissance de la réinjection des flux sortants des commandes dans d'autres commandes. On dit que l'on redirige le flux de sortie de la première commande sur le flux d'entrée de la seconde. C'est là la raison d'être du pipe, littéralement le tuyau |. Remarquez au passage que cette astuce vous permet de rechercher du texte dans un fichier en tapant less fichier puis en faisant les manipulations ci-dessus...

Cas pratique 1

On va ici décortiquer la syntaxe de la commande permettant de mettre à jour un site écrit en php pour le rendre conforme à la nouvelle méthode d'accès aux variables fournies par le client. Ces dernières sont maintenant accessibles uniquement par des tableaux, par exemple $_GET ou $_POST, selon la méthode employée pour faire parvenir les variables à php. On peut déroger à cette nouvelle règle en mettant la variable register_globals à on dans le fichier de configuration php.ini mais ça n'est pas une pratique recommandée.

Bref, ce qui nous intéresse ici est de remplacer toutes les occurences de :
if (!$printerversion) {
par
if (!$_GET["printerversion"]) {
et ce dans tous les fichiers de l'arborescence du site dont on a la charge.

J'ai utilisé la méthode ci-dessous pour mettre à jour les pages de mon site sur lequel le test de la variable printerversion permet d'afficher la version imprimable (sans tableau html) ou la version "online".

sed

La fonctionnalité la plus employée de sed est le remplacement. On fournit une expression qui doit être remplacée par une autre et un fichier auquel appliquer ce traitement et sed se charge de vous fournir, sur le flux de sortie standard, votre fichier avec les modifications opérées. La syntaxe pour créer un nouveau fichier avec les modifications est donc sed s/"expression à remplacer"/"expression nouvelle"/g fichier > nouveau_fichier.

Analysons un peu cette commande... On trouve tout d'abord sed, l'exécutable, puis 's' pour "susbstituer" ensuite on trouve l'expression à rechercher, entre slash '/' puis l'expression devant y être substituée, terminée par un autre slash. Le 'g' final permet de spécifier qu'il faudra répéter ce traitement même s'il y a plusieurs occurences de l'expression à rechercher sur la même ligne. Pour vous convaincre de tout celà on va utiliser une fonctionnalité de sed : si on ne lui fournit pas de nom de fichier, sed applique le traitement à ce que vous tapez, ce qui permet de réaliser des tests.
Tapez ceci dans une console : (les retours de l'ordinateur sont en italique). Notez que pour terminer un flux, on utilise la combinaison de touches [Ctrl]+'D'.

zaz2:~$ sed s/toto/titi/
toto
titi
toto a dit "toto"
titi a dit "toto"
zaz2:~$ sed s/toto/titi/g
toto 
titi
toto a dit "toto"
titi a dit "titi"

find

Toujours pour mener à bien notre tâche, nous devons trouver les fichiers php dans une arborescence. On va pour celà utiliser la commande find. Cet outil permet de parcourir les répertoires à la recherche de fichiers correspondant à certains critères tels que leur nom, leur date de modification, poids, appartenance etc ... Nous allons nous servir de l'option -name qui permet de faire une recherche sur le nom, tout bêtement. C'est l'utilisation la plus courante de cette commande. On doit également dire à find où commencer sa recherche, on lui passe donc en premier argument un chemin, qui servira de racine à la recherche, pour commencer la recherche dans le répertorie courant, on utilise ..

La commande utilisée va donc être :find . -name "*.php". N'oubliez pas les guillemets pour protéger la chaîne *.php.

Après vous être placé dans le bon répertoire (la racine de vos fichiers web), tapez cette commande :

xavier@servor:~/public_html > find . -name "*.php"
./index.php
./zlaser/scores.php
./zlaser/index.php
./agenda/admin/choix_groupe.php
./agenda/admin/creer_groupe.php
[...]

Vous voyez défiler les fichiers .php de votre site web avec le chemin d'accès complet vers ces derniers. Ce sera à eux qu'il faudra appliquer le traitement.

for

find permet de faire de nombreuses autres choses comme par exemple de lancer une commande pour chaque fichier trouvé mais nous n'allons pas utiliser cette méthode pour effectuer les remplacements.
Il y a plusieurs raisons à celà comme par exemple une syntaxe difficile et le fait que je ne l'utilise pas.
Je vais vous présenter ma méthode, qui consiste à utiliser la liste de fichiers fournie par find dans une boucle for en shell.

D'abord, familiarisons nous avec la forme de boucle qui nous intéresse :

xavier@servor:~/public_html > for i in 1 4 5 7; do echo $i; done
1
4
5
7

Cette syntaxe signifie que l'on assigne l'une après l'autre les valeurs 1, 4, 5 et 7 (toutes les valeurs entre in et ;) à une variable i.
Pour chaque valeur de i, on exécute les instructions situées entre do et done. On peut faire référence au contenu de la variable i dans ces instructions en écrivant $i.

On adapte ce comportement à nos besoins en utilisant le résultat de la commande find comme valeurs possibles pour la variable. On utilise pour celà le symbole ` accessible par la combinaison de touches [AltGr]+'7'. La plupart des commandes sous unix/linux peuvent agir de façon récursive sur les fichiers contenus dans une arborescence mais je trouve cette façon de faire plus pratique.

for i in `find . -name "*.php"`; do ...; done

Tout ensemble

Tout de suite, la réponse (qui peut être tapée sur une ligne en enlevant les \) :

for i in `find . -name "*.php"`; do \
sed s/"if (\!\$printerversion) {"/"if (\!\$_GET["printerversion"]) {"/g $i > $i.new; \
rm $i; mv $i.new $i; \
done

On retrouve le for qui reprend la liste de fichiers trouvée par find. A chacun de ces fichiers on applique le traitement par sed qui génère un fichier .new. Le fichier original est alors supprimé puis remplacé par le nouveau .new.

Vous remarquez que les chaînes à rechercher ainsi que la chaîne de remplacement dans sed sont mises entre guillemets. C'est une bonne habitude à prendre dans le cas de long remplacements. Enfin, remarquez que les caractères que sed interprète de façon spéciale (! veut dire "non" et $ "fin de ligne" d'habitude) doivent être précédés d'un caractère d'échappement ('\') pour signaler à sed que l'on recherche ces caractères et qu'il ne doit donc pas les interpréter comme à l'accoutumée.

Après l'exécution de cette commande à la racine de l'arborescence de votre site web, vos pages php seront compatibles avec le nouveau comportement de php.

Cas pratique 2

Dans ce deuxième cas pratique, nous allons utiliser grep pour enlever nos propres visites de nos logs de serveur web.

grep est une commande qui recherche des "motifs" dans un ou plusieurs fichiers ou l'entrée standard et affiche les lignes correspondantes.

xavier@servor:~ > grep toto
titi va à l'école
toto n'y va pas
toto n'y va pas
et voilà ! toto est découvert...
et voilà ! toto est découvert...

Dans cet exemple on demande à grep de trouver toutes les occurences de toto dans ce qu'on tape. Il nous affiche les lignes correspondantes.

On peut inverser le comportement de grep, grâce à l'option -v ou --invert-match.

xavier@servor:~ > grep -v toto
titi va à l'école
titi va à l'école
toto n'y va pas
et voilà ! toto n'est plus découvert...

Pour dire à grep de cherher les motifs dans un fichier, il suffit de lui donner le nom du fichier en question.

Admettons que notre ip soit 81.51.145.37 et que les logs contiennent les ip des visiteurs (sinon, il faudra faire pareil avec le nom d'hôte).

On cherche donc à obtenir un fichier access_log amputé des visites concernant l'IP 81.51.145.37.
On utilise grep pour afficher les lignes correspondantes puis, grâce à une redirection (>), on stocke le contenu dans un fichier access_log.new.

Si cette commande s'est bien passée, on exécute la suite. Ce comportement est obtenu grâce à la construction commande1 && commande2. Dans ce cas, la commande2 n'est exécutée que si la commande1 s'est soldée par une réussite. Notez que l'on peut obtenir le comportement inverse (commande2 n'est exécutée que si commande1 échoue) en tapant commande1 || commande2.

En utilisant cette astuce, on ne remplace le contenu du fichier access_log original que si le grep s'est bien exécuté.

grep -v 81.51.145.37 access_log > access_log.new && cat access_log.new > access_log

Vous pouvez ensuite utiliser un générateur de rapport de statistiques (webalizer, awstats, ...) pour connaître la fréquentation de votre serveur par d'autres personnes que vous. Je sais que la plupart de ces logiciels proposent de faire ce traitement eux mêmes mais autant leur faciliter la tâche, non ?

Cas pratique 3

J'ai écrit ce cas pratique dans le cadre de la liste d'entraide du site lea-linux.org.
Le problème consiste à écrire un script awk qui puisse extraire les liens d'un fichier html.
Voici le script réponse urlextract.awk (simplifié un peu):

#!/bin/awk -f
/href=\"?/ , /[ \">]/ {
        nbUrl = split ($0, ligne, "href=\"?")
        for ( i=2 ; i<=nbUrl ; ++i ) {
                split (ligne[i],url,"[ \">]");
                print url[1]
        }
}

Ca fait plaisir de revoir du awk, hein ? Alors, place aux explications...

Grâce à la construction /motif1/, /motif2/, on précise que l'on recherche tout ce qui se trouve entre motif1 et motif2.

Le motif1 (href=\"?) signifie "la chaîne de caractères href= suivie ou non par un guillemet". En effet, ? signifie "0 ou 1 occurence de ce qui précède", ici le guillemet. Notez que le guillemet fait partie des caractères devant être protégés par le symbole d'échappement puisqu'il sert habituellement à marquer les chaînes de caractères.

Le motif2 ([ \">]) signifie "un des caractères '>', '"' ou ' '" qui marquent bien la fin d'une url dans un fichier html. Vous en déduisez que pour trouver un caractère d'un ensemble, on utilise des crochets à l'intérieur desquels on met les caractères pouvant "faire l'affaire".

On se retrouve alors dans les accolades à bosser sur des lignes contenant une chaîne de caractères commençant par motif1 et terminée par motif2.

Comme on est de nature méfiante, on suppose qu'un webmaster peut mettre plus d'une url sur une seule ligne. On refait alors la recherche du motif1 dans notre chaîne globale. On utilise pour celà la fonction split, qui sépare notre chaîne ($0) en morceaux selon le motif1 et place le tableau résultant dans la variable ligne. Une fois son travail accompli, split retourne la taille du tableau. Il est important de noter que le motif servant à faire les coupures dans la chaîne est supprimé. Il n'y a donc plus dans les cases du tableau de "href=", avec ou sans guillemet.

Le début de ce tableau contient ce qui se trouvait avant la première occurence du motif1. On commence donc le traitement à la case 2 du tableau et on continue tant qu'il y a des urls (en awk la première case du tableau porte le numéro 1).
On utilise donc une boucle for mais de awk cette fois, ne confondez pas avec celle en bash vue plus haut.
La construction en est des plus classiques
for (initialisation ; condition de fin ; opération d'itération)
On initialise une variable i à 2. Elle nous servira d'indice pour le tableau.
On spécifie que l'on souhaite que la boucle se termine lorsque i vaudra nbUrl.
A chaque fin de boucle, on augmente la valeur de i de 1. On utilise pour cela la syntaxe d'incrémentation ++i, équivalente à i=i+1.

Dans cette boucle, on travaille sur des urls suivies par du texte qui nous importe peu. On réutilise donc split pour ne garder que ce qui se trouve avant le caractère mettant fin à l'url en lui fournissant motif2 comme séparateur. Le tableau résultant, url contient alors l'url en première case, que l'on affiche donc.

Et hop !

Pour tester ce script, saisissez le, n'oubliez pas de faire un chmod a+x urlextract.awk pour le rendre exécutable. Modifiez si besoin est le chemin vers awk sur votre système en première ligne du script (Tapez which awk pour le connaître). Vous pouvez alors le tester sur des pages web de votre choix, à titre d'exemple, prenons un fichier test.html :

<a href=url1></a><a href="url2"></a><a href=url3 class="blue"></a>
<a href="url4"></a><a href=url5></a><a href=url6 class="blue"></a>
<a href=url7 class="blue"></a><a style="text-decoration: none" href="url8"></a><a href=url9></a>

On essaie :

xavier@servor:~/devel_awk > ./urlextract.awk test.html
url1
url2
url3
url4
url5
url6
url7
url8
url9

En savoir plus

man find
man sed
man less
man grep
man bash

a+

Xavier Garreau - <xavier@xgarreau.org>
http://www.xgarreau.org/

Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.fr/

Membre fondateur du ROCHELUG
http://www.rochelug.org/

a+

Auteur : Xavier GARREAU
Modifié le 10.09.2004