Capitolo 23. Funzioni

Sommario
23.1. Funzioni complesse e complessità delle funzioni
23.2. Variabili locali
23.3. Ricorsività senza variabili locali

Come i "veri" linguaggi di programmazione, anche Bash dispone delle funzioni, sebbene in un'implementazione un po' limitata. Una funzione è una subroutine, un blocco di codice che rende disponibile una serie di operazioni, una "scatola nera" che esegue un compito specifico. Ogni qual volta vi è del codice che si ripete o quando un compito viene iterato con leggere variazioni, allora è il momento di prendere in considerazione l'impiego di una funzione.

function nome_funzione {
comando...
}

oppure

nome_funzione () {
comando...
}

Questa seconda forma è quella che rallegra i cuori dei programmatori C (ed è più portabile).

Come nel C, la parentesi graffa aperta può, opzionalmente, comparire nella riga successiva a quella del nome della funzione.

nome_funzione ()
{
comando...
}

Nota

Una funzione può essere "compattata" su un'unica riga.

fun () { echo "Questa ` una funzione"; echo; }

In questo caso, però, occorre mettere un punto e virgola dopo l'ultimo comando della funzione.

fun () { echo "Questa è una funzione"; echo } # Errore!

Le funzioni vengono richiamate, messe in esecuzione, semplicemente invocando i loro nomi.

Esempio 23-1. Semplici funzioni

#!/bin/bash

SOLO_UN_SECONDO=1

strana ()
{ # Questo a proposito della semplicà delle funzioni.
  echo "Questa è una funzione strana."
  echo "Ora usciamo dalla funzione strana."
} # La dichiarazione della funzione deve precedere la sua chiamata.


divertimento ()
{ # Una funzione un po' pi complessa.
  i=0
  RIPETIZIONI=30

  echo
  echo "Ed ora, che il divertimento abbia inizio."
  echo

  sleep $SOLO_UN_SECONDO    # Hey, aspetta un secondo!
  while [ $i -lt $RIPETIZIONI ]
  do
    echo "--------LE FUNZIONI--------->"
    echo "<----------SONO--------------"
    echo "<--------DIVERTENTI--------->"
    echo
    let "i+=1"
  done
}
  
  # Ora, richiamiamo le funzioni.

strana
divertimento

exit 0

La definizione della funzione deve precedere la sua prima chiamata. Non esiste alcun metodo per "dichiarare" la funzione, come, ad esempio, nel C.

f1
# Dà un messaggio d'errore poiché la funzione "f1" non è stata ancora definita.

declare -f f1      # Neanche questo aiuta.
f1                 # Ancora un messaggio d'errore.

# Tuttavia...

	  
f1 ()
{
  echo "Chiamata della funzione \"f2\" dalla funzione \"f1\"."
  f2
}

f2 ()
{
  echo "Funzione \"f2\"."
}

f1  #  La funzione "f2", in realtà, viene chiamata solo a questo punto,
    #+ sebbene vi si faccia riferimento prima della sua definizione.
    #  Questo è consentito.
    
    # Grazie, S.C.

È anche possibile annidare una funzione in un'altra, sebbene non sia molto utile.

f1 ()
{

  f2 () # annidata
  {
      echo "Funzione \"f2\", all'interno di \"f1\"."
  }

}  

f2  # Restituisce un messaggio d'errore.
    # Sarebbe inutile anche farla precedere da "declare -f f2".

echo

f1  #  Non fa niente, perché richiamare"f1" non implica richiamare 
    #+ automaticamente "f2".
f2  #  Ora è tutto a posto, "f2" viene eseguita, perché la sua 
    #+ definizione è stata resa visibile tramite la chiamata di "f1".

    # Grazie, S.C.

Le dichiarazioni di funzione possono comparire in posti impensati, anche dove dovrebbe trovarsi un comando.

ls -l | foo() { echo "foo"; }  # Consentito, ma inutile.



