Dlaczego (wyjście 1) nie wychodzi ze skryptu?

48

Mam skrypt, który nie wychodzi, kiedy chcę.

Przykładowy skrypt z tym samym błędem to:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

Zakładam, że zobaczę wynik:

:~$ ./test.sh
1
:~$

Ale tak naprawdę widzę:

:~$ ./test.sh
1
2
:~$

Czy ()tworzenie łańcuchów poleceń w jakiś sposób tworzy zakres? Z czego exitwychodzi, jeśli nie skrypt?

Minix
źródło
5
Wymaga to odpowiedzi jednym słowem: podpowłoka
Joshua,

Odpowiedzi:

87

()uruchamia polecenia w podpowłoce, więc po exitwyjściu z podpowłoki wracasz do powłoki nadrzędnej. Użyj nawiasów klamrowych, {}jeśli chcesz uruchamiać polecenia w bieżącej powłoce.

Z podręcznika bash:

Lista (lista) jest wykonywana w środowisku podpowłoki. Zmienne przypisania i wbudowane polecenia, które wpływają na środowisko powłoki, nie pozostają aktywne po zakończeniu polecenia. Status powrotu to status wyjścia z listy.

{lista; } lista jest po prostu wykonywana w bieżącym środowisku powłoki. lista musi być zakończona znakiem nowej linii lub średnikiem. Jest to znane jako polecenie grupowe. Status powrotu to status wyjścia z listy. Zauważ, że w przeciwieństwie do metaznaków (i), {i} są słowami zastrzeżonymi i muszą wystąpić, gdy słowo zastrzeżone może zostać rozpoznane. Ponieważ nie powodują podziału słów, muszą być oddzielone od listy spacją lub innym metaznakiem powłoki.

Warto wspomnieć, że składnia powłoki jest dość spójna, a podpowłoka uczestniczy również w innych ()konstrukcjach, takich jak podstawienie poleceń (również ze `..`składnią w starym stylu ) lub podstawienie procesu, więc następujące elementy nie wyjdą z bieżącej powłoki:

echo $(exit)
cat <(exit)

Chociaż może być oczywiste, że podpowłoki są zaangażowane, gdy polecenia są umieszczane jawnie w środku (), mniej widocznym faktem jest to, że są one również spawnowane w tych innych strukturach:

  • polecenie uruchomiono w tle

    exit &

    nie wychodzi z bieżącej powłoki, ponieważ (po man bash)

    Jeśli polecenie zostanie zakończone przez operatora sterującego &, powłoka wykonuje polecenie w tle w podpowłoce. Powłoka nie czeka na zakończenie polecenia, a zwracany status to 0.

  • rurociąg

    exit | echo foo

    nadal wychodzi tylko z podpowłoki.

    Jednak różne powłoki zachowują się inaczej pod tym względem. Na przykład bashumieszcza wszystkie komponenty potoku w osobnych podpowłokach (chyba że użyjesz tej lastpipeopcji w wywołaniach, w których kontrola zadań nie jest włączona), ale AT&T kshi zshuruchomisz ostatnią część w bieżącej powłoce (oba zachowania są dozwolone przez POSIX). A zatem

    exit | exit | exit

    w zasadzie nic nie robi w bashu, ale wychodzi z zsh z powodu ostatniego exit .

  • coproc exitdziała również exitw podpowłoce.

jimmij
źródło
5
Ach Teraz znajdź wszystkie miejsca, w których mój poprzednik użył niewłaściwych aparatów ortodontycznych. Dzięki za wgląd.
Minix,
10
Zwróćmy uwagę na odstępy w manualu: {i }nie są składni, są zarezerwowane słowa i musi być otoczony przestrzeni, a lista musi kończyć się średnikiem (terminator poleceń, nowego wiersza, ampersand)
Glenn Jackman
Czy to nie jest interesujące, czy faktycznie jest to inny proces, czy tylko oddzielne środowisko w wewnętrznym stosie? Dużo używam () do izolowania chdirów i musiałem mieć szczęście, że używam $$ etc, jeśli to pierwsze.
Dan Sheppard,
5
@DanSheppard Jest to inny proces, ale (echo $$)drukuje identyfikator powłoki nadrzędnej, ponieważ $$jest rozszerzany nawet przed utworzeniem podpowłoki. W rzeczywistości drukowanie identyfikatora procesu podpowłoki może być trudne, patrz stackoverflow.com/questions/9119885/…
jimmij
@jimmij, Jak to możliwe, że $$jest rozwijane przed utworzeniem podpowłoki, a mimo to $BASHPIDpokazuje prawidłową wartość podpowłoki?
Wildcard,
13

