在 Bash 脚本中引发错误

我想在 Bash 脚本中引发一个错误,其中包含消息“测试用例失败! ! !”.在巴斯怎么做?

例如:

if [ condition ]; then
raise error "Test cases failed !!!"
fi
172720 次浏览

这取决于您希望将错误消息存储在何处。

你可以这样做:

echo "Error!" > logfile.log
exit 125

或者下面这些:

echo "Error!" 1>&2
exit 64

当引发异常时,程序将停止执行。

您还可以使用类似于 exit xxx的代码,其中 xxx是您可能希望返回给操作系统的错误代码(从0到255)。在这里 12564只是随机代码,你可以退出。当您需要向操作系统指示程序异常停止(例如发生错误)时,您需要向 exit传递一个 非零退出码

作为@chepner 指出,你可以做 exit 1,也就是 未指明的错误

基本错误处理

如果您的测试用例运行程序为失败的测试返回 非零代码,您可以简单地写:

test_handler test_case_x; test_result=$?
if ((test_result != 0)); then
printf '%s\n' "Test case x failed" >&2  # write error message to stderr
exit 1                                  # or exit $test_result
fi

或者更短:

if ! test_handler test_case_x; then
printf '%s\n' "Test case x failed" >&2
exit 1
fi

或者最短的:

test_handler test_case_x || { printf '%s\n' "Test case x failed" >&2; exit 1; }

使用 test _ handler 的退出代码退出:

test_handler test_case_x || { ec=$?; printf '%s\n' "Test case x failed" >&2; exit $ec; }

高级错误处理

如果希望采用更全面的方法,可以使用错误处理程序:

exit_if_error() {
local exit_code=$1
shift
[[ $exit_code ]] &&               # do nothing if no error code passed
((exit_code != 0)) && {         # do nothing if error code is 0
printf 'ERROR: %s\n' "$@" >&2 # we can use better logging here
exit "$exit_code"             # we could also check to make sure
# error code is numeric when passed
}
}

然后在运行测试用例之后调用它:

run_test_case test_case_x
exit_if_error $? "Test case x failed"

或者

run_test_case test_case_x || exit_if_error $? "Test case x failed"

使用像 exit_if_error这样的错误处理程序的优点是:

  • 我们可以标准化所有的错误处理逻辑,如 伐木,打印 堆栈追踪,通知,做清理等在一个地方
  • 通过让错误处理程序获取错误代码作为参数,我们可以将调用方从测试退出代码错误的 if块的混乱中分离出来
  • 如果我们有一个信号处理程序(使用 陷阱) ,我们可以从那里调用错误处理程序

错误处理和日志库

下面是错误处理和日志记录的完整实现:

Https://github.com/codeforester/base/blob/master/lib/stdlib.sh


相关职位

还有一些方法可以用来解决这个问题。假设您的需求之一是运行一个包含几个 shell 命令的 shell 脚本/函数,并检查脚本是否成功运行,并在出现故障时抛出错误。

Shell 命令通常依赖于返回的退出代码,以便让 shell 知道由于某些意外事件它是成功还是失败。

所以你想要做的就是这两种类型

  • 因错误而退出
  • 退出并清除错误

根据需要执行的操作,可以使用 shell 选项。对于第一种情况,shell 提供了一个 set -e选项,对于第二种情况,您可以在 EXIT上执行 trap

我应该在脚本/函数中使用 exit吗?

使用 exit通常可以提高可读性在某些例程中,一旦你知道了答案,你想立即退出到调用例程。如果例程的定义方式是在检测到错误后不需要进一步清理,那么不立即退出意味着您必须编写更多代码。

因此,在需要对脚本执行清理操作以清理脚本终止的情况下,最好使用 exit而不是 没有

我应该在退出时使用 set -e吗?

不!

set -e尝试向 shell 添加“自动错误检测”。它的目标是在任何错误发生时导致 shell 中止,但是它带来了很多潜在的缺陷,例如,

  • If 测试的部分命令是免疫的。在本例中,如果您希望它在不存在的目录上执行 test检查时中断,那么它不会中断,而是执行 else 条件

    set -e
    f() { test -d nosuchdir && echo no dir; }
    f
    echo survived
    
  • Commands in a pipeline other than the last one, are immune. In the example below, because the most recently executed (rightmost) command's exit code is considered ( cat) and it was successful. This could be avoided by setting by the set -o pipefail option but its still a caveat.

    set -e
    somecommand that fails | cat -
    echo survived
    

Recommended for use - trap on exit

The verdict is if you want to be able to handle an error instead of blindly exiting, instead of using set -e, use a trap on the ERR pseudo signal.

The ERR trap is not to run code when the shell itself exits with a non-zero error code, but when any command run by that shell that is not part of a condition (like in if cmd, or cmd ||) exits with a non-zero exit status.

The general practice is we define an trap handler to provide additional debug information on which line and what cause the exit. Remember the exit code of the last command that caused the ERR signal would still be available at this point.

cleanup() {
exitcode=$?
printf 'error condition hit\n' 1>&2
printf 'exit code returned: %s\n' "$exitcode"
printf 'the command executing at the time of the error was: %s\n' "$BASH_COMMAND"
printf 'command present on line: %d' "${BASH_LINENO[0]}"
# Some more clean up code can be added here before exiting
exit $exitcode
}

我们只是在失败的脚本上使用下面这个处理程序

trap cleanup ERR

将这些放在第15行中包含 false的简单脚本上,您将得到的信息是

