Copyright © 2000 Ben Okopnik
Copyright © 2000 Thierry Hamon
Article paru dans le n°53 de la Gazette Linux de mai 2000.
Cet article est publié selon les termes de la 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
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.
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 à nouveau est évalué. Si le parcourt 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.
Un petit truc que j'aimerai mentionner ici. Si vous voulez que votre
boucle for
tourne un certain nombre de fois, la
syntaxe shell ressemblera à ç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 une option qui permet de
faire une sorte de boucle while
, mais c'est un peu plus
difficile à utiliser.
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 en bouclant 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 :
La commande 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
La seule ligne qui nous intéresse est celle qui nous donne le
pourcentage de paquets perdus. Avec un seul paquet, il ne peut être
que de 0% (la commande 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.
Les crochets qui contiennent les commandes précédentes sont
synonymes de la commande 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.
Tant que le test ci-dessus retourne la valeur « vrai »,
c'est-à-dire aussi longtemps que la commande 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.
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'à maintenant.
A plusieurs reprises, nous aurons besoin de vérifier la valeur d'une
condition et de contrôler l'exécution du script suivant le résultat. Pour cela,
nous avons la structure if
:
... if [ $PATON="pauvre type" ] then echo 'Pendre le boulot et bouscule le !' else echo 'Reste dans les parages; ça peut valoir le coup.' fi ...
Je vous accorde que ce n'est pas si facile... mais le sens logique est
là. Quoiqu'il en soit, si une variable appelée PATRON
a la
valeur « pauvre type » (ATTENTION, pour les programmeurs en 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 n'en sera pas affectée.
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. :)
Evidemment, 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, elles 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 assez
long exemple (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_gone) ouvrir_une_nouvelle_bouteille_de_rhum;; ... ... esac case $ANALYZE_DU_COMPORTEMENT_DES_INVITES in abat_jour_sur_la_tete) echo "Il est en train de boire";; parle_aux_plantes) echo "Elle est en train de fumer";; parle_aux_martiens) echo "Ils prennent du LSD";; objets_en_levitation) echo "Qui a pris mon jus de fruit ??";; ... ... ... esac done echo "He 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 est préférable de
l'écrire ainsi plutôt que d'utiliser plusieurs instruction 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 tous remplacer.
Pour pouvez continuer à vérifier que le statut de la nourriture pendant que vous essayez de convaincre la police que vous êtes en fait en train de tenir une réunion de collectionneurs de timbres (en fait, maintenir l'approvisionnement en gâteaux apéritifs est un facteur crucial dans ce cas), mais nous devons ignorer le statut des alcools — au fait, nous avons descendu Joe du lustre...
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
, cela affecte la boucle qui
l'entoure. continue
et break
s'appliquent
uniquement aux boucles, c'est-à-dire les structures de contrôle
for
, while
et until
.
Voici le script que nous avions écrit le mois dernier :
#!/bin/bash # "bkup" - copie les fichiers specifies dans le repertoire ~/Backup # de l'utilisateur apres avoir verifie les conflits de noms a=$(date +%T-%d_%m_%Y) cp -i $1 ~/Backup/$1.$a
C'est assez intéressant. Peu de temps après avoir terminer 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 est capable de le lire et de le comprendre. Regardons le quand même : ce que nous voulons d'un programme ou d'un script, c'est qu'il s'exécute lorsqu'on tape son nom, non ? C'est tout, ou alors qu'il nous dise pourquoi il ne fonctionne pas. Dans ce cas, nous avons ce message crypté :
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 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 juste d'apprendre :
#!/bin/bash if [ -z $1 ] then clear echo "'bkup' - copie les fichiers specifies dans le repertoire" echo "~/Backup de l'utilisateur apres avoir verifie les conflits" echo "de noms" echo echo "Usage: bkup nom_du_fichier" echo exit fi a=$(date +%T-%d_%m_%Y) cp -i $1 ~/Backup/$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. Vous n'avez même plus
besoin de mettre les commentaires du début, maintenant. 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. C'est
ce truc qui vous évitera de vous prendre la tête sur ce script l'année
prochaine, on ne sait jamais...
Avant de vous amuser avec ce script, voici quelques possibilités supplémentaires. Que se passe-t-il si vous voulez 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 specifies dans le repertoire" echo "~/Backup de l'utilisateur apres avoir verifie les conflits" echo "de noms" echo echo "Usage: bkup nom_du_fichier [rep_sauvegarde]" echo echo "bkup_dir Sous-repertoire optionel dans '~/Backup' ou le" echo "fichier sera sauvegarde." echo exit fi if [ -n $2 ] then if [ -d ~/Backup/$2 ] then subdir=$2/ else mkdir -p ~/Backup/$2 subdir=$2/ fi fi a=$(date +%T-%d_%m_%Y) cp -i $1 ~/Backup/$subdir$1.$a
Voici un résumé des modifications :
Dans la section contenant le message d'aide, on peut lire maintenant « ... l'arborescence des répertoires » plutôt que juste « répertoire ». Ceci indique les changements que nous avons fait.
La ligne « Usage : » a été modifiée pour indiquer l'argument optionnel (comme le montre les crochets). Nous avons ajouté une explication sur l'utilisation de cet argument, puisqu'elle pourrait ne pas être évidente pour tout le monde.
Une structure de contrôle 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
˜/Backup
, et 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).
Une variable subdir
est maintenant insérée dans la
commande cp, entre Backup/
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 docs
etc. Vous pouvez ainsi tout ranger dans la catégorie que vous voulez. De plus, l'ancien comportement de bkup est toujours disponible :
bkup file.xyz
copiera une sauvegarde de file.xyz
dans le répertoire
˜/Backup
. 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
˜/Backup
. Si vous écrivez quelque chose comme ça :
cp -i $1 ~/Backup/$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 ~/Backup//$1.$a
Notez que ce qui n'est pas un résultat particulièrement souhaitable, étant donné que nous voulez 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 # Ecrit par Larry, Moe, and Shemp - the Deleshun PoWeR TeaM!!! # Verifie par Curly: "Pourquoi, Bien sur qu'il marche ! Yek-Yek-Yek !" # Tout ce que vous avez a faire est d'entrer le nom de ce programme # suivi de ce que vous voulez effacer : repertoire, fichiers normaux, # plusieurs fichier, 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 vous même — seulement quelques mètres, 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 des scripts si vous n'êtes pas sûr qu'ils sont inoffensifs (notez la hypothèse très importante dans cette phrase : vous pouvez prouver qu'ils sont inoffensifs).
Les structures de contrôle (boucles et condition) 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'œil 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 ses concoctions au 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 #52
L'adaptation française de ce document a été réalisée dans le cadre du Projet de traduction de la Gazette Linux.
Vous pourrez lire d'autres articles traduits et en apprendre plus sur ce projet en visitant notre site : http://www.traduc.org/Gazette_Linux.
Si vous souhaitez apporter votre contribution, n'hésitez pas à nous rejoindre, nous serons heureux de vous accueillir.