Warum beendet (exit 1) das Skript nicht?

47

Ich habe ein Skript, das nicht beendet wird, wenn ich es möchte.

Ein Beispielskript mit demselben Fehler ist:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

Ich würde davon ausgehen, die Ausgabe zu sehen:

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

Aber ich sehe tatsächlich:

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

Erzeugt die ()Befehlsverkettung irgendwie einen Bereich? Woraus wird exitdas Skript beendet, wenn nicht?

Minix
quelle
5
Dies erfordert eine Antwort auf ein einzelnes Wort: Subshell
Joshua
Siehe auch
Wildcard

Antworten:

87

()führt Befehle in der Subshell aus, sodass exitSie die Subshell verlassen und zur übergeordneten Shell zurückkehren. Verwenden Sie geschweifte Klammern, {}wenn Sie Befehle in der aktuellen Shell ausführen möchten.

Aus dem Bash-Handbuch:

(list) list wird in einer Subshell-Umgebung ausgeführt. Variablenzuweisungen und integrierte Befehle, die sich auf die Umgebung der Shell auswirken, bleiben nach Abschluss des Befehls nicht wirksam. Der Rückgabestatus ist der Exit-Status der Liste.

{ Liste; } list wird einfach in der aktuellen Shell-Umgebung ausgeführt. Liste muss mit einem Zeilenumbruch oder Semikolon abgeschlossen werden. Dies wird als Gruppenbefehl bezeichnet. Der Rückgabestatus ist der Exit-Status der Liste. Beachten Sie, dass {und} im Gegensatz zu den Metazeichen (und) reservierte Wörter sind und dort vorkommen müssen, wo ein reserviertes Wort erkannt werden darf. Da sie keinen Wortumbruch verursachen, müssen sie durch Leerzeichen oder ein anderes Shell-Metazeichen von der Liste getrennt werden.

Es ist erwähnenswert, dass die Shell-Syntax ziemlich konsistent ist und die Subshell auch an anderen ()Konstrukten wie der Befehlssubstitution (auch mit der alten `..`Syntax) oder der Prozesssubstitution beteiligt ist, sodass die folgende Shell auch nicht verlassen wird:

echo $(exit)
cat <(exit)

Während es offensichtlich sein mag, dass Subshells involviert sind, wenn Befehle explizit darin platziert werden (), ist die weniger sichtbare Tatsache, dass sie auch in diesen anderen Strukturen erzeugt werden:

  • Befehl wurde im Hintergrund gestartet

    exit &

    verlässt die aktuelle Shell nicht, weil (nach man bash)

    Wenn ein Befehl vom Steueroperator & beendet wird, führt die Shell den Befehl im Hintergrund in einer Subshell aus. Die Shell wartet nicht auf den Abschluss des Befehls und der Rückgabestatus ist 0.

  • die Pipeline

    exit | echo foo

    verlässt immer noch nur die Unterschale.

    Unterschiedliche Schalen verhalten sich diesbezüglich jedoch unterschiedlich. bashVersetzt beispielsweise alle Komponenten der Pipeline in separate Subshells (es sei denn, Sie verwenden die lastpipeOption in Aufrufen, in denen die Jobsteuerung nicht aktiviert ist), aber AT & T kshund zshführen den letzten Teil in der aktuellen Shell aus (beide Verhalten sind von POSIX zulässig). Somit

    exit | exit | exit

    tut im Grunde nichts in bash, verlässt aber die zsh wegen der letzten exit .

  • coproc exitläuft auch exitin einer Unterschale.

