Обработка ошибок в 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 ]

2 голосов
/ 14 сентября 2012

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

#!/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 сделает это)

1 голос
/ 25 января 2017

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

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

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

0 голосов
/ 11 января 2016

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

Есть небольшая хитрость, которую можно использовать для правильной обработки ошибок без ловушек. Как вы, возможно, уже знаете из других ответов, 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, не будут распространяться на вызывающую функцию, потому что команда выполняется в подоболочке.

0 голосов
/ 08 августа 2013

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

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