LoginSignup
85
38

More than 3 years have passed since last update.

grep の -l オプション (一覧表示) と -v (条件反転) オプションを併用すると死ぬ

Last updated at Posted at 2020-06-17

問題

以下のようなファイルが同じディレクトリ内にあるとします。

apple.txt
apple
strawberry.txt
strawberry
banana.txt
banana
red_fruits.txt
apple
strawberry

また, grep1 には以下のようなオプションがあります。

-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$ に対して常に真

となることは自明ですが,口語だとやっぱり混乱しがちです…

応用

肯定条件のあと否定条件で絞り込み,みたいな器用な検索も xargs3 と組み合わせると簡単に書けます。

grep -lr <肯定条件> . | xargs grep -L <否定条件>
user@host: ~$ grep -lr apple . | xargs grep -L strawberry
./apple.txt

実行速度とか xargs の引数上限とかに気を配るなら別の書き方もありますけど,ターミナルへの書き捨てユースケースではこれぐらいで困らないと思います。

85
38
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
85
38