Tutoriel Linux

Boucle while en Bash : éviter qu’un script tourne sans fin

Débutant5 min de lecture
À retenirLinux n'est pas réservé aux experts. Le bon point de départ : une distribution accessible, une sauvegarde propre et quelques commandes comprises.

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.

Tux surveille une boucle Bash qui chauffe et trouve une sortie sûre
Une boucle Bash doit avoir une condition de sortie et une limite vérifiable.

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.

sudo apt update && sudo apt upgrade