掃描指令壞掉時不會報錯,它報一個數字——而那個數字經常是自洽的
論述基礎與限制
本卡從一個工作階段的紀錄抽出。那個階段用掃描指令驅動幾批全站替換,過程中記錄到九次指令自失,機制表列出當時可指認的那些,另有壓縮階段記下的兩種;後續的階段陸續往表裡補,所以表是開放的。每一次的共通點都是指令沒有報錯,它報了一個數字,而那個數字看起來合理。
限制有兩層。九這個數字是單一階段的自我計數,不是任何母體的發生率——它說明這類失效的形態,不說明它多常發生。表列的機制都出自同一類環境(macOS 的 BSD 工具鏈、zsh、ripgrep),換一套工具鏈會換一組機制,而共用的是「壞掉的量測不會發出訊號」這個結構。
推翻方式:拿一條被判定壞掉的指令,證明它在當時的資料上其實回報了正確答案,本卡對那一例就不成立。下面記的是實例,不是量化研究。
核心原則
一條掃描指令有兩個獨立的失效面:它量錯了,以及沒有人會發現它量錯了。 程式崩潰會停下來,而掃描指令的失敗形態是安靜地量到別的東西——路徑不存在、比較函式的定義與預期不同、旗標的意思不是想的那個。這些全部輸出一個數字,而數字沒有型別標記自己量的是什麼。
自洽是這類失效最強的偽裝。 壞掉的輸出經常與另一條獨立管道對得起來:把所有詞形塌成一組的分組指令,回報的數字剛好等於該詞的總出現次數;用行數差判定鏡像落後的指令,在剛同步過的檔案上剛好得到一致的偏移量。兩個管道相符讀起來像交叉驗證過,於是「該不該回頭查這條指令」不會被問。這與 #287 是同一個機制在不同對象上的形態:那裡是內容裡的數字停在舊值而分項加得起來,這裡是量測工具本身壞掉而輸出仍然對得上。
判斷標準是一次探測,不是一次閱讀。 讀指令讀不出這幾類問題——每一條在字面上都是對的,錯的是它與環境的互動。可執行的做法只有一個形式:餵三個已知不同的輸入進去,看它回報幾組。回報一組就是壞的,成本是一行。
已知的機制
| 機制 | 指令看起來在做什麼 | 實際發生什麼 | 輸出為什麼可信 |
|---|---|---|---|
| 定序規則 | sort | uniq -c 依詞形分組 | BSD 的 sort / uniq 在 en_US.UTF-8 下把純中文字串一律視為等值,全部塌成第一組 | 回報的數字剛好等於該詞的總出現次數,與另一條計數指令自洽 |
| shell 不分詞 | 變數展開成多個路徑 | zsh 不對未加引號的變數做分詞,多路徑被當成單一不存在的路徑;2>/dev/null 吞掉錯誤 | 每一類都回報 0,讀起來像「這批很乾淨」 |
| 旗標撞名 | rg -h 只顯示匹配、不顯示檔名 | -h 是 --help,印出說明文字 | 有輸出、而且很長 |
| 計數單位 | rg -c 數出現次數 | 它數的是有命中的檔案數 | 是個小的正整數,與預期同量級 |
| 代理量 | 行數差判定鏡像落後 | 量到的是 frontmatter 長度,不是內容落差 | 在部分檔案上剛好一致,看起來有基準值 |
| 正規化不足 | 比對內容找出鏡像缺的東西 | 把鏡像刻意丟掉的 H1 標題算成缺內容 | 十二個檔案全部命中,看起來是個系統性發現 |
| 聚合掩蓋 | grep -o <pattern> \| uniq -c 數同一個字串出現幾次 | 逐條列出是兩處而計數回報一處,成因是 regex 在其中一處的前文字元失配 | 是個小的正整數,且與「這一類我修了一處」自洽 |
另有兩種記在較早的階段:sed 's|.*:||' 同時吃掉檔名與內容而產生幻影計數;需要前後文的正規式靜默跳過落在行邊界的命中,換一條寫法之後多找到六處。
反模式
用 2>/dev/null 吞掉錯誤。 它把「這條路徑不存在」變成「這一類零命中」,而零命中在掃描報告裡是最不會被追問的結果。要抑制雜訊就抑制特定的雜訊,不要抑制整個 stderr。
零命中直接讀成合格。 零有兩個來源——真的沒有,以及指令沒跑到。這兩者在輸出上完全一樣,而只有後者需要處置。#288 講的是它的鏡像面:非零命中被讀成涵蓋。兩者合起來的結論是命中數的任何值都不構成健康度的證據,要問的是這條指令在該偵測的狀態出現時會不會動。
沿用一條在別的資料上有效的指令。 行數差在幾個檔案上剛好對得起來,就被當成通用判斷標準用到整批上。指令的正確性綁在資料的性質上,換一批資料要重驗。
在同一批資料上驗證從它歸納出來的指令。 這與 #288 的第三個反模式同型,差別在那裡的對象是 pattern 的涵蓋範圍,這裡是指令的機械正確性。
修法
探測輸入先於真實輸入。 分組指令餵三個已知不同的字串、計數指令餵一個已知出現 N 次的檔案、路徑展開先 echo 出來看。這一步的成本是幾秒,而它擋掉的是一整批基於錯誤數字的改動。
兩條機制不同的指令算同一個量。 詞形分組的總和要等於裸計數,鏡像落差要同時用行數與內容各算一次。相符不證明對(兩條可能壞在同一個地方),但不符一定有問題,而這一半就夠用。
把指令的假設寫在它旁邊。 「這條假設 sort 按 byte 比較」「這條假設 shell 會分詞」「這條假設 frontmatter 長度固定」。寫出來之後,換環境或換資料時才有東西可以逐條核對;沒寫出來的假設在出錯時看起來像資料的性質。
輸出的形態要能自我否證。 分組指令回報「只有一組」而輸入有幾千行時,那個形態本身就該觸發懷疑,不必等到有人比對。設計指令時讓它輸出組數與總數兩個值,比只輸出一個總數安全。
掃完逐條列出,不要只讀計數。 聚合是一層有損的轉換,而它損掉的正好是判定要用的東西——計數回報一處與實際兩處在報告上是同一個數字,逐條列出的成本則是拿掉管線末端的那個 uniq -c。這一條同時擋掉定序規則那一列的失效:中文字串在預設 locale 下被視為等值而全部塌成一組,逐條看得出來、看計數看不出來。
改完之後回頭重量一次。 修了一批而指標不動,是修法與量測脫節最直接的訊號(#288);指標動了則同時驗證了修法與指令。這個對照免費。
#298 finding 清單列的是抽樣位置 要求每個 finding 修完之後回掃全批驗證,而本卡是那個動作的前置條件:回掃的指令若自己壞掉,回報的零命中會直接被讀成「這一類清完了」,而那個結論正好落在最想聽到它的時點上。
判讀徵兆
| 徵兆 | 為什麼是訊號 | 下一個動作 |
|---|---|---|
| 分組指令回報的組數是 1,而輸入有數百行 | 比較函式把所有輸入視為等值 | 餵三個已知不同的輸入驗證 |
| 掃描回報的數字剛好等於另一條指令的總數 | 兩者可能出自同一個退化,不是交叉驗證 | 用機制不同的第三條指令重算 |
| 一整批類別全部回報 0 | 路徑或展開失敗的形態與真的乾淨相同 | 拿掉錯誤抑制重跑,或故意餵一個已知命中 |
指令裡有 2>/dev/null | 它把失敗轉成沉默 | 先拿掉跑一次,確認沒有被吞掉的錯誤 |
| 判定用的是代理量(行數、檔案數、大小) | 代理與目標分岔的情況正是判定要處理的難題 | 換成直接量目標,或同時算兩者比對 |
| 一條指令在幾個樣本上「剛好對」就推廣到整批 | 正確性綁在資料性質上 | 在新資料的極端樣本上重驗 |
正對照要與待判項同粒度
正對照的作用是證明「零命中」來自資料而不是來自指令,而它只在對照與待判項處於同一個粒度時成立。聚合層的命中不能替單項的鑑別力背書。
一次外部實測踩到這一步。要判定的是「某個路徑在這個 repo 有沒有刪除紀錄」,第一次挑的兩個對照路徑碰巧也從未被刪過,於是對照與待判項同樣回零——兩個零讀起來像「這條指令無效」,實際是對照挑錯了。接著改跑整個目錄回傳兩筆命中,那一步證明的是指令會動,不是它對單一路徑分得出有無。
有效的做法是先把合格的對照撈出來再挑:讓同一條指令列出這個 repo 裡實際被刪過的路徑(--name-only 之類的旗標),從那份清單裡取一個當對照,才拿得到一比零的對比。
這一種與管道壞掉的形態在輸出上相同(都是零),而成因相反——那裡指令沒跑起來,這裡指令正常執行而問錯了對象。分辨的方法是問對照本身:我用來證明管道通的那一次命中,跟我要判定的那一項是同一個粒度嗎。
回頭補對照時,哪些零命中結論要重驗
正對照可以回溯套用到已經做出的零命中結論,而不必每一條都重跑。分界在那一次呼叫產生過什麼輸出:
- 整批輸出全零:沒有任何證據顯示管道通,要補跑正對照。一次自陳的實例是安全相關的——用
grep -c查某支 skill 有沒有宣告某個 frontmatter 鍵,回 0,據此判定某次強制推送沒有旁路任何閘門。補跑之後管道正常、目視也確認無該鍵,結論成立;而它先前成立是靠管道剛好沒壞,不是靠驗證。 - 同一條 pattern 在同一次呼叫裡有別的命中:自帶對照,pattern 與管道都驗過了。
- 同一次呼叫裡別的 pattern 有命中而這一條回零:只有管道驗過了。這一條 pattern 自己寫得對不對仍然未知——一批指令共用管道而各自有各自的正規式,一個分支寫錯不會讓別的分支停止命中。這一種要單獨補一個針對這條 pattern 的正對照。
第三種最容易被歸進第二種,因為兩者在報告上都寫成「那一輪掃描有命中」。
鏡像:假紅燈不會讓人改錯,而會讓人不再看
本卡講的是假綠燈——壞掉的指令回報乾淨,於是缺陷通過。相反的一側是假紅燈:驗證工具自己算錯而回報失敗,被驗的對象從頭到尾是對的。一次拆分的行覆蓋比對連錯三次(段尾空行被吃掉、標題錨字串同時匹配到同名的子標題、少還原一個空格),三次都由斷言攔下、三次都是比對腳本的錯。
假紅燈的直接代價是零——沒有東西被改壞。它的代價落在下一次:紅燈連續三次不指向被驗對象之後,第四次被驗對象確實錯了,看的人會先懷疑工具。這一類與兩個約定並存的分歧(兩邊都對而量的不是同一個東西)不同:那裡沒有人錯,這裡工具確實錯了,而共同點是紅燈不指向被驗對象。
處置放在工具那一側:每一次假紅燈都要找出工具錯在哪一步並修掉,不能「知道是工具的問題」就跳過——跳過的那一次沒有修的成本,卻讓下一次紅燈的可信度再降一格。驗證工具自己也要有正對照,跟它驗的對象一樣:先餵一份已知會通過的輸入,確認它會亮綠。
正對照有兩個方向,各驗一側。 對一條會回零的檢查,先餵一個已知會命中的輸入,確認它會亮紅——而且要與待判項同粒度;對一條會回紅的驗證工具,先餵一份已知會通過的輸入,確認它會亮綠。上面那三次假紅燈缺的是第二個方向,同一天另一次「對照挑錯了」缺的是第一個方向的粒度。兩個方向都只要一次執行,而缺哪一個,零與紅就分別失去了它們的意義。
這條原則什麼時候不成立
輸出小到可以逐行目視核對時不成立。十行以內的結果讀完就知道對不對,探測輸入的成本高於收益。
指令有測試覆蓋時不成立——那裡的驗證責任在測試,而測試本身要能被突變驗證(改壞實作、確認對應案例失敗)。這也是最省力的治本方向:反覆使用的掃描寫成有測試的工具,不要每次現寫。
一次性的探索查詢不成立。看看有沒有東西、不據以行動的查詢,錯了的代價只是再查一次。判別看這個數字會不會決定接下來改什麼:會,就先驗指令。
跟其他抽象層原則的關係
#287 重算的觸發要綁在會改變那個數字的事件上 是最近的姊妹卡,兩者共用「內部自洽會蓋住外部失準」這個機制而對象不同:那裡是內容裡的數字停在舊值、三個分項加起來剛好等於總數;這裡是量測工具壞掉、輸出剛好等於另一條管道的值。兩張卡的分辨問句也同型——那裡問「這個數字上一次被實際算出來是什麼時候」,這裡問「這條指令上一次被實際驗證過是什麼時候」。
#288 關鍵詞清單換一個體裁就失準 處理的是同一條鏈的上游:那裡 pattern 是對的而涵蓋範圍不對,這裡 pattern 是對的而指令根本沒跑到它該跑的東西。兩者的輸出無法區分——都是一個看起來正常的命中數。合起來的操作結論是掃描報告要附兩件事:這條指令驗過沒有,以及這一類在這個體裁有哪幾種形態。
#289 一個詞是不是通用術語,用多份獨立定義的收斂度量 的修法段已經帶了本卡的一個特例(詞形分組指令要先驗),本卡把它一般化到所有驅動改動的掃描。那裡的控制詞與這裡的探測輸入是同一個設計:先用已知答案證明工具會動,再拿它去量未知。
#266 可核對的錨要選會下降的量 給的是指標的選擇判斷標準(它在該偵測的狀態出現時會不會動),本卡處理的是更前面一層——算這個指標的那條指令會不會動。一個完全符合 #266 的指標,配一條壞掉的指令,得到的仍是恆定的健康值。
#221 檢查規則的作用域要顯式列舉 講未納管目錄的零錯誤與合規目錄的零錯誤訊號相同,本卡是它在單條指令上的形態:沒跑到的零命中與真的乾淨,在輸出上一樣。兩者共用的處置也相同——把涵蓋範圍變成可以逐條核對的東西,而不是一個讀起來完整的數字。
#149 命中是候選不是判決 管的是命中之後的語意判定,本卡管的是命中之前:那個數字是不是真的來自它宣稱的那批內容。三張卡構成一條鏈——指令跑到了嗎(本卡)、pattern 涵蓋這個體裁嗎(#288)、這個命中算不算違規(#149)。
#294 非互動環境裡,要人停下來看的關卡表現成當機 處理工具明確說了話而話被讀在事後的情形。本卡的工具安靜地量到別的東西,那張的工具印了警告仍然被繞過——共同前提是指令的輸出沒有被當成需要判讀的材料,差別只在它有沒有出聲。
#301 限制類宣稱的錯誤會自我保存,因為它勸阻的正是唯一能推翻它的那個動作 與本卡共用「自己這一側的錯誤看起來像外界的性質」這個結構。本卡是壞掉的指令回報一個自洽的數字,那張卡是自己寫錯的 URL 回報成對方故障——兩者都要換一個維度重試才分得開,而那張卡多記了一層:寫成「那個站不穩」之後,聽起來已經有解釋,重試因此不會發生。
#311 reviewer 給的數字要用被審對象宣告的口徑重跑一次:同一個失效面的另一側。本卡的探測輸入抓得到壞掉的指令,抓不到沒壞而口徑不同的量測——那條指令在兩種口徑下都正常運作,差別在它算的是哪一堆檔案。
#317 已經記錄過的失效仍然重演,因為記錄住在要人先起疑才會打開的地方:本卡的機制表列了 shell 不分詞這一種,而那一種在本卡發布之後又重演了一次——兩份記錄都在場、包含一則每個工作階段都會載入的 memory。那張卡處理的是本卡修法段沒寫的一件事:「探測輸入先於真實輸入」這句話要住在哪裡才會被執行。
#318 每一格都驗過,不代表由格子導出的那句話驗過了 處理本卡的補集:工具沒壞、量的對象也對,而被驗的清單少了一項。兩者的輸出無法區分(都是一份全部相符的報告),分辨的動作不同——本卡是餵已知輸入驗管道,那裡是問「這句話要判真假得先做什麼」。
#329 掃描的作用域會默默對齊「我這次動過什麼」 是本卡的鏡像:失效樣態相同(零命中不報錯)而成因相反——那裡的指令完全正確,錯的是餵給它的路徑集合。本卡的修法擋不到它,因為正對照那一列若放在改過的檔裡就會命中、管道通過驗證而作用域仍然是窄的;那一張因此給正對照加了一個位置要求(放在沒被這一輪改動過的檔裡)。 #333 「我以為觀測到的」躲在「引用的」那一欄裡 是同族的另一種來源:本卡的數字來自壞掉的工具,那一張的數字來自從未被呼叫的工具。本卡的修法在那裡仍然有效——正對照放進輸出,要求那次呼叫真的發生並且留下痕跡;而那一張補的是分類端,把「我記得看過」與「輸出在對話紀錄裡」分成兩欄,因為兩者在報告的文字上完全同形。
- #345 修法沒有寫進檔案,而同一輪的無關改善替它簽了名:那裡的批次工具回報零筆命中是一個正確的輸出,而它在該流程裡沒有消費者。工具說了實話而沒有人在聽,後果與本卡的工具說謊相同。
- #386 破壞性指令的目標用變數的字串截取組出來時,審核的人看不到要刪什麼:本卡要求的正對照常常要建一份假資料,那張卡處理收拾假資料的那一步:清理寫在同一條指令的結尾、目標用字串截取算出來時,核准正對照的人同時核准了一次看不到目標的刪除。正對照照做,清理另開一條指令或不清理。