Prawidłowe zachowanie pułapek EXIT i ERR przy użyciu `set -eu`

27

Obserwuję dziwne zachowanie podczas używania set -e( errexit), set -u( nounset) wraz z pułapkami ERR i EXIT. Wydają się powiązane, więc postawienie ich w jednym pytaniu wydaje się rozsądne.

1) set -unie uruchamia pułapek ERR

  • Kod:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
    
  • Oczekiwany: pułapka ERR zostaje wywołana, RC! = 0
  • Faktycznie: pułapka ERR nie jest wywoływana, RC == 1
  • Uwaga: set -enie zmienia wyniku

2) Użycie set -eukodu wyjścia w pułapce WYJŚCIA wynosi 0 zamiast 1

  • Kod:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
    
  • Oczekiwany: wywoływana jest pułapka WYJŚCIA, RC == 1
  • Faktycznie: pułapka EXIT jest wywoływana, RC == 0
  • Uwaga: Podczas używania set +eRC == 1. Pułapka WYJŚCIA zwraca prawidłowe RC, gdy jakiekolwiek inne polecenie zgłosi błąd.
  • Edycja: Istnieje post SO na ten temat z interesującym komentarzem sugerującym, że może to być związane z używaną wersją Bash. Testowanie tego fragmentu w Bash 4.3.11 daje RC = 1, więc to lepiej. Niestety aktualizacja Bash (z wersji 3.2.51) na wszystkich hostach nie jest w tej chwili możliwa, dlatego musimy wymyślić inne rozwiązanie.

Czy ktoś może wyjaśnić którekolwiek z tych zachowań?

Przeszukiwanie tych tematów nie było bardzo udane, co jest dość zaskakujące, biorąc pod uwagę liczbę postów dotyczących ustawień i pułapek Basha. Jest jednak jeden wątek na forum , ale wniosek jest raczej niezadowalający.

dvdgsng
źródło
3
Od 4 wydaje mi się, że bashzerwał ze standardem i zaczął umieszczać pułapki w podpowłokach. Pułapka ma być wykonana w tym samym środowisku, z którego nastąpił powrót, ale bashnie zrobiła tego od dłuższego czasu.
mikeserv
1
Poczekaj chwilę - chcesz rozwiązanie lub wyjaśnienie? A jeśli chcesz rozwiązania, to co dokładnie? Co chcesz wystąpić? set -ei set -uoba są zaprojektowane specjalnie do zabijania skryptów powłoki. Używanie ich w warunkach, które mogą uruchomić ich aplikację, zabije skryptowaną powłokę. Nie ma poruszanie się, że z wyjątkiem do nie korzystania z nich, a zamiast tego testu dla tych warunkach, gdy mają one zastosowanie w sekwencji kodu. Zasadniczo możesz napisać dobry kod powłoki lub użyć set -eu.
mikeserv
2
Właściwie szukam obu, ponieważ nie mogłem znaleźć wystarczających informacji o tym, dlaczego -unie wyzwalać pułapki ERR (jest to błąd, więc nie powinien wyzwalać pułapki) lub kod błędu to 0 zamiast 1. ten drugi wydaje się być błędem, który został już naprawiony w późniejszej wersji, więc to tyle. Ale pierwsza część jest dość trudna do zrozumienia, jeśli nie zdajesz sobie sprawy, że błędy w ocenie powłoki (rozszerzenie parametrów) i rzeczywiste błędy w poleceniach wydają się być dwiema różnymi rzeczami. Jeśli chodzi o rozwiązanie, cóż, jak zasugerowałeś, staram się teraz unikać -eui sprawdzać ręcznie, kiedy jest to konieczne.
dvdgsng
1
@dvdsng - Dobrze. To jest właściwa droga - powinieneś opublikować swój skrypt, gdy robisz to jako odpowiedź i nagradzać się nagrodą. Naprawdę nie lubię tych opcji - nie pozwalają one na obsługę wyjątków w bezpieczny sposób.
mikeserv
1
@dvdsng - gdzie każda z tych opcji może być przydatna, znajduje się w kontekście podpowłoki. Można więc sobie wyobrazić, że cokolwiek wcześniej ich używałeś, mogło być zlokalizowane w kontekście podpowłoki, takim jak: (set -u; : $UNSET_VAR)i podobne. Tego rodzaju rzeczy też mogą być dobre - od &&czasu do czasu możesz zrzucić wiele rzeczy : (set -e; mkdir dir; cd dir; touch dirfile)jeśli dostaniesz mój dryf. Po prostu są to kontrolowane konteksty - kiedy ustawisz je jako opcje globalne, tracisz kontrolę i stajesz się kontrolowany. Zazwyczaj są jednak bardziej wydajne rozwiązania.
mikeserv

Odpowiedzi:

15

Od man bash:

  • set -u
    • Treat zmienne wyłączony i parametry inne niż specjalnymi parametrami "@"i "*"jako błąd podczas wykonywania ekspansji parametr. Jeśli nastąpi próba rozwinięcia zmiennej lub parametru nieuzbrojonego, powłoka wypisuje komunikat o błędzie i, jeśli nie jest -inieaktywna, kończy działanie ze statusem niezerowym.

