Introduction à l'écriture de scripts shell — Partie 3

Gazette Linux n°113 — Avril 2005

Ben Okopnik

Article paru dans le n°113 de la Gazette Linux d'avril 2005.

Traduction française par Joëlle Cornavin .

Relecture de la traduction française par Damien Bosq .

Article publié sous Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.


Table des matières
1. cat
2. tr
3. head/tail
4. cut/paste
5. grep
6. Conclusion
7. Références

Le mois dernier, nous avons étudié les boucles et l'exécution conditionnelle. Cette fois-ci, nous aborderons quelques-uns des outils « externes » plus simples (c'est-à-dire les utilitaires GNU) d'usage courant dans les scripts shell. Rappelez-vous que les scripts shell se composent 1) de commandes et de structures de shell internes, 2) d'outils externes, constitués des utilitaires standard, 3) de programmes installés. Les premières y seront toujours, tant que vous exécutez le script avec le même shell (le shebang y veille habituellement), les deuxièmes y seront habituellement aussi (mais faites attention à la syntaxe non portable entre différentes versions, par exemple, le commutateur -A de cat, les divers niveaux d'analyse (parsing) d'expressions régulières dans les différentes versions de grep, etc.) et la dernière est essentiellement arbitraire puisque vous ne savez pas ce qu'une autre personne exécutant votre script a installé (ou n'a pas installé) sur sa machine. Si vous avez l'intention de distribuer votre script, il se peut que vous deviez écrire du code pour tester la présence de tout programme externe que vous employez et que vous ayez à publier des avertissements s'ils sont absents.

Les outils dont vous disposez en tant que créateur de scripts sont triés par « hiérarchie de puissance ». Il est important de vous rappeler de ce principe si vous vous estimez continuellement frustré par les limitations d'un outil spécifique, il risque de ne pas avoir assez de « jus » pour faire le travail. À l'inverse, il n'y a aucun intérêt à faire appel à un utilitaire extrêmement complet et lent pour effectuer une opération simple.

Il y a quelques années, alors que j'écrivais un script qui traitait des fichiers de base de données Clipper, je me suis trouvé au pied du mur par les limitations de tableau dans bash ; au bout d'une journée et demi à m'en sortir, j'ai décidé de tout réécrire dans awk.

J'en ai eu pour 15 minutes au total.

Ne vous entêtez pas au sujet du changement des outils quand l'original s'avère sous-exploité.


1. cat

Aussi étrange que cela puisse paraître, cat — que vous avez probablement employé en d'innombrables occasions — peut faire un certain nombre de choses utiles, au-delà de la concaténation simple et de l'affichage à l'écran. À titre d'exemple, cat -v file.txt affichera le contenu de file.txt à l'écran — et vous montrera également tous les caractères non texte qui pourraient normalement être invisibles (ceci exclut les caractères des fichiers texte (textfile) standard tels que end-of-line et tab), dans la notation ^ (pour les caractères Ctrl-) et M- (pour les caractères Alt-). Ce comportement peut être très utile avec un document censé être un fichier texte, mais de nombreux utilitaires continuent à ne pas les traiter et renvoient des erreurs comme « Ceci est un fichier binaire ! ». Cette capacité peut également s'avérer pratique lorsqu'on convertit des fichiers d'un type à l'autre (reportez-vous à la section sur tr). Si vous décidez d'afficher tous les caractères du fichier, le commutateur -A remplira sa mission — les signes $ montreront les fins de lignes et le ^I montrera les tabulations. Notez que -A est juste un raccourci.

-n est une autre option utile, qui numérote toutes les lignes (vous pouvez utiliser -b pour numéroter uniquement les lignes non vide) d'un fichier — très utile quand vous voulez créer un « sélecteur de ligne », c'est-à-dire chaque fois que vous voulez avoir une « poignée » pour une ligne donnée, que vous passeriez alors à un autre utilitaire, par exemple sed (qui fonctionne bien avec les numéros de ligne).

cat peut aussi servir pour les here-docs, c'est-à-dire pour générer une sortie multiligne au format texte. La syntaxe est peu étrange mais ne pose pas de difficulité. Voici deux fragments de script montrant les différences entre l'utilisation de echo :


...
echo "'guess' - a shell script that reads your mind"
echo "and runs the program you're thinking about."
echo
echo "Syntax:"
echo
echo "guess [-fnrs]"
echo
echo "-f Force mode: if no mental activity is detected,"
echo "   take a Scientific Wild-Ass Guess (SWAG) and execute."
echo "-n Read your neighbor's mind; commonly used to retrieve"
echo "   the URLs of really good porno sites."
echo "-r Reboot brain via TCP (Telepathic Control Protocol) - for
echo "   those times when you're drawing a complete blank."
echo "-s Read the supervisor's mind; implies the '-f' option."
echo
exit
...

