È uscito il Corso Java Completo
Torna al blog

Bash scripting: automatizzare da riga di comando

Come scrivere script Bash utili: variabili, condizioni, cicli, argomenti e le tre righe di sicurezza che evitano i disastri più comuni.

Edoardo Midali

Edoardo Midali

Developer · Content Creator

6 min di lettura

Il momento in cui conviene scrivere uno script è riconoscibile: hai una sequenza di comandi che ripeti, la recuperi dalla cronologia ogni volta e ogni tanto sbagli un pezzo. Bash scripting significa salvare quella sequenza in un file, renderla affidabile e non doverla più ricordare. In questo articolo trovi il minimo indispensabile per scrivere script che funzionano davvero, comprese le righe di sicurezza che quasi nessuna guida mette all'inizio.

Cos'è

Uno script Bash è un file di testo che contiene comandi di shell, eseguiti in sequenza come se li stessi scrivendo tu.

Non c'è niente da installare e nessun linguaggio nuovo da imparare per iniziare: se sai usare il terminale, sai già scrivere la prima metà di uno script. Il resto è aggiungere condizioni, ripetizioni e controlli.

Struttura minima:

#!/usr/bin/env bash
echo "ciao"

Poi:

chmod +x script.sh   # rendilo eseguibile
./script.sh

La prima riga (shebang) dice al sistema con quale interprete eseguire il file. Senza, il comportamento dipende da come lo lanci. La forma con env trova Bash ovunque sia installato, anche dove non sta in /bin/bash.

Se il chmod ti sembra oscuro, il contesto è in permessi dei file su Linux.

Le tre righe da mettere sempre

Questa è la parte più importante dell'articolo e va messa in cima a ogni script:

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

Cosa fa, nel dettaglio:

  • -e — se un comando fallisce, lo script si ferma. Senza, prosegue allegramente come se nulla fosse.
  • -u — errore se usi una variabile mai definita. Intercetta i refusi.
  • -o pipefail — una pipe fallisce se fallisce un qualsiasi pezzo, non solo l'ultimo.

Perché conta davvero. Senza -e, uno script che fa cd /cartella/backup seguito da rm -rf * continua anche se il cd fallisce — e cancella nella cartella sbagliata. Con -e si ferma alla prima riga.

Senza -u, uno script con rm -rf "$CARTELLA/" dove CARTELLA è vuota per un refuso esegue una cancellazione dalla radice. Sono due righe che trasformano un incidente grave in un messaggio di errore.

Variabili e argomenti

nome="Edoardo"
echo "ciao $nome"

Niente spazi attorno all'uguale, e mettere sempre le variabili tra virgolette quando le usi:

rm "$file"     # corretto
rm $file       # si rompe se il nome contiene spazi

Gli argomenti passati allo script:

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

echo "primo argomento: $1"
echo "quanti argomenti: $#"
echo "tutti: $@"

Un valore predefinito quando l'argomento manca:

cartella="${1:-.}"    # usa la cartella corrente se $1 non c'è

Controllare che ci siano gli argomenti necessari, all'inizio:

