Guida alle tecnologie

Linee guida per scrivere script Bash sicuri e mantenibili

9 min read Scripting Aggiornato 22 Oct 2025
Bash sicuro: best practice per script robusti
Bash sicuro: best practice per script robusti

Tastiera di un PC Linux che mostra alcuni caratteri speciali.

Bash è potente, ma con il potere viene anche la responsabilità. Codice disordinato o non progettato può provocare danni reali: cancellare file, sovrascrivere dati o avviare processi inaspettati. È quindi fondamentale praticare una programmazione difensiva.

Fortunatamente Bash offre diversi meccanismi integrati per proteggerti. Molti consistono in miglioramenti sintattici che sostituiscono metodi più vecchi e problematici. Le raccomandazioni seguenti riducono la probabilità di bug, aiutano il debug e gestiscono i casi limite.

Perché seguire queste regole

Seguire convenzioni semplici migliora sicurezza, manutenzione e collaborazione. Script leggibili sono meno soggetti ad errori umani, più facili da rivedere e più sicuri in produzione.

Importante: nessuna regola è universale — valuta il contesto e documenta le scelte critiche.

1. Usa una shebang chiara

La prima riga di uno script dovrebbe dichiarare quale interprete eseguirà il file. Questo rende lo script autonomo e comunica il linguaggio.

Due approcci comuni:

  • Percorso assoluto all’interprete:
#!/bin/bash
echo "Hello, world"
  • Usare env per maggiore portabilità:
#!/usr/bin/env bash
echo "Hello, world"

Vantaggi e compromessi:

  • /bin/bash garantisce l’esecuzione di una specifica installazione di Bash (può essere più sicuro se controlli quel binario).
  • /usr/bin/env bash è più portabile: usa la versione di bash trovata nella PATH dell’utente, quindi funziona su sistemi con layout diversi.

Scegli in base al tuo ambiente: se distribuisci uno script a molti sistemi eterogenei, preferisci env; in contesti chiusi con policy di sicurezza, il percorso assoluto può essere preferibile.

2. Metti sempre le variabili tra virgolette

Lo spazio bianco in Linux separa argomenti. Se non si quotano le variabili, uno spazio interno a un valore può essere interpretato come più argomenti.

Esempio problematico:

#!/bin/bash

FILENAME="docs/Letter to bank.doc"
ls $FILENAME

Bash espande $FILENAME letteralmente, provocando che ls riceva più argomenti. Soluzione:

ls "$FILENAME"

Usare le parentesi graffe rende il codice più robusto quando concateni testo:

echo "_${FILENAME}_ è uno dei miei file preferiti"

La sintassi ${VAR} impedisce ambiguità quando il nome della variabile è seguito da caratteri alfanumerici.

3. Ferma lo script in caso di errore

Non ignorare i fallimenti: aggiungi un attributo di sicurezza globale all’inizio:

set -e
set -o pipefail
  • set -e: esce immediatamente se un comando termina con stato non zero (salvo eccezioni volontarie).
  • set -o pipefail: garantisce che una pipeline termini con stato non zero se anche solo uno dei comandi interni fallisce.

Esempio:

#!/bin/bash
set -e
set -o pipefail

touch /file
echo "Ora fai qualcosa con quel file..."

Senza controlli, comandi che falliscono possono essere ignorati e portare a conseguenze gravi.

Nota: set -e ha eccezioni (condizioni dentro if, &&/||, while, etc.). Comprendi le eccezioni se usi codice complesso.

4. Gestisci gli errori esplicitamente

Oltre a fallire globalmente, gestisci fallimenti specifici dove serve: ispeziona lo stato di uscita o usa operatori logici.

Controllo esplicito:

cd "$DIR"
if [ $? -ne 0 ]; then
  echo "Impossibile entrare in $DIR" >&2
  exit 1
fi

Forma compatta:

cd "$DIR" || { echo "cd fallito" >&2; exit 1; }

Suggerimento: preferisci gli operatori logici e i blocchi { …; } per mantenere comportamenti coerenti con set -e.

5. Debug: traccia i comandi con xtrace

Per capire cosa fa lo script, attiva:

set -o xtrace
# o: set -x

Questo fa stampare ogni comando espanso prima dell’esecuzione, utile per debug.

Output di uno script con xtrace che mostra i comandi eseguiti (date e ls).

Usalo in combinazione con trap per disattivare il debug in sezioni sensibili (es. password) e con LOGGING per salvare l’output di debug.

6. Preferisci opzioni lunghe per chiarezza

Molti comandi usano opzioni a singola lettera (-r), ma le opzioni lunghe (–recursive) migliorano leggibilità dello script:

rm --recursive --force filename

Le opzioni lunghe sono auto-documentanti: in script condivisi favoriscile.

7. Usa la notazione moderna per la sostituzione di comandi

