Fehler in einem Bash-Skript auslösen

103

Ich möchte einen Fehler in einem Bash-Skript mit der Meldung "Testfälle fehlgeschlagen !!!" auslösen. Wie geht das in Bash?

Beispielsweise:

if [ condition ]; then
    raise error "Test cases failed !!!"
fi
Naveen Kumar
quelle
1
Was möchten Sie bei diesem Fehler tun? Wie heißt dein Skript? Ist es nur ein Skript oder mehrere Skripte? Wie wird Ihr Skript aussehen?
Etan Reisner
nur ein Skript. Ich nannte es mit Ubuntu-Terminal wie ./script/test.sh
Naveen Kumar
unix.stackexchange.com
Seth McClaine
Keine Liebe für echo you screwed up at ... | mail -s BUG $bugtrackeremailaddress?
Infix

Antworten:

121

Dies hängt davon ab, wo die Fehlermeldung gespeichert werden soll.

Sie können Folgendes tun:

echo "Error!" > logfile.log
exit 125

Oder folgendes:

echo "Error!" 1>&2
exit 64

Wenn Sie eine Ausnahme auslösen, stoppen Sie die Programmausführung.

Sie können auch so etwas wie verwenden , exit xxxwo xxxist der Fehlercode Sie auf das Betriebssystem zurückkehren möchten (von 0 bis 255). Hier haben 125und 64sind nur zufällige Codes , die Sie mit verlassen können. Wenn Sie dem Betriebssystem mitteilen müssen, dass das Programm abnormal gestoppt wurde (z. B. ein Fehler aufgetreten ist), müssen Sie einen Exit-Code ungleich Null an übergeben exit.

Wie @chepner betonte, können Sie dies tun exit 1, was einen nicht spezifizierten Fehler bedeutet .

ForceBru
quelle
12
oder Sie könnten es an stderr senden, wo Fehler gehen sollen.
Wie sende ich es an stderr?
Naveen Kumar
2
@ user3078630, ich habe gerade meine Antwort bearbeitet. 1>&2wird den Trick machen
ForceBru
Wenn es sich um einen Fehler handelt, sollten Sie auch mit einem Exit-Status ungleich Null beenden.exitselbst verwendet den Exit-Status des zuletzt abgeschlossenen Befehls, der 0 sein kann.
chepner
3
Sofern Sie keine bestimmte Bedeutung im Sinn haben, sollten Sie nur verwenden exit 1 , was gemäß Konvention einen nicht spezifizierten Fehler bedeutet.
Chepner
36

Grundlegende Fehlerbehandlung

Wenn Ihr Testfallläufer für fehlgeschlagene Tests einen Code ungleich Null zurückgibt , können Sie einfach schreiben:

test_handler test_case_x; test_result=$?
if ((test_result != 0)); then
  printf '%s\n' "Test case x failed" >&2  # write error message to stderr
  exit 1                                  # or exit $test_result
fi

Oder noch kürzer:

if ! test_handler test_case_x; then
  printf '%s\n' "Test case x failed" >&2
  exit 1
fi

Oder das kürzeste:

test_handler test_case_x || { printf '%s\n' "Test case x failed" >&2; exit 1; }

So beenden Sie mit dem Exit-Code von test_handler:

test_handler test_case_x || { ec=$?; printf '%s\n' "Test case x failed" >&2; exit $ec; }

Erweiterte Fehlerbehandlung

Wenn Sie einen umfassenderen Ansatz verfolgen möchten, können Sie einen Fehlerbehandler haben:

exit_if_error() {
  local exit_code=$1
  shift
  [[ $exit_code ]] &&               # do nothing if no error code passed
    ((exit_code != 0)) && {         # do nothing if error code is 0
      printf 'ERROR: %s\n' "$@" >&2 # we can use better logging here
      exit "$exit_code"             # we could also check to make sure
                                    # error code is numeric when passed
    }
}

Rufen Sie es dann auf, nachdem Sie Ihren Testfall ausgeführt haben:

run_test_case test_case_x
exit_if_error $? "Test case x failed"

oder

run_test_case test_case_x || exit_if_error $? "Test case x failed"

