Aller au contenu
Administration Linux medium

Boucles Bash : for, while et until par l'exemple

34 min de lecture

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.

  • Écrire une boucle for sur une liste, un tableau, un glob de fichiers et une plage numérique
  • Écrire une boucle for façon C avec for (( i=0; i<10; i++ ))
  • Utiliser while IFS= read -r pour lire un fichier ou un CSV ligne par ligne
  • Attendre un service avec until
  • Contrôler le déroulement avec break, continue et break 2
  • Reconnaître les pièges qui cassent une boucle, et savoir quand ne pas écrire de boucle du tout

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 .log d'un répertoire
  • lire un inventaire CSV et n'agir que sur les machines de production
  • boucler sur la sortie d'une commande comme systemctl ou find
  • 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.

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 ».

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.

Fenêtre de terminal
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"
fi
done

Sortie réelle :

127.0.0.1 : joignable
10.255.255.1 : INJOIGNABLE

Notez 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.

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.

Fenêtre de terminal
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))
fi
done
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 : joignable
Bilan : 1/3 serveur(s) injoignable(s)

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.

Fenêtre de terminal
shopt -s nullglob
compresses=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.gz
3 fichier(s) compressé(s)

garde.txt n'a pas été touché : le motif *.log a filtré ce qu'il fallait, sans if supplémentaire.

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.

Fenêtre de terminal
for i in {1..5}; do
printf "%d " "$i"
done
echo
# 1 2 3 4 5
for i in {0..20..5}; do # pas de 5
printf "%d " "$i"
done
echo
# 0 5 10 15 20

Attention, 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.

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.

Fenêtre de terminal
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"
fi
done

Sortie 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 2s
Tentative 2/4 sur 10.255.255.1...
échec, nouvelle tentative dans 4s
Tentative 3/4 sur 10.255.255.1...
échec, nouvelle tentative dans 6s
Tentative 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 :

Fenêtre de terminal
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/sdc1

La 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.

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.

Fenêtre de terminal
while IFS= read -r ligne; do
printf "[%s]\n" "$ligne"
done < extrait-sshd.conf

Décomposition :

  • IFS= vide le séparateur de champs le temps du read, ce qui préserve les espaces et tabulations en début et fin de ligne
  • read -r empêche l'interprétation des backslashes (\t reste \t, il ne devient pas une tabulation)
  • < fichier redirige 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]

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.

Fenêtre de terminal
{
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.csv

Avec un parc.csv contenant quatre machines dont une de recette :

web01 10.0.0.11 frontal
web02 10.0.0.12 frontal
db01 10.0.0.21 base

La 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.

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.

Fenêtre de terminal
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 active

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.

Fenêtre de terminal
compteur=1
while (( compteur <= 3 )); do
echo "Itération $compteur"
compteur=$((compteur + 1))
done
Itération 1
Itération 2
Itération 3

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.

Fenêtre de terminal
essais=0
until nc -z 127.0.0.1 8099 2>/dev/null; do
essais=$((essais + 1))
echo " service pas encore prêt, tentative $essais..."
sleep 1
done
echo "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 :

Fenêtre de terminal
essais=0
until 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 2
done

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).

Fenêtre de terminal
lues=0
while 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 lecture
Lignes utiles lues avant arrêt : 3

La 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.

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.

Fenêtre de terminal
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é"
done
done

Avec 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 scan

La 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.

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.

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 :

Fenêtre de terminal
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 :

Fenêtre de terminal
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.

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 :

Fenêtre de terminal
for f in *.log; do
echo "Traitement de : $f"
wc -l < "$f"
done
Traitement de : *.log
bash: *.log: No such file or directory

La 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.

Fenêtre de terminal
shopt -s nullglob
n=0
for f in *.log; do
echo "traitement de : $f"
n=$((n + 1))
done
echo "itérations : $n"
itérations : 0

C'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 :