et celle d'un here-doc :


...
cat <<!
'guess' - a shell script that reads your mind
and runs the program you're thinking about.

Syntax:

guess [-fnrs]

-f Force mode: if no mental activity is detected,
   take a Scientific Wild-Ass Guess (SWAG) and execute.
-n Read your neighbor's mind; commonly used to retrieve
   the URLs of really good porno sites.
-r Reboot brain via TCP (Telepathic Control Protocol) - for
   those times when you're drawing a complete blank.
-s Read the supervisor's mind; implies the '-f' option.

!
exit
...

Tout ce qui se trouve entre les deux points d'exclamation sera affiché sur la sortie standard stdout (l'écran) tel que formaté. Notez que le terminateur (! dans ce cas) est arbitraire — vous pourriez employer EOF, ^+-+^ ou This_is_the_end_my_friend — mais ! est traditionnel. Les seules contraintes sur ce qui précède sont qu'il doit y avoir une espace entre le terminateur et le symbole de redirection placé après (sinon le redirecteur pourrait être considéré comme faisant partie du terminateur !) et le terminateur de fermeture doit être seul sur une ligne, sans le caractère d'espacement de fin. Ceci permet au terminateur d'être utilisé comme une partie du texte sans fermer le here-doc.

L'utilisation du même mécanisme avec redirection vous donne un mini-éditeur :


ben@Fenrir:~$ cat <<! > file.txt
> Everything entered here
>       will be written to file.txt
>               exactly as entered.
!
ben@Fenrir:~$ cat file.txt
Everything entered here
        will be written to file.txt
                exactly as entered.

J'ai tendance à imaginer cat comme un « processeur initial » pour du texte, qui sera ensuite traité avec d'autres outils. Il n'est pas sans importance — dans certains cas, il est pratiquement irremplaçable. En fait, votre cat peut faire des choses qui ne sont pas seulement amusantes mais utiles.


2. tr

En matière de traitement « caractère par caractère », cet utilitaire, malgré ses bizarreries à certains égards (par exemple, les caractères spécifiés par valeur ASCII doivent être saisis en octal), est l'un des plus utiles de notre boîte à outils. Voici un script qui l'utilise et remplace ces utilitaires de conversion « texte DOS vers Unix » :


#!/bin/bash
[ -z "$1" ] << { 
        echo "d2u - converts DOS text to Unix."
        echo "Syntax: d2u <file>"
        exit
}

cat "$1"|tr -d '\015'

Prenons le temps de quelques explications sur les constructs if de l'article du mois dernier. Qu'est-il arrivé à cette instruction que vous pensiez nécessaire au début du script ? Et quid de ce '<< ?

Croyez-le ou non, ils sont toujours tous là — au moins le mécanisme qui fait que « les choses se produisent correctement ». Maintenant, toutefois, au lieu d'employer la structure de l'instruction et de faire tenir nos commandes dans les emplacements réservés de la syntaxe, nous employons la valeur de renvoi des commandes et faisons effectuer le travail à la logique. Jetons un coup d'½il à ce concept très important.

Chaque fois que vous utilisez une commande, elle renvoie un code à la sortie — en général 0 en cas de succès et 1 en cas d'échec (les exceptions sont des choses comme la fonction length, qui renvoie une valeur). Certains programmes renvoient divers nombres pour des types donnés de sorties, ce qui explique pourquoi vous serez normalement amené à tester « zéro » par rapport à « différent de zéro », plutôt que tester 1 en particulier. Vous pouvez impléménter le même mécanisme dans vos scripts (c'est une bonne politique de codage) : si votre script génère une variété de messages sur différentes conditions de sortie, utilisez exit n comme dernière instruction, où n est le code à renvoyer (l'instruction exit complète renvoie la valeur de l'opération qui la précède immédiatement). Ces codes, d'ailleurs, sont invisibles — ce sont des « drapeaux » internes ; il n'y a rien d'affiché sur l'écran, donc ce n'est pas la peine de regarder. Si vous voulez voir ce qu'était le code de sortie de la dernière commande, essayez un echo $? — il stocke la valeur numérique du dernier drapeau de sortie.

