Spesso, progettando uno script complesso, è utile suddividerlo in sotto-programmi che svolgono un compito più circoscritto e che possono anche essere riutilizzati in altri script. Spesso anzi è utile raccogliere in uno o più file dei sotto-programmi di utilità comune, in modo tale da comporre una libreria con cui si possono sviluppare nuovi script più rapidamente, senza dover ogni volta riscrivere anche le procedure già utilizzate in altri casi.
La Bash ci permette di implementare questa prassi comune a molti altri linguaggi di programmazione attraverso la definizione di funzioni. Come avviene in linguaggio C, una funzione non è altro che un “sotto-programma”, identificato da un nome. Ad una funzione, come vedremo tra breve, è possibile passare una lista di parametri e la funzione può restituire un valore numerico intero.
Una funzione viene definita utilizzando l'istruzione “function” seguita dal nome identificativo della funzione stessa. Il nome della funzione deve essere differente dalle parole chiave del linguaggio Bash e da ogni altro nome di funzione e di variabile definito nell'ambito dello stesso script. Il “corpo” della funzione, le istruzioni del sottoprogramma che ne definiscono il comportamento, sono delimitate da una coppia di parentesi graffe:
1 function saluta {
2 echo "Ciao!"
3 }
La funzione può essere dichiarata anche omettendo l'istruzione “function”, ma in tal caso il nome della funzione deve essere seguito da una coppia di parentesi tonde:
1 saluta() {
2 echo "Ciao!"
3 }
Le istruzioni presenti nel corpo di una funzione vengono eseguite solo quando la funzione viene richiamata. La funzione viene richiamata dallo script Bash in cui è stata definita, semplicemente utilizzandone il nome, come se si trattasse di un qualsiasi altro comando interno della shell:
1 #!/bin/bash
2 function saluta {
3 echo -n "Ciao! "
4 }
5 for ((i=0; i<3; i++)); do
6 saluta
7 done
Nelle righe 2, 3 e 4 viene definita la funzione “saluta” che visualizza la stringa “Ciao!” su standard output. Nelle righe 5, 6 e 7 viene definito un ciclo che esegue tre iterazioni utilizzando l'istruzione “for”; ad ogni iterazione del ciclo, con l'istruzione a riga 6 viene richiamata la funzione “saluta”. L'output di questo banale esempio (“saluti.sh”) è il seguente:
$ ./saluti.sh
Ciao! Ciao! Ciao!
Lo script deve essere scritto in modo tale che la funzione venga definita prima di essere richiamata da un'istruzione dello stesso script: la definizione della funzione deve sempre precedere la prima delle istruzioni in cui la funzione viene invocata.
Naturalmente in uno stesso script possono essere definite più funzioni, purché siano identificate da nomi differenti. Una funzione può essere anche invocata da un'altra funzione; in questo caso l'ordine con cui sono definite le funzioni non ha nessuna importanza (è possibile definire la funzione “chiamante” prima della funzione “chiamata”).
1 #!/bin/bash
2 function prima {
3 echo "Prima funzione."
4 seconda
5 echo "Ancora la prima funzione."
6 }
7 function seconda {
8 echo "Seconda funzione."
9 }
10 echo "Inizio."
11 prima
Nell'esempio precedente lo script (“funzioni.sh”) invoca la funzione “prima” (riga 11), che a sua volta richiama la funzione “seconda”; l'output prodotto è il seguente:
$ ./funzioni.sh
Inizio.
Prima funzione.
Seconda funzione.
Ancora la prima funzione.
Tutte le funzioni definite nell'ambito di uno script condividono fra loro le variabili, tranne le variabili che sono state definite nell'ambito di una funzione utilizzando l'istruzione “local”. Questa istruzione restringe lo scope, ossia la visibilità di una variabile, alla sola funzione in cui la variabile viene definita; le altre funzioni potranno definire variabili differenti con lo stesso nome, ma non potranno accedere al contenuto di una variabile locale di un'altra funzione.
1 #!/bin/bash
2
3 function pippo {
4 local a
5 a=2
6 echo "Pippo: a = $a"
7 }
8
9 function pluto {
10 echo "Pluto: a = $a"
11 a=3
12 echo "Pluto: a = $a"
13 }
14
15 a=1
16 echo "a = $a"
17 pippo
18 echo "a = $a"
19 pluto
20 echo "a = $a"
L'esempio precedente mette in evidenza alcuni aspetti relativi allo scope delle variabili di uno script Bash. Lo script presenta due funzioni (“pippo” e “pluto”): nella prima viene definita una variabile locale chiamata a (riga 4) e gli viene assegnato il valore 2 (riga 5). Anche nel “corpo principale” dello script viene definita una variabile chiamata a assegnandogli il valore 1 (riga 15). Le due variabili sono differenti pur essendo identificate dallo stesso nome. Quest'ultima, quella definita nel corpo principale dello script, è visibile da tutte le funzioni dello script, tranne che nella funzione “pippo”, in cui è stata definita una variabile locale con lo stesso nome (riga 4); questa variabile è visibile solo nella funzione “pippo”.
La modifica del valore della variabile a ha impatto sulla variabile locale, con l'istruzione a riga 5, mentre ha effetto sulla variabile “globale” con le istruzioni a riga 11 e 15. Di seguito riportiamo l'output dell'esecuzione dello script:
$ ./varLocali.sh
a = 1
Pippo: a = 2
a = 1
Pluto: a = 1
Pluto: a = 3
a = 3
È possibile invocare una funzione riportando uno o più parametri sulla stessa riga, come argomento della funzione stessa. La funzione riceve i parametri nelle variabili $1, $2, ecc. secondo l'ordine con cui i parametri stessi sono elencati dopo il nome della funzione. Come per i parametri passati sulla linea di comando allo shell script, anche nel caso dei parametri passati ad una funzione si può utilizzare l'istruzione “shift” per eliminare dalla coda dei parametri il primo elemento.
1 #!/bin/bash
2 function saluta {
3 while [ $1 ]; do
4 echo "Ciao $1"
5 shift
6 done
7 }
8
9 saluta 'Marina' 'Chiara' 'Elena'
L'output della funzione precedente è il seguente:
$ ./ciao.sh
Ciao Marina
Ciao Chiara
Ciao Elena
La funzione può anche restituire un valore numerico intero utilizzando l'istruzione “return”. Il valore restituito da una funzione mediante l'istruzione return viene memorizzato nella variabile “$?”. Ad esempio nel seguente script si fa uso della funzione “somma” per calcolare la somma di tutti gli elementi passati come argomento della funzione.
1 #!/bin/bash
2 function somma {
3 local s=0
4 while [ $1 ]; do
5 ((s=s+$1))
6 shift
7 done
8 return $s
9 }
10
11 somma ${BASH_ARGV[*]}
12 echo "Somma = $?"
I numeri di cui si intende calcolare la somma vengono forniti allo script sulla riga di comando; in questo modo, utilizzando il vettore BASH_ARGV, a riga 11 viene invocata la funzione “somma” passandogli come argomento tutto il vettore. La funzione esegue un ciclo (righe 4--7) e accumula nella variabile locale s la somma degli elementi passati come argomento (a riga 5, sebbene l'espressione di somma sia delimitata dalle doppie parentesi tonde, per fare riferimento alla variabile speciale $1 è necessario utilizzare comunque il simbolo “$” perché la variabile ha un nome che altrimenti sarebbe impossibile distinguere dal valore numerico 1); quindi restituisce il valore finale della sommatoria utilizzando l'istruzione return (riga 8). Il valore restituito dalla funzione viene stampato utilizzando la variabile “$?” (riga 12).
$ ./sommatoria 10 20 30
Somma = 60
Esiste un modo meno canonico, ma ugualmente efficace, per fare in modo che le funzioni restituiscano qualsiasi tipo di dato, non solo numerico intero. Il metodo è valido se la funzione non produce nessun output su standard output. In questo caso, la funzione può utilizzare il comando “echo” per produrre su standard output il valore da restituire. La chiamata della funzione avverrà utilizzando la seguente sintassi:
variabile=$(funzione argomento)
In questo modo la stringa prodotta in output mediante il comando “echo” dalla funzione, sarà assegnato alla variabile. Ad esempio lo script precedente che calcola la somma dei numeri forniti sulla command line, può essere riscritto come segue:
1 #!/bin/bash
2 function somma {
3 local s=0
4 while [ $1 ]; do
5 ((s=s+$1))
6 shift
7 done
8 echo $s
9 }
10
11 sum=$(somma ${BASH_ARGV[*]})
12 echo "Somma = $sum"
Con l'istruzione di riga 11 alla variabile sum viene assegnato esplicitamente il valore restituito (mediante il comando “echo”, non “return”) dalla funzione somma, ottenendo una sintassi simile a quella di molti altri linguaggi di programmazione come, ad esempio, il C, il Pascal o altri ancora.
Il linguaggio Bash supporta le “funzioni ricorsive”, ossia funzioni definite in modo tale che nel corpo della funzione venga richiamata la funzione stessa.
Un esempio classico è dato dalla funzione fattoriale. In matematica, per denotare il “fattoriale di n” si usa il simbolo “n!” e con tale notazione si indica il prodotto dei primi n numeri naturali: n! = 1 ⋅ 2 ⋅ 3 ⋅ … ⋅ (n−1) ⋅ n. Ad esempio 4! = 4 ⋅ 3 ⋅ 2 ⋅ 1; osservando con attenzione questo esempio elementare ci accorgiamo che 4! = 4 ⋅ 3! (ovviamente 3! = 3 ⋅ 2 ⋅ 1).
Infatti è possibile definire la stessa funzione anche in un altro modo, utilizzando una tecnica “induttiva” basata sulla seguente espressione:
n! = n ⋅ (n−1)! se n>1
n! = 1 se n=1
Il caso n=1 è facile e costituisce la cosiddetta “base del procedimento ricorsivo”: se n=1 allora n! = 1. Se invece n>1 allora possiamo calcolare n! come prodotto tra n ed il valore di (n−1)!: n! = n ⋅ (n−1)! Sfruttando la “ricorsione” applicando più volte la definizione di n! su numeri sempre più piccoli, alla fine arriveremo a calcolare il fattoriale di n. Ad esempio 4! = 4 ⋅ 3! = 4 ⋅ (3 ⋅ 2!) = 4 ⋅ (3 ⋅ (2 ⋅ 1)) = 4 ⋅ (3 ⋅ 2) = 4 ⋅ 6 = 24.
Utilizzando il linguaggio Bash possiamo scrivere il seguente script che utilizza la funzione “fattoriale” definita in modo ricorsivo, per calcolare il fattoriale del numero intero positivo passato sulla linea di comando:
1 #!/bin/bash
2 # Calcola il fattoriale del numero passato sulla linea di comando
3 # utilizzando un algoritmo ricorsivo
4
5 function fattoriale {
6 if [ $1 -gt 1 ]; then
7 ((a=$1-1))
8 fattoriale $a
9 ((f=$1*$?))
10 else
11 f=1
12 fi
13 return $f
14 }
15
16 fattoriale $1
17 echo "$1! = $?"
A riga 8 la funzione “fattoriale” richiama se stessa passando come parametro un argomento diminuito di un'unità: questo è il cuore del procedimento ricorsivo.
Eseguendo lo script si ottiene il risultato riportato di seguito; è bene osservare che le capacità di calcolo numerico della Bash sono limitate, per cui anche con numeri piccoli (n>5) il calcolo del fattoriale “sballa” fornendo risultati non esatti.
$ ./fattoriale.sh 5
5! = 120
Può essere molto utile raccogliere in uno o più file un insieme di funzioni di uso comune, in modo tale da comporre una “libreria” riutilizzabile in più script. Con Bash una libreria non è altro che un file in cui sono riportate delle funzioni, anche del tutto indipendenti fra di loro; nel file non devono però essere presenti istruzioni esterne al corpo delle funzioni.
Per poter usare la libreria, importando le funzioni che vi sono state definite in uno shell script, si deve utilizzare il comando “source”, che consente di importare in uno script le istruzioni (le definizioni delle funzioni, nel nostro caso) contenute in un altro file.
Ad esempio supponiamo di raccogliere nel file “libreria.sh” le funzioni “multipli”, “somma” e “prodotto” definite di seguito:
1 # Libreria di funzioni per Bash
2 function multipli {
3 local n=$1
4 local k=$2
5 echo "I primi $k multipli di $n:"
6 local i=1
7 while [ $i -le $k ]; do
8 ((x=n*i))
9 echo -n "$x "
10 ((i++))
11 done
12 echo
13 }
14
15 function somma {
16 local s=0
17 while [ $1 ]; do
18 ((s=s+$1))
19 shift
20 done
21 return $s
22 }
23
24 function prodotto {
25 local p=1
26 while [ $1 ]; do
27 ((p=p*$1))
28 shift
29 done
30 return $p
31 }
Nello script “sommaProdotto.sh” costruiamo una procedura che utilizza alcune delle funzioni definire nella libreria. Naturalmente prima di poter usare tali funzioni è necessario “caricare” la libreria utilizzando il comando “source” (riga 5).
1 #!/bin/bash
2 # sommaProdotto.sh
3 # Calcola la somma e il prodotto dei numeri passati come argomento
4 # sulla command line
5 source "./libreria.sh"
6 somma ${BASH_ARGV[*]}
7 echo "Somma = $?"
8 prodotto ${BASH_ARGV[*]}
9 echo "Prodotto = $?"
Una sintassi alternativa al comando “source” è costituita dal carattere “.” (punto). La riga 5 dello script precedente potrebbe quindi essere riscritta come segue:
. "./libreria.sh"
Il comando “source” (e il comando “.”) non si limita a caricare le istruzioni contenute nel file: se sono presenti delle istruzioni esterne alle funzioni, le esegue.
Comandi interni, esterni e composti
Variabili, varibili d'ambiente e variabili speciali