Bash scripting - funzioni: differenze tra le versioni

Vai alla navigazione Vai alla ricerca
aggiunti esempi meno complessi con output normale
m (→‎Esempi: funzioni che restituiscono nomi di file: errore nella chiusura di un tag)
(aggiunti esempi meno complessi con output normale)
Riga 12: Riga 12:
}
}
</pre>
</pre>
I comandi contenuti nel corpo della funzione sono eseguiti ogni volta che nomefunzione (una singola stringa senza spazi con le stesse limitazioni dei nomi delle variabili) è scritto come comando. Si noti che <code>()</code> dopo il nome della funzione serve a identificare l'istruzione composta come una definizione di funzione, e non accetta argomenti al suo interno come altri linguaggi di programmazione, ma resta sempre <code>()</code> invariata. È buona prassi usare i commenti per specificare le opzioni richieste dalla funzione.
I comandi contenuti nel corpo della funzione sono eseguiti ogni volta che ''nomefunzione'', che è una singola stringa senza spazi con le stesse limitazioni dei nomi delle variabili, è scritto come comando. Si noti che <code>()</code> dopo il nome della funzione serve a identificare l'istruzione composta come una definizione di funzione, e non accetta argomenti al suo interno come altri linguaggi di programmazione, ma resta sempre <code>()</code> invariata.
 
È 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.
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.


Per esempio:
Per esempio:
Riga 43: Riga 45:
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ì 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;
Riga 51: Riga 53:
{{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.}}


===Esempio: funzione che conta gli argomenti con $* e $@===
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.
* Esempio che stampa il numero di argomenti passati allo script (mostrando la differenza tra <code>$*</code> e <code>$@</code>):
 
===Esempi: funzioni di controllo===
* Esempi di funzioni che controllano che un utente abbia privilegi di amministrazione o appartenga a un dato gruppo:
<pre>
# Ha un exit status di 0 (successo) solo se l'utente è root
# Numero argomenti: 0
is_root () {
    [ "$(id -u)" = 0 ]
}
 
# Ha un exit status di 0 solo se l'utente corrente è membro del gruppo scelto
# Numero argomenti: 1
# $1: nome gruppo
is_user_member_of () {
    local username; local groups
    # se l'utente è root non controllo il suo gruppo (invoca is_root)
    if is_root; then
        return 0
    fi
    # controlli
    username=$(whoami) &&
    groups=$(groups) &&
    case "$groups" in
        "${username} "* | *" ${username} "* | *" ${username}" )
            true
            ;;
        * ) false
            ;;
    esac
}
</pre>
 
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 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.
 
===Esempi: funzioni che restituiscono interi===
* Script che stampa il numero di argomenti passati allo script (mostrando la differenza tra <code>$*</code> e <code>$@</code>):
<pre>
<pre>
#! /bin/bash
#! /bin/bash
Riga 88: Riga 129:
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.


Si noti che è possibile assegnare l'output della funzione con un'espansione di comando solo perché l'output è un valore numerico, senza caratteri ASCII n. 0 e "a capo" finali.
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===
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.
 
* Funzione che stampa la lista di tutti i pacchetti installati:
<pre>
# Stampa la lista di tutti i pacchetti installati, uno per riga
# Numero argomenti: 0
# Output: la lista dei pacchetti installati, uno per riga
#        ogni riga contiene solo i caratteri [a-zA-Z0-9.+:-]
lista_pacchetti_installati () {
    dpkg --get-selections |  # lista i pacchetti noti e il loro stato
        awk '$2 ~ /^install$/ { print $1 }' # stampa il primo campo (pacchetto)
                                            # se il secondo (stato) è: install
}
</pre>
Si noti che non si controllano gli errori, perché il comando non dipende da condizioni specificate dall'utente e in condizioni normali non può fallire.
 
