Une boucle while mal bornée peut relancer une commande des milliers de fois, remplir un journal ou monopoliser un cœur CPU. Le problème vient rarement de while lui-même. C’est la condition de sortie qui ne devient jamais fausse, ou une erreur qui n’est pas prise en compte.
Avant de laisser un script Bash tourner sur un serveur, fixez une limite, ralentissez les tentatives et prévoyez une sortie vérifiable. Voici une méthode qui couvre aussi la lecture d’un fichier, break, continue et l’arrêt externe avec timeout.

Comprendre ce que la boucle while teste réellement
Bash exécute le corps de la boucle tant que la dernière commande de la condition renvoie le code de sortie 0. Une condition peut donc être un test arithmétique, une comparaison ou une commande réelle :
attempt=0
max_attempts=5
while (( attempt < max_attempts )); do
printf 'Tentative %s sur %s\n' "$((attempt + 1))" "$max_attempts"
((attempt += 1))
done
Ici, (( attempt < max_attempts )) devient faux après cinq passages. Avec la syntaxe [ "$attempt" -lt "$max_attempts" ], les espaces autour des crochets sont obligatoires. Gardez les variables numériques initialisées et citées dans les tests classiques.
Évitez ((attempt++)) dans un script lancé avec set -e. Cette expression renvoie un statut non nul lorsque l’ancienne valeur vaut zéro et peut interrompre le script. ((attempt += 1)) ne présente pas ce piège dans cet exemple.
Donner une limite aux tentatives
Une boucle de nouvelle tentative doit avoir au moins trois sorties : succès, nombre maximal atteint et interruption manuelle. Cet exemple attend un service HTTP, sans essayer indéfiniment :
#!/usr/bin/env bash
set -u
attempt=0
max_attempts=12
while (( attempt < max_attempts )); do
if curl -fsS --max-time 5 https://service.example.net/health >/dev/null; then
printf 'Service disponible\n'
break
fi
((attempt += 1))
if (( attempt < max_attempts )); then
printf 'Service indisponible, nouvelle tentative dans 5 secondes\n' >&2
sleep 5
fi
done
if (( attempt == max_attempts )); then
printf 'Abandon après %s tentatives\n' "$max_attempts" >&2
exit 1
fi
sleep empêche la boucle de marteler le service et de consommer du CPU entre deux essais. La limite intégrée au script reste préférable à un simple commentaire qui promet un arrêt. Si votre besoin consiste seulement à réafficher périodiquement une commande, utilisez watch plutôt que de recréer une boucle.
Lire un fichier sans perdre la dernière ligne
Pour lire un fichier ligne par ligne, utilisez IFS= afin de conserver les espaces en début et en fin de ligne, puis read -r pour ne pas interpréter les antislashs :
while IFS= read -r line || [[ -n $line ]]; do
printf '%s\n' "$line"
done < fichier.conf
La seconde partie de la condition traite une dernière ligne qui ne se termine pas par un retour à la ligne. Sans elle, cette ligne peut être ignorée. La redirection placée après done évite aussi le pipe inutile.
Si les lignes viennent d’une commande et que vous devez conserver une variable après la boucle, préférez la substitution de processus :
count=0
while IFS= read -r line; do
((count += 1))
done < <(find /var/log -maxdepth 1 -type f -print)
printf 'Fichiers trouvés : %s\n' "$count"
Avec find ... | while ..., Bash peut exécuter la boucle dans un sous-shell. La valeur de count retrouvée après le pipe reste alors celle du shell parent.
Sortir avec break et sauter une ligne avec continue
continue passe directement à l’itération suivante. break quitte la boucle courante. Les deux instructions rendent le comportement explicite quand certaines lignes doivent être ignorées ou arrêter le traitement :
while IFS= read -r line || [[ -n $line ]]; do
[[ -z $line ]] && continue
[[ $line == STOP ]] && break
printf 'Traitement de : %s\n' "$line"
done < file-attente.txt
N’utilisez pas break pour masquer une erreur. Testez le code de retour de la commande, écrivez un message sur la sortie d’erreur et choisissez entre nouvelle tentative, abandon de l’élément ou arrêt complet du script.
Ajouter un arrêt externe avec timeout
Une limite interne peut encore échouer si une commande du corps reste bloquée. timeout encadre alors le script ou la commande avec une durée maximale :
if timeout --signal=TERM --kill-after=5s 2m ./worker.sh; then
printf 'Traitement terminé\n'
else
rc=$?
if (( rc == 124 )); then
printf 'Arrêt après deux minutes\n' >&2
else
printf 'Échec du worker, code %s\n' "$rc" >&2
fi
exit "$rc"
fi
Le signal TERM laisse au processus une chance de fermer ses fichiers. --kill-after=5s envoie ensuite un signal plus ferme s’il refuse de s’arrêter. Le code 124 indique l’expiration du délai dans ce cas standard. Pour une tâche lancée par cron, ajoutez aussi un verrou avec flock : une boucle bornée n’empêche pas deux exécutions de se chevaucher.
Tracer la boucle avant de la laisser tourner
Commencez par contrôler la syntaxe, puis lancez le script avec une trace sur un jeu de données réduit :
bash -n ./script.sh
bash -x ./script.sh 2>trace-bash.log
tail -n 50 trace-bash.log
bash -n détecte les erreurs de syntaxe sans exécuter le script. bash -x montre les commandes après expansion, ce qui aide à repérer une variable qui ne change jamais. La trace peut contenir des secrets transmis en argument ou stockés dans des variables : protégez le fichier, puis supprimez-le après le diagnostic. Le guide sur tail permet de suivre ce journal pendant le test.
Testez enfin Ctrl + C, le délai maximal, le cas de succès et le cas d’échec. Les comportements de while, read, break et continue sont documentés dans le manuel Bash. La page de manuel de timeout détaille les signaux et les codes de retour.