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

# 問題

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

```apple.txt
apple
```

```strawberry.txt
strawberry
```

```banana.txt
banana
```

```red_fruits.txt
apple
strawberry
```

また， `grep`[^`grep`] には以下のようなオプションがあります。

> #### -v, --invert-match
> **マッチの意味を逆にして、マッチしない行を抜き出して表示します。**
> #### -l, --files-with-matches
> 通常の出力はしません。その代わりに、 grep を普通に実行した際に、何らかの検索結果を表示するような入力ファイルの名前を列挙します (訳注: すなわち、-l オプションを指定すると、 -v オプションを同時に指定しない場合は、**パターンにマッチする文字列を含む行が存在するファイルの名前を列挙する**ということです)。 個々のファイルに対する走査は、最初のマッチで終了します。
> #### -r, --recursive
各ディレクトリの下にあるすべてのファイルを再帰的に読み込みます。 ただし、シンボリックリンクはコマンドラインで指定されたときにのみたどります。 検索対象のファイルが指定されなかった場合には grep は現在のディレクトリを探すことに注意してください。 これは -d recurse オプションと等価です。

これを踏まえて，以下のような検索を試してみましょう。

## 〜を含むファイルを検索

`banana` を含むファイルを検索します。

```console
user@host: ~$ grep -lr banana .
./banana.txt
```

`apple` を含むファイルを検索します。

```console
user@host: ~$ grep -lr apple .
./red_fruits.txt
./apple.txt
```

どちらも意図した結果ですね。肯定条件のときは `-l` オプションに特に言及すべき点はありません。 

## 〜を含まないファイルを検索

### 除外されるファイルが1行しかない場合

`banana` を含ま**ない**ファイルを検索します。

```console
user@host: ~$ grep -vlr banana .
./red_fruits.txt
./strawberry.txt
./apple.txt
```

ターゲットとなるファイルは `banana.txt` でしたが，このファイルは `banana` 1行しか書かれていないため問題なく動いています。

### 除外されるファイルが2行以上から構成される場合

`apple` を含ま**ない**ファイルを検索します。

```console
user@host: ~$ grep -vlr apple .
./red_fruits.txt ⬅ ⁉ 
./banana.txt
./strawberry.txt
```

このように複数行あるファイルをターゲットとして `-vl` の組み合わせて使うといとも簡単にバグっぽい動きになってしまいます。

```
1行目にマッチ || 2行目にマッチ
```

の否定は，**ド・モルガンの法則**[^ド・モルガンの法則]に従うと直感的には

```
!(1行目にマッチ || 2行目にマッチ)
= !1行目にマッチ && !2行目にマッチ
```

という動きをして欲しい気がしますが， なんと

```
!1行目にマッチ || !2行目にマッチ
```

にされてしまいます。 `grep` はもともと （`-l` を使用しない限りは） 行単位で処理を行うもので， **`-v` は行単位の否定**として適用する，という考えで割り切れば正しい気はしますが，初見だと高確率でハマる罠です。

# 解決策

***「〜を含まないファイルを検索」のときは `-vl` の代わりに `-L` オプションを使用する！***

> #### -L, --files-without-match
> 通常の出力はしません。その代わりに、 grep を普通に実行した際に、何の検索結果も表示しないような入力ファイルの名前を列挙します (訳注: すなわち、-L オプションを指定すると、 -v オプションを同時に指定しない場合は、**パターンにマッチする文字列を含む行がまったく存在しないファイルの名前を列挙する**ということです)。 個々のファイルに対する走査は、最初のマッチで終了します。

```console
user@host: ~$ grep -Lr apple .
./banana.txt
./strawberry.txt
```

欲しかった動きになりました！

- [Is 'grep -v -l' equivalent to 'grep -L'? - Unix & Linux Stack Exchange](https://unix.stackexchange.com/questions/339619/is-grep-v-l-equivalent-to-grep-l)

`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` は二重否定として解釈すると「全行にマッチする」とも言えそうですが，正確には「全行にマッチする<ins>または 0 行</ins>」となります。 

```math
\lnot {}^\exists i \; [\lnot \mathrm{match}(L(i))] \;\;\Leftrightarrow\;\; {}^\forall i\; [\mathrm{match}(L(i))]
```

尤も，数理論理的には

- 存在記号 $\exists$ を使った式は空集合 $\emptyset$ に対して常に偽
- **全称記号 $\forall$ を使った式は空集合 $\emptyset$ に対して常に真**

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

# 応用

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

```bash
grep -lr <肯定条件> . | xargs grep -L <否定条件>
```

```console
user@host: ~$ grep -lr apple . | xargs grep -L strawberry
./apple.txt
```

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

[^`grep`]: [Man page of GREP](https://linuxjm.osdn.jp/html/GNU_grep/man1/grep.1.html)
[^ド・モルガンの法則]: [ド・モルガンの法則 - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%BB%E3%83%A2%E3%83%AB%E3%82%AC%E3%83%B3%E3%81%AE%E6%B3%95%E5%89%87)
[^`xargs`]: [Man page of XARGS](https://linuxjm.osdn.jp/html/GNU_findutils/man1/xargs.1.html)