Comportamento corretto delle trappole EXIT ed ERR quando si usa `set -eu`


27

Sto osservando un comportamento strano quando si usa set -e( errexit), set -u( nounset) insieme alle trappole ERR ed EXIT. Sembrano collegati, quindi metterli in una domanda sembra ragionevole.

1) set -unon attiva trappole ERR

  • Codice:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
  • Previsto: viene chiamata la trap ERR, RC! = 0
  • Effettivo: la trap ERR non viene chiamata, RC == 1
  • Nota: set -enon cambia il risultato

2) L'uso set -eudel codice di uscita in una trap EXIT è 0 invece di 1

  • Codice:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
  • Previsto: viene chiamata la trap EXIT, RC == 1
  • Effettivo: viene chiamata la trap EXIT, RC == 0
  • Nota: quando si utilizza set +e, RC == 1. La trap EXIT restituisce il RC corretto quando qualsiasi altro comando genera un errore.
  • Modifica: C'è un post SO su questo argomento con un commento interessante che suggerisce che questo potrebbe essere correlato alla versione di Bash in uso. Testare questo frammento con Bash 4.3.11 produce un RC = 1, quindi è meglio. Sfortunatamente al momento non è possibile aggiornare Bash (dalla 3.2.51) su tutti gli host, quindi dobbiamo trovare un'altra soluzione.

Qualcuno può spiegare uno di questi comportamenti?

La ricerca di questi argomenti non ha avuto molto successo, il che è piuttosto sorprendente dato il numero di post su impostazioni e trappole di Bash. C'è un thread del forum , però, ma la conclusione è piuttosto insoddisfacente.


3
Da 4 in poi penso di aver bashrotto con lo standard e ho iniziato a mettere trappole nelle sottosche. La trappola dovrebbe essere eseguita nello stesso ambiente da cui proviene il ritorno, ma bashnon lo fa da un po 'di tempo.
Mikeserv,

1
Aspetta un minuto: vuoi una soluzione o una spiegazione? E se vuoi una soluzione, allora una soluzione a cosa esattamente? Che cosa vuoi che accada? set -ee set -usono entrambi progettati specificamente per uccidere un guscio con script. Usarli in condizioni che potrebbero innescare la loro applicazione ucciderà una shell con script. Non c'è modo di aggirarlo, tranne per non usarli, e invece per testare quelle condizioni quando si applicano in una sequenza di codice. Quindi, in sostanza, puoi scrivere un buon codice shell o puoi usarlo set -eu.
Mikeserv,

2
In realtà, sto cercando entrambi, poiché non sono riuscito a trovare informazioni sufficienti sul perché -unon attivare la trap ERR (è un errore, quindi non dovrebbe attivare la trap) o il codice di errore è 0 invece di 1. Il quest'ultimo sembra essere un bug che è già stato corretto nella versione successiva, quindi è quello. Ma la prima parte è piuttosto difficile da capire se non ti sei reso conto che gli errori nella valutazione della shell (espansione dei parametri) e gli errori reali nei comandi sembrano essere due cose diverse. Per la soluzione, bene, come hai suggerito, ora sto cercando di evitare -eue controllare manualmente quando è necessario.
dvdgsng,

1
@dvdsng - Bene. Questa è la strada da percorrere: dovresti pubblicare la tua sceneggiatura quando fai una risposta e premiarti con la grazia. Non mi piacciono davvero quelle opzioni: non consentono la gestione delle eccezioni in modo sicuro.
Mikeserv,

1
@dvdsng - dove una di queste opzioni può essere utile, tuttavia, si trova in un contesto sottoscritto. E quindi è concepibile che qualunque cosa tu li stia usando prima potesse essere localizzata in un contesto subshell come: (set -u; : $UNSET_VAR)e simili. Anche questo tipo di cose può essere buono - puoi lasciarne cadere molte di &&tanto in tanto: (set -e; mkdir dir; cd dir; touch dirfile)se ottieni la mia deriva. È solo che si tratta di contesti controllati: quando li imposti come opzioni globali, perdi il controllo e diventi controllato. Di solito ci sono soluzioni più efficienti.
Mikeserv,

Risposte:


15

Da man bash:

  • set -u
    • Tratta variabili e parametri non impostati diversi dai parametri speciali "@"e "*"come un errore quando esegui l'espansione dei parametri. Se si tenta di espandere una variabile o un parametro non impostato, la shell stampa un messaggio di errore e, se non è -iattivo, esce con uno stato diverso da zero.

POSIX afferma che, in caso di errore di espansione , una shell non interattiva deve uscire quando l'espansione è associata a un builtin speciale shell (che è una distinzione bashche ignora regolarmente comunque, e quindi forse è irrilevante) o qualsiasi altra utilità oltre .

  • Conseguenze degli errori Shell :
    • Un errore di espansione si verifica quando vengono eseguite le espansioni della shell definite in Espansioni di parole (ad esempio "${x!y}", perché !non è un operatore valido) ; un'implementazione può considerarli come errori di sintassi se è in grado di rilevarli durante la tokenizzazione, anziché durante l'espansione.
    • [A] n la shell interattiva deve scrivere un messaggio diagnostico su errore standard senza uscire.

