Obsługa błędów w Bash

240

Jaka jest twoja ulubiona metoda radzenia sobie z błędami w Bash? Najlepszy przykład obsługi błędów, który znalazłem w Internecie, napisał William Shotts, Jr, na stronie http://www.linuxcommand.org .

Sugeruje użycie następującej funkcji do obsługi błędów w Bash:

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

Czy masz lepszą procedurę obsługi błędów używaną w skryptach Bash?

Noob
źródło
1
Zobacz szczegółową odpowiedź: Podnieś błąd w skrypcie Bash .
codeforester
1
Zobacz implementację rejestrowania i obsługi błędów tutaj: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Odpowiedzi:

154

Użyj pułapki!

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

... wtedy, gdy utworzysz plik tymczasowy:

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

i $temp_foozostaną usunięte przy wyjściu, a bieżący numer linii zostanie wydrukowany. ( set -ezapewni również zachowanie wyjścia po błędzie, chociaż wiąże się to z poważnymi zastrzeżeniami i osłabia przewidywalność i przenośność kodu).

Możesz albo pozwolić pułapce errorna ciebie zadzwonić (w takim przypadku używa domyślnego kodu wyjścia 1 i bez komunikatu) lub zadzwonić do siebie i podać wyraźne wartości; na przykład:

error ${LINENO} "the foobar failed" 2

wyjdzie ze statusem 2 i da wyraźny komunikat.

Charles Duffy
źródło
4
@draemon zamiana wielkich liter jest celowa. Wielkie litery są konwencjonalne tylko dla wbudowanych powłok i zmiennych środowiskowych - użycie małych liter do wszystkiego innego zapobiega konfliktom przestrzeni nazw. Zobacz także stackoverflow.com/questions/673055/…
Charles Duffy
1
zanim ponownie go złamiesz, sprawdź swoją zmianę. Konwencje są dobrą rzeczą, ale są drugorzędne w stosunku do funkcjonującego kodu.
Draemon
3
@Draemon, właściwie się nie zgadzam. Oczywiście uszkodzony kod zostaje zauważony i naprawiony. Złe praktyki, ale przeważnie działający kod żyje wiecznie (i zostaje rozpowszechniony).
Charles Duffy
1
ale nie zauważyłeś. Uszkodzony kod zostaje zauważony, ponieważ funkcjonowanie kodu jest podstawową kwestią.
Draemon,
5
nie jest to całkowicie nieuzasadnione ( stackoverflow.com/a/10927223/26334 ) i jeśli kod jest już niezgodny z POSIX, usunięcie słowa kluczowego funkcji nie powoduje, że może on już działać pod POSIX sh, ale moim głównym celem było to, że „ ve (IMO) zdewaluowało odpowiedź, osłabiając zalecenie użycia zestawu -e. Stackoverflow nie polega na „twoim” kodzie, chodzi o uzyskanie najlepszych odpowiedzi.
Draemon
123

To dobre rozwiązanie. Chciałem tylko dodać

set -e

jako podstawowy mechanizm błędu. Natychmiast zatrzyma skrypt, jeśli proste polecenie zawiedzie. Myślę, że powinno to być zachowanie domyślne: ponieważ takie błędy prawie zawsze oznaczają coś nieoczekiwanego, wykonywanie następujących poleceń nie jest tak naprawdę rozsądne.

Bruno De Fraine
źródło
29
set -enie jest bez gotchas: zobacz mywiki.wooledge.org/BashFAQ/105 dla kilku.
Charles Duffy
3
@CharlesDuffy, niektóre z gotch można pokonać za pomocąset -o pipefail
płyt
7
@CharlesDuffy Dzięki za wskazanie gotchas; ogólnie jednak nadal uważam, że set -ema wysoki stosunek korzyści do kosztów.
Bruno De Fraine,
3
@BrunoDeFraine Używam set -esiebie, ale wielu innych stałych bywalców w irc.freenode.org # bash odradza (dość silnie) przeciw temu. Przynajmniej te problemy powinny być dobrze zrozumiane.
Charles Duffy,
3
ustaw -e -o pipefail -u # i wiedz, co robisz
Sam Watkins
78

Czytanie wszystkich odpowiedzi na tej stronie bardzo mnie zainspirowało.

Oto moja wskazówka:

zawartość pliku: 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



Przykład użycia:
zawartość pliku: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

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

exit 0


Bieganie:

bash trap-test.sh

Wynik:

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!


Jak widać na poniższym zrzucie ekranu, dane wyjściowe są kolorowe, a komunikat błędu pojawia się w używanym języku.

wprowadź opis zdjęcia tutaj

Luca Borrione
źródło
3
ta rzecz jest niesamowita ... powinieneś stworzyć dla niej projekt github, aby ludzie mogli łatwo wprowadzać ulepszenia i wnosić wkład. Połączyłem go z log4bash i razem tworzy potężną env do tworzenia dobrych skryptów bash.
Dominik Dorn,
1
FYI - test ${#g_libs[@]} == 0nie jest zgodny z POSIX (test POSIX obsługuje =porównania ciągów lub porównania -eqnumeryczne, ale nie ==wspominając o braku tablic w POSIX), a jeśli nie próbujesz być zgodny z POSIX, dlaczego w czy testw ogóle używasz świata, a nie kontekstu matematycznego? (( ${#g_libs[@]} == 0 ))jest przecież łatwiejszy do odczytania.
Charles Duffy
2
@Luca - to jest naprawdę świetne! Twoje zdjęcie zainspirowało mnie do stworzenia własnej implementacji tego, co posuwa się jeszcze o kilka kroków dalej. Opublikowałem to w mojej odpowiedzi poniżej .
niieani
3
Bravissimo !! To doskonały sposób na debugowanie skryptu. Grazie mille Jedyne, co dodałem, to czek na OS X w ten sposób: case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
SaxDaddy
1
Trochę bezwstydna wtyczka, ale wzięliśmy ten fragment, wyczyściliśmy go, dodaliśmy więcej funkcji, ulepszyliśmy formatowanie wyjściowe i zwiększyliśmy kompatybilność z POSIX (działa zarówno w systemie Linux, jak i OSX). Został opublikowany jako część Privex ShellCore na Github: github.com/Privex/shell-core
Someguy123
22

Równoważną alternatywą dla „zestawu -e” jest

set -o errexit

To sprawia, że ​​znaczenie flagi jest bardziej wyraźne niż tylko „-e”.

Losowe dodawanie: aby tymczasowo wyłączyć flagę i powrócić do ustawień domyślnych (kontynuowanie wykonywania niezależnie od kodów wyjścia), wystarczy użyć

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

Wyklucza to prawidłową obsługę błędów wspomnianą w innych odpowiedziach, ale jest szybkie i skuteczne (podobnie jak bash).

Ben Scholbrock
źródło
1
używanie $(foo)na gołej linii, a nie tylko, foojest zwykle błędną rzeczą. Po co go promować, podając go jako przykład?
Charles Duffy,
20

Zainspirowany przedstawionymi tutaj pomysłami, opracowałem czytelny i wygodny sposób radzenia sobie z błędami w skryptach bash w moim bashowym projekcie .

Po prostu pozyskując bibliotekę, otrzymujesz następujące rzeczy po wyjęciu z pudełka (tj. Zatrzyma to wykonywanie każdego błędu, jakbyś używał set -edzięki trapon ERRi trochę bash-fu ):

Obsługa błędów bash-oo-framework

Istnieje kilka dodatkowych funkcji, które pomagają radzić sobie z błędami, takich jak try-catch lub słowo kluczowe throw , które pozwalają przerwać wykonywanie w punkcie, aby zobaczyć ślad. Dodatkowo, jeśli terminal go obsługuje, wyrzuca emoji z linii energetycznej, koloryzuje części danych wyjściowych w celu zapewnienia dużej czytelności i podkreśla metodę, która spowodowała wyjątek w kontekście wiersza kodu.

Minusem jest to, że - nie jest przenośny - kod działa w trybie bash, prawdopodobnie> = 4 (ale wyobrażam sobie, że można go przenieść z pewnym wysiłkiem w celu bash 3).

Kod jest podzielony na wiele plików dla lepszej obsługi, ale zainspirował mnie pomysł śledzenia wstecznego z powyższej odpowiedzi Luca Borrione .

Aby przeczytać więcej lub spojrzeć na źródło, zobacz GitHub:

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

niieani
źródło
Znajduje się to w projekcie Bash Object Oriented Framework . ... Na szczęście ma tylko 7,4 tys. LOC (zgodnie z GLOC ). OOP - Ból obiektowy?
ingyhere
@ tutaj jest bardzo modułowy (i przyjazny do usuwania), więc możesz użyć części wyjątków tylko wtedy, gdy po to przyszedłeś;)
niieani
11

Wolę coś naprawdę łatwego do połączenia. Używam więc czegoś, co wygląda trochę skomplikowane, ale jest łatwe w użyciu. Zwykle po prostu kopiuję i wklejam poniższy kod do swoich skryptów. Wyjaśnienie następuje po kodzie.

#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 ))`):"'

Zwykle wywołuję funkcję czyszczenia po stronie funkcji error_exit, ale różni się ona w zależności od skryptu, więc ją pominąłem. Pułapki wychwytują typowe sygnały kończące i upewniają się, że wszystko zostanie wyczyszczone. Alias ​​to prawdziwa magia. Lubię sprawdzać wszystko pod kątem awarii. Ogólnie rzecz biorąc, programy nazywam „jeśli!” instrukcja typu. Odejmując 1 od numeru linii, alias powie mi, gdzie wystąpiła awaria. Można również zadzwonić i jest to idiota. Poniżej znajduje się przykład (wystarczy zastąpić / bin / false tym, co chcesz wywołać).

#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
źródło
2
Czy możesz rozwinąć zdanie „Musimy wyraźnie zezwolić na aliasy” ? Martwiłbym się, że może to spowodować nieoczekiwane zachowanie. Czy istnieje sposób, aby osiągnąć to samo przy mniejszym wpływie?
blong
Nie potrzebuję $LINENO - 1. Pokaż poprawnie bez tego.
Kyb
Krótszy przykład użycia w bash i zshfalse || die "hello death"
kyb
6

Innym zagadnieniem jest kod wyjścia do zwrócenia. Po prostu „ 1” jest dość standardem, chociaż istnieje garść zarezerwowanych kodów wyjścia, których używa bash , a ta sama strona twierdzi, że kody zdefiniowane przez użytkownika powinny być w zakresie 64-113, aby były zgodne ze standardami C / C ++.

Możesz również rozważyć podejście oparte na wektorze bitowym, które mountwykorzystuje swoje kody wyjścia:

 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

OR-synchronizowanie kodów razem pozwala twojemu skryptowi zasygnalizować wiele jednoczesnych błędów.

yukondude
źródło
4

Używam następującego kodu pułapki, pozwala także na śledzenie błędów poprzez potoki i polecenia „time”

