10. Boucles et branchements

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.

10.1. Boucles

Une boucle est un bloc de code qui répète [41] une liste de commandes aussi longtemps que la condition de contrôle de la boucle est vraie.

boucles for

for arg in [liste]

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

[Note]

Note

À 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.53, « 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.47, « 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.

while

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
[Note]

Note

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.

until

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.



[41] Itération : exécution répétée d'une commande ou d'un groupe de commande -- habituellement mais pas toujours -- tant qu'une certaine condition reste vraie ou jusqu'à ce qu'une certaine condition soit rencontrée.