Guida avanzata di scripting Bash: Un'approfondita esplorazione dell'arte dello scripting di shell | ||
---|---|---|
Indietro | Avanti |
Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. | |
Brian Kernighan |
La shell Bash non possiede alcun debugger e neanche comandi o costrutti specifici per il debugging. [1] Gli errori di sintassi o le errate digitazioni generano messaggi d'errore criptici che, spesso, non sono di alcun aiuto per correggere uno script che non funziona.
Esempio 29-1. Uno script errato
#!/bin/bash # ex74.sh # Questo è uno script errato. # Ma dove sarà mai l'errore? a=37 if [$a -gt 27 ] then echo $a fi exit 0 |
Output dell'esecuzione dello script:
./ex74.sh: [37: command not found |
Esempio 29-2. Parola chiave mancante
#!/bin/bash # missing-keyword.sh: Che messaggio d'errore verrà generato? for a in 1 2 3 do echo "$a" # done # La necessaria parola chiave 'done', alla riga 7, #+ è stata commentata. exit 0 |
Output dello script:
missing-keyword.sh: line 10: syntax error: unexpected end of file |
I messaggi d'errore, nel riportare il numero di riga di un errore di sintassi, potrebbero ignorare le righe di commento presenti nello script.
E se uno script funziona, ma non dà i risultati attesi? Si tratta del fin troppo familiare errore logico.
Esempio 29-3. test24: un altro script errato
#!/bin/bash # Si suppone che questo script possa cancellare tutti i file della #+ directory corrente i cui nomi contengono degli spazi. # Non funziona. # Perché? bruttonome=`ls | grep ' '` # Provate questo: # echo "$bruttonome" rm "$bruttonome" exit 0 |
Si cerchi di scoprire cos'è andato storto in Esempio 29-3 decommentando la riga echo "$bruttonome". Gli enunciati echo sono utili per vedere se quello che ci si aspetta è veramente quello che si è ottenuto.
In questo caso particolare, rm
"$bruttonome" non dà il risultato
desiderato perché non si sarebbe dovuto usare
$bruttonome
con il quoting. Averlo
collocato tra apici significa assegnare a
rm un unico argomento (verifica un solo
nome di file). Una parziale correzione consiste nel togliere
gli apici a $bruttonome
ed impostare
$IFS
in modo che contenga solo il ritorno a
capo, IFS=$'\n'. Esistono, comunque,
modi più semplici per ottenere il risultato voluto.
# Metodi corretti per cancellare i file i cui nomi contengono spazi. rm *\ * rm *" "* rm *' '* # Grazie. S.C. |
Riepilogo dei sintomi di uno script errato:
comparsa del messaggio "syntax error", oppure
va in esecuzione, ma non funziona come dovrebbe (errore logico);
viene eseguito, funziona come ci si attendeva, ma provoca pericolosi effetti collaterali (bomba logica).
Gli strumenti per la correzione di script non funzionanti comprendono
gli enunciati echo posti in punti cruciali dello script, per tracciare le variabili ed avere così un quadro di quello che sta avvenendo.
Ancor meglio è un echo che visualizza qualcosa solo quando è abilitato debug.
|
l'uso del filtro tee nei punti critici per verificare i processi e i flussi di dati.
eseguire lo script con le opzioni -n -v -x
sh -n nomescript verifica gli errori di sintassi senza dover eseguire realmente lo script. Equivale ad inserire nello script set -n o set -o noexec. È da notare che alcuni tipi di errori di sintassi possono eludere questa verifica.
sh -v nomescript visualizza ogni comando prima della sua esecuzione. Equivale ad inserire nello script set -v o set -o verbose.
Le opzioni -n
e -v
agiscono bene insieme.
sh -nv nomescript fornisce una
verifica sintattica dettagliata.
sh -x nomescript visualizza il risultato di ogni comando, ma in modo abbreviato. Equivale ad inserire nello script set -x o set -o xtrace.
Inserire set -u o set -o nounset nello script permette la sua esecuzione visualizzando, però, il messaggio d'errore "unbound variable" ogni volta che si cerca di usare una variabile non dichiarata.
L'uso di una funzione "assert", per verificare una variabile o una condizione, in punti critici dello script. (È un'idea presa a prestito dal C.)
Esempio 29-4. Verificare una condizione con una funzione con assert
#!/bin/bash # assert.sh ####################################################################### assert () # Se la condizione è falsa, { #+ esce dallo script #+ con un messaggio d'errore appropriato. E_ERR_PARAM=98 E_ASSERT_FALLITA=99 if [ -z "$2" ] #Alla funzione assert() #+ non sono stati passati abbastanza parametri. then return $E_ERR_PARAM # Non fa niente. fi numriga=$2 if [ ! $1 ] then echo "Assert \"$1\" fallita:" echo "File \"$0\", riga $numriga" # Visualizza il nome del file #+ e il numero di riga. exit $E_ASSERT_FALLITA # else # return # e continua l'esecuzione dello script. fi } # Inserite una funzione assert() simile negli script #+ che necessitano del debugging. ####################################################################### a=5 b=4 condizione="$a -lt $b" # Messaggio d'errore ed uscita dallo script. # Provate ad impostare "condizione" con #+ qualcos'altro, e vedete cosa succede. assert "$condizione" $LINENO # La parte restante dello script verrà eseguita solo se "assert" non fallisce. # Altri comandi. # Ulteriori comandi . . . echo "Questo enunciato viene visualizzato solo se \"assert\" non fallisce." # ... # Altri comandi . . . exit $? |
eseguire una trap di exit.
Il comando exit, , in uno script, lancia il segnale 0 che termina il processo, cioè, lo script stesso. [2] È spesso utile eseguire una trap di exit, per esempio, per forzare la "visualizzazione" delle variabili. trap deve essere il primo comando dello script.
Specifica un'azione che deve essere eseguita alla ricezione di un segnale; è utile anche per il debugging.
trap '' 2 # Ignora l'interrupt 2 (Control-C), senza alcuna azione specificata. trap 'echo "Control-C disabilitato."' 2 # Messaggio visualizzato quando si digita Control-C. |
Esempio 29-5. Trap di exit
#!/bin/bash # Andare a caccia di variabili con trap. trap 'echo Elenco Variabili --- a = $a b = $b' EXIT # EXIT è il nome del segnale generato all'uscita dallo script. # Il comando specificato in "trap" non viene eseguito finché #+ non è stato inviato il segnale appropriato. echo "Questa visualizzazione viene eseguita prima di \"trap\" --" echo "nonostante lo script veda prima \"trap\"." echo a=39 b=36 exit 0 # Notate che anche se si commenta il comando 'exit' questo non fa #+ alcuna differenza, poiché lo script esce in ogni caso dopo #+ l'esecuzione dei comandi. |
Esempio 29-6. Pulizia dopo un Control-C
#!/bin/bash # logon.sh: Un rapido e rudimentale script per verificare se si #+ è ancora collegati. umask 177 # Per essere certi che i file temporanei non siano leggibili dal #+ mondo intero. TRUE=1 FILELOG=/var/log/messages # Fate attenzione che $FILELOG deve avere i permessi di lettura #+ (da root, chmod 644 /var/log/messages). FILETEMP=temp.$$ # Crea un file temporaneo con un nome "univoco", usando l'id di #+ processo dello script. # Un'alternativa è usare 'mktemp'. # Per esempio: # FILETEMP=`mktemp temp.XXXXXX` PAROLACHIAVE=address # A collegamento avvenuto, la riga "remote IP address xxx.xxx.xxx.xxx" # viene accodata in /var/log/messages. COLLEGATO=22 INTERRUPT_UTENTE=13 CONTROLLA_RIGHE=100 # Numero di righe del file di log da controllare. trap 'rm -f $FILETEMP; exit $INTERRUPT_UTENTE'; TERM INT # Cancella il file temporaneo se lo script viene interrotto con un control-c. echo while [ $TRUE ] # Ciclo infinito. do tail -n $CONTROLLA_RIGHE $FILELOG> $FILETEMP # Salva le ultime 100 righe del file di log di sistema nel file #+ temporaneo. Necessario, dal momento che i kernel più #+ recenti generano molti messaggi di log durante la fase di avvio. ricerca=`grep $PAROLACHIAVE $FILETEMP` # Verifica la presenza della frase "IP address", #+ che indica che il collegamento è riuscito. if [ ! -z "$ricerca" ] # Sono necessari gli apici per la possibile #+ presenza di spazi. then echo "Collegato" rm -f $FILETEMP # Cancella il file temporaneo. exit $COLLEGATO else echo -n "." # L'opzione -n di echo sopprime il ritorno a capo, #+ così si ottengono righe continue di punti. fi sleep 1 done # Nota: se sostituite la variabile PAROLACHIAVE con "Exit", #+ potete usare questo script per segnalare, mentre si è collegati, #+ uno scollegamento inaspettato. # Esercizio: Modificate lo script per ottenere quanto suggerito nella # nota precedente, rendendolo anche più elegante. exit 0 # Nick Drage ha suggerito un metodo alternativo: while true do ifconfig ppp0 | grep UP 1> /dev/null && echo "connesso" && exit 0 echo -n "." # Visualizza dei punti (.....) finché si è connessi. sleep 2 done # Problema: Può non bastare premere Control-C per terminare il processo. #+ (La visualizzazione dei punti potrebbe continuare.) # Esercizio: Risolvetelo. # Stephane Chazelas ha un'altra alternativa ancora: INTERVALLO=1 while ! tail -n 1 "$FILELOG" | grep -q "$PAROLACHIAVE" do echo -n . sleep $INTERVALLO done echo "Connesso" # Esercizio: Discutete i punti di forza e i punti deboli # di ognuno di questi differenti approcci. |
Naturalmente, il comando trap viene impiegato per altri scopi oltre a quello per il debugging.
Esempio 29-8. Esecuzione di processi multipli (su una postazione SMP)
#!/bin/bash # parent.sh # Eseguire processi multipli su una postazione SMP.* # * SMP=Symmetric multiprocessing: multiprocessore simmetrico [N.d.T.] # Autore: Tedman Eng # Questo è il primo di due script, #+ entrambi i quali devono essere presenti nella directory di lavoro corrente. LIMITE=$1 # Numero totale dei processi da mettere in esecuzione NUMPROC=4 # Numero di thread concorrenti (fork?) PROCID=1 # ID del processo che sta per partire echo "Il mio PID è $$" function inizia_thread() { if [ $PROCID -le $LIMITE ] ; then ./child.sh $PROCID& let "PROCID++" else echo "Limite raggiunto." wait exit fi } while [ "$NUMPROC" -gt 0 ]; do inizia_thread; let "NUMPROC--" done while true do trap "inizia_thread" SIGRTMIN done exit 0 # ======== Secondo script ======== #!/bin/bash # child.sh # Eseguire processi multipli su una postazione SMP. # Questo script viene richiamato da parent.sh. # Autore: Tedman Eng temp=$RANDOM indice=$1 shift let "temp %= 5" let "temp += 4" echo "Inizio $indice Tempo:$temp" "$@" sleep ${temp} echo "Termino $indice" kill -s SIGRTMIN $PPID exit 0 # =================== NOTA DELL'AUTORE DELLO SCRIPT ==================== # # Non è completamente esente da errori. # L'ho eseguito con limite = 500 e dopo poche centinaia di iterazioni, #+ uno dei thread concorrenti è scomparso! # Non sono sicuro che si tratti di collisioni dal trap dei segnali #+ o qualcos'altro. # Una volta ricevuto il trap, intercorre un breve lasso di tempo tra #+ l'esecuzione del gestore del trap e l'impostazione del trap successivo. #+ In questo intervallo il segnale di trap potrebbe andar perso e, #+ conseguentemente, anche la generazione del processo figlio. # Non ho alcun dubbio che qualcuno riuscirà a individuare il "bug" #+ e a lavorerci sopra . . . in futuro. # ====================================================================== # # -----------------------------------------------------------------------# ################################################################## # Quello che segue è lo script originale scritto da Vernia Damiano. # Sfortunatamente non funziona correttamente. ################################################################## #!/bin/bash # Lo script deve essere richiamato con almeno un parametro numerico #+ (numero dei processi simultanei). # Tutti gli altri parametri sono passati ai processi in esecuzione. INDICE=8 # Numero totale di processi da mettere in esecuzione TEMPO=5 # Tempo massimo d'attesa per processo E_NOARG=65 # Nessun argomento(i) passato allo script. if [ $# -eq 0 ] # Controlla la presenza di almeno un argomento. then echo "Utilizzo: `basename $0` numero_dei_processi [parametri passati]" exit $E_NOARG fi NUMPROC=$1 # Numero dei processi simultanei shift PARAMETRI=( "$@" ) # Parametri di ogni processo function avvia() { local temp local index temp=$RANDOM index=$1 shift let "temp %= $TEMPO" let "temp += 1" echo "Inizia $index Tempo:$temp" "$@" sleep ${temp} echo "Termina $index" kill -s SIGRTMIN $$ } function parti() { if [ $INDICE -gt 0 ] ; then avvia $INDICE "${PARAMETRI[@]}" & let "INDICE--" else trap : SIGRTMIN fi } trap parti SIGRTMIN while [ "$NUMPROC" -gt 0 ]; do parti; let "NUMPROC--" done wait trap - SIGRTMIN exit $? : <<COMMENTO_DELL'AUTORE_DELLO_SCRIPT Avevo la necessità di eseguire un programma, con determinate opzioni, su un numero diverso di file, utilizzando una macchina SMP. Ho pensato, quindi, di mantenere in esecuzione un numero specifico di processi e farne iniziare uno nuovo ogni volta . . . che uno di quest'ultimi terminava. L'istruzione "wait" non è d'aiuto, poichè attende sia per un dato processo sia per *tutti* i processi in esecuzione sullo sfondo (background). Ho scritto, di conseguenza, questo script che è in grado di svolgere questo compito, usando l'istruzione "trap". --Vernia Damiano COMMENTO_DELL'AUTORE_DELLO_SCRIPT |
trap '' SEGNALE (due apostrofi adiacenti) disabilita SEGNALE nella parte restante dello script. trap SEGNALE ripristina nuovamente la funzionalità di SEGNALE. È utile per proteggere una parte critica dello script da un interrupt indesiderato. |
trap '' 2 # Il segnale 2 è Control-C, che ora è disabilitato. comando comando comando trap 2 # Riabilita Control-C |
La versione 3 di Bash ha aggiunto le variabili speciali seguenti ad uso di chi deve eseguire il debugging.
|
[1] | Il Bash debugger di Rocky Bernstein colma, in parte, questa lacuna. |
[2] | Convenzionalmente, il segnale 0 è assegnato a exit. |