Fehlerbehandlung in Bash

239

Was ist Ihre Lieblingsmethode, um Fehler in Bash zu behandeln? Das beste Beispiel für den Umgang mit Fehlern, das ich im Internet gefunden habe, wurde von William Shotts, Jr., geschrieben http://www.linuxcommand.org geschrieben .

Er schlägt vor, die folgende Funktion zur Fehlerbehandlung in Bash zu verwenden:

#!/bin/bash

# A slicker error handling routine

# I put a variable in my scripts named PROGNAME which
# holds the name of the program being run.  You can get this
# value from the first item on the command line ($0).

# Reference: This was copied from <http://www.linuxcommand.org/wss0150.php>

PROGNAME=$(basename $0)

function error_exit
{

#   ----------------------------------------------------------------
#   Function for exit due to fatal program error
#       Accepts 1 argument:
#           string containing descriptive error message
#   ---------------------------------------------------------------- 

    echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
    exit 1
}

# Example call of the error_exit function.  Note the inclusion
# of the LINENO environment variable.  It contains the current
# line number.

echo "Example of error with line number and message"
error_exit "$LINENO: An error has occurred."

Haben Sie eine bessere Fehlerbehandlungsroutine, die Sie in Bash-Skripten verwenden?

Noob
quelle
1
Siehe diese detaillierte Antwort: Fehler in einem Bash-Skript auslösen .
Codeforester
1
Die Implementierung der Protokollierung und Fehlerbehandlung finden Sie hier: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Antworten:

154

Benutze eine Falle!

tempfiles=( )
cleanup() {
  rm -f "${tempfiles[@]}"
}
trap cleanup 0

error() {
  local parent_lineno="$1"
  local message="$2"
  local code="${3:-1}"
  if [[ -n "$message" ]] ; then
    echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}"
  else
    echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
  fi
  exit "${code}"
}
trap 'error ${LINENO}' ERR

... dann, wann immer Sie eine temporäre Datei erstellen:

temp_foo="$(mktemp -t foobar.XXXXXX)"
tempfiles+=( "$temp_foo" )

und $temp_foowird beim Beenden gelöscht und die aktuelle Zeilennummer wird gedruckt. (gibt set -eIhnen ebenfalls ein Exit-on-Error-Verhalten, obwohl es mit schwerwiegenden Einschränkungen verbunden ist und die Vorhersagbarkeit und Portabilität des Codes schwächt).

Sie können den Trap entweder errorfür Sie aufrufen lassen (in diesem Fall wird der Standard-Exit-Code 1 und keine Nachricht verwendet) oder ihn selbst aufrufen und explizite Werte angeben. zum Beispiel:

error ${LINENO} "the foobar failed" 2

wird mit Status 2 beendet und gibt eine explizite Nachricht.

Charles Duffy
quelle
4
@draemon Die variable Großschreibung ist beabsichtigt. All-Caps sind nur für Shell-Buildins und Umgebungsvariablen üblich. Die Verwendung von Kleinbuchstaben für alles andere verhindert Namespace-Konflikte. Siehe auch stackoverflow.com/questions/673055/…
Charles Duffy
1
Testen Sie Ihre Änderung, bevor Sie sie erneut brechen. Konventionen sind eine gute Sache, aber sie sind dem funktionierenden Code untergeordnet.
Draemon
3
@ Draemon, ich bin eigentlich anderer Meinung. Offensichtlich defekter Code wird bemerkt und behoben. Schlechte Praktiken, aber meistens funktionierender Code lebt für immer (und wird verbreitet).
Charles Duffy
1
aber du hast es nicht bemerkt. Defekter Code wird bemerkt, weil funktionierender Code das Hauptanliegen ist.
Draemon
5
Es ist nicht gerade kostenlos ( stackoverflow.com/a/10927223/26334 ) und wenn der Code bereits nicht mit POSIX kompatibel ist, kann das Entfernen des Funktionsschlüsselworts nicht mehr unter POSIX sh ausgeführt werden, aber mein Hauptpunkt war, dass Sie ' ve (IMO) hat die Antwort abgewertet, indem die Empfehlung zur Verwendung von set -e geschwächt wurde. Bei Stackoverflow geht es nicht um "Ihren" Code, sondern darum, die besten Antworten zu erhalten.
Draemon
123

