Bash 中的错误处理

发布于 2024-07-04 21:24:06 字数 1721 浏览 8 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(15

不语却知心 2024-07-11 21:24:06

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

有一个小技巧可以用来在没有陷阱的情况下进行正确的错误处理。 正如您可能已经从其他答案中知道的那样,如果您在命令后使用 || 运算符,则 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

这是建立在这个想法之上的通用函数。 如果您删除 local 关键字,即仅用 x=y 替换所有 local x=y,它应该可以在所有 POSIX 兼容 shell 中工作:

# [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 的命令对 Shell 变量进行的所有修改都不会传播到调用函数,因为该命令在子 shell 中运行。

Using trap is not always an option. For example, if you're writing some kind of re-usable function that needs error handling and that can be called from any script (after sourcing the file with helper functions), that function cannot assume anything about exit time of the outer script, which makes using traps very difficult. Another disadvantage of using traps is bad composability, as you risk overwriting previous trap that might be set earlier up in the caller chain.

There is a little trick that can be used to do proper error handling without traps. As you may already know from other answers, set -e doesn't work inside commands if you use || operator after them, even if you run them in a subshell; e.g., this wouldn't work:

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

But || operator is needed to prevent returning from the outer function before cleanup. The trick is to run the inner command in background, and then immediately wait for it. The wait builtin will return the exit code of the inner command, and now you're using || after wait, not the inner function, so set -e works properly inside the latter:

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

Here is the generic function that builds upon this idea. It should work in all POSIX-compatible shells if you remove local keywords, i.e. replace all local x=y with just 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
}

Example of usage:

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

Running the example:

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

The only thing that you need to be aware of when using this method is that all modifications of Shell variables done from the command you pass to run will not propagate to the calling function, because the command runs in a subshell.

御守 2024-07-11 21:24:06

这个函数最近对我很有用:

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

如果任何命令失败,则错误代码将简单地传递到块的末尾。 我发现当您不希望在前一个命令失败时执行后续命令,但您也不希望脚本立即退出(例如,在循环内)时,它很有用。

This function has been serving me rather well recently:

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

You call it by appending 0 or the last return value to the name of the command to run, so you can chain commands without having to check for error values. With this, this statement block:

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

Becomes this:

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

If any of the commands fail, the error code is simply passed to the end of the block. I find it useful when you don't want subsequent commands to execute if an earlier one failed, but you also don't want the script to exit straight away (for example, inside a loop).

战皆罪 2024-07-11 21:24:06

不确定这是否对您有帮助,但我在这里修改了一些建议的函数,以便在其中包含对错误(先前命令的退出代码)的检查。
在每次“检查”时,我还将错误的“消息”作为参数传递以用于记录目的。

#!/bin/bash

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

则在另一个脚本中调用它),我只需编写函数的名称并传递一条消息作为参数,如下所示:

#!/bin/bash

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

rm *
error_exit "Unable to delete all files"

