895 字
4 分钟
写出健壮的 Bash 脚本:选项、陷阱与防御式写法
在终端里能跑的命令,写进脚本就出事,是新手写 Bash 的日常。大多数事故来自几个可预期的陷阱:变量没加引号、命令失败没被察觉、空变量拼出危险路径、定时任务环境和交互终端不一样。本文给出一套防御式写法骨架。
起手式:set -euo pipefail
脚本开头建议固定写上:
#!/usr/bin/env bashset -euo pipefail三个选项各自的含义:
-e(errexit):任何命令返回非零退出码,脚本立即退出。没有它,备份脚本里「进入目录失败」之后,rm照样会在错误的位置执行。-u(nounset):使用未定义的变量直接报错。把rm -rf "$bakup_dir/"(手滑拼错变量名,展开为空)这类事故扼杀在摇篮。pipefail:管道中任一环节失败,整条管道算失败。grep error app.log | wc -l在日志文件不存在时默认仍返回 0 并输出 0,开了 pipefail 才会暴露grep的报错。
注意 -e 有些反直觉的例外:if 条件里、&&、|| 左侧的命令失败不会触发退出,这是特意保留的判断能力。
变量:永远加引号
单词分割是 Bash 事故第一大来源。文件名含空格时:
file="daily report.txt"cat $file # 报错:被拆成 "daily" 和 "report.txt" 两个参数cat "$file" # 正确:作为一个参数规则只有一条:变量展开一律加双引号,除非你明确需要分词(很少见)。相关约定:
- 命令替换用
$( )而不是反引号,可嵌套、可读性好。 - 只读变量用
readonly BACKUP_DIR=/data/backup,脚本开头的配置项建议大写。 - 拼路径时确认变量非空:
[ -n "$dir" ] || { echo "dir 为空" >&2; exit 1; }。
条件与退出码
[ ] 是命令不是语法糖,常见判断:
[ -f /etc/nginx/nginx.conf ] # 文件存在[ -d /var/log ] # 目录存在[ -z "$var" ] # 字符串为空[ "$code" -eq 200 ] # 数值比较用 -eq/-ne/-gt/-lt[ "$ans" = "y" ] # 字符串比较用 =判断上一条命令成败,直接把它放进 if:
if systemctl is-active --quiet nginx; then echo "nginx 运行中"else echo "nginx 已停止,尝试启动" >&2 sudo systemctl start nginxfi不要先执行命令再判断 $?,那会让 set -e 提前把脚本杀掉。
日志与错误输出
脚本的输出分两类:结果是 stdout,诊断信息走 stderr(>&2)。这样 脚本 > result.txt 不会把日志混进数据。一个简单可用的日志函数:
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; }循环里处理文件列表,用 find -print0 配合 read -d '',避免文件名含空格、换行时出错:
find /data -name '*.tmp' -print0 | while IFS= read -r -d '' f; do rm -- "$f"done定时任务环境差异
「手动跑没问题,cron 里就失败」几乎总是环境差异:cron 的 PATH 很短、没有交互终端、工作目录不同。对策:
- 脚本里调用外部命令写绝对路径,或在开头显式
export PATH=/usr/local/bin:/usr/bin:/bin; - 一律使用绝对路径引用文件;
- cron 条目里追加
>> /var/log/myjob.log 2>&1,留好现场; - 需要交互输入的命令(如未配置免密的 ssh)不要放进 cron。
提交前的最后一步:shellcheck
ShellCheck 是 Bash 静态检查工具,能抓出未加引号、弃用语法、误判退出码等绝大多数低级错误:
shellcheck backup.sh把它当成 Bash 的编译器警告来对待,告警清零再上线。配合上面这套骨架,脚本的可靠性会提升一个量级。
延伸阅读
写出健壮的 Bash 脚本:选项、陷阱与防御式写法
https://meow.us.kg/posts/bash-scripting-robust-practices/分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
最后更新于 ,距今已过 46 天
部分内容可能已过时