if [ "$USER" = bozo ]
then
  saluti_bozo ()   # Definizione di funzione inserita in un costrutto if/then.
  {
      echo "Ciao, Bozo."
  }
fi  

saluti_bozo        # Funziona solo per Bozo, agli altri utenti dà un errore.


# Qualcosa di simile potrebbe essere utile in certi contesti.
NO_EXIT=1          # Abilita la definizione di funzione seguente.

[[ $NO_EXIT -eq 1 ]] && exit() { true; }     #  Definizione di funzione 
                                             #+ in una "lista and".
# Se $NO_EXIT è uguale a 1, viene dichiarata "exit ()".
# Così si disabilita il builtin "exit" rendendolo un alias di "true".

exit  # Viene invocata la funzione "exit ()", non il builtin "exit".

# O, in modo simile:
nomefile=file1

[ -f "$nomefile" ] &&
foo () { rm -f "$nomefile"; echo "File "$nomefile" cancellato."; } ||
foo () { echo "File "$nomefile" non trovato."; touch bar; }

foo

# Grazie, S.C. e Christopher Head

23.1. Funzioni complesse e complessità delle funzioni

Le funzioni possono elaborare gli argomenti che ad esse vengono passati e restituire un exit status allo script per le successive elaborazioni.

nome_funzione $arg1 $arg2

La funzione fa riferimento agli argomenti passati in base alla loro posizione (come se fossero parametri posizionali), vale a dire, $1, $2, eccetera.

Esempio 23-2. Funzione con parametri

#!/bin/bash
# Funzioni e parametri

DEFAULT=predefinito               # Valore predefinito del parametro

funz2 () {
   if [ -z "$1" ]                 # Il parametro nr.1 è vuoto (lunghezza zero)?
   then
        echo "-Il parametro nr.1 ha lunghezza zero.-"  #  O non è stato passato
                                                       #+ alcun parametro.
   else
        echo "-Il parametro nr.1 è \"$1\".-"
   fi

   variabile=${1-$DEFAULT}        #  Cosa rappresenta
   echo "variabile = $variabile"  #+ la sostituzione di parametro?
                                  #  -------------------------------------
                                  #  Fa distinzione tra nessun parametro e 
                                  #+ parametro nullo.

   if [ "$2" ]
   then
        echo "-Il parametro nr.2 è \"$2\".-"
   fi

   return 0
}

echo
   
echo "Non viene passato niente."
funz2                          # Richiamata senza alcun parametro
echo


echo "Viene passato un parametro vuoto."
funz2 ""                       # Richiamata con un parametro di lunghezza zero
echo

echo "Viene passato un parametro nullo."
funz2 "$param_non_inizializ"   # Richiamata con un parametro non inizializzato
echo

echo "Viene passato un parametro."
funz2 primo           # Richiamata con un parametro
echo

echo "Vengono passati due parametri."
funz2 primo secondo   # Richiamata con due parametri
echo

echo "Vengono passati \"\" \"secondo\"."
funz2 "" secondo      # Richiamata con il primo parametro di lunghezza zero
echo                  # e una stringa ASCII come secondo.

exit 0

Importante

Il comando shift opera sugli argomenti passati alle funzioni (vedi Esempio 33-15).

Ma, cosa si può dire a proposito degli argomenti passati ad uno script da riga di comando? Una funzione è in grado di rilevarli? Bene, vediamo di chiarire l'argomento.

Esempio 23-3. Funzioni e argomenti passati allo script da riga di comando

#!/bin/bash
# func-cmdlinearg.sh
#  Eseguite lo script con un argomento da riga di comando,
#+ qualcosa come $0 arg1.


funz ()

{
echo "$1"
}

echo "Prima chiamata della funzione: non viene passato alcun argomento."
echo "Vediamo se l'argomento da riga di comando viene rilevato."
funz
# No! Non è stato rilevato.

echo "============================================================"
echo
echo "Seconda chiamata della funzione:\  
 argomento da riga di comado passato esplicitamente."