Das ist eine gute Lösung. Ich wollte nur hinzufügen

set -e

als rudimentärer Fehlermechanismus. Das Skript wird sofort gestoppt, wenn ein einfacher Befehl fehlschlägt. Ich denke, dies hätte das Standardverhalten sein sollen: Da solche Fehler fast immer etwas Unerwartetes bedeuten, ist es nicht wirklich „vernünftig“, die folgenden Befehle weiterhin auszuführen.

Bruno De Fraine
quelle
29
set -eist nicht ohne Fallstricke: Siehe mywiki.wooledge.org/BashFAQ/105 für mehrere.
Charles Duffy
3
@ CharlesDuffy, einige der Fallstricke können mitset -o pipefail
Kochfeldern
7
@ CharlesDuffy Danke, dass du auf die Fallstricke hingewiesen hast; Insgesamt denke ich jedoch immer noch, dass set -edas Nutzen-Kosten-Verhältnis hoch ist.
Bruno De Fraine
3
@BrunoDeFraine Ich benutze set -emich selbst, aber einige der anderen Stammgäste in irc.freenode.org # bash raten (ziemlich stark) davon ab. Zumindest sollten die fraglichen Fallstricke gut verstanden werden.
Charles Duffy
3
setze -e -o Pipefail -u # und weiß was du tust
Sam Watkins
78

Das Lesen aller Antworten auf dieser Seite hat mich sehr inspiriert.

Also, hier ist mein Hinweis:

Dateiinhalt: lib.trap.sh

lib_name='trap'
lib_version=20121026

stderr_log="/dev/shm/stderr.log"

#
# TO BE SOURCED ONLY ONCE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

if test "${g_libs[$lib_name]+_}"; then
    return 0
