2236 字
约 7 分钟
17
S01-A25 · 进程替换 `<(...)`:好用,但有三个坑

一个部署脚本用 diff <(ssh web1 'cat /etc/nginx/default.conf') <(ssh web2 'cat /etc/nginx/default.conf') 比较两台机器的配置,本地跑了没问题,放到 CI 里"输出为空、退出码 0"——因为 CI 的默认 shell 是 sh(dash),而不是 bash,于是 <(...) 被当成"重定向语法错误",而错误被"命令替换的空输出"吃掉了。

进程替换(process substitution)是 bash 的扩展:把一个"命令的输出"伪装成一个"文件路径",交给另一个命令去读——这个"伪装"很实用:diff、while read、tar 这些"只吃文件"的命令,第一次能"直接吃命令输出"——**而"省下的中间文件"正是"交互排查里最烦的那一步".
于是"只能读文件"的命令也能"直接比较两个命令的结果"。

1. 它的机制:一个 /dev/fd/N

步骤 发生了什么
1 bash 创建"管道或临时文件"
2 把读端"绑定"到 /dev/fd/63 这类路径
3 把该路径"作为参数"传给命令
4 另一侧命令"打开该路径",读到的是"管道内容"

第三项解释了"为什么路径看起来怪":它不是一个"真的文件",而是"内核 fd 的一个别名"这个"别名"决定了它的两个失效条件:"fd 不跨进程"(容器/sudo)、"只读一次"(管道语义)——**而这两条恰好是"脚本化"时最需要的.
——于是"关闭 fd"或"跨进程传递"就会失效.

"是不是 bash"这件事,可以在脚本开头"硬性检查":

// 三行"保护"(放在脚本最前)
[ -n "$BASH_VERSION" ] || { echo "需要 bash,请用 bash 运行" >&2; exit 1; }
set -o pipefail
// 或:把 shebang 写死
#!/usr/bin/env bash
// 判据:脚本"入口"就失败,而不是"跑到第 40 行才炸"

2. 三个坑之一:shell 不是 bash

环境 是否支持
bash / zsh 支持
dash / ash(sh) 不支持
make 的默认 shell /bin/sh(多为 dash)
CI 的 sh -c 视镜像

第一项之外的场合都可能"直接语法错误"——**而"错误"常常表现为"命令没输出",而不是"明显报错".

"跨容器"这件事,最稳的做法是"在容器内生成、在外面比较":

// 反例:宿主机的 fd 传不进容器
docker exec web1 diff <(cat a) <(cat b)      # 不可靠

// 正解:容器内落文件,再取出来比
docker exec web1 sh -c 'cat /etc/nginx/default.conf' > /tmp/web1.conf
docker exec web2 sh -c 'cat /etc/nginx/default.conf' > /tmp/web2.conf
diff /tmp/web1.conf /tmp/web2.conf
// 判据:不依赖"fd 跨进程"这件不成立的事

3. 三个坑之二:/dev/fd 不可跨进程

场景 结果
同 shell 内的命令读它 正常
通过 sudo 传给别的用户 可能"Permission denied"
通过 docker exec 传进容器 不可用(fd 不共享)
后台进程"稍后再读" 空(写端已关)

第三项最隐蔽:**"宿主机的 /dev/fd/63 在容器里不存在"**第三项的"典型"是"一条在生产上很好用的排查命令,进了容器就'文件不存在'"——**而"文件不存在"会被误读成"配置为空".
——于是"同样一条命令,本机行、容器里不行".

"空 vs 空"可以用"一次长度检查"挡住,而它只要两行:

// 两行"防假阴性"
[ -s /tmp/web1.conf ] && [ -s /tmp/web2.conf ] || { echo "取配置失败(空文件)"; exit 1; }
// 判据:非空之后才允许"比较",否则"比较"没有意义

4. 三个坑之三:错误被"吞"

现象 原因
输出为空、退出码 0 被替换的命令"失败了",而"读的一方"读到空
diff 返回 0 两边都是空 → "没有差异"
真正的错误看不到 错误在"子 shell"的 stderr

第二项的"后果"是"假阴性":"两台机器的配置一致"这个结论,可能建立在"两边都没读到"之上——而"一致"是最不需要复核的结论.
第二项最危险:
"空 vs 空 = 相同"**——于是"两边都没取到配置"被读成"两台机器配置一致".

"校验和"比较是"只关心'是否相同'"时最省的写法:

