Capitolo 10. Cicli ed alternative

Sommario
10.1. Cicli
10.2. Cicli annidati
10.3. Controllo del ciclo
10.4. Verifiche ed alternative

Le operazioni sui blocchi di codice sono la chiave per creare script di shell ben strutturati e organizzati. I costrutti per gestire i cicli e le scelte sono gli strumenti che consentono di raggiungere questo risultato.

10.1. Cicli

Un ciclo è un blocco di codice che itera (ripete) [1] un certo numero di comandi finché la condizione di controllo del ciclo rimane vera.

cicli for

for arg in [lista]

È il costrutto di ciclo fondamentale. Differisce significativamente dal suo analogo del linguaggio C.

for arg in [lista]
do
 comando(i)...
done

Nota

Ad ogni passo del ciclo, arg assume il valore di ognuna delle successive variabili elencate in lista.

for arg in "$var1" "$var2" "$var3" ... "$varN"  
# Al 1mo passo del ciclo, arg = $var1
# Al 2do passo del ciclo, arg = $var2
# Al 3zo passo del ciclo, arg = $var3
# ...
# Al passo Nmo del ciclo, arg = $varN

#  Bisogna applicare il "quoting" agli argomenti di [lista] per
#+ evitare una possibile suddivisione delle parole.

Gli argomenti elencati in lista possono contenere i caratteri jolly.

Se do si trova sulla stessa riga di for è necessario usare il punto e virgola dopo lista.

for arg in [lista] ; do

Esempio 10-1. Semplici cicli for

#!/bin/bash
# Elenco di pianeti.

for pianeta in Mercurio Venere Terra Marte Giove Saturno Urano Nettuno Plutone
do
  echo $pianeta  # Ogni pianeta su una riga diversa
done

echo

for pianeta in "Mercurio Venere Terra Marte Giove Saturno Urano Nettuno Plutone"
# Tutti i pianeti su un'unica riga.
# L'intera "lista" racchiusa tra apici doppi crea un'unica variabile.
do
  echo $pianeta
done

exit 0

Nota

Ogni elemento in [lista] può contenere più parametri. Ciò torna utile quando questi devono essere elaborati in gruppi. In tali casi, si deve usare il comando set (vedi Esempio 14-15) per forzare la verifica di ciascun elemento in [lista] e per assegnare ad ogni componente i rispettivi parametri posizionali.

Esempio 10-2. Ciclo for con due parametri in ogni elemento [lista]

#!/bin/bash
# Pianeti rivisitati.

# Associa il nome di ogni pianeta con la sua distanza dal sole.

for pianeta in "Mercurio 36" "Venere 67" "Terra 93"  "Marte 142" "Giove 483"
do
  set -- $pianeta  #  Verifica la variabile "pianeta" e imposta i parametri 
                   #+ posizionali.
  #  i "--" evitano sgradevoli sorprese nel caso $pianeta sia nulla
  #+ o inizi con un trattino.

  #  Potrebbe essere necessario salvare i parametri posizionali
  #+ originari, perché vengono sovrascritti.
  #  Un modo per farlo è usare un array,
  #        param_origin=("$@")

  echo "$1		$2,000,000 miglia dal sole"

  ##-------due  tab---- servono a concatenare gli zeri al parametro $2
done

# (Grazie, S.C., per i chiarimenti aggiuntivi.)

exit 0

In un ciclo for, una variabile può sostituire [lista].

Esempio 10-3. Fileinfo: operare su un elenco di file contenuto in una variabile

#!/bin/bash
# fileinfo.sh

FILE="/usr/sbin/accept
/usr/sbin/pwck
/usr/sbin/chroot
/usr/bin/fakefile
/sbin/badblocks
/sbin/ypbind"     # Elenco dei file sui quali volete informazioni.
                  # Compreso l'inesistente file /usr/bin/fakefile.

echo

for file in $FILE
do

  if [ ! -e "$file" ]       # Verifica se il file esiste.
  then
    echo "$file non esiste."; echo
    continue                # Verifica il successivo.
   fi

  ls -l $file | awk '{ print $9 "         dimensione file: " $5 }'
  # Visualizza 2 campi.

  whatis `basename $file`   # Informazioni sul file.
  #  Fate attenzione che, affinché questo script funzioni correttamente, 
  #+ bisogna aver impostato il database whatis.
  #  Per farlo, da root, eseguite /usr/bin/makewhatis.
  echo
