Jak w skrypcie Bash mogę wyjść z całego skryptu, jeśli wystąpi jakiś warunek?

717

Piszę skrypt w Bash, aby przetestować kod. Jednak głupio wydaje się uruchamianie testów, jeśli kompilacja kodu nie powiedzie się, w takim przypadku po prostu przerywam testy.

Czy jest jakiś sposób, aby to zrobić bez owijania całego skryptu w pętli while i używania przerw? Coś jak dun dun goto?

samoz
źródło

Odpowiedzi:

808

Wypróbuj to oświadczenie:

exit 1

Zamień na 1odpowiednie kody błędów. Zobacz także Kody wyjścia ze specjalnymi znaczeniami .

Michael Foukarakis
źródło
4
@CMCDragonkai, zwykle działa dowolny niezerowy kod. Jeśli nie potrzebujesz niczego specjalnego, możesz po prostu używać 1konsekwentnie. Jeśli skrypt ma być uruchamiany przez inny skrypt, możesz zdefiniować własny zestaw kodów statusu o szczególnym znaczeniu. Na przykład 1== testy nie powiodły się, 2== kompilacja nie powiodła się. Jeśli skrypt jest częścią czegoś innego, konieczne może być dostosowanie kodów w celu dostosowania do stosowanych tam praktyk. Na przykład, gdy część zestawu testów jest uruchamiana przez automake, kod 77służy do oznaczenia pominięcia testu.
Michał Górny
21
nie, to również zamyka okno, nie tylko wychodzi ze skryptu
Toni Leigh
7
@ToniLeigh bash nie ma pojęcia „okna”, prawdopodobnie jesteś zdezorientowany co do tego, co robi twoja konfiguracja - np. Emulator terminala.
Michael Foukarakis
3
@ToniLeigh Jeśli zamyka „okno”, prawdopodobnie umieszczasz exit #polecenie w funkcji, a nie w skrypcie. (W takim przypadku użyj return #zamiast tego.)
Jamie
1
@Sevenearths 0 oznacza, że ​​się powiódł, dlatego exit 0wyjdź ze skryptu i zwróci 0 (informując inne skrypty, które mogłyby wykorzystać wynik tego skryptu, to się udało)
VictorGalisson
689

Użyj zestawu -e

#!/bin/bash

set -e

/bin/command-that-fails
/bin/command-that-fails2

Skrypt zakończy się po pierwszej linii, która się nie powiedzie (zwraca niezerowy kod wyjścia). W takim przypadku polecenie, które zawiedzie 2 , nie zostanie uruchomione.

Jeśli chcesz sprawdzić status zwrotu każdej komendy, skrypt wyglądałby następująco:

#!/bin/bash

# I'm assuming you're using make

cd /project-dir
make
if [[ $? -ne 0 ]] ; then
    exit 1
fi

cd /project-dir2
make
if [[ $? -ne 0 ]] ; then
    exit 1
fi

Z ustawieniem -e wyglądałoby to tak:

#!/bin/bash

set -e

cd /project-dir
make

cd /project-dir2
make

Każde polecenie, które się nie powiedzie, spowoduje awarię całego skryptu i zwróci status wyjścia, który można sprawdzić za pomocą $? . Jeśli twój skrypt jest bardzo długi lub tworzysz wiele rzeczy, stanie się dość brzydki, jeśli dodasz wszędzie sprawdzanie statusu zwrotu.

Shizzmo
źródło
10
Z set -ewciąż może zrobić jakieś polecenia zjazdu z błędami bez zatrzymywania skrypt: command 2>&1 || echo $?.
Adobe,
6
set -eprzerwie skrypt, jeśli potok lub struktura poleceń zwróci wartość niezerową. Na przykład foo || barzakończy się niepowodzeniem tylko wtedy, gdy oba fooi barzwrócą wartość niezerową. Zwykle dobrze napisany skrypt bash będzie działał, jeśli dodasz set -ena początku, a dodatek działa jak automatyczna kontrola poprawności: przerwij skrypt, jeśli coś pójdzie nie tak.
Mikko Rantalainen,
6
Jeśli łączysz polecenia razem, możesz również zawieść, jeśli którykolwiek z nich zawiedzie, ustawiając tę set -o pipefailopcję.
Jake Biesinger
18
W rzeczywistości kod idiomatyczny bez set -ebyłby sprawiedliwy make || exit $?.
tripleee
4
Ty też masz set -u. Spójrz na nieoficjalnej bash trybie ścisłym : set -euo pipefail.
Pablo A
235

Pewien facet z SysOps nauczył mnie techniki trójpalczastego pazura:

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }

Te funkcje to * NIX OS i odporny na smak powłoki. Umieść je na początku skryptu (bash lub w inny sposób), try()dołącz swoje oświadczenie i kod.

Wyjaśnienie

(na podstawie komentarza latających owiec ).

  • yell: wypisz nazwę skryptu i wszystkie argumenty do stderr:
    • $0 jest ścieżką do skryptu;
    • $* wszystkie są argumentami.
    • >&2oznacza >przekierowanie standardowego wejścia do & pipe2 . fajka1 byłaby stdoutsama.
  • dierobi to samo co yell, ale wychodzi ze statusem wyjścia innym niż 0 , co oznacza „niepowodzenie”.
  • tryużywa ||(boolean OR), który ocenia prawą stronę tylko w przypadku niepowodzenia lewej.
    • $@to znowu wszystkie argumenty, ale inne .
c.gutierrez
źródło
3
Jestem całkiem nowy w skryptowaniu unixowym. Czy możesz wyjaśnić, w jaki sposób powyższe funkcje są wykonywane? Widzę nową składnię, której nie znam. Dzięki.
kaizenCoder
15
yell : $0jest ścieżką do skryptu. $*wszystkie są argumentami. >&2oznacza „ >przekieruj standardowe wyjście na &potok 2”. rura 1 byłaby sama stdout. więc krzyczy przedrostek wszystkich argumentów nazwą skryptu i wypisuje na stderr. die robi to samo co krzyk , ale wychodzi ze statusem wyjścia innym niż 0, co oznacza „niepowodzenie”. try używa wartości logicznej lub ||, która ocenia prawą stronę tylko wtedy, gdy lewa nie zawiodła. $@to znowu wszystkie argumenty, ale inne . mam nadzieję, że to wszystko wyjaśnia
latające owce
1
Zmodyfikowałem to, aby die() { yell "$1"; exit $2; }można było przekazać wiadomość i kod zakończenia za pomocą die "divide by zero" 115.
Mark Lakata
3
Widzę, jak bym użył yelli die. Jednak trynie tak bardzo. Czy możesz podać przykład swojego użycia?
kshenoy
2
Hmm, ale jak ich używać w skrypcie? Nie rozumiem, co masz na myśli przez „wypróbuj () swoją instrukcję i kod”.
TheJavaGuy-Ivan Milosavljević
33

Jeśli wywołasz skrypt za pomocą source, możesz użyć return <x>gdzie <x>będzie status wyjścia skryptu (użyj wartości niezerowej dla błędu lub fałszu). Ale jeśli wywołasz skrypt wykonywalny (tj. Bezpośrednio z jego nazwą pliku), instrukcja return spowoduje skargę (komunikat o błędzie „return: może tylko„ wrócić ”z funkcji lub skryptu źródłowego).

Jeśli exit <x>zostanie użyte zamiast tego, po wywołaniu skryptu sourcenastąpi wyjście z powłoki, która go uruchomiła, ale skrypt wykonywalny po prostu zakończy się, zgodnie z oczekiwaniami.

Aby obsłużyć obie sprawy w tym samym skrypcie, możesz użyć

return <x> 2> /dev/null || exit <x>

To obsłuży dowolne wywołanie. Zakłada się, że użyjesz tej instrukcji na najwyższym poziomie skryptu. Odradzałbym bezpośrednie wychodzenie ze skryptu z funkcji.

Uwaga: <x>ma być tylko liczbą.

kavadias
źródło
Nie działa dla mnie w skrypcie z funkcją return / exit wewnątrz funkcji, tzn. Czy możliwe jest wyjście z funkcji bez istniejącej powłoki, ale bez sprawienia, by funkcja wywołująca dbała o prawidłowe sprawdzenie kodu powrotu funkcji ?
sty
@jan To, co wywołujący robi (lub nie robi) z wartościami zwracanymi, jest całkowicie ortogonalne (tj. niezależne od), w jaki sposób powracasz z funkcji (... bez wychodzenia z powłoki, bez względu na wywołanie). Zależy to głównie od kodu dzwoniącego, który nie jest częścią tego pytania i odpowiedzi. Można nawet dostosować wartość zwracaną funkcji do potrzeb osoby wywołującej, ale ta odpowiedź nie ogranicza, jaka może być ta wartość zwracana ...
kavadias
11

Często dołączam funkcję o nazwie run () do obsługi błędów. Każde wywołanie, które chcę wykonać, jest przekazywane do tej funkcji, więc cały skrypt kończy działanie po awarii. Zaletą tego rozwiązania nad zestawem -e jest to, że skrypt nie wychodzi cicho po awarii linii i może powiedzieć ci, na czym polega problem. W poniższym przykładzie trzecia linia nie jest wykonywana, ponieważ skrypt kończy się po wywołaniu wartości false.