Die Vorteile eines Fehlerbehandlers mögen exit_if_error sind:

  • Wir können die gesamte Fehlerbehandlungslogik wie die Protokollierung standardisieren , Drucken eines Stack-Trace , Benachrichtigung, Bereinigung usw. an einem Ort
  • Indem wir den Fehlerbehandler veranlassen, den Fehlercode als Argument abzurufen, können wir den Aufrufer vor dem Durcheinander von bewahren if Blöcken , die Exit-Codes auf Fehler testen
  • wenn wir einen Signalhandler haben (mit Trap ), können wir den Fehlerhandler von dort aus aufrufen

Fehlerbehandlungs- und Protokollierungsbibliothek

Hier ist eine vollständige Implementierung der Fehlerbehandlung und -protokollierung:

https://github.com/codeforester/base/blob/master/lib/stdlib.sh


Zusammenhängende Posts

Codeforester
quelle
9

Es gibt noch einige weitere Möglichkeiten, wie Sie dieses Problem angehen können. Angenommen, eine Ihrer Anforderungen besteht darin, ein Shell-Skript / eine Shell-Funktion mit einigen Shell-Befehlen auszuführen, zu überprüfen, ob das Skript erfolgreich ausgeführt wurde, und bei Fehlern Fehler auszulösen.

Die Shell-Befehle basieren im Allgemeinen auf zurückgegebenen Exit-Codes, um der Shell mitzuteilen, ob sie erfolgreich war oder aufgrund unerwarteter Ereignisse fehlgeschlagen ist.

Was Sie also tun möchten, fällt auf diese beiden Kategorien

  • Beenden Sie bei einem Fehler
  • Beenden und Bereinigen bei Fehler

Je nachdem, welche Sie ausführen möchten, stehen Shell-Optionen zur Verfügung. Für den ersten Fall stellt die Shell eine Option mit set -eund für die zweite könnten Sie ein tun trapaufEXIT

Soll ich exitin meinem Skript / meiner Funktion verwenden?

Verwenden von exit allgemeine Verbesserung verbessert die Lesbarkeit. In bestimmten Routinen möchten Sie, sobald Sie die Antwort kennen, sofort zur aufrufenden Routine zurückkehren. Wenn die Routine so definiert ist, dass nach dem Erkennen eines Fehlers keine weitere Bereinigung erforderlich ist, bedeutet das nicht sofortige Beenden, dass Sie mehr Code schreiben müssen.

In Fällen, in denen Sie Bereinigungsaktionen für das Skript ausführen müssen, um die Beendigung des Skripts zu bereinigen, wird die Verwendung nicht bevorzugt exit.

Soll ich set -ebeim Beenden einen Fehler verwenden?

Nein!

set -ewar ein Versuch, der Shell "automatische Fehlererkennung" hinzuzufügen. Ihr Ziel war es, die Shell jedes Mal abzubrechen, wenn ein Fehler auftrat, aber es gibt viele potenzielle Fallstricke, zum Beispiel:

  • Die Befehle, die Teil eines if-Tests sind, sind immun. Wenn Sie im Beispiel erwarten, dass es bei der testÜberprüfung des nicht vorhandenen Verzeichnisses unterbrochen wird, wird dies nicht der Fall sein, und es wird die Bedingung else durchlaufen

    set -e
    f() { test -d nosuchdir && echo no dir; }
    f
    echo survived
  • Befehle in einer anderen als der letzten Pipeline sind immun. Im folgenden Beispiel wird der Exit-Code des zuletzt ausgeführten Befehls (ganz rechts) als ( cat) betrachtet und war erfolgreich. Dies könnte durch Einstellen der set -o pipefailOption vermieden werden, es ist jedoch immer noch eine Einschränkung.

    set -e
    somecommand that fails | cat -
    echo survived 

Empfohlen für den Gebrauch - trap beim Verlassen

Das Urteil lautet: Wenn Sie in der Lage sein möchten, einen Fehler zu behandeln, anstatt blind zu beenden, anstatt zu verwenden set -e, verwenden Sie ein trapauf demERR Pseudosignal.

Der ERRTrap soll keinen Code ausführen, wenn die Shell selbst mit einem Fehlercode ungleich Null beendet wird, sondern wenn ein Befehl von dieser Shell ausgeführt wird, der nicht Teil einer Bedingung ist (wie in if cmd, orcmd || ), mit einem Exit-Status ungleich Null beendet wird .

Die allgemeine Praxis besteht darin, einen Trap-Handler zu definieren, der zusätzliche Debug-Informationen zu welcher Zeile und welcher Ursache für den Exit bereitstellt. Denken Sie daran, dass der Exit-Code des letzten Befehls, der das ERRSignal verursacht hat , zu diesem Zeitpunkt noch verfügbar ist.