else
    if test ${#g_libs[@]} == 0; then
        declare -A g_libs
    fi
    g_libs[$lib_name]=$lib_version
fi


#
# MAIN CODE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
set -o nounset   ## set -u : exit the script if you try to use an uninitialised variable
set -o errexit   ## set -e : exit the script if any statement returns a non-true return value

exec 2>"$stderr_log"


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: EXIT_HANDLER
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function exit_handler ()
{
    local error_code="$?"

    test $error_code == 0 && return;

    #
    # LOCAL VARIABLES:
    # ------------------------------------------------------------------
    #    
    local i=0
    local regex=''
    local mem=''

    local error_file=''
    local error_lineno=''
    local error_message='unknown'

    local lineno=''


    #
    # PRINT THE HEADER:
    # ------------------------------------------------------------------
    #
    # Color the output if it's an interactive terminal
    test -t 1 && tput bold; tput setf 4                                 ## red bold
    echo -e "\n(!) EXIT HANDLER:\n"


    #
    # GETTING LAST ERROR OCCURRED:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    #
    # Read last file from the error log
    # ------------------------------------------------------------------
    #
    if test -f "$stderr_log"
        then
            stderr=$( tail -n 1 "$stderr_log" )
            rm "$stderr_log"
    fi

    #
    # Managing the line to extract information:
    # ------------------------------------------------------------------
    #

    if test -n "$stderr"
        then        
            # Exploding stderr on :
            mem="$IFS"
            local shrunk_stderr=$( echo "$stderr" | sed 's/\: /\:/g' )
            IFS=':'
            local stderr_parts=( $shrunk_stderr )
            IFS="$mem"

            # Storing information on the error
            error_file="${stderr_parts[0]}"
            error_lineno="${stderr_parts[1]}"
            error_message=""

            for (( i = 3; i <= ${#stderr_parts[@]}; i++ ))
                do
                    error_message="$error_message "${stderr_parts[$i-1]}": "
            done

            # Removing last ':' (colon character)
            error_message="${error_message%:*}"

            # Trim
            error_message="$( echo "$error_message" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
    fi

    #
    # GETTING BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
    _backtrace=$( backtrace 2 )


    #
    # MANAGING THE OUTPUT:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    local lineno=""
    regex='^([a-z]{1,}) ([0-9]{1,})$'

    if [[ $error_lineno =~ $regex ]]

        # The error line was found on the log
        # (e.g. type 'ff' without quotes wherever)
        # --------------------------------------------------------------
        then
            local row="${BASH_REMATCH[1]}"
            lineno="${BASH_REMATCH[2]}"

            echo -e "FILE:\t\t${error_file}"
            echo -e "${row^^}:\t\t${lineno}\n"

            echo -e "ERROR CODE:\t${error_code}"             
            test -t 1 && tput setf 6                                    ## white yellow
            echo -e "ERROR MESSAGE:\n$error_message"


        else
            regex="^${error_file}\$|^${error_file}\s+|\s+${error_file}\s+|\s+${error_file}\$"
            if [[ "$_backtrace" =~ $regex ]]

                # The file was found on the log but not the error line
                # (could not reproduce this case so far)
                # ------------------------------------------------------
                then
                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    echo -e "ERROR MESSAGE:\n${stderr}"

                # Neither the error line nor the error file was found on the log
                # (e.g. type 'cp ffd fdf' without quotes wherever)
                # ------------------------------------------------------
                else
                    #
                    # The error file is the first on backtrace list:

                    # Exploding backtrace on newlines
                    mem=$IFS
                    IFS='
                    '
                    #
                    # Substring: I keep only the carriage return
                    # (others needed only for tabbing purpose)
                    IFS=${IFS:0:1}
                    local lines=( $_backtrace )

                    IFS=$mem

                    error_file=""

                    if test -n "${lines[1]}"
                        then
                            array=( ${lines[1]} )

                            for (( i=2; i<${#array[@]}; i++ ))
                                do
                                    error_file="$error_file ${array[$i]}"
                            done

                            # Trim
                            error_file="$( echo "$error_file" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
                    fi

                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    if test -n "${stderr}"
                        then
                            echo -e "ERROR MESSAGE:\n${stderr}"
                        else
                            echo -e "ERROR MESSAGE:\n${error_message}"
                    fi
            fi
    fi

    #
    # PRINTING THE BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 7                                            ## white bold
    echo -e "\n$_backtrace\n"

    #
    # EXITING:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 4                                            ## red bold
    echo "Exiting!"

    test -t 1 && tput sgr0 # Reset terminal

    exit "$error_code"
}
trap exit_handler EXIT                                                  # ! ! ! TRAP EXIT ! ! !
trap exit ERR                                                           # ! ! ! TRAP ERR ! ! !


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: BACKTRACE
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function backtrace
{
    local _start_from_=0

    local params=( "$@" )
    if (( "${#params[@]}" >= "1" ))
        then
            _start_from_="$1"
    fi

    local i=0
    local first=false
    while caller $i > /dev/null
    do
        if test -n "$_start_from_" && (( "$i" + 1   >= "$_start_from_" ))
            then
                if test "$first" == false
                    then
                        echo "BACKTRACE IS:"
                        first=true
                fi
                caller $i
        fi
        let "i=i+1"
    done
}

return 0



Anwendungsbeispiel:
Dateiinhalt: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

echo "doing something wrong now .."
echo "$foo"

exit 0


Laufen:

bash trap-test.sh

Ausgabe:

doing something wrong now ..

(!) EXIT HANDLER:

FILE:       trap-test.sh
LINE:       6

ERROR CODE: 1
ERROR MESSAGE:
foo:   unassigned variable

BACKTRACE IS:
1 main trap-test.sh

Exiting!


Wie Sie dem folgenden Screenshot entnehmen können, ist die Ausgabe farbig und die Fehlermeldung wird in der verwendeten Sprache angezeigt.

Geben Sie hier die Bildbeschreibung ein

Luca Borrione
quelle
3
Diese Sache ist großartig. Sie sollten ein Github-Projekt dafür erstellen, damit die Leute leicht Verbesserungen vornehmen und einen Beitrag leisten können. Ich habe es mit log4bash kombiniert und zusammen erstellt es eine leistungsstarke Umgebung zum Erstellen guter Bash-Skripte.
Dominik Dorn
1
Zu Ihrer Information - test ${#g_libs[@]} == 0ist nicht POSIX-kompatibel (POSIX-Test unterstützt =Zeichenfolgenvergleiche oder -eqnumerische Vergleiche, aber nicht ==zu vergessen das Fehlen von Arrays in POSIX), und wenn Sie nicht versuchen, POSIX-kompatibel zu sein, warum in der Welt benutzt du testüberhaupt eher als einen mathematischen Kontext? (( ${#g_libs[@]} == 0 ))ist schließlich leichter zu lesen.
Charles Duffy
2
@ Luca - das ist wirklich toll! Ihr Bild hat mich dazu inspiriert, meine eigene Implementierung zu erstellen, die noch einige Schritte weiter geht. Ich habe es in meiner Antwort unten gepostet .
Niieani
3
Bravissimo !! Dies ist eine hervorragende Möglichkeit, ein Skript zu debuggen. Grazie Mille Das einzige, was ich hinzugefügt habe, war eine Überprüfung für OS X wie case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
folgt
1
Ein bisschen schamlos, aber wir haben dieses Snippet genommen, es bereinigt, weitere Funktionen hinzugefügt, die Ausgabeformatierung verbessert und es POSIX-kompatibler gemacht (funktioniert sowohl unter Linux als auch unter OSX). Es ist als Teil von Privex ShellCore auf Github veröffentlicht: github.com/Privex/shell-core
Someguy123
22

Eine äquivalente Alternative zu "set -e" ist

set -o errexit

Es macht die Bedeutung der Flagge etwas klarer als nur "-e".

Zufällige Hinzufügung: Um das Flag vorübergehend zu deaktivieren und zum Standard zurückzukehren (fortlaufende Ausführung unabhängig von den Exit-Codes), verwenden Sie einfach

set +e
echo "commands run here returning non-zero exit codes will not cause the entire script to fail"
echo "false returns 1 as an exit code"
false
set -e

Dies schließt eine ordnungsgemäße Fehlerbehandlung aus, die in anderen Antworten erwähnt wird, ist jedoch schnell und effektiv (genau wie Bash).

Ben Scholbrock
quelle
1
Die Verwendung $(foo)auf einer bloßen Linie und nicht nur fooist normalerweise das Falsche. Warum es fördern, indem man es als Beispiel gibt?
Charles Duffy
20

Inspiriert von den hier vorgestellten Ideen habe ich eine lesbare und bequeme Methode entwickelt, um Fehler in Bash-Skripten in meinem Bash-Boilerplate-Projekt zu behandeln .

Durch einfache die Bibliothek Sourcing, erhalten Sie die folgenden aus dem Kasten (dh es wird die Ausführung bei jedem Fehler zu stoppen, als bei Verwendung set -edurch eine trapauf ERRund einig Bash-fu ):

Bash-Oo-Framework Fehlerbehandlung

Es gibt einige zusätzliche Funktionen, die bei der Behandlung von Fehlern helfen, z. B. try and catch oder das Schlüsselwort throw , mit dem Sie die Ausführung an einem Punkt unterbrechen können, an dem die Rückverfolgung angezeigt wird. Wenn das Terminal dies unterstützt, spuckt es außerdem Powerline-Emojis aus, färbt Teile der Ausgabe für eine gute Lesbarkeit und unterstreicht die Methode, die die Ausnahme im Kontext der Codezeile verursacht hat.

Der Nachteil ist - es ist nicht portabel -, dass der Code in Bash funktioniert, wahrscheinlich nur> = 4 (aber ich würde mir vorstellen, dass er mit etwas Aufwand auf Bash 3 portiert werden könnte).

Der Code ist zur besseren Handhabung in mehrere Dateien unterteilt, aber ich wurde von der Backtrace-Idee aus der obigen Antwort von Luca Borrione inspiriert .

Weitere Informationen oder einen Blick auf die Quelle finden Sie unter GitHub:

https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw

niieani
quelle
Dies befindet sich im Bash Object Oriented Framework- Projekt. ... Zum Glück hat es nur 7,4k LOC (laut GLOC ). OOP - Objektorientierter Schmerz?
Ingyhere
@ingyhere es ist sehr modular (und löschungsfreundlich), so dass Sie den Ausnahmeteil nur verwenden können, wenn Sie dafür gekommen sind;)
niieani
11

Ich bevorzuge etwas, das wirklich einfach anzurufen ist. Also benutze ich etwas, das etwas kompliziert aussieht, aber einfach zu bedienen ist. Normalerweise kopiere ich einfach den folgenden Code und füge ihn in meine Skripte ein. Eine Erklärung folgt dem Code.

#This function is used to cleanly exit any script. It does this displaying a
# given error message, and exiting with an error code.
function error_exit {
    echo
    echo "$@"
    exit 1
}
#Trap the killer signals so that we can exit with a good message.
trap "error_exit 'Received signal SIGHUP'" SIGHUP
trap "error_exit 'Received signal SIGINT'" SIGINT
trap "error_exit 'Received signal SIGTERM'" SIGTERM

#Alias the function so that it will print a message with the following format:
#prog-name(@line#): message
#We have to explicitly allow aliases, we do this because they make calling the
#function much easier (see example).
shopt -s expand_aliases
alias die='error_exit "Error ${0}(@`echo $(( $LINENO - 1 ))`):"'

Normalerweise rufe ich die Bereinigungsfunktion neben der Funktion error_exit auf, aber dies variiert von Skript zu Skript, sodass ich es weggelassen habe. Die Fallen erfassen die gemeinsamen Abschlusssignale und sorgen dafür, dass alles aufgeräumt wird. Der Alias ​​ist das, was die wahre Magie bewirkt. Ich überprüfe gerne alles auf Fehler. Im Allgemeinen rufe ich Programme in einem "Wenn!" Typanweisung. Durch Subtrahieren von 1 von der Zeilennummer teilt mir der Alias ​​mit, wo der Fehler aufgetreten ist. Es ist auch kinderleicht anzurufen und ziemlich idiotensicher. Unten finden Sie ein Beispiel (ersetzen Sie einfach / bin / false durch das, was Sie aufrufen möchten).

#This is an example useage, it will print out
#Error prog-name (@1): Who knew false is false.
if ! /bin/false ; then
    die "Who knew false is false."
fi
Michael Nooner
quelle
2
Können Sie die Aussage "Wir müssen Aliase explizit zulassen" erweitern ? Ich würde mir Sorgen machen, dass ein unerwartetes Verhalten eintreten könnte. Gibt es eine Möglichkeit, dasselbe mit einer geringeren Wirkung zu erreichen?
Blong
Ich brauche nicht $LINENO - 1. Ohne es richtig anzeigen.
Kyb
Kürzere Verwendungsbeispiel in Bash und false || die "hello death"
Zsh
6

Eine weitere Überlegung ist der zurückzugebende Exit-Code. Nur " 1" ist ziemlich Standard, obwohl es eine Handvoll reservierter Exit-Codes gibt, die bash selbst verwendet , und dieselbe Seite argumentiert, dass benutzerdefinierte Codes im Bereich von 64 bis 113 liegen sollten, um den C / C ++ - Standards zu entsprechen.

Sie können auch den Bitvektoransatz in Betracht ziehen, mountder für seine Exit-Codes verwendet wird:

 0  success
 1  incorrect invocation or permissions
 2  system error (out of memory, cannot fork, no more loop devices)
 4  internal mount bug or missing nfs support in mount
 8  user interrupt
16  problems writing or locking /etc/mtab
32  mount failure
64  some mount succeeded

ORWenn Sie die Codes zusammenfügen, kann Ihr Skript mehrere gleichzeitige Fehler anzeigen.

Yukondude
quelle
4

Ich verwende den folgenden Trap-Code, mit dem Fehler auch über Pipes und 'Time'-Befehle verfolgt werden können

#!/bin/bash
set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
function error() {
    JOB="$0"              # job name
    LASTLINE="$1"         # line of error occurrence
    LASTERR="$2"          # error code
    echo "ERROR in ${JOB} : line ${LASTLINE} with exit code ${LASTERR}"
    exit 1
}
trap 'error ${LINENO} ${?}' ERR
Olivier Delrieu
quelle
5
Das functionSchlüsselwort ist unentgeltlich POSIX-inkompatibel. Überlegen Sie, ob Sie Ihre Erklärung nur error() {ohne Nein abgeben möchten function.
Charles Duffy
5
${$?}sollte nur sein $?, oder ${?}wenn Sie darauf bestehen, unnötige Zahnspangen zu verwenden; Das Innere $ist falsch.
Charles Duffy
3
@ CharlesDuffy jetzt ist POSIX unentgeltlich GNU / Linux-inkompatibel (trotzdem nehme ich Ihren Punkt)
Croad Langshan
3

Ich habe benutzt

die() {
        echo $1
        kill $$
}

Vor; Ich denke, weil 'Exit' für mich aus irgendeinem Grund fehlgeschlagen ist. Die oben genannten Standardeinstellungen scheinen jedoch eine gute Idee zu sein.

pjz
quelle
Besser eine Fehlermeldung an STDERR senden, nein?
Ankostis
3

Das hat mir jetzt schon eine Weile gut getan. Es druckt Fehler- oder Warnmeldungen in rot, eine Zeile pro Parameter, und ermöglicht einen optionalen Exit-Code.

# Custom errors
EX_UNKNOWN=1

warning()
{
    # Output warning messages
    # Color the output red if it's an interactive terminal
    # @param $1...: Messages

    test -t 1 && tput setf 4

    printf '%s\n' "$@" >&2

    test -t 1 && tput sgr0 # Reset terminal
    true
}

error()
{
    # Output error messages with optional exit code
    # @param $1...: Messages
    # @param $N: Exit code (optional)

    messages=( "$@" )

    # If the last parameter is a number, it's not part of the messages
    last_parameter="${messages[@]: -1}"
    if [[ "$last_parameter" =~ ^[0-9]*$ ]]
    then
        exit_code=$last_parameter
        unset messages[$((${#messages[@]} - 1))]
    fi

    warning "${messages[@]}"

    exit ${exit_code:-$EX_UNKNOWN}
}
l0b0
quelle
3

Ich bin mir nicht sicher, ob dies für Sie hilfreich sein wird, aber ich habe einige der hier vorgeschlagenen Funktionen geändert, um die Überprüfung auf den Fehler (Code vom vorherigen Befehl beenden) darin aufzunehmen. Bei jeder "Prüfung" übergebe ich als Parameter auch die "Meldung", was der Fehler für Protokollierungszwecke ist.

#!/bin/bash

error_exit()
{
    if [ "$?" != "0" ]; then
        log.sh "$1"
        exit 1
    fi
}

Um es jetzt im selben Skript (oder in einem anderen, wenn ich es verwende export -f error_exit) aufzurufen, schreibe ich einfach den Namen der Funktion und übergebe eine Nachricht als Parameter wie folgt:

#!/bin/bash

cd /home/myuser/afolder
error_exit "Unable to switch to folder"

rm *
error_exit "Unable to delete all files"

Auf diese Weise konnte ich eine wirklich robuste Bash-Datei für einen automatisierten Prozess erstellen, die im Fehlerfall stoppt und mich benachrichtigt ( log.shwird das tun).

Nelson Rodriguez
quelle
2
Erwägen Sie die Verwendung der POSIX-Syntax zum Definieren von Funktionen - nur kein functionSchlüsselwort error_exit() {.
Charles Duffy
2
Gibt es einen Grund, warum Sie das nicht einfach tun cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares
@ Pierre-OlivierVares Kein besonderer Grund, || nicht zu verwenden. Dies war nur ein Auszug aus einem vorhandenen Code, und ich habe nach jeder betreffenden Zeile die Zeilen "Fehlerbehandlung" hinzugefügt. Einige sind sehr lang und es war nur sauberer, es auf einer separaten (unmittelbaren) Linie zu haben
Nelson Rodriguez
Sieht nach einer sauberen Lösung aus, beschwert sich Shell Check: github.com/koalaman/shellcheck/wiki/SC2181
mhulse
1

Dieser Trick ist nützlich, wenn Befehle oder Funktionen fehlen. Der Name der fehlenden Funktion (oder ausführbaren Datei) wird in $ _ übergeben

function handle_error {
    status=$?
    last_call=$1

    # 127 is 'command not found'
    (( status != 127 )) && return

    echo "you tried to call $last_call"
    return
}

# Trap errors.
trap 'handle_error "$_"' ERR
Orwellophil
quelle
Wäre $_in der Funktion nicht das gleiche wie $?? Ich bin mir nicht sicher, ob es einen Grund gibt, einen in der Funktion zu verwenden, aber nicht den anderen.
Ingyhere
1

Diese Funktion hat mir in letzter Zeit ziemlich gute Dienste geleistet:

action () {
    # Test if the first parameter is non-zero
    # and return straight away if so
    if test $1 -ne 0
    then
        return $1
    fi

    # Discard the control parameter
    # and execute the rest
    shift 1
    "$@"
    local status=$?

    # Test the exit status of the command run
    # and display an error message on failure
    if test ${status} -ne 0
    then
        echo Command \""$@"\" failed >&2
    fi

    return ${status}
}

Sie rufen es auf, indem Sie 0 oder den letzten Rückgabewert an den Namen des auszuführenden Befehls anhängen, damit Sie Befehle verketten können, ohne nach Fehlerwerten suchen zu müssen. Damit blockiert diese Anweisung:

command1 param1 param2 param3...
command2 param1 param2 param3...
command3 param1 param2 param3...
command4 param1 param2 param3...
command5 param1 param2 param3...
command6 param1 param2 param3...

Wird dies:

action 0 command1 param1 param2 param3...
action $? command2 param1 param2 param3...
action $? command3 param1 param2 param3...
action $? command4 param1 param2 param3...
action $? command5 param1 param2 param3...
action $? command6 param1 param2 param3...

<<<Error-handling code here>>>

Wenn einer der Befehle fehlschlägt, wird der Fehlercode einfach an das Ende des Blocks übergeben. Ich finde es nützlich, wenn Sie nicht möchten, dass nachfolgende Befehle ausgeführt werden, wenn ein früherer fehlschlägt, aber Sie möchten auch nicht, dass das Skript sofort beendet wird (z. B. innerhalb einer Schleife).

xarxziux
quelle
0

Die Verwendung von Trap ist nicht immer eine Option. Wenn Sie beispielsweise eine wiederverwendbare Funktion schreiben, die eine Fehlerbehandlung erfordert und von jedem Skript aus aufgerufen werden kann (nachdem Sie die Datei mit Hilfsfunktionen bezogen haben), kann diese Funktion nichts über die Beendigungszeit des äußeren Skripts annehmen. Das macht die Verwendung von Fallen sehr schwierig. Ein weiterer Nachteil der Verwendung von Traps ist die schlechte Kompositionsfähigkeit, da Sie das Risiko eingehen, frühere Traps zu überschreiben, die möglicherweise früher in der Aufruferkette eingerichtet wurden.

Es gibt einen kleinen Trick, mit dem Fehler ohne Fallen richtig behandelt werden können. Wie Sie vielleicht bereits aus anderen Antworten wissen, set -efunktioniert dies nicht in Befehlen, wenn Sie den ||Operator danach verwenden, selbst wenn Sie sie in einer Subshell ausführen. zB würde das nicht funktionieren:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Der ||Bediener muss jedoch verhindern, dass er vor der Bereinigung von der äußeren Funktion zurückkehrt. Der Trick besteht darin, den inneren Befehl im Hintergrund auszuführen und dann sofort darauf zu warten. Daswait eingebaute System gibt den Exit-Code des inneren Befehls zurück, und jetzt verwenden Sie ||after waitund nicht die innere Funktion, sodass sie set -ein letzterer ordnungsgemäß funktioniert:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Hier ist die generische Funktion, die auf dieser Idee aufbaut. Es sollte in allen POSIX-kompatiblen Shells funktionieren, wenn Sie es entfernenlocal Schlüsselwörter , dh alle local x=ydurch nur ersetzen x=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

Anwendungsbeispiel:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

Beispiel ausführen:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

Das einzige, was Sie bei der Verwendung dieser Methode beachten müssen, ist, dass alle Änderungen der Shell-Variablen, die von dem Befehl, an den Sie übergeben, vorgenommen wurden run, nicht an die aufrufende Funktion weitergegeben werden, da der Befehl in einer Subshell ausgeführt wird.

sam.kozin
quelle