function run() {
  cmd_output=$(eval $1)
  return_value=$?
  if [ $return_value != 0 ]; then
    echo "Command $1 failed"
    exit -1
  else
    echo "output: $cmd_output"
    echo "Command succeeded."
  fi
  return $return_value
}
run "date"
run "false"
run "date"
Joseph Sheedy
źródło
1
Człowieku, z jakiegoś powodu bardzo podoba mi się ta odpowiedź. Rozumiem, że jest to trochę bardziej skomplikowane, ale wydaje się bardzo przydatne. Biorąc pod uwagę fakt, że nie jestem ekspertem od bash, skłaniam mnie do przekonania, że ​​moja logika jest wadliwa i coś jest nie tak z tą metodologią, w przeciwnym razie uważam, że inni chwaliliby ją. Więc jaki jest problem z tą funkcją? Czy jest coś, na co powinienem tu zwrócić uwagę?
Nie pamiętam powodu, dla którego używam eval, funkcja działa dobrze z cmd_output = $ (1 $)
Joseph Sheedy
Właśnie zaimplementowałem to jako część złożonego procesu wdrażania i działało to fantastycznie. Dziękuję i oto komentarz i opinia.
fuzzygroup,
Naprawdę niesamowita praca! To najprostsze i najczystsze rozwiązanie, które działa dobrze. Dla mnie dodałem to przed poleceniem w pętli FOR, ponieważ pętle FOR nie wychwycą set -e option. Następnie polecenie, ponieważ jest z argumentami, użyłem apostrofów aby uniknąć problemów bash tak: runTry 'mysqldump $DB_PASS --user="$DB_USER" --host="$BV_DB_HOST" --triggers --routines --events --single-transaction --verbose $DB_SCHEMA $tables -r $BACKUP_DIR/$tables$BACKUP_FILE_NAME'. Uwaga Zmieniłem nazwę funkcji na runTry.
Tony-Caffe,
1
evaljest potencjalnie niebezpieczne, jeśli zaakceptujesz dowolne dane wejściowe, ale poza tym wygląda to całkiem ładnie.
dragon788,
5

Zamiast ifkonstruować, możesz wykorzystać ocenę zwarcia :

#!/usr/bin/env bash

echo $[1+1]
echo $[2/0]              # division by 0 but execution of script proceeds
echo $[3+1]
(echo $[4/0]) || exit $? # script halted with code 1 returned from `echo`
echo $[5+1]

Zwróć uwagę na parę nawiasów, która jest konieczna ze względu na priorytet operatora przemienności. $?to specjalna zmienna ustawiona na kod zakończenia ostatnio wywoływanej komendy.

Skalee
źródło
1
jeśli to zrobię command -that --fails || exit $?, działa bez nawiasów, co echo $[4/0]takiego powoduje, że ich potrzebujemy?
Anentropic
2
@Anentropic @skalee Nawiasy nie mają nic wspólnego z pierwszeństwem, lecz obsługą wyjątków. Dzielenie przez zero spowoduje natychmiastowe wyjście z powłoki z kodem 1. Bez nawiasów (tj. Prosty echo $[4/0] || exit $?) bash nigdy nie wykona echo, nie mówiąc już o przestrzeganiu ||.
bobbogo,
1

Mam to samo pytanie, ale nie mogę go zadać, ponieważ byłoby duplikatem.

Akceptowana odpowiedź przy użyciu polecenia exit nie działa, gdy skrypt jest nieco bardziej skomplikowany. Jeśli użyjesz procesu działającego w tle, aby sprawdzić warunek, wyjście kończy tylko ten proces, ponieważ działa w podpowłoce. Aby zabić skrypt, musisz go jawnie zabić (przynajmniej to jedyny sposób, jaki znam).

Oto mały skrypt, jak to zrobić:

#!/bin/bash

boom() {
    while true; do sleep 1.2; echo boom; done
}

f() {
    echo Hello
    N=0
    while
        ((N++ <10))
    do
        sleep 1
        echo $N
        #        ((N > 5)) && exit 4 # does not work
        ((N > 5)) && { kill -9 $$; exit 5; } # works 
    done
}

boom &
f &

while true; do sleep 0.5; echo beep; done

To lepsza odpowiedź, ale wciąż niekompletna. Naprawdę nie wiem, jak pozbyć się części boomu .

Gyro Gearloose
źródło