Bash scripting - istruzioni composte
Bash scripting |
Sommario |
Istruzioni composte
Sono istruzioni composte tutte le istruzioni che possono contenere al loro interno altre istruzioni. Per esempio if
e for
sono due istruzioni composte:
if
è già stata trattata assieme all'uso di[ ... ]
nei comandi essenziali, utile anche per le condizioni del ciclowhile
;for
è già stato trattato nell' assegnazione con ciclo nella sezione delle Variabili, con la sintassi tipica di for each di alcuni linguaggi di programmazione e l'unica prevista da POSIX.
Cicli
Con while
è possibile eseguire un blocco di comandi se un comando ha successo e ripeterne l'esecuzione fintanto che il comando continua ad avere successo. Proprio come if
, while
è un'istruzione usata spesso in congiunzione con [...]
come comando di cui valutare l'exit status.
Sintassi base (in congiunzione con [...]
):
while [ espressione-booleana ] do ... done
Per esempio:
num= # ripeti finché non è soddisfatta la condizione (si noti il ! davanti a [...]) while ! [ "$num" -gt 0 ] 2> /dev/null do printf %s "Inserisci intero > 0 e premi INVIO: " # assegna l'input dell'utente a $num read num done printf %s\\n "Hai inserito: ${num}"
Ripeti per $n volte (ciclo con incremento, da 1 a $n):
i=1 while [ $i -le $n ] do ... i=$(($i + 1)) done
Oppure al posto di [...]
si può utilizzare un comando qualsiasi, per esempio per eseguire un ciclo infinito:
while : do ... done
Un ciclo può essere sempre:
- interrotto da
break
; - ripreso dalla prima istruzione del blocco, dopo aver rivalutato l'exit status del comando dopo
while
, concontinue
.
Si noti che in presenza di cicli con incremento, l'uso di continue
può portare a un ciclo infinito se non viene eseguito l'incremento:
i=1 while [ $i -le $n ] do if [ condizione ]; then i=$(($i + 1)) # incremento prima di continue! continue # senza incremento potrebbe portare a un ciclo infinito fi ... i=$(($i + 1)) # incremento done
Alternativamente in bash (non POSIX) si può utilizzare il ciclo for
con una sintassi C-like, ereditata da csh:
for ((i=1; i <= n; i++)) { ... }
E non è una svista la mancanza di $ davanti al nome delle variabili all'interno di ((...))
. Tale sintassi è supportata anche nell'espansione aritmetica intera, ma non è prevista da POSIX, e quindi non è mai stata usata in questa guida.
Composizione di comandi con pipe
Una pipe collega due comandi, trattando l'output generato dal primo come input per il secondo, in modo da svolgere funzioni più complesse tramite la composizione comandi che si occupano di funzioni più limitate. Ed è una delle caratteristiche di Unix che ogni comando debba svolgere un'unica funzione nel modo migliore possibile, permettendo la comunicazione con l'utente o un altro comando attraverso un flusso di testo, considerato un'interfaccia universale.
L'exit status di una serie di pipe è dato dall'ultimo comando.
Sintassi:
- comando1 | comando2: pipe che invia lo standard output del comando precedente al successivo. Tutti i comandi di una serie di pipe (anche l'ultimo) sono eseguiti in una subshell, e possono stare su righe diverse con la pipe come ultimo carattere;
- comando1 2>&1 | comando2: pipe che invia standard output e standard error (si veda la parte sui file descriptor) del comando precedente al successivo. Tutti i comandi di una serie di pipe (anche l'ultimo) sono eseguiti in una subshell, e possono stare su righe diverse con la pipe come ultimo carattere.
I più comuni comandi esterni impiegati con una pipe (per informazioni sulle loro opzioni si lascia alla lettura della loro pagina di manuale):
cat
per concatenare più file;head
etail
per leggere la parte iniziale e finale di un file;grep
per filtrare le righe di un file in base a un dato pattern;sed
per modificare le parti corrispondenti a un dato pattern con altre stringhe;cut
per selezionare dei campi da ogni riga, ma in base a un determinato carattere delimitatore (di default la tabulazione), conteggiato anche se ripetuto;awk
per selezionare dei campi da ogni riga, separandoli in base a spazi e tabulazioni (ignorando quelli consecutivi). Questa è la sua funzione base, maawk
è un vero e proprio linguaggio di programmazione, che se padroneggiato può sostituire le funzionalità di buona parte degli altri comandi;sort
euniq
per ordinare le righe in base a determinati campi e saltare quelle doppie;wc
per conteggiare il numero di caratteri, parole e/o linee;tr
per rimuovere o trasformare i caratteri di una data classe.
Per esempio:
# filtra le occorrenze di video nel ring buffer del kernel dmesg | grep -i video # -i per ignore-case (ignora maiuscole/minuscole) # stampa il gateway utilizzato ip route | # stampa la tabella di routing grep "^default" | # filtra solo la riga iniziante con default awk "{ print \$3 }" # stampa il terzo campo ($3 non dev'essere espanso dalla shell)
Output dei comandi e carattere ASCII n. 0
Una variabile non può contenere il carattere ASCII n. 0, che è usato per indicare la fine della stringa. Non potendo gestire direttamente il carattere ASCII n. 0, questo non può essere presente nell'espansione di un comando, ma dev'essere riservato ai soli comandi mediante l'uso di una o più pipe.
Infatti non esiste un modo di contenere il carattere ASCII n. 0 in nessuna posizione:
var=$'\000' # SBAGLIATO: $var è vuota var=$'X\000X' # SBAGLIATO: $var contiene X (solo la prima!) var=$(printf %s\000%s X X) # SBAGLIATO: $var contiene XX
Si noti che mentre è rimosso dalle espansioni, nelle assegnazioni normali viene interpretato come carattere di fine stringa, con il risultato che anche tutto ciò che segue viene tralasciato.
Nomi di file con caratteri non comuni
Questo carattere è utile perché nemmeno i file possono averlo nel proprio nome, mentre invece permettono caratteri jolly (*, ?, ...), come già visto con le espansioni di percorso, e potrebbero contenere perfino il carattere "a capo".
L'espansione di percorso funziona normalmente, anche in presenza di "a capo", ma potrebbero sorgere problemi sfruttando l'espansione di comando. Per esempio con il comando esterno find
, utilizzato per effettuare ricerche in modo ricorsivo, e che di default restituisce i file trovati stampandoli uno per riga, assumendo implicitamente che non contengano il carattere "a capo".
Uno script a titolo esemplificativo:
# creo una directory e un file con "a capo" nel nome mkdir ./prova touch ./prova/"file contenente"$'\n'"a capo nel nome" # SBAGLIATO! (per via del nome del file particolare) num_file=$(find ./prova -type f | # stampo i file regolari nella directory, uno per riga wc -l) # conto il numero di righe printf %s\\n "La directory prova contiene ${num_file} file" # restituisce 2 invece di 1 # forma corretta (ma non la più efficiente, è solo per esemplificare l'uso del carattere) num_file=$(find ./prova -type f -print0 | # stampo il carattere ASCII n. 0 dopo ogni file tr -dc '\000' | # rimuovo tutti i caratteri ASCII diversi dal n. 0 wc -c) # conto il numero di caratteri printf %s\\n "La directory prova contiene ${num_file} file" # restituisce 1 # pulizia rm -- ./prova/file*nome # elimino il file rmdir -- ./prova # elimino la directory (se vuota)
Comando find e carattere ASCII n. 0
Un abbinamento comune al comando esterno GNU find
(dotato dell'opzione -print0) è il comando esterno GNU xargs
(dotato dell'opzione -0):
find /percorso -opzione1 ... -opzioneN -print0 | # trova file che soddisfano le condizioni date xargs -0 comando [ argomenti ... ] # li passa come argomenti a un comando esterno
Si leggano i rispettivi manuali per maggiori informazioni. find
ha la possibilità di eseguire altri comandi esterni sui file trovati direttamente con le opzioni -exec ed -execdir, ma la sintassi è più complessa e non supporta più di un processo per volta, come invece xargs
.
Con bash (non POSIX) esiste un metodo per trattare indirettamente il carattere ASCII n. 0 anche con i comandi interni della shell, tramite l'istruzione read
e l'opzione per impostare il carattere come delimitatore, così da replicare la funzionalità di xargs -0
. In questo modo il carattere non verrà assegnato alle variabili, ma sarà usato all'interno di un ciclo per delimitare ogni valore dal successivo, permettendo la lettura dell'output generato da find
(con -print0
):
find /percorso -opzione1 ... -opzioneN -print0 | while read -d $'\000' file do # posso usare "$file" ... done
Si ricorda però che tutto il blocco while
, facendo parte di una pipe, è eseguito in una subshell. E che quindi tutte le variabili sono reimpostate al loro valore precedente al termine del blocco.
È invece sempre sbagliato, come soluzione generale se i nomi di file non seguono convenzioni note a priori, utilizzare l'espansione di comando con find
all'interno di un ciclo for
:
for file in $(find ...) # NO!
Concatenazione e blocchi di comandi
Più comandi, anche contenenti redirezioni, possono essere concatenati per formarne di più complessi. L'operatore di concatenazione può essere anche l'ultimo di una riga, per facilitare la leggibilità del codice.
;
- separatore di comandi, con cui un comando verrà eseguito a prescindere dal successo del precedente (in uno script è equivalente a un "a capo" e questa è la concatenazione di default)
&
- separatore che esegue il comando precedente in background (ritorna exit status sempre positivo e non riceve input da tastiera), passando al successivo
&&
- operatore logico AND, che va posizionato tra due comandi, il secondo dei quali è eseguito solo se il primo ha esito positivo
Esempio:
cd /percorso/dir && comando
Attenzione invece alla pericolosità di:
cd /tmp/tmpdir # cambia la cartella corrente in /tmp/tmpdir # ma se fallisce, quello successivo verrebbe eseguito comunque! rm -- ./* # cancella tutti i file nella directory corrente
||
- operatore logico OR, il secondo comando è eseguito solo se il primo ha effetto negativo (si può usare anche dopo una sequenza di &&, perché ha priorità inferiore):
{ ... ; }
- esegue un blocco di comandi (dopo l'ultimo servono ";" o un "a capo"). È usata nelle funzioni ed è utile per concatenare && e ||:
# interrompe la catena se un comando fallisce cd /tmp/tmpdir && rm -- ./* && rmdir -- /tmp/tmpdir || { # il blocco è eseguito solo se un comando fallisce retval=$? printf %s\\n "ERRORE (exit status: $retval)" >&2 exit $retval }
( ... )
- esegue il blocco di comandi in una subshell (le variabili vengono ripristinate al loro valore precedente, alla fine del blocco).
Catturare l'exit status
Per catturare lo stato d'uscita di un comando appena eseguito è sufficiente espandere la variabile speciale $?
, come già visto. Tuttavia in caso di fallimento del comando, il controllo effettuato via $?
avverrebbe soltanto dopo un blocco con errore (si veda la parte introduttiva sul debug).
Per evitare che un blocco abbia un exit status diverso da zero, si possono usare le concatenazioni &&
e ||
(oppure un if
):
comando && status=0 || # se corretto status=$? # se sbagliato
Funzioni
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.
All'interno del blocco di comandi (il corpo della funzione), le variabili speciali relative agli argomenti passati allo script ($# $1 $2 ... $* $@
) fanno riferimento a quelli passati alla funzione.
Sintassi della definizione:
nomefunzione () { ... }
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 ()
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 ()
invariata.
In bash è possibile premettere function
al nome della funzione, anche senza bisogno di ()
, ma questa possibilità non è prevista da POSIX.
Nel corpo di una funzione è possibile utilizzare il modificatore local
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.
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 return
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:
- 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ù. Con questo metodo è possibile passare nomi di file qualsiasi, ma al solito non il carattere ASCII n. 0;
- utilizzare una variabile globale, eventualmente con il nome della funzione per evitare doppioni (per esempio: return_nomefunzione), contenente il valore da restituire nella funzione. Si noti che anche in questo caso il carattere ASCII n. 0 non può essere assegnato.
Guida scritta da: HAL 9000 13:01, 22 lug 2014 (CEST) | Debianized 20% |
Estesa da: | |
Verificata da: | |
Verificare ed estendere la guida | Cos'è una guida Debianized |