Help us understand the problem. What is going on with this article?

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

問題

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

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 の引数上限とかに気を配るなら別の書き方もありますけど,ターミナルへの書き捨てユースケースではこれぐらいで困らないと思います。

mpyw
PHP(Laravel) / JavaScript(React/Redux/ReactNative/Vue) / MySQL あたりが得意分野なWeb系エンジニア。最近マンネリ化がひどいので Go / Kotlin / Rust / Swift あたりから何か掘り下げたいと思っている。Go は 2.x 出てから書きます。古い記事はそのまま参考にしないようにご注意ください
http://gravatar.com/mpyw
synapse
Synapseは、オンラインサロンサービスにおけるパイオニアとして、かつて存在していたスタートアップです。
https://synapseam.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした