Обработка ошибок в Bash

240

Какой ваш любимый метод для обработки ошибок в 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?

Noob
источник
1
Посмотрите подробный ответ: выведите ошибку в скрипте Bash .
codeforester
1
Смотрите реализацию ведения журналов и обработки ошибок здесь: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester,

Ответы:

154

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

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 и выдаст явное сообщение.

Чарльз Даффи
источник
4
@draemon переменная капитализация является преднамеренной. All-caps обычен только для встроенных командных оболочек и переменных окружения - использование строчных букв для всего остального предотвращает конфликты пространства имен. См. Также stackoverflow.com/questions/673055/…
Чарльз Даффи
1
прежде чем сломать его снова, проверьте свои изменения. Соглашения - это хорошо, но они вторичны по отношению к функционирующему коду.
Draemon
3
@ Дракон, я на самом деле не согласен. Очевидно, что нарушенный код замечен и исправлен. Плохие практики, но в основном работающий код, живут вечно (и распространяются).
Чарльз Даффи
1
но ты не заметил Сломанный код замечают, потому что основной задачей является функционирование кода.
Draemon
5
это не совсем бесполезно ( stackoverflow.com/a/10927223/26334 ) и если код уже несовместим с POSIX, удаление ключевого слова function больше не делает его способным работать под POSIX sh, но моя главная мысль состояла в том, что вы ' ve (IMO) обесценил ответ, ослабив рекомендацию использовать set -e. Stackoverflow - это не «ваш» код, а наилучшие ответы.
Draemon
123

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

set -e

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

Бруно Де Фрейн
источник
29
set -eне без ошибок : см. mywiki.wooledge.org/BashFAQ/105 для нескольких.
Чарльз Даффи
3
@CharlesDuffy, некоторые из ошибок можно преодолеть с помощьюset -o pipefail
конфорки
7
@CharlesDuffy Спасибо, что указали на ошибки; в целом, я все еще думаю, что set -eимеет высокое соотношение выгод и затрат.
Бруно Де Фрейн,
3
@BrunoDeFraine Я использую set -eсебя, но ряд других постоянных в irc.freenode.org # bash советуют (в довольно сильных выражениях) против этого. Как минимум, рассматриваемые ошибки должны быть хорошо поняты.
Чарльз Даффи
3
установите -e -o pipefail -u # и узнайте, что вы делаете
Сэм Уоткинс
78

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

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

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


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

введите описание изображения здесь

Лука Боррионе
источник
3
это здорово ... вам нужно создать для него проект github, чтобы люди могли легко вносить улучшения и вносить свой вклад. Я объединил его с log4bash, и вместе он создает мощную среду для создания хороших скриптов bash.
Доминик Дорн
1
FYI - test ${#g_libs[@]} == 0не POSIX-совместимый (тест POSIX поддерживает =сравнение строк или -eqчисловые сравнения, но не ==, не говоря уже об отсутствии массивов в POSIX), и если вы не пытаетесь быть POSIX-совместимым, почему в мир вы используете testвообще, а не математический контекст? (( ${#g_libs[@]} == 0 ))В конце концов, легче читать.
Чарльз Даффи
2
@ Лука - это действительно здорово! Ваша картина вдохновила меня на создание собственной реализации этого, которая продвинулась еще дальше. Я разместил это в своем ответе ниже .
niieani
3
Брависсимо !! Это отличный способ отладки скрипта. Grazie mille Единственное, что я добавил, это проверка на OS X, подобная этой: 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
Немного бесстыдного самостоятельного подключения, но мы взяли этот фрагмент, очистили его, добавили больше функций, улучшили форматирование вывода и сделали его более совместимым с POSIX (работает как в Linux, так и в OSX). Он публикуется как часть Privex ShellCore на Github: github.com/Privex/shell-core
Someguy123
22

Эквивалентная альтернатива "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).

Ben Scholbrock
источник
1
использование $(foo)на голой линии, а не просто fooобычно неправильно. Зачем продвигать это, приводя в качестве примера?
Чарльз Даффи
20

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

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

Обработка ошибок в bash-oo-framework

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

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

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

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

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

об.
источник
Это внутри проекта Bash Object Oriented Framework . ... К счастью, он имеет только 7.4k LOC (согласно GLOC ). ООП - Объектно-ориентированная боль?
ingyhere
@ingyhere очень модульный (и дружественный к удалению), поэтому вы можете использовать только часть исключений, если это то, для чего вы пришли;)
niieani
11

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

#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
Майкл Нунер
источник
2
Можете ли вы расширить выражение «Мы должны явно разрешить псевдонимы» ? Я бы волновался, что это может привести к неожиданному поведению. Есть ли способ добиться того же с меньшим воздействием?
Блонг
Я не нужно $LINENO - 1. Покажи правильно без этого.
kyb
Более короткий пример использования в bash и zshfalse || die "hello death"
kyb
6

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