cleanup() {
    exitcode=$?
    printf 'error condition hit\n' 1>&2
    printf 'exit code returned: %s\n' "$exitcode"
    printf 'the command executing at the time of the error was: %s\n' "$BASH_COMMAND"
    printf 'command present on line: %d' "${BASH_LINENO[0]}"
    # Some more clean up code can be added here before exiting
    exit $exitcode
}

und wir verwenden diesen Handler wie folgt über dem fehlerhaften Skript

trap cleanup ERR

Wenn Sie dies in einem einfachen Skript zusammenstellen, das falsein Zeile 15 die Informationen enthält, die Sie erhalten würden

error condition hit
exit code returned: 1
the command executing at the time of the error was: false
command present on line: 15

Das trapbietet auch Optionen unabhängig vom Fehler, die Bereinigung nur nach Abschluss der Shell (z. B. wenn Ihr Shell-Skript beendet wird) auf Signal auszuführen EXIT. Sie können auch mehrere Signale gleichzeitig abfangen. Die Liste der unterstützten Signale, auf die abgefangen werden soll, finden Sie auf der Handbuchseite trap.1p - Linux

Eine andere Sache, die Sie beachten sollten, wäre zu verstehen, dass keine der bereitgestellten Methoden funktioniert, wenn Sie sich mit Sub-Shells befassen. In diesem Fall müssen Sie möglicherweise Ihre eigene Fehlerbehandlung hinzufügen.

  • Auf einer Sub-Shell mit set -ewürde nicht funktionieren. Das falseist auf die Sub-Shell beschränkt und wird niemals an die übergeordnete Shell weitergegeben. Um die Fehlerbehandlung hier durchzuführen, fügen Sie Ihre eigene Logik hinzu(false) || false

    set -e
    (false)
    echo survived
  • Das gleiche passiert auch mit trap. Die folgende Logik würde aus den oben genannten Gründen nicht funktionieren.

    trap 'echo error' ERR
    (false)
Inian
quelle
5

Hier ist eine einfache Falle, die das letzte Argument von STDERR ausgibt, die fehlgeschlagene Zeile meldet und das Skript mit der Zeilennummer als Exit-Code beendet. Beachten Sie, dass dies nicht immer großartige Ideen sind, aber dies zeigt einige kreative Anwendungen, auf denen Sie aufbauen können.

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR

Ich habe das in ein Skript mit einer Schleife eingefügt, um es zu testen. Ich überprüfe nur, ob einige Zufallszahlen getroffen wurden. Sie könnten tatsächliche Tests verwenden. Wenn ich auf Kaution gehen muss, rufe ich false (was die Falle auslöst) mit der Nachricht an, die ich werfen möchte.

Lassen Sie den Trap für eine erweiterte Funktionalität eine Verarbeitungsfunktion aufrufen. Sie können immer eine case-Anweisung für Ihr Argument ($ _) verwenden, wenn Sie mehr Bereinigung usw. durchführen müssen. Weisen Sie einer var für ein wenig syntaktischen Zucker zu -

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR
throw=false
raise=false

while :
do x=$(( $RANDOM % 10 ))
   case "$x" in
   0) $throw "DIVISION BY ZERO" ;;
   3) $raise "MAGIC NUMBER"     ;;
   *) echo got $x               ;;
   esac
done

Beispielausgabe:

# bash tst
got 2
got 8
DIVISION BY ZERO at 6
# echo $?
6

Offensichtlich könnten Sie

runTest1 "Test1 fails" # message not used if it succeeds

Viel Raum für Designverbesserungen.

Die Nachteile sind die Tatsache, dass falsees nicht schön ist (also der Zucker), und andere Dinge, die die Falle auslösen, könnten ein wenig dumm aussehen. Trotzdem mag ich diese Methode.

Paul Hodges
quelle
4

Sie haben zwei Möglichkeiten: Leiten Sie die Ausgabe des Skripts in eine Datei um, führen Sie eine Protokolldatei in das Skript ein und

  1. Ausgabe in eine Datei umleiten :

Hier nehmen Sie an, dass das Skript alle notwendigen Informationen einschließlich Warn- und Fehlermeldungen ausgibt. Sie können die Ausgabe dann in eine Datei Ihrer Wahl umleiten.

./runTests &> output.log

