grep が 0 件を返すとき、無いのは「その文字列」とは限らない
ある社内ツールのソースから関数名を grep したら 0 件だった。
ファイルは存在するし、その関数は確かに書かれている。エラーも警告も出ない。ただ 0 件。
結論から言うと、そのファイルには生の NUL バイトが 1 個入っていた。
NUL が 1 個あると、grep はそのファイルを読まない
grep も rg も、NUL を含むファイルをバイナリと判定する。
そして無音でスキップする。返ってくる 0 件は「その文字列が無い」ではなく
「そのファイルを見ていない」を意味している。
厄介なのは、他のツールは普通に読めることだ。
エディタでも sed でも中身は見える。嘘をつくのは検索ツールだけなので、
「grep で引けない=存在しない」と読み違える。
-aを付ければ出る。出たらそれが答え合わせになる- 検査は言語側で数える。
open(f,'rb').read().count(b'\x00') - 1 ファイルだけ調べても意味がない。ツリー全体を走査して初めて母集団が揃う
このケースでは、区切り文字として NUL を選んだこと自体は正しい判断だった。
扱うデータに絶対に出現しない値なので、キーの衝突が起きない。
問題は書き方のほうで、ソースにバイトを直接埋めたのが事故の原因だった。
バイトを直接埋めるとそのファイルは検索から消える。
バックスラッシュ + u0000 のエスケープ表記で書けば、
文字列としての値は同じままバイナリ判定だけが消える。挙動は変わらない。
変わるのは、検索ツールから見えるかどうかだけだ。
直すときにエディタの一括置換を使ってはいけない。不可視文字は目視で確認できないので、
別の制御文字に化けても気づけない。「NUL がちょうど 1 個」をコードでアサートしてから
バイト指定で置換する。
余談だが、この事故を説明するために書いたメモ自身にも NUL が 2 個混ざっていた。
つまりそのメモも grep で引けなかった。汚染は原因ファイルの中で完結しない。
文字化けの検出では、検出器のほうが化ける
同じ「ツールが嘘をつく」系で、文字コードまわりにもいくつか型がある。
以下は Windows と Git Bash を併用していた時期に踏んだものだ。
| 症状 | 実際に起きていること |
|---|---|
| 置換文字を PCRE で探して 0 件 | シェル環境によって日本語パターンのマッチが不安定 |
| 画面上の文字列が化けて見える | ファイル実体は正常。表示層だけが化けている |
| 置換ツールが対象を見つけられない | 入力したバイト列が実体と一致していない |
| 標準出力に出した瞬間に落ちる | cp932 のコンソールが置換文字を出力できない |
とくに危ないのは 2 行目だ。**表示が化けているだけのファイルを「壊れている」と判断して打ち直すと、
そこで初めて本物の破損が入る**。健全なファイルを自分の手で壊すことになる。
対処はどれも同じ方向を向いている。
- 検出はエンコーディングを明示した言語側で数える。置換文字の個数を数えるだけでいい
- 日本語を含む大きな置換は、ASCII のアンカーで前後をスライスするスクリプトで行う。化けた文字列を検索キーにしない
- バックスラッシュや置換文字をシェルのヒアドキュメント経由で渡すと潰れる。コードポイントで生成する(
chr(0x5C)/chr(0xFFFD)) - 置換スクリプトはいったんファイルに書いてから実行する。シェル経由で組み立てない
- 出力先がその文字を扱えないなら、標準出力を諦めて UTF-8 のファイルへ書く
共通する形
4 つとも、症状は「静かに間違った答えが返る」だ。落ちてくれるほうがまだ親切で、
0 件や「変更なし」は正常な結果の顔をしている。
検出器が 0 件と言ったとき、「無い」のか「見ていない」のかを区別する手段を必ず 1 つ持つ。
区別できないなら、その 0 件は情報量ゼロだ。
まとめ
- grep の 0 件は、NUL が 1 個あるだけで無音の嘘になる。
-aと言語側の実測で確かめる - NUL を区切り文字に選ぶのは妥当。ソースにはエスケープ表記で書く
- 不可視文字の一括置換をエディタでやらない。個数をアサートしてから置換する
- 表示の文字化けを実体の破損と決めない。打ち直しが破損を持ち込む
- 文字コード系の検査は、エンコーディングを明示した言語側でだけ信用する