你最喜欢在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, 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}
}

我使用

die() {
        echo $1
        kill $$
}

之前;我想因为某种原因,“退出”对我来说是失败的。不过,上面的默认设置似乎是个好主意。

使用陷阱!

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同样会给你错误退出的行为,尽管它有严重的警告,并削弱了代码的可预测性和可移植性)。

您可以让trap为您调用错误(在这种情况下,它使用默认的退出代码1和无消息)或自己调用它并提供显式值;例如:

error ${LINENO} "the foobar failed" 2

将以状态2退出,并给出一个显式消息。

或者使用-s extdebug,并对陷阱的第一行进行一些修改,以全面地捕获所有非零退出代码(mind set -e非错误非零退出代码):

error() {
  local last_exit_status="$?"
  local parent_lineno="$1"
  local message="${2:-(no message ($last_exit_status))}"
  local code="${3:-$last_exit_status}"
  # ... continue as above
}
trap 'error ${LINENO}' ERR
shopt -s extdebug

这也与set -eu“兼容”。

使用陷阱并不总是一个选择。例如,如果您正在编写某种需要错误处理的可重用函数,并且可以从任何脚本调用(在使用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中。