现在,要在同一个脚本中调用它(如果我使用export -f error_exit , 能够为某些自动化过程创建一个非常强大的 bash 文件,并且在出现错误时它将停止并通知我(log.sh 会做到这一点)

Not sure if this will be helpful to you, but I modified some of the suggested functions here in order to include the check for the error (exit code from prior command) within it.
On each "check" I also pass as a parameter the "message" of what the error is for logging purposes.

#!/bin/bash

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

Now to call it within the same script (or in another one if I use export -f error_exit) I simply write the name of the function and pass a message as parameter, like this:

#!/bin/bash

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

rm *
error_exit "Unable to delete all files"

Using this I was able to create a really robust bash file for some automated process and it will stop in case of errors and notify me (log.sh will do that)

挥剑断情 2024-07-11 21:24:06

这个技巧对于丢失命令或函数很有用。 缺少的函数(或可执行文件)的名称将在 $_ 中传递

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

This trick is useful for missing commands or functions. The name of the missing function (or executable) will be passed in $_

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
酷到爆炸 2024-07-11 21:24:06

有时 set -etrap ERRset -o 、set -o pipelinefailset - o errtrace 无法正常工作,因为它们尝试向 shell 添加自动错误检测。 这在实践中效果并不好。

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

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

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

{ # try

    command &&
    # your command 

} || { 
    # catch exception 
}

或者你可以使用 if else

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

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

Sometimes set -e , trap ERR ,set -o ,set -o pipefail and set -o errtrace not work properly because they attempt to add automatic error detection to the shell. This does not work well in practice.

In my opinion, instead of using set -e and other stuffs, you should write your own error checking code. If you wise to use set -e, be aware of potential gotchas.

To avoid Error while running the code you can use exec 1>/dev/null or exec 2>/dev/null
/dev/null in Linux is a null device file. This will discard anything written to it and will return EOF on reading. you can use this at end of the command

For try/catch you can use && or || to achieve Similar behaviour
use can use && like this

{ # try

    command &&
    # your command 

} || { 
    # catch exception 
}

or you can use if else :

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

$? show output of the last command ,it return 1 or 0

朕就是辣么酷 2024-07-11 21:24:06

已经为我服务了一段时间了。 它以红色打印错误或警告消息,每个参数一行,并允许可选的退出代码。

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

This has served me well for a while now. It prints error or warning messages in red, one line per parameter, and allows an optional exit code.

# 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}
}
空城缀染半城烟沙 2024-07-11 21:24:06

我以前用过

die() {
        echo $1
        kill $
}

; 我想是因为“退出”由于某种原因对我来说失败了。 不过,上述默认值似乎是个好主意。

I've used

die() {
        echo $1
        kill $
}

before; i think because 'exit' was failing for me for some reason. The above defaults seem like a good idea, though.

长发绾君心 2024-07-11 21:24:06

另一个考虑因素是要返回的退出代码。 尽管有一些,但“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 将代码组合在一起允许您的脚本发出多个同时发生的错误信号。

Another consideration is the exit code to return. Just "1" is pretty standard, although there are a handful of reserved exit codes that bash itself uses, and that same page argues that user-defined codes should be in the range 64-113 to conform to C/C++ standards.

You might also consider the bit vector approach that mount uses for its exit codes:

 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-ing the codes together allows your script to signal multiple simultaneous errors.

国际总奸 2024-07-11 21:24:06

我使用以下陷阱代码,它还允许通过管道和“时间”命令跟踪错误

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

I use the following trap code, it also allows errors to be traced through pipes and 'time' commands

#!/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
海夕 2024-07-11 21:24:06

我更喜欢真正容易调用的东西。 所以我用的东西看起来有点复杂,但是很容易使用。 我通常只是将下面的代码复制并粘贴到我的脚本中。 代码后面有解释。

#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 函数中调用 cleanup 函数,但这因脚本而异,所以我将其省略。 陷阱捕获常见的终止信号并确保所有内容都得到清理。 别名才是真正的魔力。 我喜欢检查一切是否失败。 所以一般来说我用“if!”来称呼程序。 类型声明。 通过从行号中减去 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

I prefer something really easy to call. So I use something that looks a little complicated, but is easy to use. I usually just copy-and-paste the code below into my scripts. An explanation follows the code.

#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
好多鱼好多余 2024-07-11 21:24:06

受到此处提出的想法的启发,我开发了一种可读且方便的方法来处理 中的 bash 脚本中的错误bash 样板项目

通过简单地获取该库,您可以立即得到以下内容(即,它将在出现任何错误时停止执行,就像使用 set -e 一样,这要归功于 trap 上的 trap code>ERR 和一些 bash-fu):

bash-oo-framework 错误处理

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

缺点是 - 它不可移植 - 代码可以在 bash 中运行,可能只能 >= 4 (但我想它可以通过一些努力移植到 bash 3)。

