
manapi
编写的Shell壁纸调试bash脚本就像在大海捞针一样,特别是当现有代码库中出现新的添加而没有及时考虑结构,日志记录和可靠性问题时。您可能会因为自己的错误以及管理复杂的脚本而陷入这种情况。 Mail.ru云解决方案
团队翻译了一篇包含指导原则的文章,可以帮助您更好地编写,调试和维护脚本。信不信由你,没有什么比编写每次都能使用的干净,随时可用的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在开发过程中捕获错误
值得将ShellCheck之类的东西集成到您的开发和测试管道中,以验证bash代码的最佳实践。
我在本地开发环境中使用它来获取有关在开发过程中可能遗漏的语法,语义和一些代码错误的报告。它是用于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"
这可以帮助我确定需要优化的脚本中的问题/缓慢区域。
祝好运!
还有什么要读的:
- Go和GPU缓存。
- 基于S3对象存储Mail.ru Cloud Solutions中的Webhooks的事件驱动应用程序的示例。
- 我们有关数字转换的电报频道。