通过打印堆栈跟踪来修复 Bash 脚本中的错误

在脚本中未处理的错误上自动打印堆栈跟踪可以使查找和修复代码中的错误变得更加容易。
6 位读者喜欢这个。
Bug tracking magnifying glass on computer screen

Pixabay, testbytes, CC0

没有人想编写糟糕的代码,但不可避免地会产生错误。大多数现代语言,如 Java、JavaScript、Python 等,在遇到未处理的异常时会自动打印堆栈跟踪,但 shell 脚本则不会。如果可以打印堆栈跟踪,这将使在 shell 脚本中查找和修复错误变得更加容易,并且只需稍加努力,您就可以做到。

Shell 脚本可以跨越多个文件,编写良好的代码会进一步分解为函数。当这些脚本足够大时,跟踪 shell 脚本中出现的问题可能会很困难。从错误向后遍历代码到开头的堆栈跟踪可以向您展示代码在哪里失败,并让您更好地理解原因,以便您可以正确修复它。

为了实现堆栈跟踪,我在脚本开头以以下方式使用 trap

set -E

trap 'ERRO_LINENO=$LINENO' ERR
trap '_failure' EXIT

此示例完成了一些事情,但我将首先解决第二个问题,trap 'ERRO_LINENO=$LINENO' ERR。此行确保脚本捕获所有以非零退出代码(即错误)退出的命令,并将错误发出信号的命令的行号保存在文件中。 这不会在退出时捕获。

上面的第一行(set -E)确保错误陷阱在整个脚本中继承。 如果没有这个,每当您进入 ifuntil 块时,例如,您将失去对正确行号的跟踪。

第二个陷阱捕获来自脚本的退出信号并将其发送到 _failure 函数,我稍后将定义它。 但是,如果您尝试调试脚本,为什么在退出时而不是错误时呢? 在 bash 脚本中,命令失败通常用于控制逻辑,或者可能因设计原因而被完全忽略为不重要。 例如,假设在脚本的开头,您正在查看是否已安装特定程序,然后再询问用户是否希望您为他们安装它

if [[ ! -z $(command -v some_command) ]]
then
   # CAPTURE LOCATION OF some_command
   SOME_COMMAND_EXEC=$(which some_command)
else
   # echo $? would give us a non-zero value here; i.e. an error code
   # IGNORE ERR: ASK USER IF THEY WANT TO INSTALL some_command
fi

如果您要在每个错误上停止处理,并且未安装 some_command,这将过早地结束脚本,这显然不是您想在此处执行的操作,因此通常,您只想在脚本因错误而意外退出时记录错误和堆栈跟踪。

要强制脚本在出现意外错误时退出,请使用 set -e 选项

set -e
# SCRIPT WILL EXIT IF ANY COMMAND RETURNS A NON-ZERO CODE
# WHILE set -e IS IN FORCE
set +e
# COMMANDS WITH ERRORS WILL NOT CAUSE THE SCRIPT TO EXIT HERE

下一个问题是,在哪些示例中您可能希望脚本退出并突出显示失败? 常见示例包括以下

  1. 无法访问的远程系统
  2. 远程系统身份验证失败
  3. 正在源的配置或脚本文件中的语法错误
  4. Docker 镜像构建
  5. 编译器错误

在脚本完成后梳理许多页面的日志,寻找可能难以发现的任何可能的错误,这可能会非常令人沮丧。当您发现脚本运行很久之后出现问题,现在必须梳理多组日志以查找可能出现的问题以及原因时,会更加令人沮丧。最糟糕的是,错误已经存在一段时间,而您只在最糟糕的时间发现它。在任何情况下,尽快查明问题并修复它始终是首要任务。