* Funzione che stampa informazioni su un singolo pacchetto:
<pre>
# Stampa la versione di Debian, la sezione e l'architettura del pacchetto
# Numero argomenti: 1
# $1 - nome pacchetto Debian
# Output: versione sezione architettura
#        (tre stringhe con i soli caratteri [a-zA-Z0-9.+:/-])
# Exit status:
#  0 (successo) solo se il pacchetto è installato
#  1 se il pacchetto è installato ma nessuna informazione è trovata
#  2 se il pacchetto non è installato
#  3 se il pacchetto non esiste / non è stato trovato
#  4 nome di pacchetto non valido
stampa_info_pacchetto () {
    local pacchetto; local policy; local info; local versione; local arch
    # inizializzazione
    pacchetto=$1
    policy=
    info=
    versione=
    arch=
    # controlla nome del pacchetto (classe dei caratteri ammessi: [a-z0-9.+:-])
    if [ -z "${pacchetto##*[!a-z0-9.+:-]*}" ]; then
        return 4  # nome del pacchetto non valido
    fi
    # leggi la policy del pacchetto
    if ! policy=$(apt-cache policy "$pacchetto" 2> /dev/null); then
        return 3  # pacchetto non trovato
    fi
    # estrae informazioni nel formato versione/sezione:architettura
    # ma non stampa niente se non è installato
    info=$(printf %s\\n "$policy" |
        awk '$1 ~ /^\*\*\*$/ {
                getline
                print $3":"$4
            }')
        # Spiegazione righe con awk:
        # 1- estraggo la riga in cui il primo campo è: *** (scritto con escape)
        #    corrispondente alla versione attualmente installata
        # 2- leggo la riga successiva (getline)
        #    corrispondente a quella con maggiore priorità o in base all'ordine
        #    in /etc/apt/sources.list ed /etc/apt/sources.list.d/*
        # 3- stampo il terzo e il quarto campo, unendoli con ":"
        #    (campi: priorità origine versione/sezione architettura ...)
        # Risultato (stampato):
        # - se installato: versione/sezione:architettura
        # - se non installato: (niente)
    # controlla output
    if [ -z "$info" ]; then
        return 2            # non installato
    fi
    # estrai campi
    arch=${info##*:}        # estrai architettura (amd64, i386)
    info=${info%${arch}}    # rimuovi architettura
    info=${info%:}          # rimuovi : (non è equivalente a farlo assieme!)
    sezione=${info##*/}    # estrai sezione (main, contrib, non-free)
    info=${info%${sezione}} # rimuovi sezione
    info=${info%/}          # rimuovi /
    versione=$info          # quello che resta è la versione
    # controlla campi (che non siano vuoti e non contengano caratteri invalidi)
    if [ -z "$versione" ] || [ -z "$sezione" ] || [ -z "$arch" ] ||
      [ -z "${info##*[!a-zA-Z0-9.+:/-]*}" ]
    then
        return 1            # informazioni non trovate
    else
        # stampa campi del pacchetto
        printf %s\\n "${versione} ${sezione} ${arch}"
        return 0            # successo
    fi
}
</pre>
Si noti che il pacchetto preso come argomento dalla funzione potrebbe non esistere oppure non essere installato. Per creare una funzione generale, riusabile in altri script e accettante qualsiasi argomento, è necessario controllare lo stato di uscita dei comandi richiamati, e terminare la funzione con un valore di uscita maggiore di zero, così che possano essere gestiti nel corpo principale dello script. E fare uso di diversi valori di uscita permette inoltre di gestire i diversi errori in modo personalizzato.
 
* Per esempio le funzioni precedenti possono essere combinate in uno script:
<pre>
#! /bin/bash
 
 
### funzioni
 
lista_pacchetti_installati () {
    # il contenuto della funzione definita precedentemente
    ...
}
 
stampa_info_pacchetti () {
    # il contenuto della funzione definita precedentemente
    ...
}
 
 
### main
 
# invia l'output della funzione a un blocco con una pipe
lista_pacchetti_installati | {
    # subshell perché il blocco è dopo una pipe
 
    # exit status di default
    status=0
    # leggilo riga per riga (si veda la parte sui file descriptor)
    while read pacchetto
    do
        # assegna l'output della funzione (3 campi, salvo errore) a $info
        info=$(stampa_info_pacchetto "$pacchetto") || {
            # gestisci errore
            nuovo_status=$?
            # mantieni sempre lo stesso numero di campi nell'output
            if [ "$nuovo_status" = 1 ]; then
                # ignoro l'errore, perché potrebbe essere stato installato
                # con dpkg/gdebi oppure da APT ma non essere più presente
                info="NOTFOUND NOTFOUND NOTFOUND"
            else
                # gestisco l'errore, aggiornando $status
                info="ERROR ERROR ERROR"
                # se ci sono più errori tieni quello più grave (maggiore)
                if [ "$nuovo_status" -gt "$status" ]; then
                    status=$nuovo_status
                fi
            fi
        }
        # stampa il nome del pacchetto (1 campo) e le informazioni (3 campi)
        printf %s\\n "${pacchetto} ${info}"
    done
 
    # restituisce l'exit status
    exit $status  # non termina lo script, ma solo la subshell
                  # (non farebbe differenza, se non ci fosse nient'altro
                  #  dopo il blocco, ma è meglio renderlo esplicito, in caso
                  #  il codice venga esteso in un secondo momento)
 
} || exit $?      # termina lo script, con l'exit status della subshell
                  # (se diverso da zero, perché si usa l'operatore logico OR)
 
exit 0            # altrimenti exit status 0 (successo)
                  # tutti i pacchetti installati trovati o non trovati
                  # ma senza altri errori
</pre>
 
Lo sforzo di mantenere sempre lo stesso numero di campi, dato che i pacchetti non contengono spazi, permette di utilizzare l'output dello script con altri comandi, per eseguire funzioni ancora più avanzate (ordinamento, ricerca, selezione, ecc...).
 
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===
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. Ne consegue che l'espansione di comando non è adatta a essere impiegata con comandi che restituiscono nomi di file, e questo si applica anche alle funzioni, salvo i file seguano convenzioni note a priori (come quelli di sistema).
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.
 
Con i nomi di file, anche se non esiste il problema del carattere ASCII n. 0, sono possibili caratteri "a capo" che, se in ultima posizione, verrebbero rimossi dall'espansione di comando. Il problema ovviamente non si pone per i file che seguono convenzioni note a priori, come i file di sistema (ma se lo script si occupa di controllare che non ci siano anomalie nei file di sistema, non può partire da questa assunzione!), o i file creati dallo script stesso, purché:
* con opzioni non determinate da input esterni;
* con opzioni determinate da input esterni, ma controllate, rifiutando quelle non accettabili.
 
Per esempio, funzione che crea un file temporaneo e ne restituisce il nome:
<pre>
# Crea un file regolare temporaneo con nome qualsiasi o scelto come argomento
# Numero argomenti: 0-1
# [ $1: nome del file ]
# Output: stringa contenente il nome del file creato (se è creato)
# Exit status:
#  0 (successo) se il file è stato creato
#  1 se la creazione del file è fallita
#  2 se il file già esisteva
#  3 se il nome scelto non è valido (solo caratteri [a-zA-Z0-9.,_-])
crea_file_temporaneo () {
    local tmpfile
    if [ $# -eq 0 ]; then
        # mktemp segue convenzioni note a priori (senza argomenti)
        tmpfile=$(mktemp) ||
            return 1          # creazione fallita
    # posso scegliere regole meno stringenti, è solo un esempio...
    elif [ -z "${1##*[!a-zA-Z0-9.,_-]*}" ]; then
        return 3              # nome non valido
    # devo controllare che non esista nulla con quel nome per poterlo creare
    # non basterebbe controllare che non esiste un file regolare
    elif [ -e "$1" ]; then
        return 2              # file già esistente
    else
        tmpfile="/tmp/${1}"  # lo creo in /tmp
        touch -- "$tmpfile" ||
            return 1          # creazione fallita
    fi
    printf %s\\n "$tmpfile"  # stampo il nome del file creato, con percorso
    return 0                  # successo
}
</pre>
 
La restituzione di un valore invece è più complessa se il nome del file o percorso da restituire '''non''' è:
* scelto dallo script senza input esterni;
* generato da comandi con convenzioni note a priori;
* appartenente a percorsi contenenti file seguenti convenzioni note a priori, se lo scopo dello script non è assicurarsene;
* rifiutabile, per esempio perché già esiste o perché è ricavato da una successione di comandi, ognuno dei quali potrebbe perdere gli "a capo" finali.


Non è ovviamente il caso per quelli utente. In questa sezione si presenteranno tre metodi per definire funzioni robuste in grado di gestire nomi di file arbitrari da assegnare a una variabile, tra quelli già discussi brevemente.
====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.


* Esempio di script che stampa la directory genitore (''parent'') di un dato file, gestendo eventuali "a capo" durante l'assegnazione:
* Esempio di script che stampa la directory genitore (''parent'') di un dato file, gestendo eventuali "a capo" durante l'assegnazione:
Riga 210: Riga 455:
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 costante (definite con <code>readonly</code>), all'interno di una funzione, per rendere più semplice la comprensione del codice ed evitare modifiche accidentali (con effetti a catena in più parti del codice), e ricorrere invece ai parametri.
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.




3 581

contributi

Menu di navigazione