Überfüllen von Fehlern bei der Befehlsersetzung mit "-o errtrace" (dh setzen von -E)

14

Nach diesem Referenzhandbuch :

-E (auch -o errtrace)

Wenn diese Option aktiviert ist, werden alle Traps in ERR von Shell-Funktionen, Befehlsersetzungen und Befehlen übernommen, die in einer Subshell-Umgebung ausgeführt werden. Die ERR-Falle wird in solchen Fällen normalerweise nicht vererbt.

Ich muss es jedoch falsch interpretieren, da Folgendes nicht funktioniert:

#!/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 ! "

Ich kenne schon einfache Zuordnung, my_var=$(made_up_name)werde das Skript mit verlassen set -e(also errexit).

Soll -E/-o errtracedas wie der obige Code funktionieren? Oder habe ich es höchstwahrscheinlich falsch gelesen?

dgo.a
quelle
2
Das ist eine gute Frage. Ersetzen echo $( made up name )durch $( made up name )erzeugt das gewünschte Verhalten. Ich habe jedoch keine Erklärung.
Iruvar
Ich weiß nichts über Bash -E, aber ich weiß, dass -e nur dann einen Shell-Exit bewirkt, wenn der Fehler aus dem letzten Befehl in einer Pipeline resultiert. Ihre var=$( pipe )und $( pipe )Beispiele würden also beide Pipe-Endpunkte darstellen, wohingegen pipe > echodies nicht der Fall wäre. Meine Manpage sagt: "1. Der Ausfall eines einzelnen Befehls in einer
Multibefehlspipeline
Sie können es jedoch zum Scheitern bringen: echo $ ($ {madeupname?}). Aber das ist festgelegt. Wieder ist -E außerhalb meiner eigenen Erfahrung.
mikeserv
@mikeserv @ 1_CR Das bash manual @ echo gibt an, dass echoimmer 0 zurückgegeben wird. Dies muss in der Analyse berücksichtigt werden ...

Antworten:

5

Hinweis: zshBeschwert sich über "schlechte Muster", wenn Sie für die meisten Beispiele hier keine "Inline-Kommentare" zulassen und diese nicht wie bisher über eine Proxy-Shell ausführen sh <<-\CMD.

Okay, also, wie ich in den obigen Kommentaren angegeben habe, weiß ich nicht genau, was bash istset -E , aber ich weiß, dass POSIX-kompatible Shells ein einfaches Mittel zum Testen eines Werts darstellen, wenn Sie es wünschen:

    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

Oben sehen Sie werden feststellen , dass wenn ich verwendet parameter expansionzu testen , um ${empty?} _test()noch returns ein Pass - wie in der letzten bekundete wird echoDies geschieht , weil der ausgefallene Wert der tötet $( command substitution )Subshell , die es enthält, aber seine Eltern Schale - _testzu diesem Zeitpunkt - hält auf Lkw - Transporte. Und echoes ist mir egal - es ist reichlich glücklich, nur zu dienen, \newline; echoist kein Test.

Aber bedenke folgendes:

    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

Da ich die _test()'sEingabe mit einem vorab ausgewerteten Parameter gespeist habe , versucht INIT here-documentdie _test()Funktion überhaupt nicht, sie auszuführen. Außerdem gibt die shShell den Geist anscheinend ganz auf und echo "this doesnt even print" druckt nicht einmal.

Wahrscheinlich ist , dass nicht , was Sie wollen.

Dies geschieht, weil die ${var?}style parameter-expansion so konzipiert ist, dass sieshell im Falle eines fehlenden Parameters beendet wird. Dies funktioniert folgendermaßen :

${parameter:?[word]}

Fehler angeben, wenn Nulloder Unset.Wenn der Parameter nicht gesetzt oder null ist, muss das expansion of word(oder eine Meldung, die angibt , dass der Parameter nicht gesetzt ist, wenn das Wort weggelassen wird) written to standard errorund das sein shell exits with a non-zero exit status. Ansonsten ist der Wert von parameter shall be substituted. Eine interaktive Shell muss nicht beendet werden.

Ich werde nicht das gesamte Dokument kopieren / einfügen, aber wenn Sie einen Fehler für einen set but nullWert wünschen, verwenden Sie das Formular:

