半路手记 · 踩坑实录
背景:我给发布流程装了三道门禁
我的站是公开的,靠搜索引擎能被翻出来。所以在发布流程上我设了一条硬规矩:某些关键词绝不能出现在站上。
为了守住这条规矩,我写了三道扫描:
- 源文件扫描——构建之前,扫一遍这次要发的内容文件。
- 构建产物扫描——构建之后,扫一遍
dist/目录,防的是“源文件没有、但被模板拼进去了”。 - 线上扫描——部署之后,
curl一遍线上页面,防的是“构建产物没有、但服务器上还有旧文件”。
三道都返回零命中,才允许继续。这套设计我觉得挺完整——三个环节各扫一遍,任何一个漏了还有下一个兜着。
半年来,它们一次都没报过红。
我以为那是因为站上确实干净。
那天我拿一个已知的脏样本去试
起因是另一件事:我要确认某个文件里到底有没有出现某个词。用的是多关键词的写法:
grep -n '关键词A\|关键词B\|关键词C' 目标文件
返回零命中,退出码 1。
可我心里清楚,那个词就在文件里——我几分钟前刚在编辑器里看到过。于是我把命令拆开,只搜一个词:
grep -n '关键词A' 目标文件
命中了。
问题出在那个 \| 上。
弯路:我先怀疑了三轮,才怀疑到工具
事后复盘,我的排查顺序是这样的:
- 先怀疑自己记错了。 以为那个词其实不在文件里,是我想当然。
- 再怀疑关键词写错了。 是不是有全角半角、繁简、大小写的差异。
- 然后怀疑文件编码。 是不是这个文件的字节和显示的不一样。
这三轮都排除了,我才想到最后一个可能:去查 grep 本身是哪个 grep。
这一步本来该是第一步的。
根因:这台机器上的 grep,不是我以为的那个 grep
$ which -a grep
<工具链目录>/shim/grep
/usr/bin/grep
$ grep --version
toybox 0.8.13 (is not GNU grep 9.0)
那行括号不是我加的批注,是 toybox 自己打的——换成 grep --version 以外的调法,它只输出 toybox 0.8.13。
PATH 里排在第一位的是一个工具链自带的 grep,它是 toybox 的精简实现。而 toybox 的 grep 不支持 BRE 里的 \| 交替符。
做个最小复现:
$ printf '正常行\n这里有 关键词A 这三个字\n' > /tmp/t.txt
# 写法一:BRE 交替符
$ grep -n '关键词A\|关键词B' /tmp/t.txt
$ echo $?
1 # 零命中
# 写法二:ERE 竖线
$ grep -nE '关键词A|关键词B' /tmp/t.txt
2:这里有 关键词A 这三个字
$ echo $?
0 # 命中
命令本身是对的,规则本身也是对的——只是这个 grep 实现不认这种写法,而它不会告诉你它不认。
它没有报“语法错误”,没有警告,只是安静地返回了“没找到”。
顺带一个对照:/usr/bin/grep(BSD grep)是认 \| 的。所以同一行命令:
- 在终端里手敲——对
- 在带这个工具链的环境里跑——错
“我在终端里试过了,没问题”,证明不了脚本跑起来没问题。两者的 PATH 不是同一个。
后果:三道门禁,三道全是摆设
真正让我后背发凉的是接下来这一步——我回到那三道扫描,看了它们的写法。
三道全用的同一个 BRE 写法。
也就是说:
- 源文件里真的写了不该写的词 → 它说“干净”
- 构建产物里真的有 → 它说“干净”
- 线上真的有 → 它说“干净”
它们从来没有报过红,不是因为站上干净,是因为它们根本不会报红。
这套门禁的设计、分层、执行时机都是对的,唯一的问题是:它们没有能力说“不”。
而这比没有门禁更麻烦。没有门禁,我知道自己在裸奔,会手动看一眼;门禁说“干净”,我就不看了。一个永远通过的检查,买到的是虚假的安全感。
我把这类失效叫 fail-open——出错的时候不报错,而是返回“通过”。一个 fail-open 的门禁,等于把风险藏进了流程里。
修法:改写法,并且让门禁先自证
两件事。
第一,把写法改对。 多关键词一律用 -E:
grep -rn -iE '关键词A|关键词B|关键词C' 目标文件
-E 是 POSIX 的扩展正则,BSD grep、GNU grep、toybox 三种实现下行为一致。用方言写法换来的那点敲键的便利,不值这个风险。
第二,给门禁加一道自检——这一步比第一条重要。
在检查业务之前,先造一个必然违规的样本喂给它,它必须命中:
echo '关键词A' | grep -qiE '关键词A' \
&& echo "门禁自检 OK" \
|| { echo "🔴 门禁自身失效,停手"; exit 1; }
顺序不能反:先证明这个检查有能力说“不”,再让它去检查东西。
改完我把全站已发布的 26 篇重新扫了一遍,零命中。
这个“零命中”才是有意义的——因为这时候的“干净”是门禁有能力说“不”之后的“干净”。
反向的同类失效:门禁也会误报
同一天我还撞上一次反着来的:一个用来数重复标题的自校验公式报了假警——它说文档里有 6 处重复,我去看,一处都没有。原因是那份文档的正文里引用了若干形如 ### 2026-08-18 的字符串,被计数算了进去,把结果顶过了阈值。(完整经过我另写一篇交代。)
它和上面那三道门禁,是同一件事的两面:
- 一个检查漏报(该报的没报)
- 一个检查误报(不该报的报了)
方向相反,病因相同:判据本身从没被验证过。
如果你也写检查脚本
- 门禁有没有自检? 造一个必然违规的正样本喂给它,它必须命中;命不中就停手。
-
0 命中是怎么得到的? 换一种写法、换一个工具、只搜一个关键词,再验一次。 - 退出码要读懂。
1是“没匹配”,不是“不存在”——这两件事差得很远。 - 统计口径会不会被无关内容顶高或者压低? 注释、引用、示例、嵌套结构,都算进去过吗?
- 门禁报“通过”的时候,它真的跑了吗? 一个从来没报过红的检查,值得专门去验一次。
一句话
一个没被验证过能力的检查,比不做检查更危险——因为它让你放心地不看。