Par Ben Okopnik
ben-fuzzybear@yahoo.com
Le mois dernier, nous nous sommes intéressés aux bases de l'écriture de scripts shell ainsi qu'à quelques mécanismes sous-jacents qui font que tout ça fonctionne. Cette fois ci, nous allons voir comment les boucles et d'autres structures de contrôle nous permettent de gérer l'exécution de programmes dans des scripts, mais aussi d'adopter quelques bonnes habitudes lors de l'écriture de scripts shell.
La seule chose à noter dans cet article est l'utilisation des points de
suspension (...)
. Je les utilise pour indiquer que le code
présenté n'est qu'un fragment, et non un script entier. Si ça peut
vous aider, considérez les comme des lignes de code qui n'ont pas été
écrites.
FOR;DO;DONE
Souvent, les scripts sont écrits pour automatiser des tâches répétitives. Un exemple pris au hasard : si vous deviez éditer, à la suite, une série de fichiers dans un répertoire particulier, vous utiliseriez un script qui ressemble à ça :
#!/bin/bash for n in ~/hebdomadaire/*.txt do ae $n done echo "Terminé."
ou comme ça :
#!/bin/bash for n in ~/hebdomadaire/*.txt; do ae $n;done; echo "Terminé."
Dans les deux cas, le code fait exactement la même chose. Mais la première version est beaucoup plus lisible, surtout si vous écrivez de gros scripts avec plusieurs niveaux. Un conseil pour l'écriture des scripts, indentez chaque niveau (les commandes à l'intérieur des boucles). Il sera plus facile de corriger les erreurs et de suivre votre code.
La structure de contrôle ci-dessus est appelée une boucle for
(boucle Pour tout
). Elle évalue le test pour tous les
éléments restant dans une liste, c'est-à-dire « Est-ce qu'il reste
encore des fichiers, après ceux que nous avons déjà lus, et qui
instancient l'expression
~/hebdomadaire/*.txt
? ». Si le résultat
du test est vrai, le nom de l'élément courant dans la liste est
affecté à la variable de la boucle (n
dans notre exemple), le
corps de la boucle est exécuté (la partie entre do
et
done
) et le test est à nouveau évalué. Si le parcours de la
liste est terminé, for
s'arrête de boucler et passe le
contrôle à la ligne qui suit le mot clé done
. Dans notre
exemple, la commande echo
.
Il y a un petit truc que j'aimerais mentionner ici. Si vous voulez que votre
boucle for
tourne un certain nombre de fois, la
syntaxe shell doit ressembler à ça :
#!/bin/bash for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 do echo $i done
Quelle horreur ! Si vous voulez faire, disons, 250 itérations,
vous devriez toutes les écrire ! Heureusement, il y a un
raccourci, la commande seq
, qui affiche une liste de
nombre de 1 au nombre maximum donné en argument. Par exemple :
#!/bin/bash for i in $(seq 15) do echo $i done
Sémantiquement, ce script est identique au précédent. seq
fait
partie du paquetage GNU shellutils
, qui est probablement déjà
installé sur votre système. Il y a aussi la possibilité de faire la même chose
avec une boucle while
, mais c'est un peu plus difficile.
WHILE;DO;DONE
Souvent, vous avez besoin d'un mécanisme de contrôle qui agit en
fonction d'une condition spécifiée, plutôt que suivant les éléments
d'une liste. La boucle while
remplit ce rôle :
#!/bin/bash pppd call provider & while [ -n "$(ping -c 1 192.168.0.1|grep 100%)" ] do echo "En cours de connexion" done echo "Connexion établie."
Le déroulement de ce script est le suivant : on invoque
pppd
le pingouin PPP ... je veux dire le démon PPP :)
, puis
on boucle jusqu'à ce qu'une connexion soit établie (si vous voulez
utiliser ce script, remplacer 192.168.0.1 par l'adresse IP de votre
FAI). Voici maintenant les détails :
ping -c 1 xxx.xxx.xxx.xxx
envoie un seul
ping à l'adresse donnée en argument. Notez qu'il faut que ce soit une
adresse IP et non une URL (le ping
échouera immédiatement à
cause de l'absence de DNS). S'il n'y a pas de réponse dans les 10
secondes qui suivent, quelque chose comme ça s'affichera :
PING xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx): 56 data bytes ping: sendto: Network is unreachable ping: wrote xxx.xxx.xxx.xxx 64 chars, ret=-1 --- xxx.xxx.xxx.xxx ping statistics --- 1 packets transmitted, 0 packets received, 100% packet loss
ping
a été exécutée avec succès) ou
100%. En redirigeant la sortie de la commande ping
avec un
tube vers l'entrée de la commande grep 100%
, nous mettons en
évidence cette ligne, dans le cas où la perte est en fait de 100%. Si
aucun paquet n'est perdu (0%), la sortie reste vide. Notez que la
chaîne 100% n'est en rien spéciale : nous aurions pu utiliser
ret=-1
, unreacheable
ou tout autre chose qui n'est
affichée qu'en cas d'absence de réponse de l'adresse.
test
. Celle-ci retourne 0 ou 1 (vrai
ou faux) suivant l'évaluation de l'intérieur des crochets. L'opérateur
-n
retourne vrai si la longueur de la chaîne est plus
grande que 0. Puisque nous supposons que la chaîne est contiguë (sans
espace), et que la ligne que nous vérifions ne l'est pas, nous devons
placer des doubles cotes (") autour du résultat de la commande (c'est
une technique que vous utiliserez très souvent lorsque vous écrirez
des scripts). Notez que les crochets doivent être entourés d'espaces,
c'est-à-dire que [-n $STRING]
ne marchera pas, [ -n
$STRING ]
est correct. Pour plus d'information sur les
utilisations de test
, tapez la commande help
test
. Vous verrez qu'un grand nombre d'opérateurs très utiles
sont disponibles.
ping
échouera,
la boucle while
continuera de s'exécuter, en affichant toute
les dix secondes , le message "Connecting...
. Aussitôt qu'une
commande ping
s'exécute avec succès, c'est-à-dire que le test
retourne la valeur faux, on sortira de la boucle while
et
l'instruction qui suit done
prendra le contrôle.
UNTIL;DO;DONE
La boucle until
est l'inverse du while
. Elle
continue de boucler tant que le test est faux et s'arrête lorsqu'il
devient vrai. Je n'ai jamais eu l'occasion de l'utiliser. La boucle
while
et les possibilités des tests disponibles étaient
suffisants pour ce que j'ai fait jusqu'à présent.
IF;THEN;[ELSE];FI
À plusieurs reprises, nous aurons besoin de vérifier la valeur d'une
condition et de contrôler l'exécution du script en fonction du résultat.
Pour cela, nous avons la structure if
:
... if [ $PATRON="pauvre type" ] then echo 'Pend le boulot et bouscule le !' else echo 'Reste dans les parages ; ça peut valoir le coup.' fi ...
< He Hé ! > Je vous accorde que ce n'est pas si facile... mais le sens logique est
là. Quoi qu'il en soit, si une variable appelée PATRON
a la
valeur pauvre type (remarque pour les programmeurs C :
=
et ==
sont équivalents dans les tests, aucune
affectation n'est réalisée), alors la première instruction
echo
sera exécutée. Dans tous les autres cas, ce sera la
seconde instruction echo
(si la variable PATRON
a la
valeur idiot, l'exécution se déroulera aussi de cette manière. Désolé
;)
). Notez que la partie else
est optionnelle, comme dans ce
fragment de script :
... if [ -n $ERROR ] then echo 'Detection d'une erreur. Sortie.' exit fi ...
Cette procédure sort bien entendu si la variable ERROR
n'est
pas vide, sinon l'exécution du programme ne sera pas affectée.
CASE;IN;;ESAC
La dernière instruction que vous pouvez utiliser pour des redirections
conditionnelles est celle correspondant à un if
multiple,
basée sur l'évaluation d'un test. Si, par exemple, nous savons que les
seules valeurs possibles renvoyées par un programme imaginaire appelé
intel_cpu_test sont 4, 8, 16, 32 ou 64, alors nous pouvons
écrire le script suivant :
#!/bin/bash case $(intel_cpu_test) in 4) echo "Vous faites tourner Linux sur une calculatrice ?";; 8) echo "Ce 8088 a passé l'âge de la retraite ...";; 16) echo "Vous êtes un adepte du 286, vous ?";; 32) echo "Un de ceux qui ont les derniers gadgets à la mode !";; 64) echo "Oooh... vraiment jaloux de ce CPU !";; *) echo "Mais quelle bête avez-vous ?";; esac
(Avant de m'inonder de mail au sujet de l'utilisation de Linux sur un
286 ou 8088... Vous ne pouvez pas non plus le faire tourner sur une
calculatrice. :)
)
Évidemment, le caractère "*" à la fin permet de prendre en compte
toutes les autres valeurs : Si quelqu'un du Laboratoire Secret de
Intel lance le script sur leur nouveau CPU (nom de code
Ultra-Maga-Super-du-Tonnerre), nous voulons que le script soit capable de
renvoyer une réponse plutôt que de planter. Notez les doubles
point-virgules, ils terminent chaque couple valeur/instruction et
sont (pour certaines raisons) une source d'erreur courante lors de
l'utilisation de l'instruction case/esac
. Faites très
attention lorsque vous écrivez vos scripts.
Ces instructions interrompent l'exécution d'un programme dans des cas
spécifiques. Le break
, une fois exécuté, interrompt
immédiatement l'exécution de la boucle dans laquelle il se
trouve. L'instruction continue
ignore l'itération courante de
la boucle. Ces instructions peuvent être utiles dans de nombreuses
situations, en particulier dans les boucles importantes où l'existence
d'une condition donné rend les tests suivants inutiles. Voici un exemple
assez long (mais, je l'espère, compréhensible) :
... while [ faire_la_fete ] do case $ETAT_NOURRITURE in plus_de_chips) reapprovisionnement_en_chips;; cacahuetes_terminees) remplir_le_bol_de_cacahuetes;; plus_de_bretzels) ouvrir_un_nouveau_paquet_de_bretzels;; ... ... esac if [ police_sur_place ] then parler_aux_gentils_policiers continue fi case $ETAT_ALCOOL in plus_de_vodka) ouvrir_une_nouvelle_bouteille_de_vodka;; plus_de_rhum) ouvrir_une_nouvelle_bouteille_de_rhum;; ... ... esac case $ANALYZE_DU_COMPORTEMENT_DES_INVITES in abat_jour_sur_la_tete) echo "Il a bu";; parle_aux_plantes) echo "Elle a fumé";; parlent_aux_martiens) echo "Ils prennent du LSD";; objets_en_levitation) echo "Qui a pris mon jus de fruit ??";; ... ... ... esac done echo "Hé mec ? ... quel jour sommes nous ?"
Deux points importants : notez que, dans la vérification du
statut des différents amuse-gueules de la soirée, il serait peut-être
mieux d'écrire ça avec plusieurs instructions if
;
les chips et les bretzels peuvent venir à manquer en
même temps, c'est-à-dire qu'ils ne sont pas mutuellement exclusifs. Tel
que c'est écrit, les chips sont les plus prioritaires. Si deux types
des gâteaux-apéritifs viennent à manquer simultanément, cela prendra
deux itérations de la boucle pour tout remplacer.
Pour pouvez continuer à vérifier que le statut de la nourriture pendant que vous essayez de convaincre la police que vous êtes, contre toute évidence, en train de tenir une réunion de collectionneurs de timbres (en fait, maintenir l'approvisionnement en beignets est, à ce moment précis, un facteur crucial), mais nous devons ignorer le statut des alcools -- au fait, nous avons descendu Joe du lustre juste à temps...
L'instruction continue
ignore la dernière partie de la boucle
while
aussi longtemps que la fonction
police_sur_place retourne la valeur vraie. La boucle est
tronquée à cet endroit. Notez que même si l'instruction est à
l'intérieur de la structure if
, elle affecte la boucle qui
l'entoure. continue
et break
s'appliquent
uniquement aux boucles, c'est-à-dire aux structures de contrôle
for
, while
et until
.
Voici le script que nous avions écrit le mois dernier :
#!/bin/bash # "bkup" - copie les fichiers spécifiés dans le répertoire ~/Sauvegarde de # l'utilisateur après avoir vérifié qu'il n'y ait pas de conflit de nom. a=$(date +%T-%d_%m_%Y) cp -i $1 ~/Sauvegarde/$1.$a
C'est assez intéressant. Peu de temps après avoir terminé l'article
du mois dernier, j'ai écrit un bout de code en C sur une machine où
rcs
(Revision Control System -- le Système de contrôle des
versions GNU) n'était pas installé. Ce script me fut très utile comme
'micro-rcs'. Je l'ai utilisé pour conserver des versions du projet. C'est
simple et des scripts de ce type peuvent être utiles lorsqu'on s'y
attend le moins.
Le script ci-dessus est utilisable, pour vous comme pour n'importe qui capable de le lire et de le comprendre. Regardons le quand même : ce que nous attendons d'un programme ou d'un script, c'est qu'il s'exécute lorsqu'on tape son nom, n'est-ce pas ? Si ça ne se passe pas comme ça, on voudrait qu'il nous dise pourquoi il ne fonctionne pas. Et jusqu'à présent, si ce cas se présente, nous obtenons ce message pas clair du tout :
cp: missing destination file Try `cp --help' for more information.
(Fichier destination absent. Essayez `cp --help' pour plus d'information)
Que ce soit pour n'importe qui, ou pour nous même, lorsque nous
oublions exactement comment utiliser ce script extrêmement complexe
avec d'innombrable options :)
, nous avons besoin d'effectuer une
vérification des erreurs, et surtout, d'avoir des informations sur son
utilisation et sa syntaxe. Voyons comment nous pouvons appliquer ce
que nous venons d'apprendre :
#!/bin/bash if [ -z $1 ] then clear echo "'bkup' - copie les fichiers spécifiés dans le répertoire" echo "~/Sauvegarde de l'utilisateur après avoir vérifié les conflits" echo "de noms" echo echo "Usage: bkup nom_du_fichier" echo exit fi a=$(date +%T-%d_%m_%Y) cp -i $1 ~/Sauvegarde/$1.$a
L'opérateur -z
de test
retourne la valeur '0' (vrai)
lorsque la chaîne en argument est de longueur nulle. De cette manière,
nous vérifions si bkup
a été exécuté sans nom de fichier. A
mon avis, la meilleure place pour afficher l'aide et les informations
sur l'utilisation du script est au tout début. Si vous oubliez quelles
sont les options, vous avez juste à exécuter le script sans rien et
vous pourrez ainsi vous rafraîchir la mémoire. Du coup, vous n'avez même
plus besoin de mettre les commentaires du début. Notez que vous
avez les simplement inclus dans les informations concernant
l'utilisation du script. C'est quand même une bonne idée de mettre des
commentaires pour expliquer les points qui ne sont pas évidents.
Ce bout de script super optimisé que vous avez réussi à écrire vous
prendra la tête l'année prochaine, si vous ne le faites pas...
Avant de vous amuser avec ce script, voici quelques possibilités supplémentaires. Que faudrait-il faire si vous vouliez avoir la possibilité de copier différents types de fichiers dans différents répertoires ? Utilisons ce que nous avons appris :
#!/bin/bash if [ -z $1 ] then clear echo "'bkup' - copie les fichiers spécifiés dans l'arborescence des répertoires" echo "~/Sauvegarde de l'utilisateur après avoir vérifié les conflits" echo "de noms" echo echo "Usage: bkup nom_du_fichier [rep_sauvegarde]" echo echo "rep_sauvegarde Sous-répertoire optionel dans '~/Sauvegarde' ou le" echo "fichier sera sauvegardé." echo exit fi if [ -n $2 ] then if [ -d ~/Sauvegarde/$2 ] then subdir=$2/ else mkdir -p ~/Sauvegarde/$2 subdir=$2/ fi fi a=$(date +%T-%d_%m_%Y) cp -i $1 ~/Sauvegarde/$subdir$1.$a
Voici un résumé des modifications :
if
a été ajoutée afin de
vérifier si $2
(le deuxième argument de la commande
bkup
) existe. Si c'est le cas, on vérifie que le répertoire
dont le nom est donné en second argument existe dans l'arborescence de
~/Sauvegarde
, et on le crée s'il n'existe pas (l'opérateur
-d
permet de tester que le fichier existe et qu'il s'agit
d'un répertoire).
subdir
est maintenant insérée dans la
commande cp
, entre Sauvegarde/
et $1
.
Vous pouvez désormais taper ce genre de chose :
bkup my_new_program.c c bkup filter.awk awk bkup filter.awk filters bkup Letter_to_Mom.txt docset ainsi de suite. Vous pouvez ainsi tout ranger dans la catégorie que vous voulez. De plus, l'ancien comportement de
bkup
est
toujours disponible :
bkup file.xyzcopiera une sauvegarde de
file.xyz
dans le répertoire
~/Sauvegarde
. C'est utile pour les fichiers que vous ne
savez pas où ranger.
Au fait, pourquoi avons nous ajouté un /
à $2
dans
le test if
qui précède la ligne cp
? Hé bien,
si $2
n'existe pas, vous voulez que bkup
se comporte
comme avant, c'est-à-dire qu'il copie le fichier dans le répertoire
~/Sauvegarde
. Si vous écrivez quelque chose comme ça :
cp -i $1 ~/Sauvegarde/$subdir/$1.$a
(notez le /
supplémentaire entre $subdir
et
$1
), et que $2
n'est pas spécifié, alors
$subdir
est vide et la ligne ci-dessus devient :
cp -i $1 ~/Sauvegarde//$1.$a
Notez que ce qui n'est pas un résultat particulièrement souhaitable, étant donné que nous voulons rester, si possible, fidèles aux habitudes d'écriture en shell.
En fait, c'est vraiment une bonne idée de considérer toutes les possibilités, lorsque vous placez des variables dans une chaîne. Une erreur classique de ce type ressemble au script suivant :
NE PAS UTILISER CE SCRIPT ! #!/bin/bash # Écrit par Larry, Moe, and Shemp - the Deleshun PoWeR TeaM!!! # Vérifié par Curly : " Pourquoi, Bien sûr qu'il marche ! Yek-Yek-Yek ! " # Tout ce que vous avez à faire est d'entrer le nom de ce programme # suivi de ce que vous voulez effacer : répértoire, fichiers normaux, # plusieurs fichiers, tout est bon ! rm -rf $1* NE PAS UTILISER CE SCRIPT !
(Au moins, ils vous aurons prévenus :)
)
Que se passe-t-il si quelqu'un lance le programme ci-dessus sans argument ? La commande devient alors :
rm -rf *
En supposant que vous êtes l'utilisateur Dupond dans votre répertoire de travail, le résultat est plutôt horrible : tous vos fichiers personnels seront détruits. Ça devient une catastrophe si vous vous trouvez en super-utilisateur à la racine : le système est complètement anéanti !
Les virus semblent vraiment gentils, des choses vraiment inoffensives...
Faites attention lorsque vous écrivez vos scripts. Comme vous l'avez vu, vous avez la possibilité de détruire complètement votre système en un éclair.
Unix was never designed to keep people from doing stupid things,
because that policy would also keep them from doing clever
things.
(« Unix n'a jamais été conçu pour empêcher les gens de faire des
choses stupides, parce que ça les empêcherait aussi de faire des
choses intelligentes »)
-- Doug Gwyn
Unix gives you just enough rope to hang yourself -- and then a
couple more feet, just to be sure.
(« Unix vous donne juste assez de corde pour vous pendre
-- et quelques mètres de plus, juste pour être sûr »)
-- Eric Allman
La philosophie prend tout son sens : puissance illimitée dans les outils, restrictions grâce aux permissions. Mais elle impose une responsabilité : vous devez faire attention. Comme corollaire, lorsque vous vous connectez en root, ne lancez pas un scripts si vous ne pouvez pas prouver qu'il est inoffensif (notez l'hypothèse très importante dans cette phrase : prouver qu'il est inoffensif).
Les structures de contrôle (boucles et conditions) sont très importantes dans la plupart des scripts. Lorsque nous analyserons d'autres scripts shell, dans les futurs articles, vous verrez une myriade de façon de les utiliser (un script même de complexité moyenne ne peut exister sans ces structures).
Le mois prochain, nous jetterons un coup d'oeil sur quelques outils
qui sont couramment utilisés dans les scripts shell (des outils qui
vous sont peut-être familiers, lorsque vous les utilisez en ligne de
commandes). Nous explorerons aussi comment ils peuvent être connectés
entre eux pour produire les résultats voulus. Nous disséquerons deux
scripts (les miens, à moins que quelqu'un d'autre soit suffisamment
courageux pour envoyer le résultat de ce qu'il a concocté à son
clavier). Ayez peur, ayez très peur :)
Tous commentaires et corrections sur cette série d'articles sont les
bienvenus, ainsi que n'importe quel script intéressant que vous
pourriez m'envoyer. Toutes les attaques/critiques seront envoyées à
/dev/null
(Oh, non, c'est déjà plein ...)
Au mois prochain,
Linuxement !
``What's this script do?
'unzip; touch; finger; mount; gasp; yes; umount; sleep'
Hint for the answer: not everything is computer-oriented. Sometimes you're in a sleeping bag, camping out with your girlfriend.''
« Que fait ce script ?
'unzip; touch; finger; mount; gasp; yes; umount; sleep'
Un indice : Il ne faut pas toujours penser qu'à l'informatique. Quelquefois, vous êtes dans un sac de couchage, en camping avec votre petite amie. »
-- Frans van der Zande
Les pages de manuel de 'bash', 'seq', 'ping', 'grep'.
La commande "help" pour 'for', 'while', 'until', 'if', 'case', 'test', 'break', 'continue'.
"Introduction to Shell Scripting - The Basics" par Ben Okopnik (Introduction à l'écriture de scripts shell, les bases, Linux Gazette numéro 52).
Copyright 1999, Ben Okopnik. Paru dans le numéro 53 de la Linux Gazette de Mai 2000.
Traduction française de Thierry Hamon.