Les renommages en masse ressemblent à ce genre de « petit changement » qui arrive sur votre agenda comme un rendez-vous chez le dentiste et qui s’en va comme un canal radiculaire. Ce n’est jamais seulement renommer. C’est des collisions, des caractères bizarres, la casse, des déplacements entre systèmes de fichiers, des applications qui mettent en cache des chemins, et ce répertoire où quelqu’un a rangé « final_FINAL_v7 (use this).docx ».
Si vous allez renommer des centaines ou des millions de fichiers, vous avez besoin d’un plan qui anticipe l’échec. Pas parce que vous êtes négligent, mais parce que les systèmes de fichiers, les habitudes humaines de nommage et les calendriers d’entreprise sont adversaires. Voici une approche pratique et orientée production : des simulations (dry runs) réellement exploitables, un renommage en deux phases pour éviter les collisions, des journaux d’audit pour revenir en arrière, et des vérifications de performance pour ne pas transformer votre NAS en radiateur de salle serveur.
Incontournables pour des renommages sûrs
1) Un renommage est une métadonnée — jusqu’à preuve du contraire
Sur un même système de fichiers, rename() est généralement « juste de la métadonnée » : rapide, atomique et sans copie du contenu. C’est la bonne nouvelle. La mauvaise nouvelle est que votre outil peut ne pas effectuer un renommage pur. Si votre « renommage » traverse des limites de systèmes de fichiers — par exemple si vous déplacez des fichiers d’un point de montage à un autre — il peut se dégrader en copie+suppression. Cela change le timing, le comportement en cas d’échec, les permissions et vos options de récupération.
Décision : traitez chaque renommage en masse comme un événement de gestion du changement. Vous planifiez l’atomicité, mais vous vérifiez que vous l’obtenez réellement.
2) Dry-run signifie « prédire exactement ce qui va se passer »
Un dry-run qui imprime une liste vague n’est pas un dry-run. Votre dry-run doit produire :
- une correspondance un-à-un de chemin source → chemin destination
- un rapport de collisions (incluant les collisions liées à la casse)
- un plan de rollback (une mappage inverse que vous pouvez rejouer)
- un ordre stable (pour que les relances donnent le même résultat)
La sortie du dry-run doit être quelque chose que vous pouvez comparer (diff), revoir et approuver. Si vous ne pouvez pas expliquer pourquoi un fichier précis deviendra un nom précis, vous ne faites pas de dry-run. Vous espérez.
3) Le renommage en deux phases empêche les auto-collisions
La chute classique : renommer a.txt en A.txt sur un système de fichiers insensible à la casse, ou renommer plusieurs fichiers vers la même forme normalisée (espaces vers underscores, passage en minuscules, suppression de la ponctuation). La solution est ennuyeuse et correcte : renommer tout d’abord vers des noms temporaires uniques, puis vers les noms finaux.
4) Les journaux ne sont pas optionnels — le rollback a besoin de justificatifs
Vous voulez un journal en mode append-only contenant les opérations mv exactes effectuées, dans l’ordre, plus les sauts et les erreurs. En cas d’incident, la différence entre « nous pouvons revenir en arrière » et « nous pensons pouvoir revenir en arrière » est un fichier d’audit que vous pouvez rejouer.
5) « Ça marche sur mon laptop » n’est pas une stratégie pour les systèmes de fichiers
Les sémantiques des systèmes de fichiers varient : ext4, XFS, ZFS, partages SMB, montages NFS, montages FUSE basés objet — chacun a ses particularités. Certains sont insensibles à la casse, d’autres ont des limites étranges, certains sont lents sur les opérations de répertoire volumineux. Votre plan de renommage doit inclure une lecture rapide de ce sur quoi vous opérez.
Une citation à garder sur votre bureau : « L’espoir n’est pas une stratégie. » — Gene Kranz
Faits intéressants et petite histoire
- Atomicité de POSIX rename : Sur un même système de fichiers,
rename()est conçu pour être atomique : vous n’obtenez pas un nom à moitié renommé. C’est pourquoi c’est la colonne vertébrale des mises à jour sûres de fichiers. - Windows a forgé les attentes insensibles à la casse : Les systèmes de fichiers insensibles à la casse sont devenus courants sur les postes de travail, et ils surprennent encore les ingénieurs qui supposent que
Report.csvetreport.csvsont distincts. - Les outils Unix anciens supposaient « pas d’espaces » : Beaucoup de schémas shell classiques sont nés à une époque où les noms de fichiers avec espaces étaient considérés comme une erreur utilisateur. Aujourd’hui, c’est banal.
- SMB et NFS peuvent ajouter de la latence aux métadonnées : Un renommage modifie des métadonnées, mais les métadonnées distantes peuvent être lentes et bavardes, surtout sur des liens chargés ou avec une consistance stricte.
- La taille du répertoire compte : Beaucoup de systèmes de fichiers gèrent bien les répertoires énormes maintenant, mais des opérations comme l’énumération et le stat sur des millions d’entrées restent un goulot d’étranglement lors de la planification et de la vérification.
- La normalisation Unicode est un véritable piège : Certaines plateformes normalisent Unicode différemment, donc un nom « visuellement identique » peut être une séquence d’octets différente. Les renommages en masse peuvent accidentellement provoquer des collisions.
- Les liens physiques compliquent les attentes : Renommer un fichier lié physiquement change l’entrée de répertoire, pas l’inode. Si vous attendiez « deux fichiers », vous pourriez en réalité avoir un inode avec deux noms.
- Les snapshots ont changé la donne : Avec ZFS et systèmes similaires, prendre un snapshot avant le renommage rend le rollback « restaurer les noms » plus faisable — parfois simplement en revenant en arrière sur le dataset.
Ce qui foire réellement (et pourquoi)
Collision : deux anciens noms deviennent un seul nouveau nom
La sanitisation des noms est l’endroit où les collisions naissent. Convertir les espaces en underscores, tout mettre en minuscules, supprimer la ponctuation, et soudain :
ACME - Q4.csvACME_Q4.csvacme q4.csv
…souhaitent tous devenir acme_q4.csv. Si votre script « dernier écrit gagne », vous avez perdu des données sans toucher au contenu des fichiers. Le fichier est là. Le nom est faux. C’est pire : une corruption silencieuse du sens.
Changements seulement de casse sur des montages insensibles à la casse
Sur un système de fichiers insensible à la casse, renommer foo en Foo peut être traité comme un « no-op » ou peut nécessiter une gymnastique (renommer en temp, puis renommer en souhaité). Les partages distants rendent cela encore plus amusant.
Le « renommage » inter-systèmes devient copie+suppression
Si vous utilisez des outils qui déplacent des fichiers entre des montages tout en « renommant », vous n’exécutez plus des mises à jour atomiques de métadonnées. Vous copiez des données, consommez de la bande passante et créez un mode d’échec avec des copies partielles et des originaux manquants.
Dérive des permissions et de la propriété
Un renommage pur conserve les métadonnées de l’inode. Une copie+suppression ne le fait pas. Soudainement, le « même fichier » a un nouveau propriétaire, des ACL différentes et un contexte SELinux différent. C’est ainsi que survient une panne causée par un renommage.
Les applications n’aiment pas qu’on change leurs chemins
Certaines applications stockent des chemins absolus dans des bases de données, fichiers de configuration ou caches. Renommer des fichiers sous elles revient à déplacer le bureau de quelqu’un pendant une réunion. C’est drôle jusqu’à ce que vous soyez la personne qui présente.
Blague #1 : Renommer des fichiers en production, c’est comme réorganiser la cuisine à 2 h du matin — vous ne retrouverez rien et tout le monde le découvrira au petit-déjeuner.
Tâches pratiques : commandes, sorties, décisions
Vous trouverez ci-dessous de vraies tâches opérationnelles à exécuter avant le renommage, pendant l’exécution et après. Chaque tâche inclut une commande, une sortie d’exemple et la décision que vous en tirez.
Tâche 1 : Confirmer où résident les données (ne traversez pas les systèmes de fichiers par accident)
cr0x@server:~$ df -Th /data/projects
Filesystem Type Size Used Avail Use% Mounted on
tank/projects zfs 8.0T 5.1T 2.9T 64% /data/projects
Ce que ça signifie : Vous êtes sur le dataset ZFS tank/projects. Si vous renommez à l’intérieur de ce montage, c’est metadata-only et compatible snapshots.
Décision : Assurez-vous que votre script ne déplace pas les fichiers vers un autre point de montage. Gardez source et destination sous le même système de fichiers.
Tâche 2 : Vérifier les options de montage et si c’est un partage réseau
cr0x@server:~$ mount | grep -E ' /data/projects | type (nfs|cifs)'
tank/projects on /data/projects type zfs (rw,xattr,noacl)
Ce que ça signifie : ZFS local, pas NFS/SMB. La latence de renommage devrait être stable.
Décision : Si vous voyez NFS/CIFS, planifiez le renommage en heures creuses et attendez-vous à des allers-retours metadata.
Tâche 3 : Compter les fichiers et répertoires (estimer l’aire d’impact)
cr0x@server:~$ find /data/projects/clientA -type f | wc -l
284913
Ce que ça signifie : ~285k fichiers. Les étapes qui font un stat() par fichier vont prendre du temps.
Décision : Utilisez des scans efficaces ; évitez les sous-processus par fichier si possible. Envisagez le batching ou la parallélisation avec précaution (plus bas).
Tâche 4 : Identifier les « caractères problématiques » dans les noms (espaces, nouvelles lignes, caractères de contrôle)
cr0x@server:~$ find /data/projects/clientA -type f -name $'*\n*' -print | head
/data/projects/clientA/inbox/weird
name.txt
Ce que ça signifie : Au moins un nom de fichier contient une nouvelle ligne. Ce n’est pas théorique ; c’est réel.
Décision : Votre pipeline doit être NUL-délimitée (-print0 + read -d '') et vos journaux doivent être non ambigus (échappement ou formats sûrs NUL).
Tâche 5 : Vérifier la présence de tirets en tête (les outils les interprètent comme options)
cr0x@server:~$ find /data/projects/clientA -type f -name '-*' | head
/data/projects/clientA/inbox/-final.pdf
Ce que ça signifie : Au moins un fichier commence par -.
Décision : Utilisez toujours mv -- "$src" "$dst" pour que les noms de fichiers ne soient jamais interprétés comme des options.
Tâche 6 : Détecter les collisions liées à la casse qui vous affecteront sur SMB ou macOS
cr0x@server:~$ find /data/projects/clientA -maxdepth 2 -type f -printf '%f\n' | awk '{print tolower($0)}' | sort | uniq -d | head
readme.txt
Ce que ça signifie : Il existe au moins deux fichiers dont les noms ne diffèrent que par la casse quelque part dans ce scan superficiel.
Décision : Si l’environnement de destination est insensible à la casse (ou peut l’être), vous avez besoin d’une politique de normalisation et d’une résolution de collisions (suffixes, hachage, ou conservation de la casse d’origine).
Tâche 7 : Trouver les basenames dupliqués après votre normalisation prévue (prévisualiser les collisions)
cr0x@server:~$ find /data/projects/clientA -type f -printf '%p\n' | \
awk -F/ '{
base=$NF
norm=tolower(base)
gsub(/[^a-z0-9._-]+/,"_",norm)
print norm
}' | sort | uniq -c | awk '$1>1{print $0}' | head
2 acme_q4.csv
3 invoice_2024_01.pdf
Ce que ça signifie : Votre normalisation prévue créera des collisions pour au moins deux noms.
Décision : Décidez maintenant de la politique de collision : ignorer, suffixer avec __DUP2, inclure le répertoire parent, ou ajouter un court hash.
Tâche 8 : Prendre un snapshot (si disponible) avant de changer les noms
cr0x@server:~$ zfs snapshot tank/projects@pre_rename_clientA
cr0x@server:~$ zfs list -t snapshot -o name,creation | grep pre_rename_clientA
tank/projects@pre_rename_clientA Mon Feb 5 10:12 2026
Ce que ça signifie : Vous avez maintenant un snapshot point-in-time du dataset.
Décision : Si le renommage tourne mal, vous pouvez rollbacker le dataset (gros marteau) ou utiliser la navigation dans les snapshots pour restaurer les noms.
Tâche 9 : Mesurer la performance metadata de base (pour repérer les goulots)
cr0x@server:~$ /usr/bin/time -f 'elapsed=%E cpu=%P' bash -c 'find /data/projects/clientA -maxdepth 2 -type f -print0 | xargs -0 -n 1000 stat >/dev/null'
elapsed=0:08.41 cpu=92%
Ce que ça signifie : Le stat d’un échantillon est rapide et gourmand en CPU (bon signe : métadonnées locales, pas limitées par le réseau).
Décision : Si le temps est énorme et que le CPU est bas, vous êtes lié par l’I/O ou le réseau. Planifiez des lots plus petits et exécutez en heures creuses.
Tâche 10 : Vérifier l’épuisement des inodes (oui, ça arrive encore)
cr0x@server:~$ df -i /data/projects
Filesystem Inodes IUsed IFree IUse% Mounted on
tank/projects - - - - /data/projects
Ce que ça signifie : Sur ZFS, le rapport d’inodes diffère ; vous n’obtiendrez pas les comptes d’inodes classiques.
Décision : Sur ext4/XFS, vous vérifieriez la disponibilité des inodes. Sur ZFS, concentrez-vous sur l’espace et la performance des métadonnées.
Tâche 11 : Construire une liste de fichiers déterministe (ordre stable pour reproductibilité)
cr0x@server:~$ cd /data/projects/clientA
cr0x@server:~$ find . -type f -print0 | sort -z > /tmp/clientA.files.zlist
cr0x@server:~$ python3 - <<'PY'
import os
p="/tmp/clientA.files.zlist"
print("bytes", os.path.getsize(p))
PY
bytes 19844712
Ce que ça signifie : Vous avez maintenant une liste NUL-délimitée stable que vous pouvez réutiliser pour le dry-run et l’exécution.
Décision : Geler la portée. Si de nouveaux fichiers arrivent pendant la fenêtre de renommage, traitez-les dans une seconde passe — ne courez pas après une cible mouvante.
Tâche 12 : Lancer un vrai dry-run (mapping source → dest) et l’inspecter
cr0x@server:~$ bash safe_rename.sh --plan --root /data/projects/clientA --rule 'lower,sanitize' --out /tmp/rename.plan.tsv
Plan written: /tmp/rename.plan.tsv
Rows: 284913
Collisions: 17
Skips: 0
Ce que ça signifie : Votre plan a trouvé 17 collisions. Bien : il n’a pas fait semblant que tout allait bien.
Décision : Arrêtez-vous et résolvez les collisions avant d’exécuter. Ajustez les règles ou choisissez une politique de collision.
Tâche 13 : Vérifier que les collisions sont réellement problématiques (spot-check)
cr0x@server:~$ awk -F'\t' '$4=="COLLISION"{print $0}' /tmp/rename.plan.tsv | head -3
/data/projects/clientA/inbox/ACME - Q4.csv /data/projects/clientA/inbox/acme_q4.csv COLLISION with:/data/projects/clientA/inbox/ACME_Q4.csv
/data/projects/clientA/inbox/ACME_Q4.csv /data/projects/clientA/inbox/acme_q4.csv COLLISION with:/data/projects/clientA/inbox/ACME - Q4.csv
/data/projects/clientA/finance/Invoice 2024-01.pdf /data/projects/clientA/finance/invoice_2024-01.pdf COLLISION with:/data/projects/clientA/finance/Invoice_2024_01.pdf
Ce que ça signifie : Deux noms originaux convergent. Ce n’est pas un faux positif.
Décision : Choisir : ajouter des suffixes de hachage, conserver une variante, ou isoler les doublons dans un répertoire de quarantaine.
Tâche 14 : Exécuter d’abord un canari réduit
cr0x@server:~$ head -z -n 2000 /tmp/clientA.files.zlist > /tmp/clientA.canary.zlist
cr0x@server:~$ bash safe_rename.sh --apply --filelist /tmp/clientA.canary.zlist --root /data/projects/clientA --rule 'lower,sanitize' --log /tmp/rename.canary.log
Applied.
Renamed: 1983
Unchanged: 17
Errors: 0
Ce que ça signifie : Un canari a réussi. Certains fichiers sont restés inchangés (déjà normalisés ou exclus).
Décision : Validez les applications en aval et les permissions maintenant, avant de toucher aux 282k autres fichiers.
Tâche 15 : Vérifier que le renommage n’a pas modifié le contenu des fichiers (échantillonnage)
cr0x@server:~$ awk -F'\t' '$3=="RENAMED"{print $1"\t"$2}' /tmp/rename.canary.log | head -5
/data/projects/clientA/inbox/Report 1.txt /data/projects/clientA/inbox/report_1.txt
/data/projects/clientA/inbox/Spec (Draft).pdf /data/projects/clientA/inbox/spec_draft_.pdf
/data/projects/clientA/inbox/notes!.md /data/projects/clientA/inbox/notes_.md
/data/projects/clientA/inbox/Q4#plan.xlsx /data/projects/clientA/inbox/q4_plan.xlsx
/data/projects/clientA/inbox/Team Photo.JPG /data/projects/clientA/inbox/team_photo.jpg
cr0x@server:~$ src="/data/projects/clientA/inbox/Report 1.txt"
cr0x@server:~$ dst="/data/projects/clientA/inbox/report_1.txt"
cr0x@server:~$ sha256sum "$dst"
8c1b5e9b6bbfe19c1f0f3f51c2d7e1ce0b41b2dcf2a44d26e7e4a6e93c39d9d0 /data/projects/clientA/inbox/report_1.txt
Ce que ça signifie : Vous pouvez hasher le fichier renommé. Pour un renommage pur, le contenu doit correspondre, mais vous échantillonnez pour détecter des copies/transcodages accidentels.
Décision : Si les hashes diffèrent pour des fichiers « renommés », vous ne faites pas des renommages — vous faites autre chose. Arrêtez.
Tâche 16 : Surveiller l’avancement et le taux d’erreurs en direct
cr0x@server:~$ tail -f /tmp/rename.full.log
2026-02-05T10:22:11Z RENAMED /data/projects/clientA/inbox/Team Photo.JPG /data/projects/clientA/inbox/team_photo.jpg
2026-02-05T10:22:11Z UNCHANGED /data/projects/clientA/inbox/readme.txt /data/projects/clientA/inbox/readme.txt
2026-02-05T10:22:11Z ERROR /data/projects/clientA/legal/Contract?.pdf Permission denied
Ce que ça signifie : Vous observez une erreur de permission.
Décision : Ne continuez pas sans réfléchir si les erreurs se concentrent. Décidez d’ignorer et de rapporter, ou de corriger les permissions et de réessayer.
Le script : dry-run d’abord, renommage en deux phases, journal de rollback
Ce script est conçu pour le monde sale : espaces, nouvelles lignes, tirets en tête, collisions, et systèmes de fichiers qui ne partagent pas votre optimisme. Il prend en charge :
- Mode plan : produit un TSV de plan avec détection de collisions
- Mode application : effectue un renommage en deux phases pour éviter les collisions
- Rollback : utilise le journal pour inverser l’opération
- Portée stable : liste de fichiers NUL-délimitée optionnelle pour renommer exactement ce que vous avez revu
Opinion : si vous renommez plus que quelques centaines de fichiers, n’« exécutez pas juste un one-liner ». Les one-liners sont géniaux jusqu’à ce que vous deviez expliquer à l’Audit pourquoi revenue_final.xlsx a disparu. Utilisez un script avec des journaux.
cr0x@server:~$ cat safe_rename.sh
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
safe_rename.sh --plan|--apply|--rollback
--root PATH Root directory containing files to rename
--rule RULES Comma-separated rules: lower,sanitize
--out PLAN.tsv (plan) output plan TSV
--log LOG.tsv (apply/rollback) operation log
--filelist FILE.zlist Optional NUL-delimited, sorted file list (relative or absolute)
--collision-policy POLICY one of: error, suffix-hash
USAGE
}
mode=""
root=""
rules=""
out=""
log=""
filelist=""
collision_policy="error"
while [[ $# -gt 0 ]]; do
case "$1" in
--plan|--apply|--rollback) mode="${1#--}"; shift ;;
--root) root="$2"; shift 2 ;;
--rule) rules="$2"; shift 2 ;;
--out) out="$2"; shift 2 ;;
--log) log="$2"; shift 2 ;;
--filelist) filelist="$2"; shift 2 ;;
--collision-policy) collision_policy="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
[[ -n "$mode" ]] || { echo "Missing mode" >&2; usage; exit 2; }
[[ "$mode" == "rollback" || -n "$root" ]] || { echo "Missing --root" >&2; exit 2; }
[[ -n "$rules" || "$mode" == "rollback" ]] || { echo "Missing --rule" >&2; exit 2; }
ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
norm_name() {
local name="$1"
local outname="$name"
IFS=',' read -r -a rr <<< "$rules"
for r in "${rr[@]}"; do
case "$r" in
lower) outname="$(printf '%s' "$outname" | tr '[:upper:]' '[:lower:]')" ;;
sanitize)
outname="$(printf '%s' "$outname" | sed -E 's/[^a-zA-Z0-9._-]+/_/g; s/^_+//; s/_+$//')"
;;
*) echo "Unknown rule: $r" >&2; exit 2 ;;
esac
done
printf '%s' "$outname"
}
sha8() {
# short stable suffix for collision avoidance
printf '%s' "$1" | sha256sum | awk '{print substr($1,1,8)}'
}
emit_file_list() {
# Output NUL-delimited absolute file paths
if [[ -n "$filelist" ]]; then
# filelist may contain relative entries; anchor under root if relative
while IFS= read -r -d '' p; do
if [[ "$p" = /* ]]; then
printf '%s\0' "$p"
else
printf '%s\0' "$root/$p"
fi
done < "$filelist"
else
find "$root" -type f -print0
fi
}
plan() {
[[ -n "$out" ]] || { echo "Missing --out" >&2; exit 2; }
: > "$out"
declare -A seen=()
local collisions=0 rows=0 skips=0
while IFS= read -r -d '' src; do
rows=$((rows+1))
local dir base dstbase dst status extra
dir="$(dirname -- "$src")"
base="$(basename -- "$src")"
dstbase="$(norm_name "$base")"
dst="$dir/$dstbase"
if [[ "$src" == "$dst" ]]; then
status="UNCHANGED"; extra=""
skips=$((skips+1))
else
# collision check: destination path already targeted by another source
if [[ -n "${seen[$dst]:-}" ]]; then
status="COLLISION"; extra="with:${seen[$dst]}"
collisions=$((collisions+1))
else
status="OK"; extra=""
fi
seen[$dst]="$src"
fi
printf '%s\t%s\t%s\t%s\n' "$src" "$dst" "$status" "$extra" >> "$out"
done < <(emit_file_list)
echo "Plan written: $out"
echo "Rows: $rows"
echo "Collisions: $collisions"
echo "Skips: $skips"
if [[ "$collisions" -gt 0 && "$collision_policy" == "error" ]]; then
echo "Refusing to proceed with collisions under policy=error." >&2
fi
}
apply() {
[[ -n "$log" ]] || { echo "Missing --log" >&2; exit 2; }
local planfile
planfile="$(mktemp /tmp/rename.plan.XXXXXX.tsv)"
out="$planfile"
plan >/tmp/rename.plan.summary
if grep -q $'\tCOLLISION\t' "$planfile"; then
if [[ "$collision_policy" == "error" ]]; then
echo "Collisions detected; aborting. See: $planfile" >&2
exit 3
fi
fi
: > "$log"
# Phase 1: rename to temporary unique names in same directory
# We only rename items that will change.
while IFS=$'\t' read -r src dst status extra; do
[[ "$status" == "OK" ]] || continue
local dir base tmp
dir="$(dirname -- "$src")"
base="$(basename -- "$src")"
tmp="$dir/.rename_tmp.$(sha8 "$src").$base"
# Ensure temp name doesn't exist
if [[ -e "$tmp" ]]; then
echo -e "$(ts)\tERROR\t$src\t$temp_exists" >> "$log"
echo "Temp already exists: $tmp" >&2
exit 4
fi
mv -- "$src" "$tmp"
printf '%s\tPHASE1\t%s\t%s\n' "$(ts)" "$tmp" "$dst" >> "$log"
done < "$planfile"
# Phase 2: rename temp to final, with optional collision suffixing
while IFS=$'\t' read -r src dst status extra; do
if [[ "$status" == "UNCHANGED" ]]; then
printf '%s\tUNCHANGED\t%s\t%s\n' "$(ts)" "$src" "$dst" >> "$log"
continue
fi
[[ "$status" == "OK" || "$status" == "COLLISION" ]] || continue
local dir base tmp final
dir="$(dirname -- "$src")"
base="$(basename -- "$src")"
tmp="$dir/.rename_tmp.$(sha8 "$src").$base"
final="$dst"
if [[ "$status" == "COLLISION" && "$collision_policy" == "suffix-hash" ]]; then
# append hash before extension
local b ext h
h="$(sha8 "$src")"
b="$(basename -- "$dst")"
ext=""
if [[ "$b" == *.* && "$b" != .* ]]; then
ext=".${b##*.}"
b="${b%.*}"
fi
final="$dir/${b}__${h}${ext}"
elif [[ "$status" == "COLLISION" ]]; then
printf '%s\tERROR\t%s\t%s\n' "$(ts)" "$src" "collision" >> "$log"
continue
fi
if [[ -e "$final" ]]; then
printf '%s\tERROR\t%s\t%s\n' "$(ts)" "$src" "dest_exists:$final" >> "$log"
continue
fi
mv -- "$tmp" "$final"
printf '%s\tRENAMED\t%s\t%s\n' "$(ts)" "$src" "$final" >> "$log"
done < "$planfile"
echo "Applied."
echo "Renamed: $(awk -F'\t' '$2==\"RENAMED\"{c++} END{print c+0}' "$log")"
echo "Unchanged: $(awk -F'\t' '$2==\"UNCHANGED\"{c++} END{print c+0}' "$log")"
echo "Errors: $(awk -F'\t' '$2==\"ERROR\"{c++} END{print c+0}' "$log")"
}
rollback() {
[[ -n "$log" ]] || { echo "Missing --log" >&2; exit 2; }
# Roll back in reverse order; only RENAMED and PHASE1 entries matter.
tac "$log" | while IFS=$'\t' read -r t action a b; do
case "$action" in
RENAMED)
# a=src(original), b=final; move final back to original name
if [[ -e "$b" ]]; then
mv -- "$b" "$a"
printf '%s\tROLLED_BACK\t%s\t%s\n' "$(ts)" "$b" "$a"
else
printf '%s\tROLLBACK_MISSING\t%s\t%s\n' "$(ts)" "$b" "$a"
fi
;;
PHASE1)
# a=temp, b=dst planned; if temp still exists, move back to original? not stored here
# We don't have original in PHASE1 entry; rollback relies primarily on RENAMED lines.
:
;;
*) : ;;
esac
done
}
case "$mode" in
plan) plan ;;
apply) apply ;;
rollback) rollback ;;
*) echo "Bad mode: $mode" >&2; exit 2 ;;
esac
Comment l’exécuter en toute sécurité (comme des adultes)
1) Générer une liste de fichiers (optionnel mais recommandé). 2) Planifier. 3) Examiner les collisions. 4) Appliquer un canari. 5) Appliquer l’ensemble avec journalisation. 6) Vérifier.
cr0x@server:~$ cd /data/projects/clientA
cr0x@server:~$ find . -type f -print0 | sort -z > /tmp/clientA.files.zlist
cr0x@server:~$ bash safe_rename.sh --plan --root /data/projects/clientA --filelist /tmp/clientA.files.zlist --rule 'lower,sanitize' --out /tmp/clientA.plan.tsv
Plan written: /tmp/clientA.plan.tsv
Rows: 284913
Collisions: 17
Skips: 9211
Maintenant vous faites un choix. Si les collisions sont inacceptables, ajustez les règles de nommage ou traitez-les selon une politique. Si les collisions sont attendues et que vous avez besoin d’unicité déterministe, utilisez --collision-policy suffix-hash. Cela produit des noms de fichiers stables et explicables.
cr0x@server:~$ bash safe_rename.sh --apply --root /data/projects/clientA --filelist /tmp/clientA.files.zlist --rule 'lower,sanitize' --collision-policy suffix-hash --log /tmp/clientA.rename.log
Applied.
Renamed: 275702
Unchanged: 9211
Errors: 0
Blague #2 : Un renommage en masse sans journal, c’est comme un tour de magie sans public — personne ne sait ce qui s’est passé, y compris vous.
Playbook de diagnostic rapide
Quand un renommage en masse est lent ou instable, les gens adorent se disputer sur « le réseau » ou « ZFS qui est bizarre ». Ne discutez pas. Mesurez trois choses et vous trouverez généralement le goulot en dix minutes.
Premier : est-ce des métadonnées locales ou distantes ?
- Vérifier le type de montage (
mount,df -Th). Si c’est NFS/SMB/FUSE, attendez de la latence metadata. - Symptôme des métadonnées distantes : CPU faible, temps élevé, progression par à-coups.
cr0x@server:~$ /usr/bin/time -f 'elapsed=%E cpu=%P' bash -c 'for i in {1..2000}; do stat /data/projects/clientA/inbox/readme.txt >/dev/null; done'
elapsed=0:00.36 cpu=99%
Décision : Si ceci prend des secondes à minutes avec un CPU bas, vous êtes lié par la latence. Réduisez les allers-retours (opérations groupées, évitez les commandes externes par fichier) et exécutez en heures creuses.
Second : le répertoire est-il énorme et l’énumération est-elle le goulot ?
cr0x@server:~$ /usr/bin/time -f 'elapsed=%E' ls -U /data/projects/clientA/inbox >/dev/null
elapsed=0:02.91
Décision : Si lister un répertoire prend des secondes, renommer des milliers de fichiers à l’intérieur va poser problème. Fractionnez par sous-répertoires ou réorganisez avant de renommer.
Troisième : copiez-vous accidentellement le contenu des fichiers ?
cr0x@server:~$ awk -F: '$2==" /data/projects"{print}' /proc/mounts
tank/projects /data/projects zfs rw,xattr,noacl 0 0
Décision : Assurez-vous que les chemins source et destination restent dans /data/projects. Si votre outil « déplace » vers un autre montage, arrêtez et repensez.
Bonus : détecter rapidement la contention au niveau du système de fichiers
cr0x@server:~$ iostat -xz 1 3
Linux 6.6.0 (server) 02/05/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.15 0.00 6.42 1.11 0.00 80.32
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
nvme0n1 2.10 88.3 0.00 0.00 1.42 42.0 55.10 1400.0 10.00 15.36 4.10 25.4 0.25 12.4
Décision : Si %util est saturé et que l’await augmente, vous êtes limité par le stockage. Réduisez la concurrence et évitez les tempêtes de renommage parallèles.
Erreurs courantes : symptôme → cause → correction
1) Symptom : « Certains fichiers ont disparu »
Cause : Collision et écrasement. Votre script a renommé plusieurs sources vers une même destination et a écrasé ou sauté silencieusement.
Correction : Renommage en deux phases plus politique explicite de collision. Le défaut doit être « erreur et arrêt », pas « on s’en fiche et on continue ». Utilisez --collision-policy suffix-hash seulement si les parties prenantes acceptent des noms moins lisibles.
2) Symptom : « C’était rapide en dev, lent en prod »
Cause : Prod est sur NFS/SMB ou a un serveur de métadonnées chargé. Dev était sur SSD local.
Correction : Mesurez la latence des métadonnées. Réduisez les sous-processus par fichier. Exécutez en heures creuses. Si nécessaire, parallélisez prudemment (quelques workers), pas « un par cœur ».
3) Symptom : « Nous n’avons changé que la casse, mais rien ne s’est passé »
Cause : Système de fichiers insensible à la casse traite foo et Foo comme la même entrée, donc le renommage est un no-op ou génère des erreurs.
Correction : Renommage en deux étapes : foo → .__tmp__ → Foo. L’approche en deux phases du script fait cela.
4) Symptom : « La moitié de l’exécution a échoué avec Permission denied »
Cause : Certains répertoires/fichiers ont des permissions plus strictes, des ACL ou des flags immuables. Ou vous renommez sous un répertoire que vous pouvez lire mais pas écrire.
Correction : Préflight avec vérifications d’écriture sur les répertoires, pas seulement sur les fichiers. Sur Linux, la permission d’écriture du répertoire gouverne les renommages.
cr0x@server:~$ namei -l /data/projects/clientA/legal/Contract?.pdf
f: /data/projects/clientA/legal/Contract?.pdf
drwxr-xr-x root root /
drwxr-xr-x root root data
drwxr-xr-x root root projects
drwxr-x--- team legal clientA
dr-xr-x--- team legal legal
-rw-r----- team legal Contract?.pdf
Décision : Si le répertoire cible n’est pas inscriptible (dr-x), votre renommage échouera. Corrigez les perms du répertoire ou excluez ce sous-arbre.
5) Symptom : « Le rollback n’a pas tout restauré »
Cause : Pas de mappage complet, ou vous avez modifié le journal, ou l’exécution a eu des résultats mixtes (des temporaires PHASE1 laissés après un crash).
Correction : Traitez les journaux comme append-only. Stockez-les hors-hôte si vous êtes paranoïaque. Après toute erreur, inventoriezez les noms temporaires (.rename_tmp.) et résolvez-les avant de relancer.
cr0x@server:~$ find /data/projects/clientA -name '.rename_tmp.*' | head
/data/projects/clientA/inbox/.rename_tmp.1a2b3c4d.Report 1.txt
Décision : Si des temporaires existent, arrêtez les nouvelles exécutions. Finissez proprement la Phase 2 ou rollbackez.
6) Symptom : « L’application ne trouve plus les fichiers après le renommage »
Cause : L’application stockait des chemins absolus ; vous les avez changés sans mettre à jour les références.
Correction : Mettez à jour les références (BD/config) dans la même fenêtre de changement, ou conservez la compatibilité via des symlinks (attention : les symlinks ne fonctionnent pas partout et peuvent perturber les outils de sauvegarde).
Checklists / plan pas à pas
Checklist pré-vol (ne la sautez pas sauf si vous aimez les surprises)
- Confirmer le système de fichiers et le type de montage (
df -Th,mount). Décider si les métadonnées sont locales ou distantes. - Confirmer la portée : compter les fichiers, identifier les répertoires géants.
- Scanner les noms pathologiques : nouvelles lignes, tirets en tête, Unicode étrange. Décider si vous allez sanitiser ou préserver.
- Définir les règles de nommage : mise en minuscules ? espaces ? ponctuation ? gestion des extensions ? Définir ce qu’est « correct ».
- Politique de collision : défaut à arrêter sur collision ; utiliser suffix-hash seulement si nécessaire.
- Sauvegardes/snapshots : prendre un snapshot si possible ; sinon assurer que les sauvegardes suivront toujours les renommages.
- Geler les apports : arrêter l’arrivée de nouveaux fichiers, ou s’engager à une seconde passe.
Plan d’exécution (la partie à mettre dans un ticket de changement)
- Créer une liste de fichiers stable (NUL-délimitée, triée).
- Générer le TSV de plan ; revoir le rapport de collisions.
- Exécuter un canari sur 1–2k fichiers représentatifs.
- Valider un consommateur en aval (index de recherche, système de build, appli) avec les résultats du canari.
- Appliquer le renommage complet avec journalisation activée ; surveiller le journal.
- Après achèvement : rechercher des temporaires restants, des lignes d’erreur et des motifs de noms inattendus.
- Communiquer : publier l’emplacement du journal de renommage et les règles de normalisation utilisées.
Checklist de vérification post-renommage
- Erreurs : le comptage des entrées ERROR via
awkdoit être zéro ou expliqué. - Fichiers temporaires : aucun
.rename_tmp.*ne doit rester. - Résultats de collision : si suffix-hash a été utilisé, vérifier que les parties prenantes acceptent le format.
- Sanité des applications : les workflows clés résolvent toujours les chemins.
- Sanité des sauvegardes : le prochain run de sauvegarde ne doit pas réingérer tout comme « nouvelles données non reliées ».
cr0x@server:~$ awk -F'\t' '$2=="ERROR"{c++} END{print c+0}' /tmp/clientA.rename.log
0
Décision : Si non-zéro, décidez de corriger et relancer uniquement pour ces chemins (préféré) ou de rollbacker.
cr0x@server:~$ find /data/projects/clientA -name '.rename_tmp.*' | wc -l
0
Décision : Si non-zéro, vous avez eu une exécution interrompue ou une intervention manuelle. Résolvez avant de déclarer victoire.
Trois mini-récits d’entreprise des tranchées du renommage
Incident #1 : La mauvaise hypothèse (« rename est toujours atomique »)
La demande semblait anodine : « Normaliser les noms de fichiers dans le répertoire d’export en minuscules. » Le répertoire vivait sous /exports, et l’ingénieur a supposé que c’était un montage local normal. Il a écrit un script rapide qui déplaçait des fichiers dans un répertoire de staging (également sous /exports), les renommait, puis les déplaçait de retour. Cela fonctionnait bien en test.
En production, /exports était monté via autofs sur NFS, et le répertoire de staging était en réalité un point de montage différent sur un backend différent. Le « move » est devenu silencieusement copy+delete. En pleine exécution, le réseau s’est dégradé. La phase de copie a ralenti ; la phase de suppression a quand même eu lieu pour certains fichiers ; le script a retenté plusieurs fois, produisant un jardin de fichiers partiels et d’originaux manquants.
La suite n’était pas drôle : les jobs en aval ont vu des « nouveaux » fichiers avec de nouveaux noms, plus des fichiers attendus manquants. Leur logique de réconciliation était basée sur les chemins, pas le contenu, donc elle a signalé une pile de faux écarts. Les équipes financières ont été alertées. Personne n’a apprécié.
La correction n’était pas sophistiquée. Ils ont relancé l’opération comme un renommage en place pur dans un seul système de fichiers, après avoir confirmé les frontières de montage avec df -Th. Ils ont aussi ajouté un contrôle strict « refuser d’opérer si la destination est sur un device différent ». C’est le genre de garde-fou qu’on n’apprécie que quand on en a besoin.
Incident #2 : L’optimisation qui s’est retournée contre eux (« paralléliser »)
Une autre équipe avait des millions de petites images à renommer. Un ingénieur a vu une estimation en temps qui l’a fait tressaillir et a fait ce que font les ingénieurs : lancer des jobs parallèles. Pas juste quelques-uns — des dizaines de workers, chacun martelant mv et stat en boucle serrée.
Le stockage était un NAS partagé avec un serveur de métadonnées déjà occupé. Les renommages sont des opérations metadata, et les opérations metadata peuvent être les plus sérialisées de votre flotte. La course parallèle a transformé une file d’attente gérable en une horde tonitruante. La latence a flambé pour des workloads non liés. Les builds ont ralenti. Le pipeline CI de quelqu’un a expiré et a relancé, aggravant la situation.
Le comble : le temps d’achèvement total s’est à peine amélioré. La charge n’était pas limitée par le CPU ; elle était limitée par les métadonnées. Plus de workers signifiait juste plus d’attente en file tout en irritant tout le monde.
Ils se sont remis en réglant la concurrence à un petit nombre fixe, en regroupant les opérations et en mesurant le débit avec un canari avant de s’engager. L’« optimisation » n’était pas la parallélisation, mais le fait de ne pas lutter contre les goulots du stockage.
Incident #3 : La pratique ennuyeuse qui a sauvé la mise (snapshots + revue de plan)
Une organisation soumise à la conformité devait sanitiser les noms avant archivage : supprimer les espaces, ôter les caractères spéciaux, conserver les extensions. L’équipe a eu le bon sens de traiter cela comme un changement opérationnel, pas comme un tour de shell improvisé. Ils ont pris un snapshot, généré un fichier de plan et fait relire le rapport de collisions par un second ingénieur.
Pendant la revue, ils ont trouvé un motif : un sous-ensemble de fichiers utilisait la ponctuation pour encoder du sens (penser à des noms de type « A/B test »), et la sanitisation aurait aplati des catégories distinctes. Pas une perte de données — pire, une mauvaise étiquetage. Les noms existeraient toujours, mais la sémantique serait perdue.
Ils ont ajusté le jeu de règles pour préserver certains caractères et ajouté le suffix-hash seulement pour la poignée de collisions restantes. Puis ils ont exécuté un canari, validé qu’un job d’index interne correspondait toujours aux motifs attendus, et procédé à l’exécution complète. Le job d’archivage s’est ensuite terminé sans réingérer tout parce que le système de stockage reconnaissait les inodes ; seuls les noms ont changé.
Rien de glamour là-dedans. Personne n’a eu de trophée. Mais cela a évité le type de catastrophe au ralenti qui finit en salle de réunion avec un diaporama chronologique.
FAQ
1) Puis-je simplement utiliser la commande rename ?
Parfois. Mais il existe plusieurs implémentations de rename (basée Perl vs util-linux), des options différentes et des comportements très variables. Pour du travail en production, je préfère un script qui journalise chaque action et réalise explicitement la détection de collisions.
2) Pourquoi ne pas utiliser find ... -exec mv ?
Vous pouvez, mais il est facile de générer une surcharge de sous-processus par fichier et presque impossible de faire un dry-run significatif avec détection de collisions. Aussi, vous vous détesterez quand un nom comportant une nouvelle ligne brisera votre parsing de journal.
3) Le renommage va-t-il changer le contenu ou les timestamps des fichiers ?
Un renommage pur sur le même système de fichiers ne change pas le contenu. Il ne modifie généralement pas la mtime, mais la ctime (temps de changement d’inode) changera car les métadonnées changent. Si vous voyez des changements de contenu, vous avez probablement fait un copy+delete en traversant des systèmes de fichiers.
4) Qu’en est-il des liens physiques — vais-je dupliquer les données ?
Renommer ne duplique pas les données. Mais les liens physiques signifient que plusieurs noms pointent vers le même inode. Si vous renommez un des noms, les autres restent. Si vous attendiez « un seul fichier », vous pouvez trouver « un autre fichier » toujours présent sous un ancien nom.
5) Comment gérer les collisions en toute sécurité ?
Par défaut, arrêtez et corrigez vos règles. Si les exigences métiers demandent « sans revue humaine », utilisez une unicité déterministe (suffix-hash) et documentez le format. Évitez les suffixes aléatoires ; vous voulez des résultats reproductibles.
6) Comment faire un rollback ?
Le mieux : rollback via snapshot du système de fichiers (quand acceptable). Sinon : rejouer un mappage inverse depuis le journal. C’est pourquoi le script journalise RENAMED src original → dest final. Si vous n’avez pas de journal complet, le rollback devient de l’archéologie.
7) Faut-il exécuter ça en root ?
Seulement si c’est nécessaire. Root fait « disparaître » les problèmes de permission et les remplace par des problèmes de responsabilité. Préférez exécuter en tant que compte de service propriétaire des données, avec des droits explicites si besoin.
8) Comment garder les applications fonctionnelles après des renommages ?
Mettez à jour les références (BD/config) dans le même changement, ou utilisez une couche de compatibilité comme des symlinks. Les symlinks peuvent casser des outils et des hypothèses de sécurité, donc testez-les. Pour certains workflows, il vaut mieux mettre à jour l’application que masquer les chemins.
9) Mettre en minuscules les noms est-ce toujours une bonne idée ?
Non. C’est pratique, mais cela peut détruire du sens (codes produit, conventions humaines) et provoquer des collisions. Mettez en minuscules seulement si vous avez une raison et une stratégie de collision.
10) Comment gérer Unicode en toute sécurité ?
Au minimum, traitez les noms comme des octets dans votre pipeline et évitez les outils qui supposent du « texte imprimable ». Si vous devez normaliser Unicode, faites-le avec une bibliothèque dédiée et une détection de collisions. « Ça a l’air identique » n’est pas une garantie.
Étapes suivantes à effectuer réellement
- Choisir la politique : décidez vos règles de normalisation et le comportement en cas de collision. Notez-le dans le ticket.
- Geler la portée : générez une liste de fichiers triée et NUL-délimitée pour renommer exactement ce que vous avez examiné.
- Planifier d’abord : générez le TSV de mappage et examinez sérieusement les collisions.
- Snapshot si possible : snapshot ZFS, snapshot système de fichiers, ou au moins confirmer que les sauvegardes sont saines.
- Canari : 1–2k fichiers représentatifs, puis validez les consommateurs en aval.
- Exécution complète avec journal : taillez le journal, comptez les erreurs et recherchez les temporaires restants.
- Communiquer les changements : publiez le jeu de règles et le chemin du journal. Les gens demanderont « où est mon fichier » pendant des semaines.
Si vous retenez une chose : un renommage en masse sûr ne porte pas sur des transformations de chaînes astucieuses. Il s’agit de prouver que chaque chemin source mappe vers un unique chemin destination, sans collisions, avec rollback, et que le système de fichiers fait bien ce que vous pensez qu’il fait.