Potenti Strategie di Ciclo: Iterare File e Liste negli Script Bash

Padroneggia le tecniche essenziali di ciclo in Bash usando `for` e `while` per automatizzare in modo efficiente attività di sistema ripetitive. Questa guida completa copre l'iterazione su liste, l'elaborazione di sequenze numeriche e la gestione robusta dei file riga per riga utilizzando le migliori pratiche come `while IFS= read -r`. Impara la sintassi fondamentale, il controllo avanzato dei cicli (`break`, `continue`) e le tecniche essenziali per uno scripting shell potente e affidabile, completo di esempi pratici di codice.

Potenti Strategie di Ciclo: Iterare File e Liste negli Script Bash

I cicli Bash sono il punto in cui i piccoli comandi shell diventano automazione utile. Che tu debba elaborare ogni file in una directory, eseguire un'attività un numero prestabilito di volte o leggere dati di configurazione riga per riga, i cicli ti forniscono la struttura per ripetere il lavoro senza copiare e incollare comandi.

I due cicli che userai di più sono for e while. Usa for quando hai già un insieme noto di elementi, come un array o un glob di file. Usa while quando il ciclo è guidato da una condizione o dalla lettura di input. Questa semplice suddivisione rende molti script più facili da ragionare.


Il Ciclo for: Iterare su Insiemi Fissi

Il ciclo for è ideale quando conosci in anticipo la raccolta di elementi che devi elaborare. Questa raccolta può essere un elenco esplicito di valori, i risultati di un comando o un insieme di file trovati tramite globbing.

1. Iterare su Liste Standard

Il caso d'uso più semplice è iterare su una breve lista di parole scritte direttamente nello script.

Sintassi

for VARIABILE in LISTA_DI_ELEMENTI; do
    # Comandi che usano $VARIABILE
done

Esempio: Elaborazione di una Lista di Utenti

# Lista di utenti da elaborare
UTENTI="alice bob carlo"

for utente in $UTENTI; do
  echo "Controllo della home directory per $utente..."
  if [ -d "/home/$utente" ]; then
    echo "$utente è attivo."
  else
    echo "Attenzione: home directory di $utente mancante."
  fi
done

Questo pattern va bene per nomi semplici. Se un elemento può contenere spazi, usa un array invece di una stringa separata da spazi:

UTENTI=("alice" "bob" "mary jane")

for utente in "${UTENTI[@]}"; do
  echo "Controllo di $utente"
done

2. Iterazione Numerica in Stile C

Per attività che richiedono conteggi o sequenze numeriche specifiche, Bash supporta un ciclo for in stile C, spesso combinato con l'espansione delle parentesi graffe o il comando seq.

Sintassi (Stile C)

for (( INIZIALIZZAZIONE; CONDIZIONE; INCREMENTO )); do
    # Comandi
done

Esempio: Script di Countdown

# Cicla 5 volte (i inizia da 1, continua finché i è minore o uguale a 5)
for (( i=1; i<=5; i++ )); do
  echo "Numero iterazione: $i"
  sleep 1
done
echo "Fatto!"

Alternativa: Usare l'Espansione delle Parentesi Graffe per Sequenze Semplici

L'espansione delle parentesi graffe è più semplice e veloce rispetto all'uso di seq per generare interi o sequenze contigue.

# Genera numeri da 10 a 1
for num in {10..1}; do
  echo "Conto alla rovescia: $num"
done

3. Iterare su File e Directory (Globbing)

Usare i wildcard (*) all'interno del ciclo for ti permette di elaborare file che corrispondono a un pattern specifico, come tutti i file di log o tutti gli script in una directory.

Esempio: Archiviazione di File di Log

Cita la variabile ("$file") quando hai a che fare con nomi di file, specialmente quelli contenenti spazi o caratteri speciali.

DIR_DEST="/var/log/applicazione"

