Une fois que vous avez compris qu'un script n'est rien d'autre qu'une suite de commandes, vous avez fait un grand pas ; chaque script consiste à poser des rails : l'utilisateur n'a plus qu'à les suivre (si tout cela ne vous paraît pas évident, consultez la page d'initiation à la programmation en shell).
Toutefois, avoir posé les rails ne suffit pas toujours : il peut importer de disposer de mécanismes d'aiguillage. Les boucles vous permettront de réaliser ces mécanismes d'aiguillage, qui donneront à vos scripts souplesse et efficacité.
if
et case
if
Robert le jardinier programme un hachoir en shell, et veut en interdire
l'accès à tout autre utilisateur que lui-même, afin de protéger le petit
Émile de ce dangereux instrument. Le programme hachoir
se
pose la question suivante : est-ce que $USER
vaut
« Robert » ?
C'est pour les structures de type « si... alors... ou sinon »,
la commande if
a été faite. Elle est constituée de cinq
termes, dont trois sont obligatoires :
if
(si), qui marque la condition à remplir ;
then
(alors), qui marque les conséquences si la
condition est remplie ; elif
(contraction de else if, qui signifie
« sinon, si... »), qui est facultatif et marque une
condition alternative ;else
(sinon), qui est facultatif et marque le
comportement à adopter si la condition n'est pas remplie ; fi
(if
à l'envers), qui marque la fin de
la boucle.On peut donc taper :
if commande ; then commandes ; else commandes ; fi
ou bien (car le point et virgule équivaut à un saut de ligne) :
if condition then commandes elif condition then commandes else commandes fi
Par exemple, pour le hachoir de Robert le jardinier, on aura :
if test $USER = Robert then echo "Le hachoir va se mettre en marche." mettre en marche le hachoir else echo "Quand tu seras grand, $USER." echo "Et fais bien attention en traversant la rue." fi
Comme vous pouvez le constater, à la suite des commandes
then
et else
, vous pouvez mettre autant de
commandes que vous le voulez. Le shell exécute tout ce qu'il
trouve jusqu'à la prochaine instruction.
Par exemple, si la condition indiquée par if
est
remplie, le shell va exécuter tout ce qui suit then
jusqu'à
ce qu'il trouve l'une de ces instructions :
elif
,else
oufi
.Autre exemple :
#!/bin/sh # Fichier "mon-pwd" if test $PWD = $HOME then echo "Vous êtes dans votre répertoire d'accueil." elif test $PWD = $HOME/bin then echo "Vous êtes dans votre répertoire d'exécutables." elif test $PWD = $HOME/bin/solaris then echo "Vous êtes dans votre répertoire d'exécutables pour Solaris." else echo 'Vous êtes dans un répertoire quelconque, qui n'est ni $HOME,' echo 'ni $HOME/bin, ni $HOME/bin/solaris. '"Vous êtes dans $PWD." fi
elif
successifs est
illimité, alors qu'il ne peut (et c'est logique) y avoir qu'une
seule condition if
, une seule instruction else
et une seule instruction fi
dans une boucle. $HOME
est cité dans des guillemets simples, alors que $PWD
est
cité entre guillemets doubles. Par conséquent, $HOME ne sera pas
interprété (le mot « $HOME » apparaîtra tel quel), tandis que
$PWD
sera interprété (il sera remplacé, par exemple, par
/home/toto
).case
case
à if
?La boucle if
est très pratique, mais devient rapidement
illisible lorsqu'un aiguillage offre plusieurs sorties, et que l'on doit
répéter une condition pusieurs fois sous des formes à peine
différentes. Typiquement, un programme comme celui-ci est pénible à
écrire, à lire et à débuguer :
if test $PWD = $HOME then echo "Vous êtes dans votre répertoire d'accueil." elif test $PWD = $HOME/bin then echo "Vous êtes dans votre répertoire d'exécutables." elif test $PWD = $HOME/bin/solaris then echo "Vous êtes dans votre répertoire d'exécutables pour Solaris." elif test $PWD = $HOME/prive then echo "Vous êtes dans votre répertoire privé." else echo 'Vous êtes dans un répertoire quelconque, qui n'est ni $HOME,' echo 'ni $HOME/bin, ni $HOME/bin/solaris. '"Vous êtes dans $PWD." fi
Pourquoi ce programme est-il pénible à écrire, à lire et à débuguer ?
test
$PWD =
» ; et qui dit redondance dit possibilité de
faute de frappe – erreur particulièrement difficile à repérer
à l'œil nu ;if
n'est pas, dans la boucle, sur le même plan que
elif
, alors que logiquement les deux instructions le
sont), ce qui nuit à la lisibilité.La boucle case
entend remédier à ces inconvénients. Il
s'agit tout simplement de la simplification d'un usage fréquent de la
structure if
. On peut récrire le programme suivant avec
case
:
case "$PWD" in "$HOME") echo "Vous êtes dans votre répertoire d'accueil.";; "$HOME/bin") echo "Vous êtes dans votre répertoire d'exécutables.";; "$HOME/bin/solaris") echo "Vous êtes dans votre répertoire d'exécutables pour Solaris.";; "$HOME/prive") echo "Vous êtes dans votre répertoire privé.";; "*") echo 'Vous êtes dans un répertoire quelconque, qui n'est ni $HOME,' echo 'ni $HOME/bin, ni $HOME/bin/solaris. '"Vous êtes dans $PWD.";; esac
Le gain de lisibilité est flagrant (si si, c'est flagrant, je vous assure) :
elif
;case
La boucle case
adopte la syntaxe suivante :
case chaîne in motif) commandes ;; motif) commandes ;; esac
La boucle case
commence par évaluer la
chaîne. Ensuite, elle va lire chaque motif. Enfin, et
dès qu'une chaîne correspond au motif, elle exécute
les commandes appropriées. En anglais, case signifie
« cas » ; cette structure de contrôle permet en effet de
réagir adéquatement aux différents « cas de figure »
possibles.
Vous observerez que :
case
est close par l'instruction
esac
, qui n'est autre que le mot case
à
l'envers (de même que la boucle if
est terminée par
l'instruction fi
).
Un motif (pattern) est un mot contenant éventuellement
les constructions *
, ?
, [a-d]
,
avec la même signification que pour les raccourcis dans les noms de
fichiers (les jokers). Exemple :
case $var in [0-9]*) echo "$var est un nombre.";; [a-zA-Z]*) echo "$var est un mot.";; *) echo "$var n'est ni un nombre ni un mot.";; esac
Que faire si je veux aiguiller plusieurs motifs différents de la même façon ? Le premier réflexe est de répéter, presque à l'identique, des lignes, de la façon suivante :
#!/bin/sh # Fichier "canards" echo "Quel est ton nom ?" read nom case $nom in "Riri") echo "Ton oncle s'appelle Donald, Riri.";; "Fifi") echo "Ton oncle s'appelle Donald, Fifi.";; "Loulou") echo "Ton oncle s'appelle Donald, Loulou.";; "*") echo "Tu n'es pas un neveu de Donald.";; esac
Mais il n'est jamais bon de répéter manuellement des lignes de programmation, car c'est générateur de fautes. Il faut toujours chercher des raccourcis, et ne se répéter qu'en dernier recours. On utilisera donc le caractère « | », qui permet de mettre sur le même plan différents motifs. On pourra donc écrire :
#!/bin/sh # Fichier "canards" echo "Quel est ton nom ?" read nom case $nom in "Riri"|"Fifi"|"Loulou") echo "Ton oncle s'appelle Donald, $nom.";; "*") echo "Tu n'es pas un neveu de Donald.";; esac
On gagne ainsi en concision, et on évite bien des fautes d'inattention, qui sont particulièrement coriaces à débuguer.
La boucle case
exécute les commandes correspondant au
premier motif correct, et ne parcourt pas les motifs
suivants si elle en a trouvé un. Par conséquent, si le dernier
motif est « * », cela revient à l'instruction
else
dans la boucle if
.
while
, until
et for
Les structures if
et case
sont des
structures d'aiguillage ; les structures while
,
until
et for
sont proprement des
boucles. C'est-à-dire qu'elles soumettent à une condition déterminée la
répétition (ou non) d'une suite de commandes.
while
La boucle while
exécute les commandes de manière
répétée tant que la première commande réussit ; en
anglais, while signifie « tant que ».
while
La boucle while
adopte la syntaxe suivante :
while condition do commandes done
Après l'instruction do
, vous pouvez mettre
autant de commandes que vous le désirez, suivies de retours
chariot ou bien de points et virgules (« ; »). Le shell
exécutera tout ce qu'il trouve, jusqu'à ce qu'il tombe sur l'instruction
done
.
echo "Tapez le mot de passe :" read password while test "$password" != mot de passe correct do echo "Mot de passe incorrect." echo "Tapez le mot de passe" read password done echo "Mot de passe correct." echo "Bienvenue sur les ordinateurs secrets du Pentagone."
Les lignes qui suivent la boucle while
seront exécutées si, et seulement si, la condition est remplie,
c'est-à-dire si l'utilisateur tape le bon mot de passe. (En pratique, il
est très fortement déconseillé de taper un mot de passe en clair dans un
script, car rien n'est plus facile que de l'éditer !...).
while
Supposons que vous vouliez créer cinquante fichiers sous la forme
fichier1
, fichier2
, fichier3
,
etc. Un petit script vous fera ça plus vite que cinquante lignes de
commande.
#!/bin/sh # Fichier "50fichiers" numero=1 while test $numero != 51 do # la commande "touch" permet de créer un fichier vide : touch fichier"$numero" # cette commande ajoute 1 à la variable "numero" : numero=$(($numero + 1)) done
Avec ce script, la variable numero
est
d'abord créée avec la valeur 1. Ensuite, le shell examine si la
valeur de numero
est différente de 51 ; c'est le
cas. Par conséquent, le shell exécute les commandes suivantes : il
crée un fichier fichier1
, puis ajoute 1 à la variable
numero
, qui vaut donc 2. Et c'est reparti pour un
tour ! Le shell examine si la valeur de numero
,
c'est-à-dire 2, est différente de 51, etc.
Petit conseil pour faire les choses le plus proprement possible : dans le script qui précède, remplacez « 51 » par une variable : le script sera beaucoup plus lisible. Exemple :
#!/bin/sh # Fichier "50fichiers" numero=1 limite=51 while test $numero != $limite do # la commande "touch" permet de créer un fichier vide : touch fichier"$numero" # cette commande ajoute 1 à la variable "numero" : numero=$(($numero + 1)) done
until
Until signifie « jusqu'à ce que » en anglais. Cette
boucle est le pendant de la boucle while
, à ceci près que
la condition ne détermine pas la cause de la répétition
de la boucle, mais celle de son interruption.
until
until condition do commandes done
On aura ainsi :
echo "Tapez le mot de passe :" read password until test $password = mot de passe correct do echo "Mot de passe incorrect." echo "Tapez le mot de passe" read password done echo "Mot de passe correct." echo "Bienvenue sur les ordinateurs secrets de la NASA."
Il revient en effet au même de répéter une suite de commandes « tant que le mot de passe est incorrect » ou bien « jusqu'à ce que le mot de passe soit correct », car un mot de passe déterminé ne peut être que correct ou incorrect.
while
ou until
?Vous allez donc me demander que préférer entre while
et
until
, si les deux sont si souvent équivalentes ? Eh
bien, celle dont la condition sera la plus facile à écrire, à lire et à
débuguer. Si, comme dans notre exemple, elles sont aussi simples
l'une que l'autre, choisissez votre préférée ou prenez-en une au hasard.
for
La boucle for
affecte successivement à une variable chaque
chaîne de caractères trouvée dans une liste de chaînes, et
exécute les commandes une fois pour chaque chaîne.
for
for var in liste de chaînes do commandes done
#!/bin/sh # Fichier "liste" for element in * do echo "$element" done
affiche les noms de tous les fichiers du répertoire courant, un par ligne.
Concrètement, le shell va prendre la liste des chaînes, en
l'occurrence « * », c'est-à-dire tous les fichiers et
répertoires du répertoire courant. À chacun de ces éléments, il va
attribuer la variable element
, puis exécuter les commandes
spécifiées, en l'occurrence echo
"$element"
. Ainsi, si dans mon répertoire j'ai les
fichiers a
, b
et c
, le shell va
exécuter successivement echo "a"
, echo
"b"
et echo "c"
.
for
Comme while
et until
, for
est une
boucle au sens propre. Mais il y a une différence notable entre ces deux
groupes de boucle : while
et until
sont
plus appropriés à la répétition à l'identique d'une séquence de
commandes, tandis que for
convient particulièrement
aux répétitions soumises à de légères variations.
Prenons par exemple le script qui permettait, avec while
,
de créer 50 fichiers rapidement. La numération des passages par la
boucle était assez pénible, et éclatée en trois endroits :
numero
(et, éventuellement, de la valeur 51 à
la variable limite
) ;test $numero != 51
ou
test $numero != $limite
;numero
, avec la
ligne numero=$(($numero + 1))
.
Pourquoi dire en trois fois ce qui, dans les langages naturels
(c'est-à-dire humains), s'énonce en une seule : « répéter
n fois cette séquence de commandes » ? La boucle
for
se révèle alors indispensable :
#!/bin/sh # Fichier "50fichiers-for" # la commande "seq a b" compte de a à b for numero in `seq 1 50` do touch fichier"$numero" done
Avouez que le script est beaucoup plus simple de cette manière !
select
Il est parfois utile de donner à l'utilisateur du script des formulaires sous forme de questions à choix multiples.
Par exemple, je suis sociologue et je veux dresser des statistiques sur les opinions d'un échantillon représentatif de normaliens : sont-ils favorables ou défavorables à la destruction du NIR (Nouvel Immeuble Rataud) et à la reconstruction du VIR (Vieil Immeuble Rataud, a.k.a. Pavillon) ?
Spontanément, je fais le script suivant :
#!/bin/sh # Fichier "vote-nir" echo "Êtes-vous favorable au remplacement du NIR par le VIR ?" echo "(Répondez « pour » ou « contre »)" read opinion # M'envoyer le résultat par mail echo "$opinion" | mail bourdieu
Seulement voilà, de nombreuses personnes vont faire des fautes de frappe, et taper « poru » au lieu de « pour », « cnotre » au lieu de « contre », etc. Résultat : au lieu de laisser d'autres scripts traiter automatiquement les résultats des statistiques qui me sont envoyés par courriel, je devrai tout regarder à la main et cocher moi-même les résultats pour ne pas en oublier. Dans ce cas, autant faire un sondage classique à la sortie du Pot !
Heureusement, il existe une parade : la boucle
select
.
select
select variable in valeurs possibles do commandes done
Voici le script de notre sociologue :
#!/bin/sh # Fichier "vote-nir" echo "Êtes-vous favorable au remplacement du NIR par le VIR ?" select opinion in Pour Contre do break done # M'envoyer le résultat par mail echo "$opinion" | mail bourdieu
Les utilisateurs verront alors :
Êtes-vous favorable au remplacement du NIR par le VIR ? 1) Pour 2) Contre
Ils devront alors taper 1 ou 2, selon leur opinion.
Vous avez remarqué que l'instruction do
est suivie de la
commande break
. Celle-ci indique de sortir de la boucle
select
. Sans break
, select
est en
effet une boucle, comme while
, until
et
for
. Nous allons voir pourquoi.
select
comme boucle
Par défaut, select
se comporte comme une boucle. Observons
le comportement du script suivant :
#!/bin/sh # Fichier "piege" echo "Si tu arrives à sortir de ce script, je te donne 30 euros." select issue in "Sortir" "Quitter" # la commande "continue" est une commande vide : elle ne fait rien de spécial do continue done
Essayez maintenant d'exécuter ce script :
Si tu arrives à sortir de ce script, je te donne 30 euros. 1) Sortir 2) Quitter #? 1 #? 2 #? 1 #? 2 #? 1 (etc.)
Ce comportement est parfois utile, par exemple si vous programmez un petit jeu, ou autre. Ou encore, pour prévenir les réponses non valides. Exemple :
#!/bin/sh # Fichier "vote-nir" echo "Êtes-vous favorable au remplacement du NIR par le VIR ?" select opinion in Pour Contre do case $opinion in # Laisser passer ceux qui répondent correctement à la question "Pour"|"Contre") break;; # Au cas où des zozos tapent sur autre chose que 1 ou 2 "*") continue;; esac done # M'envoyer le résultat par mail echo "$opinion" | mail bourdieu
Grâce à cela, si un normalien tape une réponse
invalide, select
attend qu'il tape quelque chose de
correct ; s'il tape quelque chose de correct, la réponse est
envoyée au sociologue.
Vous aurez remarqué au passage que l'on peut sans problème imbriquer des structures de contrôle les unes dans les autres.
On remarque que la condition des commandes if
et
while
est une commande. Chaque commande renvoie un code de
retour (qui est ignoré en utilisation normale). Si le code est 0, la
commande a réussi ; sinon, la commande a échoué. Par exemple, le
compilateur cc
renvoie un code d'erreur non nul si le
fichier compilé contient des erreurs, ou s'il n'existe pas.
Les commandes if
et while
considèrent donc le
code de retour 0 comme « vrai », et tout autre code comme
« faux ».
Il existe une commande test
, qui évalue des expressions
booléennes (c'est-à-dire dont la valeur ne peut être que vraie ou
fausse) passées en argument, et renvoie un code de retour en fonction du
résultat. Elle est bien utile pour les scripts. Exemple :
if test $var = foo then echo 'La variable vaut foo' else echo 'La variable ne vaut pas foo' fi
Pour en savoir plus sur les opérateurs de test, consultez la page tests et calculs arithmétiques Ou bien vous pouvez revenir à la page centrale sur le shell, d'où vous pourrez vous orienter vers d'autres parties du cours.