funz $1
# Ora è stato rilevato!

exit 0

Rispetto ad alcuni altri linguaggi di programmazione, gli script di shell normalmente passano i parametri alle funzioni solo per valore. I nomi delle variabili (che in realtà sono dei puntatori), se passati come parametri alle funzioni, vengono trattati come stringhe. Le funzioni interpretano i loro argomenti letteralmente.

La referenziazione indiretta a variabili (vedi Esempio 34-2) offre una specie di meccanismo, un po' goffo, per passare i puntatori a variabile alle funzioni.

Esempio 23-4. Passare una referenziazione indiretta a una funzione

#!/bin/bash
# ind-func.sh: Passare una referenziazione indiretta a una funzione.

var_echo ()
{
echo "$1"
}

messaggio=Ciao
Ciao=Arrivederci

var_echo "$messaggio"      # Ciao
# Adesso passiamo una referenziazione indiretta alla funzione.
var_echo "${!messaggio}"   # Arrivederci

echo "-------------"

# Cosa succede se modifichiamo il contenuto della variabile "Ciao"?
Ciao="Ancora ciao!"
var_echo "$messaggio"      # Ciao
var_echo "${!messaggio}"   # Ancora ciao!

exit 0

La domanda logica successiva è se i parametri possono essere dereferenziati dopo essere stati passati alla funzione.

Esempio 23-5. Dereferenziare un parametro passato a una funzione

#!/bin/bash
# dereference.sh
# Dereferenziare un parametro passato ad una funzione.
# Script di Bruce W. Clare.

dereferenzia ()
{
     y=\$"$1"   # Nome della variabile.
     echo $y    # $Prova

     x=`eval "expr \"$y\" "`
     echo $1=$x
     eval "$1=\"Un testo diverso \""  # Assegna un nuovo valore.
}

Prova="Un testo"
echo $Prova "prima"    # Un testo prima

dereferenzia Prova
echo $Prova "dopo"     # Un testo diverso dopo

exit 0

Esempio 23-6. Ancora, dereferenziare un parametro passato a una funzione

#!/bin/bash
# ref-params.sh: Dereferenziare un parametro passato a una funzione.
#                (Esempio complesso)

ITERAZIONI=3  # Numero di input da immettere.
contai=1

lettura () {
  #  Richiamata nella forma lettura nomevariabile,
  #+ visualizza il dato precedente tra parentesi quadre come dato predefinito,
  #+ quindi chiede un nuovo valore.

  local var_locale

  echo -n "Inserisci un dato "
  eval 'echo -n "[$'$1'] "'  #  Dato precedente.
# eval echo -n "[\$$1] "     #  Più facile da capire,
                             #+ ma si perde lo spazio finale al prompt.
  read var_locale
  [ -n "$var_locale" ] && eval $1=\$var_locale

  #  "Lista And": se "var_locale" è presente allora viene impostata 
  #+ al valore di "$1".
}

echo

while [ "$contai" -le "$ITERAZIONI" ]
do
  lettura var
  echo "Inserimento nr.$contai = $var"
  let "contai += 1"
  echo
done


# Grazie a Stephane Chazelas per aver fornito questo istruttivo esempio.

exit 0

Exit e Return

exit status