Fenêtre de terminal
erreurs=0
grep -E " 5[0-9]{2}$" acces.log | while IFS= read -r ligne; do
erreurs=$((erreurs + 1))
echo " [$erreurs] $ligne"
done
echo "après la boucle : erreurs=$erreurs"
[1] 10.0.0.4 - POST /login 500
[2] 10.0.0.2 - GET /api/v1/users 500
après la boucle : erreurs=0

Le 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 :

Fenêtre de terminal
erreurs=0
while 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 500
après la boucle : erreurs=2

Le 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.

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é :

Fenêtre de terminal
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 :

Fenêtre de terminal
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.

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 :

Fenêtre de terminal
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] -> INTROUVABLE

Le 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.

Fenêtre de terminal
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
[./incident
2026.log] -> existe

Deux fichiers, deux itérations, et les deux existent. Le nom multiligne est correctement transmis d'un seul tenant.

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 :

Fenêtre de terminal
n=5
for 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 :

Fenêtre de terminal
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.

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.

BesoinBoucle recommandée
Liste connue (serveurs, arguments, valeurs)for item in "${liste[@]}"
Fichiers d'un répertoirefor f in *.log avec shopt -s nullglob
Nombre d'itérations connufor i in {1..10}
Index, incrément ou calcul dans la conditionfor (( i=0; i<10; i++ ))
Lire un fichier ligne par lignewhile IFS= read -r ligne; do ... done < fichier
Lire la sortie d'une commandewhile IFS= read -r ligne; do ... done < <(commande)
Parcourir une arborescence, noms hostileswhile IFS= read -r -d '' f avec find -print0
Attendre un service, un port, un fichieruntil condition; do sleep 2; done
Condition dépendant d'un compteur modifiéwhile (( compteur < max ))

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.

Fenêtre de terminal
# boucle inutile : un processus chmod par fichier
for f in *.conf; do chmod 644 "$f"; done
# équivalent, un seul processus
chmod 644 ./*.conf

Le 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 :

Fenêtre de terminal
# séquentiel : 1012 ms
for f in *.log; do gzip -k "$f"; done
# 4 compressions en parallèle : 323 ms
printf '%s\0' ./*.log | xargs -0 -P 4 -n 1 gzip -k

Trois 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.

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ômeCause probableSolution
Plus d'itérations que de fichiers, noms tronquésfor f in $(ls) : word splitting sur les espaces (SC2045)Utiliser le glob : for f in *
Un fichier est traité deux foisLa sortie de ls contenait un motif re-développé par le globbingUtiliser le glob : for f in *
La boucle tourne une fois avec *.log en valeurAucun fichier ne correspond, Bash passe la chaîne littéraleshopt -s nullglob, ou [[ -e "$f" ]] || continue
Compteur ou variable à 0 après le whilecommande | while : la boucle tourne dans un sous-shell (SC2030/SC2031)while ... done < <(commande)
Indentation et espaces de fin disparusread sans IFS= rogne les bordures de lignewhile IFS= read -r ligne
Backslashes mangés, deux lignes fusionnéesread sans -r interprète les échappements (SC2162)while IFS= read -r ligne
Chemins « introuvables » alors qu'ils existentUn nom de fichier contient un saut de lignefind -print0 + while IFS= read -r -d '' f
{1..$n} donne une seule itération littéraleLes accolades se développent avant les variablesfor (( i=1; i<=n; i++ )) ou seq "$n"
break ne sort pas de la boucle externebreak ne quitte que le niveau le plus internebreak 2 (ou continue 2)

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

6 questions
6 min.
70% requis

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

  • for pour une liste connue, while pour un flux de taille inconnue, until pour attendre une condition extérieure.
  • for f in * et jamais for 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 < fichier est la forme canonique : IFS= préserve les espaces de bordure, -r préserve les backslashes.
  • done < <(commande) et jamais commande | while read : le pipe crée un sous-shell et vos variables modifiées sont perdues.
  • find -print0 avec while 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 2 et continue 2 agissent sur la boucle externe des boucles imbriquées.
  • Pas de boucle quand la commande accepte déjà plusieurs arguments, ou quand xargs -P peut paralléliser.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn