Intrappolare gli errori nella sostituzione dei comandi usando “-o errtrace” (cioè impostare -E)


14

Secondo questo manuale di riferimento :

-E (anche -o errtrace)

Se impostato, qualsiasi trap su ERR viene ereditato da funzioni shell, sostituzioni di comandi e comandi eseguiti in un ambiente subshell. La trappola ERR non viene normalmente ereditata in questi casi.

Tuttavia, devo interpretarlo in modo errato, perché non funziona:

#!/usr/bin/env bash
# -*- bash -*-

set -e -o pipefail -o errtrace -o functrace

function boom {
  echo "err status: $?"
  exit $?
}
trap boom ERR


echo $( made up name )
echo "  ! should not be reached ! "

Conosco già un semplice incarico, my_var=$(made_up_name)uscirà dallo script con set -e(cioè errexit).

È -E/-o errtracedovrebbe funzionare come il codice di cui sopra? O, molto probabilmente, l'ho letto male?


2
Questa è una buona domanda La sostituzione echo $( made up name )con $( made up name )produce il comportamento desiderato. Non ho una spiegazione però.
Iruvar,

Non so di -h di bash ma so che -e ha effetto sull'uscita di una shell solo se l'errore deriva dall'ultimo comando in una pipeline. Quindi il tuo var=$( pipe )e gli $( pipe )esempi rappresenterebbero entrambi i punti finali del tubo, mentre pipe > echonon lo sarebbero. La mia pagina man dice: "1. Il fallimento di un singolo comando in una pipeline multi-comando non deve far uscire la shell. Si dovrà considerare solo il fallimento della pipeline stessa."
mikeserv

Puoi farlo fallire però: echo $ ($ {madeupname?}). Ma questo è tutto. Ancora una volta, -E è al di fuori della mia esperienza.
Mikeserv,

@mikeserv @ 1_CR Il manuale bash @ echo indica che echorestituisce sempre 0. Questo deve essere preso in considerazione nell'analisi ...

Risposte:


5

Nota: zshsi lamenterà di "cattivi schemi" se non lo si configura per accettare "commenti incorporati" per la maggior parte degli esempi qui e non eseguirli attraverso una shell proxy come ho fatto con sh <<-\CMD.

Ok, quindi, come ho affermato nei commenti sopra, non so specificamente di bashset -E , ma so che le shell compatibili con POSIX forniscono un semplice mezzo per testare un valore se lo desideri:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
        echo "echo still works" 
    }
    _test && echo "_test doesnt fail"
    # END
    CMD
sh: line 1: empty: error string
+ echo

+ echo 'echo still works'
echo still works
+ echo '_test doesnt fail'
_test doesnt fail

Sopra vedrai che anche se ho usato parameter expansionper testare ${empty?} _test()ancora returns un passaggio - come è dimostrato nell'ultimo echoCiò si verifica perché il valore non riuscito uccide la $( command substitution )subshell che lo contiene, ma la shell madre - _testin questo momento - continua a trasportare. E echonon importa - è molto felice di servire solo un non\newline; echo è un test.

Ma considera questo:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    _test ||\
            echo "this doesnt even print"
    # END
    CMD
_test+ sh: line 1: empty: function doesnt run

Dato che ho _test()'sinserito input con un parametro pre-valutato nel INIT here-documentmomento in cui la _test()funzione non tenta nemmeno di essere eseguita. Inoltre, la shshell sembra rinunciare completamente al fantasma e echo "this doesnt even print" non stampa nemmeno.

Probabilmente non è quello che vuoi.

Ciò accade perché l' ${var?}espansione dei parametri di stile è progettata per uscireshell dall'eventualità di un parametro mancante, funziona in questo modo :

${parameter:?[word]}

Indica Errore se Nullo Unset.Se il parametro non è impostato o è nullo, il expansion of word(o un messaggio che indica che non è impostato se la parola è omessa) deve essere written to standard errore il shell exits with a non-zero exit status. Altrimenti, il valore di parameter shall be substituted. Non è necessario uscire da una shell interattiva.

Non copierò / incollerò l'intero documento, ma se si desidera un errore per un set but nullvalore, utilizzare il modulo:

${var :? error message }

Con il :coloncome sopra. Se vuoi che un nullvalore abbia successo, ometti i due punti. Puoi anche negarlo e fallire solo per i valori impostati, come mostrerò tra poco.

Un'altra corsa di _test():

    sh <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    echo "this runs" |\
        ( _test ; echo "this doesnt" ) ||\
            echo "now it prints"
    # END
    CMD
this runs
sh: line 1: empty: function doesnt run
now it prints

Funziona con tutti i tipi di test rapidi, ma soprattutto lo vedrai _test(), eseguito dalla metà del pipelinefallimento, e in effetti la sua command listsottoshell contenuta fallisce del tutto, poiché nessuno dei comandi all'interno della funzione viene eseguito, né la seguente viene echoeseguita affatto, sebbene sia anche dimostrato che può essere facilmente testato perché echo "now it prints" ora stampa.

Il diavolo è nei dettagli, immagino. Nel caso sopra, la shell che esce non è dello script _main | logic | pipelinema è richiesto ( subshell in which we ${test?} ) ||un po 'di sandbox.

E potrebbe non essere ovvio, ma se vuoi passare solo per il caso opposto, o solo set=valori, è anche abbastanza semplice:

    sh <<-\CMD
    N= #N is NULL
    _test=$N #_test is also NULL and
    v="something you would rather do without"    
    ( #this subshell dies
        echo "v is ${v+set}: and its value is ${v:+not NULL}"
        echo "So this ${_test:-"\$_test:="} will equal ${_test:="$v"}"
        ${_test:+${N:?so you test for it with a little nesting}}
        echo "sure wish we could do some other things"
    )
    ( #this subshell does some other things 
        unset v #to ensure it is definitely unset
        echo "But here v is ${v-unset}: ${v:+you certainly wont see this}"
        echo "So this ${_test:-"\$_test:="} will equal NULL ${_test:="$v"}"
        ${_test:+${N:?is never substituted}}
        echo "so now we can do some other things" 
    )
    #and even though we set _test and unset v in the subshell
    echo "_test is still ${_test:-"NULL"} and ${v:+"v is still $v"}"
    # END
    CMD
v is set: and its value is not NULL
So this $_test:= will equal something you would rather do without
sh: line 7: N: so you test for it with a little nesting
But here v is unset:
So this $_test:= will equal NULL
so now we can do some other things
_test is still NULL and v is still something you would rather do without

L'esempio precedente sfrutta tutte e 4 le forme di sostituzione dei parametri POSIX e i loro vari :colon nullo not nulltest. Ci sono maggiori informazioni nel link sopra, ed eccolo di nuovo .

E immagino che dovremmo mostrare anche il nostro _testlavoro funzionale, giusto? Dichiariamo semplicemente empty=somethingcome parametro per la nostra funzione (o in qualsiasi momento in anticipo):

    sh <<-\CMD
    _test() { echo $( echo ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?tested as a pass before function runs}
    INIT
    echo "this runs" >&2 |\
        ( empty=not_empty _test ; echo "yay! I print now!" ) ||\
            echo "suspiciously quiet"
    # END
    CMD
this runs
not_empty
echo still works
yay! I print now!

Va notato che questa valutazione è indipendente: non richiede alcun test aggiuntivo per fallire. Un altro paio di esempi:

    sh <<-\CMD
    empty= 
    ${empty?null, no colon, no failure}
    unset empty
    echo "${empty?this is stderr} this is not"
    # END
    CMD
sh: line 3: empty: this is stderr

    sh <<-\CMD
    _input_fn() { set -- "$@" #redundant
            echo ${*?WHERES MY DATA?}
            #echo is not necessary though
            shift #sure hope we have more than $1 parameter
            : ${*?WHERES MY DATA?} #: do nothing, gracefully
    }
    _input_fn heres some stuff
    _input_fn one #here
    # shell dies - third try doesnt run
    _input_fn you there?
    # END
    CMD
heres some stuff
one
sh: line :5 *: WHERES MY DATA?

E così finalmente torniamo alla domanda originale: come gestire gli errori in una $(command substitution)subshell? La verità è che ci sono due modi, ma nessuno dei due è diretto. Il nocciolo del problema è il processo di valutazione della shell - le espansioni della shell (incluso $(command substitution)) avvengono prima nel processo di valutazione della shell rispetto all'esecuzione del comando shell corrente - che è quando gli errori potrebbero essere catturati e intrappolati.

Il problema riscontrato dall'operazione è che quando la shell corrente valuta gli errori, la $(command substitution)subshell è già stata sostituita, senza errori.

Quindi quali sono i due modi? O lo fai esplicitamente all'interno della $(command substitution)subshell con i test come faresti senza di esso, oppure ne assorbi i risultati in una variabile di shell corrente e ne testa il valore.

Metodo 1:

    echo "$(madeup && echo \: || echo '${fail:?die}')" |\
          . /dev/stdin

sh: command not found: madeup
/dev/stdin:1: fail: die

    echo $?

126

Metodo 2:

    var="$(madeup)" ; echo "${var:?die} still not stderr"

sh: command not found: madeup
sh: var: die

    echo $?

1

Ciò fallirà indipendentemente dal numero di variabili dichiarate per riga:

   v1="$(madeup)" v2="$(ls)" ; echo "${v1:?}" "${v2:?}"

sh: command not found: madeup
sh: v1: parameter not set

E il nostro valore di ritorno rimane costante:

    echo $?
1

ORA LA TRAPPOLA:

    trap 'printf %s\\n trap resurrects shell!' ERR
    v1="$(madeup)" v2="$(printf %s\\n shown after trap)"
    echo "${v1:?#1 - still stderr}" "${v2:?invisible}"

sh: command not found: madeup
sh: v1: #1 - still stderr
trap
resurrects
shell!
shown
after
trap

    echo $?
0

Ad esempio, praticamente la sua specifica esatta: echo $ {v = $ (madeupname)}; echo "$ {v:? trap1st}! non è stato raggiunto!"
Mikeserv,

1
Riassumendo: Queste espansioni di parametri sono un ottimo modo per controllare se sono impostate variabili ecc. Per quanto riguarda lo stato di uscita dell'eco, ho notato che echo "abc" "${v1:?}"non sembra funzionare (abc mai stampato). E la shell restituisce 1. Questo è vero anche con o senza un comando ( "${v1:?}"direttamente sul cli). Ma per lo script di OP, tutto ciò che era necessario per innescare la trap è posizionare la sua assegnazione variabile contenente una sostituzione per un comando inesistente da solo su una singola riga. Altrimenti il ​​comportamento di echo è di restituire sempre 0, a meno che non sia interrotto come con i test che hai spiegato.

Basta v=$( madeup ). Non vedo cosa non sia sicuro con quello. È solo un compito, ad esempio la persona ha sbagliato a scrivere il comando v="$(lss)". Errori. Sì, puoi verificare con lo stato di errore dell'ultimo comando $? - perché è il comando sulla riga (un compito senza nome di comando) e nient'altro - non un argomento da echeggiare. Inoltre, qui è intrappolato dalla funzione come! = 0, quindi ricevi un feedback due volte. Altrimenti sicuramente, come spieghi, c'è un modo migliore per fare ciò in modo ordinato all'interno di un framework, ma OP aveva una sola riga: un'eco più la sua sostituzione fallita. Si stava chiedendo dell'eco.

Sì, la domanda è menzionata nella domanda oltre che per scontata, e sebbene non potessi offrire consigli specifici su come utilizzare -E, ho provato a mostrare risultati simili al problema dimostrato più che possibile senza ricorrere a bashismi. In ogni caso, sono in particolare i problemi che menzionate, come l'assegnazione singola per riga, che rendono difficili da gestire tali soluzioni in una pipeline, che ho anche dimostrato come gestire. Anche se è vero quello che dici: non c'è nulla di pericoloso nel assegnarlo semplicemente.
Mikeserv,

1
Un buon punto: forse è necessario uno sforzo maggiore. Ci penserò.
Mikeserv,

1

Se impostato, qualsiasi trap su ERR viene ereditato da funzioni shell, sostituzioni di comandi e comandi eseguiti in un ambiente subshell

Nel tuo script è un'esecuzione di comando ( echo $( made up name )). In bash i comandi sono delimitati con uno dei due ; o con una nuova linea . Nel comando

echo $( made up name )

$( made up name )è considerato come parte del comando. Anche se questa parte ha esito negativo e restituisce un errore, l'intero comando viene eseguito correttamente poiché echonon lo si conosce. Come il comando restituisce con 0 nessuna trap attivata.

Devi metterlo in due comandi, assegnazione ed eco

var=$(made_up_name)
echo $var

echo $varnon fallisce: $varviene espanso per svuotarsi prima echodi poterlo effettivamente vedere.
Mikeserv,

0

Ciò è dovuto a un bug in bash. Durante la sostituzione del comando intervenire

echo $( made up name )

madeviene eseguito (o non riesce a essere trovato) in una subshell, ma la subshell è "ottimizzata" in modo tale da non utilizzare alcune trap dalla shell padre. Questo è stato risolto nella versione 4.4.5 :

In determinate circostanze, un semplice comando è ottimizzato per eliminare un fork, con conseguente non esecuzione di una trap EXIT.

Con bash 4.4.5 o versioni successive, dovresti vedere il seguente output:

error.sh: line 13: made: command not found
err status: 127
  ! should not be reached !

Il gestore trap è stato chiamato come previsto, quindi viene chiusa la subshell. (set -e provoca solo l'uscita della subshell, non del genitore, in modo che il messaggio "non dovrebbe essere raggiunto" dovrebbe, in effetti, essere raggiunto.)

Una soluzione alternativa per le versioni precedenti consiste nel forzare la creazione di una subshell completa e non ottimizzata:

echo $( ( made up name ) )

Gli spazi extra sono richiesti per distinguersi dall'espansione aritmetica.

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.