11. Boucles et branchements

À quoi bon cette répétition, femme ?

--Shakespeare, Othello

Les opérations sur les blocs de code sont la clé des scripts shell structurés et organisés. La syntaxe des boucles et branchements fournit les outils pour cela.

11.1. Boucles

Une boucle est un bloc de code qui itère [51] 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 syntaxe de base pour les boucles. Elle diffère significativement de sa contrepartie 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 placés entre guillemets pour empêcher toute
#+ séparation de mots.

L'argument liste peut contenir des caractères jokers.

Si do est sur la même ligne que for, il faut impérativement mettre un point virgule après la liste.

for arg in [liste] ; do

Exemple 11.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 ? À cause des blancs dans la variable.
do
  echo $planete
done

exit 0

Chaque élément de la [liste] peut contenir plusieurs paramètres. C'est utile pour travailler sur des groupes de paramètres. Dans un tel cas, utilisez la commande set (voir l'Exemple 15.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 de position.

Exemple 11.2. Boucle for avec deux paramètres dans chaque élément de la [liste]

#!/bin/bash
# Les planètes, revisitées.

# Associe le nom de chaque planète à sa distance du soleil.

for planete in "Mercure 57" "Vénus 108" "Terre 150"  "Mars 227" "Jupiter 779"
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 par un tiret.

  #  Il peut être utile de sauvegarder les paramètres de position d'origine, 
  #+ car ils vont être écrasés.
  #  Un moyen pour cela est d'utiliser un tableau,
  #        parametres_d_origine=("$@")

  echo "$1              $2 000 000 km du soleil"
  #-------deux tabulations---on colle les zéros à la suite du paramètre $2
done

# (Merci, S.C., pour les clarifications supplémentaires.)

exit 0

Dans une boucle for, la [liste] peut aussi être alimentée par une variable.

Exemple 11.3. Fileinfo : opérer sur une liste de fichiers contenue dans une variable

#!/bin/bash
# file-info.sh

FICHIERS="/usr/sbin/accept
/usr/sbin/pwck
/usr/sbin/chroot
/usr/bin/fauxfichier
/sbin/badblocks
/sbin/ypbind"   # Liste de fichiers qui vous intéressent.
                # Nous avons ajouté un fichier fantaisiste, /usr/bin/fauxfichier.

echo

for fichier in $FICHIERS
do

  if [ ! -e "$fichier" ]       # Vérifie que 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.
  #  Remarque : pour que la commande ci-dessus puisse fonctionner. 
  #+ la base de données whatis doit avoir été d'abord configurée.
  #  Pour cela, en tant que root, lancez /usr/bin/makewhatis.
  echo
done  

exit 0

Dans une boucle for, si la [liste] contient des caractères jokers (* et ?) utilisés dans le remplacement des noms de fichier, alors l'expansion des noms de fichiers a lieu.

Exemple 11.4. Opérer 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 motif 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.15, « Générer des nombres premiers en utilisant l'opérateur modulo ». Voir aussi Exemple 15.17, « Inverser les paramètres de position ».

Exemple 11.5. in [liste] manquant dans une boucle for

#!/bin/bash

# Appeler ce script avec ou sans arguments, et observer ce qui se passe.

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, y compris les blancs).

echo

exit 0

Il est possible d'utiliser une évaluation de commande pour générer la [liste] d'une boucle for. Voir aussi l'Exemple 16.54, « Utiliser seq pour générer l'incrément d'une boucle », l'Exemple 11.10, « Afficher les liens symboliques dans un répertoire » et l'Exemple 16.48, « Conversion de base ».

Exemple 11.6. Générer la [liste] dans une boucle for avec une évaluation de commande

#!/bin/bash
# for-loopcmd.sh : Une boucle for avec une [liste]
# générée par une évaluation 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 d'une évaluation de commande pour créer la [liste].

Exemple 11.7. Un équivalent de grep pour les fichiers binaires

#!/bin/bash
#  bin-grep.sh: Dans un fichier binaire, trouve toutes les chaînes de caractères 
#* qui sont conformes à un motif.

# Équivalent de "grep" pour les fichiers binaires.
# Similaire par son effet à "grep -a"

E_MAUVAISARGS=65
E_PASDEFICHIER=66

