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 -e termina quando un comando semplice fallisce, tranne in casi come test if, parti di liste && e ||, e alcune sostituzioni di comando.
  • set -u termina quando espandi una variabile non impostata.
  • set -o pipefail fa 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.