Corso Linux / Gestire gli errori Bash e verificare una copia di file
Uno script di copia più affidabile controlla origine e destinazione, gestisce esplicitamente gli errori ed evita di presentare un risultato parziale come completo. Non affidarti soltanto a set -e: definisci il significato di ogni errore, quando rifiutare una sovrascrittura e come eliminare i dati temporanei.
Questa lezione estende il primo script Bash con rami di errore e limiti delle operazioni sui file. Per le regole di &&, || e delle pipeline consulta il flusso di esecuzione della shell.
Definire le condizioni del laboratorio
L'esempio copia un file ordinario con un nuovo nome in una directory affidabile sotto il tuo controllo e confronta il contenuto. La directory di destinazione deve esistere e non deve essere modificabile da utenti non attendibili. Non è una difesa contro modifiche ostili e concorrenti dei percorsi, né uno strumento per il backup dei database. Eseguilo senza privilegi amministrativi, su dati di prova e con una sorgente che rimane invariata.
Servono Bash e le versioni GNU di mktemp, dirname, cp, ln, rm, cmp e find. Controlla la disponibilità con command -v bash mktemp dirname cp ln rm cmp find e registra le versioni. Il file system deve supportare i collegamenti fisici, detti anche hard link; l'opzione ln -T usata qui è quella GNU.
Creare lo script di copia con verifica
Il blocco crea una directory temporanea nella home, vi entra e prepara il file di origine e checked-copy.sh. Le prime operazioni sono collegate da &&: un errore nella preparazione impedisce di proseguire con la creazione dello script nella directory sbagliata. Controlla il risultato e pwd prima delle ultime tre righe: queste sono comandi separati, quindi devi fermarti se il controllo sintattico o la copia falliscono. Mantieni lo stesso terminale per le prove successive e salva il file con terminatori di riga LF.
lab=$(mktemp -d "$HOME/linux-safe-copy.XXXXXX") &&
cd "$lab" &&
mkdir output &&
printf "important sample\n" > "source file.txt" &&
cat > checked-copy.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail
umask 077
if (( $# != 2 )); then
printf "Usage: %s SOURCE DESTINATION\n" "$0" >&2
exit 2
fi
source_file=$1
destination=$2
if [[ ! -f "$source_file" || -L "$source_file" ]]; then
printf "Source must be a regular non-symlink file\n" >&2
exit 1
fi
if [[ -e "$destination" || -L "$destination" ]]; then
printf "Destination already exists\n" >&2
exit 1
fi
parent=$(dirname -- "$destination")
if [[ ! -d "$parent" ]]; then
printf "Destination directory is missing\n" >&2
exit 1
fi
work=$(mktemp -d "$parent/.copy-work.XXXXXX") || exit 1
cleanup() { rm -rf -- "$work"; }
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if ! cp -- "$source_file" "$work/payload"; then
printf "Copy failed\n" >&2
exit 1
fi
if ! cmp -s -- "$source_file" "$work/payload"; then
printf "Verification failed\n" >&2
exit 1
fi
if ! ln -T -- "$work/payload" "$destination"; then
printf "Destination creation failed\n" >&2
exit 1
fi
printf "Created: %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"
Con i dati iniziali attenditi Created: output/copy file.txt. bash -n verifica la sintassi senza eseguire; cmp non stampa nulla e termina con 0 quando i contenuti coincidono. Leggi $? subito dopo il comando da verificare. Per cmp, 1 indica dati diversi e 2 un problema di confronto: nessuno dei due significa copia verificata.
Lo script controlla il numero di argomenti prima di leggere $1 e $2, perché set -u tratta come errore molte espansioni di variabili non definite. umask 077 limita su Linux i permessi iniziali dei nuovi oggetti; non modifica quelli di file già esistenti. Il controllo della sorgente rifiuta i collegamenti simbolici. La coppia -e e -L rifiuta anche una destinazione costituita da un collegamento simbolico interrotto.
Il programma crea una directory temporanea con GNU mktemp nella directory di destinazione, vi copia il contenuto e confronta la copia. Solo dopo crea il nome finale mediante un hard link. Il file temporaneo e la destinazione si trovano sullo stesso file system, condizione necessaria per questo collegamento. Il collegamento finale fa riferimento alla copia, non direttamente al file originale.
ln -T -- tratta il secondo percorso come nome finale, anche se nel frattempo identifica una directory: non crea un link al suo interno. Senza -f non sostituisce un nome già esistente. Questa scelta evita il comportamento “copia dentro una directory”, ma non rende sicuri percorsi i cui componenti o directory superiori possono essere sostituiti da un aggressore. Se il file system non supporta hard link, l'ultima fase deve fallire: non aggirarla automaticamente con una copia che cambi le garanzie del programma.
Il gestore EXIT rimuove il nome temporaneo; dopo una creazione riuscita il collegamento di destinazione rimane. rm -rf riceve soltanto la directory di lavoro appena ottenuta da mktemp, mai l'origine o la destinazione fornite dall'utente. Se la rimozione fallisce, i suoi messaggi non vanno ignorati: questo piccolo gestore non ha una politica di ritentativi né trasforma automaticamente una copia riuscita in un errore di pulizia.
Il confronto verifica i byte in quel momento, non la proprietà, gli elenchi di controllo degli accessi (ACL), gli attributi estesi o la persistenza fisica dopo un'interruzione di alimentazione. Una sorgente che cambia durante o dopo il confronto può invalidare l'interpretazione del risultato; serve una procedura diversa per dati attivi o coerenti a livello applicativo.
Provare deliberatamente le condizioni di errore
bash checked-copy.sh "source file.txt" "output/copy file.txt"
printf "exit=%s\n" "$?"
bash checked-copy.sh missing.txt output/new.txt
printf "exit=%s\n" "$?"
find output -maxdepth 1 -name ".copy-work.*" -print
La prima chiamata ripete il nome già creato e deve terminare con 1 senza alterare la destinazione. Anche una sorgente inesistente deve produrre stato 1; senza i due argomenti richiesti attenditi invece 2. Per provare un'altra condizione, usa bash checked-copy.sh "source file.txt" "missing-parent/new.txt" dopo aver verificato che missing-parent non esista: deve fallire senza creare la directory.
Dopo una conclusione normale o un errore gestito, find non deve elencare directory .copy-work.*. Le due prove mostrate rifiutano l'input prima di creare il temporaneo: da sole non verificano i rami di pulizia dopo un errore di copia, confronto o collegamento. Per controllare anche questi rami, in un laboratorio isolato si possono simulare separatamente i comandi che falliscono e verificare stato 1, assenza della nuova destinazione e rimozione del temporaneo. Una simulazione non dimostra il comportamento di un vero guasto del disco.
I gestori scelgono stato 130 per INT e 143 per TERM, poi viene eseguita anche la pulizia EXIT. I tempi di gestione dei segnali possono dipendere dai comandi in esecuzione: non presumere un'interruzione istantanea. Con SIGKILL o una perdita di alimentazione i trap non possono eseguire la pulizia; occorre una politica separata per i residui. Al termine delle prove verifica di nuovo con cmp "source file.txt" "output/copy file.txt" che la prima copia sia ancora identica.
Capire i limiti di errexit e pipefail
bash -c 'false | cat; printf "status=%s\n" "$?"'
bash -o pipefail -c 'false | cat; printf "status=%s\n" "$?"'
La prima pipeline stampa normalmente status=0, perché l'ultimo comando, cat, riesce. La seconda stampa status=1: pipefail rende visibile il fallimento di false. Più in generale, con questa opzione lo stato è quello del comando più a destra che termina con un valore diverso da zero, oppure 0 se riescono tutti. Le due shell possono comunque terminare con 0 perché l'ultimo printf riesce: distingui lo stato stampato della pipeline da quello dell'intero comando bash -c.
pipefail modifica la propagazione degli stati, non ripristina i dati e non interrompe da solo lo script. set -e, chiamato anche errexit, ha eccezioni legate a condizioni, liste &&/||, negazione e pipeline. Per esempio, il test false in bash -e -c 'if false; then :; fi; printf "continued\n"' non impedisce la stampa di continued. Devi verificare il contesto preciso in cui ti aspetti l'arresto, anche quando chiami funzioni.
if ! command; then semplifica il ramo di errore, ma in quel ramo $? riflette la negazione e vale 0 quando il comando originale è fallito. Se ti serve il codice originale, salvalo subito nel ramo alternativo: if command; then ...; else rc=$?; ...; fi. Per esempio bash -c 'if (exit 7); then :; else rc=$?; printf "rc=%s\n" "$rc"; fi' deve stampare rc=7. Definisci codici e messaggi che consentano al chiamante di distinguere i casi.
Controllare le ipotesi prima dell'automazione
- Proteggi le espansioni con le virgolette e verifica quali comandi accettano
--prima degli argomenti che possono iniziare con un trattino. - Se introduci una modalità di anteprima, separala da quella che modifica i dati e verifica che l'anteprima non scriva file. Lo script qui presentato esegue la copia: non ha una modalità di anteprima.
- Esamina proprietà e permessi delle directory, collegamenti simbolici, modifiche concorrenti dei percorsi e della sorgente. I controlli Bash sui nomi non sono una difesa completa dalle corse tra controllo e uso.
- Prova successo, errori e segnali gestiti separatamente. Conserva backup recuperabili: la copia verificata non sostituisce una strategia di backup.
I test con Git Bash e gli strumenti GNU su Windows possono verificare la logica e un hard link sul file system usato, ma non dimostrano i permessi Linux di umask, gli ACL o la resistenza a guasti reali. Per una procedura operativa ripeti le prove nel sistema Linux e nel file system di destinazione. Prima di ripulire il laboratorio controlla il percorso e rimuovi soltanto i dati creati per la prova.