${var :? error message }

Mit dem :colonwie oben. Wenn ein nullWert erfolgreich sein soll, lassen Sie den Doppelpunkt weg. Sie können es auch negieren und nur für festgelegte Werte fehlschlagen, wie ich gleich zeigen werde.

Ein weiterer Lauf von _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

Dies funktioniert mit allen Arten von Schnelltests. Oben sehen Sie jedoch, dass die _test()Ausführung ab der Mitte des Fehlers pipelineund die darin enthaltene command listSubshell vollständig fehlschlägt, da keiner der Befehle in der Funktion ausgeführt wird und der folgende echoüberhaupt nicht ausgeführt wird. es wird aber auch gezeigt, dass es einfach getestet werden kann, da es echo "now it prints" jetzt druckt.

Der Teufel steckt wohl im Detail. Im obigen Fall ist die Shell, die beendet wird, nicht die des Skripts, _main | logic | pipelinesondern die ( subshell in which we ${test?} ) ||, für die ein wenig Sandboxing erforderlich ist.

Und es mag nicht offensichtlich sein, aber wenn Sie nur für den umgekehrten Fall oder nur für set=Werte übergehen wollten , ist es auch ziemlich einfach:

    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

Das obige Beispiel nutzt alle 4 Formen der POSIX-Parametersubstitution und ihre verschiedenen :colon nulloder not nullTests. Es gibt mehr Informationen im obigen Link und hier ist es wieder .

Und ich denke, wir sollten auch unsere _testFunktionsarbeit zeigen , oder? Wir deklarieren nur empty=somethingals Parameter für unsere Funktion (oder jederzeit vorher):

    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!

Es ist zu beachten, dass diese Bewertung für sich genommen ist - es ist kein zusätzlicher Test erforderlich, um fehlzuschlagen. Noch ein paar Beispiele:

    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?

Und so kommen wir schließlich zur ursprünglichen Frage zurück: Wie $(command substitution)gehe ich mit Fehlern in einer Subshell um? Die Wahrheit ist - es gibt zwei Möglichkeiten, aber keine ist direkt. Der Kern des Problems ist der Evaluierungsprozess der Shell - Shell-Erweiterungen (einschließlich $(command substitution)) treten im Evaluierungsprozess der Shell früher auf als die aktuelle Ausführung von Shell-Befehlen - und dies ist der Zeitpunkt, an dem Ihre Fehler abgefangen und abgefangen werden könnten.

Das Problem, das bei der $(command substitution)Operation auftritt, ist , dass die Subshell zum Zeitpunkt der Auswertung der aktuellen Shell auf Fehler bereits ersetzt wurde - es verbleiben keine Fehler.

Also, was sind die beiden Möglichkeiten? Entweder machen Sie es explizit in der $(command substitution)Subshell mit Tests, als würden Sie es ohne, oder Sie absorbieren die Ergebnisse in eine aktuelle Shell-Variable und testen ihren Wert.

Methode 1:

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

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

    echo $?

126

Methode 2:

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

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

    echo $?

1

Dies schlägt unabhängig von der Anzahl der pro Zeile deklarierten Variablen fehl:

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

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

Und unser Rückgabewert bleibt konstant:

    echo $?
1

JETZT DIE FALLE:

    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
