Korrektes Verhalten von EXIT- und ERR-Traps bei Verwendung von "set -eu"

27

Ich beobachte ein seltsames Verhalten, wenn ich set -e( errexit), set -u( nounset) zusammen mit ERR- und EXIT-Traps verwende. Sie scheinen verwandt zu sein, daher erscheint es vernünftig, sie in eine Frage zu stellen.

1) set -ulöst keine ERR-Traps aus

  • Code:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
  • Erwartet: ERR-Trap wird aufgerufen, RC! = 0
  • Aktuell: ERR-Trap wird nicht aufgerufen, RC == 1
  • Hinweis: set -eÄndert das Ergebnis nicht

2) Bei Verwendung set -eudes Exit-Codes in einem EXIT-Trap ist 0 anstelle von 1

  • Code:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
  • Erwartet: EXIT-Trap wird aufgerufen, RC == 1
  • Aktuell: EXIT-Trap wird aufgerufen, RC == 0
  • Hinweis: Bei Verwendung von set +eRC == 1. Der EXIT-Trap gibt den richtigen RC zurück, wenn ein anderer Befehl einen Fehler ausgibt.
  • Bearbeiten: Es gibt einen SO-Beitrag zu diesem Thema mit einem interessanten Kommentar, der darauf hinweist , dass dies möglicherweise mit der verwendeten Bash-Version zusammenhängt. Das Testen dieses Snippets mit Bash 4.3.11 ergibt eine RC = 1, das ist also besser. Ein Upgrade von Bash (von 3.2.51) auf alle Hosts ist derzeit leider nicht möglich, daher müssen wir uns eine andere Lösung einfallen lassen.

Kann jemand eines dieser Verhalten erklären?

Das Durchsuchen dieser Themen war nicht sehr erfolgreich, was angesichts der Anzahl der Posts zu Bash-Einstellungen und -Fallen eher überraschend ist. Es gibt zwar einen Forenthread , aber das Fazit ist eher unbefriedigend.

dvdgsng
quelle
3
Ich glaube, ab 4 Jahren hat bashder Standard gebrochen und Fallen in Unterschalen gelegt. Die Falle soll in derselben Umgebung ausgeführt werden, aus der die Rückgabe kam, bashhat dies aber seit einiger Zeit nicht mehr getan.
mikeserv
1
Warten Sie eine Minute - möchten Sie eine Lösung oder eine Erklärung? Und wenn Sie eine Lösung wollen, für was genau? Was möchten Sie tun? set -eund set -ubeide wurden speziell entwickelt, um eine Skript-Shell zu töten . Mit ihnen unter Bedingungen , die ihre Anwendung auslösen könnten wird töten eine Skript Shell. Daran führt kein Weg vorbei, außer sie nicht zu verwenden und stattdessen auf diese Bedingungen zu testen , wenn sie in einer Codesequenz angewendet werden. Grundsätzlich können Sie also guten Shell-Code schreiben oder verwenden set -eu.
mikeserv
2
Eigentlich suche ich nach beidem, da ich keine ausreichenden Informationen darüber finden konnte, warum -udie ERR-Falle nicht ausgelöst werden sollte (es ist ein Fehler, sollte es also nicht die Falle auslösen) oder der Fehlercode 0 statt 1 ist Letzteres scheint ein Fehler zu sein, der bereits in einer späteren Version behoben wurde. Der erste Teil ist jedoch schwer zu verstehen, wenn Sie nicht bemerkt haben, dass Fehler in der Shell-Auswertung (Parametererweiterung) und tatsächliche Fehler in Befehlen zwei verschiedene Dinge zu sein scheinen. Wie Sie vorgeschlagen haben, versuche ich jetzt, die Lösung zu vermeiden -euund manuell zu überprüfen, ob sie erforderlich ist.
dvdgsng
1
@dvdsng - Gut. Das ist der richtige Weg - Sie sollten Ihr Skript veröffentlichen, wenn Sie dies als Antwort tun, und sich das Kopfgeld zuerkennen. Ich mag diese Optionen wirklich nicht - sie erlauben keine sichere Ausnahmebehandlung.
mikeserv
1
@dvdsng - wo eine dieser Optionen nützlich sein kann, befindet sich jedoch in einem untergeordneten Kontext. Und so ist es denkbar, dass alles, wofür Sie sie zuvor verwendet haben, in einen Subshell-Kontext wie: (set -u; : $UNSET_VAR)und ähnliches lokalisiert werden kann . Diese Art von Zeug kann auch gut sein - Sie können &&gelegentlich eine Menge davon fallen lassen:, (set -e; mkdir dir; cd dir; touch dirfile)wenn Sie meinen Drift bekommen. Es ist nur so, dass dies kontrollierte Kontexte sind - wenn Sie sie als globale Optionen festlegen, verlieren Sie die Kontrolle und werden kontrolliert. In der Regel gibt es jedoch effizientere Lösungen.
mikeserv

