HowTo Bash

Script Bash: struttura base e buone pratiche

Come organizzare uno script shell pulito: variabili, controlli, log, gestione errori e permessi.

Uno script Bash funziona anche se "improvvisato", ma quando cresce diventa fragile. Una struttura chiara ti fa risparmiare tempo su bug, modifiche e manutenzione.

1. Perche' strutturare bene uno script

  • Piu' facile da leggere e modificare
  • Meno errori con spazi, variabili vuote e path strani
  • Debug piu' semplice
  • Riutilizzo di funzioni in altri script

2. Template base consigliato

Un buon punto di partenza per script Bash moderni:

#!/usr/bin/env bash
set -euo pipefail

SCRIPT_NAME="$(basename "$0")"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="/tmp/${SCRIPT_NAME%.sh}.log"

usage() {
  cat <<'EOF'
Uso: script.sh [opzioni]

Opzioni:
  -h, --help     Mostra questo help
  -v, --verbose  Output piu' dettagliato
EOF
}

log() {
  printf '[%s] %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG_FILE"
}

die() {
  log "ERRORE: $*"
  exit 1
}

main() {
  log "Avvio script: $SCRIPT_NAME"
  # logica principale qui
}

main "$@"
set -euo pipefail aiuta a intercettare errori presto, ma va usato con consapevolezza (specialmente con pipeline, comandi opzionali e test).

3. Variabili e costanti

Regole pratiche

  • Usa nomi chiari: backup_dir, source_file
  • Quote quasi sempre le espansioni: "$var"
  • Per costanti usa maiuscolo: DEFAULT_PORT=22
  • Per array usa Bash array, non stringhe concatenate
DEFAULT_PORT=22
backup_dir="/var/backups/app"
files=("config.yml" "db.sql" "notes.txt")

for f in "${files[@]}"; do
  printf 'File: %s\n' "$f"
done
Evita for f in $(ls ...): rompe facilmente con spazi e caratteri speciali nei nomi file.

4. Funzioni e organizzazione del codice

Separa il codice in funzioni piccole con una responsabilita' chiara.

check_requirements() {
  command -v rsync >/dev/null 2>&1 || die "rsync non trovato"
}

run_backup() {
  local src="$1"
  local dst="$2"
  rsync -av --delete "$src/" "$dst/"
}

main() {
  check_requirements
  run_backup "/etc" "/backup/etc"
}

Ordine consigliato nel file:

  1. Shebang + opzioni shell
  2. Costanti/variabili globali
  3. Funzioni utility (log, die, usage)
  4. Funzioni di business
  5. main
  6. Chiamata finale: main "$@"

5. Argomenti, opzioni e help

Gestisci gli argomenti in modo esplicito. Esempio semplice con case:

VERBOSE=0
TARGET_DIR=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    -h|--help)
      usage
      exit 0
      ;;
    -v|--verbose)
      VERBOSE=1
      shift
      ;;
    -t|--target)
      [[ $# -lt 2 ]] && die "Valore mancante per $1"
      TARGET_DIR="$2"
      shift 2
      ;;
    *)
      die "Argomento non riconosciuto: $1"
      ;;
  esac
done

[[ -z "$TARGET_DIR" ]] && die "Devi specificare --target"
Per script piu' complessi puoi usare getopts (opzioni corte) o un parser dedicato, ma il pattern con while + case e' spesso sufficiente.

6. Gestione errori (best practice reali)

Controlla le dipendenze prima

command -v curl >/dev/null 2>&1 || die "curl non installato"
command -v jq   >/dev/null 2>&1 || die "jq non installato"

Non ignorare i codici di uscita

if ! cp "$src" "$dst"; then
  die "Copia fallita: $src -> $dst"
fi

Valori di default sicuri

: "${TMPDIR:=/tmp}"
: "${BACKUP_KEEP_DAYS:=7}"

Evita effetti collaterali pericolosi

Prima di fare operazioni distruttive (rm, mv, sync), stampa il target e valida che non sia vuoto:
[[ -n "${target_dir:-}" ]] || die "target_dir vuota"
[[ "$target_dir" != "/" ]] || die "Rifiutato: target_dir e' /"

7. Logging semplice ma utile

Usa funzioni dedicate per log con livelli.

log_info()  { printf '[INFO]  %s\n' "$*"; }
log_warn()  { printf '[WARN]  %s\n' "$*" >&2; }
log_error() { printf '[ERROR] %s\n' "$*" >&2; }

log_info "Backup avviato"
log_warn "Spazio disco basso"
log_error "Connessione fallita"

Se vuoi salvare anche su file:

exec > >(tee -a "$LOG_FILE")
exec 2>&1
Questo redirige stdout/stderr a tee. Utile per script di manutenzione, meno adatto se vuoi output "pulito" per parsing automatico.

8. Trap e cleanup

Quando crei file temporanei o lockfile, usa trap per pulire anche in caso di errore/interruzione.

TMP_FILE="$(mktemp)"

cleanup() {
  rm -f "$TMP_FILE"
}

trap cleanup EXIT
trap 'echo "Interrotto"; exit 130' INT TERM

9. Permessi e sicurezza

  • Rendi eseguibile solo se serve: chmod +x script.sh
  • Verifica se richiede root all'inizio dello script
  • Non salvare password in chiaro nello script
  • Usa umask se generi file sensibili
require_root() {
  [[ "${EUID:-$(id -u)}" -eq 0 ]] || die "Esegui come root"
}
Se usi input esterno (argomenti, file, output di comandi), trattalo come non fidato: quota sempre e valida il formato.

10. Debug, lint e test

Debug rapido

bash -x ./script.sh --target /tmp/test
bash -n ./script.sh
  • -x: traccia i comandi eseguiti
  • -n: controlla solo la sintassi

ShellCheck (fortemente consigliato)

shellcheck script.sh

Ti segnala errori comuni, quote mancanti, uso errato di variabili e pattern fragili.

Mini checklist finale

[ ] Shebang corretto (#!/usr/bin/env bash)
[ ] set -euo pipefail (se adatto allo script)
[ ] Variabili quotate
[ ] Argomenti validati
[ ] Error handling con die()/exit code
[ ] Trap cleanup se uso file temporanei
[ ] bash -n e shellcheck eseguiti

Template finale riutilizzabile

#!/usr/bin/env bash
set -euo pipefail

usage() { echo "Uso: $0 --target DIR"; }
die() { echo "ERRORE: $*" >&2; exit 1; }

TARGET=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --target) TARGET="${2:-}"; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) die "Argomento non riconosciuto: $1" ;;
  esac
done

[[ -n "$TARGET" ]] || die "Specifica --target"
[[ -d "$TARGET" ]] || die "Directory non trovata: $TARGET"

main() {
  echo "Operazione su: $TARGET"
}

main "$@"

Torna alla sezione HowTo Linux