Linee guida per scrivere script Bash sicuri e mantenibili

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 $FILENAMEBash 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
fiForma 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 -xQuesto fa stampare ogni comando espanso prima dell’esecuzione, utile per debug.

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 filenameLe 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 -- *.mdQuesto evita scenari catastrofici (es. rm * con un file chiamato -rf).


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 $DIRSoluzione: 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)
- Bozza locale: sviluppa lo script con shebang e set -e -o pipefail.
- Aggiungi commenti alla testa: scopo, prerequisiti, esempio d’uso.
- Scrivi test di integrazione semplici: possibili input, file temporanei.
- Esegui shellcheck e correggi i warning principali.
- Rivedi la gestione degli errori e le condizioni di uscita.
- Definisci logging e file di lock, se necessario.
- Packaging: imposta permessi minimi (es. 750) e includi checksum quando appropriato.
- Distribuzione graduale: roll-out canary su pochi nodi, monitoraggio.
Incident runbook e rollback
Se uno script causa problemi in produzione:
- Isola l’effetto: disattiva cron job o pipeline che lo eseguono.
- Ripristina da backup (se applicabile).
- Esegui script diagnostici in ambiente standby.
- Revert: ripristina la versione precedente del repository o del pacchetto.
- 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
- 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- 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>&1Sicurezza 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
fiGalleria 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.