人人都会AI编程

15.5 常见脚本坑点与避坑指南

更新时间:2026-07-12

Shell 脚本看似简单,但很多不易察觉的细节会在特定条件下触发严重错误。以下是实际工作中频繁“坑人”的场景和应对策略,掌握它们能大幅提升脚本的健壮性。

坑点 1:变量未加引号导致单词拆分和通配符展开

file="my document.txt"
rm $file          # 危险!变量被拆分为 "my" 和 "document.txt",可能误删文件

当变量值包含空格、制表符,或恰好包含 *? 等通配符时,不加引号的变量会被 shell 拆分为多个参数,甚至被展开为当前目录下的匹配文件名。始终用双引号包裹变量rm "$file"。唯一例外是当你明确需要单词拆分(如使用 $@ 时),但也应使用 "$@" 保留原样。

坑点 2:set -e(出错即退出)的行为并不直观
set -e 让脚本在任意命令返回非零状态时立即退出,看起来可以防止错误蔓延,但许多场景下它会意外终止脚本,或者出乎意料地不生效:

  • 管道中只有最后一个命令的退出码被检查,前面的失败会被忽略;
  • 命令在 ifwhile&&|| 等条件中执行时,set -e 被自动屏蔽;
  • 子 shell 中的失败不会导致父 shell 退出。

更健壮的做法是显式检查关键命令的退出状态:

if ! command; then
    echo "命令失败" >&2
    exit 1
fi

如果仍要使用 set -e,建议同时设置 set -o pipefail,让管道中任意命令失败都返回非零。

坑点 3:管道中的错误被悄然吞没

cat /var/log/app.log | grep "ERROR" | wc -l

如果 grep 找不到匹配行,它会返回退出码 1,但管道只取最后一个命令 wc -l 的退出码(通常为 0),整个组合看起来永远成功。在需要捕获 grep 结果的场景下,可以启用 pipefail 选项:

set -o pipefail
cat /var/log/app.log | grep "ERROR" | wc -l

或者在管道后检查 ${PIPESTATUS[@]} 数组,逐个确认每个命令的退出码。

坑点 4:[[[ 的差异带来的隐患
[ 是 POSIX 标准命令,功能有限,容易出错;[[ 是 bash 内置的增强版,支持更多运算符且无需引用变量。例如:

var=""
[ $var = "test" ]      # 语法错误,$var 为空时变成 [ = "test" ]
[[ $var = "test" ]]    # 正常工作,bash 自动处理空值

另外 [ 不支持正则匹配和 <> 字符串比较(会误解为重定向),而 [[ 提供了 =~ 正则操作符和安全的比较。在编写 Bash 脚本时,建议统一使用 [[ 进行条件测试。

坑点 5:cd 后未检查是否成功

cd /some/path
rm -rf *      # 如果 cd 失败,* 会在当前目录执行,灾难!

必须立即检查 cd 的退出状态,或使用 cd /some/path || exit 1 阻止后续命令运行。更严谨的做法是用大括号或子 shell 限制破坏范围:

pushd /some/path && { rm -rf ./*; popd; }

对于关键脚本,应该设置 set -u,并在脚本开头保存当前目录(original_dir=$(pwd)),最后用 cd "$original_dir" 恢复。

坑点 6:循环中处理包含空格或特殊字符的文件名

for f in $(ls *.txt); do
    echo "处理文件: $f"
done

如果某个文件名是 my report.txt,循环将被拆分为 myreport.txt 两次迭代。正确方法是使用通配符配合双引号:

for f in *.txt; do
    [ -e "$f" ] || continue
    echo "处理文件: $f"
done

需要处理任意路径时,使用 find 结合 -print0while IFS= read -r -d '' 来安全读取。

坑点 7:使用 rm -rf 时变量未定义或为空

rm -rf /$TMP_DIR      # 若 TMP_DIR 未设置,删除的就是根目录!

必须在使用前检查变量非空,或者使用参数扩展设置默认值:

rm -rf "${TMP_DIR:?TMP_DIR 未设置}"   # 如果未设置,脚本立即出错退出
rm -rf "${TMP_DIR:-/tmp/default}"    # 未设置时使用 /tmp/default

更重要的是,绝对不要将用户输入直接拼接到 rm 命令中。

坑点 8:命令替换中的尾随换行符被自动删除

value=$(cat file.txt)
echo "内容:$value"

命令替换 $(...) 会删除输出末尾的所有换行符,可能导致文件结尾的空行丢失。如果需要保留精确的输出内容(包括尾部空行),可以在读取后手动添加一个占位符:

value=$(cat file.txt; echo x)
value="${value%x}"

或者使用 readarray 将行读入数组逐一处理。

坑点 9:硬编码路径和依赖外部命令的假设
脚本中直接写 /usr/bin/python3,在另外环境中 Python 可能位于 /usr/local/bin;假设 awksed 版本一致,却遇到 BSD 与 GNU 行为差异。用环境变量或 command -v 查找工具、允许通过变量覆盖路径,例如:PYTHON="${PYTHON:-python3}"。同时,使用 #!/usr/bin/env bash 作为 shebang 可提高跨平台兼容性,但要注意它可能引入环境变量中的恶意 Python 或 Bash。

总结:打造强健脚本的习惯

  • 所有变量引用加双引号:"$var"
  • 每个可能失败的关键命令后检查返回值。
  • 使用 [[ ]] 代替 [ ](如果使用 Bash)。
  • cdrm 操作前先验证路径。
  • 启用 set -u(未定义变量报错)、set -o pipefail,但不要盲目依赖 set -e
  • 先小范围测试,再逐步扩展。

每次写完脚本后用 ShellCheck 进行静态检查,它能自动捕获上述绝大部分陷阱,是避免低级错误的最有效工具。