Cours Linux / Gérer les erreurs Bash et copier un fichier sans écrasement
Pour copier un fichier par script sans écraser une destination existante, vérifiez les deux chemins, créez une copie provisoire dans le dossier cible, comparez les contenus, puis publiez-la sous un nom neuf. Gérez chaque échec explicitement et nettoyez seulement le nom temporaire créé par le script. set -e ne remplace pas ces décisions.
Cette leçon prolonge le premier script Bash avec arguments et boucle et les règles d’exécution des commandes Bash. L’atelier agit sous votre dossier personnel, sans sudo, sur des fichiers de pratique. Il suppose un dossier cible fiable, non modifié simultanément par un autre utilisateur ou processus. Il n’est pas conçu comme un outil de sécurité contre un acteur capable de remplacer les chemins entre deux contrôles.
Définir ce que la copie doit garantir
Le programme prend une source et un nom de destination. Il refuse une source absente ou qui est elle-même un lien symbolique, refuse une destination déjà présente, copie dans un sous-dossier temporaire placé dans le dossier cible, puis utilise un lien matériel pour donner un nom définitif à la copie vérifiée. Le dossier cible doit déjà exister. Le programme ne traite qu’un fichier ordinaire : il ne préserve ni un ensemble de fichiers, ni les listes de contrôle d’accès (ACL), ni les attributs étendus, ni une base de données en cours d’écriture.
Un lien matériel est un second nom pour les mêmes données sur un système de fichiers. Cette dernière opération exige que le nom temporaire et la destination soient sur le même système de fichiers ; ils sont ici créés dans le même dossier parent. Certains systèmes de fichiers ou politiques n’autorisent pourtant pas les liens matériels. Si ln échoue, l’exercice s’arrête sans substituer automatiquement une opération qui pourrait écraser une autre cible.
Préparer les fichiers et enregistrer le script complet
Vérifiez la disponibilité de Bash et des outils mktemp, cp, cmp, ln, rm, rmdir et dirname. Dans un même terminal, créez un nouveau dossier sous votre dossier personnel. La chaîne && empêche l’écriture du fichier source si une étape antérieure échoue ; lisez le chemin affiché avant de poursuivre.
lab=$(mktemp -d "$HOME/linux-safe-copy.XXXXXX") &&
cd "$lab" &&
mkdir -- output &&
printf 'important sample\n' > "source file.txt" &&
printf 'Dossier : %s\n' "$lab"
Copiez ensuite le bloc entier ci-dessous dans le même terminal. Le délimiteur 'EOF' empêche le shell courant d’interpréter les variables avant que le fichier ne soit exécuté. bash -n ne détecte que les erreurs de syntaxe. L’appel final doit annoncer output/copy file.txt ; cmp doit se terminer sans sortie et avec l’état 0.
cat > checked-copy.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail
umask 077
if (( $# != 2 )); then
printf 'Utilisation : %s SOURCE DESTINATION\n' "$0" >&2
exit 2
fi
source_file=$1
destination=$2
if [[ ! -f "$source_file" || -L "$source_file" ]]; then
printf 'La source doit être un fichier ordinaire sans lien symbolique\n' >&2
exit 1
fi
if [[ -e "$destination" || -L "$destination" ]]; then
printf 'La destination existe déjà\n' >&2
exit 1
fi
parent=$(dirname -- "$destination") || exit 1
if [[ ! -d "$parent" || -L "$parent" ]]; then
printf 'Dossier de destination absent ou symbolique\n' >&2
exit 1
fi
work=$(mktemp -d "$parent/.copy-work.XXXXXX") || exit 1
cleanup() {
result=$?
trap - EXIT
if [[ -d "$work" && ! -L "$work" ]]; then
if [[ -e "$work/payload" || -L "$work/payload" ]]; then
rm -- "$work/payload" || result=1
fi
rmdir -- "$work" || result=1
else
printf 'Travail temporaire introuvable ou changé : %s\n' "$work" >&2
result=1
fi
exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if ! cp -- "$source_file" "$work/payload"; then
printf 'Échec de la copie provisoire\n' >&2
exit 1
fi
if ! cmp -s -- "$source_file" "$work/payload"; then
printf 'Copie non identique ou comparaison impossible\n' >&2
exit 1
fi
if ! ln -- "$work/payload" "$destination"; then
printf 'Création de la destination refusée\n' >&2
exit 1
fi
printf 'Créé : %s\n' "$destination"
EOF
bash -n checked-copy.sh
bash checked-copy.sh "source file.txt" "output/copy file.txt"
cmp -- "source file.txt" "output/copy file.txt"
umask 077 restreint les permissions des nouveaux fichiers et dossiers selon le système. Les guillemets gardent source file.txt comme un seul argument. -- sépare les options des noms dans les outils qui le prennent en charge : un nom commençant par un tiret ne devient pas une option. Les contrôles -e et -L détectent une destination existante, même si elle est un lien symbolique rompu. Les chemins appartiennent à votre atelier ; ne fournissez pas un répertoire partagé où quelqu’un pourrait changer les entrées pendant l’exécution.
Comprendre la copie provisoire et son nettoyage
mktemp -d crée un sous-dossier unique dans output. Après cp, cmp -s exige un contenu identique avant l’appel à ln. Si la comparaison ne peut pas se faire, le programme traite aussi cela comme un échec. ln sans option de remplacement refuse normalement de créer le nouveau nom si celui-ci existe déjà : une nouvelle vérification intervient donc au moment de la création, en plus du contrôle initial. Consultez les manuels officiels de mktemp et de ln pour les contraintes de ces opérations.
À la sortie, trap lance cleanup. Cette fonction ne vise que payload sous le dossier temporaire enregistré, puis ne retire ce dossier qu’avec rmdir s’il est vide. Elle ne supprime récursivement ni la source ni la destination. Après un lien matériel réussi, enlever le nom provisoire laisse le nom définitif et ses données. Si le nettoyage échoue, le script renvoie un état d’échec et conserve le chemin de pratique pour inspection. Un signal SIGKILL ou une panne électrique ne lance pas le piège EXIT ; les restes éventuels demandent alors une inspection manuelle. La référence Bash sur trap distingue ces cas.
Cette procédure ne promet pas une écriture durable sur le support physique ni un instantané cohérent si la source change entre la copie et la comparaison. Une politique de sauvegarde ou une modification de fichier partagée demande aussi une gestion des processus concurrents et des garanties adaptées au système de fichiers.
Faire échouer volontairement les deux entrées
Refaites deux appels dans le dossier de pratique et lisez les états immédiatement. Le premier essaie de créer un nom déjà présent : il doit échouer sans modifier la copie existante. Le second donne une source absente : il doit échouer avant toute copie. Vérifiez ensuite qu’aucun dossier temporaire n’est resté dans output.
bash checked-copy.sh "source file.txt" "output/copy file.txt"
printf 'état=%s\n' "$?"
bash checked-copy.sh missing.txt output/new.txt
printf 'état=%s\n' "$?"
cmp -- "source file.txt" "output/copy file.txt"
find output -maxdepth 1 -name '.copy-work.*' -print
Les deux appels doivent renvoyer 1 dans cet exemple. cmp doit rester silencieux avec l’état 0 et find ne doit rien afficher. Un dossier résiduel ne doit pas être effacé aveuglément : examinez son chemin et son contenu, surtout si le programme a été interrompu. Les conventions numériques 1 et 2 sont définies ici par le script ; le nombre exact d’un outil externe peut différer.
Observer pipefail et les limites de set -e
Une pipeline renvoie d’ordinaire l’état de sa dernière commande. Les deux essais suivants n’écrivent aucun fichier. Ils montrent pourquoi pipefail peut révéler une erreur antérieure dans la pipeline ; il ne répare pas les données ni ne décide de la suite à votre place.
bash -c 'false | cat; printf "état=%s\n" "$?"'
bash -o pipefail -c 'false | cat; printf "état=%s\n" "$?"'
Attendez état=0 puis état=1. set -e, appelé errexit, possède des exceptions dans les conditions, les listes && et ||, les pipelines et d’autres contextes. Il faut tester les branches importantes explicitement, comme if ! cp …; then. Dans ce dernier cas, $? reflète la négation ; pour garder le code original, utilisez if commande; then …; else rc=$?; …; fi. Le manuel Bash de set, errexit et pipefail décrit ces règles.
Adapter les contrôles avant une automatisation réelle
Sur un répertoire partagé ou un système sensible, l’attaquant ou un autre processus peut changer une entrée entre [[ -f ]], cp, cmp et ln. Une vérification de chemin suivie d’une opération n’est pas une transaction atomique. Déterminez qui peut écrire dans chaque dossier, vérifiez les liens symboliques et les permissions, puis utilisez des interfaces conçues pour vos garanties de concurrence. Ne donnez pas des droits administrateur à ce script d’initiation pour contourner un refus.
Avant de réutiliser ce principe dans une tâche planifiée, testez séparément succès, source absente, destination présente, échec de copie, échec de comparaison, signal et nettoyage. Préservez les sauvegardes nécessaires et distinguez prévisualisation et changement réel. Une copie vérifiée octet par octet n’établit toujours pas qu’une application sait reprendre son travail avec le fichier produit.