POSIX stwierdza, że ​​w przypadku błędu rozszerzenia , nieinteraktywna powłoka powinna wyjść, gdy rozszerzenie jest powiązane albo ze specjalnym wbudowanym powłoką (które to rozróżnienie bashi tak regularnie ignoruje, a więc być może jest nieistotne) lub jakąkolwiek inną użytecznością poza .

  • Konsekwencje błędów powłoki :
    • Błąd rozszerzenie to taki, który występuje, gdy rozszerzenia powłoki zdefiniowane w Worda rozszerzeniami są przeprowadzane (na przykład "${x!y}", ponieważ !nie jest ważne Operator) ; implementacja może traktować je jako błędy składniowe, jeśli jest w stanie je wykryć podczas tokenizacji, a nie podczas ekspansji.
    • [A] n powłoka interaktywna powinna napisać komunikat diagnostyczny do standardowego błędu bez wychodzenia.

Również z man bash:

  • trap ... ERR
    • Jeśli sigspec to ERR , polecenie arg jest wykonywane za każdym razem, gdy potok (który może składać się z pojedynczego prostego polecenia) , listy lub polecenia złożonego zwraca niezerowy status wyjścia, z zastrzeżeniem następujących warunków:
      • ERR pułapka nie jest wykonywana, gdy nie powiodła się komenda jest częścią listy poleceń natychmiast po whilelub untilhasła ...
      • ... część testu w ifoświadczeniu ...
      • ... część polecenia wykonanego na liście &&lub ||z wyjątkiem polecenia następującego po ostatnim &&lub ||...
      • ... dowolne polecenie w przygotowaniu, ale ostatnie ...
      • ... lub jeśli wartość zwracana polecenia jest odwracana za pomocą !.
    • Są to te same warunki, których przestrzega opcja errexit -e .

Zauważ, że pułapka ERR polega na ocenie powrotu innego polecenia. Ale gdy wystąpi błąd rozszerzenia , nie jest uruchamiane polecenie, aby cokolwiek zwrócić. W twoim przykładzie echo nigdy się nie zdarza - ponieważ gdy powłoka ocenia i rozwija argumenty, napotyka -uzmienną nset, która została określona przez jawną opcję powłoki, aby spowodować natychmiastowe wyjście z bieżącej, skryptowanej powłoki.

I tak, pułapka WYJŚCIA , jeśli istnieje, jest wykonywana, a powłoka wychodzi z komunikatem diagnostycznym i kończy stan inny niż 0 - dokładnie tak, jak powinna.

Co do rc: 0 , to spodziewam się, że jest to jakiś błąd specyficzny dla wersji - prawdopodobnie związany z dwoma wyzwalaczami dla EXIT występującymi w tym samym czasie i tym, który otrzymuje kod wyjścia drugiego (który nie powinien wystąpić) . W każdym razie z aktualnym bashplikiem binarnym zainstalowanym przez pacman:

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

Dodałem pierwszy wiersz, abyś mógł zobaczyć, że warunki powłoki są takie same jak dla powłoki skryptowej - nie jest ona interaktywna. Dane wyjściowe to:

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

Oto kilka istotnych uwag z ostatnich dzienników zmian :

  • Naprawiono błąd, który powodował $?nieprawidłowe ustawianie poleceń asynchronicznych .
  • Naprawiono błąd, który powodował, że komunikaty o błędach generowane przez błędy rozszerzenia w forkomendach miały niewłaściwy numer linii.
  • Naprawiono błąd, który powodował, że SIGINT i SIGQUIT nie mogły być trapłączone w asynchronicznych poleceniach podpowłoki.
  • Naprawiono problem z obsługą przerwań, który powodował ignorowanie drugiego i kolejnego SIGINT przez interaktywne powłoki.
  • Skorupa nie blokuje odbiór sygnałów podczas pracy trapładowarki dla tych sygnałów i pozwala większość trap koparki do uruchomienia rekurencyjnie (bieganie trapkoparki podczas trapobsługi jest wykonywany) .

Myślę, że jest to ostatni lub pierwszy, który jest najbardziej odpowiedni - lub być może kombinacja tych dwóch. Program trapobsługi jest z natury asynchroniczny, ponieważ jego zadaniem jest oczekiwanie i obsługa sygnałów asynchronicznych . I uruchamiasz dwa jednocześnie za pomocą -eui $UNSET_VAR.

Może więc powinieneś po prostu zaktualizować, ale jeśli ci się spodoba, zrobisz to z inną powłoką.

mikeserv
źródło
Dziękujemy za wyjaśnienie, w jaki sposób rozszerzenie parametrów jest obsługiwane w różny sposób. To wyjaśniło mi wiele rzeczy.
dvdgsng
Przyznam ci nagrodę, ponieważ twoje wyjaśnienie było najbardziej pomocne.
dvdgsng,
@dvdgsng - Gracias. Czy z ciekawości wpadłeś kiedyś na swoje rozwiązanie?
mikeserv
9

(Używam bash 4.2.53). W części pierwszej strona podręcznika użytkownika bash mówi tylko: „Błąd standardowy zostanie zapisany do standardowego błędu, a nieinteraktywna powłoka zostanie zamknięta”. Nie oznacza to, że zostanie wywołana pułapka ERR, choć zgadzam się, że byłoby użyteczne, gdyby tak się stało.

Aby być pragmatycznym, jeśli tak naprawdę chcesz bardziej czysto radzić sobie z niezdefiniowanymi zmiennymi, możliwym rozwiązaniem jest umieszczenie większości kodu w funkcji, a następnie wykonanie tej funkcji w podpowłoce i odzyskanie kodu powrotu i wyniku stderr. Oto przykład, w którym „cmd ()” jest funkcją:

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

Po mojej uderzeniu dostaję

./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
źródło
fajne, praktyczne rozwiązanie, które w rzeczywistości dodaje wartość!
Florian Heigl