git ls-files -z | xargs -0 LC_ALL=C grep 写法中,LC_ALL=C 被 xargs 当作要执行的程序而非环境变量,导致 grep 根本没运行,命令以成功码静默退出。
我在一次搜索中损失了大约四十分钟。
那个命令是在整个仓库中查找一个字符串,类似这种命令你每天都会不加思索地跑二十次。它没有打印任何匹配。我理解成了没有匹配。它返回了退出码 0,而一次成功但无结果的搜索也会返回 0,所以没有任何东西引起注意。
实际有 218 个匹配。
那个命令是 git ls-files -z | xargs -0 LC_ALL=C grep -n 'PATTERN'。如果你以前写过这行,再看一下。xargs 把跟在它后面的第一个 token 作为要执行的程序。那个 token 是 LC_ALL=C。它不是程序。xargs 试图执行它,什么都没得到,没有输出,然后返回成功。grep 从来没有运行过。locale 变量把命令吃掉了。
修复方法是 xargs -0 env LC_ALL=C grep。一个单词。但之所以花了四十分钟不是因为这个 bug 很隐蔽——而是因为它的失败表现得完全像一个正确结果。Empty stdout, empty stderr, exit 0. Every signal I would have used to check it agreed that the search had worked and found nothing.
这是那周我第五次遇到这种情况,也是我唯一能抓住这次的原因。
当 pattern 包含 ="/ 时,git grep 返回静默的假零。在 Windows 上的 Git Bash 中,MSYS 路径转换会在 git.exe 看到参数之前就把看起来像 Unix 路径的参数重写成 Windows 路径。所以 git grep 'href="/blog/' 实际上搜索的是完全不同的东西,然后报告什么都没有。Plain grep 是 MSYS 二进制文件,不受影响——这就是两个命令不一致的原因,也是为什么 git grep 零看起来更权威。MSYS_NO_PATHCONV=1 可以修复它。我在这个仓库上运行的每个链接、导航和内部 href 搜索都值得怀疑。
grep -P 遇到非 ASCII 字符类在这个 locale 下会崩溃。它向 stderr 打印 -P supports only unibyte and UTF-8 locales,但 stdout 没有任何输出。如果你只在读 stdout——而且你确实在读,因为匹配结果出现在那里——你看到的就是一个干净的零。
grep -c $'\r$' 会错误计算行尾。cat -A 剥离 CR 并把 CRLF 文件显示为 LF。两者都被用来判断一个文件的行尾是否即将被破坏,但回答的都是不同于原问题的问题。
六个工具,一周时间,全都在报告什么都没有——而那个东西就在它们面前。
What made me start checking is that I had already been burned by the abstract version of this.
A defect class in that project had been recorded as closed. The note said, in effect, no item in the bank now has this problem. It was written after a sweep, the sweep was real, and the sweep had found and fixed everything it looked at.
The sweep had been aimed at one particular string. The claim was about a whole class. Those are different statements, and the gap between them had been sitting there for four days with a live instance in it — an instance the same document mentioned in a different section, which nobody had read against the first one.
That is the same failure as the xargs line. A detector was pointed at one thing, returned zero for that thing, and the zero got promoted into a claim about something larger. The tool was working correctly. The inference was not.
And this is where it interacts badly with agents. I do a lot of this work with a coding agent, and an agent that runs a search and gets no output will tell you, accurately and confidently, that it found no occurrences. It is not wrong about what it observed. It is reporting a zero it has no way to distinguish from a broken detector — and it will report it in a clear declarative sentence that reads exactly like a finding.
This is not a quirk of one tool. Designing systems that can distinguish a real result from a failed instrument is a named competency — the CCAR-F exam's Context Management & Reliability domain covers it directly, down to building a taxonomy that separates transient failures worth retrying from permanent ones that need different handling.
I have written about this before, in a different form: your coding agent's summary of its own work is not evidence. This is the narrower case. A zero that an agent reports is not evidence of absence. It is evidence that a command produced no output, and those are only the same thing when the command ran.
Before you trust a zero, prove the detector fires.
Run the same command against something you know is there. If you are sweeping for a flag that should have been removed, first search for a flag you know is still present and confirm you get hits. If that control comes back empty too, your detector is broken and the real search was never a search.
It costs one command. It would have caught all six.
In practice it looks like this. Say you are confirming a deprecated flag is gone:
grep -rn -- '--old-flag' . # the real search: expect 0
grep -rn -- '--flag-that-remains' . # the control: expect hits
If the second one is also empty, stop. You have not learned that the flag is gone; you have learned that your search does not work in this directory, with this shell, against these files. The control costs two seconds and it is the only thing standing between a zero and a belief.
The control has to be a string you have independently confirmed is present — ideally by opening the file and looking at it. A control you assume is there is just a second unproved search, and two unproved searches agreeing tells you nothing at all.
There is a second half, for when the zero is real but the claim is bigger than the search: state the property you are claiming, then name the detector that tests that property. If your detector only tests the specific instance that made you look, you have closed an instance. Say so. Do not write down that the class is closed, because in three weeks somebody — possibly you — will read that sentence and stop looking.
The same distinction matters one level up, in orchestrator and subagent designs: a subagent returning "no results" and a subagent that failed to run are different states, and every extra handoff is one more place that difference can be lost.
And a third, which caught me on a different question the same week: before attributing a change to a commit, check that the code actually differed. I had a metric that dropped in a particular week and a new event that appeared in the same week, and the fit was so tight that I built a whole explanation on it. The event was new. The behaviour was not — the file was byte-identical across both periods, and the event had simply started measuring something that had been there for a month. Two blob hashes would have killed it in five minutes. A new event is not a new behaviour.
Not much, and that is the point. I keep a short list of tools that have lied to me on this setup, with the failure mode and the fix, and I read it before I trust a zero on anything that matters. There are six entries. I expect there to be more.
The list is worth more than any individual fix on it, because the entries are not really about xargs or git grep. They are about a category of result that looks identical whether it is true or whether the machinery failed — and that category is much larger than six commands. It includes every empty search, every clean test run that did not actually execute, every green check on a job that skipped, and every confident report from an agent that a thing does not exist.
A zero is a measurement. Measurements have instruments. Check the instrument.
If you build production systems with Claude and want to know whether that judgement is exam-ready, our free readiness diagnostic scores you across all five domains in about five minutes, and the full 60-question exam simulation runs to the 120-minute limit the published guide specifies.