Le funzioni restituiscono un valore, chiamato exit status. L'exit status può essere specificato in maniera esplicita con l'istruzione return, altrimenti corrisponde all'exit status dell'ultimo comando della funzione (0 in caso di successo, un codice d'errore diverso da zero in caso contrario). Questo exit status può essere usato nello script facendovi riferimento tramite $?. Questo meccanismo consente alle funzioni di avere un "valore di ritorno" simile a quello delle funzioni del C.

return

Termina una funzione. Il comando return [1] può avere opzionalmente come argomento un intero, che viene restituito allo script chiamante come "exit status" della funzione. Questo exit status viene assegnato alla variabile $?.

Esempio 23-7. Il maggiore di due numeri

#!/bin/bash
# max.sh: Maggiore di due numeri.

E_ERR_PARAM=-198    # Se vengono passati meno di 2 parametri alla funzione.
UGUALI=-199         # Valore di ritorno se i due numeri sono uguali.
#  Errore per i valori fuori intervallo passati come parametri alla funzione.

max2 ()             # Restituisce il maggiore di due numeri.
{                   # Nota: i numeri confrontati devono essere minori di 257.
if [ -z "$2" ]
then
  return $E_ERR_PARAM
fi

if [ "$1" -eq "$2" ]
then
  return $UGUALI
else
  if [ "$1" -gt "$2" ]
  then
    return $1
  else
    return $2
  fi
fi
}

max2 33 34
val_ritorno=$?

if [ "$val_ritorno" -eq $E_ERR_PARAM ]
then
  echo "Bisogna passare due parametri alla funzione."
elif [ "$val_ritorno" -eq $UGUALI ]
  then
    echo "I due numeri sono uguali."
else
    echo "Il maggiore dei due numeri è $val_ritorno."
fi

  
exit 0

#  Esercizio (facile):
#  ------------------
#  Trasformatelo in uno script interattivo,
#+ cioè, deve essere lo script a richiedere l'input (i due numeri).

Suggerimento

Per fare in modo che una funzione possa restituire una stringa o un array , si deve fare ricorso ad una variabile dedicata.

conteggio_righe_di_etc_passwd()
{
  [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
  #  Se /etc/passwd ha i permessi di lettura, imposta REPLY al 
  #+ numero delle righe.
  #  Restituisce o il valore del parametro o un'informazione di stato.
  # 'echo' sembrerebbe non necessario, ma . . .
  #+ rimuove dall'output gli spazi in eccesso.
}

if conteggio_righe_di_etc_passwd
then
  echo "Ci sono $REPLY righe in /etc/passwd."
else
  echo "Non posso contare le righe in /etc/passwd."
fi

# Grazie, S.C.

Esempio 23-8. Convertire i numeri arabi in numeri romani

#!/bin/bash

# Conversione di numeri arabi in numeri romani
# Intervallo: 0 - 200
# È rudimentale, ma funziona.

#  Viene lasciato come esercizio l'estensione dell'intervallo e 
#+ altri miglioramenti dello script.

# Utilizzo: numero da convertire in numero romano

LIMITE=200
E_ERR_ARG=65
E_FUORI_INTERVALLO=66

if [ -z "$1" ]
then
  echo "Utilizzo: `basename $0` numero-da-convertire"
  exit $E_ERR_ARG
fi

num=$1
if [ "$num" -gt $LIMITE ]
then
  echo "Fuori intervallo!"
  exit $E_FUORI_INTERVALLO
fi

calcola_romano ()   # Si deve dichiarare la funzione prima di richiamarla.
{
numero=$1
fattore=$2
c_romano=$3
let "resto = numero - fattore"
while [ "$resto" -ge 0 ]
do
  echo -n $c_romano
  let "numero -= fattore"
  let "resto = numero - fattore"
done

return $numero
       # Esercizio:
       # ----------
       # Spiegate come opera la funzione.
       # Suggerimento: divisione per mezzo di sottrazioni successive.
}
   

calcola_romano $num 100 C
num=$?
calcola_romano $num 90 LXXXX
num=$?
calcola_romano $num 50 L
num=$?
calcola_romano $num 40 XL
num=$?
calcola_romano $num 10 X
num=$?
calcola_romano $num 9 IX
num=$?
calcola_romano $num 5 V
num=$?
calcola_romano $num 4 IV
num=$?
calcola_romano $num 1 I

echo

exit 0

Vedi anche Esempio 10-28.

Importante

Il più grande intero positivo che una funzione può restituire è 255. Il comando return è strettamente legato al concetto di exit status, e ciò è la causa di questa particolare limitazione. Fortunatamente, esistono diversi espedienti per quelle situazioni che richiedono un valore di ritorno della funzione maggiore di 255.

Esempio 23-9. Verificare valori di ritorno di grandi dimensioni in una funzione

#!/bin/bash
# return-test.sh

# Il maggiore valore positivo che una funzione può restituire è 255.

val_ritorno ()         # Restituisce tutto quello che gli viene passato.
{
  return $1
}

val_ritorno 27         # o.k.
echo $?                # Restituisce 27.

val_ritorno 255        # Ancora o.k.
echo $?                # Restituisce 255.

val_ritorno 257        # Errore!
echo $?                # Restituisce 1 (codice d'errore generico).

# ============================================================
val_ritorno -151896    # Funziona con grandi numeri negativi?
echo $?                # Restituirà -151896?
                       # No! Viene restituito 168.
#  Le versioni di Bash precedenti alla 2.05b permettevano
#+ valori di ritorno di grandi numeri negativi.
#  Quelle più recenti non consentono questa scappatoia.
#  Ciò potrebbe rendere malfunzionanti i vecchi script.
#  Attenzione!
# ============================================================

exit 0

Un espediente per ottenere un intero di grandi dimensioni consiste semplicemente nell'assegnare il "valore di ritorno" ad una variabile globale.

Val_Ritorno=  #  Variabile globale che contiene un valore di ritorno
              #+ della funzione maggiore di 255.

ver_alt_ritorno ()
{
  fvar=$1
  Val_Ritorno=$fvar
  return      # Restituisce 0 (successo).
}

ver_alt_ritorno 1
echo $?                                    # 0
echo "valore di ritorno = $Val_Ritorno"    # 1

ver_alt_ritorno 256
echo "valore di ritorno = $Val_Ritorno"    # 256

ver_alt_ritorno 257
echo "valore di ritorno = $Val_Ritorno"    # 257

ver_alt_ritorno 25701
echo "valore di ritorno = $Val_Ritorno"    # 25701

Un metodo anche più elegante consiste nel visualizzare allo stdout il "valore di ritorno" della funzione con il comando echo e poi "catturarlo" per mezzo della sostituzione di comando. Per una discussione sull'argomento vedi la Sezione 33.7.

Esempio 23-10. Confronto di due interi di grandi dimensioni

#!/bin/bash
# max2.sh: Maggiore di due GRANDI interi.

#  È il precedente esempio "max.sh" ,
#+ modificato per consentire il confronto di grandi numeri.

UGUALI=0            # Valore di ritorno se i due parametri sono uguali.
E_ERR_PARAM=-99999  # Numero di parametri passati alla funzione insufficiente.
#           ^^^^^^    Nessun parametro passato avrà mai un valore simile. 

max2 ()             # "Restituisce" il maggiore di due numeri.
{
if [ -z "$2" ]
then
  echo $E_ERR_PARAM
  return
fi

if [ "$1" -eq "$2" ]
then
  echo $UGUALI
  return
else
  if [ "$1" -gt "$2" ]
  then
      valritorno=$1
  else
      valritorno=$2
  fi
fi

echo $valritorno    # Visualizza (allo stdout) il valore invece di restituirlo.
                    # Perché?

}


val_ritorno=$(max2 33001 33997)
#             ^^^^              Nome della funzione
#                  ^^^^^ ^^^^^  Parametri passati
#  Si tratta, in realtà, di una forma di sostituzione di comando:
#+ che tratta una funzione come se fosse un comando
#+ e che assegna lo stdout della funzione alla variabile "val_ritorno".


# =============================== RISULTATO ==============================
if [ "$val_ritorno" -eq "$E_ERR_PARAM" ]
then
  echo "Errore nel numero di parametri passati alla funzione di confronto!"
elif [ "$val_ritorno" -eq "$UGUALI" ]
  then
      echo "I due numeri sono uguali."
  else
      echo "Il maggiore dei due numeri è $val_ritorno."
fi  
# ========================================================================

exit 0

#  Esercizi:
#  --------
#  1) Trovate un modo più elegante per verificare
#+    il numero di parametri passati alla funzione.
#  2) Semplificate la struttura if/then presente nel blocco "RISULTATO."
#  3) Riscrivete lo script in modo che l'input sia dato dai parametri passati
#+    da riga di comando.

Ecco un altro esempio di " cattura" del "valore di ritorno" di una funzione. Per comprenderlo è necessario conoscere un po' awk.

durata_mese ()  #  Vuole come argomento il numero 
                #+ del mese.
{               #  Restituisce il numero dei giorni del mese.
Gmese="31 28 31 30 31 30 31 31 30 31 30 31"  # Dichiarata come locale?
echo "$Gmese" | awk '{ print $'"${1}"' }'    # Trucco.
#                            ^^^^^^^^^
# Parametro passato alla funzione  ($1 -- numero del mese) e poi a awk.
# Awk lo vede come "print $1 . . . print $12" (secondo il numero del mese)
# Modello per il passaggio di un parametro a uno script awk incorporato:
#                                               $'"${parametro_script}"'

#  È necessaria una verifica di correttezza dell'intervallo (1-12)
#+ e dei giorni di febbraio per gli anni bisestili.
}

# ----------------------------------------------
# Esempio di utilizzo:
mese=4           # aprile, (4o mese).
nr_giorni=$(durata_mese $mese)
echo $nr_giorni  # 30
# ----------------------------------------------

Vedi anche Esempio A-7.

Esercizio: Utilizzando le conoscenze fin qui acquisite, si estenda il precedente esempio dei numeri romani in modo che accetti un input arbitrario maggiore di 255.

Redirezione

Redirigere lo stdin di una funzione

Una funzione è essenzialmente un blocco di codice, il che significa che il suo stdin può essere rediretto (come in Esempio 3-1).

Esempio 23-11. Il vero nome dal nome utente

#!/bin/bash
# realname.sh

# Partendo dal nome dell'utente, ricava il "vero nome" da /etc/passwd.


CONTOARG=1  # Si aspetta un argomento.
E_ERR_ARG=65

file=/etc/passwd
modello=$1

if [ $# -ne "$CONTOARG" ]
then
  echo "Utilizzo: `basename $0` NOME-UTENTE"
  exit $E_ERR_ARG
fi

ricerca ()      #  Esamina il file alla ricerca del modello, quindi visualizza
                #+ la parte rilevante della riga.
{
while read riga # "while" non necessariamente vuole la "[ condizione]"
do
  echo "$riga" | grep $1 | awk -F":" '{ print $5 }' #  awk deve usare 
                                                    #+ i ":" come delimitatore.
done
} <$file  # Redirige nello stdin della funzione.

ricerca $modello

# Certo, l'intero script si sarebbe potuto ridurre a
#       grep MODELLO /etc/passwd | awk -F":" '{ print $5 }'
# oppure
#       awk -F: '/MODELLO/ {print $5}'
# oppure
#       awk -F: '($1 == "nomeutente") { print $5 }' #  il vero nome dal 
                                                    #+ nome utente
# Tuttavia, non sarebbe stato altrettanto istruttivo.

exit 0

Esiste un metodo alternativo, che confonde forse meno, per redirigere lo stdin di una funzione. Questo comporta la redirezione dello stdin in un blocco di codice compreso tra parentesi graffe all'interno della funzione.

# Invece di:
Funzione ()
{
 ...
} < file

# Provate:
Funzione ()
{
  {
    ...
   } < file
}

# Analogamente,

Funzione ()  # Questa funziona.
{
  {
  echo $*
  } | tr a b
}

Funzione ()  # Questa, invece, no.
{
  echo $*
} | tr a b   # In questo caso è obbligatorio il blocco di codice annidato.


# Grazie, S.C.

Note

[1]

Il comando return è un builtin Bash.