Pour les tester, bash fournit un mécanisme simple, les mots réservés « << » (ET logique) et « || » (OU logique). Dans le script ci-dessus, l'instruction indique essentiellement « si $1 a une longueur de zéro, alors les instructions suivantes (echo... echo... exit) devraient être exécutées ». Si vous n'êtes pas à l'aise avec la logique binaire, celle-ci peut être source de confusion, voici un récapitulatif qui suffira pour nos objectifs : revenons à la préhistoire et, lorsque nous apprenions que les ordinateurs signifiaient compréhension de la conception du matériel, nous avions des gadgets appelés « portes ET » et « portes OU » — des circuits logiques — qui fonctionnaient comme suit :


      ET (<<)                           OU (||)
  table de vérité                   table de vérité

     A   B  sort                       A   B  sort
    -----------                       -----------
   | 0 | 0 | 0 |                     | 0 | 0 | 0 |
   | 0 | 1 | 0 |                     | 0 | 1 | 1 |
   | 1 | 0 | 0 |                     | 1 | 0 | 1 |
   | 1 | 1 | 1 |                     | 1 | 1 | 1 |
    -----------                       -----------

Si une entrée quelconque est 0,   Si une entrée quelconque est 1,    
la sortie sera 0.                 la sortie sera 1.

En d'autres termes, si nous connaissions la valeur d'une des entrées, nous pourrions décider si nous devions évaluer l'autre entrée ou non (par exemple avec une porte ET, si l'entrée connue est un 0, nous n'avons pas besoin d'évaluer l'autre car nous savons ce que sera la sortie !) C'est la logique que nous employons lorsque nous avons affaire aux opérateurs logiques dans le shell également : si nous avons quelque chose qui est vraie en face d'un opérateur ET, nous devons évidemment évaluer (c'est-à-dire exécuter) la partie arrière, et la même chose pour une entrée fausse pour un opérateur OU.

À titre de comparaison, voici deux fragments de scripts qui font en gros la même chose.

Tout d'abord :


if [ -z $1 ]
then
        echo "Enter a parameter."
else
        echo "Parameter entered."
fi

Puis :


[ -z $1 ] && echo "Enter a parameter." || echo "Parameter entered."

Soyez prudent quant à l'emploi de la seconde version pour tout ce qui est plus complexe que les instructions echo : si vous utilisez une commande dans la partie située après le « << » qui renvoie un code d'échec, celui-ci ainsi que les instructions placées après « || » seront exécutés, à moins que vous ne forciez une sortie réussie explicite ! Ce comportement en soi peut être utile, si c'est ce dont vous avez besoin, mais vous devez vous rendre compte de la façon dont fonctionne le mécanisme.

Revenons au script d2u originel, la partie « active » du script :



cat "$1"|tr -d '\015'

transfère le texte original text dans tr, ce qui supprime le caractère « CR/retour chariot » (0x0D) de DOS, représenté ici en octal (\015), ce qui fait la différence entre le texte DOS et le texte Unix. Nous utilisons juste le caractère « LF/Saut de ligne » (0x0A), alors que DOS emploie à la fois (CR/LF). C'est pourquoi le texte Unix en DOS ressemble à :

Ceci est la ligne un*Ceci est la ligne deux*Ceci est la ligne trois*

Et le texte DOS en Unix ressemble à :


Ceci est la ligne un^M
Ceci est la ligne deux^M
Ceci est la ligne trois^M

Un conseil est applicable à n'importe quel créateur de script shell : une étude approfondie de la page de man de tr sera très profitable. C'est un outil que vous serez amené à utiliser maintes fois.


3. head/tail

Une paire d'outils très utiles, avec une syntaxe presque identique. Par défaut, ils affichent, respectivement, les dix premières/dernières lignes d'un fichier donné ; le nombre et les unités sont aisément changés via la syntaxe. Voici un fragment qui montre comment lire une ligne donnée dans un fichier, en utilisant son numéro de ligne comme une « poignée » (reportez-vous à la section tr) :



handle=5
line="$(head -$handle $1|tail -1)"

Après avoir fixé la valeur de « $handle » à « 5 », nous utilisons « head -$handle » pour lire un fichier spécifié sur la ligne de commande et afficher toutes les lignes de 1 à 5 ; nous employons ensuite « tail -1 » pour n'en lire que la dernière ligne. Ceci peut, bien sûr, être effectué avec des outils plus puissants comme sed.

Ces programmes peuvent aussi servir à « identifier » des fichiers très volumineux sans la nécessité d'en lire la totalité ; si vous savez qu'une, parmi plusieurs très grosses base de données, contient un nom de champ unique qui l'identifie comme celui que vous cherchez, vous pouvez faire ceci :


for fname in *dbf
do
        head -c10k "$fname"|grep -is "cost_in_sheckels_per_cubit"
        echo $fname
done

