問題
以下のようなファイルが同じディレクトリ内にあるとします。
apple
strawberry
banana
apple
strawberry
また, grep
1 には以下のようなオプションがあります。
-v, --invert-match
マッチの意味を逆にして、マッチしない行を抜き出して表示します。
-l, --files-with-matches
通常の出力はしません。その代わりに、 grep を普通に実行した際に、何らかの検索結果を表示するような入力ファイルの名前を列挙します (訳注: すなわち、-l オプションを指定すると、 -v オプションを同時に指定しない場合は、パターンにマッチする文字列を含む行が存在するファイルの名前を列挙するということです)。 個々のファイルに対する走査は、最初のマッチで終了します。
-r, --recursive
各ディレクトリの下にあるすべてのファイルを再帰的に読み込みます。 ただし、シンボリックリンクはコマンドラインで指定されたときにのみたどります。 検索対象のファイルが指定されなかった場合には grep は現在のディレクトリを探すことに注意してください。 これは -d recurse オプションと等価です。
これを踏まえて,以下のような検索を試してみましょう。
〜を含むファイルを検索
banana
を含むファイルを検索します。
user@host: ~$ grep -lr banana .
./banana.txt
apple
を含むファイルを検索します。
user@host: ~$ grep -lr apple .
./red_fruits.txt
./apple.txt
どちらも意図した結果ですね。肯定条件のときは -l
オプションに特に言及すべき点はありません。
〜を含まないファイルを検索
除外されるファイルが1行しかない場合
banana
を含まないファイルを検索します。
user@host: ~$ grep -vlr banana .
./red_fruits.txt
./strawberry.txt
./apple.txt
ターゲットとなるファイルは banana.txt
でしたが,このファイルは banana
1行しか書かれていないため問題なく動いています。
除外されるファイルが2行以上から構成される場合
apple
を含まないファイルを検索します。
user@host: ~$ grep -vlr apple .
./red_fruits.txt ⬅ ⁉
./banana.txt
./strawberry.txt
このように複数行あるファイルをターゲットとして -vl
の組み合わせて使うといとも簡単にバグっぽい動きになってしまいます。
1行目にマッチ || 2行目にマッチ
の否定は,ド・モルガンの法則2に従うと直感的には
!(1行目にマッチ || 2行目にマッチ)
= !1行目にマッチ && !2行目にマッチ
という動きをして欲しい気がしますが, なんと
!1行目にマッチ || !2行目にマッチ
にされてしまいます。 grep
はもともと (-l
を使用しない限りは) 行単位で処理を行うもので, -v
は行単位の否定として適用する,という考えで割り切れば正しい気はしますが,初見だと高確率でハマる罠です。
解決策
「〜を含まないファイルを検索」のときは -vl
の代わりに -L
オプションを使用する!
-L, --files-without-match
通常の出力はしません。その代わりに、 grep を普通に実行した際に、何の検索結果も表示しないような入力ファイルの名前を列挙します (訳注: すなわち、-L オプションを指定すると、 -v オプションを同時に指定しない場合は、パターンにマッチする文字列を含む行がまったく存在しないファイルの名前を列挙するということです)。 個々のファイルに対する走査は、最初のマッチで終了します。
user@host: ~$ grep -Lr apple .
./banana.txt
./strawberry.txt
欲しかった動きになりました!
grep v l
で検索したら一番上にこのページを Google 先生が出してくれました,最高。
数式で整理
- $i$ 行目のテキストは $L(i)$ とする
- $L(i)$ が検索パターンにマッチすることを $\mathrm{match}(L(i))$ と表現する
↓ Special Thanks: @kzm4269 さんにコメントいただいた内容を反映しました
フラグ | 式表現 | 口語表現 |
---|---|---|
-l |
${} ^\exists i ; [\mathrm{match}(L(i))]$ | マッチ「する」行が存在「する」ファイル |
-vl |
${}^\exists i ; [\lnot \mathrm{match}(L(i))]$ | マッチ「しない」行が存在「する」ファイル |
-L |
$\lnot {}^\exists i ; [\mathrm{match}(L(i))]$ | マッチ「する」行が存在「しない」ファイル |
-vL |
$\lnot {}^\exists i ; [\lnot \mathrm{match}(L(i))]$ | マッチ「しない」行が存在「しない」ファイル |
フラグの変化 | 効果 |
---|---|
-v の付与 |
行単位 の否定 |
-l → -L の変化 |
全体結果 の否定 |
基本的にはそれぞれ -l
と -L
単体で使うことが多いとは思いますが,これらはどちらも -v
と複合することを覚えておきましょう。
また 0 行のファイルに関しては -L
-vL
は「行が存在しない」 ため共に真であることにも注意です。一見 -vL
は二重否定として解釈すると「全行にマッチする」とも言えそうですが,正確には「全行にマッチするまたは 0 行」となります。
\lnot {}^\exists i \; [\lnot \mathrm{match}(L(i))] \;\;\Leftrightarrow\;\; {}^\forall i\; [\mathrm{match}(L(i))]
尤も,数理論理的には
- 存在記号 $\exists$ を使った式は空集合 $\emptyset$ に対して常に偽
- 全称記号 $\forall$ を使った式は空集合 $\emptyset$ に対して常に真
となることは自明ですが,口語だとやっぱり混乱しがちです…
応用
肯定条件のあと否定条件で絞り込み,みたいな器用な検索も xargs
3 と組み合わせると簡単に書けます。
grep -lr <肯定条件> . | xargs grep -L <否定条件>
user@host: ~$ grep -lr apple . | xargs grep -L strawberry
./apple.txt
実行速度とか xargs
の引数上限とかに気を配るなら別の書き方もありますけど,ターミナルへの書き捨てユースケースではこれぐらいで困らないと思います。