Evita la vecchia sintassi con backtick che è deprecata e più difficile da nidificare. Preferisci:

VAR=$(ls)

La forma $(…) è più leggibile e nidificabile.

8. Valori di default per variabili

Puoi definire valori di fallback senza codice aggiuntivo:

CMD=${PAGER:-more}
DIR=${1:-${HOME:-/home/utente}}

Questa sintassi è utile per supportare argomenti, poi variabili d’ambiente, quindi un default.

9. Usa la doppia linea “–“ per separare opzioni e argomenti

Se un file ha un nome che inizia con “-“ può essere interpretato come opzione. Usa “–“ per indicare che tutto dopo è un argomento:

rm -- *.md

Questo evita scenari catastrofici (es. rm * con un file chiamato -rf).

Una sessione terminale che crea un file con trattino iniziale e ne conferma l'esistenza con ls.

Errore di ls che mostra

10. Dichiarare variabili locali nelle funzioni

Per default le variabili in Bash sono globali, anche dentro le funzioni. Questo può causare sovrascritture involontarie.

Esempio pericoloso:

#!/bin/bash

function run {
  DIR=$(pwd)
  echo "doing something..."
}

DIR="/usr/local/bin"
run
echo $DIR

Soluzione: usare local:

function run {
  local DIR=$(pwd)
  # altra logica
}

Localizza denominate variabili per ridurre il rischio di effetti collaterali.

Quando queste regole falliscono: controesempi e limiti

  • set -e non ferma sempre lo script: comandi in ‘if’, ‘while’, parte destra di &&/|| non sono sempre catturati. Non fare affidamento esclusivamente su set -e per la logica critica.
  • Variabili locali non risolvono conflitti di nome trasversali per processi paralleli o subshell: quando usi ( … ) o pipe, ricorda che le subshell non vedono le variabili locali del processo padre.
  • Usare /usr/bin/env può introdurre dipendenze dalla PATH dell’utente: in ambienti lock-down questa non è desiderata.

Approcci alternativi

  • Adotta shell più moderne o linguaggi progettati per scripting più complesso (Python, Perl) quando lo script cresce in complessità.
  • Usa strumenti di analisi statica: shellcheck (spesso disponibile come pacchetto) segnala pattern pericolosi.
  • Containerizza gli script per isolare l’ambiente d’esecuzione e ridurre differenze di PATH.

Modelli mentali e regole pratiche

  • Regola 1: pensa sempre a cosa succede se un comando fallisce — e prepara un piano di recupero.
  • Regola 2: preferisci la chiarezza alla brevità; uno script leggibile è meno rischioso.
  • Regola 3: tratta i nomi di file come input non fidato: possono contenere spazi, newline, caratteri speciali.

Heuristics utili:

  • Quote everything: se non sai se una variabile conterrà spazi, falla tra virgolette.
  • Fail fast: lascia che lo script fallisca all’errore, salvo gestioni mirate.
  • Principle of least privilege: crea e modifica quel che serve con i permessi minimi.

Checklist role-based per revisioni rapide

Sviluppatore:

  • Shebang presente e giustificato
  • set -e e set -o pipefail dove appropriato
  • Variabili sempre quotate
  • Nessun uso non necessario di eval
  • Funzioni con variabili locali
  • Test semplici per casi limite

Operazioni / DevOps:

  • Logging e rotazione dei log configurati
  • Recovery / rollback definito
  • Controllo dei permessi dei file
  • Scan di sicurezza su cron job

Revisore di sicurezza:

  • Nessun passaggio di input non sanitizzato a shell expansion
  • Uso di PATH controllato o binari con percorso assoluto
  • Variabili d’ambiente sensibili non esportate accidentalmente

Playbook rapido: scrivere e rilasciare uno script Bash (SOP)

  1. Bozza locale: sviluppa lo script con shebang e set -e -o pipefail.
  2. Aggiungi commenti alla testa: scopo, prerequisiti, esempio d’uso.
  3. Scrivi test di integrazione semplici: possibili input, file temporanei.
  4. Esegui shellcheck e correggi i warning principali.
  5. Rivedi la gestione degli errori e le condizioni di uscita.
  6. Definisci logging e file di lock, se necessario.
  7. Packaging: imposta permessi minimi (es. 750) e includi checksum quando appropriato.
  8. Distribuzione graduale: roll-out canary su pochi nodi, monitoraggio.

Incident runbook e rollback

Se uno script causa problemi in produzione:

  1. Isola l’effetto: disattiva cron job o pipeline che lo eseguono.
  2. Ripristina da backup (se applicabile).
  3. Esegui script diagnostici in ambiente standby.
  4. Revert: ripristina la versione precedente del repository o del pacchetto.
  5. Post-mortem: analizza le cause, aggiorna checklist e aggiungi test che prevengano la regressione.

Test case e criteri di accettazione

