Développement Web

Boucles et traitement de fichiers en Bash : for, while, read

13 min de lecture
📍 Article principal de la série : Bash scripting : le guide complet pour automatiser sous Linux. Ce tutoriel fait partie de la série « Bash scripting ». Il prolonge Variables, conditions et tests.

Renommer cinq fichiers à la main, c’est supportable. En renommer trois cents — les photos d’un catalogue produit envoyées pêle-mêle par un client, avec des espaces, des majuscules et des accents dans les noms — c’est une corvée qui prend l’après-midi et où l’on se trompe. C’est précisément ce que les boucles font à votre place : appliquer la même action à chaque élément d’un lot, sans fatigue et sans erreur. À la fin de ce tutoriel, un seul script nettoiera un dossier entier de photos en quelques secondes.

Dans le fil rouge de la série, Boubacar reçoit régulièrement des dossiers de photos pour les boutiques en ligne qu’il gère. Les noms sont catastrophiques : IMG 2024 (1).JPG, Photo Final!!.png… Il va écrire renommer-photos.sh pour tout convertir en une convention propre : produit-001.jpg, produit-002.jpg, etc.

🎯 Ce que vous allez apprendre

  • Écrire des boucles for (sur une liste et à la C), while et until ;
  • Parcourir des fichiers proprement avec le globbing (*.jpg) et éviter le piège for f in $(ls) ;
  • Lire un fichier ligne par ligne avec while IFS= read -r sans rien casser ;
  • Manipuler des noms de fichiers avec l’expansion de paramètres (${f##*/}, ${f%.jpg}, ${f,,}).

🛠️ Ce que vous allez construire

Un script renommer-photos.sh qui prend en argument un dossier de photos, les renomme selon une convention numérotée propre, ignore les fichiers déjà conformes, et affiche un compte rendu de chaque opération. La corvée de l’après-midi devient une commande d’une ligne.

Prérequis

  • Avoir suivi Variables, conditions et tests (if, [[ ]], codes de sortie).
  • Test express : si vous savez écrire un if [[ -f "$x" ]], vous êtes prêt.
  • Bash 4+ (l’expansion ${var,,} exige Bash 4 ; testé sous Bash 5.3). ⏱️ ~35 minutes.

Étape 1 — La boucle for sur une liste

La forme la plus courante de boucle parcourt une liste de valeurs, une par une. À chaque tour, la variable de boucle prend la valeur suivante, et le bloc entre do et done s’exécute. Commençons par le cas le plus simple : une liste écrite à la main.

for client in boutique-aminata association-thies cabinet-diop; do
  echo "Traitement du site : $client"
done

À chaque itération, $client vaut tour à tour chacun des trois noms, et la ligne echo s’exécute trois fois. La structure for ... in ...; do ... done est le squelette : on y reviendra sans cesse. Notez le point-virgule avant do (ou un retour à la ligne), exactement comme pour le if.

Étape 2 — Parcourir des fichiers avec le globbing

Le vrai usage, c’est de boucler sur des fichiers. Et ici, une tentation guette tous les débutants : écrire for f in $(ls *.jpg). Ne le faites jamais. Cette forme casse dès qu’un nom contient une espace (le fichier Photo Final.jpg est vu comme deux fichiers Photo et Final.jpg). La bonne méthode est le globbing : on donne directement le motif à for, et Bash le développe en liste de fichiers réels.

for photo in *.jpg; do
  echo "Photo trouvée : $photo"
done

Bash remplace *.jpg par tous les fichiers .jpg du dossier courant, en gérant correctement les espaces et caractères spéciaux. Chaque $photo est un nom intact. Un détail à connaître : si aucun fichier ne correspond, Bash laisse le motif *.jpg littéral, et la boucle tourne une fois avec cette valeur bizarre. On corrige ce comportement à l’étape suivante.

Étape 3 — Sécuriser le globbing avec shopt

Deux options de Bash rendent le parcours de fichiers fiable. nullglob fait qu’un motif sans correspondance se développe en rien (la boucle ne tourne pas) plutôt qu’en texte littéral. nocaseglob rend le motif insensible à la casse, pour attraper aussi bien .jpg que .JPG. On les active avec shopt -s.

shopt -s nullglob nocaseglob

for photo in *.jpg *.jpeg *.png; do
  echo "À traiter : $photo"
done

Désormais, si le dossier ne contient aucune image, la boucle est simplement sautée — pas de message parasite. Et un fichier nommé VACANCES.JPG est bien attrapé par le motif *.jpg grâce à nocaseglob. Ces deux réglages devraient être un réflexe dans tout script qui parcourt des fichiers.

Point d’étape — Créez un dossier de test avec quelques fichiers (touch "Photo Une.jpg" deux.JPG trois.png) et vérifiez que votre boucle les liste tous, espaces compris. C’est le terrain de jeu pour la suite.

Étape 4 — Manipuler les noms de fichiers

Renommer, c’est transformer un nom en un autre. Bash dispose pour cela d’opérations d’expansion de paramètres très efficaces, qui évitent de lancer des commandes externes pour chaque fichier (important quand on en traite des centaines). Voici les plus utiles sur des chemins.

chemin="/var/www/boutique/Photo Une.JPG"

echo "${chemin##*/}"   # Photo Une.JPG   → ne garde que le nom (comme basename)
echo "${chemin%/*}"    # /var/www/boutique → ne garde que le dossier (comme dirname)
echo "${chemin##*.}"   # JPG             → l'extension seule
nom="${chemin##*/}"
echo "${nom%.*}"       # Photo Une       → le nom sans extension
echo "${nom,,}"        # photo une.jpg   → tout en minuscules (Bash 4+)
echo "${nom// /-}"     # Photo-Une.JPG   → remplace toutes les espaces par des tirets

Décortiquons : ##*/ supprime le plus long préfixe finissant par / (donc tout le chemin, on garde le nom). %.* supprime le plus court suffixe commençant par . (donc l’extension). ,, met en minuscules. // /- est un remplacer-tout : « remplace toutes les espaces par des tirets ». Ces transformations sont instantanées et purement internes à Bash — pas de processus lancé, contrairement à basename ou sed.

Étape 5 — La boucle while read pour lire un fichier

L’autre grande boucle, while, répète tant qu’une condition reste vraie. Son usage emblématique est la lecture d’un fichier ligne par ligne. La forme correcte, à mémoriser telle quelle, est while IFS= read -r ligne; do ... done < fichier. Chaque mot compte.

while IFS= read -r ligne; do
  echo "Site à sauvegarder : $ligne"
done < sites.txt

Pourquoi cette formule exacte ? read -r lit une ligne sans interpréter les antislashs (sinon un \ dans un nom serait mangé). IFS= en début de ligne empêche read de rogner les espaces de début et de fin. Et < sites.txt redirige le fichier vers la boucle. Avec cette forme, vous lisez n’importe quel fichier — une liste de sites, de fichiers, d’URLs — sans surprise. C’est un idiome qu’on garde tel quel.

Pour lire la sortie d’une commande plutôt qu’un fichier, on utilise la substitution de processus < <(commande), qui évite un piège subtil du pipe (les variables modifiées dans une boucle après un | sont parfois perdues) :

compteur=0
while IFS= read -r fichier; do
  (( compteur++ ))
done < <(find . -name '*.log')
echo "$compteur journaux trouvés."

Ici la variable compteur garde bien sa valeur après la boucle, ce qui ne serait pas garanti avec find ... | while .... Retenez la forme done < <(commande) dès que vous bouclez sur une sortie ET modifiez une variable.

Parfois on veut d’abord rassembler tous les éléments dans une liste pour les compter ou les trier avant d’agir. Bash dispose pour cela de tableaux, et de mapfile (alias readarray) qui charge des lignes directement dans un tableau, une par case. C’est plus sûr que de bricoler avec des chaînes séparées par des espaces.

mapfile -t photos < <(find . -maxdepth 1 -name '*.jpg')

echo "Nombre de photos : ${#photos[@]}"   # taille du tableau
echo "Première : ${photos[0]}"              # premier élément
for p in "${photos[@]}"; do                 # parcourir tout le tableau
  echo " - $p"
done

L’option -t retire le saut de ligne final de chaque entrée. On accède à la taille avec ${#photos[@]}, à un élément par son index ${photos[0]}, et on parcourt tout avec "${photos[@]}" — les guillemets et le [@] sont essentiels pour préserver les noms à espaces. Les tableaux deviennent vite indispensables dès qu’un script manipule des collections (fichiers, serveurs, options).

Point d’étape — Vous distinguez for (sur une liste connue, typiquement des fichiers via globbing) et while read (sur les lignes d’un fichier ou d’une commande), et vous savez stocker une liste dans un tableau avec mapfile. Vous transformez un nom de fichier sans outil externe.

Étape 6 — La boucle for à la C et les compteurs

Quand on a besoin d’un compteur numérique — pour numéroter nos photos, justement — la forme arithmétique for (( )) est la plus claire. Elle reprend la syntaxe des langages comme C : initialisation, condition, incrément.

for (( i = 1; i <= 5; i++ )); do
  printf 'produit-%03d\n' "$i"
done

Cette boucle affiche produit-001 à produit-005. i++ augmente i de 1 à chaque tour, i <= 5 est la condition d’arrêt. Le %03d de printf formate le nombre sur trois chiffres avec des zéros devant — indispensable pour que les fichiers se trient correctement (sinon produit-10 se classe avant produit-2). On combine maintenant tout ça.

Étape 7 — Assembler renommer-photos.sh

Voici l’outil complet. Il se place dans le dossier passé en argument, active le globbing sûr, et numérote chaque image avec un compteur, en minuscules et sans espaces. Il refuse d’écraser un fichier existant grâce à mv -n, et le -- protège contre les noms commençant par un tiret.

#!/usr/bin/env bash
#
# renommer-photos.sh — normalise les noms des photos d'un dossier.
# Usage : ./renommer-photos.sh ~/clients/boutique-aminata/photos
# Boîte à outils Atelier — tutoriel 3.

dossier="${1:?Usage : $0 <dossier-photos>}"

cd "$dossier" || { echo "❌ Dossier inaccessible : $dossier" >&2; exit 1; }

shopt -s nullglob nocaseglob

compteur=1
renommees=0
for photo in *.jpg *.jpeg *.png; do
  ext="${photo##*.}"          # extension d'origine
  ext="${ext,,}"             # en minuscules
  [[ "$ext" == "jpeg" ]] && ext="jpg"   # on uniformise jpeg → jpg
  nouveau=$(printf 'produit-%03d.%s' "$compteur" "$ext")

  if [[ "$photo" == "$nouveau" ]]; then
    echo "⏭  $photo est déjà conforme."
  elif [[ -e "$nouveau" ]]; then
    echo "⚠️  $nouveau existe déjà, $photo non renommée." >&2
  else
    mv -n -- "$photo" "$nouveau"
    echo "✅ $photo → $nouveau"
    (( renommees++ ))
  fi
  (( compteur++ ))
done

echo "Terminé : $renommees photo(s) renommée(s) sur $(( compteur - 1 )) examinée(s)."

Plusieurs réflexes acquis se retrouvent ici : l’argument obligatoire ${1:?...}, le cd ... || { ...; exit 1; } qui regroupe message et sortie en cas d’échec, le globbing sécurisé, l’expansion de paramètres pour l’extension, et printf '%03d' pour la numérotation. La condition [[ "$photo" == "$nouveau" ]] rend le script idempotent : on peut le relancer sans tout re-renommer en boucle. Testez-le sur un dossier de copies (jamais sur vos originaux du premier coup !).

Point d’étape final — Sur un dossier de test, le script renomme les images en produit-001.jpg, produit-002.jpg… et affiche un bilan. Relancez-le : il doit dire « déjà conforme » pour les fichiers déjà traités, preuve qu’il est sûr à relancer.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
Un fichier *.jpg littéral est « traité » Aucun fichier ne correspond et nullglob n’est pas actif shopt -s nullglob en tête de script
Noms à espaces découpés en plusieurs for f in $(ls) ou variable sans guillemets Boucler sur un glob ; toujours "$f"
Espaces de fin de ligne perdus à la lecture read sans IFS= while IFS= read -r ligne
Compteur remis à zéro après un pipe cmd | while ouvre un sous-shell Utiliser done < <(cmd)
${nom,,}: bad substitution Bash trop ancien (3.2, ex. macOS) Installer un Bash 4+ ou utiliser tr '[:upper:]' '[:lower:]'

🌍 Adaptation au contexte ouest-africain

Le traitement par lot est une mine d’or pour les freelances et les petites agences de la région : préparer des centaines de photos produit pour une boutique en ligne, organiser les justificatifs scannés d’une association, ranger des PDF de factures. Tout cela se fait localement, sans abonnement à un logiciel cloud payant en devises. Un point d’attention : sur des supports lents (clé USB, disque externe sur un vieux port), le traitement de gros volumes prend du temps — testez d’abord sur un petit échantillon, et travaillez sur une copie pour ne jamais risquer les originaux d’un client.

✅ Récapitulatif

Vous savez désormais répéter une action sur des lots entiers. Vous maîtrisez la boucle for sur une liste et sur un glob (*.jpg), la forme arithmétique for (( )) avec compteur, et la boucle while IFS= read -r pour parcourir un fichier ou une commande ligne par ligne. Vous évitez les deux grands pièges — for f in $(ls) et le pipe qui perd les variables — et vous transformez les noms de fichiers à la volée avec l’expansion de paramètres. renommer-photos.sh rejoint la boîte à outils, et il est idempotent.

🧾 Aide-mémoire

Élément Rôle
for x in *.jpg; do ... done Boucle sur des fichiers (globbing sûr)
for (( i=1; i<=N; i++ )) Boucle avec compteur numérique
while IFS= read -r l; do ... done < f Lire un fichier ligne par ligne
done < <(commande) Boucler sur une sortie sans perdre les variables
shopt -s nullglob nocaseglob Glob sûr et insensible à la casse
${f##*/} / ${f%.*} Nom seul / nom sans extension
${f,,} / ${f// /-} Minuscules / remplacer espaces par tirets

💪 À vous de jouer

Modifiez renommer-photos.sh pour qu’il range les images renommées dans un sous-dossier pretes/ au lieu de les renommer sur place — plus sûr, car les originaux restent intacts.

Voir une solution
destination="$dossier/pretes"
mkdir -p "$destination"
# ... dans la boucle, remplacer le mv par :
cp -n -- "$photo" "$destination/$nouveau" && echo "✅ $photo → pretes/$nouveau"

On utilise cp au lieu de mv pour conserver les originaux, et mkdir -p crée le dossier de destination une seule fois avant la boucle.

Tutoriels frères

Pour aller plus loin

FAQ

Q : Pourquoi ne pas faire for f in $(ls) ?
R : Parce que ls renvoie du texte que Bash redécoupe sur les espaces : un nom de fichier contenant une espace est cassé en plusieurs, et les caractères spéciaux s’emmêlent. Le globbing (for f in *) donne des noms intacts.

Q : Quelle différence entre while et until ?
R : while répète tant que la condition est vraie ; until répète jusqu’à ce que la condition devienne vraie (donc tant qu’elle est fausse). until est rare ; on s’en sert pour attendre qu’un service démarre, par exemple.

Q : Mon expansion ${nom,,} renvoie « bad substitution ». Pourquoi ?
R : Cette syntaxe exige Bash 4. Vous êtes probablement sur un Bash 3.2 (macOS par défaut). Installez un Bash récent, ou utilisez tr '[:upper:]' '[:lower:]' à la place.

Q : Comment parcourir aussi les sous-dossiers ?
R : Le glob simple *.jpg ne descend pas dans l’arborescence. Activez shopt -s globstar et utilisez **/*.jpg, ou passez par find avec une boucle while read.

📚 Ressources et références

Mots-clés : boucle bash, for bash, while read bash, globbing, traitement de fichiers, renommer fichiers en lot, expansion de paramètres, nullglob.

Partager