Anche da man bash:

  • trap ... ERR
    • Se un sigspec è ERR , il comando arg viene eseguito ogni volta che una pipeline (che può consistere in un singolo comando semplice) , un elenco o un comando composto restituisce uno stato di uscita diverso da zero, fatte salve le seguenti condizioni:
      • Il trap ERR non viene eseguito se il comando non riuscito fa parte dell'elenco dei comandi immediatamente dopo una whileo untilparola chiave ...
      • ... parte del test in una ifdichiarazione ...
      • ... parte di un comando eseguito in una &&o ||lista tranne il comando che segue il finale &&o ||...
      • ... qualsiasi comando in una pipeline ma l'ultimo ...
      • ... o se il valore di ritorno del comando viene invertito usando !.
    • Queste sono le stesse condizioni rispettate dall'opzione errexit -e .

Nota sopra che la trap ERR riguarda la valutazione del ritorno di alcuni altri comandi. Ma quando si verifica un errore di espansione , non viene eseguito alcun comando per restituire qualcosa. Nel tuo esempio, echo non succede mai - perché mentre la shell valuta ed espande i suoi argomenti incontra una -uvariabile nset, che è stata specificata dall'opzione esplicita della shell per causare un'uscita immediata dalla shell corrente, con script.

Quindi viene eseguita la trap EXIT , se presente, e la shell esce con un messaggio diagnostico e esce dallo stato diverso da 0, esattamente come dovrebbe fare.

Per quanto riguarda la cosa rc: 0 , mi aspetto che sia un bug specifico della versione di qualche tipo - probabilmente a che fare con i due trigger per l' EXIT che si verificano contemporaneamente e l'uno che ottiene il codice di uscita dell'altro (che non dovrebbe accadere) . E comunque, con un bashbinario aggiornato installato da pacman:

bash <<\IN
    printf "shell options:\t$-\n"
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
IN

Ho aggiunto la prima linea in modo da poter vedere che le condizioni della shell sono quelle di una shell script - è non è interattivo. L'output è:

shell options:  hB
bash: line 4: UNSET_VAR: unbound variable
EXIT (rc: 1)

Ecco alcune note rilevanti dai recenti log delle modifiche :

  • Risolto un problema per il quale i comandi asincroni non $?venivano impostati correttamente.
  • Risolto un bug che causava il numero di riga errato nei messaggi di errore generati da errori di espansione nei forcomandi.
  • Risolto un problema per il quale SIGINT e SIGQUIT non trappotevano essere letti nei comandi subshell asincroni.
  • Risolto il problema con la gestione degli interruzioni che causava l'ignoramento di un secondo e successivo SIGINT da parte di shell interattive.
  • La shell non blocca più la ricezione di segnali durante l'esecuzione di trapgestori per tali segnali e consente alla maggior parte dei trap gestori di essere eseguiti in modo ricorsivo (eseguendo trapgestori mentre un trapgestore sta eseguendo) .

Penso che sia l'ultimo o il primo ad essere più rilevante - o forse una combinazione dei due. Un trapgestore è per sua natura asincrono perché il suo intero compito è di attendere e gestire segnali asincroni . E ne attivi due contemporaneamente con -eue $UNSET_VAR.

E quindi forse dovresti semplicemente aggiornare, ma se ti piaci, lo farai con una shell completamente diversa.


Grazie per la spiegazione di come viene gestita diversamente l'espansione dei parametri. Questo mi ha chiarito molte cose.
dvdgsng,

Ti concedo la generosità perché la tua spiegazione è stata di grande aiuto.
dvdgsng,

@dvdgsng - Gracias. Per curiosità, ti è mai venuto in mente la tua soluzione?
Mikeserv,

9

(Sto usando bash 4.2.53). Per la parte 1, la pagina man di bash dice semplicemente "Un messaggio di errore verrà scritto nell'errore standard e una shell non interattiva uscirà". Non dice che verrà chiamata una trappola ERR, anche se concordo che sarebbe utile se lo facesse.

Per essere pragmatico, se quello che vuoi davvero è far fronte in modo più pulito con variabili non definite, una possibile soluzione è quella di mettere la maggior parte del codice all'interno di una funzione, quindi eseguire quella funzione in una sotto-shell e recuperare il codice di ritorno e l'output stderr. Ecco un esempio in cui "cmd ()" è la funzione:

#!/bin/bash
trap 'rc=$?; echo "ERR at line ${LINENO} (rc: $rc)"; exit $rc' ERR
trap 'rc=$?; echo "EXIT (rc: $rc)"; exit $rc' EXIT
set -u
set -E # export trap to functions

cmd(){
 echo "args=$*"
 echo ${UNSET_VAR}
 echo hello
}
oops(){
 rc=$?
 echo "$@"
 return $rc # provoke ERR trap
}

exec 3>&1 # copy stdin to use in $()
if output=$(cmd "$@" 2>&1 >&3) # collect stderr, not stdout 
then    echo ok
else    oops "fail: $output"
fi

Sulla mia bash ottengo

./script my stuff; echo "exit was $?"
args=my stuff
fail: ./script: line 9: UNSET_VAR: unbound variable
ERR at line 15 (rc: 1)
EXIT (rc: 1)
exit was 1

bello, una soluzione pratica che aggiunge davvero valore!
Florian Heigl,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.