Warum funktioniert set -e nicht in Subshells mit Klammern () gefolgt von einer ODER-Liste ||?

30

Ich habe in letzter Zeit einige Skripte wie diese gefunden:

( set -e ; do-stuff; do-more-stuff; ) || echo failed

Das sieht für mich gut aus, aber es funktioniert nicht! Das set -egilt nicht, wenn Sie das hinzufügen ||. Ohne das funktioniert es gut:

$ ( set -e; false; echo passed; ); echo $?
1

Wenn ich jedoch das hinzufüge ||, set -ewird das ignoriert:

$ ( set -e; false; echo passed; ) || echo failed
passed

Die Verwendung einer echten, separaten Shell funktioniert wie erwartet:

$ sh -c 'set -e; false; echo passed;' || echo failed
failed

Ich habe dies in mehreren verschiedenen Shells versucht (bash, dash, ksh93) und alle verhalten sich gleich, es ist also kein Fehler. Kann das jemand erklären?

Verrückter Wissenschaftler
quelle
Das `(....)` `-Konstrukt startet eine separate Shell, um deren Inhalt auszuführen. Alle Einstellungen in ihr gelten nicht außerhalb.
Vonbrand
@vonbrand, du hast den Punkt verpasst. Er möchte, dass es innerhalb der Subshell angewendet wird, aber die ||Außenseite der Subshell beeinflusst das Verhalten innerhalb der Subshell.
cjm
1
Vergleichen Sie (set -e; echo 1; false; echo 2)mit(set -e; echo 1; false; echo 2) || echo 3
Johan

Antworten:

32

Nach diesem Thread , dann ist es das Verhalten POSIX legt fest , für die Verwendung von „ set -e“ in einer Subshell.

(Ich war auch überrascht.)

Erstens das Verhalten:

Die -eEinstellung wird ignoriert, wenn die zusammengesetzte Liste nach while ausgeführt wird, bis oder falls elif ein reserviertes Wort ist, eine Pipeline, die mit! Beginnt. reserviertes Wort oder ein beliebiger Befehl einer AND-OR-Liste außer dem letzten.

Der zweite Beitrag stellt fest,

Zusammenfassend gesagt, sollte set -e in (Subshell-Code) nicht unabhängig vom umgebenden Kontext funktionieren?

Nein. Der POSIX-Beschreibung ist klar, dass der umgebende Kontext Einfluss darauf hat, ob set -e in einer Subshell ignoriert wird.

Es gibt ein wenig mehr im vierten Beitrag, auch von Eric Blake,

Punkt 3 erfordert keine Unterschalen, um die Kontexte zu überschreiben, in denen set -eignoriert wird. Das heißt, wenn Sie sich in einem Kontext befinden, in dem Sie -eignoriert werden, können Sie nichts tun, um -eerneut gehorcht zu werden, nicht einmal eine Unterschale.

$ bash -c 'set -e; if (set -e; false; echo hi); then :; fi; echo $?' 
hi 
0 

Auch wenn wir set -ezweimal aufgerufen haben (sowohl in der übergeordneten als auch in der Subshell), dass die Subshell in einem Kontext vorhanden -eist, in dem sie ignoriert wird (die Bedingung einer if-Anweisung), können wir in der Subshell nichts tun, um sie wieder zu aktivieren -e.

Dieses Verhalten ist auf jeden Fall überraschend. Es ist kontraintuitiv: Man würde erwarten, dass die erneute Aktivierung von set -eeine Wirkung hat und dass der umgebende Kontext keinen Präzedenzfall darstellt; Darüber hinaus macht der Wortlaut des POSIX-Standards dies nicht besonders deutlich. Wenn Sie es in dem Kontext lesen, in dem der Befehl fehlschlägt, gilt die Regel nicht: Es gilt nur im umgebenden Kontext, es gilt jedoch vollständig für ihn.

Aaron D. Marasco
quelle
Vielen Dank für diese Links, sie waren sehr interessant. Mein Beispiel (IMO) ist jedoch wesentlich anders. Die meisten dieser Diskussion ist , ob Satz -e in einer Eltern - Shell durch die Subshell vererbt wird: set -e; (false; echo passed;) || echo failed. Es überrascht mich eigentlich nicht, dass -e in diesem Fall angesichts des Wortlauts der Norm ignoriert wird. In meinem Fall setze ich jedoch explizit -e in die Subshell und erwarte, dass die Subshell bei einem Fehler beendet wird. Es gibt keine UND-ODER-Liste in der Subshell ...
MadScientist
Ich stimme dir nicht zu. Der zweite Beitrag (ich kann die Anker nicht zum Laufen bringen ) besagt: " Die POSIX-Beschreibung ist klar, dass der umgebende Kontext Einfluss darauf hat, ob set -e in einer Subshell ignoriert wird. " - Die Subshell befindet sich in der UND-ODER-Liste.
Aaron D. Marasco
Der vierte Beitrag (auch Erik Blake) sagt auch: " Auch wenn wir set -e zweimal aufgerufen haben (sowohl in der übergeordneten als auch in der Subshell), existiert die Subshell in einem Kontext, in dem -e ignoriert wird (die Bedingung eines if Aussage), gibt es nichts, was wir in der Subshell tun können, um -e wieder zu aktivieren. "
Aaron D. Marasco
Du hast recht; Ich bin mir nicht sicher, wie ich diese falsch verstanden habe. Vielen Dank.
MadScientist
1
Es freut mich zu erfahren, dass dieses Verhalten, über das ich mir die Haare zerreiße, POSIX-konform ist. Worum geht es also ?! ifund ||und &&sind ansteckend? Das ist absurd
Steven Lu
7

set -eHat in der Tat keine Auswirkung in Subshells, wenn Sie den ||Operator danach verwenden. zB würde das nicht funktionieren:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Aaron D. Marasco erklärt in seiner Antwort hervorragend, warum es sich so verhält.

Hier ist ein kleiner Trick, mit dem dies behoben werden kann: Führen Sie den inneren Befehl im Hintergrund aus und warten Sie sofort darauf. Der waiteingebaute Code gibt den Exit-Code des inneren Befehls zurück. Jetzt verwenden Sie ||after wait, nicht die innere Funktion, und set -efunktioniert in letzterer ordnungsgemäß:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Hier ist die generische Funktion, die auf dieser Idee aufbaut. Es sollte in allen POSIX-kompatiblen Shells funktionieren, wenn Sie localSchlüsselwörter entfernen , dh alle local x=ydurch Folgendes ersetzen x=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

Anwendungsbeispiel:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

Beispiel ausführen:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

Das einzige, was Sie beachten müssen, wenn Sie diese Methode verwenden, ist, dass alle Änderungen an Shell-Variablen, die Sie mit dem übergebenen Befehl vornehmen, runnicht an die aufrufende Funktion weitergegeben werden, da der Befehl in einer Subshell ausgeführt wird.

Skozin
quelle
2

Ich würde nicht ausschließen, dass es ein Fehler ist, nur weil sich mehrere Shells so verhalten. ;-)

Ich habe mehr Spaß zu bieten:

start cmd:> ( eval 'set -e'; false; echo passed; ) || echo failed
passed

start cmd:> ( eval 'set -e; false'; echo passed; ) || echo failed
failed

start cmd:> ( eval 'set -e; false; echo passed;' ) || echo failed
failed

Darf ich aus Man Bash zitieren (4.2.24):

Die Shell wird nicht beendet, wenn der fehlgeschlagene [...] Befehl Teil eines Befehls ist, der in einem && oder || ausgeführt wird Liste mit Ausnahme des Befehls nach dem letzten && oder || [...]

Vielleicht führt die Auswertung mehrerer Befehle dazu, dass das || ignoriert wird Kontext.

Hauke ​​Laging
quelle
Nun, wenn sich alle Shells so verhalten, ist das per Definition kein Fehler ... es ist das Standardverhalten :-). Wir mögen das Verhalten als nicht intuitiv beklagen, aber ... Der Trick mit eval ist sehr interessant, das ist sicher.
MadScientist
Welche Shell benutzt du? Der evalTrick funktioniert bei mir nicht. Ich habe versucht, Bash, Bash im Posix-Modus und Dash.
Dunatotatos
@ Dunatotatos, wie Hauke ​​sagte, das war bash4.2. Es wurde in bash4.3 "behoben". pdksh-basierte Shells haben das gleiche "Problem". Und mehrere Versionen von mehreren Shells haben alle möglichen "Probleme" set -e. set -eist absichtlich kaputt. Ich würde es nur für das einfachste Shellskript ohne Kontrollstrukturen, Subshells oder Befehlsersetzungen verwenden.
Stéphane Chazelas
1

Problemumgehung, wenn Sie die oberste Ebene verwenden set -e

Ich bin auf diese Frage gekommen, weil ich set -eals Fehlererkennungsmethode Folgendes verwendet habe:

/usr/bin/env bash
set -e
do_stuff
( take_best_sub_action_1; take_best_sub_action_2 ) || do_worse_fallback
do_more_stuff

und ohne ||würde das Skript aufhören zu laufen und niemals erreichen do_more_stuff.

Da es keine saubere Lösung zu geben scheint, denke ich, dass ich set +emeine Skripte einfach bearbeiten werde:

/usr/bin/env bash
set -e
do_stuff
set +e
( take_best_sub_action_1; take_best_sub_action_2 )
exit_status=$?
set -e
if [ "$exit_status" -ne 0 ]; then
  do_worse_fallback
fi
do_more_stuff
Ciro Santilli ist ein Schauspieler
quelle