案例 Shell 生产事故复盘:for i in $(ls) 是怎么删错路径的

2026-08-29 21:12:32 views 9

Shell 生产事故复盘:for i in $(ls) 是怎么删错路径的

一起 CI/CD 数据丢失事故,起因是这种常见写法:

for i in $(ls /data/logs/*.log); do
    rm -rf "${i}"
done

某天日志目录里出现了一个带空格的文件名:access 2024-01.log$(ls ...) 命令替换后,for 循环按默认 IFS(空格、Tab、换行)做词分割,这一行输出被拆成两个词:/data/logs/access2024-01.log。第一个词恰好是归档目录 access,于是 rm -rf 把这个目录整个删了。数据直接丢失。

这个现象可以自己验证:

mkdir -p /tmp/demo && cd /tmp/demo
mkdir old_access
touch "old_access 2024-01.log"
for i in $(ls *.log); do echo "rm -rf ${i}"; rm -rf "${i}"; done

输出里会出现 rm -rf old_access,然后该目录被删。

1. 循环文件用 find -print0,不要用 for + ls

安全做法是让分隔符不再依赖空格或换行:

find /data/logs -maxdepth 1 -type f -name "*.log" -print0 | xargs -0 rm -f

-print0 用空字符分隔文件名,xargs -0 按空字符读取,因此空格、换行、特殊字符都不会破坏参数。如果需要在循环里执行更复杂操作,用:

while IFS= read -r -d '' file; do
    echo "delete: $file"
    rm -f "${file}"
done < <(find /data/logs -maxdepth 1 -type f -name "*.log" -print0)

2. 变量引用必须加双引号

rm -rf $filefile="foo bar" 时会变成 rm -rf foo bar,等于删除两个路径。"${file}" 把整个变量内容作为一个整体,空格注入无效。这是最基本的保命符。

3. 用 set -euo pipefail 替代手工检查 $?

很多老脚本写 command; if [ $? -eq 0 ]; then ...,但链条一长就容易漏检。成熟做法是脚本开头直接声明:

#!/usr/bin/env bash
set -euo pipefail
  • -e:任意命令失败立即退出,避免一个报错后脚本继续执行,扩大破坏范围。
  • -u:使用未定义变量直接报错,避免空变量变成危险路径,比如 rm -rf "${UNDEFINED_VAR}/"
  • -o pipefail:管道中任何一个环节失败,整体都算失败。没有它时,mysqldump db | gzip > backup.gz 中即使 dump 失败,gzip 成功,管道返回码也是 0,后续误以为备份成功。
  • 调试时加 -x,会打印每一条实际执行的命令。

4. 管道右侧是子进程,变量修改带不回来

这个陷阱非常隐蔽:

count=0
cat log | while read line; do
    count=$((count+1))
done
echo "$count"   # 输出 0

管道右侧的 while 在子 shell 中执行,count 的修改只发生在子进程内,循环结束子进程销毁,主 shell 里的 count 还是 0。正确写法是使用输入重定向:

count=0
while read -r line; do
    count=$((count+1))
done < log
echo "$count"

这样循环留在当前 shell 进程,变量修改才能保留。

5. 文本处理多使用 awk

简单的 grepcut 链在处理列数不定、空白不规则的日志时很脆弱。awk 天然按列处理,也更易保证逻辑正确:

ps aux | awk '$11 ~ /nginx$/ {print $2}'

这比 ps aux | grep nginx | grep -v grep | awk '{print $2}' 少很多坑。

6. trap 保证异常退出时清理

脚本中途退出,临时文件可能残留,甚至被后续任务误用。用 trap 统一清理:

TMP_FILE=$(mktemp)

cleanup() {
    rm -f "${TMP_FILE}"
    echo "清理资源完成"
}

trap cleanup EXIT INT TERM

EXIT 捕获脚本正常或异常退出,INTTERM 捕获 Ctrl+C 与 kill 信号。这样即使 set -e 触发强制退出,也能收尾。

7. crontab / Jenkins 里 PATH 不一致

脚本手动运行正常,放到 crontab 或 Jenkins 里却提示 command not found,多半是环境变量 PATH 不同。cron 环境下 PATH 往往只有 /usr/bin:/bin,而你的命令行里有 /usr/local/bin。解决办法:

  • 脚本开头显式设置 export PATH="/usr/local/bin:/usr/bin:/bin"
  • 外部命令全部使用绝对路径
  • 排查时先执行 env | sort 打印实际环境
  • 未开启 set -e 的情况下,每个可能失败的命令后都要处理 $?,不能跳到下一个命令才判断

生产级脚本模板

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

TMP_FILE=$(mktemp)

cleanup() {
    rm -f "${TMP_FILE}"
    echo "清理资源完成"
}

trap cleanup EXIT INT TERM

# 主逻辑

把每段脚本都当作会运行在带空格文件名、空变量、坏管道、无 PATH 的环境来写。for i in $(ls) 这类写法,应该永远留在个人测试目录,而不是生产。

复制全文 生成海报 Shell 运维 健壮性

推荐文章

程序员茄子在线接单