#!/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
źródło
5
functionSłowo kluczowe jest bezinteresownie POSIX-niekompatybilne. Zastanów się nad tym, aby złożyć oświadczenie sprawiedliwie error() {, bez functionwcześniejszego.
Charles Duffy
5
${$?}powinno być $?, lub ${?}jeśli nalegasz na użycie niepotrzebnych aparatów ortodontycznych; wnętrze $jest złe.
Charles Duffy
3
@CharlesDuffy do tej pory POSIX jest nieobowiązkowo niezgodny z GNU / Linuksem (nadal uważam, że masz
rację
3

Użyłem

die() {
        echo $1
        kill $$
}

przed; myślę, że „wyjście” z jakiegoś powodu zawiodło mnie. Powyższe wartości domyślne wydają się jednak dobrym pomysłem.

pjz
źródło
Lepiej wyślij komunikat o błędzie do STDERR, nie?
ankostis
3

To służył mi dobrze przez jakiś czas teraz. Drukuje komunikaty o błędach lub ostrzeżenia na czerwono, jeden wiersz na parametr i umożliwia opcjonalny kod wyjścia.

# 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
źródło
3

Nie jestem pewien, czy to ci pomoże, ale zmodyfikowałem tutaj niektóre z sugerowanych funkcji, aby uwzględnić w nim sprawdzenie błędu (kod wyjścia z poprzedniego polecenia). Przy każdym „sprawdzaniu” przekazuję również jako parametr „komunikat” o błędzie do celów logowania.

#!/bin/bash

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

Teraz, aby wywołać go w tym samym skrypcie (lub w innym, jeśli go używam export -f error_exit), po prostu piszę nazwę funkcji i przekazuję komunikat jako parametr, jak poniżej:

#!/bin/bash

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

rm *
error_exit "Unable to delete all files"

Dzięki temu udało mi się stworzyć naprawdę solidny plik bash dla niektórych zautomatyzowanych procesów, który zatrzyma się w przypadku błędów i powiadomi mnie ( log.shzrobi to)

Nelson Rodriguez
źródło
2
Rozważ użycie składni POSIX do definiowania funkcji - bez functionsłowa kluczowego, tylko error_exit() {.
Charles Duffy,
2
czy istnieje powód, dla którego po prostu tego nie robisz cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares
@ Pierre-OlivierVares Nie ma szczególnego powodu, aby nie używać ||. To był tylko fragment istniejącego kodu i właśnie dodałem wiersze „obsługi błędów” po każdym wierszu dotyczącym. Niektóre są bardzo długie i czystsze było mieć je na osobnej (bezpośredniej) linii
Nelson Rodriguez
Wygląda to na czyste rozwiązanie, ale sprawdzanie powłoki narzeka: github.com/koalaman/shellcheck/wiki/SC2181
mhulse
1

Ta sztuczka jest przydatna w przypadku brakujących poleceń lub funkcji. Nazwa brakującej funkcji (lub pliku wykonywalnego) zostanie przekazana w $ _

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
Orwellophile
źródło
Nie $_byłby dostępny w funkcji tak samo jak $?? Nie jestem pewien, czy istnieje jakikolwiek powód, aby używać jednego w funkcji, ale nie drugiego.
ingyhere
1

Ta funkcja służy mi ostatnio całkiem dobrze:

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}
}

Wywołujesz go, dodając 0 lub ostatnią zwracaną wartość do nazwy polecenia do uruchomienia, dzięki czemu możesz łączyć polecenia bez konieczności sprawdzania wartości błędów. Dzięki temu ten blok instrukcji:

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

Staje się to:

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>>>

Jeśli którekolwiek z poleceń nie powiedzie się, kod błędu jest po prostu przekazywany na koniec bloku. Uważam, że jest to użyteczne, gdy nie chcesz, aby kolejne polecenia były wykonywane, jeśli poprzednie nie powiodło się, ale nie chcesz też, aby skrypt natychmiast zakończył działanie (na przykład w pętli).

xarxziux
źródło
0

Korzystanie z pułapki nie zawsze jest opcją. Na przykład, jeśli piszesz jakąś funkcję wielokrotnego użytku, która wymaga obsługi błędów i którą można wywołać z dowolnego skryptu (po uzyskaniu pliku z funkcjami pomocniczymi), funkcja ta nie może zakładać niczego o czasie wyjścia zewnętrznego skryptu, co bardzo utrudnia korzystanie z pułapek. Kolejną wadą korzystania z pułapek jest zła kompozycja, ponieważ istnieje ryzyko zastąpienia poprzedniej pułapki, która może zostać wcześniej ustawiona w łańcuchu dzwoniącego.

Istnieje mała sztuczka, której można użyć do prawidłowej obsługi błędów bez pułapek. Jak zapewne wiesz z innych odpowiedzi, set -enie działa wewnątrz poleceń, jeśli używasz ||operatora po nich, nawet jeśli uruchamiasz je w podpowłoce; np. to nie zadziała:

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

Ale ||operator jest potrzebny, aby zapobiec powrotowi z funkcji zewnętrznej przed czyszczeniem. Sztuką jest uruchomienie wewnętrznego polecenia w tle, a następnie natychmiast na niego poczekać. waitWbudowane zwróci kod wyjścia polecenia wewnętrznej, a teraz używasz ||po wait, a nie funkcji wewnętrznej, więc set -edziała prawidłowo wewnątrz tego ostatniego:

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

Oto ogólna funkcja oparta na tym pomyśle. Powinien on działać we wszystkich powłokach zgodnych z POSIX, jeśli usuniesz localsłowa kluczowe, tzn. Zastąpisz local x=yje tylko 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
}

Przykład użycia:

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

Uruchamianie przykładu:

$ ./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

Jedyną rzeczą, o której musisz wiedzieć, korzystając z tej metody, jest to, że wszystkie modyfikacje zmiennych Shell wykonane z komendy, którą przekazujesz run, nie będą propagowane do funkcji wywołującej, ponieważ komenda działa w podpowłoce.

sam.kozin
źródło