Dlaczego set -e nie działa w podpowłokach z nawiasami (), po których następuje lista LUB ||?

30

Ostatnio natknąłem się na takie skrypty:

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

Dla mnie to wygląda dobrze, ale to nie działa! set -eNie stosuje się, gdy dodasz ||. Bez tego działa dobrze:

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

Jednak jeśli dodam ||, to set -ejest ignorowane:

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

Użycie prawdziwej, osobnej powłoki działa zgodnie z oczekiwaniami:

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

Próbowałem tego w wielu różnych powłokach (bash, dash, ksh93) i wszystkie zachowują się w ten sam sposób, więc nie jest to błąd. Czy ktoś może to wyjaśnić?

Szalony naukowiec
źródło
Konstrukcja `(....)` `uruchamia osobną powłokę do uruchomienia jej zawartości, żadne ustawienia w niej nie obowiązują na zewnątrz.
vonbrand
@vonbrand, nie trafiłeś w sedno. Chce, aby zastosował się w podpowłoce, ale ||zewnętrzna podpowłoka wpływa na zachowanie w podpowłoce.
cjm
1
Porównaj (set -e; echo 1; false; echo 2)z(set -e; echo 1; false; echo 2) || echo 3
Johan

Odpowiedzi:

32

Zgodnie z tym wątkiem jest to zachowanie, które POSIX określa dla używania „ set -e” w podpowłoce.

(Również byłem zaskoczony.)

Po pierwsze zachowanie:

-eUstawienie powinno być ignorowane podczas wykonywania listę związek po chwili dopiero, jeśli, lub Elif zarezerwowanym słowem, rurociąg począwszy od! słowo zastrzeżone lub dowolne polecenie z listy AND-OR inne niż ostatnie.

Drugi post zauważa,

Podsumowując, czy ustawienie -e w (kod podpowłoki) nie powinno działać niezależnie od otaczającego kontekstu?

Nie. Opis POSIX jest jasny, że otaczający kontekst wpływa na to, czy zestaw -e jest ignorowany w podpowłoce.

W czwartym poście jest trochę więcej, również autorstwa Erica Blake'a,

Punkt 3 nie wymaga, aby podpowłoki zastępowały konteksty, w których set -ejest ignorowane. Oznacza to, że gdy znajdziesz się w kontekście, w którym -ejest się ignorowanym, nie możesz nic zrobić, aby być -eponownie posłusznym, nawet podpowłoki.

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

Mimo że set -edwukrotnie zadzwoniliśmy (zarówno w rodzicu, jak i podpowłoce), fakt, że podpowłoka istnieje w kontekście, w którym -ejest ignorowana (warunek instrukcji if), nic nie możemy zrobić w podpowłoce, aby ponownie włączyć -e.

To zachowanie jest zdecydowanie zaskakujące. Jest to sprzeczne z intuicją: można oczekiwać, że ponowne włączenie set -ebędzie miało skutek, a otaczający kontekst nie będzie miał precedensu; ponadto sformułowanie standardu POSIX nie wyjaśnia tego szczególnie wyraźnie. Jeśli czytasz to w kontekście, w którym polecenie się nie udaje, reguła nie ma zastosowania: ma zastosowanie tylko w otaczającym kontekście, jednak odnosi się do niej całkowicie.

Aaron D. Marasco
źródło
Dzięki za te linki były bardzo interesujące. Jednak mój przykład jest (IMO) zasadniczo inny. Większość z tej dyskusji jest to, czy zestaw -e w powłoce macierzystej jest dziedziczona przez podpowłoce: set -e; (false; echo passed;) || echo failed. Nie dziwi mnie, że -e jest ignorowane w tym przypadku, biorąc pod uwagę sformułowanie standardu. W moim przypadku jednak jawnie ustawiam -e w podpowłoce i oczekuję , że podpowłoka zakończy działanie po awarii. W podpowłoce nie ma listy AND-OR ...
MadScientist
Nie zgadzam się. Drugi post (nie mogę uruchomić kotwic) mówi: „ Opis POSIX jest jasny, że otaczający kontekst wpływa na to, czy zestaw -e jest ignorowany w podpowłoce. ” - podpowłoka znajduje się na liście AND-OR.
Aaron D. Marasco
Czwarty post (także Erik Blake) mówi również: „ Mimo że dwukrotnie wywołaliśmy zestaw -e (zarówno w rodzica, jak i w podpowłoce), fakt, że podpowłoka istnieje w kontekście, w którym -e jest ignorowany (warunek if oświadczenie), nic nie możemy zrobić w podpowłoce, aby ponownie włączyć -e.
Aaron D. Marasco
Masz rację; Nie jestem pewien, jak je źle odczytałem. Dzięki.
MadScientist
1
Z przyjemnością dowiaduję się, że to zachowanie, które rozdzieram, okazuje się być zgodne ze specyfikacją POSIX. Więc o co chodzi ?! ifi ||i &&są zaraźliwe? to absurdalne
Steven Lu
7

Rzeczywiście, set -enie ma żadnego efektu w podpowłokach, jeśli użyjesz ||operatora po nich; np. to nie zadziała:

#!/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 w swojej odpowiedzi świetnie wyjaśnia, dlaczego tak się zachowuje.

Oto mała sztuczka, której można użyć, aby to naprawić: uruchom wewnętrzne polecenie w tle, a następnie natychmiast poczekaj na to. waitWbudowane zwróci kod wyjścia polecenia wewnętrznej, a teraz używasz ||po wait, a nie funkcji wewnętrznej, więc set -edziała prawidłowo wewnątrz tego ostatniego:

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

Oto ogólna funkcja oparta na tym pomyśle. Powinien on działać we wszystkich powłokach zgodnych z POSIX, jeśli usuniesz localsłowa kluczowe, tzn. Zastąpisz local x=yje tylko 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
}

Przykład użycia:

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

Uruchamianie przykładu:

$ ./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

Jedyną rzeczą, o której musisz wiedzieć podczas korzystania z tej metody jest to, że wszystkie modyfikacje zmiennych Shell wykonane z komendy, którą przekazujesz run, nie będą propagowane do funkcji wywołującej, ponieważ komenda działa w podpowłoce.

skozin
źródło
2

Nie wykluczyłbym, że to błąd tylko dlatego, że kilka pocisków zachowuje się w ten sposób. ;-)

Mam więcej zabawy do zaoferowania:

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

Czy mogę zacytować fragment man bash (4.2.24):

Powłoka nie kończy działania, jeśli polecenie, które się nie powiedzie, jest [...] częścią dowolnego polecenia wykonanego w && lub || lista oprócz polecenia następującego po końcowym && lub || [...]

Być może ewaluacja kilku poleceń prowadzi do ignorowania || kontekst.

Hauke ​​Laging
źródło
Cóż, jeśli wszystkie powłoki zachowują się w ten sposób, to z definicji nie jest to błąd ... to standardowe zachowanie :-). Możemy ubolewać nad zachowaniem jako nieintuicyjnym, ale ... Sztuczka z eval jest bardzo interesująca, to na pewno.
MadScientist
Jakiej powłoki używasz? evalSztuczka nie działa dla mnie. Próbowałem bash, bash w trybie posix i dash.
Dunatotatos
@Dunatotatos, jak powiedział Hauke, to było bash4.2. Zostało to „naprawione” w bash4.3. Powłoki oparte na pdksh będą miały ten sam „problem”. I kilka wersji kilku powłok ma wiele różnych „problemów” z set -e. set -ejest zepsuty przez projekt. Nie użyłbym tego do niczego poza najprostszymi skryptami powłoki bez struktur kontrolnych, podpowłok lub podstawień poleceń.
Stéphane Chazelas
1

Obejście problemu przy używaniu najwyższego poziomu set -e

Doszedłem do tego pytania, ponieważ używałem set -ejako metody wykrywania błędów:

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

i bez ||tego skrypt przestanie działać i nigdy nie osiągnie do_more_stuff.

Ponieważ wydaje się, że nie ma czystego rozwiązania, myślę, że po prostu zrobię proste set +ena moich skryptach:

/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
źródło