# Cicla su tutti i file che terminano con .log nella directory di destinazione
for filelog in "$DIR_DEST"/*.log; do

  # Controlla se il file esiste effettivamente (previene l'esecuzione sul letterale "*.log" se nessun file corrisponde)
  if [ -f "$filelog" ]; then
    echo "Compressione di $filelog..."
    gzip "$filelog"
  fi
done

Il Ciclo while: Esecuzione Basata su Condizioni

Il ciclo while continua a eseguire un blocco di comandi finché una condizione specificata rimane vera. È comunemente usato per leggere flussi di input, monitorare condizioni o gestire attività in cui il numero di iterazioni è sconosciuto.

1. Ciclo while Base

Sintassi

while CONDIZIONE; do
    # Comandi
done

Esempio: Attesa di una Risorsa

Questo ciclo usa il comando test ([ ]) per verificare se una directory esiste prima di procedere.

PERCORSO_RISORSA="/mnt/data/condivisa"

while [ ! -d "$PERCORSO_RISORSA" ]; do
  echo "Attesa del montaggio della risorsa $PERCORSO_RISORSA..."
  sleep 5
done

echo "Risorsa disponibile. Avvio del backup."

2. Il Pattern Robusto while read

L'applicazione più potente del ciclo while è la lettura del contenuto di un file o di un flusso di output riga per riga. Questo pattern è di gran lunga superiore all'uso di un ciclo for sull'output di cat, poiché gestisce in modo affidabile spazi e caratteri speciali.

Migliore Pratica: Lettura Riga per Riga

Per garantire la massima robustezza, utilizziamo tre componenti chiave:

  1. IFS=: Pulisce il Separatore di Campo Interno, assicurando che l'intera riga, inclusi gli spazi iniziali/finali, venga letta nella variabile.
  2. read -r: L'opzione -r impedisce l'interpretazione delle barre rovesciate (lettura raw), che è fondamentale per percorsi e stringhe complesse.
  3. Redirezione di Input (<): Reindirizza il contenuto del file nel ciclo, assicurando che il ciclo venga eseguito nel contesto della shell corrente (prevenendo problemi di subshell).
# File contenente dati, un elemento per riga
FILE_CONFIG="/etc/app/server.txt"

while IFS= read -r nome_server; do
  
  # Salta righe vuote o commentate
  if [[ -z "$nome_server" || "$nome_server" =~ ^# ]]; then
    continue
  fi

  echo "Ping del server: $nome_server"
  ping -c 1 "$nome_server"

done < "$FILE_CONFIG"

Suggerimento: Evitare cat nei Cicli

Preferisci while ... done < file a cat file | while ... quando leggi un file. Nella maggior parte delle configurazioni Bash, una pipeline esegue il ciclo in una subshell, quindi le variabili modificate all'interno del ciclo vengono perse quando il ciclo termina.

3. Gestire Nomi di File da find

Per l'elaborazione ricorsiva di file, evita di analizzare l'output semplice di find riga per riga. I nomi di file possono contenere spazi e, raramente, nuove righe. Usa output delimitato da null:

find /var/log/applicazione -type f -name '*.log' -print0 |
while IFS= read -r -d '' filelog; do
  echo "Trovato log: $filelog"
  gzip -- "$filelog"
done

La coppia -print0 e read -d '' tratta il byte null come separatore. Il -- prima di "$filelog" dice a gzip che i valori seguenti sono operandi, non opzioni, proteggendoti da nomi di file che iniziano con -.

Controllo Avanzato dei Cicli e Tecniche

Script efficaci richiedono la capacità di controllare l'esecuzione del ciclo in base alle condizioni di runtime.

1. Controllo del Flusso: break e continue

  • break: Esce immediatamente dall'intero ciclo, indipendentemente dalle iterazioni o condizioni rimanenti.
  • continue: Salta l'iterazione corrente e passa immediatamente alla successiva (o rivaluta la condizione while).

Esempio: Cerca e Ferma

BERSAGLIO_RICERCA="target.conf"

for file in /etc/*; do
  if [ -f "$file" ] && [[ "$file" == *"$BERSAGLIO_RICERCA"* ]]; then
    echo "Trovata configurazione target in: $file"
    break  # Interrompe l'elaborazione una volta trovato
  elif [ -d "$file" ]; then
    continue # Salta le directory, controlla solo i file
  fi
  echo "Controllo del file: $file"
done

2. Gestire Delimitatori Complessi usando IFS

Mentre la lettura di file riga per riga richiede la pulizia di IFS, l'iterazione su una lista separata da un carattere diverso (come una virgola) richiede l'impostazione temporanea di IFS.

DATI_CSV="dato1,dato2,dato3,dato4"
IFS_VECCHIO=$IFS # Salva l'IFS originale
IFS=','       # Imposta IFS sul carattere virgola

for elemento in $DATI_CSV; do
  echo "Elemento trovato: $elemento"
done

IFS=$IFS_VECCHIO # Ripristina immediatamente l'IFS originale dopo il ciclo

Attenzione: Modifiche Globali a IFS

Salva sempre il $IFS originale prima di modificarlo all'interno di uno script (es., IFS_VECCHIO=$IFS). La mancata ripristino del valore originale può causare comportamenti imprevedibili nei comandi successivi.

Migliori Pratiche per Cicli Bash Robusto

Pratica Motivazione
Cita Sempre le Variabili Usa "$variabile" per prevenire la suddivisione delle parole e l'espansione dei glob, specialmente nell'iterazione dei file.
Usa while IFS= read -r Il metodo più affidabile per elaborare file riga per riga, gestendo correttamente spazi e caratteri speciali.
Controlla l'Esistenza Quando usi il globbing (*.txt), includi sempre un controllo (if [ -f "$file" ];) per assicurarti che il ciclo non elabori il nome letterale del pattern se nessun file corrisponde.
Localizza le Variabili Usa la parola chiave local all'interno delle funzioni per evitare che le variabili del ciclo sovrascrivano accidentalmente le variabili globali.
Usa Comandi Built-in Rispetto a Comandi Esterni Usa l'espansione delle parentesi graffe ({1..10}) o i cicli in stile C piuttosto che generare comandi esterni come seq per le prestazioni.

Una Regola Pratica

Usa gli array per le liste in memoria, i glob per insiemi di file semplici, while IFS= read -r per input orientato alle righe e l'output di find delimitato da null per la gestione ricorsiva dei nomi di file. Cita le espansioni per impostazione predefinita. Aggiungi controlli di esistenza intorno ai glob. Tieni break e continue per i casi in cui rendono il ciclo più facile da leggere, non come un modo per nascondere un flusso di controllo complicato.

La maggior parte dei bug dei cicli Bash deriva dalla suddivisione delle parole, da nomi di file imprevisti o dal presupposto che l'input sia più pulito di quanto non sia. Se il tuo ciclo gestisce deliberatamente spazi, righe vuote, commenti e corrispondenze mancanti, sopravviverà al lavoro di automazione reale.