Antworten:

15

Von man bash:

  • set -u
    • Treat ungesetzt Variablen und andere Parameter als die speziellen Parameter "@"und "*"als Fehler , wenn der Parameter Expansion durchführt. Wenn versucht wird, eine nicht -igesetzte Variable oder einen nicht gesetzten Parameter zu erweitern, gibt die Shell eine Fehlermeldung aus und beendet das Programm mit einem Status ungleich Null , wenn sie nicht interaktiv ist.

POSIX gibt an, dass im Falle eines Erweiterungsfehlers eine nicht interaktive Shell beendet wird, wenn die Erweiterung entweder mit einer speziellen eingebauten Shell (die von einer Unterscheidung bashregelmäßig ignoriert wird und daher möglicherweise irrelevant ist) oder einem anderen Dienstprogramm verknüpft ist .

  • Folgen von Shell-Fehlern :
    • Ein Erweiterungsfehler tritt auf, wenn die in Word Expansions definierten Shell-Erweiterungen ausgeführt werden (z. B. "${x!y}"weil !es sich nicht um einen gültigen Operator handelt) . Eine Implementierung kann diese als Syntaxfehler behandeln, wenn sie diese während der Tokenisierung und nicht während der Erweiterung erkennen kann.
    • [A] n Interactive Shell schreibt eine Diagnosemeldung an Standardfehler, ohne zu beenden.

Auch von man bash:

  • trap ... ERR
    • Wenn eine Sigspec ERR ist , wird der Befehl arg immer dann ausgeführt, wenn eine Pipeline (die aus einem einzelnen einfachen Befehl bestehen kann) , eine Liste oder ein zusammengesetzter Befehl unter den folgenden Bedingungen einen Exit-Status ungleich Null zurückgibt:
      • Der ERR- Trap wird nicht ausgeführt, wenn der fehlgeschlagene Befehl unmittelbar nach einem whileoder until-Schlüsselwort in der Befehlsliste enthalten ist.
      • ... Teil des Tests in einer ifErklärung ...
      • ... Teil eines Befehls, der in einer &&oder ||-Liste ausgeführt wird, mit Ausnahme des Befehls nach dem letzten &&oder ||...
      • ... irgendein Befehl in einer Pipeline, aber der letzte ...
      • ... oder wenn der Rückgabewert des Befehls mit invertiert wird !.
    • Dies sind die gleichen Bedingungen, die von der Option errexit eingehalten werden -e .

Beachten Sie, dass es in der ERR- Falle ausschließlich um die Auswertung der Rückkehr eines anderen Befehls geht. Wenn jedoch ein Erweiterungsfehler auftritt, wird kein Befehl ausgeführt, um etwas zurückzugeben. In Ihrem Beispiel echo geschieht dies niemals, da die Shell beim Auswerten und -uErweitern ihrer Argumente auf eine nset-Variable stößt , die durch die explizite Shell-Option angegeben wurde, um ein sofortiges Verlassen der aktuellen, skriptbasierten Shell zu bewirken.

