你最喜欢在Bash中处理错误的方法是什么? 我在网上找到的处理错误的最好的例子是William Shotts, Jr在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脚本中是否有更好的错误处理例程?


当前回答

使用陷阱并不总是一个选择。例如,如果您正在编写某种需要错误处理的可重用函数,并且可以从任何脚本调用(在使用helper函数源文件之后),则该函数不能假设外部脚本的退出时间,这使得使用trap非常困难。使用陷阱的另一个缺点是糟糕的可组合性,因为您可能会覆盖之前可能在调用者链中较早设置的陷阱。

有一个小技巧可以用来在没有陷阱的情况下进行正确的错误处理。正如你可能已经从其他答案中知道的,如果你在命令后使用||操作符,set -e在命令中不起作用,即使你在子shell中运行它们;例如,这行不通:

#!/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兼容的shell中工作,即用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

在使用此方法时,您需要注意的唯一一件事是,您传递给运行的命令对Shell变量所做的所有修改都不会传播到调用函数,因为该命令运行在子Shell中。

其他回答

“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一样)。

我更喜欢简单的称呼。所以我用了一些看起来有点复杂,但很容易使用的东西。我通常只是复制并粘贴下面的代码到我的脚本。代码后面有解释。

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

I usually put a call to the cleanup function in side the error_exit function, but this varies from script to script so I left it out. The traps catch the common terminating signals and make sure everything gets cleaned up. The alias is what does the real magic. I like to check everything for failure. So in general I call programs in an "if !" type statement. By subtracting 1 from the line number the alias will tell me where the failure occurred. It is also dead simple to call, and pretty much idiot proof. Below is an example (just replace /bin/false with whatever you are going to call).

#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

受本文介绍的思想的启发,我在bash样板项目中开发了一种可读且方便的方法来处理bash脚本中的错误。

通过简单地获取库,你可以得到以下结果(即它会在任何错误时停止执行,就像使用set -e一样,这要感谢ERR上的陷阱和一些bash-fu):

还有一些额外的特性可以帮助处理错误,比如try和catch,或者throw关键字,它们允许您在某个点中断执行以查看回溯。此外,如果终端支持它,它会输出电力线表情符号,为输出的部分颜色以提高可读性,并在代码行上下文中强调导致异常的方法。

缺点是—它不可移植—代码在bash中工作,可能仅>= 4(但我可以想象它可以通过一些努力移植到bash 3)。

为了更好地处理,代码被分离到多个文件中,但我受到了Luca Borrione上面回答的回溯思想的启发。

要阅读更多或查看源代码,请参阅GitHub:

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

有时set -e, trap ERR,set -o,set -o pipefail和set -o errtrace不能正常工作,因为它们试图在shell中添加自动错误检测。这在实践中并不奏效。

在我看来,你应该编写自己的错误检查代码,而不是使用set -e和其他东西。如果您明智地使用set -e,请注意潜在的陷阱。

为了避免在运行代码时出现错误,您可以使用exec 1>/dev/null或exec 2>/dev/null Linux中的/dev/null是一个空设备文件。这将丢弃写入它的任何内容,并在读取时返回EOF。您可以在命令末尾使用此命令

对于try/catch,您可以使用&&或||来实现类似的行为 Use可以像这样使用&&

{ # try

    command &&
    # your command 

} || { 
    # catch exception 
}

或者你可以用if else:

if [[ Condition ]]; then
    # if true
else
    # if false
fi

$ ?显示最后一个命令的输出,它返回1或0

这对我很有帮助。它以红色打印错误或警告消息,每个参数一行,并允许可选的退出代码。

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