done  

exit 0

In un ciclo for è possibile il globbing se in [lista] sono presenti i caratteri jolly (* e ?), che vengono usati per l'espansione dei nomi dei file.

Esempio 10-4. Agire sui file con un ciclo for

#!/bin/bash
# list-glob.sh: Generare [lista] in un ciclo for usando il "globbing".

echo

for file in *
#           ^  Bash esegue l'espansione del nome del file
#+             nelle espressioni che riconoscono il globbing.
do
  ls -l "$file"  # Elenca tutti i file in $PWD (directory corrente).
  #  Ricordate che il carattere jolly  "*" verifica tutti i file,
  #+ tuttavia, il "globbing" non verifica i file i cui nomi iniziano
  #+ con un punto.

  #  Se il modello non verifica nessun file, allora si autoespande.
  #  Per evitarlo impostate l'opzione nullglob 
  #+   (shopt -s nullglob).
  #  Grazie, S.C.
done

echo; echo

for file in [jx]*
do
  rm -f $file    #  Cancella solo i file i cui nomi iniziano con
                 #+ "j" o "x" presenti in $PWD.
  echo "Rimosso il file \"$file\"".
done

echo

exit 0

Omettere in [lista] in un ciclo for fa sì che il ciclo agisca su $@ -- i parametri posizionali . Una dimostrazione particolarmente intelligente di ciò è illustrata in Esempio A-16. Vedi anche Esempio 14-16

Esempio 10-5. Tralasciare in [lista] in un ciclo for

#!/bin/bash

# Invocate lo script sia con che senza argomenti e osservate cosa succede.

for a
do
 echo -n "$a "
done

#  Manca 'in lista', quindi il ciclo opera su '$@'
#+ (elenco degli argomenti da riga di comando, compresi gli spazi).

echo

exit 0

È possibile impiegare la sostituzione di comando per generare [lista]. Vedi anche Esempio 15-49, Esempio 10-10 ed Esempio 15-43.

Esempio 10-6. Generare [lista] in un ciclo for con la sostituzione di comando

#!/bin/bash
#  for-loopcmd.sh: un ciclo for con [lista] 
#+ prodotta dalla sostituzione di comando.

NUMERI="9 7 3 8 37.53"

for numero in `echo $NUMERI`  # for numero in 9 7 3 8 37.53
do
  echo -n "$numero "
done

echo 
exit 0

Ecco un esempio un po' più complesso dell'utilizzo della sostituzione di comando per creare [lista].

Esempio 10-7. Un'alternativa con grep per i file binari

#!/bin/bash
# bin-grep.sh:  Localizza le stringhe in un file binario.

# Un'alternativa con "grep" per file binari.
# Effetto simile a "grep -a"

E_ERR_ARG=65
E_NOFILE=66