yukondude
источник
4

Я использую следующий код ловушки, он также позволяет отслеживать ошибки через каналы и команды времени

#!/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
источник
5
functionКлючевое слово беспричинно POSIX-несовместимое. Подумайте о том, чтобы сделать свое заявление просто error() {, без functionнего.
Чарльз Даффи
5
${$?}должно быть $?, или ${?}если вы настаиваете на использовании ненужных скобок; внутреннее $неправильно.
Чарльз Даффи
3
@CharlesDuffy к настоящему времени, POSIX безвозмездно несовместим с GNU / Linux (все же, я понимаю вашу точку зрения)
Croad Langshan
3

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

die() {
        echo $1
        kill $$
}

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

pjz
источник
Лучше отправить сообщение об ошибке в STDERR, нет?
ankostis
3

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

# 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
источник
3

Не уверен, будет ли это полезно для вас, но я изменил некоторые из предложенных здесь функций, чтобы включить в них проверку на наличие ошибки (код выхода из предыдущей команды). На каждую «проверку» я также передаю в качестве параметра «сообщение» о том, что это за ошибка для целей регистрации.

#!/bin/bash

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

Теперь, чтобы вызвать его в том же скрипте (или в другом, если я его использую export -f error_exit), я просто пишу имя функции и передаю сообщение в качестве параметра, например так:

#!/bin/bash

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

rm *
error_exit "Unable to delete all files"

Используя это, я смог создать действительно надежный bash-файл для некоторого автоматизированного процесса, и он остановится в случае ошибок и log.shсообщит мне ( сделает это)

Нельсон Родригес
источник
2
Попробуйте использовать синтаксис POSIX для определения функций - без functionключевых слов, просто error_exit() {.
Чарльз Даффи
2
есть ли причина, почему вы не просто делаете cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Пьер-Оливье Варес
@ Pierre-OlivierVares Нет особых причин не использовать ||. Это была просто выдержка из существующего кода, и я просто добавил строки «обработки ошибок» после каждой соответствующей строки. Некоторые из них очень длинные, и было просто чище иметь их на отдельной (немедленной) линии
Нельсон Родригес
Выглядит как чистое решение, но проверка оболочки жалуется: github.com/koalaman/shellcheck/wiki/SC2181
mhulse
1

Этот трюк полезен для пропущенных команд или функций. Имя отсутствующей функции (или исполняемого файла) будет передано в $ _

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
источник
Не $_будет доступен в функции так же, как $?? Я не уверен, что есть какая-либо причина использовать один в функции, но не другой.
ingyhere
1

Эта функция в последнее время довольно хорошо меня обслуживает:

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

Вы вызываете его, добавляя 0 или последнее возвращаемое значение к имени команды для запуска, так что вы можете объединять команды без необходимости проверять наличие ошибок. С этим, этот блок заявления:

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

Становится так:

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

Если какая-либо из команд не выполняется, код ошибки просто передается в конец блока. Я нахожу это полезным, когда вы не хотите, чтобы последующие команды выполнялись, если предыдущая не удалась, но вы также не хотите, чтобы скрипт сразу выходил (например, внутри цикла).

xarxziux
источник
0

Использование ловушки не всегда вариант. Например, если вы пишете какую-то повторно используемую функцию, которая требует обработки ошибок и которая может быть вызвана из любого скрипта (после получения файла вспомогательными функциями), эта функция не может предполагать время выхода внешнего скрипта, что делает использование ловушек очень сложным. Другим недостатком использования прерываний является плохая компоновка, так как вы рискуете перезаписать предыдущее прерывание, которое может быть установлено ранее в цепочке вызывающих.

Есть небольшая хитрость, которую можно использовать для правильной обработки ошибок без ловушек. Как вы уже знаете из других ответов, set -eне работает внутри команд, если вы используете ||оператор после них, даже если вы запускаете их в подоболочке; например, это не сработает:

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

Но ||оператор необходим для предотвращения возврата из внешней функции перед очисткой. Хитрость заключается в том, чтобы запустить внутреннюю команду в фоновом режиме, а затем сразу же дождаться ее. waitВстроенный возвращают код завершения внутренней команды, и теперь вы используете ||после того wait, а не внутренней функции, поэтому set -eработает должным образом внутри последний:

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

Вот общая функция, основанная на этой идее. Он должен работать во всех POSIX-совместимых оболочках, если вы удаляете localключевые слова, т.е. заменяете все local x=yпросто 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
}

Пример использования:

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

Выполнение примера:

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

Единственное, что вам нужно знать при использовании этого метода, это то, что все изменения переменных оболочки, сделанные из команды, которую вы передаете run, не будут распространяться на вызывающую функцию, потому что команда выполняется в подоболочке.

оборота сам.козин
источник