// 一行"只比差异"(不必比内容)
diff <(sha256sum < /tmp/web1.conf) <(sha256sum < /tmp/web2.conf)
// 更省:直接比两个哈希字符串
[ "$(sha256sum < /tmp/web1.conf)" = "$(sha256sum < /tmp/web2.conf)" ] && echo same
// 判据:只需"是否一致"时,哈希比"逐行 diff"快且稳

5. 三种"稳妥"的替代

替代 说明
先落到临时文件再比较 最稳(多一步 IO)
用 bash -c 显式指定 解决"shell 不对"
用"校验和"比较 不必比内容(sha256sum)

第一项是"跨环境"的正解:**mktemp + 两次重定向 + diff**第一项的"成本"是"多两个文件、多两次 IO":在"配置文件只有几 KB"时,这点开销可以忽略——**而在"几 GB 的导出"时,它反而是"唯一可行"的(管道会背压卡死).
——**代价是"两个临时文件",收益是"在任何 shell 里都对".

// 三行"跨环境安全"的比较(不依赖 bash 扩展)
a=$(mktemp); b=$(mktemp)
ssh web1 'cat /etc/nginx/default.conf' > "$a"
ssh web2 'cat /etc/nginx/default.conf' > "$b"
diff "$a" "$b"; rc=$?; rm -f "$a" "$b"
// 关键:加上"非空校验"(否则"两边都空"被当成相同)
[ -s "$a" ] && [ -s "$b" ] || { echo "取配置失败"; exit 1; }

"交互式"的价值不该被否定,它省掉的是"最烦的一步":

// 三种"交互排查"的用法
1 比两台机器:diff <(ssh a 'cat conf') <(ssh b 'cat conf')
2 比两个时间点:diff <(git show HEAD~1:f) f
3 边看边筛:while read -r l; do echo "$l"; done < <(cmd)
// 判据:只在"自己的终端"里用,不进脚本

6. "什么场景真该用它"

场景 适合
交互式比较两个命令输出 很适合
一次性的排查(diff <(a) <(b)) 很适合
写进"要跨环境跑的脚本" 不适合
需要"错误可诊断"的 CI 不适合

7. 边界:什么时候"临时文件"更好

场景 为什么 做法
输出可能很大(GB) 管道"背压"会"卡死两侧" 临时文件 + 流式处理
需要"多次读同一份" 管道只能读一次 临时文件
需要"权限隔离"(sudo/容器) fd 不共享 临时文件
需要"保留证据"(排查) 事后可看 临时文件

最后一条与"可诊断"有关的边界:"错误要能留下证据":<(...) 的错误只出现在"那个子 shell 的 stderr",而它"常常没被收集";"临时文件 + rc + 非空校验"能把"取失败"与"真没差异"分开——而这一条,比"省一个文件"重要得多.
判断口径一句话:
"这条命令是在"我的交互终端里跑",还是"在别人的环境里跑""**。交互(自己排查),<(...) 最省事;进脚本、进 CI、进容器,就用"临时文件 + 非空校验"——而"非空校验"这一步,才是"两台机器配置一致"与"两边都没读到"的分界线.

8. 自测

  1. 进程替换的本质是什么?为什么"路径看起来怪"?
  2. 为什么"shell 不是 bash"时"错误不显眼"?
  3. 为什么 /dev/fd 不能"跨进程(容器)"使用?
  4. "输出为空、退出码 0"为什么会发生?它最危险的后果是什么?
  5. "临时文件"方案要多做哪一步才算"稳妥"?
  6. 判断"该用 <(...) 还是临时文件"的那个问题是什么?

补一句"代价"的量化:<(...) 省的是"一个中间文件",而它要求"bash + 同进程 + 只读一次 + 错误可收集"四个前提同时成立——四个前提里"脚本化"通常只满足"bash"一条。所以它更像"交互式语法糖",而不是"脚本原语";而"临时文件 + 非空校验"虽然多三行,却把这四个前提全部绕开。

9. 一句话总结

进程替换是"交互式的好工具、脚本里的坏依赖"——它把"两个命令的输出"变成"两个可比较的文件",代价是"依赖 bash、依赖同进程、且错误会被静默"。而"三行临时文件 + 非空校验"能换来"在任何环境都成立"的确定性。

S01-A25 · 进程替换 `<(...)`:好用,但有三个坑
https://www.clxhxhhr.top/posts/6700/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。