Capitolo 29. Debugging

 

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
Cosa c'è di sbagliato nello script precedente (suggerimento: dopo if)?

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
	
È da notare che il messaggio d'errore non necessariamente si riferisce alla riga in cui questo si verifica, ma a quella dove l'interprete Bash si rende finalmente conto della sua presenza.

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:

  1. comparsa del messaggio "syntax error", oppure

  2. va in esecuzione, ma non funziona come dovrebbe (errore logico);

  3. viene eseguito, funziona come ci si attendeva, ma provoca pericolosi effetti collaterali (bomba logica).

Gli strumenti per la correzione di script non funzionanti comprendono

  1. gli enunciati echo posti in punti cruciali dello script, per tracciare le variabili ed avere così un quadro di quello che sta avvenendo.

    Suggerimento

    Ancor meglio è un echo che visualizza qualcosa solo quando è abilitato debug.

    ### debecho (debug-echo), di Stefano Falsetto ###
    ### Visualizza i parametri passati solo se DEBUG non è vuota. ###
    debecho () {
      if [ ! -z "$DEBUG" ]; then
         echo "$1" >&2
         #         ^^^ allo stderr
      fi
    }
    
    DEBUG=on
    Quel_che_vuoi=non_nulla
    debecho $Quel_che_vuoi   # non_nulla
    
    DEBUG=
    Quel_che_vuoi=non_nulla
    debecho $Quel_che_vuoi   # (Nessuna visualizzazione.)

  2. l'uso del filtro tee nei punti critici per verificare i processi e i flussi di dati.

  3. 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.

  4. 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 $?
  5. Usare la variabile $LINENO con il builtin caller.

  6. 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.

Trap dei segnali

trap

Specifica un'azione che deve essere eseguita alla ricezione di un segnale; è utile anche per il debugging.

Nota

Un segnale è semplicemente un messaggio inviato ad un processo, o dal kernel o da un altro processo, che gli comunica di eseguire un'azione specifica (solitamente di terminare). Per esempio, la pressione di Control-C invia un interrupt utente, il segnale INT, al programma in esecuzione.

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.

Nota

Fornendo DEBUG come argomento a trap, viene eseguita l'azione specificata dopo ogni comando presente nello script. Questo consente, per esempio, il tracciamento delle variabili.

Esempio 29-7. Tracciare una variabile

#!/bin/bash

trap 'echo "TRACCIA-VARIABILE> \$variabile = \"$variabile\""' DEBUG
# Visualizza il valore di $variabile dopo l'esecuzione di ogni comando.

variabile=29;

echo "La \"\$variabile\" è stata inizializzata a $variabile."

let "variabile *= 3"
echo "\"\$variabile\" è stata moltiplicata per 3."

exit $?

#  Il costrutto "trap 'comando1 . . . comando2 . . .' DEBUG" è più
#+ appropriato nel contesto di uno script complesso, 
#+ dove l'inserimento di molti enunciati "echo $variabile" 
#+ si rivela goffo, oltre che una perdita di tempo.

# Grazie, Stephane Chazelas per la puntualizzazione.

exit 0

Risultato dello script:


TRACCIA-VARIABILE> $variabile = ""
TRACCIA-VARIABILE> $variabile = "29"
La "$variabile" è stata inizializzata a 29.
TRACCIA-VARIABILE> $variabile = "29"
TRACCIA-VARIABILE> $variabile = "87"
La "$variabile" è stata moltiplicata per 3.
TRACCIA-VARIABILE> $variabile = "87"

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

Nota

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
	

Note

[1]

Il Bash debugger di Rocky Bernstein colma, in parte, questa lacuna.

[2]

Convenzionalmente, il segnale 0 è assegnato a exit.