Le cas ci-dessus est assez simple ; nous prenons les 10 premiers ko (vous devrez ajuster cette valeur à un bloc quelconque de la taille nécessaire pour capturer tous les noms de champ) du début de chaque base de données à l'aide de head, puis utilisez grep pour chercher la chaîne. Si vous la trouvez, nous affichons le nom du fichier. Ceux d'entre vous qui doivent traiter un grand nombre de bases de données de plusieurs mégaoctets peuvent vraiment apprécier cette fonctionnalité.

tail est intéressant en soi ; une des différences de syntaxe entre cette commande et head est le commutateur +, qui répond à la question « comment lire tout ce qui se trouve après les x premiers caractères/lignes ? » Croyez-le ou non, cela peut être une question très importante et il peut être très difficile d'y répondre de n'importe quelle autre manière... À titre d'exemple, pour obtenir la sortie d'une commande comme ls -l sans le total, essayez ls -l|tail +2.


4. cut/paste

D'après mon expérience, cut joue un rôle beaucoup important que paste : cette commande est très efficace pour traiter des champs dans des données formatées, ce qui vous permet de séparer les infos dont vous avez besoin. Par exemple, supposons que vous ayez un répertoire où vous devez obtenir une liste de tous les fichiers d'une taille de 100 k ou plus, une fois par semaine (les fichiers journaux au-dessus d'une limite de taille, peut-être). Vous pouvez configurer une tâche cron qui vous sera envoyée par courrier électronique :


...
ls -lr --sort=size $dir|tr -s ' '|cut -d ' ' -f 5,8|grep \
-E ^'[1-9]{6,} '|mail joe@thefarm.com -s "Logfile info"
...

ls -lr --sort=size $dir nous donne un listing de $dir trié par taille en ordre inverse (du plus petit au plus grand). Nous transférons cela via tr -s ' ' pour réduire tous les espaces répétés en un seul, puis nous faisons appel à cut avec un espace comme délimiteur (maintenant que les espaces sont uniques, nous pouvons véritablement les employer pour séparer les champs) pour renvoyer les champs 5 et 8 (taille et nom de fichier). Nous utilisons ensuite grep pour examiner le tout début de la ligne (où la taille est répertoriée) et afficher chaque ligne qui commence par un chiffre, répète cette correspondance 5 fois et est suivi d'un espace. Les lignes qui correspondent sont transmises dans mail et envoyées au destinataire.

paste peut être utile parfois. La manière la plus simple de décrire cette commande est de l'imaginer comme un « cat vertical » : elle fusionne les fichiers ligne par ligne, au lieu du début à la fin. Si vous avez par exemple deux fichiers contenant respectivement les noms des personnes de votre équipe de sport préférée et que leurs résultats sont consignés pour chacun dans l'ordre correct, il suffit d'en accoler deux avec paste. Si vous spéicifez les noms des fichiers d'abord et les résultats ensuite, chaque obtenur contiendra le nom suivi du résultat, séparé par une tabulation ou un délimiteur quelconque que vous avez indiqué avec l'option -d


5. grep

Le couteau suisse d'Unix. Cet utilitaire, ainsi que ses variantes plus spécialisées fgrep et egrep, sert principalement à chercher dans des fichiers des chaînes de texte correspondantes, en utilisant le mécanisme d'expressions régulières (regexp pour indiquer le texte devant correspondre.

fgrep peut servir pour rechercher une citation dans un grand nombre de fichiers d'un répertoire.


Odin:~$ grep -iA 12 "who hath desired the sea" *
Poems.txt-Who hath desired the Sea? - the sight of salt water unbounded -
Poems.txt-The heave and the halt and the hurl and the crash of the comber
Poems.txt- wind-hounded?
Poems.txt-The sleek-barrelled swell before storm, grey, foamless, enormous,
Poems.txt- and growing -
Poems.txt-Stark calm on the lap of the Line or the crazy-eyed hurricane
Poems.txt- blowing -
Poems.txt-His Sea in no showing the same - his Sea and the same 'neath each
Poems.txt- showing:
Poems.txt- His Sea as she slackens or thrills?
Poems.txt-So and no otherwise - so and no otherwise - hillmen desire their
Poems.txt- Hills!
Odin:~$

grep offre une grande variété d'options (le commutateur -A <n> que j'ai utilisé précédemment détermine le nombre de lignes de contexte après la ligne mise en correspondance qui seront affichées. Le commutateur -i signifie « ignorer la casse ») qui permet de faire des recherches précises dans un seul fichier ou un groupe de fichiers ainsi que de spécifier le type de sortie quand une correspondance est trouvée (ou inversement, quand il n'y a pas de correspondance). J'ai utilisé grep dans plusieurs des « exemples » de scripts jusqu'ici, et je m'en sers en moyenne une douzaine de fois par jour, ligne de commande et scripts confondus.

Vous pouvez également l'employer pour faire des recherches dans des fichiers binaires, à l'aide de l'option -a ; une procédure de dernier recours utile occasionnellement pour ces programmes où l'auteur a caché les infos de syntaxe/d'aide derrière quelque obscur commutateur, quand man, info et le répertoire /usr/doc/ s'avèrent vides.

Souvent, il y a une condition requise pour exécuter une tâche donnée le même nombre de fois qu'il y a de lignes « utiles » dans un fichier donné, par exemple lire chaque ligne d'une fichier de configuration et analyser sa syntaxe. grep nous aide ici, également :


...
for n in $(egrep -v '^[            ]*(#|$)' ~/.scheduler)
do
        ...
        ...
done

C'est un fragment d'un programme d'ordonnancement que j'ai écrit il y a quelque temps ; chaque fois que j'ouvre une session, il me rappelle mes rendez-vous, etc. pour ce jour. egrep, dans cet exemple, trouve toutes les lignes qui ne sont pas des commentaires ou des blancs, en ignorant (via l'option -v) toutes les lignes qui soit commencent par un caractère # soit par un nombre quelconque d'espaces ou de tabulations précédant un # ou une fin de ligne (représentée par le métacaractère $). Notez que les crochets ci-dessus, qui définissent une classe de caractères ou une plage de caractères à mettre en correspondance, contiennent réellement un espace et une tabulation — les deux étant malheureusement invisibles. Par ailleurs, la raison pour laquelle j'ai employé e(xtended) grep ici est que la plupart des versions du grep simple ne savent pas comment analyser la syntaxe du construct d'alternance (a|b) — et une classe de caractère ne fonctionnera pas pour cela, du fait que les métacaractères perdent leur signification spéciale dans des classes de caractères et sont simplement considérés comme des caractères.

Le résultat de ce qui précède est que nous n'itérons que sur le beef dans le fichier de configuration, en ignorant toute entrée non programmatique ; les lignes « fonctionnelles » sont analysées, dans le corps de la boucle for (les détails ne sont pas montrés dans ce fragment) dans les variables de date et de textes, et le script exécute une routine d'« alarme et d'affichage » si la date du rendez-vous correspond à la date d'aujourd'hui.


6. Conclusion

Pour pouvoir produire de bons scripts shell, vous devez bien connaître le fonctionnement de tous ces outils ou, tout au moins, avoir une bonne idée de ce qu'un outil donné peut faire et ne pas faire (vous pouvez toujours chercher la syntaxe exacte via man). Il y a de nombreux autres outils plus complexes et plus puissants à notre disposition — mais ces six programmes vous mettront le pied à l'étrier et ce pour un certain temps, tout en vous donnant un vaste champ de possibliités pour l'expérimentation des scripts de votre cru.


7. Références

Ben est le rédacteur en chef de la Linux Gazette© et il est membre de l'« Answer Gang© ».

Né à Moscou (Russie) en 1962, Ben a commencé à s'intéresser à l'électricité à l'âge de six ans. Il l'a démontré rapidement en plantant une fourchette dans une prise électrique, déclenchant ainsi un début d'incendie et depuis lors, il est plongé dans le dédale de la technologie. Il travaille sur les ordinateurs depuis le tout début, lorsqu'il fallait les construire en soudant des composants sur des circuits imprimés et que les programmes devaient tenir dans 4 ko de mémoire. Il paierait chèrement tout psychologue susceptible de le guérir des affreux cauchemars qu'il en a gardés.

Ses expériences ultérieures comprennent la création de logiciels dans près de douze langages, la maintenance de réseaux et de bases de données à l'approche d'un ouragan, la rédaction d'articles pour des publications allant des magazines consacrés à la voile aux revues sur la technologie. Après une croisière en voilier de sept ans sur l'Atlantique et la mer des Caraïbes ponctuée de quelques escales sur la côte est des États-Unis, il a pour l'instant jeté l'ancre à St Augustine (Floride). Il est instructeur technique chez Sun Microsystems© et travaille également à titre privé comme consultant open source/développeur web. Ses passe-temps actuels sont notamment l'aviation, le yoga, les arts martiaux, la moto, l'écriture et l'histoire romaine. Son Palm Pilot© est truffé d'alarmes dont la plupart contiennent des points d'exclamation.

Ben travaille avec Linux depuis 1997 et le crédite de sa perte complète d'intérêt pour les campagnes de guerre nucléaire sur les territoires nord-ouest du Pacifique.