Une boucle est l’un des mécanismes les plus utiles de la programmation.
Elle permet de transformer :
faire quelque chose une fois
en :
faire quelque chose 10 fois
faire quelque chose pour chaque fichier
faire quelque chose tant qu'une condition est vraie
faire quelque chose jusqu'à ce qu'un service réponde
En Bash, les boucles sont particulièrement utiles parce que l’administration système consiste précisément à répéter les mêmes opérations sur :
- des fichiers ;
- des utilisateurs ;
- des serveurs ;
- des services ;
- des lignes de texte ;
- des sauvegardes ;
- des paquets ;
- des machines distantes.
Une boucle bien écrite peut faire gagner plusieurs heures.
Une boucle mal écrite peut effectuer plusieurs milliers de fois une opération que vous n’auriez déjà pas dû effectuer une seule fois.
Le principal avantage d’une boucle est qu’elle répète exactement ce qu’on lui demande. C’est également son principal défaut.
Qu’est-ce qu’une boucle ?
Une boucle est une structure de contrôle permettant d’exécuter plusieurs fois un bloc de commandes.
On peut répéter :
- pour chaque élément d’une liste ;
- tant qu’une condition réussit ;
- jusqu’à ce qu’une condition réussisse ;
- un nombre déterminé de fois ;
- indéfiniment jusqu’à une interruption.
Conceptuellement :
début
↓
tester / choisir un élément
↓
exécuter des commandes
↓
recommencer ?
├── oui → nouvelle itération
└── non → sortie de la boucle
Chaque passage dans la boucle s’appelle une :
itération
Les principales boucles Bash
Bash fournit principalement :
| Construction | Utilisation |
|---|---|
for |
Parcourir des éléments ou répéter selon un compteur |
while |
Répéter tant qu’une commande ou condition réussit |
until |
Répéter jusqu’à ce qu’une commande ou condition réussisse |
select |
Construire une boucle de sélection interactive |
select est moins utilisé, mais il fait bien partie des constructions itératives de Bash.
Comprendre une notion essentielle : Bash teste des statuts
Avant de parler de :
while
until
if
il faut comprendre que Bash raisonne beaucoup en :
codes de retour
Une commande qui réussit retourne généralement :
0
Une commande qui échoue retourne généralement un statut :
non nul
Cela signifie qu’une construction :
while commande; do
...
done
signifie essentiellement :
« Tant que cette commande retourne un statut de succès, continue. »
La boucle for
for est idéale lorsqu’on possède une collection connue d’éléments.
Syntaxe générale :
for variable in element1 element2 element3; do
commandes
done
À chaque itération :
variable
reçoit la valeur de l’élément courant.
For sur une liste fixe
#!/bin/bash
for fruit in pomme banane cerise; do
printf 'Fruit : %s\n' "$fruit"
done
Résultat :
Fruit : pomme
Fruit : banane
Fruit : cerise
Bash exécute trois itérations :
fruit=pomme
fruit=banane
fruit=cerise
Les éléments peuvent contenir des espaces
Il suffit de les citer correctement :
for ville in "New York" "Los Angeles" "Buenos Aires"; do
printf 'Ville : %s\n' "$ville"
done
Chaque chaîne citée reste un élément unique.
Pourquoi « $variable » reste important dans la boucle
Même si :
ville
contient :
New York
ceci :
printf '%s\n' "$ville"
transmet une seule chaîne.
En règle générale, les expansions contenant des données doivent rester citées :
"$fruit"
"$fichier"
"$serveur"
"$repertoire"
For avec une séquence numérique
Bash possède la brace expansion :
for i in {1..5}; do
printf 'Compteur : %d\n' "$i"
done
Elle génère :
1
2
3
4
5
Ordre décroissant
for i in {5..1}; do
printf '%d\n' "$i"
done
Résultat :
5
4
3
2
1
Avec un pas
for i in {0..10..2}; do
printf 'Pair : %d\n' "$i"
done
Résultat :
Pair : 0
Pair : 2
Pair : 4
Pair : 6
Pair : 8
Pair : 10
La brace expansion est textuelle
Une construction :
{1..5}
est développée par Bash très tôt, avant notamment l’expansion des variables.
C’est important.
Ce code paraît logique :
max=5
for i in {1..$max}; do
printf '%s\n' "$i"
done
mais il ne produit pas dynamiquement :
1 2 3 4 5
comme on pourrait l’espérer.
La variable :
$max
n’est pas remplacée avant que Bash décide si l’expression entre accolades forme une séquence valide.
Pour une borne variable, utilisez une boucle arithmétique
max=5
for ((i = 1; i <= max; i++)); do
printf '%d\n' "$i"
done
Cette forme est beaucoup plus adaptée lorsqu’une valeur est calculée à l’exécution.
La boucle for de style C
Bash possède une seconde syntaxe :
for ((initialisation; condition; modification)); do
commandes
done
Exemple :
for ((i = 0; i < 5; i++)); do
printf 'i vaut %d\n' "$i"
done
Décomposition
i = 0
est exécuté une fois au départ.
Puis :
i < 5
est testé avant chaque itération.
Enfin :
i++
est exécuté après chaque passage.
Conceptuellement :
i = 0
↓
i < 5 ?
↓ oui
commandes
↓
i++
↓
recommencer
Compter à l’envers
for ((i = 10; i >= 0; i--)); do
printf '%d\n' "$i"
done
Changer le pas
for ((i = 0; i <= 100; i += 10)); do
printf '%d %%\n' "$i"
done
La boucle arithmétique n’a pas besoin de $ devant les variables
Dans :
(( ... ))
on peut écrire :
i < max
plutôt que :
$i < $max
Les noms y sont interprétés comme variables arithmétiques.
For sur les paramètres du script
Imaginons :
./script.sh alpha beta gamma
Vous pouvez parcourir les arguments avec :
for argument in "$@"; do
printf 'Argument : %s\n' "$argument"
done
Forme raccourcie
En Bash, ceci :
for argument; do
printf '%s\n' "$argument"
done
équivaut à :
for argument in "$@"; do
printf '%s\n' "$argument"
done
C’est élégant, mais la version explicite peut être plus pédagogique lorsqu’on apprend le langage.
Pourquoi « $@ » est important
Si le script reçoit :
./script.sh "rapport annuel.txt" "photo vacances.jpg"
avec :
for fichier in "$@"; do
...
done
il reçoit correctement deux éléments.
Évitez :
for fichier in $@; do
...
done
car les espaces contenus dans les arguments peuvent alors être interprétés comme séparateurs.
For sur les fichiers d’un dossier
Exemple classique :
for fichier in *.txt; do
printf 'Fichier : %s\n' "$fichier"
done
Le glob :
*.txt
est développé par Bash en liste de noms.
Un fichier appelé :
rapport annuel.txt
reste un seul élément produit par l’expansion du glob.
Le problème apparaît plutôt si vous oubliez ensuite les guillemets :
cat "$fichier"
est correct.
Ne faites pas for fichier in $(ls)
Cette construction :
for fichier in $(ls *.txt); do
...
done
est fragile.
La sortie de :
ls
est transformée en texte puis redécoupée par le shell.
Les noms contenant :
- espaces ;
- tabulations ;
- retours à la ligne ;
peuvent donc être détruits logiquement.
Le shell sait déjà faire :
*.txt
directement.
Le piège : aucun fichier ne correspond
Supposons qu’aucun fichier :
*.txt
n’existe.
Par défaut, Bash laisse généralement le motif inchangé.
La boucle peut donc recevoir littéralement :
*.txt
Solution simple : vérifier l’existence
for fichier in *.txt; do
[[ -e "$fichier" ]] || continue
printf 'Fichier : %s\n' "$fichier"
done
Solution Bash : nullglob
On peut demander à Bash qu’un glob sans correspondance produise zéro élément :
shopt -s nullglob
for fichier in *.txt; do
printf 'Fichier : %s\n' "$fichier"
done
S’il n’y a aucun `.txt`, la boucle effectue simplement :
0 itération
failglob
Une autre option Bash est :
shopt -s failglob
Avec elle, un motif sans correspondance provoque une erreur d’expansion.
Selon le script, cela peut être préférable à :
continuer silencieusement
Les fichiers cachés
Le glob :
*
ne sélectionne normalement pas les noms commençant par :
.
comme :
.bashrc
.gitignore
.secret
Pour modifier ce comportement dans Bash :
shopt -s dotglob
Mais activez cette option volontairement.
Un script de suppression qui commence soudainement à inclure tous les fichiers cachés vient de changer de personnalité.
Boucler sur les fichiers d’un répertoire précis
repertoire="/srv/data"
for fichier in "$repertoire"/*.txt; do
[[ -e "$fichier" ]] || continue
printf 'Traitement de : %s\n' "$fichier"
done
On cite :
"$repertoire"
mais pas :
*.txt
car le motif doit rester actif.
Fichiers commençant par un tiret
Un fichier peut s’appeler :
-important.txt
Lorsqu’on le transmet à une commande, utilisez si possible :
--
Par exemple :
rm -- "$fichier"
cp -- "$fichier" "$destination"
cat -- "$fichier"
Le :
--
indique à de nombreuses commandes :
« Ce qui suit n’est plus une option. »
Combiner for et tests de fichiers
for chemin in ./*; do
if [[ -d "$chemin" ]]; then
printf 'Répertoire : %s\n' "$chemin"
elif [[ -f "$chemin" ]]; then
printf 'Fichier : %s\n' "$chemin"
elif [[ -L "$chemin" ]]; then
printf 'Lien : %s\n' "$chemin"
else
printf 'Autre : %s\n' "$chemin"
fi
done
Attention à l’ordre des tests avec les liens symboliques
Selon ce que vous voulez savoir, un lien symbolique pointant vers un fichier peut satisfaire certains tests appliqués à sa cible.
Si vous voulez identifier spécifiquement les liens :
if [[ -L "$chemin" ]]; then
...
elif [[ -d "$chemin" ]]; then
...
elif [[ -f "$chemin" ]]; then
...
fi
peut être plus approprié.
La boucle while
Syntaxe :
while condition; do
commandes
done
La boucle continue tant que :
condition
retourne un statut de succès.
Compteur avec while
compteur=0
while (( compteur < 5 )); do
printf 'Compteur : %d\n' "$compteur"
compteur=$((compteur + 1))
done
Résultat :
Compteur : 0
Compteur : 1
Compteur : 2
Compteur : 3
Compteur : 4
Pourquoi utiliser (( )) ici ?
L’original utilisait :
while [ "$compteur" -lt 5 ]; do
Cette syntaxe fonctionne.
Mais puisque nous écrivons spécifiquement du Bash et manipulons des nombres :
while (( compteur < 5 )); do
exprime directement une condition arithmétique.
La commande [ n’est pas une parenthèse magique
Dans :
[ "$compteur" -lt 5 ]
le :
[
est essentiellement une commande de test.
C’est pourquoi les espaces sont nécessaires :
[ "$x" -eq 1 ]
et non :
["$x"-eq1]
[[ ]] est préférable pour beaucoup de tests Bash
Pour des chaînes et fichiers :
if [[ -f "$fichier" ]]; then
...
fi
Pour l’arithmétique :
if (( compteur < 10 )); then
...
fi
Cela rend souvent le script plus lisible qu’un mélange constant de :
[ ]
-eq
-lt
-gt
Attention à ((compteur++)) avec set -e
Cette subtilité est importante.
Une expression arithmétique Bash :
(( expression ))
retourne :
succès si la valeur calculée est non nulle
échec si elle vaut zéro
Supposons :
compteur=0
((compteur++))
L’expression vaut :
0
avant l’incrément.
Son statut peut donc être non nul.
Dans un script avec :
set -e
cela peut provoquer une sortie inattendue selon le contexte.
Forme simple et explicite
compteur=$((compteur + 1))
évite ce piège lorsque la valeur de retour arithmétique ne vous intéresse pas.
While sur une vraie commande
Une boucle n’a pas besoin d’utiliser :
[[ ... ]]
comme condition.
Elle peut tester directement un programme.
Par exemple :
while ping -c 1 -W 1 192.168.1.1 >/dev/null 2>&1; do
printf 'La cible répond.\n'
sleep 5
done
La boucle continue tant que :
ping
réussit.
Attendre qu’un service réponde
Exemple HTTP :
while ! curl -fsS --max-time 2 \
"http://127.0.0.1:8080/health" \
>/dev/null; do
printf 'Service indisponible, nouvelle tentative...\n'
sleep 2
done
printf 'Service disponible.\n'
Ici :
!
inverse le statut.
La boucle continue donc :
tant que curl échoue
Mais attention aux boucles d’attente infinies
Le script précédent peut attendre :
2 secondes
2 minutes
2 jours
l'effondrement thermique de l'univers
si le service ne revient jamais.
Pour une automatisation sérieuse, ajoutez souvent :
- un nombre maximal de tentatives ;
- un timeout global ;
- un message d’erreur ;
- un code de sortie non nul.
Attendre avec un nombre maximal de tentatives
max=10
tentative=1
while (( tentative <= max )); do
if curl -fsS --max-time 2 \
"http://127.0.0.1:8080/health" \
>/dev/null; then
printf 'Service disponible.\n'
break
fi
printf 'Tentative %d/%d échouée.\n' \
"$tentative" "$max"
tentative=$((tentative + 1))
sleep 2
done
if (( tentative > max )); then
printf 'Le service reste indisponible.\n' >&2
exit 1
fi
La boucle infinie
Forme classique :
while true; do
printf 'Ctrl+C pour arrêter.\n'
sleep 1
done
true retourne toujours :
0
La condition réussit donc éternellement.
Forme compacte Bash
On rencontre également :
while :; do
...
done
Le :
:
est un builtin ne faisant pratiquement rien et retournant un succès.
Le résultat est également une boucle infinie.
Une boucle infinie n’est pas forcément une erreur
Elle est tout à fait légitime pour :
- daemon simple ;
- surveillance ;
- menu interactif ;
- traitement continu ;
- attente d’événements.
Le problème n’est pas :
while true
Le problème est :
while true
+
aucune temporisation
+
aucune sortie
+
une commande coûteuse
Une boucle infinie peut consommer un CPU entier
Comparez :
while true; do
:
done
avec :
while true; do
verifier_service
sleep 5
done
Dans le premier cas, Bash recommence aussi vite qu’il le peut.
Dans le second, il attend cinq secondes entre les vérifications.
Lire un fichier ligne par ligne
La forme classique robuste :
while IFS= read -r ligne; do
printf 'Ligne : %s\n' "$ligne"
done < monfichier.txt
Pourquoi IFS= ?
IFS signifie :
Internal Field Separator
Avec :
IFS=
on demande à :
read
de ne pas découper ou supprimer involontairement certains caractères de séparation au début et à la fin de la ligne.
Pourquoi read -r ?
Sans :
-r
les antislashs peuvent avoir une signification particulière.
Avec :
read -r
on demande de les conserver comme données ordinaires.
Pour lire du texte arbitraire :
IFS= read -r ligne
est donc un excellent réflexe.
Le problème de la dernière ligne sans saut de ligne
Un fichier texte normalement formé termine ses lignes par un caractère :
newline
Mais un fichier peut parfois finir ainsi :
ligne 1\n
ligne 2\n
dernière ligne sans newline
À EOF, :
read
peut retourner un statut d’échec tout en ayant placé les derniers caractères dans la variable.
Si cette dernière ligne doit absolument être traitée :
while IFS= read -r ligne || [[ -n "$ligne" ]]; do
printf 'Ligne : %s\n' "$ligne"
done < monfichier.txt
Lire plusieurs champs
On peut utiliser un séparateur spécifique :
while IFS=: read -r utilisateur motdepasse uid gid gecos home shell; do
printf 'Utilisateur : %s, UID : %s\n' \
"$utilisateur" "$uid"
done < /etc/passwd
Ici :
:
est le séparateur des champs de :
/etc/passwd
Ne faites pas for ligne in $(cat fichier)
Ce code :
for ligne in $(cat fichier.txt); do
...
done
ne parcourt pas correctement les lignes.
Il parcourt les mots produits après découpage par le shell.
Une ligne :
Jean Dupont Paris
devient potentiellement :
Jean
Dupont
Paris
Pour des lignes :
while IFS= read -r ligne
est la construction adaptée.
Le piège des pipelines et des sous-shells
Cette construction semble naturelle :
compteur=0
cat fichier.txt |
while IFS= read -r ligne; do
compteur=$((compteur + 1))
done
printf 'Total : %d\n' "$compteur"
Mais Bash peut exécuter la boucle de la pipeline dans un :
subshell
Les modifications de :
compteur
restent alors dans ce sous-shell.
Le shell principal peut afficher :
Total : 0
ce qui constitue une excellente occasion de douter de l’arithmétique élémentaire.
Préférez une redirection
compteur=0
while IFS= read -r ligne; do
compteur=$((compteur + 1))
done < fichier.txt
printf 'Total : %d\n' "$compteur"
Ici, la boucle s’exécute dans le shell courant.
Avec la sortie d’une commande
En Bash, utilisez une substitution de processus :
compteur=0
while IFS= read -r ligne; do
compteur=$((compteur + 1))
done < <(commande)
printf 'Total : %d\n' "$compteur"
La syntaxe :
<(commande)
est une fonctionnalité Bash appelée :
process substitution
La boucle until
until ressemble à :
while
avec une logique inversée.
Syntaxe :
until condition; do
commandes
done
La boucle continue tant que la condition :
échoue
et s’arrête lorsqu’elle :
réussit
Exemple avec compteur
i=0
until (( i >= 5 )); do
printf 'i = %d\n' "$i"
i=$((i + 1))
done
Résultat :
i = 0
i = 1
i = 2
i = 3
i = 4
While et until peuvent exprimer la même logique
Cette boucle :
while (( i < 5 )); do
...
done
peut être formulée :
until (( i >= 5 )); do
...
done
Les deux sont correctes.
Choisissez la forme qui exprime le plus naturellement l’intention.
Exemple pratique avec until
Attendre qu’un fichier apparaisse :
until [[ -f "/tmp/application.ready" ]]; do
printf 'En attente de l’application...\n'
sleep 2
done
printf 'Application prête.\n'
Attendre qu’un service systemd devienne actif
until systemctl is-active --quiet nginx; do
printf 'nginx n’est pas encore actif.\n'
sleep 2
done
printf 'nginx est actif.\n'
Encore une fois : prévoyez un timeout
Une automatisation ne devrait pas toujours attendre éternellement.
Exemple :
tentative=1
maximum=15
until systemctl is-active --quiet nginx; do
if (( tentative >= maximum )); then
printf 'nginx n’est pas devenu actif.\n' >&2
exit 1
fi
printf 'Tentative %d/%d...\n' \
"$tentative" "$maximum"
tentative=$((tentative + 1))
sleep 2
done
printf 'nginx est actif.\n'
break : quitter une boucle
break arrête immédiatement la boucle courante.
Exemple :
for i in {1..10}; do
if (( i == 5 )); then
printf 'Arrêt à %d.\n' "$i"
break
fi
printf '%d\n' "$i"
done
Résultat :
1
2
3
4
Arrêt à 5.
break est utile lorsqu’on a trouvé ce qu’on cherchait
for fichier in ./*.conf; do
[[ -e "$fichier" ]] || continue
if grep -qF "production=true" "$fichier"; then
printf 'Configuration trouvée : %s\n' "$fichier"
break
fi
done
Une fois le bon fichier trouvé, il n’est pas forcément nécessaire de continuer à examiner les 14 000 suivants.
continue : passer à l’itération suivante
continue n’arrête pas la boucle entière.
Il abandonne seulement l’itération actuelle.
for i in {1..5}; do
if (( i == 3 )); then
printf 'Je saute %d.\n' "$i"
continue
fi
printf 'i = %d\n' "$i"
done
Résultat :
i = 1
i = 2
Je saute 3.
i = 4
i = 5
Continue est très pratique pour filtrer
for fichier in ./*; do
[[ -f "$fichier" ]] || continue
[[ -r "$fichier" ]] || continue
printf 'Traitement : %s\n' "$fichier"
done
La logique devient :
pas un fichier ?
→ suivant
pas lisible ?
→ suivant
sinon
→ traitement
Réduire l’imbrication avec continue
Comparez :
for fichier in ./*; do
if [[ -f "$fichier" ]]; then
if [[ -r "$fichier" ]]; then
if [[ "$fichier" == *.txt ]]; then
traiter "$fichier"
fi
fi
fi
done
avec :
for fichier in ./*; do
[[ -f "$fichier" ]] || continue
[[ -r "$fichier" ]] || continue
[[ "$fichier" == *.txt ]] || continue
traiter "$fichier"
done
La seconde version est souvent beaucoup plus lisible.
Break et continue peuvent recevoir un nombre
Bash permet :
break 2
pour sortir de deux niveaux de boucles imbriquées.
De même :
continue 2
agit sur le deuxième niveau de boucle englobant.
Exemple avec break 2
for serveur in web01 web02 web03; do
for port in 22 80 443; do
printf 'Test %s:%s\n' "$serveur" "$port"
if [[ "$serveur" == "web02" && "$port" == "80" ]]; then
printf 'Condition trouvée, sortie complète.\n'
break 2
fi
done
done
Sans :
2
break ne quitterait que la boucle :
for port
Les boucles imbriquées
Une boucle peut naturellement contenir une autre boucle.
for serveur in web01 web02; do
for port in 22 80 443; do
printf '%s:%s\n' "$serveur" "$port"
done
done
Résultat :
web01:22
web01:80
web01:443
web02:22
web02:80
web02:443
Le nombre d’itérations se multiplie
Si vous avez :
1000 serveurs
×
1000 fichiers
vous obtenez :
1 000 000 itérations
Une boucle imbriquée parfaitement correcte peut donc devenir très lente simplement parce que son coût algorithmique explose.
Ne faites pas une commande externe inutile à chaque tour
Supposons :
for fichier in ...; do
commande_externe
done
Si la boucle contient :
500 000 éléments
vous lancez :
500 000 processus
Le coût peut devenir important.
Dans certains cas :
find -exec ... +;xargscorrectement utilisé ;- un programme traitant plusieurs entrées à la fois ;
awk;- un autre langage ;
seront beaucoup plus efficaces.
Exemple : éviter 10 000 appels à rm
Cette boucle :
for fichier in ./*.tmp; do
[[ -e "$fichier" ]] || continue
rm -- "$fichier"
done
lance potentiellement :
rm
une fois par fichier.
Selon le besoin, cette commande peut suffire :
rm -- ./*.tmp
avec les précautions nécessaires concernant l’absence de correspondance.
Ou :
find . -maxdepth 1 -type f -name '*.tmp' -delete
si le comportement correspond exactement à l’objectif.
Une boucle n’est pas toujours la meilleure solution
Le shell possède déjà de nombreux outils capables de traiter plusieurs éléments :
cp
mv
rm
find
grep
sed
awk
xargs
rsync
Avant d’écrire :
for ...
do
...
done
demandez-vous si la commande peut déjà traiter toute la collection en une fois.
La boucle select
Bash propose également :
select
pour créer rapidement des menus.
Exemple :
PS3="Votre choix : "
select action in "Afficher la date" "Afficher l'utilisateur" "Quitter"; do
case "$REPLY" in
1)
date
;;
2)
whoami
;;
3)
break
;;
*)
printf 'Choix invalide.\n' >&2
;;
esac
done
Ce que fait select
Bash affiche automatiquement :
1) Afficher la date
2) Afficher l'utilisateur
3) Quitter
Votre choix :
La variable :
REPLY
contient la réponse numérique brute.
La variable choisie :
action
contient l’élément correspondant lorsqu’un choix valide est effectué.
Utiliser directement la valeur sélectionnée
PS3="Choisissez : "
select fruit in pomme banane cerise; do
if [[ -n "$fruit" ]]; then
printf 'Vous avez choisi : %s\n' "$fruit"
break
fi
printf 'Choix invalide.\n' >&2
done
Select reste une boucle
Si vous n’utilisez pas :
break
le menu est proposé de nouveau après chaque traitement.
C’est pratique pour les petits outils administratifs interactifs.
Pour une vraie interface complexe, Bash finit néanmoins par devenir un framework graphique particulièrement médiocre.
Boucle interactive avec while
L’exemple original :
while true; do
read -r -p "Entrez un mot (ou stop pour sortir) : " mot
[[ "$mot" == "stop" ]] && break
printf 'Vous avez écrit : %s\n' "$mot"
done
fonctionne correctement.
Version avec case
while true; do
read -r -p "Commande [status/heure/quit] : " commande
case "$commande" in
status)
systemctl --failed
;;
heure)
date
;;
quit|exit|q)
break
;;
*)
printf 'Commande inconnue.\n' >&2
;;
esac
done
Pour plusieurs possibilités, :
case
est souvent plus lisible qu’une longue succession de :
if
elif
elif
elif
Read peut recevoir un timeout
Avec :
read -r -t 10 -p "Réponse : " reponse
Bash attend au maximum environ :
10 secondes
selon le contexte.
Exemple :
if read -r -t 10 -p "Continuer ? [o/N] " reponse; then
printf '\nRéponse reçue : %s\n' "$reponse"
else
printf '\nAucune réponse dans le délai imparti.\n'
fi
Ne mélangez pas interaction et automatisation sans réfléchir
Un script exécuté depuis :
cron
n’a généralement aucun humain devant lui pour répondre :
Continuer ? [o/N]
Une boucle interactive doit donc être utilisée dans un programme réellement prévu pour un terminal.
Tester si une entrée vient d’un terminal
if [[ -t 0 ]]; then
printf 'Mode interactif disponible.\n'
else
printf 'Pas de terminal sur stdin.\n'
fi
Boucles et fonctions
Une fonction peut naturellement contenir une boucle :
afficher_compteur() {
local max="$1"
local i
for ((i = 1; i <= max; i++)); do
printf 'Boucle interne : %d\n' "$i"
done
}
afficher_compteur 3
Une boucle peut appeler une fonction
verifier_serveur() {
local serveur="$1"
if ping -c 1 -W 1 "$serveur" >/dev/null 2>&1; then
printf '[OK] %s\n' "$serveur"
else
printf '[ERREUR] %s\n' "$serveur" >&2
return 1
fi
}
for serveur in router01 dns01 web01; do
verifier_serveur "$serveur"
done
Cette organisation sépare :
parcourir les serveurs
de :
vérifier un serveur
et rend le script beaucoup plus lisible.
Parcourir un tableau
Bash possède des tableaux indexés.
serveurs=(
"web01"
"web02"
"db01"
"backup01"
)
Pour les parcourir :
for serveur in "${serveurs[@]}"; do
printf '%s\n' "$serveur"
done
Pourquoi « ${tableau[@]} » ?
Cette expansion citée conserve chaque élément comme argument distinct.
Si un élément vaut :
serveur de secours
il reste un seul élément de la boucle.
Parcourir les indices d’un tableau
for index in "${!serveurs[@]}"; do
printf 'Index %s : %s\n' \
"$index" "${serveurs[$index]}"
done
Tableaux associatifs
declare -A ports=(
[ssh]=22
[http]=80
[https]=443
)
Parcourir les clés :
for service in "${!ports[@]}"; do
printf '%s utilise le port %s\n' \
"$service" "${ports[$service]}"
done
L’ordre des éléments d’un tableau associatif ne doit généralement pas être considéré comme une interface de tri garantie pour votre logique.
Parcourir la sortie d’une commande : attention
Cette forme :
for utilisateur in $(cut -d: -f1 /etc/passwd); do
...
done
peut fonctionner ici parce que les noms d’utilisateurs Unix respectent normalement des contraintes évitant les espaces.
Mais généraliser :
for x in $(commande)
est dangereux.
La sortie subit :
- substitution de commande ;
- word splitting ;
- filename expansion.
Pour des lignes arbitraires, utilisez while read
while IFS= read -r ligne; do
printf '%s\n' "$ligne"
done < <(commande)
Pour des noms de fichiers arbitraires, utilisez NUL
Les noms de fichiers Unix peuvent contenir des retours à la ligne.
Une liste séparée par :
\n
n’est donc pas totalement sûre pour représenter n’importe quel nom.
Le séparateur robuste est :
NUL
Find avec -print0
while IFS= read -r -d '' fichier; do
printf 'Fichier : %s\n' "$fichier"
done < <(
find /srv/data \
-type f \
-name '*.log' \
-print0
)
Ici :
find -print0
sépare les noms avec un octet NUL.
Et :
read -d ''
lit jusqu’à ce séparateur.
Pourquoi NUL ?
Un nom de fichier Unix peut contenir presque n’importe quel octet.
Mais il ne peut pas contenir :
NUL
et un composant de chemin ne peut évidemment pas contenir :
/
comme caractère ordinaire de son nom.
NUL constitue donc un séparateur fiable pour transmettre une collection de chemins.
Mapfile pour charger des lignes
Si vous voulez stocker les lignes d’un fichier dans un tableau Bash :
mapfile -t lignes < monfichier.txt
Puis :
for ligne in "${lignes[@]}"; do
printf '%s\n' "$ligne"
done
L’option :
-t
retire le délimiteur de fin de ligne.
Mapfile et NUL
Pour des chemins provenant de :
find -print0
on peut également faire :
mapfile -d '' -t fichiers < <(
find /srv/data -type f -print0
)
for fichier in "${fichiers[@]}"; do
printf '%s\n' "$fichier"
done
Attention à la mémoire
mapfile charge toutes les entrées dans un tableau.
Pour :
200 fichiers
aucun problème.
Pour :
20 millions de lignes
une boucle :
while read
traitant les données progressivement peut être beaucoup plus appropriée.
Boucle sur SSH
Exemple :
serveurs=(
"web01.example.net"
"web02.example.net"
"db01.example.net"
)
for serveur in "${serveurs[@]}"; do
printf '=== %s ===\n' "$serveur"
if ssh -- "$serveur" 'uptime'; then
printf '[OK] %s\n' "$serveur"
else
printf '[ERREUR] %s\n' "$serveur" >&2
fi
done
Une erreur sur un serveur doit-elle arrêter toute la boucle ?
C’est une décision de conception.
Pour un audit :
serveur 1 échoue
→ continuer les autres
est souvent souhaitable.
Pour un déploiement transactionnel :
serveur 1 échoue
→ arrêter immédiatement
peut être préférable.
Une boucle robuste doit définir clairement cette politique.
Collecter le nombre d’échecs
erreurs=0
for serveur in "${serveurs[@]}"; do
if ssh -o ConnectTimeout=5 \
-- "$serveur" \
'uptime' >/dev/null; then
printf '[OK] %s\n' "$serveur"
else
printf '[ERREUR] %s\n' "$serveur" >&2
erreurs=$((erreurs + 1))
fi
done
if (( erreurs > 0 )); then
printf '%d serveur(s) en erreur.\n' "$erreurs" >&2
exit 1
fi
Boucle de sauvegarde
sources=(
"/etc"
"/srv/www"
"/srv/scripts"
)
destination="/mnt/backup"
for source in "${sources[@]}"; do
if [[ ! -e "$source" ]]; then
printf 'Source absente : %s\n' "$source" >&2
continue
fi
printf 'Sauvegarde de %s...\n' "$source"
if rsync -a -- "$source" "$destination/"; then
printf '[OK] %s\n' "$source"
else
printf '[ERREUR] %s\n' "$source" >&2
fi
done
Boucle de renommage
Supposons des fichiers :
rapport1.txt
rapport2.txt
rapport3.txt
On peut les préfixer :
for fichier in rapport*.txt; do
[[ -e "$fichier" ]] || continue
nouveau="archive_$fichier"
printf '%s → %s\n' "$fichier" "$nouveau"
mv -- "$fichier" "$nouveau"
done
Avant un renommage massif : prévisualisez
Commencez par :
for fichier in rapport*.txt; do
[[ -e "$fichier" ]] || continue
nouveau="archive_$fichier"
printf '%s → %s\n' "$fichier" "$nouveau"
done
sans :
mv
Une fois le résultat validé, ajoutez l’opération réelle.
La boucle n’est pas un système de rollback
Si :
itération 1 → réussie
itération 2 → réussie
itération 3 → réussie
itération 4 → échoue
Bash ne revient pas automatiquement en arrière sur les trois premières opérations.
Une boucle n’est pas une transaction.
Pour une opération critique, prévoyez :
- validation préalable ;
- sauvegarde ;
- journalisation ;
- mécanisme de reprise ;
- éventuellement opération atomique adaptée.
Boucler sur les PID
Pour certaines opérations d’administration :
for pid in "${pids[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
printf 'PID %s existe.\n' "$pid"
fi
done
kill -0 n’envoie pas réellement un signal destructeur.
Il permet notamment de tester certaines propriétés d’existence ou de permission relatives au processus.
Boucles et sleep
Une boucle de polling doit souvent contenir :
sleep
Exemple :
while true; do
verifier_etat
sleep 30
done
Sans temporisation :
while true; do
verifier_etat
done
peut lancer la vérification plusieurs milliers de fois par seconde.
Le polling n’est pas toujours idéal
Si le système possède un mécanisme événementiel :
- inotify ;
- systemd ;
- webhook ;
- queue ;
- signal ;
- socket ;
il peut être plus efficace d’attendre un événement que de vérifier :
ça a changé ?
ça a changé ?
ça a changé ?
ça a changé ?
toutes les 100 millisecondes.
Une boucle avec timeout global
Une technique simple utilise :
SECONDS
variable spéciale de Bash.
SECONDS=0
timeout=30
while ! systemctl is-active --quiet nginx; do
if (( SECONDS >= timeout )); then
printf 'Timeout après %d secondes.\n' "$timeout" >&2
exit 1
fi
sleep 1
done
printf 'nginx est actif après %d seconde(s).\n' "$SECONDS"
SECONDS est pratique pour des délais simples
Bash maintient automatiquement cette variable en fonction du temps écoulé.
Pour des besoins temporels plus sophistiqués, utilisez un mécanisme adapté.
Timeout d’une commande unique
Sous GNU/Linux, la commande :
timeout
peut limiter la durée d’un programme :
timeout 10s commande
Mais :
timeout
et :
while
répondent à des besoins différents.
Contrôler plusieurs conditions
tentative=0
maximum=20
while (( tentative < maximum )); do
if [[ -f /tmp/service.ready ]] &&
systemctl is-active --quiet application; then
printf 'Application prête.\n'
break
fi
tentative=$((tentative + 1))
sleep 1
done
Les opérateurs logiques dans [[ ]]
On peut écrire :
if [[ -f "$fichier" && -r "$fichier" ]]; then
...
fi
ou :
if [[ "$reponse" == "o" || "$reponse" == "oui" ]]; then
...
fi
Les commandes peuvent aussi être combinées directement
if commande1 && commande2; then
...
fi
Ici, :
commande2
n’est exécutée que si :
commande1
réussit.
Boucles et set -e
Beaucoup de scripts commencent par :
set -e
ou :
set -Eeuo pipefail
Les interactions entre :
errexit
conditions
while
until
&&
||
!
sont subtiles.
Une commande utilisée comme test de :
while
peut légitimement retourner :
non zéro
sans que cela soit considéré comme une erreur fatale à cet endroit.
Exemple naturel
while grep -q "EN_ATTENTE" fichier; do
sleep 1
done
Lorsque :
grep
ne trouve plus le motif, son statut non nul signifie précisément :
« La boucle doit s’arrêter. »
Ce n’est pas une catastrophe nécessitant l’arrêt anticipé d’errexit.
Ne transformez pas tous les statuts non nuls en anomalies
Selon la commande :
non zéro
peut signifier :
- erreur ;
- aucune correspondance ;
- condition fausse ;
- différence détectée ;
- timeout ;
- autre état documenté.
La boucle doit interpréter le statut selon la commande réellement utilisée.
Boucles et pipefail
Supposons :
generer |
filtrer |
while IFS= read -r ligne; do
traiter "$ligne"
done
Les pipelines ajoutent plusieurs questions :
- quelle commande a échoué ?
- quel est le statut global ?
- la boucle est-elle dans un sous-shell ?
Avec :
set -o pipefail
les échecs intermédiaires d’une pipeline deviennent plus visibles dans son statut global.
Mais cela ne résout pas automatiquement le problème de portée des variables dans les sous-shells.
Préférez souvent cette forme
while IFS= read -r ligne; do
traiter "$ligne"
done < <(generer | filtrer)
pour garder la boucle dans le shell courant lorsque c’est important.
Boucler sur une liste générée dynamiquement
Si la liste peut être stockée proprement dans un tableau :
serveurs=(
"web01"
"web02"
"db01"
)
for serveur in "${serveurs[@]}"; do
...
done
C’est généralement plus sûr qu’une chaîne contenant :
"web01 web02 db01"
que l’on espère ensuite voir découpée exactement comme prévu.
Les tableaux représentent mieux les listes
Une variable scalaire :
liste="un deux trois"
est :
une chaîne
pas :
un tableau de trois éléments
La distinction devient importante dès qu’un élément contient un espace.
Mauvais exemple
fichiers="rapport.txt rapport annuel.txt"
for fichier in $fichiers; do
printf '%s\n' "$fichier"
done
Le résultat est découpé en :
rapport.txt
rapport
annuel.txt
Bon exemple
fichiers=(
"rapport.txt"
"rapport annuel.txt"
)
for fichier in "${fichiers[@]}"; do
printf '%s\n' "$fichier"
done
Boucles et chaînes vides
Un tableau peut contenir un élément vide :
valeurs=(
"alpha"
""
"gamma"
)
Avec :
for valeur in "${valeurs[@]}"; do
printf '<%s>\n' "$valeur"
done
vous obtenez bien trois éléments :
<alpha>
<>
<gamma>
Le quoting préserve l’élément vide.
Une boucle peut-elle modifier sa propre liste ?
Dans :
for element in "${tableau[@]}"; do
...
done
la liste sur laquelle le :
for
itère est issue de l’expansion effectuée au début de la construction correspondante.
Modifier le tableau dans le corps ne constitue donc pas une méthode simple pour injecter de nouvelles itérations dans le parcours déjà établi.
Si votre ensemble de travail doit évoluer dynamiquement, :
while
avec une file explicite peut être plus approprié.
Traiter une file avec while
queue=(
"tache1"
"tache2"
"tache3"
)
while (( ${#queue[@]} > 0 )); do
tache="${queue[0]}"
queue=("${queue[@]:1}")
printf 'Traitement : %s\n' "$tache"
done
Pour des volumes importants, cette manipulation répétée de tableau n’est cependant pas nécessairement la structure la plus performante.
Une boucle peut être parallélisée
La forme :
for fichier in "${fichiers[@]}"; do
traiter "$fichier"
done
est séquentielle :
fichier 1
puis
fichier 2
puis
fichier 3
On pourrait être tenté de faire :
for fichier in "${fichiers[@]}"; do
traiter "$fichier" &
done
wait
Chaque traitement part alors en arrière-plan.
Mais attention au parallélisme sans limite
Pour :
10 fichiers
cela peut être acceptable.
Pour :
200 000 fichiers
vous venez potentiellement d’essayer de lancer :
200 000 processus
simultanément.
Le noyau appréciera l’ambition.
Pas nécessairement la méthode.
GNU parallel ou xargs -P
Pour un parallélisme contrôlé, des outils spécialisés peuvent être plus appropriés.
Exemple simple avec des données adaptées :
printf '%s\0' "${fichiers[@]}" |
xargs -0 -n 1 -P 4 commande
Ici :
-P 4
limite le parallélisme à quatre processus.
Le choix dépend cependant fortement de la commande exécutée et de la manière dont ses arguments doivent être transmis.
Ne parallélisez pas simplement parce que vous pouvez
Une opération peut être limitée par :
- le disque ;
- le réseau ;
- une API ;
- un serveur distant ;
- des verrous ;
- une base de données.
Passer de :
1 tâche
à :
100 tâches concurrentes
peut améliorer les performances.
Ou transformer le service distant en projet d’analyse post-mortem.
Piège : modification d’une variable dans un sous-shell
Autre exemple :
compteur=0
(
compteur=10
)
printf '%d\n' "$compteur"
Le résultat reste :
0
Le sous-shell possède sa propre copie de l’environnement shell.
Cette notion explique de nombreux comportements apparemment mystérieux dans les boucles placées dans des pipelines.
Boucle et cd
Ce code :
for dir in */; do
cd "$dir" || continue
faire_quelque_chose
done
est problématique.
Après la première itération, le répertoire courant a changé.
Les chemins des itérations suivantes ne correspondent peut-être plus à l’endroit initial.
Utiliser un sous-shell
for dir in */; do
(
cd -- "$dir" || exit
faire_quelque_chose
)
done
Le :
cd
n’affecte que le sous-shell de cette itération.
La boucle principale reste dans son répertoire original.
Ou éviter cd
Encore mieux lorsque possible :
for dir in */; do
faire_quelque_chose "$dir"
done
Les chemins explicites réduisent souvent les surprises.
Boucle de nettoyage avec find
Imaginons que nous voulions supprimer des :
*.tmp
de plus de 30 jours.
Avant toute suppression :
find /srv/cache \
-type f \
-name '*.tmp' \
-mtime +30 \
-print
Lorsque le résultat est validé :
find /srv/cache \
-type f \
-name '*.tmp' \
-mtime +30 \
-delete
Une boucle Bash n’est donc pas nécessaire dans ce cas.
Si une logique complexe est nécessaire
while IFS= read -r -d '' fichier; do
taille="$(stat -c '%s' "$fichier")"
if (( taille > 100000000 )); then
printf 'Gros fichier ancien : %s\n' "$fichier"
fi
done < <(
find /srv/cache \
-type f \
-name '*.tmp' \
-mtime +30 \
-print0
)
La boucle devient intéressante lorsque chaque élément exige une vraie logique.
Boucles et erreurs destructives
Cette boucle :
for fichier in "$destination"/*; do
rm -rf -- "$fichier"
done
peut parfaitement supprimer tout le contenu de :
$destination
Si :
destination
vaut le mauvais chemin, la boucle ne remettra pas votre jugement en question.
Valider avant une boucle destructive
destination="/srv/app/cache"
if [[ "$destination" != "/srv/app/cache" ]]; then
printf 'Destination refusée.\n' >&2
exit 1
fi
if [[ ! -d "$destination" ]]; then
printf 'Destination absente.\n' >&2
exit 1
fi
Le niveau de validation réel doit naturellement correspondre aux risques du script.
Ajoutez un mode dry-run
dry_run=true
for fichier in "$destination"/*.tmp; do
[[ -e "$fichier" ]] || continue
if "$dry_run"; then
printf '[DRY-RUN] suppression : %s\n' "$fichier"
else
rm -- "$fichier"
fi
done
Une boucle destructive avec :
--dry-run
est souvent beaucoup plus agréable à relire en production.
Boucle avec confirmation
for fichier in ./*.tmp; do
[[ -e "$fichier" ]] || continue
read -r -p "Supprimer '$fichier' ? [o/N] " reponse
case "$reponse" in
o|O|oui|OUI)
rm -- "$fichier"
;;
*)
printf 'Conservé : %s\n' "$fichier"
;;
esac
done
Pour mille fichiers, cette interface deviendra toutefois une expérience méditative assez longue.
Compter les succès et échecs
succes=0
erreurs=0
for fichier in "${fichiers[@]}"; do
if traiter "$fichier"; then
succes=$((succes + 1))
else
erreurs=$((erreurs + 1))
fi
done
printf 'Succès : %d\n' "$succes"
printf 'Erreurs : %d\n' "$erreurs"
if (( erreurs > 0 )); then
exit 1
fi
Cette structure est excellente pour les traitements par lot.
Conserver les éléments en erreur
erreurs=()
for fichier in "${fichiers[@]}"; do
if ! traiter "$fichier"; then
erreurs+=("$fichier")
fi
done
if (( ${#erreurs[@]} > 0 )); then
printf 'Éléments en erreur :\n' >&2
printf ' - %s\n' "${erreurs[@]}" >&2
exit 1
fi
Une boucle peut produire un statut global utile
Pour une tâche automatisée :
0
devrait généralement signifier :
tout ce qui devait réussir a réussi
et un statut non nul :
quelque chose nécessite une attention
Le scheduler ou système de supervision peut alors détecter l’échec.
Boucle de retry avec délai croissant
Pour certains services réseau, on peut augmenter progressivement l’attente :
delai=1
maximum=5
for ((tentative = 1; tentative <= maximum; tentative++)); do
if curl -fsS \
--max-time 5 \
"https://example.com/health" \
>/dev/null; then
printf 'Succès à la tentative %d.\n' "$tentative"
exit 0
fi
printf 'Tentative %d/%d échouée.\n' \
"$tentative" "$maximum"
if (( tentative < maximum )); then
sleep "$delai"
delai=$((delai * 2))
fi
done
printf 'Échec après %d tentatives.\n' "$maximum" >&2
exit 1
Backoff exponentiel
Les délais deviennent ici :
1
2
4
8
...
Ce principe est courant pour éviter de bombarder un service indisponible avec des requêtes en boucle serrée.
Ajoutez parfois du jitter
Si cent machines effectuent exactement :
1 s
2 s
4 s
8 s
elles risquent de retenter toutes en même temps.
Les systèmes distribués ajoutent souvent une petite composante aléatoire :
jitter
afin d’éviter cette synchronisation.
Pour un petit script local, ce niveau de sophistication n’est évidemment pas toujours nécessaire.
Boucle de traitement CSV : attention
On voit parfois :
while IFS=, read -r nom email ville; do
...
done < fichier.csv
Cela fonctionne seulement pour un format extrêmement simple.
Un vrai CSV peut contenir :
"Dupont, Jean",jean@example.com,"Paris, France"
Le parsing CSV comporte :
- guillemets ;
- virgules échappées ;
- retours à la ligne dans les champs selon format ;
- autres règles.
Pour du CSV réel :
utilisez un parseur CSV.
Toutes les données séparées par une virgule ne deviennent pas magiquement simples parce que Bash possède :
IFS=,
Erreur classique : oublier do
Incorrect :
for i in {1..5}
printf '%s\n' "$i"
done
Correct :
for i in {1..5}; do
printf '%s\n' "$i"
done
Erreur classique : oublier done
Une boucle commencée avec :
for
while
until
se termine par :
done
et non :
fi
fi termine :
if
esac termine :
case
Bash n’a pas voulu rendre les mots-clés uniformes.
Il avait manifestement d’autres priorités.
Erreur classique : for i in 1..5
Ceci :
for i in 1..5; do
...
done
contient un seul élément :
1..5
Il ne représente pas une séquence.
Utilisez :
{1..5}
ou :
for ((i = 1; i <= 5; i++))
Erreur classique : utiliser = pour comparer des nombres
Avec :
[ ... ]
une comparaison numérique utilise notamment :
-eq
-ne
-lt
-le
-gt
-ge
Exemple :
if [ "$i" -eq 3 ]; then
...
fi
Mais en Bash, pour l’arithmétique, préférez souvent :
if (( i == 3 )); then
...
fi
Erreur classique : oublier de citer une variable
Fragile :
for fichier in ...; do
cp $fichier /backup
done
Préférable :
for fichier in ...; do
cp -- "$fichier" /backup/
done
Erreur classique : parser ls
Évitez :
for fichier in $(ls)
Utilisez :
for fichier in ./*
ou :
find ... -print0
selon les besoins.
Erreur classique : le glob littéral
Ce code :
for fichier in *.log; do
printf '%s\n' "$fichier"
done
peut afficher :
*.log
si aucun fichier ne correspond.
Utilisez :
[[ -e "$fichier" ]] || continue
ou :
shopt -s nullglob
Erreur classique : oublier les fichiers cachés
Le :
*
ne signifie pas automatiquement :
absolument tout
Les fichiers commençant par un point sont normalement exclus.
Erreur classique : boucle infinie accidentelle
Exemple :
compteur=0
while (( compteur < 10 )); do
printf '%d\n' "$compteur"
done
Il manque :
compteur=$((compteur + 1))
La condition :
compteur < 10
restera vraie éternellement.
Erreur classique : modifier la mauvaise variable
i=0
while (( i < 10 )); do
printf '%d\n' "$i"
j=$((j + 1))
done
i ne change jamais.
La boucle non plus.
Erreur classique : pas de sleep dans un polling
while ! condition; do
:
done
peut saturer inutilement un cœur CPU.
Préférez souvent :
while ! condition; do
sleep 1
done
Erreur classique : aucune limite de retry
Ce code :
until curl ...; do
sleep 5
done
peut être parfaitement correct pour un daemon.
Pour un déploiement automatisé, il peut également bloquer le pipeline pendant trois jours.
Définissez un timeout lorsque l’attente infinie n’est pas explicitement souhaitée.
Erreur classique : variables perdues après une pipeline
Fragile :
compteur=0
cat fichier |
while read -r ligne; do
compteur=$((compteur + 1))
done
printf '%d\n' "$compteur"
Préférez :
compteur=0
while IFS= read -r ligne; do
compteur=$((compteur + 1))
done < fichier
printf '%d\n' "$compteur"
Erreur classique : traiter des fichiers avec une liste textuelle
Les chemins doivent être manipulés comme des éléments distincts, pas comme une longue chaîne que Bash devra redécouper.
Préférez :
- globs ;
- tableaux ;
find -print0;read -d ''.
Erreur classique : lancer tout en arrière-plan
for fichier in ...; do
traitement "$fichier" &
done
peut créer des milliers de processus.
Contrôlez le parallélisme.
Erreur classique : utiliser une boucle quand une commande suffit
Avant :
for fichier in *.txt; do
grep "ERREUR" "$fichier"
done
demandez-vous si :
grep "ERREUR" -- ./*.txt
répond déjà au besoin.
Chaque boucle supplémentaire est également une logique supplémentaire à maintenir.
Testez la syntaxe sans exécuter
bash -n script.sh
Cette commande peut détecter :
doneoublié ;domanquant ;- guillemet non fermé ;
- parenthèse mal formée ;
- autres erreurs syntaxiques.
Utilisez ShellCheck
Sur Debian :
sudo apt install shellcheck
Puis :
shellcheck script.sh
ShellCheck peut notamment signaler de nombreux problèmes liés à :
- quoting ;
- boucles sur la sortie de commandes ;
- tableaux ;
- variables ;
- portabilité ;
- subshells ;
- globbing.
Exemple complet : analyser plusieurs fichiers
#!/bin/bash
set -u
shopt -s nullglob
fichiers=(./*.log)
if (( ${#fichiers[@]} == 0 )); then
printf 'Aucun fichier .log trouvé.\n'
exit 0
fi
erreurs=0
for fichier in "${fichiers[@]}"; do
printf 'Analyse : %s\n' "$fichier"
if grep -qF "ERROR" "$fichier"; then
printf '[ALERTE] ERROR détecté dans %s\n' \
"$fichier" >&2
erreurs=$((erreurs + 1))
else
printf '[OK] %s\n' "$fichier"
fi
done
printf 'Fichiers analysés : %d\n' "${#fichiers[@]}"
printf 'Fichiers en alerte : %d\n' "$erreurs"
if (( erreurs > 0 )); then
exit 1
fi
Exemple complet : attendre un service proprement
#!/bin/bash
set -u
readonly URL="http://127.0.0.1:8080/health"
readonly MAX_ATTEMPTS=20
readonly DELAY=3
for ((tentative = 1; tentative <= MAX_ATTEMPTS; tentative++)); do
printf 'Test %d/%d...\n' \
"$tentative" "$MAX_ATTEMPTS"
if curl -fsS \
--max-time 2 \
"$URL" \
>/dev/null; then
printf 'Service disponible.\n'
exit 0
fi
if (( tentative < MAX_ATTEMPTS )); then
sleep "$DELAY"
fi
done
printf 'Service indisponible après %d tentatives.\n' \
"$MAX_ATTEMPTS" >&2
exit 1
Exemple complet : lire un inventaire de serveurs
Fichier :
web01.example.net
web02.example.net
db01.example.net
Script :
#!/bin/bash
set -u
inventaire="serveurs.txt"
erreurs=0
if [[ ! -r "$inventaire" ]]; then
printf 'Inventaire illisible : %s\n' "$inventaire" >&2
exit 1
fi
while IFS= read -r serveur || [[ -n "$serveur" ]]; do
[[ -n "$serveur" ]] || continue
[[ "$serveur" == \#* ]] && continue
printf 'Test : %s\n' "$serveur"
if ping -c 1 -W 1 "$serveur" >/dev/null 2>&1; then
printf '[OK] %s\n' "$serveur"
else
printf '[ERREUR] %s\n' "$serveur" >&2
erreurs=$((erreurs + 1))
fi
done < "$inventaire"
if (( erreurs > 0 )); then
printf '%d cible(s) sans réponse ICMP.\n' "$erreurs" >&2
exit 1
fi
Ping ne signifie pas « service disponible »
Le script précédent teste :
ICMP
Il ne prouve pas que :
SSH
HTTP
HTTPS
SMB
base de données
fonctionnent.
Une vraie supervision doit tester le service correspondant.
Exemple complet : menu Bash
#!/bin/bash
PS3="Votre choix : "
select action in \
"Afficher la date" \
"Afficher l’espace disque" \
"Afficher les services en échec" \
"Quitter"
do
case "$REPLY" in
1)
date
;;
2)
df -h
;;
3)
systemctl --failed
;;
4)
printf 'Fin.\n'
break
;;
*)
printf 'Choix invalide.\n' >&2
;;
esac
done
Exemple complet : traitement sécurisé de fichiers
#!/bin/bash
set -u
readonly REPERTOIRE="/srv/import"
if [[ ! -d "$REPERTOIRE" ]]; then
printf 'Répertoire absent : %s\n' "$REPERTOIRE" >&2
exit 1
fi
traites=0
erreurs=0
while IFS= read -r -d '' fichier; do
printf 'Traitement de : %s\n' "$fichier"
if traiter_fichier "$fichier"; then
traites=$((traites + 1))
else
printf 'Échec : %s\n' "$fichier" >&2
erreurs=$((erreurs + 1))
fi
done < <(
find "$REPERTOIRE" \
-type f \
-name '*.csv' \
-print0
)
printf 'Traités : %d\n' "$traites"
printf 'Erreurs : %d\n' "$erreurs"
(( erreurs == 0 ))
La dernière expression peut devenir le statut du script
Dans :
(( erreurs == 0 ))
Bash retourne :
0
si :
erreurs == 0
et non zéro sinon.
Pour davantage de lisibilité dans un script destiné à des débutants, on peut néanmoins écrire :
if (( erreurs > 0 )); then
exit 1
fi
exit 0
Comment choisir entre for, while et until ?
| Situation | Boucle généralement adaptée |
|---|---|
| Parcourir une liste | for |
| Parcourir un tableau | for |
| Compter de 1 à N | for (( ... )) |
| Lire un fichier ligne par ligne | while read |
| Continuer tant qu’un état est vrai | while |
| Attendre qu’un état devienne vrai | until |
| Créer un petit menu interactif | select |
| Boucle permanente | while true ou équivalent |
For ou while pour un compteur ?
Les deux fonctionnent.
For
for ((i = 0; i < 10; i++)); do
printf '%d\n' "$i"
done
While
i=0
while (( i < 10 )); do
printf '%d\n' "$i"
i=$((i + 1))
done
Pour un simple compteur déterministe, :
for (( ... ))
est souvent plus compact.
Pour une condition évoluant de manière plus complexe, :
while
peut mieux exprimer l’intention.
While ou until ?
Comparez :
while [[ ! -f "$fichier" ]]; do
sleep 1
done
et :
until [[ -f "$fichier" ]]; do
sleep 1
done
La seconde formulation peut être plus naturelle :
« Attends jusqu’à ce que le fichier existe. »
La lisibilité compte davantage que l’élégance apparente
Cette construction :
while ! condition_complexe_inversee; do
...
done
peut être techniquement correcte.
Mais si :
until condition_positive; do
...
done
exprime plus clairement l’objectif, utilisez-la.
L’administrateur qui relira le script à 3 h 17 appréciera.
Checklist avant d’écrire une boucle
- Qu’est-ce qui doit être parcouru ?
- Quel est exactement un élément ?
- Les éléments peuvent-ils contenir des espaces ?
- S’agit-il de noms de fichiers arbitraires ?
- La liste peut-elle être vide ?
- La boucle doit-elle continuer après une erreur ?
- Existe-t-il une condition de sortie garantie ?
- Faut-il un timeout ?
- Faut-il une temporisation ?
- La commande exécutée possède-t-elle déjà un mode de traitement multiple ?
Checklist pour une boucle sur des fichiers
- éviter de parser
ls; - utiliser les globs directement lorsqu’ils suffisent ;
- citer les expansions de chemin ;
- prévoir le cas où aucun glob ne correspond ;
- penser aux fichiers cachés ;
- penser aux noms commençant par
-; - utiliser NUL pour des collections de noms totalement arbitraires ;
- prévisualiser avant une opération destructive.
Checklist pour while read
- utiliser
IFS=; - utiliser
read -r; - citer
"$ligne"; - gérer la dernière ligne sans newline si nécessaire ;
- éviter une pipeline si les variables de la boucle doivent survivre ;
- ne pas confondre ligne de texte et nom de fichier arbitraire.
Checklist pour une boucle d’attente
- la condition teste-t-elle réellement le service recherché ?
- un
sleepest-il présent ? - combien de temps maximum doit-on attendre ?
- combien de tentatives ?
- quel statut retourner après échec ?
- faut-il journaliser les tentatives ?
- un backoff serait-il préférable ?
Checklist pour une boucle destructive
- Afficher les cibles avant de modifier.
- Vérifier les variables de chemin.
- Vérifier le répertoire ou filesystem réellement concerné.
- Utiliser
--lorsque la commande le permet. - Prévoir un dry-run.
- Vérifier les sauvegardes.
- Tester sur un environnement fictif.
- Vérifier l’utilisateur effectif.
Les constructions essentielles à retenir
| Construction | Rôle |
|---|---|
for x in ... |
Parcourir une liste |
for (( ... )) |
Boucle arithmétique |
for x in "$@" |
Parcourir les arguments du script |
while condition |
Continuer tant que la condition réussit |
until condition |
Continuer jusqu’à ce que la condition réussisse |
while IFS= read -r ligne |
Lire un flux ligne par ligne |
break |
Quitter la boucle |
continue |
Passer à l’itération suivante |
break 2 |
Quitter plusieurs niveaux imbriqués |
select |
Créer un menu interactif |
shopt -s nullglob |
Faire disparaître un glob sans correspondance |
find ... -print0 |
Produire des noms de fichiers séparés par NUL |
Les anti-patterns à retenir
| À éviter | Préférer |
|---|---|
for f in $(ls) |
for f in ./* |
for ligne in $(cat fichier) |
while IFS= read -r ligne |
for f in $liste |
Tableau + "${tableau[@]}" |
{1..$max} |
for (( i=1; i<=max; i++ )) |
Polling sans sleep |
Temporisation ou mécanisme événementiel |
| Retry éternel involontaire | Nombre maximal / timeout |
| Pipeline + compteur à conserver | Redirection ou substitution de processus |
| Suppression immédiatement | Prévisualisation / dry-run puis suppression |
Méthode recommandée pour construire une boucle
Commencez toujours par identifier les données.
Quels éléments vais-je parcourir ?
Puis écrivez une boucle qui ne fait qu’afficher :
for element in ...; do
printf '%s\n' "$element"
done
Vérifiez :
- le nombre d’éléments ;
- leur valeur ;
- les espaces ;
- les cas vides ;
- les éléments inattendus.
Ensuite seulement, remplacez :
printf
par :
la véritable opération
Exemple : méthode prudente
Objectif :
supprimer les .tmp de /srv/cache
Première version :
for fichier in /srv/cache/*.tmp; do
[[ -e "$fichier" ]] || continue
printf 'Cible : %s\n' "$fichier"
done
Deuxième étape :
for fichier in /srv/cache/*.tmp; do
[[ -e "$fichier" ]] || continue
printf '[DRY-RUN] rm -- %q\n' "$fichier"
done
Puis seulement après validation :
for fichier in /srv/cache/*.tmp; do
[[ -e "$fichier" ]] || continue
rm -- "$fichier"
done
Pourquoi %q est utile en Bash
Le format Bash :
printf '%q\n' "$variable"
produit une représentation permettant de mieux visualiser certains caractères spéciaux.
Par exemple, un fichier :
rapport annuel.txt
peut être présenté sous une forme révélant clairement l’espace.
C’est utile lors du debug de chemins inhabituels.
Une boucle doit être observable
Pour un script automatique, vous devez idéalement pouvoir savoir :
- combien d’éléments ont été trouvés ;
- combien ont été traités ;
- combien ont échoué ;
- quels éléments ont échoué ;
- combien de temps l’opération a pris.
Un :
for ...
qui tourne silencieusement pendant 45 minutes n’est pas nécessairement cassé.
Mais il réussit admirablement à donner cette impression.
Une boucle doit avoir une politique d’erreur
Pour chaque commande susceptible d’échouer, choisissez volontairement :
échec
├── arrêter tout
├── ignorer
├── continuer mais compter l'erreur
├── retenter
└── restaurer / nettoyer
Ne laissez pas la réponse dépendre accidentellement de :
set -e
ou du statut d’une commande intermédiaire que personne n’avait pensé à vérifier.
Une boucle doit avoir une condition de fin compréhensible
Avant de lancer :
while
until
posez-vous :
« Quel événement rendra cette condition différente ? »
Si la réponse est :
« Normalement quelque chose devrait finir par changer. »
ajoutez probablement :
timeout
limite
journalisation
Conclusion : répéter intelligemment
Les boucles Bash sont simples à apprendre :
for
while
until
done
Mais leur utilisation robuste demande de comprendre :
- les expansions du shell ;
- les guillemets ;
- les tableaux ;
- les globs ;
- les statuts de commandes ;
- les sous-shells ;
- les entrées ligne par ligne ;
- les conditions de sortie.
Retenez surtout :
forest idéal pour parcourir une liste connue ;for (( ... ))est préférable pour une boucle numérique dynamique ;- la brace expansion
{1..5}est textuelle et ne doit pas être confondue avec une vraie boucle numérique ; whilecontinue tant que sa condition retourne un succès ;untilcontinue tant que sa condition échoue ;breakquitte une boucle etcontinuesaute une itération ;break netcontinue npeuvent agir sur plusieurs niveaux imbriqués ;- un glob sans correspondance peut rester littéral si
nullglobn’est pas activé ; - les globs ne sélectionnent normalement pas les fichiers cachés ;
- il ne faut pas parser
lspour construire une boucle de fichiers ; - pour lire un fichier ligne par ligne, utilisez
while IFS= read -r; - pour des noms de fichiers totalement arbitraires, préférez un séparateur NUL ;
- une boucle placée dans une pipeline peut s’exécuter dans un sous-shell ;
- une boucle d’attente doit généralement avoir un
sleepet souvent un timeout ; - une boucle destructive mérite une prévisualisation ou un mode dry-run ;
- et une boucle n’est pas nécessaire lorsqu’une commande sait déjà traiter efficacement toute la collection.
Une boucle Bash correcte ressemble finalement à ceci :
données bien définies
↓
itération sûre
↓
variables correctement citées
↓
erreurs contrôlées
↓
condition de sortie claire
↓
résultat vérifiable
Une boucle incorrecte ressemble plutôt à :
while true; do
rm -rf "$quelque_chose"
done
Dans les deux cas, Bash fera exactement ce qui est écrit.
La seule différence est que dans le second scénario, il le fera avec une remarquable constance jusqu’à ce qu’un humain, un signal ou l’absence définitive de fichiers mette fin à l’expérience.
