Обработка ошибок в Bash - PullRequest
211 голосов
/ 15 сентября 2008

Какой ваш любимый метод обработки ошибок в Bash? Лучший пример обработки ошибок, которые я нашел в Интернете, был написан Уильямом Шоттсом, младшим по адресу http://www.linuxcommand.org.

Он предлагает использовать следующую функцию для обработки ошибок в 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."

Есть ли у вас лучшая процедура обработки ошибок, которую вы используете в скриптах Bash?

Ответы [ 14 ]

145 голосов
/ 09 октября 2008

Используйте ловушку!

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

... затем при создании временного файла:

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

и $temp_foo будут удалены при выходе, и будет напечатан номер текущей строки. (set -e также даст вам поведение выхода при ошибке, , хотя оно сопровождается серьезными предостережениями и ослабляет предсказуемость и переносимость кода).

Вы можете либо позволить ловушке вызвать для вас error (в этом случае он использует код завершения по умолчанию 1 и никакого сообщения), либо вызвать его самостоятельно и предоставить явные значения; например:

error ${LINENO} "the foobar failed" 2

выйдет со статусом 2 и выдаст явное сообщение.

112 голосов
/ 16 сентября 2008

Это прекрасное решение. Я просто хотел добавить

set -e

как элементарный механизм ошибок. Он немедленно остановит ваш скрипт, если простая команда не удастся. Я думаю, что это должно было быть поведением по умолчанию: поскольку такие ошибки почти всегда означают что-то неожиданное, на самом деле не является «нормальным» продолжать выполнение следующих команд.

71 голосов
/ 27 октября 2012

Чтение всех ответов на этой странице меня очень вдохновило.

Итак, вот мой совет:

содержимое файла: 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



Пример использования:
содержимое файла: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

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

exit 0


Запуск:

bash trap-test.sh

Выход:

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!


Как вы можете видеть на скриншоте ниже, выходные данные окрашены, и сообщение об ошибке появляется на используемом языке.

enter image description here

22 голосов
/ 10 декабря 2009

Эквивалентная альтернатива "set -e" -

set -o errexit

Это делает значение флага несколько понятнее, чем просто "-e".

Случайное добавление: чтобы временно отключить флаг и вернуться к значению по умолчанию (для продолжения выполнения независимо от кодов выхода), просто используйте

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

Это исключает правильную обработку ошибок, упомянутую в других ответах, но быстро и эффективно (как bash).

19 голосов
/ 04 мая 2015

Вдохновленный представленными здесь идеями, я разработал удобный и понятный способ обработки ошибок в сценариях bash в моем проекте bash шаблон .

Просто получая библиотеку, вы получаете из коробки следующее (т.е. она остановит выполнение при любой ошибке, как если бы вы использовали set -e благодаря trap на ERR и некоторым bash- фу ):

bash-oo-framework error handling

Существуют некоторые дополнительные функции, которые помогают обрабатывать ошибки, такие как try and catch или ключевое слово throw , которое позволяет прервать выполнение в точке, чтобы увидеть обратную трассировку. Кроме того, если терминал поддерживает его, он выплевывает смайлы Powerline, окрашивает части вывода для большой читабельности и подчеркивает метод, который вызвал исключение в контексте строки кода.

Недостатком является то, что он не переносим - код работает на bash, вероятно,> = 4 (но я мог бы предположить, что он может быть перенесен с некоторым усилием на bash 3).

Код разделен на несколько файлов для лучшей обработки, но меня вдохновила идея обратного следа из ответа, приведенного выше Лукой Боррионе .

Чтобы узнать больше или взглянуть на источник, см. GitHub:

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

11 голосов
/ 20 ноября 2009

Я предпочитаю что-то действительно простое для вызова. Поэтому я использую то, что выглядит немного сложным, но простым в использовании. Я обычно просто копирую и вставляю приведенный ниже код в мои скрипты. Объяснение следует за кодом.

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

Я обычно помещаю вызов функции очистки в сторону функции error_exit, но это варьируется от сценария к сценарию, поэтому я не учел его. Ловушки улавливают общие завершающие сигналы и следят за тем, чтобы все было очищено. Псевдоним - это то, что делает настоящую магию. Мне нравится проверять все на наличие ошибок. Так что в общем я называю программы "если!" Тип заявления. Вычитая 1 из номера строки, псевдоним скажет мне, где произошла ошибка. Это также очень просто, и в значительной степени идиотское доказательство. Ниже приведен пример (просто замените / bin / false на то, что вы собираетесь вызывать).

#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
6 голосов
/ 08 октября 2008

Еще одним соображением является код возврата для возврата. Просто «1» довольно стандартно, хотя есть несколько зарезервированных кодов выхода, которые bash использует , и на той же странице утверждается, что определяемые пользователем коды должны находиться в диапазоне 64-113, чтобы соответствовать стандартам C / C ++.

Вы могли бы также рассмотреть подход битового вектора, который mount использует для своих кодов выхода:

 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 - объединение кодов позволяет вашему скрипту сигнализировать о нескольких одновременных ошибках.

4 голосов
/ 19 июля 2012

Я использую следующий код прерывания, он также позволяет отслеживать ошибки по каналам и командам '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
3 голосов
/ 09 июня 2011

Это уже некоторое время хорошо мне служило. Он печатает сообщения об ошибках или предупреждения красным цветом, по одной строке на параметр, и допускает необязательный код выхода.

# 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}
}
3 голосов
/ 15 сентября 2008

Я использовал

die() {
        echo $1
        kill $$
}

раньше; я думаю, потому что «выход» для меня почему-то не удался. Вышеуказанные значения по умолчанию кажутся хорошей идеей.

...