← 全部笔记

Shell 脚本写稳一点:几个默认就该加的东西

2026-10-31 工具与效率 约 7 分钟

脚本写得越多越会发现:出问题的往往不是逻辑,是「某一步失败了但没人发现,后面一路错下去」。 几行约定能挡掉大部分。

一、开头三行

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

三个开关各自管什么:

  • -e 任何命令返回非零就退出。没它的话,中间某步失败会继续往下跑。
  • -u 引用未定义变量时报错退出。防止 rm -rf "$PREFIX/" 里 PREFIX 拼错变成空串。
  • -o pipefail 管道的返回值取「最后一个失败的命令」,而不是最后一个命令。 默认行为下 false | true 算成功,很容易掩盖错误。

IFS 改成只有换行和制表符,是为了防止文件名里有空格时被拆成多个参数—— 这在批量处理文件时特别关键。

二、set -e 并不总是生效

这是最容易误解的一点。set -e 在这些位置不生效:

  • 作为 if / while / until 的条件。
  • 前面有 ! 取反。
  • 命令在 && 或 || 的左侧(除了最后一个)。
  • 函数调用被放在上述任何位置时,函数体内整段都不受 -e 保护。
# 这里 ls 失败不会退出脚本,因为它在 if 条件里
if ls /nope; then echo ok; fi

# 这里会退出(命令失败且不在条件位置)
ls /nope

需要显式失败时,用 || 兜住:

curl -fsS "$URL" -o out.json || { echo "下载失败"; exit 1; }

三、引号:能加就加

# 危险:变量为空或被空格拆开时行为全变
cp $src $dst
rm -rf $dir/

# 安全
cp "$src" "$dst"
rm -rf "${dir:?}"      # 变量为空或未设置时直接报错退出

${var:?} 这个写法值得记住:它保证「变量必须有值,否则终止」。 出现在 rm -rf 前面时,等于给删除操作加了一道保险。

遍历文件时别用 for f in $(ls)——文件名带空格就散架了:

for f in ./*.log; do
  [ -e "$f" ] || continue     # 没有匹配时 glob 不会展开,要跳过
  gzip "$f"
done

四、[[ ]] 比 [ ] 好用

[ "$a" = "$b" ]          # 需要处处加引号,否则空值报语法错
[[ $a == "$b" ]]        # 不做分词,变量不加引号也安全

[[ ]] 还支持这几样 [ ] 没有的:

[[ $name == *.txt ]]      # 通配匹配
[[ $name =~ ^[0-9]+$ ]]   # 正则匹配
[[ -f $f && -r $f ]]      # 逻辑组合,写在里面更清晰

五、临时文件与 trap

tmp=$(mktemp -d) || exit 1
trap 'rm -rf "$tmp"' EXIT

mktemp 生成不重复的名字,-d 表示建目录。 自己拼 /tmp/myapp.$$ 有被抢占的风险(符号链接攻击),不只是重名问题。

trap ... EXIT 在脚本以任何方式退出时都会执行, 包括中途报错和 Ctrl-C。没有它的话,脚本一挂就留下一堆临时文件和残留进程。

# 同时清理子进程
trap 'kill "$pid" 2>/dev/null; rm -rf "$tmp"' EXIT INT TERM

六、几个常用小技巧

读取输入的每一行

while IFS= read -r line; do
  echo "$line"
done < list.txt

-r 让反斜杠不被解释;IFS= 保住了行首行尾的空格。 少了任意一个,某些行都会被悄悄改掉。

传参用 "$@"

myfunc() {
  printf '%s\n' "$@"      # 每个参数单独一行,带空格的也能正确保留
}
myfunc "a b" c

"$@" 展开成一个个独立参数,$* 会先拼成一个字符串再拆开,两者不可混用。

取脚本所在目录

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)

这样无论从哪里调用脚本,脚本内的相对路径都基于脚本自身位置,而不是当前工作目录—— 定时任务里这条几乎是必需的。

七、跑一遍 shellcheck

shellcheck run.sh

它会把「变量没加引号」「cd 没检查返回值」「[ ] 里用了 &&」 这类问题直接指出来,而且还解释为什么。写完脚本先过一遍,比之后再查快得多。

最后一条经验:脚本里所有「不可能失败」的命令,都值得想一下失败时会怎样。 真正难查的故障,几乎都出在这些地方。


一句话总结:开头三行 set、变量全加引号、临时文件用 mktemp 配 trap;记住 set -e 在管道和条件里会失效。