关于linux:在Bash脚本中引发错误

Raise error in a Bash script

我想在bash脚本中引发一个错误,并显示消息"测试用例失败!!.如何在bash中执行此操作?

例如:

1
2
3
if [ condition ]; then
    raise error"Test cases failed !!!"
fi


这取决于错误消息存储的位置。

您可以执行以下操作:

1
2
echo"Error!"> logfile.log
exit 125

或以下内容:

1
2
echo"Error!" 1>&2
exit 64

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

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

正如@chepner所指出的,您可以执行exit 1,这意味着一个未指定的错误。


基本错误处理

如果测试用例运行程序为失败的测试返回一个非零代码,您只需编写:

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

或者更短:

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

或最短的:

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

要使用测试处理程序的退出代码退出:

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

高级错误处理

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

1
2
3
4
5
6
7
8
9
10
11
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
'
"$@">&2 # we can use better logging here
      exit"$exit_code"             # we could also check to make sure
                                    # error code is numeric when passed
    }
}

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

1
2
run_test_case test_case_x
exit_if_error $?"Test case x failed"

1
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

相关帖子

  • bash中的错误处理
  • bash hacker wiki上的"caller"内置命令
  • Linux中有没有标准的退出状态代码?
  • bashfaq/105-为什么set-e(或set-o errexit或trap err)不按我的预期执行?
  • 相当于bash中的__FILE____LINE__
  • bash中有try-catch命令吗
  • 要向错误处理程序添加堆栈跟踪,您可能需要查看以下文章:bash脚本调用的已执行程序的跟踪
  • 忽略shell脚本中的特定错误
  • 在shell管道中捕获错误代码
  • 如何管理shell脚本中的日志详细性?
  • 如何在bash中记录函数名和行号?
  • 在bash中,双方括号[]比单方括号[]更可取吗?


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

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

所以你想做的是这两类

  • 错误退出
  • 错误时退出并清除

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

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

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

因此,在需要对脚本执行清理操作以使脚本终止干净的情况下,最好不要使用EXIT

退出时是否应使用set -e作为错误?

No!

set -e试图向shell添加"自动错误检测"。它的目标是在任何时候发生错误时使shell中止,但是它有很多潜在的缺陷,例如,

  • 作为中频测试的一部分的命令是免疫的。在这个例子中,如果您希望它在test上中断检查不存在的目录,那么它不会中断检查,而是转到其他条件。

    1
    2
    3
    4
    set -e
    f() { test -d nosuchdir && echo no dir; }
    f
    echo survived
  • 除最后一个命令外,管道中的命令是免疫的。在下面的示例中,因为考虑了最近执行的(最右边的)命令的退出代码(cat),并且它是成功的。这可以通过设置set -o pipefail选项来避免,但它仍然是一个警告。

    1
    2
    3
    set -e
    somecommand that fails | cat -
    echo survived

建议在出口处使用-trap

结论是,如果您希望能够处理错误而不是盲目退出,而不是使用set -e,则在ERR伪信号上使用trap

当shell本身使用非零错误代码退出时,ERR陷阱不运行代码,而是当该shell运行的任何不属于某个条件的命令(如cmdcmd ||以非零退出状态退出时)不运行代码。

一般的做法是,我们定义一个陷阱处理程序,以提供关于哪一行以及导致退出的原因的其他调试信息。记住,导致ERR信号的最后一个命令的退出代码此时仍然可用。

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

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

1
trap cleanup ERR

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

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

trap还提供了选项,不管错误如何,只要在shell完成时运行清理(例如shell脚本退出),信号为EXIT。您还可以同时捕获多个信号。可在trap.1p-linux手册页上找到要捕获的受支持信号的列表。

另一件需要注意的事情是要理解,如果涉及到子shell,那么所提供的方法都不起作用,您可能需要添加自己的错误处理。

  • 在带有set -e的子shell上不起作用。false仅限于子shell,从不传播到父shell。要在这里执行错误处理,请添加您自己的逻辑来执行EDOCX1[24]

    1
    2
    3
    set -e
    (false)
    echo survived
  • 同样的情况也发生在trap上。由于上述原因,下面的逻辑不起作用。

    1
    2
    trap 'echo error' ERR
    (false)

这里有一个简单的陷阱,它打印任何未能执行stderr的操作的最后一个参数,报告失败的行,并以行号作为退出代码退出脚本。注意,这些并不总是很好的想法,但这展示了一些您可以构建的创造性应用程序。

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

我把它放在一个带有循环的脚本中进行测试。我只是检查一些随机数的命中率;您可以使用实际的测试。如果我需要保释,我会用我想抛出的信息来调用false(这会触发陷阱)。

对于详细的功能,让陷阱调用一个处理函数。如果需要进行更多的清理等操作,可以在arg($u)上始终使用case语句。请为var分配一点语法糖分。-

1
2
3
4
5
6
7
8
9
10
11
12
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

样品输出:

1
2
3
4
5
6
# bash tst
got 2
got 8
DIVISION BY ZERO at 6
# echo $?
6

显然,你可以

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

设计改进空间很大。

撤退的原因包括:false不漂亮(因此糖也不好看),而其他绊倒陷阱的事情可能看起来有点愚蠢。不过,我还是喜欢这种方法。


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

1
2
3
4
5
6
7
8
9
10
# 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日期的%N是纳秒,我们稍后将其截断为微秒)。

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

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

1
2
3
4
5
6
7
8
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"

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

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

    1
    ./runTests &> output.log

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

    使用这种方法,您不必在脚本中引入日志文件,这样逻辑就简单了一点。

  • 将日志文件引入脚本:
  • 在脚本中,通过硬编码添加日志文件:

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

    或通过参数传递:

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

    最好在执行时将时间戳添加到脚本顶部的日志文件中:

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

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

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

    这将把错误附加到日志文件并继续执行。如果您希望在出现严重错误时停止执行,可以执行脚本:

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

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

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

    如果您有一个相对较小的脚本,或者希望在不修改它的情况下执行其他人的脚本,那么第一种方法更合适。

    如果始终希望日志文件位于同一位置,则这是2的更好选项。另外,如果您已经创建了一个包含多个组件的大型脚本,那么您可能希望以不同的方式记录每个部分,第二种方法是您唯一的选择。