Trappole Comuni dello Scripting Bash e Come Evitarle
Evita i comuni bug dello scripting Bash con una gestione degli errori più sicura, quoting, array, trap e parsing degli argomenti.
Errori Comuni nello Scripting Bash e Come Evitarli
Gli errori nello scripting Bash di solito si manifestano quando il tuo script incontra nomi di file reali, variabili mancanti, comandi falliti o input imprevisti. Uno script che funziona sul tuo portatile può rompersi in CI o in produzione se si basa su impostazioni predefinite lasche.
Non è necessario rendere ogni script shell complicato. Devi però quotare le espansioni, controllare intenzionalmente i fallimenti e testare con nomi che contengono spazi.
Imposta Impostazioni Predefinite Più Sicure con Cura
Molti script iniziano con:
#!/usr/bin/env bash
set -euo pipefail
Questa è una buona base per molti script di automazione, ma ogni opzione ha i suoi lati taglienti:
set -etermina quando un comando semplice fallisce, tranne in casi come testif, parti di liste&&e||, e alcune sostituzioni di comando.set -utermina quando espandi una variabile non impostata.set -o pipefailfa fallire una pipeline se un qualsiasi comando nella pipeline fallisce, non solo l'ultimo comando.
Usa queste opzioni quando un fallimento precoce è più sicuro che continuare. Per comandi in cui il fallimento è previsto, gestisci esplicitamente lo stato.
if ! grep -q "ready" status.txt; then
echo "il servizio non è ancora pronto"
exit 1
fi
Cita le Espansioni delle Variabili
Le variabili non quotate sono il bug Bash più comune. Bash esegue la suddivisione in parole e l'espansione dei glob sulle espansioni non quotate, quindi un percorso come release notes/*.txt può diventare diversi argomenti o corrispondere a file che non intendevi.
file="release notes.txt"
# Male: si rompe perché il valore viene suddiviso in due parole.
rm $file
# Bene: passa un argomento esatto.
rm -- "$file"
Usa -- prima dei nomi di file controllati dall'utente quando un comando lo supporta. Questo impedisce che un nome file come -rf venga interpretato come un'opzione.
Usa Array per Liste di Argomenti
Non memorizzare un comando con argomenti in una singola stringa e poi eseguirlo. La quotatura diventa rapidamente fragile.
# Male
flags="-a --exclude node_modules"
rsync $flags "$src" "$dest"
# Bene
flags=(-a --exclude "node_modules")
rsync "${flags[@]}" "$src" "$dest"
Gli array preservano i confini degli argomenti. Questo è importante quando un argomento contiene spazi, caratteri wildcard o valori che iniziano con un trattino.
Preferisci $(...) ai Backtick
I backtick sono difficili da annidare e facili da leggere male. Usa $(...) per la sostituzione di comando.
current_branch="$(git rev-parse --abbrev-ref HEAD)"
echo "costruendo il branch: $current_branch"
Mantieni quotate le sostituzioni di comando a meno che non desideri deliberatamente la suddivisione in parole.
Leggi i File Senza Perdere Dati
Questo pattern sembra innocuo ma si rompe con gli spazi e può alterare i backslash:
for line in $(cat hosts.txt); do
echo "$line"
done
Leggi i file con while IFS= read -r invece.
while IFS= read -r host; do
echo "controllando $host"
done < hosts.txt
IFS= preserva gli spazi iniziali e finali. -r impedisce che le sequenze di escape con backslash vengano interpretate.
Gestisci i File Temporanei con mktemp e trap
Percorsi temporanei hardcodati possono entrare in conflitto con un altro processo o lasciare file obsoleti. Crea un percorso unico e puliscilo all'uscita.
tmp_file="$(mktemp)"
cleanup() {
rm -f "$tmp_file"
}
trap cleanup EXIT
printf '%s\n' "dati di lavoro" > "$tmp_file"
Per le directory, usa mktemp -d e rimuovi la directory nella tua funzione di pulizia.
Analizza le Opzioni con getopts
L'analisi manuale degli argomenti spesso perde casi limite. Per le opzioni brevi, il getopts integrato di Bash è solitamente sufficiente.
verbose=false
output=""
while getopts ":vo:" opt; do
case "$opt" in
v) verbose=true ;;
o) output="$OPTARG" ;;
:)
echo "L'opzione -$OPTARG richiede un argomento" >&2
exit 2
;;
\?)
echo "Opzione sconosciuta: -$OPTARG" >&2
exit 2
;;
esac
done
shift "$((OPTIND - 1))"
getopts gestisce flag brevi come -v e -o file. Se il tuo script necessita di opzioni lunghe come --output, scrivi un parser accurato o usa un linguaggio con una libreria di parsing degli argomenti più robusta.
Controlla i Comandi che Possono Fallire
Non dare per scontato che un comando abbia funzionato perché ha stampato qualcosa. Controlla le operazioni importanti prima di usare il loro output.
if ! archive="$(tar -czf app.tar.gz app 2>&1)"; then
echo "archiviazione fallita: $archive" >&2
exit 1
fi
Per le pipeline, abilita pipefail quando un fallimento nel mezzo dovrebbe far fallire l'intera pipeline.
set -o pipefail
journalctl -u api.service | grep -i "error"
Senza pipefail, lo stato della pipeline normalmente proviene dall'ultimo comando.
Evita Bash Quando la Portabilità è Importante
Se il tuo script usa array, [[ ... ]], mapfile o pipefail, è uno script Bash. Inizialo con:
#!/usr/bin/env bash
Se hai bisogno di portabilità POSIX sh, evita le funzionalità solo Bash e testa con la shell utilizzata dal tuo sistema di destinazione. Non scrivere uno script Bash con #!/bin/sh e sperare che si comporti allo stesso modo ovunque.
Conclusione
Il modo più veloce per migliorare i tuoi script Bash è testarli con input disordinati: spazi nei nomi dei file, variabili mancanti, file vuoti e comandi che falliscono. Cita le espansioni, usa array per le liste di argomenti, pulisci i file temporanei con trap e rendi espliciti i percorsi di fallimento. Il tuo futuro te stesso passerà meno tempo a fare debug di script che funzionavano solo con input perfetti.