Une boucle Bash répète une action sur chaque élément d'un ensemble. Avec for, vous itérez sur une liste connue à l'avance : des serveurs à pinguer, des fichiers .log à compresser, les valeurs d'un tableau. Avec while, vous traitez un flux dont vous ignorez la taille : les lignes d'un CSV, la sortie d'une commande. Avec until, vous attendez qu'une condition devienne vraie, typiquement un service qui démarre.
Cette page va droit aux exemples d'administration réellement exécutés, puis aux pièges qui font échouer les scripts en production : for f in $(ls), le glob qui ne correspond à rien, le compteur avalé par un sous-shell.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Écrire une boucle
forsur une liste, un tableau, un glob de fichiers et une plage numérique - Écrire une boucle
forfaçon C avecfor (( i=0; i<10; i++ )) - Utiliser
while IFS= read -rpour lire un fichier ou un CSV ligne par ligne - Attendre un service avec
until - Contrôler le déroulement avec
break,continueetbreak 2 - Reconnaître les pièges qui cassent une boucle, et savoir quand ne pas écrire de boucle du tout
Dans quel contexte ?
Section intitulée « Dans quel contexte ? »Une boucle devient utile dès qu'une action doit se répéter sur plusieurs éléments. En administration Linux, les cas reviennent toujours aux mêmes familles :
- vérifier la joignabilité d'une liste de serveurs
- compresser ou faire tourner les fichiers
.logd'un répertoire - lire un inventaire CSV et n'agir que sur les machines de production
- boucler sur la sortie d'une commande comme
systemctloufind - attendre qu'un port s'ouvre avant de poursuivre un déploiement
- réessayer une opération réseau plusieurs fois avant d'abandonner
En préparation RHCSA et LFCS, l'itération sur une liste (for) et la lecture de fichier (while read) tombent régulièrement dans les exercices de scripting.
La boucle for : itérer sur une liste connue
Section intitulée « La boucle for : itérer sur une liste connue »for est la boucle la plus utilisée en administration. Le principe est simple : vous lui donnez une liste d'éléments, elle exécute le corps de la boucle une fois par élément, en plaçant l'élément courant dans une variable. La syntaxe se lit presque comme une phrase : « pour chaque hote dans cette liste, faire ceci ».
Une liste littérale
Section intitulée « Une liste littérale »Le cas le plus direct : vous écrivez la liste à la main. Ici, un contrôle de joignabilité sur deux adresses, avec ping -c1 -W1 (un seul paquet, une seconde d'attente maximum) pour ne pas bloquer le script.
for hote in 127.0.0.1 10.255.255.1; do if ping -c1 -W1 "$hote" >/dev/null 2>&1; then echo "$hote : joignable" else echo "$hote : INJOIGNABLE" fidoneSortie réelle :
127.0.0.1 : joignable10.255.255.1 : INJOIGNABLENotez les guillemets doubles autour de "$hote". Ils sont systématiques dès qu'une variable est passée à une commande : sans eux, une valeur contenant un espace serait découpée en plusieurs arguments.
Un tableau, avec un compteur
Section intitulée « Un tableau, avec un compteur »Dès que la liste dépasse trois éléments, un tableau est plus lisible et plus facile à modifier. "${serveurs[@]}" développe le tableau en autant d'éléments qu'il contient, et ${#serveurs[@]} donne le nombre d'éléments. On en profite pour tenir un compteur et afficher un bilan.
serveurs=("127.0.0.1" "10.255.255.1" "127.0.0.2")injoignables=0
for hote in "${serveurs[@]}"; do if ping -c1 -W1 "$hote" >/dev/null 2>&1; then echo " $hote : joignable" else echo " $hote : INJOIGNABLE" injoignables=$((injoignables + 1)) fidone
echo "Bilan : $injoignables/${#serveurs[@]} serveur(s) injoignable(s)"Sortie réelle :
127.0.0.1 : joignable 10.255.255.1 : INJOIGNABLE 127.0.0.2 : joignableBilan : 1/3 serveur(s) injoignable(s)Les fichiers d'un répertoire, avec un glob
Section intitulée « Les fichiers d'un répertoire, avec un glob »Pour itérer sur des fichiers, la bonne forme est le glob (*.log), pas une commande. Bash développe le motif en une liste de noms de fichiers, sans découper sur les espaces, ce qui règle d'emblée la plupart des problèmes de noms exotiques.
L'exemple compresse tous les .log d'un répertoire et laisse les autres fichiers tranquilles. Le shopt -s nullglob est expliqué plus bas : il évite le cas désagréable où aucun fichier ne correspond.
shopt -s nullglobcompresses=0
for f in *.log; do gzip "$f" echo " compressé : $f -> $f.gz" compresses=$((compresses + 1))done
echo "$compresses fichier(s) compressé(s)"Dans un répertoire contenant access.log, error.log, debug.log et garde.txt :
compressé : access.log -> access.log.gz compressé : debug.log -> debug.log.gz compressé : error.log -> error.log.gz3 fichier(s) compressé(s)garde.txt n'a pas été touché : le motif *.log a filtré ce qu'il fallait, sans if supplémentaire.
Une plage numérique
Section intitulée « Une plage numérique »Bash sait développer une plage avec les accolades. C'est la forme la plus lisible pour un nombre d'itérations connu, et elle accepte un pas.
for i in {1..5}; do printf "%d " "$i"doneecho# 1 2 3 4 5
for i in {0..20..5}; do # pas de 5 printf "%d " "$i"doneecho# 0 5 10 15 20Attention, cette syntaxe a une limite importante : elle ne fonctionne pas avec une variable. {1..$n} ne se développe pas, le piège est détaillé dans la section dédiée.
La boucle for façon C
Section intitulée « La boucle for façon C »Quand vous avez besoin d'un index, d'un incrément personnalisé ou d'un calcul dans la condition, la forme arithmétique for (( init; condition; incrément )) est plus adaptée. Elle se lit comme en C ou en Java : initialiser, tester, incrémenter.
Voici une boucle de nouvelle tentative avec attente croissante, un grand classique des scripts de déploiement qui doivent tolérer un service lent à répondre.
max=4
for (( essai=1; essai<=max; essai++ )); do attente=$(( essai * 2 )) echo "Tentative $essai/$max sur 10.255.255.1..."
if ping -c1 -W1 10.255.255.1 >/dev/null 2>&1; then echo " serveur joignable" break fi
if (( essai < max )); then echo " échec, nouvelle tentative dans ${attente}s" sleep "$attente" else echo " échec définitif après $max tentatives" fidoneSortie réelle (le serveur ne répond jamais, les quatre tentatives sont consommées) :
Tentative 1/4 sur 10.255.255.1... échec, nouvelle tentative dans 2sTentative 2/4 sur 10.255.255.1... échec, nouvelle tentative dans 4sTentative 3/4 sur 10.255.255.1... échec, nouvelle tentative dans 6sTentative 4/4 sur 10.255.255.1... échec définitif après 4 tentativesÀ l'intérieur des doubles parenthèses, on écrit les variables sans $ (essai<=max et non $essai<=$max) et l'arithmétique est native. C'est aussi la forme à utiliser pour parcourir un tableau par index, quand vous avez besoin du numéro de l'élément et pas seulement de sa valeur :
disques=("/dev/sda1" "/dev/sdb1" "/dev/sdc1")
for (( i=0; i<${#disques[@]}; i++ )); do printf " disque %d/%d : %s\n" "$((i+1))" "${#disques[@]}" "${disques[i]}"done disque 1/3 : /dev/sda1 disque 2/3 : /dev/sdb1 disque 3/3 : /dev/sdc1La boucle while : traiter un flux de taille inconnue
Section intitulée « La boucle while : traiter un flux de taille inconnue »while répète tant qu'une condition reste vraie. En administration, sa condition la plus fréquente n'est pas un compteur mais une lecture réussie : « tant que j'arrive à lire une ligne, traite-la ». C'est l'outil du texte, des fichiers et des sorties de commande.
Lire un fichier ligne par ligne
Section intitulée « Lire un fichier ligne par ligne »Voici la forme canonique, à mémoriser telle quelle. Chacun de ses trois éléments répare un problème précis, démontré plus bas dans les pièges.
while IFS= read -r ligne; do printf "[%s]\n" "$ligne"done < extrait-sshd.confDécomposition :
IFS=vide le séparateur de champs le temps duread, ce qui préserve les espaces et tabulations en début et fin de ligneread -rempêche l'interprétation des backslashes (\treste\t, il ne devient pas une tabulation)< fichierredirige le fichier vers l'entrée de la boucle, sans créer de sous-shell
Sur un extrait de configuration indenté, la sortie montre que l'indentation d'origine est intacte :
[ ListenAddress 10.0.0.5 ][ PermitRootLogin no]Lire un CSV champ par champ
Section intitulée « Lire un CSV champ par champ »read sait aussi découper une ligne en champs. Il suffit de lui indiquer le séparateur via IFS et de lui donner plusieurs variables. Ici, un inventaire de serveurs dont on ne garde que la production.
Le premier read -r _entete consomme la ligne d'en-tête avant la boucle. Les accolades regroupent les deux lectures pour qu'elles partagent la même redirection.
{ read -r _entete while IFS=, read -r nom ip role env; do [ "$env" = "prod" ] || continue printf "%-8s %-12s %s\n" "$nom" "$ip" "$role" done} < parc.csvAvec un parc.csv contenant quatre machines dont une de recette :
web01 10.0.0.11 frontalweb02 10.0.0.12 frontaldb01 10.0.0.21 baseLa machine test01 a bien été écartée par le continue. Notez la différence avec la forme précédente : ici on veut découper, donc IFS=,. Quand on veut la ligne brute, on met IFS= vide.
Boucler sur la sortie d'une commande
Section intitulée « Boucler sur la sortie d'une commande »Pour itérer sur ce que produit une commande, la bonne construction est done < <(commande), appelée substitution de processus. Elle branche la sortie de la commande sur l'entrée de la boucle sans passer par un pipe, ce qui évite le piège du sous-shell détaillé plus bas.
L'exemple parcourt les services actifs et interroge leur état. Ici on veut découper le nom de l'unité du reste de la ligne, donc on laisse IFS par défaut et on range le surplus dans _reste.
while read -r unite _reste; do etat=$(systemctl is-active "$unite") printf " %-24s %s\n" "$unite" "$etat"done < <(systemctl list-units --type=service --state=running --no-legend --no-pager)Sortie réelle (extrait) :
atd.service active caddy.service active containerd.service active cron.service activeUn simple compteur
Section intitulée « Un simple compteur »while accepte aussi une condition arithmétique classique, avec les doubles parenthèses. C'est la forme à choisir quand le nombre d'itérations dépend d'une variable modifiée dans la boucle.
compteur=1while (( compteur <= 3 )); do echo "Itération $compteur" compteur=$((compteur + 1))doneItération 1Itération 2Itération 3until : attendre qu'une condition devienne vraie
Section intitulée « until : attendre qu'une condition devienne vraie »until est l'exact inverse de while : la boucle tourne tant que la condition est fausse, et s'arrête dès qu'elle devient vraie. Son usage emblématique en administration est l'attente d'un service : on répète le test jusqu'à ce que le port réponde.
L'exemple attend qu'un port TCP s'ouvre. Le test nc -z tente une connexion sans envoyer de données ; tant qu'il échoue, on patiente.
essais=0until nc -z 127.0.0.1 8099 2>/dev/null; do essais=$((essais + 1)) echo " service pas encore prêt, tentative $essais..." sleep 1doneecho "Port 8099 ouvert après $essais tentative(s)."En lançant un service qui met trois secondes à démarrer, la sortie réelle est :
service pas encore prêt, tentative 1... service pas encore prêt, tentative 2... service pas encore prêt, tentative 3...Port 8099 ouvert après 3 tentative(s).Telle quelle, cette boucle attend indéfiniment si le service ne démarre jamais. En production, ajoutez systématiquement un garde-fou qui abandonne au bout de N tentatives et sort en erreur, sinon votre déploiement se bloque en silence :
essais=0until nc -z 127.0.0.1 5432 2>/dev/null; do essais=$((essais + 1)) if (( essais >= 30 )); then echo "PostgreSQL toujours indisponible après 30 tentatives" >&2 exit 1 fi sleep 2donebreak et continue : maîtriser le déroulement
Section intitulée « break et continue : maîtriser le déroulement »Ces deux mots-clés modifient le cours d'une boucle. continue abandonne l'itération courante et passe à la suivante, il sert à filtrer. break quitte la boucle immédiatement, il sert à s'arrêter dès qu'on a trouvé ou dès qu'une condition rend la suite inutile.
L'exemple lit un fichier de configuration : il ignore les lignes vides et les commentaires (continue), et stoppe tout s'il rencontre une erreur bloquante (break).
lues=0while IFS= read -r ligne; do [[ -z "$ligne" ]] && continue # ligne vide [[ "$ligne" == \#* ]] && continue # commentaire
if [[ "$ligne" == FATAL:* ]]; then echo " erreur bloquante : ${ligne#FATAL: }" echo " arrêt de la lecture" break fi
echo " paramètre retenu : $ligne" lues=$((lues + 1))done < sauvegarde.conf
echo "Lignes utiles lues avant arrêt : $lues"Sortie réelle :
paramètre retenu : source=/var/www paramètre retenu : destination=/mnt/backup paramètre retenu : retention=30 erreur bloquante : quota disque dépassé arrêt de la lectureLignes utiles lues avant arrêt : 3La preuve que break a bien fonctionné : le fichier contenait une ligne retention=60 après la ligne FATAL:, et elle n'a jamais été lue.
Sortir de deux boucles avec break 2
Section intitulée « Sortir de deux boucles avec break 2 »Par défaut, break ne quitte que la boucle la plus interne. Dans des boucles imbriquées, cela ne suffit pas : vous sortez de la boucle des ports, mais celle des hôtes continue. break 2 quitte deux niveaux d'un coup.
Le scénario : chercher un service inattendu en écoute sur une liste d'hôtes et de ports, et tout arrêter à la première trouvaille.
for hote in 127.0.0.1 127.0.0.2; do for port in 9101 9102 9103; do if nc -z -w1 "$hote" "$port" 2>/dev/null; then echo " $hote:$port OUVERT, arrêt complet du scan" break 2 fi echo " $hote:$port fermé" donedoneAvec un service réellement en écoute sur le seul port 9103 :
127.0.0.1:9101 fermé 127.0.0.1:9102 fermé 127.0.0.1:9103 OUVERT, arrêt complet du scanLa boucle externe n'a jamais atteint 127.0.0.2 : les deux niveaux ont bien été quittés. continue 2 existe sur le même principe : passer à l'hôte suivant sans tester les ports restants.
Les pièges qui cassent les scripts en production
Section intitulée « Les pièges qui cassent les scripts en production »Cette section est la plus importante de la page. Chacun de ces pièges a été reproduit et sa sortie est réelle. La plupart ne se manifestent pas sur votre poste de test avec des noms de fichiers propres : ils explosent le jour où un fichier contient un espace, où un répertoire est vide, ou où un compteur revient à zéro sans prévenir.
Bon réflexe : shellcheck détecte la majorité de ces erreurs avant l'exécution. Les codes SC cités ci-dessous sont ceux qu'il remonte réellement.
Piège 1 : for f in $(ls)
Section intitulée « Piège 1 : for f in $(ls) »C'est l'erreur la plus répandue. La sortie de ls est un texte, que Bash soumet ensuite au word splitting (découpage sur les espaces) puis au globbing (développement des motifs). Deux dégâts, pas un seul.
Premier dégât, le découpage. Avec trois fichiers dont deux contiennent un espace :
for f in $(ls); do echo "[$f]"; done[access.log][erreurs][2026.log][rapport][annuel.log]Trois fichiers, cinq itérations. Les noms ont été coupés en morceaux, et aucun des morceaux ne correspond à un fichier existant.
Second dégât, plus vicieux : le globbing de la sortie. Avec trois fichiers dont un s'appelle littéralement app-*.log :
[app-a.log][app-b.log][app-a.log][app-b.log][app-*.log]Trois fichiers, cinq itérations, et surtout app-a.log et app-b.log traités deux fois. Le nom app-*.log sorti par ls a été re-développé comme un motif. Sur une boucle qui compresse ou supprime, un traitement en double est un incident.
La parade est le glob, qui ne souffre d'aucun des deux problèmes :
for f in *; do echo "[$f]"; done[app-a.log][app-b.log][app-*.log]Trois fichiers, trois itérations, noms intacts. Shellcheck signale la mauvaise forme en erreur : SC2045 (error): Iterating over ls output is fragile. Use globs.
Piège 2 : le glob qui ne correspond à rien
Section intitulée « Piège 2 : le glob qui ne correspond à rien »Comportement contre-intuitif de Bash : si un motif ne correspond à aucun fichier, il n'est pas remplacé par une liste vide, il est passé tel quel, en chaîne littérale. La boucle tourne donc une fois, sur un fichier qui n'existe pas.
Dans un répertoire sans aucun .log :
for f in *.log; do echo "Traitement de : $f" wc -l < "$f"doneTraitement de : *.logbash: *.log: No such file or directoryLa boucle s'est exécutée une fois, avec $f valant la chaîne *.log, et la commande a échoué. Sur une boucle de suppression ou de déplacement, ce genre de tour peut faire des dégâts sur un fichier réellement nommé ainsi.
La parade propre est shopt -s nullglob : un motif sans correspondance devient une liste vide, et la boucle ne tourne tout simplement pas.
shopt -s nullglobn=0for f in *.log; do echo "traitement de : $f" n=$((n + 1))doneecho "itérations : $n"itérations : 0C'est exactement zéro itération, le comportement attendu. La parade alternative, si vous ne voulez pas modifier les options du shell, est de tester chaque entrée avec [[ -e "$f" ]] || continue en début de boucle.
Piège 3 : commande | while read, le sous-shell qui avale vos variables
Section intitulée « Piège 3 : commande | while read, le sous-shell qui avale vos variables »Un pipe crée un sous-shell. La boucle while placée à droite d'un | s'exécute donc dans un processus fils, avec sa propre copie des variables. Tout ce que vous y modifiez est perdu dès la boucle terminée, sans le moindre message d'erreur.
L'exemple compte les erreurs 5xx dans un journal d'accès :
erreurs=0grep -E " 5[0-9]{2}$" acces.log | while IFS= read -r ligne; do erreurs=$((erreurs + 1)) echo " [$erreurs] $ligne"doneecho "après la boucle : erreurs=$erreurs" [1] 10.0.0.4 - POST /login 500 [2] 10.0.0.2 - GET /api/v1/users 500après la boucle : erreurs=0Le compteur monte bien à 2 dans la boucle, puis retombe à 0 juste après. C'est le symptôme classique, et il est difficile à diagnostiquer parce que rien n'échoue visiblement.
La parade est la substitution de processus, done < <(commande). La boucle reste alors dans le shell courant :
erreurs=0while IFS= read -r ligne; do erreurs=$((erreurs + 1)) echo " [$erreurs] $ligne"done < <(grep -E " 5[0-9]{2}$" acces.log)echo "après la boucle : erreurs=$erreurs" [1] 10.0.0.4 - POST /login 500 [2] 10.0.0.2 - GET /api/v1/users 500après la boucle : erreurs=2Le compteur survit. Shellcheck voit le problème et le nomme précisément : SC2030 (info): Modification of erreurs is local (to subshell caused by pipeline) puis SC2031 (info): erreurs was modified in a subshell. That change might be lost.
Piège 4 : read sans IFS= ni -r
Section intitulée « Piège 4 : read sans IFS= ni -r »Ces deux options ne protègent pas la même chose, et beaucoup de guides les récitent sans le dire. Voici ce que chacune répare exactement.
IFS= protège les espaces de début et de fin de ligne. Sans lui, read supprime les espaces et tabulations en bordure. Sur un extrait de configuration indenté :
while read -r ligne; do printf "[%s]\n" "$ligne"; done < extrait-sshd.conf[ListenAddress 10.0.0.5][PermitRootLogin no]L'indentation et les espaces de fin ont disparu. Avec IFS= :
[ ListenAddress 10.0.0.5 ][ PermitRootLogin no]Tout est préservé. Point important, souvent mal compris : les espaces internes ne sont jamais un problème quand vous lisez dans une seule variable. Celle-ci absorbe la fin de la ligne, espaces compris. Seules les bordures sont rognées.
-r protège les backslashes. Sans lui, read traite le backslash comme un caractère d'échappement. Sur un fichier de motifs contenant ^error\t[0-9]+, un chemin de partage \\srv-fichiers\partage et une ligne terminée par un backslash :
while IFS= read ligne; do printf "[%s]\n" "$ligne"; done < motifs.txt[^errort[0-9]+][\srv-fichierspartage][sauvegarde suite-de-la-ligne]Trois dégâts distincts : le \t a été mangé (il reste t), le double backslash a été réduit à un seul, et surtout la ligne terminée par \ a été fusionnée avec la suivante. Quatre lignes en entrée, trois itérations en sortie. Avec -r :
[^error\t[0-9]+][\\srv-fichiers\partage][sauvegarde \][suite-de-la-ligne]Chaque ligne est intacte et le compte est bon. Shellcheck rappelle la règle : SC2162 (info): read without -r will mangle backslashes. D'où la forme canonique qui combine les deux protections : while IFS= read -r ligne.
Piège 5 : les noms de fichiers hostiles
Section intitulée « Piège 5 : les noms de fichiers hostiles »Ce piège est régulièrement mal expliqué. On lit souvent que find | while read casse « à cause des espaces ». C'est faux : avec IFS= read -r, un nom contenant des espaces passe très bien, puisque la variable absorbe toute la ligne.
Le vrai coupable est le saut de ligne dans un nom de fichier. Il est légal sous Linux, et il détruit la logique « une ligne égale un fichier ». Avec deux fichiers, rapport annuel.log et un fichier dont le nom contient un retour à la ligne :
while IFS= read -r f; do [ -e "$f" ] && etat="existe" || etat="INTROUVABLE" printf " [%s] -> %s\n" "$f" "$etat"done < <(find . -type f) [./rapport annuel.log] -> existe [./incident] -> INTROUVABLE [2026.log] -> INTROUVABLELe fichier avec espaces passe sans problème, ce qui confirme que l'espace n'est pas le sujet. En revanche, le nom contenant un saut de ligne a été coupé en deux itérations fantômes, qui ne correspondent à aucun fichier existant. Deux fichiers, trois itérations.
La parade est de changer de séparateur : find -print0 termine chaque nom par un octet nul, le seul caractère qui ne peut pas apparaître dans un nom de fichier. Côté boucle, read -d '' lit jusqu'à cet octet nul.
while IFS= read -r -d '' f; do [ -e "$f" ] && etat="existe" || etat="INTROUVABLE" printf " [%s] -> %s\n" "$f" "$etat"done < <(find . -type f -print0) [./rapport annuel.log] -> existe [./incident2026.log] -> existeDeux fichiers, deux itérations, et les deux existent. Le nom multiligne est correctement transmis d'un seul tenant.
Piège 6 : {1..$n} ne se développe pas
Section intitulée « Piège 6 : {1..$n} ne se développe pas »Le développement des accolades a lieu avant le remplacement des variables. Écrire {1..$n} ne produit donc pas une plage, mais la chaîne littérale :
n=5for i in {1..$n}; do printf "[%s] " "$i"; done[{1..5}]Une seule itération, sur une chaîne inutilisable. Deux parades, toutes deux vérifiées :
n=5
for i in $(seq "$n"); do printf "[%s] " "$i"; done# [1] [2] [3] [4] [5]
for (( i=1; i<=n; i++ )); do printf "[%s] " "$i"; done# [1] [2] [3] [4] [5]La forme for (( )) est préférable : elle évite un appel à seq et reste du Bash pur. Shellcheck détecte d'ailleurs la mauvaise version tout seul : SC2051 (warning): Bash doesn't support variables in brace range expansions.
Quelle boucle pour quel besoin
Section intitulée « Quelle boucle pour quel besoin »Le choix se ramène presque toujours à une question simple : savez-vous à l'avance combien d'éléments vous allez traiter ? Si oui, for. Si non, while. Si vous attendez un événement extérieur, until.
| Besoin | Boucle recommandée |
|---|---|
| Liste connue (serveurs, arguments, valeurs) | for item in "${liste[@]}" |
| Fichiers d'un répertoire | for f in *.log avec shopt -s nullglob |
| Nombre d'itérations connu | for i in {1..10} |
| Index, incrément ou calcul dans la condition | for (( i=0; i<10; i++ )) |
| Lire un fichier ligne par ligne | while IFS= read -r ligne; do ... done < fichier |
| Lire la sortie d'une commande | while IFS= read -r ligne; do ... done < <(commande) |
| Parcourir une arborescence, noms hostiles | while IFS= read -r -d '' f avec find -print0 |
| Attendre un service, un port, un fichier | until condition; do sleep 2; done |
| Condition dépendant d'un compteur modifié | while (( compteur < max )) |
Quand ne pas écrire de boucle
Section intitulée « Quand ne pas écrire de boucle »Une boucle n'est pas toujours la bonne réponse. Deux situations très courantes méritent d'y renoncer, et le gain est net.
La commande accepte déjà plusieurs arguments. Beaucoup d'outils Unix prennent une liste de fichiers. Écrire une boucle autour d'eux n'apporte rien, c'est juste plus lent (un processus par fichier) et plus verbeux.
# boucle inutile : un processus chmod par fichierfor f in *.conf; do chmod 644 "$f"; done
# équivalent, un seul processuschmod 644 ./*.confLe résultat est strictement identique. Le même raisonnement vaut pour rm, cp, chown, ou pour grep -r qui explore une arborescence sans qu'on ait à boucler dessus.
Le traitement est long et parallélisable. Une boucle for est séquentielle : chaque itération attend la précédente. Sur des tâches indépendantes qui coûtent du temps CPU ou du réseau, xargs -P lance plusieurs traitements en parallèle.
Mesure réelle sur 8 fichiers d'environ 4 Mo, sur une machine à 16 cœurs :
# séquentiel : 1012 msfor f in *.log; do gzip -k "$f"; done
# 4 compressions en parallèle : 323 msprintf '%s\0' ./*.log | xargs -0 -P 4 -n 1 gzip -kTrois fois plus rapide pour une ligne à peine plus longue. Le -0 et le printf '%s\0' conservent au passage la robustesse face aux noms de fichiers exotiques vue au piège 5. Pour des besoins plus riches (barre de progression, exécution distante, reprise sur erreur), GNU parallel va plus loin.
- xargs : construire des commandes à partir d'entrées : la référence complète, dont
-Pet-0. - GNU Parallel : exécuter en parallèle : quand
xargs -Pne suffit plus.
Dépannage
Section intitulée « Dépannage »Les symptômes ci-dessous correspondent aux pièges démontrés plus haut. Le réflexe préalable reste de passer le script à shellcheck, qui identifie seul la plupart de ces cas.
| Symptôme | Cause probable | Solution |
|---|---|---|
| Plus d'itérations que de fichiers, noms tronqués | for f in $(ls) : word splitting sur les espaces (SC2045) | Utiliser le glob : for f in * |
| Un fichier est traité deux fois | La sortie de ls contenait un motif re-développé par le globbing | Utiliser le glob : for f in * |
La boucle tourne une fois avec *.log en valeur | Aucun fichier ne correspond, Bash passe la chaîne littérale | shopt -s nullglob, ou [[ -e "$f" ]] || continue |
Compteur ou variable à 0 après le while | commande | while : la boucle tourne dans un sous-shell (SC2030/SC2031) | while ... done < <(commande) |
| Indentation et espaces de fin disparus | read sans IFS= rogne les bordures de ligne | while IFS= read -r ligne |
| Backslashes mangés, deux lignes fusionnées | read sans -r interprète les échappements (SC2162) | while IFS= read -r ligne |
| Chemins « introuvables » alors qu'ils existent | Un nom de fichier contient un saut de ligne | find -print0 + while IFS= read -r -d '' f |
{1..$n} donne une seule itération littérale | Les accolades se développent avant les variables | for (( i=1; i<=n; i++ )) ou seq "$n" |
break ne sort pas de la boucle externe | break ne quitte que le niveau le plus interne | break 2 (ou continue 2) |
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »forpour une liste connue,whilepour un flux de taille inconnue,untilpour attendre une condition extérieure.for f in *et jamaisfor f in $(ls): le glob ne découpe pas sur les espaces et ne re-développe pas les motifs.shopt -s nullglobévite qu'un motif sans correspondance soit passé en chaîne littérale.while IFS= read -r ligne; do ... done < fichierest la forme canonique :IFS=préserve les espaces de bordure,-rpréserve les backslashes.done < <(commande)et jamaiscommande | while read: le pipe crée un sous-shell et vos variables modifiées sont perdues.find -print0avecwhile IFS= read -r -d ''dès qu'un nom de fichier peut contenir un saut de ligne.for (( i=0; i<10; i++ ))pour un index ou un calcul, et pour contourner{1..$n}qui ne se développe pas.break 2etcontinue 2agissent sur la boucle externe des boucles imbriquées.- Pas de boucle quand la commande accepte déjà plusieurs arguments, ou quand
xargs -Ppeut paralléliser.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Fonctions Bash : factoriser le corps d'une boucle devenue trop longue.
- Déboguer un script Bash : tracer une boucle avec
set -xquand le résultat ne colle pas. - find : rechercher des fichiers : construire la liste de fichiers à parcourir, dont
-print0. - xargs : construire des commandes à partir d'entrées : remplacer une boucle par un traitement parallèle.
- GNU Parallel : exécuter en parallèle : aller plus loin que
xargs -P.