调试 bash 脚本就像大海捞针,特别是当现有代码库中出现新的添加内容而没有及时考虑结构、日志记录和可靠性问题时。 您可能会发现自己陷入这种情况,要么是由于自己的错误,要么是在管理复杂的脚本堆时。
团队
在文章中,作者分享了他在过去几年中所学到的知识,以及一些让他措手不及的常见错误。 这很重要,因为每个软件开发人员在其职业生涯的某个阶段都会使用脚本来自动化日常工作任务。
陷阱处理程序
当脚本执行期间发生意外情况时,我遇到的大多数 bash 脚本都不会使用有效的清理机制。
惊喜可能来自外部,例如接收来自核心的信号。 处理此类情况对于确保脚本足够可靠以在生产系统上运行非常重要。 我经常使用退出处理程序来响应这样的场景:
function handle_exit() {
// Add cleanup code here
// for eg. rm -f "/tmp/${lock_file}.lock"
// exit with an appropriate status code
}
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trap
是一个 shell 内置命令,可帮助您注册一个清理函数,在出现任何信号时调用该函数。 然而,对于处理者应特别小心,例如 SIGINT
,这会导致脚本中止。
此外,在大多数情况下,您应该只捕获 EXIT
,但想法是您实际上可以为每个单独的信号自定义脚本的行为。
内置设置功能 - 错误时快速终止
错误发生后立即响应并迅速停止执行非常重要。 没有什么比继续运行这样的命令更糟糕的了:
rm -rf ${directory_name}/*
请注意变量 directory_name
还没决定。
使用内置函数来处理此类场景很重要 set
,例如 set -o errexit
, set -o pipefail
или set -o nounset
在脚本的开头。 这些函数确保您的脚本在遇到任何非零退出代码、使用未定义的变量、通过管道传递的无效命令等时立即退出:
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
function print_var() {
echo "${var_value}"
}
print_var
$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable
注: 内置函数,例如 set -o errexit
,一旦出现“原始”返回代码(零除外),就会退出脚本。 因此最好引入自定义错误处理,例如:
#!/bin/bash
error_exit() {
line=$1
shift 1
echo "ERROR: non zero return code from line: $line -- $@"
exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code
以这种方式编写脚本迫使您更加小心脚本中所有命令的行为,并在错误出乎您意料之前预见到错误的可能性。
ShellCheck 用于检测开发过程中的错误
值得集成类似的东西
我在本地开发环境中使用它来获取有关语法、语义以及我在开发时可能错过的代码中的一些错误的报告。 这是一个针对 bash 脚本的静态分析工具,我强烈建议使用它。
使用您自己的退出代码
POSIX 中的返回码不仅是零或一,而且是零或非零值。 使用这些功能可为各种错误情况返回自定义错误代码(201-254 之间)。
然后,包装您的脚本的其他脚本可以使用此信息来准确了解发生的错误类型并做出相应的反应:
#!/usr/bin/env bash
SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241
function read_file() {
if ${file_not_found}; then
return ${FILE_NOT_FOUND}
fi
}
注: 请特别小心您定义的变量名称,以避免意外覆盖环境变量。
记录功能
漂亮且结构化的日志记录对于轻松理解脚本的结果非常重要。 与其他高级编程语言一样,我总是在 bash 脚本中使用本机日志记录功能,例如 __msg_info
, __msg_error
等。
这有助于通过仅在一处进行更改来提供标准化的日志记录结构:
#!/usr/bin/env bash
function __msg_error() {
[[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}
function __msg_debug() {
[[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}
function __msg_info() {
[[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}
__msg_error "File could not be found. Cannot proceed"
__msg_debug "Starting script execution with 276MB of available RAM"
我通常尝试在我的脚本中使用某种机制 __init
,其中此类记录器变量和其他系统变量被初始化或设置为默认值。 这些变量也可以在脚本调用期间从命令行选项设置。
例如,类似:
$ ./run-script.sh --debug
当执行这样的脚本时,它确保将系统范围的设置设置为默认值(如果需要),或者至少在必要时初始化为适当的值。
我通常根据用户界面和用户可以/应该深入研究的配置细节之间的权衡来选择初始化什么和不做什么。
重用和清理系统状态的架构
模块化/可重用代码
├── framework
│ ├── common
│ │ ├── loggers.sh
│ │ ├── mail_reports.sh
│ │ └── slack_reports.sh
│ └── daily_database_operation.sh
我保留了一个单独的存储库,可以用它来初始化我想要开发的新项目/bash 脚本。 任何可以重用的内容都可以存储在存储库中,并由想要使用该功能的其他项目检索。 以这种方式组织项目可以显着减少其他脚本的大小,并确保代码库较小且易于测试。
如上例所示,所有日志记录功能,例如 __msg_info
, __msg_error
其他的,例如 Slack 报告,单独包含在 common/*
并在其他场景中动态连接,例如 daily_database_operation.sh
.
留下一个干净的系统
如果您在脚本运行时加载任何资源,建议将所有此类数据存储在具有随机名称的共享目录中,例如 /tmp/AlRhYbD97/*
。 您可以使用随机文本生成器来选择目录名称:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
工作完成后,可以在上面讨论的挂钩处理程序中提供此类目录的清理。 如果不处理临时目录,它们会累积并在某个阶段导致主机出现意外问题,例如磁盘已满。
使用锁定文件
通常,您需要确保在任何给定时间一台主机上仅运行一个脚本实例。 这可以使用锁定文件来完成。
我通常创建锁定文件 /tmp/project_name/*.lock
并检查它们是否出现在脚本的开头。 这有助于脚本正常终止,并避免并行运行的另一个脚本对系统状态进行意外更改。 如果您需要在给定主机上并行执行相同的脚本,则不需要锁定文件。
衡量和改进
我们经常需要使用长时间运行的脚本,例如日常数据库操作。 此类操作通常涉及一系列步骤:加载数据、检查异常、导入数据、发送状态报告等。
在这种情况下,我总是尝试将脚本分解为单独的小脚本,并使用以下方法报告它们的状态和执行时间:
time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1
稍后我可以通过以下方式查看执行时间:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
这有助于我识别脚本中需要优化的问题/缓慢区域。
祝你好运!
还有什么要读的:
来源: habr.com