if [ $# -ne 2 ]
then
  echo "Utilizzo: `basename $0` stringa_di_ricerca nomefile"
  exit $E_ERR_ARG
fi

if [ ! -f "$2" ]
then
  echo "Il file \"$2\" non esiste."
  exit $E_NOFILE
fi  


IFS=$'\012'       # Su suggerimento di Anton Filippov.
                  # nella versione precedente era: IFS="\n"        
for parola in $( strings "$2" | grep "$1" )
# Il comando "strings" elenca le stringhe nei file binari.
# L'output viene collegato (pipe) a "grep" che verifica la stringa cercata.
do
  echo $parola
done

#  Come ha sottolineato S.C., le righe 23 - 30 potrebbero essere
#+ sostituite con la più semplice
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'


#  Provate qualcosa come  "./bin-grep.sh mem /bin/ls"  per esercitarvi 
#+ con questo script.

exit 0

Sempre sullo stesso tema.

Esempio 10-8. Elencare tutti gli utenti del sistema

#!/bin/bash
# userlist.sh

FILE_PASSWORD=/etc/passwd
n=1           # Numero utente

for nome in $(awk 'BEGIN{FS=":"}{print $1}' < "$FILE_PASSWORD" )
# Separatore di campo = :^^^^^^
# Visualizza il primo campo      ^^^^^^^^         
# Ottiene l'input dal file delle password    ^^^^^^^^^^^^^^^^^
do
  echo "UTENTE nr.$n = $nome"
  let "n += 1"
done  


# UTENTE nr.1 = root
# UTENTE nr.2 = bin
# UTENTE nr.3 = daemon
# ...
# UTENTE nr.30 = bozo

exit 0

#  Esercizio:
#  ---------
#  Com'è che un utente ordinario (o uno script eseguito dallo stesso)
#+ riesce a leggere /etc/passwd?
#  Non si tratta di una falla per la sicurezza? Perché o perché no?

Esempio finale di [lista] risultante dalla sostituzione di comando.

Esempio 10-9. Verificare tutti i file binari di una directory in cerca degli autori

#!/bin/bash
# findstring.sh:
# Cerca una stringa particolare nei binari di una directory specificata.

directory=/usr/bin/
stringa="Free Software Foundation"  # Vede quali file sono della FSF.

for file in $( find $directory -type f -name '*' | sort )
do
  strings -f $file | grep "$stringa" | sed -e "s%$directory%%"
  #  Nell'espressione  "sed", è necessario sostituire il normale
  #+ delimitatore "/" perché si dà il caso che "/" sia uno dei
  #+ caratteri che deve essere filtrato.
done

exit 0

#  Esercizio (facile):
#  ------------------
#  Modificate lo script in modo tale che accetti come parametri da
#+ riga di comando $directory e $stringa.

L'output di un ciclo for può essere collegato con una pipe ad un comando o ad una serie di comandi.

Esempio 10-10. Elencare i link simbolici presenti in una directory

#!/bin/bash
# symlinks.sh: Elenca i link simbolici presenti in una directory.


directory=${1-`pwd`}
#  Imposta come predefinita la directory di lavoro corrente, nel caso non ne 
#+ venga specificata alcuna.
#  Corrisponde al seguente blocco di codice.
# -------------------------------------------------------------------
# ARG=1                 # Si aspetta un argomento da riga di comando.
#
# if [ $# -ne "$ARG" ]  # Se non c'è 1 argomento...
# then
#   directory=`pwd`     # directory di lavoro corrente
# else
#   directory=$1
# fi
# -------------------------------------------------------------------

echo "Link simbolici nella directory \"$directory\""

for file in "$( find $directory -type l )"  # -type l = link simbolici
do
  echo "$file"
done | sort                                 #  Se manca sort, l'elenco
                                            #+ non verrà ordinato.
#  Per essere precisi, in realtà in questo caso un ciclo non sarebbe necessario,
#+ perché l'output del comando "find" viene espanso in un'unica parola.
#  Tuttavia, illustra bene questa modalità e ne facilita la comprensione.

#  Come ha evidenziato Dominik 'Aeneas' Schnitzer,
#+ se non si usa il "quoting" per $( find $directory -type l ) i nomi dei 
#+ file contenenti spazi non vengono visualizzati correttamente.
#  Il nome viene troncato al primo spazio incontrato.

exit 0


# Jean Helou propone la seguente alternativa:

echo "Link simbolici nella directory \"$directory\""
# Salva l'IFS corrente. Non si è mai troppo prudenti.
VECCHIOIFS=$IFS
IFS=:

for file in $(find $directory -type l -printf "%p$IFS")
do     #                              ^^^^^^^^^^^^^^^^
       echo "$file"
       done|sort

Lo stdout di un ciclo può essere rediretto in un file, come dimostra la piccola modifica apportata all'esempio precedente.

Esempio 10-11. Link simbolici presenti in una directory salvati in un file

#!/bin/bash
# symlinks.sh: Elenca i link simbolici presenti in una directory.

OUTFILE=symlinks.list                         # file di memorizzazione

directory=${1-`pwd`}
#  Imposta come predefinita la directory di lavoro corrente, nel caso non 
#+ ne venga specificata alcuna.


echo "Link simbolici nella directory \"$directory\"" > "$OUTFILE"
echo "----------------------------------" >> "$OUTFILE"

for file in "$( find $directory -type l )"    # -type l = link simbolici
do
  echo "$file"
done | sort >> "$OUTFILE"                     #  stdout del ciclo rediretto
#           ^^^^^^^^^^^^^                        al file di memorizzazione.

exit 0

Vi è una sintassi alternativa per il ciclo for che risulta molto familiare ai programmatori in linguaggio C. Si basa sull'uso del costrutto doppie parentesi.

Esempio 10-12. Un ciclo for in stile C

#!/bin/bash
# Due modi per contare fino a 10.

echo

# Sintassi standard.
for a in 1 2 3 4 5 6 7 8 9 10
do
  echo -n "$a "
done  

echo; echo

# 
+===================================================================+

# Ora facciamo la stessa cosa usando la sintassi in stile C.

LIMITE=10

for ((a=1; a <= LIMITE; a++))  # Doppie parentesi, e "LIMITE" senza "$".
do
  echo -n "$a "
done                           # Un costrutto preso in prestito da 'ksh93'.

echo; echo

# +===================================================================+

#  Uso dell' "operatore virgola" del C per incrementare due variabili
#+ contemporaneamente.

for ((a=1, b=1; a <= LIMITE; a++, b++))  # La virgola concatena le operazioni.
do
  echo -n "$a-$b "
done

echo; echo

exit 0

Vedi anche Esempio 26-15, Esempio 26-16 e Esempio A-6.

---

Adesso un ciclo for impiegato in un'applicazione "pratica".

Esempio 10-13. Utilizzare efax in modalità batch

#!/bin/bash
# Inviare un fax (dovete aver installato il pacchetto 'efax')

ARG_ATTESI=2
E_ERR_ARG=65

if [ $# -ne $ARG_ATTESI ]
# Verifica il corretto numero di argomenti.
then
   echo "Utilizzo: `basename $0` nr_telefono file_testo"
   exit $E_ERR_ARG
fi


if [ ! -f "$2" ]
then
  echo "Il file $2 non è un file di testo"
  #     Il file non è un file regolare oppure il file non esiste.
  exit $E_ERR_ARG
fi
  

fax make $2              # Crea file fax formattati dai file di testo.

for file in $(ls $2.0*)  # Concatena i file appena creati.
                         #  Usa il carattere jolly ("globbing" del nome del file)
                         #+ nella variabile lista.
do
  fil="$fil $file"
done  

efax -d /dev/ttyS3 -o1 -t "T$1" $fil   # Infine, esegue il lavoro.


#  Come ha sottolineato S.C. il ciclo for potrebbe essere sostituito con
#     efax -d /dev/ttyS3 -o1 -t "T$1" $2.0*
#+ ma non sarebbe stato altrettanto istruttivo [sorriso].

exit 0
while

Questo costrutto verifica una condizione data all'inizio del ciclo che viene mantenuto in esecuzione finché quella condizione rimane vera (restituisce exit status 0). A differenza del ciclo for, il ciclo while viene usato in quelle situazioni in cui il numero delle iterazioni non è conosciuto in anticipo.

while [ condizione ]
do
 comando(i)...
done

Il costrutto parentesi quadre in un ciclo while è una nostra vecchia conoscenza, la verifica parentesi quadre usata nel costrutto if/then. Di fatto, un ciclo while consente l'uso del più versatile costrutto doppie parentesi quadre (while [[ condizione ]]).

Come nel caso dei cicli for, collocare il do sulla stessa riga della condizione di verifica rende necessario l'uso del punto e virgola.

while [ condizione ] ; do

È da notare che alcuni cicli while specializzati, come per esempio il costrutto getopts, si discostano un po' dalla struttura standard appena illustrata.

Esempio 10-14. Un semplice ciclo while

#!/bin/bash

var0=0
LIMITE=10

while [ "$var0" -lt "$LIMITE" ]
#      ^                     ^
# Spazi perché si tratta di un "costrutto parentesi quadre" . . .
do
  echo -n "$var0 "        # -n sopprime il ritorno a capo.
  #             ^           Lo spazio serve a separare i numeri visualizzati.
  var0=`expr $var0 + 1`   # var0=$(($var0+1))  anche questa forma va bene.
                          # var0=$((var0 + 1)) anche questa forma va bene.
                          # let "var0 += 1"    anche questa forma va bene.
done                      # Anche vari altri metodi funzionano.

echo

exit 0

Esempio 10-15. Un altro ciclo while

#!/bin/bash

echo
                                #  Equivalente a:
while [ "$var1" != "fine" ]     #  while test "$var1" != "fine"
do
  echo "Immetti la variabile #1 (fine per terminare) "
  read var1                     #  Non 'read $var1' (perché?).
  echo "variabile #1 = $var1"   #  È necessario il "quoting"
                                #+ per la presenza di "#"...
  # Se l'input è 'fine', viene visualizzato a questo punto.
  # La verifica per l'interruzione del ciclo, infatti, è posta all'inizio.

  echo
done  

exit 0

Un ciclo while può avere più condizioni. Ma è solamente quella finale che stabilisce quando il ciclo deve terminare. Per questo scopo, però, è necessaria una sintassi leggermente differente.

Esempio 10-16. Ciclo while con condizioni multiple

#!/bin/bash

var1=nonimpostata
precedente=$var1

while echo "variabile-precedente = $precedente"
      echo
      precedente=$var1
      [ "$var1" != fine ] # Tiene traccia del precedente valore di $var1.
      #  "while" con quattro condizioni, ma è solo l'ultima che controlla
      #+ il ciclo.
      #  È l'*ultimo* exit status quello che conta.
do
echo "Immetti la variable nr.1 (fine per terminare) "
  read var1
  echo "variabile nr.1 = $var1"
done

# Cercate di capire come tutto questo funziona.
# È un tantino complicato.

exit 0

Come per il ciclo for, anche per un ciclo while si può impiegare una sintassi in stile C usando il costrutto doppie parentesi (vedi anche Esempio 9-31).

Esempio 10-17. Sintassi in stile C di un ciclo while

#!/bin/bash
# wh-loopc.sh: Contare fino a 10 con un ciclo "while".

LIMITE=10
a=1

while [ "$a" -le $LIMITE ]
do
  echo -n "$a "
  let "a+=1"
done           # Fin qui nessuna novità.

echo; echo

# +=================================================================+

# Rifatto con la sintassi del C.

((a = 1))      # a=1
#  Le doppie parentesi consentono gli spazi nell'impostazione di una
#+ variabile, come in C.

while (( a <= LIMITE ))   #  Doppie parentesi senza "$" che precede
                          #+ il nome della variabile.
do
  echo -n "$a "
  ((a += 1))   # let "a+=1"
  #  Si.
  #  Le doppie parentesi consentono di incrementare una variabile
  #+ con la sintassi del C.
done

echo

# Ora i programmatori in C si sentiranno a casa loro anche con Bash.

exit 0

Un ciclo while può richiamare una funzione inserendola al posto della condizione.

t=0
			    
condizione ()
{
  ((t++))
			    
  if [ $t -lt 5 ]
   then
    return 0  # vero
  else
    return 1  # falso
  fi
}
			    
while condizione
#     ^^^^^^^^^^
#     Chiamata di funzione -- ciclo di quattro iterazioni.
do
  echo "Ancora in esecuzione: t = $t"
done
			    
# Ancora in esecuzione: t = 1
# Ancora in esecuzione: t = 2
# Ancora in esecuzione: t = 3
# Ancora in esecuzione: t = 4

Abbinando la potenza del comando read ad un ciclo while, otteniamo il pratico costrutto while read, utile per la lettura e verifica di file.

Nota

Un ciclo while può avere il proprio stdin rediretto da un file tramite il < alla fine del blocco.

In un ciclo while il relativo stdin può essere fornito da una pipe.

until

Questo costrutto verifica una condizione data all'inizio del ciclo che viene mantenuto in esecuzione finché quella condizione rimane falsa (il contrario del ciclo while).

until [ condizione-falsa ]
do
 comando(i)...
done

Notate che un ciclo until verifica la condizione all'inizio del ciclo, differendo, in questo, da analoghi costrutti di alcuni linguaggi di programmazione.

Come nel caso dei cicli for, collocare il do sulla stessa riga della condizione di verifica rende necessario l'uso del punto e virgola.

until [ condizione-falsa ] ; do

Esempio 10-18. Ciclo until

#!/bin/bash

CONDIZIONE_CONCLUSIONE=fine

until [ "$var1" = "$CONDIZIONE_CONCLUSIONE" ]
# Condizione di verifica all'inizio del ciclo.
do
  echo "Immetti variabile nr.1 "
  echo "($CONDIZIONE_CONCLUSIONE per terminare)"
  read var1
  echo "variabile nr.1 = $var1"
  echo
done  

exit 0

Note

[1]

Iterazione: esecuzione ripetuta di un comando, o gruppo di comandi, finché perdura una data condizione, o finché una data condizione viene soddisfatta.