Guida avanzata di scripting Bash: Un'approfondita esplorazione dell'arte dello scripting di shell | ||
---|---|---|
Indietro | Avanti |
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...
}
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...
}
Una funzione può essere "compattata" su un'unica riga.
In questo caso, però, occorre mettere un punto e virgola dopo l'ultimo comando della funzione.
|
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 |
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 |
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 |
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.
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). |
Per fare in modo che una funzione possa restituire una stringa o un array , si deve fare ricorso ad una variabile dedicata.
|
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.
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
Un espediente per ottenere un intero di grandi dimensioni consiste semplicemente nell'assegnare il "valore di ritorno" ad una variabile globale.
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
Ecco un altro esempio di " cattura" del "valore di ritorno" di una funzione. Per comprenderlo è necessario conoscere un po' awk.
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. |
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. |
[1] | Il comando return è un builtin Bash. |