Der obige Befehl leitet sowohl die Standardausgabe als auch die Fehlerausgabe in Ihre Protokolldatei um.

Mit diesem Ansatz müssen Sie keine Protokolldatei in das Skript einfügen, sodass die Logik ein wenig einfacher ist.

  1. Führen Sie eine Protokolldatei in das Skript ein :

Fügen Sie in Ihrem Skript eine Protokolldatei hinzu, indem Sie sie entweder fest codieren:

logFile='./path/to/log/file.log'

oder Übergeben an einen Parameter:

logFile="${1}"  # This assumes the first parameter to the script is the log file

Es ist eine gute Idee, den Zeitstempel zum Zeitpunkt der Ausführung zur Protokolldatei oben im Skript hinzuzufügen:

date '+%Y%-m%d-%H%M%S' >> "${logFile}"

Sie können dann Ihre Fehlermeldungen in die Protokolldatei umleiten

if [ condition ]; then
    echo "Test cases failed!!" >> "${logFile}"; 
fi

Dadurch wird der Fehler an die Protokolldatei angehängt und die Ausführung fortgesetzt. Wenn Sie die Ausführung stoppen möchten, wenn kritische Fehler auftreten, können Sie exitdas folgende Skript ausführen :

if [ condition ]; then
    echo "Test cases failed!!" >> "${logFile}"; 
    # Clean up if needed
    exit 1;
fi

Beachten Sie, dass exit 1 dass das Programm die Ausführung aufgrund eines nicht angegebenen Fehlers stoppt. Sie können dies anpassen, wenn Sie möchten.

Mit diesem Ansatz können Sie Ihre Protokolle anpassen und für jede Komponente Ihres Skripts eine andere Protokolldatei erstellen.


Wenn Sie ein relativ kleines Skript haben oder das Skript eines anderen ausführen möchten, ohne es zu ändern, ist der erste Ansatz besser geeignet.

Wenn Sie immer möchten, dass sich die Protokolldatei am selben Speicherort befindet, ist dies die bessere Option der 2. Auch wenn Sie ein großes Skript mit mehreren Komponenten erstellt haben, möchten Sie möglicherweise jedes Teil anders protokollieren, und der zweite Ansatz ist Ihr einziger Möglichkeit.

Alamoot
quelle
3

Ich finde es oft nützlich, eine Funktion zur Behandlung von Fehlermeldungen zu schreiben, damit der Code insgesamt sauberer ist.

# Usage: die [exit_code] [error message]
die() {
  local code=$? now=$(date +%T.%N)
  if [ "$1" -ge 0 ] 2>/dev/null; then  # assume $1 is an error code if numeric
    code="$1"
    shift
  fi
  echo "$0: ERROR at ${now%???}${1:+: $*}" >&2
  exit $code
}

Dadurch wird der Fehlercode aus dem vorherigen Befehl übernommen und beim Beenden des gesamten Skripts als Standardfehlercode verwendet. Außerdem wird die Uhrzeit mit Mikrosekunden angegeben, sofern dies unterstützt wird (das GNU-Datum %Nist Nanosekunden, die wir später auf Mikrosekunden kürzen).

Wenn die erste Option Null oder eine positive Ganzzahl ist, wird sie zum Exit-Code und wird aus der Liste der Optionen entfernt. Wir melden die Nachricht dann an den Standardfehler mit dem Namen des Skripts, dem Wort "ERROR" und der Zeit (wir verwenden die Parametererweiterung, um Nanosekunden auf Mikrosekunden abzuschneiden oder für Nicht-GNU-Zeiten, z . B. 12:34:56.%Num 12:34:56). Nach dem Wort ERROR werden ein Doppelpunkt und ein Leerzeichen hinzugefügt, jedoch nur, wenn eine Fehlermeldung angezeigt wird. Schließlich beenden wir das Skript mit dem zuvor festgelegten Exit-Code und lösen wie gewohnt alle Traps aus.

Einige Beispiele (vorausgesetzt, der Code lebt in script.sh):

if [ condition ]; then die 123 "condition not met"; fi
# exit code 123, message "script.sh: ERROR at 14:58:01.234564: condition not met"

$command |grep -q condition || die 1 "'$command' lacked 'condition'"
# exit code 1, "script.sh: ERROR at 14:58:55.825626: 'foo' lacked 'condition'"

$command || die
# exit code comes from command's, message "script.sh: ERROR at 14:59:15.575089"
Adam Katz
quelle