Bash scripting - funzioni: differenze tra le versioni

m
 
(4 versioni intermedie di 2 utenti non mostrate)
Riga 1: Riga 1:
{{Bash scripting}}
{{Bash scripting}}
=Funzioni=
__TOC__
Una funzione è un raggruppamento di comandi eseguito ogni volta che il nome della funzione è utilizzato come un comando. Per convenzione le definizioni delle funzioni precedono il corpo principale dello script.
Una funzione è un raggruppamento di comandi eseguito ogni volta che il nome della funzione è utilizzato come un comando. Per convenzione le definizioni delle funzioni precedono il corpo principale dello script.


Riga 16: Riga 16:
È buona prassi usare i commenti per specificare le opzioni richieste dalla funzione, in modo da rendere chiaro a chi legge il codice come verranno utilizzati.
È buona prassi usare i commenti per specificare le opzioni richieste dalla funzione, in modo da rendere chiaro a chi legge il codice come verranno utilizzati.


In '''bash''' è possibile premettere <code>function</code> al nome della funzione, anche senza bisogno di <code>()</code>, ma questa possibilità non è prevista da ''POSIX''.
In '''bash''' è possibile premettere <code>function</code> al nome della funzione, anche senza bisogno di <code>()</code>, ma questa possibilità non è prevista da [[POSIX]].


Nel corpo di una funzione è possibile utilizzare il modificatore <code>local</code> prima di un'assegnazione per rendere la variabile locale, ossia non accessibile fuori dal corpo della funzione. È bene evitare le variabili globali il più possibile (se non sono costanti), ricorrendo ai passaggi di parametri, per evitare la modifica accidentale di variabili esistenti fuori dalla funzione. Questo modificatore non è previsto da ''POSIX'', dove ogni variabile è globale, ma è comunque supportato da ogni moderna shell compatibile e caldamente consigliato.
Nel corpo di una funzione è possibile utilizzare il modificatore <code>local</code> prima di un'assegnazione per rendere la variabile locale, ossia non accessibile fuori dal corpo della funzione. È bene evitare le variabili globali il più possibile (se non sono costanti), ricorrendo ai passaggi di parametri, per evitare la modifica accidentale di variabili esistenti fuori dalla funzione. Questo modificatore non è previsto da ''POSIX'', dove ogni variabile è globale, ma è comunque supportato da ogni moderna shell compatibile e caldamente consigliato.
Riga 42: Riga 42:
</pre>
</pre>


==Valori di ritorno==
== Valori di ritorno ==
Il valore di ritorno di una funzione è quello dell'ultimo blocco eseguito. In alternativa è possibile specificarlo all'interno dei blocchi direttamente con la parola riservata <code>return</code> seguito dall'exit status (un valore qualsiasi tra 0, l'unico per "successo", e 255), facendo terminare la funzione immediatamente.
Il valore di ritorno di una funzione è quello dell'ultimo blocco eseguito. In alternativa è possibile specificarlo all'interno dei blocchi direttamente con la parola riservata <code>return</code> seguito dall'exit status (un valore qualsiasi tra 0, l'unico per "successo", e 255), facendo terminare la funzione immediatamente.


Se la funzione deve restituire un valore diverso da successo/insuccesso, o comunque per cui basterebbe l'exit status, è possibile:
Se la funzione deve restituire un valore diverso da successo/insuccesso, o comunque per cui basterebbe l'exit status, è possibile stamparlo come output, così da permettere anche l'interazione con comandi esterni tramite una pipe.
 