if [ $# -lt 1 ]; then
  echo "Uso: $0 <cartella>" >&2
  exit 1
fi

>&2 manda il messaggio sugli errori standard e exit 1 segnala il fallimento a chi ha lanciato lo script. Sono dettagli che contano quando lo script viene usato dentro un altro script o da uno scheduler.

Condizioni

if [ -f "$file" ]; then
  echo "il file esiste"
elif [ -d "$file" ]; then
  echo "è una cartella"
else
  echo "non esiste"
fi

I test che userai davvero:

TestVero se
-f fileè un file
-d cartellaè una cartella
-e percorsoesiste (qualsiasi cosa)
-z "$var"la stringa è vuota
-n "$var"la stringa non è vuota
"$a" = "$b"stringhe uguali
$a -eq $bnumeri uguali
$a -gt $bmaggiore

Il confronto tra stringhe usa =, quello tra numeri usa -eq. Scambiarli è l'errore più comune, e non sempre dà errore: a volte dà solo il risultato sbagliato.

Cicli

Sui file:

for file in *.txt; do
  echo "elaboro $file"
done

Su una lista:

for ambiente in dev staging prod; do
  echo "deploy su $ambiente"
done

Riga per riga da un file — e qui la forma corretta è meno ovvia:

while IFS= read -r riga; do
  echo "$riga"
done < elenco.txt

IFS= evita che spazi iniziali e finali vengano tagliati, -r impedisce che le barre rovesciate vengano interpretate. Senza, i dati che contengono caratteri particolari si corrompono in silenzio.

Funzioni

log() {
  echo "[$(date +%H:%M:%S)] $*"
}

log "inizio backup"

Le funzioni vanno definite prima di essere usate. Servono soprattutto a dare un nome a un blocco e a non ripetere lo stesso codice tre volte.

Uno script completo

Un backup che si può leggere tutto:

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

SORGENTE="${1:?serve la cartella sorgente}"
DESTINAZIONE="${2:-/var/backup}"
DATA=$(date +%Y-%m-%d)
ARCHIVIO="$DESTINAZIONE/backup-$DATA.tar.gz"

log() { echo "[$(date +%H:%M:%S)] $*"; }

if [ ! -d "$SORGENTE" ]; then
  log "ERRORE: $SORGENTE non esiste"
  exit 1
fi

mkdir -p "$DESTINAZIONE"

log "creo $ARCHIVIO"
tar -czf "$ARCHIVIO" -C "$SORGENTE" .

log "elimino i backup più vecchi di 30 giorni"
find "$DESTINAZIONE" -name 'backup-*.tar.gz' -mtime +30 -delete

log "fatto"

${1:?messaggio} interrompe con un errore chiaro se l'argomento manca: più conciso di un if, e altrettanto esplicito.

Errori che costano

Non mettere le variabili tra virgolette. La causa numero uno di script che funzionano finché un nome non contiene uno spazio.

Cancellare senza verificare. Prima di un rm costruito da variabili, stampalo:

echo rm -rf "$CARTELLA"    # guarda cosa farebbe

Togli echo solo quando l'output è quello che ti aspetti.

Percorsi relativi. Uno script lanciato da uno scheduler parte da una cartella che non è quella che immagini. Usa percorsi assoluti, o posizionati esplicitamente:

cd "$(dirname "$0")"

Non gestire il caso di lista vuota. Un ciclo for su un glob che non corrisponde a nulla viene eseguito una volta con il valore letterale del glob. Si evita con shopt -s nullglob.

Non testare prima su dati veri. Ovvio, e comunque il modo più frequente di fare danni.

Quando invece serve Python

Bash è ottimo per orchestrare comandi. Diventa la scelta sbagliata quando lo script:

  • supera le 150-200 righe
  • manipola JSON, XML o strutture dati
  • ha bisogno di logica complessa o gestione degli errori articolata
  • deve girare anche su Windows

In quei casi conviene automatizzare con Python: resta leggibile dove Bash diventa criptico.

La linea di demarcazione pratica: se stai concatenando comandi di sistema, Bash. Se stai elaborando dati, Python.

In sintesi

Uno script Bash è la sequenza di comandi che già usi, salvata in un file e resa ripetibile.

Se ricordi una sola cosa, ricorda set -euo pipefail e le virgolette attorno alle variabili. Da soli evitano la grande maggioranza degli script che falliscono male: quelli che proseguono dopo un errore e quelli che si rompono su un nome con lo spazio.

Il momento giusto per scriverne uno è la terza volta che ripeti la stessa sequenza a mano. Il momento giusto per passare a Python è quando lo script comincia a somigliare a un programma.

Per il funzionamento della shell sotto — espansioni, pipe, redirezioni — vedi cos'è la shell Bash. Per farlo girare a orari fissi su un server, il contesto è in come configurare un VPS da zero.