Shell 脚本编写的五个好习惯
Shell 脚本在自动化运维、CI/CD 和日常工具链中无处不在,但一个看似能跑的单文件脚本,往往在真实环境里暴露出各种隐患。与其靠踩坑补课,不如从写起就养成五个好习惯,让脚本更健壮、更易维护。
一、把 set -euo pipefail 放在脚本开头
这是最便宜也最有效的一道防线。-e 让脚本在任意命令非零退出时立刻终止,而不是继续往下执行并在错误状态上层层掩盖;-u 对未定义的变量直接报错,避免把空值当成合法输入传给后续命令;-o pipefail 让管道的退出码反映整条管道里第一个非零退出(默认只看最后一段),这样 grep 找不到匹配、awk 解析失败都不会被后面的 echo / cat 吞掉。建议把它写成一行,紧跟 shebang 之后:
#!/usr/bin/env bash
set -euo pipefail
如果遇到某段命令允许失败(比如尝试性清理),可以显式用 || true 或 if 分支承接,而不是全局关掉 -e,这样读脚本的人一眼就知道哪里是刻意放行。
二、变量永远加双引号
未经引号保护的 $var 遇到空格、通配符或空值,都会在词法分析阶段产生意料之外的行为。"$var" 保证变量按一个词展开;当变量为空时,"" 保留为一个空参数而不是被删除,这对 rm -f "$dir" 之类的命令尤为关键。例外是显式做 glob 展开(如 *.log)或在 for 循环的 unquoted 遍历时不加引号,但这类场景应加注释说明意图。对数组和命令替换同样适用:用 printf '%s\n' "$(cmd)" 而不是直接 echo $(cmd),避免 echo 吞掉 -n / -e 或受空参数影响。
三、用函数组织逻辑,而不是在顶层写一长串
超过三四十行的脚本就该考虑拆分了。把"准备环境 / 拉取数据 / 转换 / 输出 / 清理"拆成同名函数,主流程变成一份可读的执行清单。函数内部优先使用局部变量(local var=…),避免污染全局;需要跨函数共享的状态尽量通过参数和返回值传递,而不是依赖隐式的全局变量。遇到复杂的分支,让函数只做一件事并返回退出码,调用方根据退出码决定重试、降级还是中止,后续加上日志后也更容易定位是哪一层出了问题。
四、重视日志与退出码
生产脚本最怕"静默失败"。与其满屏 echo,不如写一个 log 函数,统一带上时间戳和级别,输出到 stderr,方便和标准输出分离:
log_info() { echo "[INFO] $(date '+%Y-%m-%dT%H:%M:%S%z') $*" >&2; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%dT%H:%M:%S%z') $*" >&2; }
配合 set -e 时,最后一条执行的语句的退出码就是脚本的退出码,因此在函数末尾不要把重要检查藏在管道里,也不要无差别 echo "done" 覆盖真实错误。对外暴露的脚本应在文档或 help 分支中写清每个非零退出码的含义(例如 1 参数错误、2 环境缺失、3 运行失败),方便上层 CI 或调用脚本做判断。
五、在提交前用 shellcheck 跑一遍
shellcheck 几乎能覆盖上面四条的常见疏忽:它会提醒你 set 选项缺失、变量未加引号、$(…) 内的换行风险、被命令替换吞掉的返回值,以及 grep | while 造成的子 shell 作用域丢失等。在本地直接 shellcheck script.sh,或把它挂到 pre-commit / CI 阶段,让检查变成流程而不是记忆。遇到 shellcheck 报告但确有理由的场景,用 # shellcheck disable=SCxxx 在行内关闭并附原因注释,而不是全局关掉。记住:shellcheck 报告的是"可能出问题的写法",最终是否修正仍取决于你对执行环境的判断,但它能把大量低级错误拦截在运行之前。
小结
五个习惯并不冲突,反而互相放大效果:set -euo pipefail 让错误尽早暴露,引号让变量行为可预测,函数让结构清晰,日志与退出码让故障可追踪,shellcheck 在前置环节兜底。坚持下来,脚本从"能跑"进化到"敢在生产跑",维护成本会显著下降。
- 点赞
- 收藏
- 关注作者
评论(0)