Shell脚本编写习惯

生产级 Shell 脚本编写规范指南

本文旨在为 Linux 用户及工程师提供一套标准化的 Shell 脚本编写实践。遵循这些规范可以显著降低脚本在生产环境中的不可预见性,并提高代码的可维护性。

1. 解释器声明 (Shebang)

标准做法: #!/usr/bin/env bash

  • 技术原理: 不同 Unix-like 系统的 Bash 二进制文件路径可能不同(如 /bin/bash/usr/local/bin/bash)。env 命令会在当前系统的 PATH 环境变量中动态查找解释器路径。
  • 适用场景: 需要在 macOS、Ubuntu、CentOS 等多种发行版间移植的脚本。

2. 错误处理与防御性编程 (Shell Options)

标准做法: 在脚本开头显式声明执行选项。

1
2
3
4
set -o errexit
set -o nounset
set -o pipefail

或简写为:set -euo pipefail

  • errexit (-e): 脚本中任何命令返回码非零时立即退出,遵循“及早失败”原则。
  • nounset (-u): 尝试引用未定义变量时视为错误并退出,防止因变量名拼写错误导致的意外操作。
  • pipefail: 确保管道命令中任何一个环节失败,整行命令即被视为失败。

3. 变量引用的规范化

标准做法: 始终在引用变量时使用双引号 "$VAR"

  • 原因: 防止单词拆分 (Word Splitting) 和通配符扩展 (Globbing)。未加引号的引用会导致 Shell 将包含空格或特殊字符的变量解析为多个参数。

4. 条件测试表达式

标准做法: 优先使用 [[ ... ]] 而非 [ ... ]test

  • 技术优势: [[ 是 Bash 内建关键字,支持 &&|| 以及正则表达式匹配 (=~),且在进行字符串比较时无需对变量加额外引号也可避免语法错误。

5. 路径处理与执行环境无关化

标准做法: 通过脚本自身定位其所在目录,避免使用相对路径。

1
2
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"

  • 应用场景: 当脚本需要加载配置文件或调用同目录下的其他组件时,使用 "$SCRIPT_DIR/config.conf" 可确保脚本从系统任意路径被调用时都能正常定位资源。

6. 函数内部的作用域控制

标准做法: 在函数内声明变量时必须使用 local 关键字。

  • 原因: Shell 默认变量作用域为全局。不使用 local 会导致函数内部变量意外覆盖全局同名变量,造成难以追踪的状态污染。

7. 资源清理与信号捕获 (Resource Management)

标准做法: 使用 trap 确保资源(如临时文件、锁文件)在脚本退出时被正确释放。

1
2
3
tmp_file=$(mktemp)
trap 'rm -f "$tmp_file"' EXIT

  • 技术要点: EXIT 信号涵盖了正常退出、出错退出以及接收到终止信号(如 Ctrl+C)的情况。

8. 静态代码分析工具 (ShellCheck)

标准做法: 在发布前使用 shellcheck 扫描脚本。

注意: shellcheck 并非 Linux 内置命令,需根据发行版自行安装(如 apt install shellcheck)。它已成为 Shell 脚本工程化的事实标准。

  • 基本用法: shellcheck your_script.sh
  • 核心价值:
  1. 自动识别逻辑缺陷: 如未加引号的变量、无效的条件判断、潜在的资源泄露。
  2. 强制符合最佳实践: 提供具体的规则代码(如 SC2086),并给出详细的修改建议。
  3. CI/CD 集成: 在自动化流水线中,可以使用 -f json-f checkstyle 格式输出结果,作为代码合并的前置检查条件。

总结:专业脚本模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bash

# 1. 开启安全模式
set -euo pipefail

# 2. 获取脚本绝对路径
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"

# 3. 资源清理钩子
cleanup() {
# 执行清理逻辑
:
}
trap cleanup EXIT

# 4. 函数定义 (使用 local 变量)
main() {
local task_name="Production_Deploy"
echo "Starting $task_name in $SCRIPT_DIR..."
}

main "$@"