jimmij
quelle
5
Ah. Jetzt finde ich alle Stellen, an denen mein Vorgänger die falschen Zahnspangen verwendet hat. Danke für den Einblick.
Minix
10
Beachten Sie sorgfältig den Abstand in der Manpage: {und }sind nicht Syntax, sie sind reservierte Wörter und müssen durch Leerzeichen umgeben sein, und die Liste muss mit einem Befehlsabschlussende (Semikolon, Newline, Ampersand)
glenn Jackman
Ist dies aus Interesse eher ein anderer Prozess oder nur eine separate Umgebung in einem internen Stapel? Ich benutze () viel, um Chdirs zu isolieren, und muss Glück gehabt haben, wenn ich $$ etc verwendet habe.
Dan Sheppard
5
@DanSheppard Dies ist ein anderer Prozess, der jedoch die (echo $$)ID der übergeordneten Shell $$ausgibt, da diese bereits erweitert wird, bevor eine untergeordnete Shell erstellt wird. Tatsächlich kann das Drucken der Subshell-Prozess-ID schwierig sein (siehe stackoverflow.com/questions/9119885/…
jimmij
@jimmij, Wie kann es sein, dass $$das erweitert wird, bevor die Subshell erstellt wird, und dennoch $BASHPIDden richtigen Wert für eine Subshell anzeigt?
Wildcard
13

Das Ausführen exitin einer Subshell ist eine Falle:

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

Das Skript druckt 42, beendet die Subshell mit dem Rückkehrcode 1und fährt mit dem Skript fort. Selbst das Ersetzen des Anrufs durch echo $(CALC) || exit 1hilft nicht, da der Rückgabecode echo0 ist, unabhängig vom Rückgabecode von calc. Und calcwird vor ausgeführt echo.

Noch rätselhafter ist es, den Effekt zu vereiteln, exitindem Sie localihn wie im folgenden Skript in Built- in einschließen. Ich bin über das Problem gestolpert, als ich eine Funktion zur Überprüfung eines Eingabewerts geschrieben habe. Beispiel:

Ich möchte eine Datei mit dem Namen "year month day.log" 20141211.logfür heute erstellen . Das Datum wird von einem Benutzer eingegeben, der möglicherweise keinen angemessenen Wert angibt. Daher fnameüberprüfe ich in meiner Funktion den Rückgabewert von date, um die Gültigkeit der Benutzereingabe zu überprüfen:

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

Sieht gut aus. Lassen Sie das Skript benannt werden s.sh. Wenn der Benutzer das Skript mit aufruft ./s.sh "Thu Dec 11 20:45:49 CET 2014", wird die Datei 20141211.logerstellt. Wenn der Benutzer jedoch Folgendes eingibt ./s.sh "Thu hec 11 20:45:49 CET 2014", gibt das Skript Folgendes aus:

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

Die Zeile fname…besagt, dass in der Subshell fehlerhafte Eingabedaten erkannt wurden. Das exit 1am Ende der local …Zeile wird jedoch nie ausgelöst, da die localDirektive immer zurückkehrt 0. Dies liegt daran local, dass nach ausgeführt wird $(fname)und somit seinen Rückkehrcode überschreibt. Aus diesem Grund wird das Skript fortgesetzt und touchmit einem leeren Parameter aufgerufen. Dieses Beispiel ist einfach, aber das Verhalten von bash kann in einer realen Anwendung ziemlich verwirrend sein. Ich weiß, echte Programmierer benutzen keine Einheimischen. «

Um es deutlich zu machen: Ohne localwird das Skript wie erwartet abgebrochen, wenn ein ungültiges Datum eingegeben wird.

Das Update ist, die Linie wie zu teilen

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

Das seltsame Verhalten entspricht der Dokumentation localin der Manpage von bash: "Der Rückgabestatus ist 0, sofern local nicht außerhalb einer Funktion verwendet wird, ein ungültiger Name angegeben wird oder name eine schreibgeschützte Variable ist."

Obwohl dies kein Fehler ist, habe ich das Gefühl, dass das Verhalten von bash nicht intuitiv ist. Mir ist die Ausführungsreihenfolge bekannt, localsollte aber eine fehlerhafte Zuordnung nicht maskieren.

Meine erste Antwort enthielt einige Ungenauigkeiten. Nach einer aufschlussreichen und eingehenden Diskussion mit mikeserv (danke dafür) habe ich sie repariert.

hermannk
quelle
@mikeserv: Ich habe ein Beispiel hinzugefügt, um die Relevanz zu zeigen.
Hermannk
@mikeserv: Ja, du hast recht. Noch knapper. Aber die Falle ist immer noch da.
Hermannk
@mikeserv: Sorry, mein Beispiel war kaputt. Ich hatte den Test in vergessen doit().
Hermannk
Lassen Sie uns diese Diskussion im Chat fortsetzen .
Hermannk
1

Die Klammern beginnen eine Subshell und der Exit beendet nur diese Subshell.

Sie können den Exitcode mit lesen $?und diesen in Ihr Skript einfügen, um das Skript zu beenden, wenn die Subshell beendet wurde:

#!/bin/bash

function bla() {
    return 1
}

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

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

echo '2'
rubo77
quelle
1

Die eigentliche Lösung:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

Die Fehlergruppierung wird nur ausgeführt, wenn blaein Fehlerstatus zurückgegeben wird. Sie exitbefindet sich nicht in einer Subshell, sodass das gesamte Skript angehalten wird.

Walf
quelle