没有人愿意写坏代码,但不可避免地会产生bug。大多数现代语言,如Java、JavaScript、Python等,在遇到未处理的异常时都会自动打印出堆栈跟踪,但shell脚本却没有。如果你能打印出堆栈跟踪,那么发现和修复shell脚本中的bug就会容易得多,而且,只要稍加努力,你就能做到。
Shell脚本可以跨越多个文件,而且写得很好的代码会进一步分解成函数。当shell脚本出错时,追踪问题在这些脚本变得足够大时是很困难的。堆栈跟踪可以将代码从错误处往回走到开始处,可以显示你的代码在哪里出了问题,并让你更好地了解原因,以便你能正确地修复它。
为了实现堆栈跟踪,我在脚本的开头以如下方式使用陷阱。
set -E
trap 'ERRO_LINENO=$LINENO' ERR
trap '_failure' EXIT
这个例子完成了几件事,但我先讲第二个,即陷阱'ERRO_LINENO=$LINENO' ERR。这一行确保脚本捕获所有以非零退出代码(即错误)退出的命令,并在文件中保存发出错误信号的命令的行号。这在退出时不会被捕获。
上面的第一行(set -E)确保错误陷阱在整个脚本中被继承下来。如果不这样做,每当你进入一个if或until块时,你就会失去正确的行号的追踪。
第二个陷阱捕捉来自脚本的退出信号,并将其发送到_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
下一个问题是,有哪些例子你可能希望你的脚本退出并突出显示一个失败?常见的例子包括以下几种。
- 一个无法到达的远程系统
- 对远程系统的认证失败
- 配置文件或脚本文件中的语法错误被源化
- Docker镜像的构建
- 编译器错误
在一个脚本完成后,梳理许多页的日志,寻找任何可能的、难以发现的错误,这可能是非常令人沮丧的。更令人沮丧的是,当你在运行脚本后很久才发现有问题,现在不得不梳理多套日志来寻找可能出错的地方。最糟糕的是当错误已经存在了一段时间,而你却在最糟糕的时候才发现它。在任何情况下,尽快找出问题并解决它始终是首要任务。
# 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值-的输出来检查退出是否出乎意料,$-是一个内置的shell值,用于保存当前的bash shell设置,看看set -e是否生效。如果脚本因错误而退出,并且错误是意外的,那么堆栈跟踪就会输出到控制台。
以下内置的shell值被用来建立堆栈跟踪。
- BASH_SOURCE:文件名的数组,其中每个命令都被调用回主脚本。
- FUNCNAME:匹配BASH_SOURCE中每个文件的行号数组。
- BASH_LINENO: 匹配BASH_SOURCE的每个文件的行号数组。
- BASH_COMMAND:最后执行的带有标志和参数的命令。
如果脚本以一种意外的方式出错,它就会在上述变量上循环,并按顺序输出每个变量,这样就可以建立一个堆栈跟踪。失败命令的行号并没有被保存在上述数组中,但这就是为什么每次命令失败时,你都会用上面的第一个陷阱语句捕获行号。
把它全部放在一起
创建以下两个文件来支持测试,所以你可以看到信息是如何在多个文件中收集的。首先,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
这个练习应该可以让你了解到,如果你的脚本中有错别字,会有什么输出和什么时候出现。