查看示例堆栈跟踪代码(可在此处下载

# Sample code for generating a stack trace on catastrophic failure

set -E

trap 'ERRO_LINENO=$LINENO' ERR
trap '_failure' EXIT

_failure() {
  ERR_CODE=$? # capture last command exit code
  set +xv # turns off debug logging, just in case
  if [[  $- =~ e && ${ERR_CODE} != 0 ]]
  then
      # only log stack trace if requested (set -e)
      # and last command failed
      echo
      echo "========= CATASTROPHIC COMMAND FAIL ========="
      echo
      echo "SCRIPT EXITED ON ERROR CODE: ${ERR_CODE}"
      echo
      LEN=${#BASH_LINENO[@]}
      for (( INDEX=0; INDEX<$LEN-1; INDEX++ ))
      do
          echo '---'
          echo "FILE: $(basename ${BASH_SOURCE[${INDEX}+1]})"
          echo "  FUNCTION: ${FUNCNAME[${INDEX}+1]}"
          if [[ ${INDEX} > 0 ]]
          then
           # commands in stack trace
              echo "  COMMAND: ${FUNCNAME[${INDEX}]}"
              echo "  LINE: ${BASH_LINENO[${INDEX}]}"
          else
              # command that failed
              echo "  COMMAND: ${BASH_COMMAND}"
              echo "  LINE: ${ERRO_LINENO}"
          fi
      done
      echo
      echo "======= END CATASTROPHIC COMMAND FAIL ======="
      echo
  fi
}

# set working directory to this directory for duration of this test
cd "$(dirname ${0})"

echo 'Beginning stacktrace test'

set -e
source ./testfile1.sh
source ./testfile2.sh
set +e

_file1_function1

在上面的 stacktrace.sh 中,_failure 函数要做的第一件事是使用内置的 shell 值 $? 捕获最后一个命令的退出代码。 然后,它通过检查 $- 的输出来检查退出是否意外,$- 是一个保存当前 bash shell 设置的内置 shell 值,以查看是否强制执行了 set -e。 如果脚本因错误而退出,并且该错误是意外的,则堆栈跟踪将输出到控制台。

以下内置 shell 值用于构建堆栈跟踪

  1. BASH_SOURCE:文件名数组,其中每个命令都被回调到主脚本。
  2. FUNCNAME:与 BASH_SOURCE 中每个文件匹配的行号数组。
  3. BASH_LINENO:每个文件与 BASH_SOURCE 匹配的行号数组。
  4. BASH_COMMAND:具有标志和参数的最后一个执行的命令。

如果脚本以意外方式出错退出,它会循环遍历上述变量并按顺序输出每个变量,以便可以构建堆栈跟踪。 失败命令的行号未保存在上述数组中,但这就是您每次使用上面的第一个 trap 语句捕获命令失败时的行号的原因。

将它们放在一起

创建以下两个文件以支持测试,以便您可以了解如何在多个文件中收集信息。 首先,testfile1.sh

_file1_function1() {
   echo
   echo "executing in _file1_function1"
   echo

   _file2_function1
}

# adsfadfaf

_file1_function2() {
   echo
   echo "executing in _file1_function2"
   echo
  
   set -e
   curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

   # function never called
   _file2_does_not_exist
}

接下来,testfile2.sh

_file2_function1() {
   echo
   echo "executing in _file2_function1"
   echo

   curl this_curl_will_simply_fail

   _file1_function2
}

注意:如果您自己创建这些文件,请确保使 stacktrace.sh 文件可执行。

执行 stacktrace.sh 将输出以下内容

~/shell-stack-trace-example$./stracktrace.sh
Beginning stacktrace test

executing in _file1_function1

executing in _file2_function1
curl: (6) Could not resolve host: this_curl_will_simply_fail

executing in _file1_function2
curl: (6) Could not resolve host: this_curl_will_fail_and_CAUSE_A_STACK_TRACE

========= CATASTROPHIC COMMAND FAIL =========

SCRIPT EXITED ON ERROR CODE: 6

---
FILE: testfile1.sh
  FUNCTION: _file1_function2
  COMMAND: curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE
  LINE: 15
---
FILE: testfile2.sh
  FUNCTION: _file2_function1
  COMMAND: _file1_function2
  LINE: 7
---
FILE: testfile1.sh
  FUNCTION: _file1_function1
  COMMAND: _file2_function1
  LINE: 5
---
FILE: stracktrace.sh
  FUNCTION: main
  COMMAND: _file1_function1
  LINE: 53

======= END CATASTROPHIC COMMAND FAIL =======

要获得额外的奖励,请尝试取消注释 testfile1.sh 中的行并再次执行 stacktrace.sh

# adsfadfaf

然后重新注释该行,并改为注释掉 testfile1.sh 中导致堆栈跟踪的以下行并最后一次运行 stacktrace.sh

curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

如果您在脚本中输入错误,此练习应该让您了解输出以及何时发生。

接下来要阅读的内容
标签
Evan "Hippy" Slatis
我为红帽服务部门担任顾问,我专注于 OpenShift 上的应用程序部署和 CI/CD,并且我运行自己的 OSS 项目 el-CICD (https://github.com/elcicd),这是一个完整的 CICD COTS 解决方案,适用于 OKD/OpenShift 容器平台。 我是多家初创公司的资深人士,并且我已经担任软件开发人员/架构师,主要使用 Java,将近 30 年了。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.