Esempi di test manuali/automatici da includere:

  • Input con spazi e caratteri speciali.
  • File con nome che inizia con “-“.
  • Pipeline in cui il primo comando fallisce.
  • Valori d’ambiente non impostati (usare fallback).
  • Esecuzione con PATH diversa (test in container pulito).

Criteri di accettazione:

  • Lo script esce con codice zero in condizioni normali documentate.
  • Tutti i fallimenti previsti producono messaggi chiari su stderr.
  • Non ci sono write non autorizzate a percorsi critici.

Esempi pratici estesi

  1. Proteggere rm su nomi rischiosi:
#!/usr/bin/env bash
set -euo pipefail

# Usa -- per separare opzioni da argomenti
TARGET_DIR="${1:-.}"
# Usa un ciclo per sicurezza e controlli ulteriori
for f in "$TARGET_DIR"/*; do
  # ignora se non ci sono file
  [ -e "$f" ] || continue
  rm -- "$f"
done
  1. Eseguire un comando remoto con fallback e log:
#!/bin/bash
set -euo pipefail

HOST=${HOST:-}
SSH_BIN=${SSH_BIN:-/usr/bin/ssh}
LOGFILE=${LOGFILE:-/var/log/myscript.log}

if [ -z "$HOST" ]; then
  echo "HOST non impostato" >&2
  exit 2
fi

# Loggare l'operazione
echo "$(date -u) - eseguo comandi su $HOST" >> "$LOGFILE"
$SSH_BIN "$HOST" -- 'hostname && uptime' >> "$LOGFILE" 2>&1

Sicurezza e hardening

  • Evita eval: eval espone ad injection quando l’input non è fidato.
  • Controlla PATH: quando esegui binari critici, preferisci percorsi assoluti.
  • Minimizza permessi: non eseguire come root se non indispensabile.
  • Sanifica input: rimuovi caratteri pericolosi o usa liste bianche per nomi di file e comandi.

Note sulla privacy e conformità (GDPR)

Se lo script processa dati personali:

  • Limita logging dei dati sensibili (anonimizza prima di loggare).
  • Assicurati che i backup siano cifrati e i permessi siano adeguati.
  • Documenta quale dato è trattato, per quale scopo e per quanto tempo.

Compatibilità e migrazione

  • dash vs bash: molte distribuzioni usano /bin/sh come dash (più restrittiva). Non usare estensioni di Bash se lo script deve essere compatibile con /bin/sh.
  • Versioni di Bash: alcune funzionalità (es. associative arrays) richiedono Bash >= 4.0. Verifica la versione con:
if [ -z "${BASH_VERSION:-}" ] || [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
  echo "Bash >= 4 richiesto" >&2
  exit 1
fi

Galleria di casi limite

  • Nomi file contenenti newline. Usa read -r -d ‘’ per gestirli in modo sicuro.
  • Parametri numerici: con prove negative assicurati che gli indici o limiti non causino overflow logici.
  • Concorrenza: quando più istanze accedono a risorse comuni, usa lockfile o flock.

Snippet per leggere file in modo sicuro:

while IFS= read -r line; do
  printf '%s\n' "$line"
done < "$file"

Piccola guida di riferimento (cheat sheet)

  • Quote sempre: “$VAR”
  • Preferisci $(…) a ...
  • set -euo pipefail: buona base
  • local VAR: variabili locali in funzioni
  • rm – file: usa – per separare opzioni
  • shellcheck: strumento di lint

Glossario in una riga

  • shebang: prima riga che indica l’interprete (es. #!/usr/bin/env bash).
  • subshell: esecuzione in un ambiente figlio, tra parentesi ( … ).
  • pipefail: opzione che fa fallire la pipeline se uno dei comandi fallisce.

Riepilogo

Queste tecniche aiutano a scrivere script Bash più sicuri, leggibili e manutenibili: usa shebang portabili, cita le variabili, attiva opzioni di sicurezza, preferisci la sintassi moderna e gestisci gli errori in modo esplicito. Integra review, test e strumenti automatici nel tuo flusso di lavoro per ridurre i rischi in produzione.

Note finali: applica le regole in modo ponderato e documenta eccezioni e motivazioni di design.

Autore
Redazione

Materiali simili

Installare e usare Podman su Debian 11
DevOps

Installare e usare Podman su Debian 11

Guida rapida a apt-pinning su Debian
Linux

Guida rapida a apt-pinning su Debian

Forzare FSR 4 con OptiScaler: guida completa
Guide.

Forzare FSR 4 con OptiScaler: guida completa

Dansguardian + Squid NTLM su Debian Etch
Rete

Dansguardian + Squid NTLM su Debian Etch

Riparare errore installazione SD su Android
Android

Riparare errore installazione SD su Android

Cartelle di rete con KNetAttach e remote:/
Linux

Cartelle di rete con KNetAttach e remote:/