Wykonanie exitw podpowłoce to jedna pułapka:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

Skrypt wypisuje 42, wychodzi z podpowłoki z kodem powrotu 1i kontynuuje wykonywanie skryptu. Nawet zastąpienie wywołania echo $(CALC) || exit 1nie pomaga, ponieważ kod powrotu echowynosi 0 niezależnie od kodu powrotu calc. I calcjest wykonywany przed echo.

Jeszcze więcej zagadek niweczy efekt exitzawijania go do localwbudowanego, jak w poniższym skrypcie. Natknąłem się na problem, gdy napisałem funkcję do weryfikacji wartości wejściowej. Przykład:

Chcę utworzyć plik o nazwie „rok miesiąc dzień.log”, tj. 20141211.logNa dziś. Data jest wprowadzana przez użytkownika, który może nie podać rozsądnej wartości. Dlatego w mojej funkcji fnamesprawdzam wartość zwracaną w datecelu sprawdzenia poprawności danych wejściowych użytkownika:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"

Wygląda dobrze. Niech skrypt ma nazwę s.sh. Jeśli użytkownik wywoła skrypt za pomocą ./s.sh "Thu Dec 11 20:45:49 CET 2014", plik 20141211.logzostanie utworzony. Jeśli jednak użytkownik wpisze ./s.sh "Thu hec 11 20:45:49 CET 2014", skrypt wyświetli:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

Linia fname…mówi, że w podpowłoce wykryto złe dane wejściowe. Ale exit 1koniec local …linii nigdy się nie uruchamia, ponieważ localdyrektywa zawsze powraca 0. Wynika to z faktu, że localjest wykonywany po, $(fname) a tym samym zastępuje swój kod powrotu. Z tego powodu skrypt jest kontynuowany i wywołuje się touchz pustym parametrem. Ten przykład jest prosty, ale zachowanie bash może być mylące w prawdziwej aplikacji. Wiem, prawdziwi programiści nie używają miejscowych

Aby było to jasne: bez localskryptu zostanie przerwany zgodnie z oczekiwaniami po wprowadzeniu niepoprawnej daty.

Rozwiązaniem jest podzielenie linii jak

local FNAME
FNAME=$(fname "$1") || exit 1

Dziwne zachowanie jest zgodne z dokumentacją strony localpodręcznika bash: „Zwracany status to 0, chyba że lokalny jest używany poza funkcją, podana jest niepoprawna nazwa lub nazwa jest zmienną tylko do odczytu”.

Chociaż nie jestem błędem, uważam, że zachowanie bash jest sprzeczne z intuicją. Zdaję sobie sprawę z sekwencji wykonania, localnie powinien jednak maskować zepsutego zadania.

Moja wstępna odpowiedź zawierała pewne niedokładności. Po odkrywczej i dogłębnej dyskusji z mikeserv (dziękuję za to) postanowiłem je naprawić.

hermannk
źródło
@mikeserv: dodałem przykład pokazujący trafność.
hermannk
@mikeserv: Tak, masz rację. Jeszcze terser. Ale pułapka wciąż tam jest.
hermannk
@mikeserv: Przepraszamy, mój przykład został zepsuty. Zapomniałem testu doit().
hermannk
2

Rzeczywiste rozwiązanie:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

Grupowanie błędów zostanie wykonane tylko wtedy, gdy blazwróci status błędu, i exitnie znajduje się w podpowłoce, więc cały skrypt zatrzymuje się.

Walf
źródło
1

Nawiasy otwierają podpowłokę, a wyjście wychodzi tylko z tej podpowłoki.

Możesz odczytać kod wyjścia za pomocą $?i dodać go do skryptu, aby wyjść ze skryptu, jeśli opuścisz podpowłokę:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'
rubo77
źródło