What needs this iteration, woman?
--Shakespeare, Othello
Les opérations sur des blocs de code sont la clé pour des scripts shell structurés, organisés. Les constructions de boucles et de branchement fournissent les outils pour accomplir ceci.
Une boucle est un bloc de code qui répète [36] une liste de commandes aussi longtemps que la condition de contrôle de la boucle est vraie.
boucles for
C'est la construction de boucle de base. Elle diffère de façon significative de sa contre-partie en C.
for arg in [liste]
do
commande(s)...
done
À chaque passage dans la boucle, arg prend successivement la valeur de toutes les variables de la liste.
for arg in "$var1" "$var2" "$var3" ... "$varN" # Lors du tour 1 de la boucle, arg = $var1 # Lors du tour 2 de la boucle, arg = $var2 # Lors du tour 3 de la boucle, arg = $var3 # ... # Lors du tour N de la boucle, arg = $varN # Les arguments dans [liste] sont mis entre guillemets pour empêcher une #+ possible séparation des mots.
L'argument liste peut contenir des caractères joker.
Si do est sur la même ligne que for, il est impératif d'avoir un point virgule après la liste.
for arg in [liste] ; do
Exemple 10.1. Des boucles for simples
#!/bin/bash # Liste les planètes. for planete in Mercure Vénus Terre Mars Jupiter Saturne Uranus Neptune Pluton do echo $planete # Chaque plannète sur une ligne séparée. done echo for planete in "Mercure Vénus Terre Mars Jupiter Saturne Uranus Neptune Pluton" # Toutes les planètes sur la même ligne. # La 'liste' entière entourée par des guillemets crée une variable simple. # Pourquoi ? Espaces blancs dans la variable. do echo $planete done exit 0
Chaque élément de la [liste] peut contenir de multiples paramètres. C'est utile pour travailler sur des paramètres en groupe. Dans de tels cas, utilisez la commande set (voir l'Exemple 14.16, « Utiliser set avec les paramètres de position ») pour forcer l'analyse de chaque élément de la [liste] et l'affectation de chaque composant aux paramètres positionnels.
Exemple 10.2. Boucle for avec deux paramètres dans chaque élément de la [liste]
#!/bin/bash # Planètes revisitées. # Associe le nom de chaque planète à sa distance du soleil. for planete in "Mercure 36" "Vénus 67" "Terre 93" "Mars 142" "Jupiter 483" do set -- $planete # Analyse la variable "planete" #+ et initialise les paramètres de position. # Le "--" empêche de mauvaises surprises si $planete est nul #+ ou commence avec un tiret. # Il peut être utile de sauvegarder les paramètres de position originaux #+ car ils seront écrasés. # Une façon de le faire est d'utiliser un tableau, # parametres_originaux=("$@") echo "$1 $2.000.000 miles du soleil" #-------deux tabulations---concatènent les zéros dans le paramètre $2 done # (Merci, S.C., pour les clarifications supplémentaires.) exit 0
Une variable peut fournir la [liste] dans une boucle for.
Exemple 10.3. Fileinfo : opérer sur une liste de fichiers contenue dans une variable
#!/bin/bash # fileinfo.sh FICHIERS="/usr/sbin/accept /usr/sbin/pwck /usr/sbin/chroot /usr/bin/fakefile /sbin/badblocks /sbin/ypbind" # Liste de fichiers qui vous intéressent. # Envoyez-les dans un fichier quelconque, /usr/bin/fauxfichier. echo for fichier in $FICHIERS do if [ ! -e "$fichier" ] # Vérifie si le fichier existe. then echo "$fichier n'existe pas."; echo continue # Au suivant. fi ls -l $fichier | awk '{ print $9 " taille: " $5 }' # Affiche 2 champs. whatis `basename $fichier` # Informations sur le fichier. # Notez que la base de données whatis doit avoir été configurée #+ pour que ceci fonctionne. # Pour cela, en tant que root, lancez /usr/bin/makewhatis. echo done exit 0
Si la [liste] dans une boucle for contient des caractères joker (* et ?) utilisés dans le remplacement des noms de fichier, alors l'expansion des noms de fichiers a lieu.
Exemple 10.4. Agir sur des fichiers à l'aide d'une boucle for
#!/bin/bash # list-glob.sh: Générer une [liste] dans une boucle for #+ en utilisant le remplacement. echo for fichier in * # ^ Bash réalise une expansion de noms de fichiers #+ sur les expressions que le "globbing" reconnaît. do ls -l "$fichier" # Liste tous les fichiers de $PWD (répertoire courant). # Rappelez-vous que le caractère joker "*" correspond à chaque nom de fichier, #+ néanmoins, lors du remplacement, il ne récupère pas les fichier commençant #+ par un point. # Si le modèle ne correspond à aucun fichier, il s'étend à lui-même. # Pour empêcher ceci, utilisez l'option nullglob #+ (shopt -s nullglob). # Merci, S.C. done echo; echo for fichier in [jx]* do rm -f $fichier # Supprime seulement les fichiers commençant par un "j" ou # un "x" dans $PWD. echo "Suppression du fichier \"$fichier\"". done echo exit 0
Omettre la partie in [liste] d'une boucle for fait en sorte que la boucle opère sur $@, les paramètres de position. Une illustration particulièrement intelligente de ceci est l'Exemple A.16, « primes: Générer des nombres premiers en utilisant l'opérateur modulo ». Voir aussi Exemple 14.17, « Inverser les paramètres de position ».
Exemple 10.5. in [liste] manquant dans une boucle for
#!/bin/bash # Appeler ce script à la fois avec et sans arguments, et voir ce que cela donne. for a do echo -n "$a " done # La 'liste' est manquante, donc la boucle opère sur '$@' #+ (la liste d'arguments sur la ligne de commande, incluant les espaces blancs). echo exit 0
Il est possible d'utiliser la substitution de commandes pour générer la [liste] d'une boucle for. Voir aussi l'Exemple 15.51, « Utiliser seq pour générer l'incrément d'une boucle », l'Exemple 10.10, « Afficher les liens symboliques dans un répertoire » et l'Exemple 15.45, « Conversion de base ».
Exemple 10.6. Générer la [liste] dans une boucle for avec la substitution de commandes
#!/bin/bash # for-loopcmd.sh : Une boucle for avec une [liste] # générée par une substitution de commande. NOMBRES="9 7 3 8 37.53" for nombre in `echo $NOMBRES` # for nombre in 9 7 3 8 37.53 do echo -n "$nombre " done echo exit 0
Voici un exemple un peu plus complexe de l'utilisation de la substitution de commandes pour créer la [liste].
Exemple 10.7. Un remplaçant de grep pour les fichiers binaires
#!/bin/bash # bin-grep.sh: Trouve les chaînes de caractères correspondantes dans un fichier #+ binaire. # Un remplacement de "grep" pour les fichiers binaires. # Similaire par son effet à "grep -a" E_MAUVAISARGS=65 E_SANSFICHIER=66 if [ $# -ne 2 ] then echo "Usage: `basename $0` chaine_recherché nomfichier" exit $E_MAUVAISARGS fi if [ ! -f "$2" ] then echo "Le fichier \"$2\" n'existe pas." exit $E_SANSFICHIER fi IFS=$'\012' # Suivant la suggestion de Anton Filippov. # était auparavant : IFS="\n" for word in $( strings "$2" | grep "$1" ) # La commande "strings" liste les chaînes de caractères dans les fichiers #+ binaires. # Sortie envoyée via un tube dans "grep", qui cherche la chaîne désirée. do echo $word done # Comme S.C. l'a indiqué, les lignes 23 à 31 ci-dessus pourraient être #+ remplacées avec la chaîne # strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]' # Essayez quelque chose comme "./bin-grep.sh mem /bin/ls" #+ pour comprendre ce script. exit 0
Un peu la même chose.
Exemple 10.8. Afficher tous les utilisateurs du système
#!/bin/bash # userlist.sh FICHIER_MOTS_DE_PASSE=/etc/passwd n=1 # Nombre d'utilisateurs for nom in $(awk 'BEGIN{FS=":"}{print $1}' < "$FICHIER_MOTS_DE_PASSE" ) # Champ séparateur = : ^^^^^^ # Affiche le premier champ ^^^^^^^^ # Obtient l'entrée à partir du fichier ^^^^^^^^^^^^^^^^^^^^^^ do echo "UTILISATEUR #$n = $nom" let "n += 1" done # UTILISATEUR #1 = root # UTILISATEUR #2 = bin # UTILISATEUR #3 = daemon # ... # UTILISATEUR #30 = bozo exit 0 # Exercice : # --------- # Comment se fait-il qu'un utilisateur (ou un script lancé par cet utilisateur) #+ puisse lire /etc/passwd ? # N'est-ce pas un trou de sécurité ? Pourquoi ou pourquoi pas ?
Un dernier exemple d'une [liste] résultant d'une substitution de commande.
Exemple 10.9. Rechercher les auteurs de tous les binaires d'un répertoire
#!/bin/bash # findstring.sh : # Cherche une chaîne de caractères particulière dans des binaires d'un #+ répertoire particulier. repertoire=/usr/bin/ chainef="Free Software Foundation" # Voir quels fichiers viennent de la FSF. for fichier in $( find $repertoire -type f -name '*' | sort ) do strings -f $fichier | grep "$chainef" | sed -e "s%$repertoire%%" # Dans l'expression "sed", il est nécessaire de substituer le délimiteur #+ standard "/" parce que "/" se trouve être un caractère filtré. Ne pas le #+ faire provoque un message d'erreur (essayez). done exit 0 # Exercice (facile): # --------------- # Convertir ce script pour prendre en paramètres de ligne de commande les #+ variables $repertoire et $chainef.
La sortie d'une boucle for peut être envoyée via un tube à une ou plusieurs commandes.
Exemple 10.10. Afficher les liens symboliques dans un répertoire
#!/bin/bash # symlinks.sh : Liste les liens symboliques d'un répertoire. repertoire=${1-`pwd`} # Par défaut, le répertoire courant, si le répertoire n'est pas spécifié. # Équivalent au bloc de code ci-dessous. # ----------------------------------------------------------------- # ARGS=1 # Attend un argument en ligne de commande. # # if [ $# -ne "$ARGS" ] # Si sans argument... # then # repertoire=`pwd` # répertoire courant # else # repertoire=$1 # fi # ----------------------------------------------------------------- echo "Liens symboliques du répertoire \"$repertoire\"" for fichier in "$( find $repertoire -type l )" # -type l = liens symboliques do echo "$fichier" done | sort # Sinon la liste de fichiers n'est pas triée. # Une boucle n'est pas réellement nécessaire ici, #+ car la sortie de la commande "find" est étendue en un seul mot. # Néanmoins, il est facile de comprendre et d'illustrer ceci. # Comme Dominik 'Aeneas' Schnitzer l'indique, ne pas mettre entre guillemets #+ $( find $repertoire -type l ) #+ fera échouer le script sur les noms de fichier comprenant des espaces. # Même ceci ne prendra que le premier champ de chaque argument. exit 0 # -------------------------------------------------------- # Jean Helou propose l'alternative suivante : echo "Liens symboliques du répertoire \"$repertoire\"" # Sauvegarde du IFS actuel. On n'est jamais trop prudent. OLDIFS=$IFS IFS=: for fichier in $(find $repertoire -type l -printf "%p$IFS") do # ^^^^^^^^^^^^^^^^ echo "$fichier" done|sort # Et James "Mike" Conley suggère la modification du code de Helou : OLDIFS=$IFS IFS='' # Un IFS vide singifie aucun séparateur de mots for file in $( find $directory -type l ) do echo $file done | sort # Ceci fonctionne dans le cas "pathologique" d'un nom de répertoire contenant #+ une virgule. # "Ceci corrige aussi le cas "pathologique" d'un nom de répertoire contenant #+ une virgule (ou une espace dans un exemple précédent)."
Le stdout d'une boucle peut être redirigé vers un fichier, comme cette légère modification du précédent exemple le montre.
Exemple 10.11. Liens symboliques dans un répertoire, sauvés dans un fichier
#!/bin/bash # symlinks.sh : Liste les liens symboliques dans un répertoire. FICHIER_DE_SORTIE=liste.liens_symboliques # fichier de sauvegarde repertoire=${1-`pwd`} # Par défaut, le répertoire courant si aucun autre n'a été spécifié. echo "liens symboliques dans le répertoire \"$repertoire\"" > "$FICHIER_DE_SORTIE" echo "----------------------------------------------------" >> "$FICHIER_DE_SORTIE" for fichier in "$( find $repertoire -type l )" # -type l = liens symboliques do echo "$fichier" done | sort >> "$FICHIER_DE_SORTIE" # stdout de la boucle # ^^^^^^^^^^^^^^^^^^ redirigé vers le fichier de sauvegarde. exit 0
Il existe une autre syntaxe pour une boucle for ressemblant fortement à celle du C. Elle nécessite des parenthèses doubles.
Exemple 10.12. Une boucle for à la C
#!/bin/bash # Deux façons de compter jusqu'à 10. echo # Syntaxe standard. for a in 1 2 3 4 5 6 7 8 9 10 do echo -n "$a " done echo; echo # +==========================================+ # Maintenant, faisons de même en utilisant une syntaxe C. LIMITE=10 for ((a=1; a <= LIMITE ; a++)) # Double parenthèses, et "LIMITE" sans "$". do echo -n "$a " done # Une construction empruntée à 'ksh93'. echo; echo # +=========================================================================+ # Utilisons l'opérateur "virgule" C pour incrémenter deux variables en même #+ temps. for ((a=1, b=1; a <= LIMITE ; a++, b++)) # La virgule chaîne les opérations. do echo -n "$a-$b " done echo; echo exit 0
Voir aussi l'Exemple 26.15, « Application complexe des tableaux Exploration d'une étrange série mathématique », l'Exemple 26.16, « Simuler un tableau à deux dimensions, puis son test » et l'Exemple A.6, « collatz : Séries de Collatz ».
---
Maintenant, une boucle for utilisée dans un contexte de la « vie quotidienne ».
Exemple 10.13. Utiliser efax en mode batch
#!/bin/bash # Fax (doit avoir installé 'efax'). ARGUMENTS_ATTENDUS=2 E_MAUVAISARGS=65 MODEM_PORT="/dev/ttyS2" # Cela peut être différent sur votre machine. # Port par défaut de la carte modem PCMCIA. if [ $# -ne $ARGUMENTS_ATTENDUS ] # Vérifie le bon nombre d'arguments en ligne de commande. then echo "Usage: `basename $0` téléphone# fichier-texte" exit $E_MAUVAISARGS fi if [ ! -f "$2" ] then echo "Le fichier $2 n'est pas un fichier texte" # Ce fichier n'est pas un fichier standard ou il n'existe pas. exit $E_MAUVAISARGS fi fax make $2 # Crée des fichiers formatés pour le fax à partir de #+ fichiers texte. for fichier in $(ls $2.0*) # Concatène les fichiers convertis. # Utilise le caractère joker dans la liste des variables. do fic="$fic $fichier" done efax -d "$MODEM_PORT" -t "T$1" $fic # Fait le boulot. # Essayez d'ajouter -o1 si la ligne ci-dessus échoue. # Comme S.C. l'a indiqué, la boucle for peut être supprimée avec # efax -d /dev/ttyS2 -o1 -t "T$1" $2.0* # mais ce n'est pas aussi instructif. exit $? # De plus, efax envoie des messages de diagnostique sur stdout.
Cette construction teste une condition au début de la boucle et continue à boucler tant que la condition est vraie (renvoie un 0 comme code de sortie). Par opposition à une boucle for, une boucle while trouve son utilité dans des situations où le nombre de répétitions n'est pas connu dès le départ.
while [ condition ]
do
commande(s)...
done
La construction utilisant des crochets dans une boucle while n'est rien de plus que notre ancien ami, le test entre crochets utilisé dans un test if/then. En fait, une boucle while peut être légalement utilisé avec la construction à double chrochets (while [[ condition ]]) car elle est plus versatile.
Comme c'est le cas avec les boucles for, placer le do sur la même ligne que le test de la condition nécessite un point virgule.
while [ condition ] ; do
Note that the test brackets are not mandatory in a while loop. See, for example, the getopts construct.
Exemple 10.14. Simple boucle while
#!/bin/bash var0=0 LIMITE=10 while [ "$var0" -lt "$LIMITE" ] # ^ ^ # Espaces ajoutés car ce sont des tests entre crochets. do echo -n "$var0 " # -n supprime le retour chariot. # ^ espace, pour séparer les numéros affichés. var0=`expr $var0 + 1` # var0=$(($var0+1)) fonctionne aussi. # var0=$((var0 + 1)) fonctionne aussi. # let "var0 += 1" fonctionne aussi. done # D'autres méthodes fonctionnent aussi. echo exit 0
Exemple 10.15. Une autre boucle while
#!/bin/bash echo # Équivalent à while [ "$var1" != "fin" ] # while test "$var1" != "fin" do echo "Variable d'entrée #1 (quitte avec fin) " read var1 # pas de 'read $var1' (pourquoi?). echo "variable #1 = $var1" # A besoin des guillemets à cause du "#"... # Si l'entrée est 'fin', l'affiche ici. # Ne teste pas la condition de fin avant de revenir en haut de la boucle. echo done exit 0
Une boucle while peut avoir de multiples conditions. Seule la condition finale détermine quand la boucle se termine. Malgré tout, ceci nécessite une syntaxe de boucle légèrement différente.
Exemple 10.16. Boucle while avec de multiples conditions
#!/bin/bash var1=unset precedent=$var1 while echo "Variable précédente = $precedent" echo precedent=$var1 [ "$var1" != fin ] # Garde trace de ce que $var1 valait précédemment. # Quatre conditions sur "while", mais seule la dernière contrôle la #+ boucle. # Le *dernier* code de sortie est celui qui compte. do echo "Variable d'entrée #1 (quitte avec fin) " read var1 echo "variable #1 = $var1" done # Essayez de comprendre comment cela fonctionne. # Il y a un peu d'astuce. exit 0
Comme pour une boucle for, une boucle while peut employer une syntaxe identique à C en utilisant la construction avec des parenthèses doubles (voir aussi l'Exemple 9.33, « Manipulation, à la façon du C, de variables »).
Exemple 10.17. Syntaxe à la C pour une boucle while
#!/bin/bash # wh-loopc.sh : Compter jusqu'à 10 dans une boucle "while". LIMITE=10 a=1 while [ "$a" -le $LIMITE ] do echo -n "$a " let "a+=1" done # Pas de surprise jusqu'ici. echo; echo # +=================================================================+ # Maintenant, de nouveau mais avec une syntaxe C. ((a = 1)) # a=1 # Les doubles parenthèses permettent l'utilisation des espaces pour initialiser #+ une variable, comme en C. while (( a <= LIMITE )) # Doubles parenthèses, et pas de "$" devant la variable. do echo -n "$a " ((a += 1)) # let "a+=1" # Oui, en effet. # Les doubles parenthèses permettent d'incrémenter une variable avec une #+ syntaxe style C. done echo # Les programmeurs C se sentent chez eux avec Bash. exit 0
À l'intérieur des crochets de test, une boucle while peut appeler une fonction.
t=0 condition () { ((t++)) if [ $t -lt 5 ] then return 0 # true else return 1 # false fi } while condition # ^^^^^^^^^ # Appel de fonction -- quatre itérations de boucle. do echo "Toujours en cours : t = $t" done # Toujours en cours : t = 1 # Toujours en cours : t = 2 # Toujours en cours : t = 3 # Toujours en cours : t = 4
En couplant la puissance de la commande read avec une boucle while, nous obtenons la construction while read, utile pour lire et analyser des fichiers.
cat $nomfichier | # Fournit des informations à partir d'un fichier. while read ligne # Tant qu'il y a une nouvelle ligne à lire... do ... done
Une boucle while peut avoir son stdin redirigé vers un fichier par un < à la fin.
Une boucle while peut avoir son entrée standard (stdin) fourni via un tube.
Cette construction teste une condition au début de la boucle et continue à boucler tant que la condition est fausse (l'opposé de la boucle while).
until [ condition-est-vraie ]
do
commande(s)...
done
Notez qu'une boucle until teste la condition de fin au début de la boucle, contrairement aux constructions similaires dans certains langages de programmation.
Comme c'est le cas avec les boucles for, placez do sur la même ligne que le test de la condition nécessite un point virgule.
until [ condition-est-vraie ] ; do
Exemple 10.18. Boucle until
#!/bin/bash CONDITION_FINALE=fin until [ "$var1" = "$CONDITION_FINALE" ] # Condition du test ici, en haut de la boucle. do echo "Variable d'entrée #1 " echo "($CONDITION_FINALE pour sortir)" read var1 echo "variable #1 = $var1" done exit 0
Comment choisir entre une boucle for, une boucle while et une boucle until ? En C, vous utiliserez typiquement une boucle for quand le nombre d'itérations est déjà connu. Néanmoins, avec Bash, la situation est plus compliquée. La boucle for en Bash est moins structurée et plus flexible que son équivalent dans d'autres langages. Du coup, n'hésitez pas à utiliser le type de boucle qui vous permet de faire ce que vous souhaitez de la façon la plus simple.
[36] Itération : exécution répétée d'une commande ou d'un groupe de commande tant qu'une certaine condition reste vraie ou jusqu'à ce qu'une certaine condition soit rencontrée.