Und so wird der EXIT- Trap, falls vorhanden, ausgeführt, und die Shell wird mit einer Diagnosemeldung und einem anderen Beendigungsstatus als 0 beendet - genau so, wie es sein sollte.

Ich gehe davon aus, dass es sich bei der Sache rc: 0 um einen versionsspezifischen Fehler handelt. Dies hängt wahrscheinlich damit zusammen, dass die beiden Auslöser für den EXIT gleichzeitig auftreten und der eine den Exit-Code des anderen abruft (der nicht auftreten sollte) . Und wie auch immer, mit einer aktuellen bashBinärdatei, wie installiert von pacman:

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

Ich habe die erste Zeile hinzugefügt, damit Sie sehen können, dass die Bedingungen der Shell denen einer Skript-Shell entsprechen - sie ist nicht interaktiv. Die Ausgabe ist:

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

Hier sind einige relevante Hinweise aus aktuellen Änderungsprotokollen :

  • Ein Fehler wurde behoben, der dazu führte, dass asynchrone Befehle nicht $?richtig eingestellt wurden.
  • Es wurde ein Fehler behoben, der dazu führte, dass Fehlermeldungen, die durch Erweiterungsfehler in forBefehlen generiert wurden, die falsche Zeilennummer hatten.
  • Ein Fehler, verursacht SIGINT und SIGQUIT nicht als trapPable in asynchroner Subshell - Befehle.
  • Es wurde ein Problem mit der Interrupt-Behandlung behoben, das dazu führte, dass ein zweites und nachfolgendes SIGINT von interaktiven Shells ignoriert wurde.
  • Die Shell blockiert nicht länger den Empfang von Signalen, während trapHandler für diese Signale ausgeführt werden, und ermöglicht, dass die meisten trap Handler rekursiv ausgeführt werden (Ausführen von trapHandlern, während ein trapHandler ausgeführt wird) .

Ich denke, es ist entweder das Letzte oder das Erste, das am relevantesten ist - oder möglicherweise eine Kombination aus beiden. Ein trapHandler ist von Natur aus asynchron, da seine gesamte Aufgabe darin besteht, auf asynchrone Signale zu warten und diese zu verarbeiten . Und Sie lösen zwei gleichzeitig mit -euund aus $UNSET_VAR.

Und vielleicht solltest du es einfach aktualisieren, aber wenn du es magst, machst du es mit einer anderen Shell.

mikeserv
quelle
Vielen Dank für die Erklärung, wie die Parametererweiterung anders gehandhabt wird. Das hat eine Menge Dinge für mich geklärt.
dvdgsng
Ich gewähre dir das Kopfgeld, weil deine Erklärung am hilfreichsten war.
dvdgsng
@dvdgsng - Gracias. Sind Sie aus Neugier jemals auf Ihre Lösung gekommen?
mikeserv
9

(Ich benutze Bash 4.2.53). Für Teil 1 heißt es in der Manpage der Bash nur "In den Standardfehler wird eine Fehlermeldung geschrieben, und eine nicht interaktive Shell wird beendet". Es heißt nicht, dass eine ERR-Falle aufgerufen wird, obwohl ich damit einverstanden bin, dass dies nützlich wäre.

Um pragmatisch zu sein: Wenn Sie wirklich mit undefinierten Variablen klarer umgehen möchten, besteht eine mögliche Lösung darin, den größten Teil Ihres Codes in eine Funktion zu integrieren, diese Funktion in einer Sub-Shell auszuführen und den Rückkehrcode und die stderr-Ausgabe wiederherzustellen. Hier ist ein Beispiel, in dem "cmd ()" die Funktion ist:

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

Auf meine Prügel bekomme ich

./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
meuh
quelle
schön, eine praktische Lösung, die tatsächlich Mehrwert schafft!
Florian Heigl