U / min mikeserv
quelle
Zum Beispiel praktisch seine genaue Spezifikation: echo $ {v = $ (madeupname)}; echo "$ {v:? trap1st}! ist nicht erreicht!"
mikeserv
1
Zusammenfassend lässt sich sagen, dass diese Parametererweiterungen eine großartige Möglichkeit sind, um zu steuern, ob Variablen gesetzt sind usw. In Bezug auf den Exit-Status von Echo habe ich festgestellt, dass echo "abc" "${v1:?}"das Programm scheinbar nicht ausgeführt wird (abc wurde nie gedruckt). Und die Shell gibt 1 zurück. Dies gilt mit oder ohne Befehl even ( "${v1:?}"direkt auf der CLI). Für das OP-Skript war es jedoch nur erforderlich, seine Variablenzuweisung, die einen Ersatz für einen nicht vorhandenen Befehl enthält, in eine einzelne Zeile zu setzen. Ansonsten ist das Verhalten des Echos immer 0 zurückzugeben, sofern es nicht wie bei den von Ihnen erläuterten Tests unterbrochen wurde.
Nur v=$( madeup ). Ich sehe nicht, was daran unsicher ist. Es ist nur eine Aufgabe, die Person hat zum Beispiel den Befehl falsch geschrieben v="$(lss)". Es ist ein Fehler. Ja können Sie mit dem Fehlerstatus des letzten Befehls $ überprüfen? - weil es der Befehl in der Zeile ist (eine Zuweisung ohne Befehlsnamen) und sonst nichts - kein Argument zum Echo. Außerdem wird es hier von der Funktion als! = 0 abgefangen, sodass Sie zweimal eine Rückmeldung erhalten. Ansonsten gibt es sicher, wie Sie erklären, eine bessere Möglichkeit, dies innerhalb eines Rahmens zu ordnen, aber OP hatte eine einzige Zeile: ein Echo plus seine fehlgeschlagene Substitution. Er wunderte sich über das Echo.
Ja, eine einfache Zuordnung wird in der Frage ebenso erwähnt wie eine Selbstverständlichkeit, und obwohl ich keine konkreten Ratschläge zum Einsatz von -E geben konnte, habe ich versucht, ähnliche Ergebnisse zu dem gezeigten Problem zu zeigen, die mehr als möglich waren, ohne darauf zurückzugreifen Bashismen. In jedem Fall sind es gerade die von Ihnen angesprochenen Probleme - wie die Einzelzuweisung pro Zeile -, die es schwierig machen, solche Lösungen in einer Pipeline zu handhaben, und ich habe auch gezeigt, wie man damit umgeht. Obwohl es wahr ist, was Sie sagen, gibt es nichts Unsicheres daran, es einfach zuzuweisen.
mikeserv
1
Guter Punkt - vielleicht sind mehr Anstrengungen erforderlich. Ich denke darüber nach.
mikeserv
1

Wenn diese Option aktiviert ist, werden alle Traps in ERR von Shell-Funktionen, Befehlsersetzungen und Befehlen übernommen, die in einer Subshell-Umgebung ausgeführt werden

In Ihrem Skript ist es eine Befehlsausführung ( echo $( made up name )). In Bash-Befehlen wird entweder mit ; oder mit neuer Zeile . Im Befehl

echo $( made up name )

$( made up name )wird als Teil des Befehls betrachtet. Selbst wenn dieser Teil fehlschlägt und mit einem Fehler zurückkehrt, wird der gesamte Befehl erfolgreich ausgeführt, da er echonichts davon weiß. Wenn der Befehl mit 0 zurückkehrt, wird kein Trap ausgelöst.

Sie müssen es in zwei Befehle, Zuweisung und Echo, setzen

var=$(made_up_name)
echo $var
Totti
quelle
echo $varnicht scheitern - $varwird erweitert, um leer zu sein, bevor man es sich überhaupt echoansieht.
mikeserv
0

Dies ist auf einen Fehler in der Bash zurückzuführen. Während der Befehlsersetzung treten Sie ein

echo $( made up name )

madewird in einer Subshell ausgeführt (oder nicht gefunden), die Subshell ist jedoch so "optimiert", dass sie keine Überfüllungen der übergeordneten Shell verwendet. Dies wurde in Version 4.4.5 behoben :

Unter bestimmten Umständen wird ein einfacher Befehl optimiert, um eine Verzweigung zu beseitigen, was dazu führt, dass ein EXIT-Trap nicht ausgeführt wird.

Mit bash 4.4.5 oder höher sollten Sie die folgende Ausgabe sehen:

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

Der Trap-Handler wurde wie erwartet aufgerufen, und die Subshell wird beendet. ( set -eBewirkt nur, dass die Subshell beendet wird, nicht die übergeordnete, sodass die Nachricht "sollte nicht erreicht werden" tatsächlich erreicht werden sollte.)

Eine Problemumgehung für ältere Versionen besteht darin, die Erstellung einer vollständigen, nicht optimierten Subshell zu erzwingen:

echo $( ( made up name ) )

Die zusätzlichen Räume werden benötigt von Arithmetic Erweiterung zu unterscheiden.

JigglyNaga
quelle