if [ $# -ne 2 ]
then
  echo "Usage: `basename $0` chaine_recherchee nomfichier"
  exit $E_MAUVAISARGS
fi

if [ ! -f "$2" ]
then
        echo "Le fichier \"$2\" n'existe pas."
  exit $E_PASDEFICHIER
fi  

IFS=$'\012'       # Suivant la suggestion de Anton Filippov.
                  # on avait auparavant :  IFS="\n"
for mot in $( strings "$2" | grep "$1" )
# La commande "strings" extrait les chaînes de caractères des fichiers
#+ binaires.
# Sortie envoyée à travers un tube vers "grep", qui recherche la
# chaîne désirée.

do
  echo $mot
done

#  Comme nous l'a indiqué S.C., on pourrait remplacer 
#+ les lignes 23 à 33 ci-dessus par la chaîne :
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'


#+ Pour comprendre ce script,
#+ Essayez quelque chose comme "./bin-grep.sh mem /bin/ls"

exit 0

Un peu la même chose.

Exemple 11.8. Afficher tous les utilisateurs du système

#!/bin/bash
# userlist.sh

FICHIER_DES_MOTS_DE_PASSE=/etc/passwd
n=1           # Nombre d'utilisateurs

for nom in $(awk 'BEGIN{FS=":"}{print $1}' < "$FICHIER_DES_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 ?

Encore un exemple d'une [liste] résultant d'une substitution de commande.

Exemple 11.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 les 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 remplacer 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.

Un dernier exemple de [list] / substitution de commande, avec cette fois une « commande » qui est une fonction.

genere_liste ()
{
  echo "un deux trois"
}

for mot in $(genere_liste)  
         # "mot" va chercher le résultat de la fonction
do
  echo "$mot"
done

# un
# deux
# trois

La sortie d'une boucle for peut être envoyée via un tube à une ou plusieurs commandes.

Exemple 11.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 dans le 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.
#  La boucle n'est ici pas réellement nécessaire,
#+ car la sortie de la commande "find" est étendue en un seul mot.
#  Néanmoins, il est facile de le comprendre et de l'illustrer.

#  Dominik 'Aeneas' Schnitzer nous indique que ne pas mettre entre guillemets
#+ $( find $repertoire -type l )
#+ ferait échouer le script sur les noms de fichier comprenant des espaces.
#  Même cette écriture ne recueillera que le premier champ de chaque argument.

exit 0


# --------------------------------------------------------
# Jean Helou propose l'alternative suivante :

echo "Liens symboliques dans le 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 cette modification du code de Helou :

OLDIFS=$IFS
IFS='' # Un IFS vide signifie aucun séparateur de mots
for fichier in $( find $directory -type l )
do
  echo $fichier
  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 espace, comme dans un exemple précédent)."

La sortie standard d'une boucle peut être redirigée vers un fichier, comme le montre cette légère modification de l'exemple précédent.

Exemple 11.11. Liens symboliques dans un répertoire, enregistré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"    # sortie standard de la boucle
#               ^^^^^^^^^^^^^^^^^^       redirigée vers le fichier d'enregistrement.

exit 0

Il existe une autre syntaxe pour une boucle for, ressemblant fortement à celle du C. Elle nécessite des parenthèses doubles.

Exemple 11.12. Une boucle for comme en 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 27.15, « Application complexe des tableaux Exploration d'une étrange série mathématique », l'Exemple 27.16, « Simuler un tableau à deux dimensions, puis son test » et l'Exemple A.6, « Séries de Collatz ».

---

Maintenant, une boucle for utilisée dans un contexte de la « vie quotidienne ».

Exemple 11.13. Utilisation de efax en mode batch

#!/bin/bash
# Envoi de fax ('efax' doit être installé).

ARGUMENTS_ATTENDUS=2
E_MAUVAISARGS=65
PORT_MODEM="/dev/ttyS2" # Cela peut être différent sur votre machine.
#                ^^^^^    Le port par défaut de la carte modem PCMCIA.

if [ $# -ne $ARGUMENTS_ATTENDUS ]
# Vérifie le nombre des arguments sur la 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              #  À partir de fichiers texte, crée des
                         #+ fichiers formatés pour le fax

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 pédagogique [sourire].

exit $?  #  efax envoie aussi des messages de diagnostic sur la
         #+ sortie standard.

while

Avec cette syntaxe, on teste une condition donnée au début de la boucle et on continue à boucler tant que cette 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 syntaxe avec crochets dans une boucle while n'est rien d'autre que notre vieille connaissance, le test entre crochets utilisé dans un test if/then. En réalité, une boucle while peut de manière tout à fait correcte utiliser la syntaxe plus souple avec doubles crochets (while [[ condition ]]).

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 11.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 11.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 11.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 à celle de C, avec les parenthèses doubles (voir aussi l'Exemple 8.5, « Manipulation de variables à la manière de C. »).

Exemple 11.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 syntaxe while read, qui s'avère bien 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

# =========== Snippet from "sd.sh" example script ========== #

  while read value   # Read one data point at a time.
  do
    rt=$(echo "scale=$SC; $rt + $value" | bc)
    (( ct++ ))
  done

  am=$(echo "scale=$SC; $rt / $ct" | bc)

  echo $am; return $ct   # This function "returns" TWO values!
  #  Caution: This little trick will not work if $ct > 255!
  #  To handle a larger number of data points,
  #+ simply comment out the "return $ct" above.
} <"$datafile"   # Feed in data file.

[Note]

Note

Une boucle while peut avoir son entrée stantard redirigée 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 (le contraire 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 pour les boucles for, placer do sur la même ligne que le test de la condition nécessite un point virgule.

until [ condition-est-vraie ] ; do

Exemple 11.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.



[51] Itération : exécution répétée d'une commande ou d'un groupe de commandes, en général -- mais pas toujours, tant qu' une certaine condition reste vraie ou jusqu'à ce qu' une certaine condition devienne vraie.