error condition hit
exit code returned: 1
the command executing at the time of the error was: false
command present on line: 15

无论发生什么错误,trap还提供了在信号 EXIT时在 shell 完成时(例如您的 shell 脚本退出)运行清理的选项。您还可以同时捕获多个信号。在 1 p-Linux 手册页上可以找到所支持的陷阱信号列表

另一件需要注意的事情是,如果处理的是子 shell,那么所提供的方法都不能正常工作,在这种情况下,您可能需要添加自己的错误处理。

  • set -e的子外壳程序是行不通的。false仅限于子 shell,从不传播到父 shell。要在这里执行错误处理,请添加您自己的逻辑来执行 (false) || false

    set -e
    (false)
    echo survived
    
  • The same happens with trap also. The logic below wouldn't work for the reasons mentioned above.

    trap 'echo error' ERR
    (false)
    

我经常发现编写一个函数来处理错误消息是很有用的,这样代码总体上会更干净。

# Usage: die [exit_code] [error message]
die() {
local code=$? now=$(date +%T.%N)
if [ "$1" -ge 0 ] 2>/dev/null; then  # assume $1 is an error code if numeric
code="$1"
shift
fi
echo "$0: ERROR at ${now%???}${1:+: $*}" >&2
exit $code
}

这将获取前一个命令中的错误代码,并在退出整个脚本时将其用作默认错误代码。它还记录了支持的时间,其中包含微秒(GNU date 的 %N是纳秒,稍后我们将其截断为微秒)。

如果第一个选项为零或正整数,它将成为退出代码,我们将它从选项列表中删除。然后,我们将消息报告给标准错误,包括脚本名称、单词“ ERROR”和时间(我们使用参数展开将纳秒截断为微秒,或者对于非 GNU 时间,将参数展开将 12:34:56.%N截断为 12:34:56)。在 ERROR 后面添加冒号和空格,但只有在提供了错误消息时才添加。最后,我们使用先前确定的退出代码退出脚本,像往常一样触发任何陷阱。

一些示例(假设代码位于 script.sh中) :

if [ condition ]; then die 123 "condition not met"; fi
# exit code 123, message "script.sh: ERROR at 14:58:01.234564: condition not met"


$command |grep -q condition || die 1 "'$command' lacked 'condition'"
# exit code 1, "script.sh: ERROR at 14:58:55.825626: 'foo' lacked 'condition'"


$command || die
# exit code comes from command's, message "script.sh: ERROR at 14:59:15.575089"

下面是一个简单的陷阱,它输出 STDERR 失败的内容的最后一个参数,报告它失败的行,并退出脚本,行号作为退出代码。请注意,这些并不总是伟大的想法,但这展示了一些创造性的应用程序,您可以在此基础上构建。

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR

我把它放在一个脚本中,用一个循环来测试它。我只是检查一些随机数你可能会用到实际测试。如果我需要放弃,我会用我想抛出的信息调用 false (这会触发陷阱)。

对于详细说明的功能,有陷阱调用一个处理函数。如果需要进行更多的清理,可以在 arg ($_)上使用 case 语句,等等。为一个 var 分配一点语法 Sugar-

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR
throw=false
raise=false


while :
do x=$(( $RANDOM % 10 ))
case "$x" in
0) $throw "DIVISION BY ZERO" ;;
3) $raise "MAGIC NUMBER"     ;;
*) echo got $x               ;;
esac
done

输出样本:

# bash tst
got 2
got 8
DIVISION BY ZERO at 6
# echo $?
6

显然,你可以

runTest1 "Test1 fails" # message not used if it succeeds

还有很大的改进空间。

缺点包括事实上 false不是很漂亮(因此是糖) ,其他东西绊倒陷阱可能看起来有点愚蠢。不过,我还是喜欢这个方法。

您有两个选项: 将脚本的输出重定向到一个文件,在脚本中引入一个日志文件,以及

  1. 将输出重定向到文件 :

这里假设脚本输出所有必要的信息,包括警告和错误消息。然后可以将输出重定向到所选的文件。

./runTests &> output.log

上面的命令将标准输出和错误输出重定向到日志文件。

使用这种方法,您不必在脚本中引入日志文件,因此逻辑稍微容易一些。

  1. 在脚本 中引入一个日志文件:

在脚本中添加一个日志文件,通过硬编码实现:

logFile='./path/to/log/file.log'

或者通过一个参数传递:

logFile="${1}"  # This assumes the first parameter to the script is the log file

将执行时的时间戳添加到脚本顶部的日志文件中是一个好主意:

date '+%Y%-m%d-%H%M%S' >> "${logFile}"

然后可以将错误消息重定向到日志文件

if [ condition ]; then
echo "Test cases failed!!" >> "${logFile}";
fi

这将把错误追加到日志文件并继续执行。如果您希望在发生关键错误时停止执行,可以 exit脚本:

if [ condition ]; then
echo "Test cases failed!!" >> "${logFile}";
# Clean up if needed
exit 1;
fi

请注意,exit 1表示程序由于未指定的错误而停止执行。如果你愿意,你可以自定义这个。

使用这种方法,您可以自定义日志,并为脚本的每个组件提供不同的日志文件。


如果您有一个相对较小的脚本,或者希望执行其他人的脚本而不修改它,则第一种方法更为合适。

如果您总是希望日志文件位于相同的位置,这是2中的较好选项。此外,如果您已经创建了一个包含多个组件的大脚本,那么您可能希望以不同的方式记录每个组件,而第二种方法是您唯一的选择。