我的检查脚本一直说“干净”,直到我给它喂了一个脏样本

我的检查脚本一直说“干净”,直到我给它喂了一个脏样本

半路手记 · 踩坑实录

背景:我给发布流程装了三道门禁

我的站是公开的,靠搜索引擎能被翻出来。所以在发布流程上我设了一条硬规矩:某些关键词绝不能出现在站上。

为了守住这条规矩,我写了三道扫描:

  1. 源文件扫描——构建之前,扫一遍这次要发的内容文件。
  2. 构建产物扫描——构建之后,扫一遍 dist/ 目录,防的是“源文件没有、但被模板拼进去了”。
  3. 线上扫描——部署之后,curl 一遍线上页面,防的是“构建产物没有、但服务器上还有旧文件”。

三道都返回零命中,才允许继续。这套设计我觉得挺完整——三个环节各扫一遍,任何一个漏了还有下一个兜着。

半年来,它们一次都没报过红。

我以为那是因为站上确实干净。

那天我拿一个已知的脏样本去试

起因是另一件事:我要确认某个文件里到底有没有出现某个词。用的是多关键词的写法:

grep -n '关键词A\|关键词B\|关键词C' 目标文件

返回零命中,退出码 1。

可我心里清楚,那个词就在文件里——我几分钟前刚在编辑器里看到过。于是我把命令拆开,只搜一个词:

grep -n '关键词A' 目标文件

命中了。

问题出在那个 \| 上。

弯路:我先怀疑了三轮,才怀疑到工具

事后复盘,我的排查顺序是这样的:

  1. 先怀疑自己记错了。 以为那个词其实不在文件里,是我想当然。
  2. 再怀疑关键词写错了。 是不是有全角半角、繁简、大小写的差异。
  3. 然后怀疑文件编码。 是不是这个文件的字节和显示的不一样。

这三轮都排除了,我才想到最后一个可能:去查 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 是“没匹配”,不是“不存在”——这两件事差得很远。
  • 统计口径会不会被无关内容顶高或者压低? 注释、引用、示例、嵌套结构,都算进去过吗?
  • 门禁报“通过”的时候,它真的跑了吗? 一个从来没报过红的检查,值得专门去验一次。

一句话

一个没被验证过能力的检查,比不做检查更危险——因为它让你放心地不看。