Shell 脚本看似简单,但很多不易察觉的细节会在特定条件下触发严重错误。以下是实际工作中频繁“坑人”的场景和应对策略,掌握它们能大幅提升脚本的健壮性。
坑点 1:变量未加引号导致单词拆分和通配符展开
file="my document.txt"
rm $file # 危险!变量被拆分为 "my" 和 "document.txt",可能误删文件
当变量值包含空格、制表符,或恰好包含 *、? 等通配符时,不加引号的变量会被 shell 拆分为多个参数,甚至被展开为当前目录下的匹配文件名。始终用双引号包裹变量:rm "$file"。唯一例外是当你明确需要单词拆分(如使用 $@ 时),但也应使用 "$@" 保留原样。
坑点 2:set -e(出错即退出)的行为并不直观set -e 让脚本在任意命令返回非零状态时立即退出,看起来可以防止错误蔓延,但许多场景下它会意外终止脚本,或者出乎意料地不生效:
- 管道中只有最后一个命令的退出码被检查,前面的失败会被忽略;
- 命令在
if、while、&&、||等条件中执行时,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,循环将被拆分为 my 和 report.txt 两次迭代。正确方法是使用通配符配合双引号:
for f in *.txt; do
[ -e "$f" ] || continue
echo "处理文件: $f"
done
需要处理任意路径时,使用 find 结合 -print0 和 while 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;假设 awk、sed 版本一致,却遇到 BSD 与 GNU 行为差异。用环境变量或 command -v 查找工具、允许通过变量覆盖路径,例如:PYTHON="${PYTHON:-python3}"。同时,使用 #!/usr/bin/env bash 作为 shebang 可提高跨平台兼容性,但要注意它可能引入环境变量中的恶意 Python 或 Bash。
总结:打造强健脚本的习惯
- 所有变量引用加双引号:
"$var"。 - 每个可能失败的关键命令后检查返回值。
- 使用
[[ ]]代替[ ](如果使用 Bash)。 cd和rm操作前先验证路径。- 启用
set -u(未定义变量报错)、set -o pipefail,但不要盲目依赖set -e。 - 先小范围测试,再逐步扩展。
每次写完脚本后用 ShellCheck 进行静态检查,它能自动捕获上述绝大部分陷阱,是避免低级错误的最有效工具。