Se invece si vuole assegnare questo valore a una variabile possono sorgere delle complicazioni, come per l'assegnazione dell'espansione di un comando, ma esistono diverse possibilità:
* stamparlo come output, così che sia assegnabile a una variabile con l'espansione di comando, ma soltanto se non contiene caratteri non ammissibili (il carattere ASCII n. 0 e possibili "a capo" finali, che verrebbero rimossi). Si noti che questo metodo '''non''' è possibile in generale per i nomi di file, a meno che non seguano convenzioni note a priori;
* stamparlo come output, così che sia assegnabile a una variabile con l'espansione di comando, ma soltanto se non contiene caratteri non ammissibili (il carattere ASCII n. 0 e possibili "a capo" finali, che verrebbero rimossi). Si noti che questo metodo '''non''' è possibile in generale per i nomi di file, a meno che non seguano convenzioni note a priori;
* stamparlo come output seguito da un carattere qualsiasi (per esempio X), così che sia assegnabile a una variabile con l'espansione di comando, rimuovendo in seguito il carattere finale in più (e i *DUE* in più se c'è un "a capo" dopo il valore, o verrebbe preservato). Con questo metodo è possibile passare nomi di file qualsiasi, ma al solito non il carattere ASCII n. 0;
* stamparlo come output seguito da un carattere qualsiasi (per esempio X), così che sia assegnabile a una variabile con l'espansione di comando, rimuovendo in seguito il carattere finale in più (e i *DUE* in più se c'è un "a capo" dopo il valore, o verrebbe preservato). Con questo metodo è possibile passare nomi di file qualsiasi, ma al solito non il carattere ASCII n. 0, e inoltre la funzione non sarà utilizzabile direttamente in una pipe con comandi qualsiasi;
* stamparlo come output, ricordandosi nell'assegnazione di aggiungere <code>&& printf %s X</code> all'interno dell'espansione di comando dopo la chiamata di funzione, rimuovendo in seguito il carattere o i due caratteri finali in più. È equivalente al metodo precedente, ma può essere usato anche senza passare per un'assegnazione;
* stamparlo come output senza aggiungere caratteri all'output della funzione, ricordandosi invece a ogni assegnazione di aggiungere <code>&& printf %s X</code> dopo la chiamata di funzione all'interno dell'espansione di comando, rimuovendo in seguito il carattere o i due caratteri finali in più. È equivalente al metodo precedente, ma è più flessibile e può essere usato anche senza passare per un'assegnazione, permettendo l'uso della funzione in una pipe con altri comandi;
* utilizzare una variabile globale, eventualmente con il nome della funzione per evitare doppioni (per esempio: retval_nomefunzione), contenente il valore da restituire nella funzione. Si noti che anche in questo caso il carattere ASCII n. 0 non può essere assegnato.
* utilizzare una variabile globale, eventualmente con il nome della funzione per evitare doppioni (per esempio: retval_nomefunzione), contenente il valore da restituire nella funzione. Per l'assegnazione alla variabile globale bisogna utilizzare lo stesso accorgimento solo in caso debba contenere l'output di altri comandi, ma basterebbe ricordarsene solo all'interno della funzione. La funzione non può essere utilizzata direttamente in una pipe; e al solito il carattere ASCII n. 0 non può essere assegnato.


{{Warningbox | Esiste anche la possibilità di ricorrere a <code>eval</code>, ma il suo uso non è trattato in questa guida per la sua potenziale '''pericolosità''': possibile <code>code injection</code> e <code>privilege escalation</code>, se l'input non è controllato (''sanitized''). Su '''bash''' è molto meglio ricorrere all'espansione di parametro per l'accesso indiretto, per accedere a una variabile il cui nome è contenuto in un'altra; e ad array associativi per gli altri casi.}}
{{Warningbox | Esiste anche la possibilità di ricorrere a <code>eval</code>, ma il suo uso non è trattato in questa guida per la sua potenziale '''pericolosità''': possibile <code>code injection</code> e <code>privilege escalation</code>, se l'input non è controllato (''sanitized''). Su '''bash''' è molto meglio ricorrere all'espansione di parametro per l'accesso indiretto, per accedere a una variabile il cui nome è contenuto in un'altra; e ad array associativi per gli altri casi.}}


Riguardo il carattere ASCII n. 0 si noti che una funzione può generarlo e stamparlo senza problemi, e quindi anche inviarlo tramite una pipe a comandi esterni. Gli unici problemi sono nella lettura di tale valore senza passare per comandi esterni e nell'assegnazione dell'output della funzione a una variabile: sarebbe possibile soltanto trasformando tutto in un'altra codifica (per esempio base64, esadecimale o ottale) come già visto in precedenza.
Riguardo il carattere ASCII n. 0 va ribadito che una funzione può generarlo e stamparlo senza problemi, perfino con <code>printf</code>, e quindi anche inviarlo tramite una pipe a comandi esterni. I problemi si riscontrano soltanto nella sua lettura, senza passare per comandi esterni, e nell'assegnazione dell'output della funzione a una variabile. Sarebbe possibile soltanto trasformando tutto in un'altra codifica (per esempio base64, esadecimale o ottale), come già visto in precedenza.


===Esempi: funzioni di controllo===
=== Esempi: funzioni di controllo ===
* Esempi di funzioni che controllano che un utente abbia privilegi di amministrazione o appartenga a un dato gruppo:
* Esempi di funzioni che controllano che un utente abbia privilegi di amministrazione o appartenga a un dato gruppo:
<pre>
<pre>
Riga 68: Riga 70:
# $1: nome gruppo
# $1: nome gruppo
is_user_member_of () {
is_user_member_of () {
     local username; local groups
     local name; local groups
    name=$1
     # se l'utente è root non controllo il suo gruppo (invoca is_root)
     # se l'utente è root non controllo il suo gruppo (invoca is_root)
     if is_root; then
     if is_root; then
Riga 74: Riga 77:
     fi
     fi
     # controlli
     # controlli
    username=$(whoami) &&
     groups=$(groups) &&
     groups=$(groups) &&
     case "$groups" in
     case "$groups" in
         "$username" | "${username} "* | *" ${username} "* | *" ${username}" )
         "$name" | "${name} "* | *" ${name} "* | *" ${name}" )
             true
             true
             ;;
             ;;
Riga 87: Riga 89:


In questi esempi di funzione il valore di ritorno della funzione è soltanto il suo exit status, che può essere controllato:
In questi esempi di funzione il valore di ritorno della funzione è soltanto il suo exit status, che può essere controllato:
* con <code>if</code> applicato direttamente al comando o subito dopo al suo exit status (la variabile speciale <code>$?</code>);
* con <code>if</code> applicato direttamente al comando o subito dopo la sua esecuzione con la variabile speciale <code>$?</code>, che contiene sempre l'exit status dell'ultimo comando eseguito;
* con gli operatori logici di concatenazione <code>||</code> e <code>&&</code>, applicati al comando (per chiarezza del codice).
* con gli operatori logici di concatenazione <code>||</code> e <code>&&</code>, applicati al comando (per chiarezza del codice).


Si noti che questi controlli servono soltanto a gestire il caso in cui uno script pensato per un amministratore, o per membri di un determinato gruppo, venga eseguito per errore da un utente senza gli adeguati privilegi. E permette di informare l'utente prima di eseguire qualsiasi operazione, altrimenti non sarebbe in grado di eseguire operazioni che richiedono privilegi più elevati.
Si noti che questi controlli servono soltanto a gestire il caso in cui uno script pensato per un amministratore, o per membri di un determinato gruppo, venga eseguito per errore da un utente senza gli adeguati privilegi. E permette di informare l'utente prima di eseguire qualsiasi operazione, altrimenti non sarebbe in grado di eseguire operazioni che richiedono privilegi più elevati.


===Esempi: funzioni che restituiscono interi===
=== Esempi: funzioni che restituiscono interi ===
* Script che stampa il numero di argomenti passati allo script (mostrando la differenza tra <code>$*</code> e <code>$@</code>):
* Script che stampa il numero di argomenti passati allo script (mostrando la differenza tra <code>$*</code> e <code>$@</code>):
<pre>
<pre>
Riga 117: Riga 119:
</pre>
</pre>


Si provi a creare lo script (per esempio count.sh), rendendolo eseguibile (si veda l'introduzione) e a provare a chiamarlo con diversi argomenti. In particolare:
Si provi a creare lo script (per esempio <code>count.sh</code>), rendendolo eseguibile (si veda l'introduzione) e a provare a chiamarlo con diversi argomenti. In particolare:
$ ./script.sh 1 2 3
<pre>$ ./script.sh 1 2 3</pre>
Argomenti contati: 1,3,3,3
Argomenti contati: 1,3,3,3


$ ./script.sh "1 stringa" "2 stringa" "3 stringa"
<pre>$ ./script.sh "1 stringa" "2 stringa" "3 stringa"</pre>
Argomenti contati: 1,6,6,3
Argomenti contati: 1,6,6,3


$ ./script.sh "/*/"
<pre>$ ./script.sh "/*/"</pre>
Argomenti contati: 1,N,N,1 (dove N è il numero di cartelle presenti in /)
Argomenti contati: 1,N,N,1 (dove N è il numero di cartelle presenti in /).


Ovviamente la seconda e la terza posizione combacia sempre perché l'espansione di <code>$*</code> e <code>$@</code> differisce solo quando queste variabili speciali sono quotate. E soltanto <code>"$@"</code> (quotata) preserva la lista degli argomenti, così com'era stata passata allo script o alla funzione.
Ovviamente la seconda e la terza posizione combacia sempre perché l'espansione di <code>$*</code> e <code>$@</code> differisce solo quando queste variabili speciali sono quotate. E soltanto <code>"$@"</code> (quotata) preserva la lista degli argomenti, così com'era stata passata allo script o alla funzione.
Riga 131: Riga 133:
Si noti che è possibile assegnare l'output della funzione con un'espansione di comando perché l'output è sempre un valore numerico, e quindi ovviamente senza caratteri ASCII n. 0 e "a capo" finali che vanno preservati.
Si noti che è possibile assegnare l'output della funzione con un'espansione di comando perché l'output è sempre un valore numerico, e quindi ovviamente senza caratteri ASCII n. 0 e "a capo" finali che vanno preservati.


===Esempi: funzioni che restituiscono informazioni su pacchetti Debian===
=== Esempi: funzioni che restituiscono informazioni su pacchetti Debian ===
Si noti che i nomi dei pacchetti seguono convenzioni note a priori (per maggiori informazioni si legga il [https://www.debian.org/doc/debian-policy/ch-controlfields.html Debian Policy Manual]), e in particolare non possono contenere né il carattere ASCII n. 0 né "a capo" finali. Quindi sono stampabili normalmente da una funzione e utilizzabili in un'espansione di comando.
Si noti che i nomi dei [[pacchetto|pacchetti]] seguono convenzioni note a priori (per maggiori informazioni si legga il [https://www.debian.org/doc/debian-policy/ch-controlfields.html Debian Policy Manual]), e in particolare non possono contenere né il carattere ASCII n. 0 né "a capo" finali. Quindi sono stampabili normalmente da una funzione e utilizzabili in un'espansione di comando.


* Funzione che stampa la lista di tutti i pacchetti installati:
* Funzione che stampa la lista di tutti i pacchetti installati:
Riga 248: Riga 250:
     status=0
     status=0
     # leggilo riga per riga (si veda la parte sui file descriptor)
     # leggilo riga per riga (si veda la parte sui file descriptor)
     while read pacchetto
     while read -r pacchetto
     do
     do
         # assegna l'output della funzione (3 campi, salvo errore) a $info
         # assegna l'output della funzione (3 campi, salvo errore) a $info
Riga 290: Riga 292:
Nella sezione successiva, che tratterà i nomi di file qualsiasi (che possono contenere anche il carattere "a capo"), la gestione dell'output di una funzione diventa molto più complicata.
Nella sezione successiva, che tratterà i nomi di file qualsiasi (che possono contenere anche il carattere "a capo"), la gestione dell'output di una funzione diventa molto più complicata.


===Esempi: funzioni che restituiscono nomi di file===
=== Esempi: funzioni che restituiscono nomi di file ===
Restituire nomi di file permette di sapere in anticipo che non contengono il carattere ASCII n. 0, ed è un vantaggio rispetto a una stringa qualsiasi. Infatti, come rimarcato più volte, non è possibile in generale assegnare il contenuto di un file binario a una variabile, ma solo leggere riga per riga un file di testo (che non contiene il carattere ASCII n. 0). Quindi quando si gestiscono stringhe bisogna sempre chiedersi quali caratteri permettono.
Restituire nomi di file permette di sapere in anticipo che non contengono il carattere ASCII n. 0, ed è un vantaggio rispetto a una stringa qualsiasi. Infatti, come rimarcato più volte, non è possibile in generale assegnare il contenuto di un file binario a una variabile, ma solo leggere riga per riga un file di testo (che non contiene il carattere ASCII n. 0). Quindi quando si gestiscono stringhe bisogna sempre chiedersi quali caratteri permettono.


Riga 337: Riga 339:
* rifiutabile, per esempio perché già esiste o perché è ricavato da una successione di comandi, ognuno dei quali potrebbe perdere gli "a capo" finali.
* rifiutabile, per esempio perché già esiste o perché è ricavato da una successione di comandi, ognuno dei quali potrebbe perdere gli "a capo" finali.


====Esempi più complessi con nomi di file arbitrari====
==== Esempi più complessi con nomi di file arbitrari ====
I nomi di file possono contenere tutti i caratteri, tranne <code>/</code> e il carattere ASCII n. 0, mentre i percorsi non possono contenere il solo carattere ASCII n. 0. In questa sezione si presenteranno tre metodi per definire funzioni robuste in grado di gestire nomi di file e percorsi arbitrari da assegnare a una variabile, tra quelli già discussi brevemente.
I nomi di file possono contenere tutti i caratteri, tranne <code>/</code> e il carattere ASCII n. 0, mentre i percorsi non possono contenere il solo carattere ASCII n. 0. In questa sezione si presenteranno tre metodi per definire funzioni robuste in grado di gestire nomi di file e percorsi arbitrari da assegnare a una variabile, tra quelli già discussi brevemente.


Riga 397: Riga 399:
</pre>
</pre>
L'espansione di comando con <code>dirname</code> non garantisce che i caratteri "a capo" finali siano preservati. Quando un comando qualsiasi (interno, funzione o comando esterno) restituisce un nome di file arbitrario, che non segue convenzioni note a priori (come i file di sistema), non può essere usato senza accorgimenti in un'espansione di comando.
L'espansione di comando con <code>dirname</code> non garantisce che i caratteri "a capo" finali siano preservati. Quando un comando qualsiasi (interno, funzione o comando esterno) restituisce un nome di file arbitrario, che non segue convenzioni note a priori (come i file di sistema), non può essere usato senza accorgimenti in un'espansione di comando.


* Lo stesso esempio ricorrendo invece a una variabile globale per il valore di ritorno:
* Lo stesso esempio ricorrendo invece a una variabile globale per il valore di ritorno:
Riga 455: Riga 456:
In questo caso è necessario ricordarsi dei due caratteri finali da rimuovere unicamente all'interno della funzione stessa, e non a ogni chiamata. Lo svantaggio è nell'uso di una variabile globale per ogni funzione, per questo è bene usare una forma non ambigua, contenente il nome della funzione e un prefisso comune, usato solo per queste variabili.
In questo caso è necessario ricordarsi dei due caratteri finali da rimuovere unicamente all'interno della funzione stessa, e non a ogni chiamata. Lo svantaggio è nell'uso di una variabile globale per ogni funzione, per questo è bene usare una forma non ambigua, contenente il nome della funzione e un prefisso comune, usato solo per queste variabili.


Si noti che tutte le variabili sono globali, tranne quelle in una funzione definite con <code>local</code>. È bene però non usare mai altre variabili globali, se non sono costanti definite con <code>readonly</code>, all'interno di una funzione e ricorrere invece ai parametri, per rendere più semplice la comprensione del codice ed evitare modifiche accidentali con effetti a catena in più parti del codice.
Si noti che tutte le variabili sono globali, tranne quelle in una funzione definite con <code>local</code>. È bene però non usare mai altre variabili globali, se non sono costanti definite con <code>readonly</code>, all'interno di una funzione e ricorrere invece ai parametri, per rendere più semplice la comprensione del codice ed evitare modifiche accidentali con effetti a catena in più parti del codice. Inoltre va sempre considerato che una variabile non è globale, se è assegnata in una subshell: pertanto per esempio la funzione non può essere impiegata in una pipe con altri comandi, per leggerne l'output.
 


* Forma alternativa, con controllo di errori e interruzione immediata:
* Forma alternativa, con controllo di errori e interruzione immediata:
3 581

contributi