为了更好地处理,代码被分成多个文件,但我受到了 的回溯想法的启发Luca Borrione 的上述答案

要了解更多信息或查看源代码,请参阅 GitHub:

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

Inspired by the ideas presented here, I have developed a readable and convenient way to handle errors in bash scripts in my bash boilerplate project.

By simply sourcing the library, you get the following out of the box (i.e. it will halt execution on any error, as if using set -e thanks to a trap on ERR and some bash-fu):

bash-oo-framework error handling

There are some extra features that help handle errors, such as try and catch, or the throw keyword, that allows you to break execution at a point to see the backtrace. Plus, if the terminal supports it, it spits out powerline emojis, colors parts of the output for great readability, and underlines the method that caused the exception in the context of the line of code.

The downside is - it's not portable - the code works in bash, probably >= 4 only (but I'd imagine it could be ported with some effort to bash 3).

The code is separated into multiple files for better handling, but I was inspired by the backtrace idea from the answer above by Luca Borrione.

To read more or take a look at the source, see GitHub:

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

遥远的她 2024-07-11 21:24:06

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

An equivalent alternative to "set -e" is

set -o errexit

It makes the meaning of the flag somewhat clearer than just "-e".

Random addition: to temporarily disable the flag, and return to the default (of continuing execution regardless of exit codes), just use

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

This precludes proper error handling mentioned in other responses, but is quick & effective (just like bash).

凉月流沐 2024-07-11 21:24:06

使用陷阱!

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 退出,并给出明确的消息。

或者 shopt -s extdebug 并对陷阱的第一行进行一些修改,以全面捕获所有非零退出代码(注意 set -e non-error non -零退出代码):

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“兼容”。

Use a trap!

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

...then, whenever you create a temporary file:

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

and $temp_foo will be deleted on exit, and the current line number will be printed. (set -e will likewise give you exit-on-error behavior, though it comes with serious caveats and weakens code's predictability and portability).

You can either let the trap call error for you (in which case it uses the default exit code of 1 and no message) or call it yourself and provide explicit values; for instance:

error ${LINENO} "the foobar failed" 2

will exit with status 2, and give an explicit message.

Alternatively shopt -s extdebug and give the first lines of the trap a little modification to trap all non-zero exit codes across the board (mind set -e non-error non-zero exit codes):

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

This then is also "compatible" with set -eu.

打小就很酷 2024-07-11 21:24:06

这是一个很好的解决方案。 我只是想添加

set -e

一个基本的错误机制。 如果一个简单的命令失败,它将立即停止您的脚本。 我认为这应该是默认行为:因为此类错误几乎总是意味着意外的事情,所以继续执行以下命令并不是真正的“理智”。

That's a fine solution. I just wanted to add

set -e

as a rudimentary error mechanism. It will immediately stop your script if a simple command fails. I think this should have been the default behavior: since such errors almost always signify something unexpected, it is not really 'sane' to keep executing the following commands.

山有枢 2024-07-11 21:24:06

阅读本页上的所有答案给了我很多启发。

所以,这是我的提示:

文件内容: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,})

使用示例:
文件内容: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!

正如您从下面的屏幕截图中看到的,输出是彩色的,并且错误消息以所使用的语言显示。

在此处输入图像描述

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

运行:

输出:

正如您从下面的屏幕截图中看到的,输出是彩色的,并且错误消息以所使用的语言显示。

在此处输入图像描述

Reading all the answers on this page inspired me a lot.

So, here's my hint:

file content: 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,})

Example of usage:
file content: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

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

exit 0

Running:

bash trap-test.sh

Output:

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!

As you can see from the screenshot below, the output is colored and the error message comes in the used language.

enter image description here

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

Example of usage:
file content: trap-test.sh

Running:

Output:

As you can see from the screenshot below, the output is colored and the error message comes in the used language.

enter image description here

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文