Linux sans magie #33 — Boucles Bash : comprendre for, while, break et continue
Comprendre les boucles Bash avec for et while, utiliser break et continue, parcourir des fichiers sans casser les noms contenant des espaces, et éviter les pièges classiques.
Dans le #32, nous avons vu comment Bash choisit une branche avec if en fonction du statut d’une commande.
Une boucle reprend la même logique, mais avec une répétition : exécuter plusieurs fois un bloc de commandes, soit pour chaque élément d’une liste, soit tant qu’une condition reste vraie.
Dans ce #33, nous allons utiliser for, while, break et continue, avec un point particulièrement important en shell : parcourir des valeurs et des noms de fichiers sans casser ceux qui contiennent des espaces.
La boucle for parcourt une liste de mots
La forme la plus simple est :
for variable in valeur1 valeur2 valeur3
do
commandes
done
À chaque tour, Bash affecte un élément à la variable, puis exécute le bloc entre do et done.
Exemple :
for environnement in dev test production
do
printf 'Environnement : %s\n' "$environnement"
done
Résultat attendu :
Environnement : dev
Environnement : test
Environnement : production
La variable environnement change donc de valeur à chaque itération.
Les éléments peuvent venir d’une expansion
Une boucle for n’est pas limitée à une liste écrite en dur.
Exemple :
prefixe='srv'
for numero in 01 02 03
do
printf '%s-%s\n' "$prefixe" "$numero"
done
Résultat :
srv-01
srv-02
srv-03
Le principe important reste le même que dans les articles précédents : citez les expansions quand leur contenu doit rester un argument unique.
Ici :
"$prefixe"
"$numero"
préservent les valeurs transmises à printf.
Préparer un lab avec des noms de fichiers réalistes
Créons un répertoire temporaire :
LAB_DIR="$(mktemp -d)"
printf 'Lab : %s\n' "$LAB_DIR"
cd "$LAB_DIR"
Créons plusieurs fichiers, dont un contenant un espace :
touch 'alpha.log'
touch 'beta.log'
touch 'rapport final.log'
touch 'notes.txt'
Vérifiez le contenu :
printf '%s\n' "$LAB_DIR"/*
Le lab reste local et ne nécessite aucun privilège particulier.
Parcourir des fichiers avec un glob
Pour traiter les fichiers .log du répertoire :
for fichier in "$LAB_DIR"/*.log
do
printf 'Fichier : %s\n' "$fichier"
done
Le glob *.log est développé par Bash en plusieurs chemins. Chaque chemin devient un élément de la boucle.
Le point important est la suite :
"$fichier"
La variable doit rester citée lors de son utilisation. Ainsi, un chemin comme :
/tmp/.../rapport final.log
reste transmis comme un seul argument.
Dans ce lab, au moins un fichier .log existe. Dans un script générique, il faut aussi réfléchir au cas où un glob ne correspond à aucun fichier, car Bash peut alors laisser le motif non développé selon les options actives du shell.
Pourquoi éviter for f in $(find ...)
Cette construction est tentante :
for fichier in $(find . -type f)
do
printf '%s\n' "$fichier"
done
Elle est fragile.
La substitution de commande $(...) produit du texte. Lorsqu’elle est utilisée sans guillemets dans cette position, le résultat est soumis au word splitting. Un nom comme :
./rapport final.log
peut être découpé en deux éléments :
./rapport
final.log
La boucle ne parcourt alors plus les vrais noms de fichiers.
Pour un répertoire simple comme notre lab, un glob est plus direct :
for fichier in "$LAB_DIR"/*.log
do
printf '%s\n' "$fichier"
done
La règle à retenir n’est pas « ne jamais utiliser find », mais ne pas transformer aveuglément sa sortie textuelle en liste de mots avec $(find ...).
Utiliser continue pour ignorer une itération
continue arrête l’itération courante et passe à la suivante.
Exemple :
for fichier in "$LAB_DIR"/*
do
if [[ $fichier != *.log ]]
then
continue
fi
printf 'Log retenu : %s\n' "$fichier"
done
Le fichier notes.txt est ignoré.
Les fichiers .log sont affichés.
continue est utile lorsque la logique la plus lisible consiste à éliminer rapidement les cas qui ne nous intéressent pas.
Utiliser break pour sortir d’une boucle
break termine la boucle en cours.
Exemple :
for fichier in "$LAB_DIR"/*
do
printf 'Analyse : %s\n' "$fichier"
if [[ $fichier == *'rapport final.log' ]]
then
printf 'Fichier cible trouvé.\n'
break
fi
done
Dès que la condition correspond, la boucle s’arrête.
Les éléments qui auraient été parcourus ensuite ne le sont plus.
Cette mécanique est utile lorsqu’une seule correspondance suffit et qu’il n’est pas nécessaire de poursuivre le traitement.
La boucle while répète tant qu’une condition réussit
La forme générale est :
while condition
do
commandes
done
Comme avec if, la condition est déterminée par le statut de sortie de la commande évaluée.
Exemple avec un compteur :
compteur=1
while [[ $compteur -le 3 ]]
do
printf 'Tour %s\n' "$compteur"
((compteur++))
done
Résultat attendu :
Tour 1
Tour 2
Tour 3
Lorsque compteur vaut 4, la condition [[ $compteur -le 3 ]] devient fausse et la boucle se termine.
Ici, ((compteur++)) est utilisé uniquement pour faire progresser le compteur. Les expressions arithmétiques Bash seront étudiées plus en détail dans un article ultérieur.
Une boucle while peut dépendre directement d’une commande
Une condition de boucle n’a pas besoin d’être une expression [[ ... ]].
Par exemple, lisons un fichier ligne par ligne :
cat > services.txt <<'EOF'
web
api
worker
EOF
Puis :
while IFS= read -r service
do
printf 'Service : %s\n' "$service"
done < services.txt
À chaque itération, le builtin read lit une ligne.
Tant que read réussit, le bloc est exécuté. À la fin du fichier, read retourne un statut non nul et la boucle se termine.
Le préfixe :
IFS= read -r service
est une forme courante pour lire une ligne sans découpage selon les espaces de IFS et sans interpréter les antislashs comme échappements.
while et état de la variable : attention aux pipelines
Cette forme peut sembler naturelle :
printf '%s\n' un deux trois | while IFS= read -r valeur
do
compteur=$((compteur + 1))
done
Mais selon le contexte Bash, le corps d’une boucle placée dans un pipeline peut s’exécuter dans un sous-shell. Une variable modifiée dans cette boucle ne doit donc pas être supposée disponible ensuite dans le shell parent.
Lorsque vous avez besoin de conserver l’état modifié après la boucle, une redirection est plus prévisible :
compteur=0
while IFS= read -r valeur
do
compteur=$((compteur + 1))
done < services.txt
printf 'Nombre : %s\n' "$compteur"
Avec le fichier créé plus haut, le résultat attendu est :
Nombre : 3
Combiner while, continue et break
Créons un fichier de valeurs :
cat > valeurs.txt <<'EOF'
alpha
ignorer
beta
stop
gamma
EOF
Puis :
while IFS= read -r valeur
do
if [[ $valeur == 'ignorer' ]]
then
continue
fi
if [[ $valeur == 'stop' ]]
then
break
fi
printf 'Valeur traitée : %s\n' "$valeur"
done < valeurs.txt
Résultat attendu :
Valeur traitée : alpha
Valeur traitée : beta
La ligne ignorer ne produit aucune sortie, puis stop termine la boucle avant gamma.
Cet exemple montre clairement la différence :
continue: abandonner seulement l’itération courante ;break: abandonner la boucle entière.
Bonnes pratiques
Citez les variables utilisées dans les commandes du corps de boucle :
printf '%s\n' "$fichier"
Pour parcourir des fichiers d’un répertoire connu, préférez un glob correctement construit à une substitution de commande qui transforme une sortie textuelle en liste :
for fichier in "$repertoire"/*.log
Évitez :
for fichier in $(find ...)
lorsque les noms de fichiers peuvent contenir des espaces ou d’autres caractères interprétés lors du découpage.
Avec while read, utilisez une forme explicite lorsque vous voulez conserver les lignes telles quelles :
while IFS= read -r ligne
do
...
done < fichier
Assurez-vous aussi qu’une boucle while possède une condition qui finira par devenir fausse, sauf si une boucle volontairement infinie est réellement le comportement recherché.
Sécurité
Une boucle amplifie l’effet d’une commande : une erreur dans le corps peut être répétée sur de nombreux éléments.
Avant de remplacer les printf de démonstration par une commande de modification ou de suppression, vérifiez :
- la valeur réellement parcourue ;
- le quoting de chaque variable ;
- le périmètre du glob ou de l’entrée ;
- le comportement en cas de fichier inattendu ;
- la méthode d’arrêt de la boucle.
Une commande destructive placée dans une boucle peut multiplier rapidement les conséquences d’une erreur de chemin ou de sélection.
Les exemples de cet article ne modifient que le répertoire temporaire du lab.
Coûts
Le lab utilise uniquement Bash et quelques petits fichiers locaux. Il n’utilise aucun service cloud ni ressource facturable.
Limites et points de vigilance
Cet article ne couvre pas encore :
- les tableaux Bash ;
- la boucle arithmétique
for (( ... )); until;select;- les boucles imbriquées et
break N/continue N; - la lecture robuste de noms de fichiers arbitraires contenant notamment des retours à la ligne ;
- la parallélisation de traitements ;
- l’écriture structurée d’un script complet avec arguments et fonctions.
Le prochain article utilisera ces acquis pour assembler un script Bash propre.
Rollback ou nettoyage
Avant de supprimer le lab, vérifiez sa valeur :
printf '%s\n' "$LAB_DIR"
Quittez le répertoire :
cd /
Puis supprimez uniquement le répertoire temporaire créé pour l’exercice :
rm -rf -- "$LAB_DIR"
Cette commande est destructive pour le contenu du lab. Vérifiez donc LAB_DIR avant de l’exécuter.
Références officielles
- Bash Reference Manual — Looping Constructs — GNU Project
- Bash Reference Manual — Bourne Shell Builtins — GNU Project
- Bash Reference Manual — Word Splitting — GNU Project
- Bash Reference Manual — Filename Expansion — GNU Project
- Bash Reference Manual — Pipelines — GNU Project
Conclusion
Les boucles Bash reposent sur quelques idées simples :
forparcourt une liste ;whilerépète tant qu’une condition réussit ;continuesaute au tour suivant ;breakquitte la boucle.
La difficulté réelle vient surtout de la manière dont le shell construit les mots et les arguments. En citant correctement les variables et en évitant de convertir naïvement une sortie textuelle comme $(find ...) en liste de fichiers, les boucles deviennent beaucoup plus prévisibles.
