git pathspecを使った高度なgrep

```sh
$git --version
git version 2.19.1
```

# tl;dr

- `git grep`や`git ls-files`などでのパスの絞り方にはpathspecという共通仕様がある
- `:`に続けてmagic word/signatureを使うことができ、ディレクトリの除外など高度な検索ができる

```
# リポジトリルート上のファイルでtestという文字列を含むものを検索
$git grep -w test ':(top)*' ':(top,exclude)*/*'
```

# きっかけ

@laiso さんの「[git-grepで特定のディレクトリを除外する](https://qiita.com/laiso/items/6a453abdf3998a3475eb)」が便利でよく使うのだが、
`git grep word ':!exclude/' .`という記法がなかなか覚えられず、都度記事を見てしまう。

なぜ`:!`で除外を表せるのかを理解しないとダメだと思い、仕様を調べてみた。

# glob(7)の仕様?

```
$git grep --help
```

https://git-scm.com/docs/git-grep/

helpで仕様を確認してみる。optionがずらりと並ぶが、引数の最後に`[<pathspec>…​]`とある

(`--`はoptionではないことの明示なのでpathspecが最後の引数ならなくても良い)

[`pathspec`で検索してみる](https://git-scm.com/docs/git-grep/#git-grep-ltpathspecgt82308203)と、

> If given, limit the search to paths matching at least one pattern. Both leading paths match and glob(7) patterns are supported.

とある。
前半に`glob(7)`をサポートと書かれているので、今度はglobの仕様を追ってみる。

```sh
#if __linux__
$man 7 glob
#elif __APPLE__
$man n glob
```

http://man7.org/linux/man-pages/man7/glob.7.html

- `?`は任意の一文字
- `*`は任意の文字列（空文字も含む）
- `[a,z]`はaかz、`[a-z]`ならaからzの中の一文字
  - `[^abc]`はa,b,c以外の一文字（先頭にブラケットで否定）

```sh
$git grep 'word' .
index.js:2:word
src/ab/index.js:2:word
src/ac/index.js:2:word
src/index.js:2:word

$git grep 'word' src/a[a,b]*
src/ab/index.js:2:word

$git grep 'word' src/a?
src/ab/index.js:2:word
src/ac/index.js:2:word
```

といった記法が使えることがわかった。しかし、肝心の文字列否定は仕様にない。

# gitglossary

実は先ほどの`pathspec`の仕様の説明文には下記の続きがある。

> For more details about the <pathspec> syntax, see the pathspec entry in gitglossary[7].

自分が初めて調べた時、gitのversionが2.14.0で、そのhelpにはこの一文はなかった

https://git-scm.com/docs/git-grep/2.14.0#git-grep-ltpathspecgt82308203

しかし2.14.3以降で、上記の記述が追加されていた（web検索のおかげで発見）

https://git-scm.com/docs/git-grep/2.14.3#git-grep-ltpathspecgt82308203

詳細な仕様を確認してみる

```
$git --help gitglossary
```

https://git-scm.com/docs/gitglossary/

https://git-scm.com/docs/gitglossary/#gitglossary-aiddefpathspecapathspec

# pathspec要点

- `git ls-files`, `git ls-tree`, `git add`, `git grep`, `git diff`, `git checkout`などで共通の仕様
- `/`はディレクトリを表し、末尾につけば配下も対象となる
- `fnmatch(3)`に従う
  - ex, `git grep 'word' 'src/*.js'`は`src/index.js`,`src/ab/index.js`,`src/ac/index.js`にマッチ
  - pathspecはクオートで囲まないと意図通りに動かないので注意

後半が本題

- `:`始まりでmagic word（または一文字のmagic signature）を続けると、下記ルールを適用できる
- magic word(long form)の場合は`:(magic_word_1,magic_word_2)pathspec`と表現

|magic signature|magic word|効果|
|---|---|---|
|`/`|top|通常カレントディレクトリ以下が検索対象だがルートディレクトリから検索|
||literal|`*`,`?`を文字列として扱う|
||icase|大文字小文字区別せず検索|
||glob|`**`で階層任意で検索できる(literalとは互換性がない)|
||attr|attributeを利用: [gitattributes](https://git-scm.com/docs/gitattributes)|
|`!`,`^`|exclude|除外|

```sh
$git ls-files
index.js
src/ab/index.js
src/ac/index.js
src/index.js

$git ls-files 'INDEX.JS'
$git ls-files ':(icase)INDEX.JS'
index.js

# **はコロン無しでも使えたりするが、
$git ls-files **/index.js
src/ab/index.js
src/ac/index.js
src/index.js
# その場合は**/のスラッシュがディレクトリを表すため、ディレクトリルートが検索対象にならない。magic wordを入れれば全てが対象となる
$git ls-files ':(glob)**/index.js'
index.js
src/ab/index.js
src/ac/index.js
src/index.js

$cd src
$git ls-files 'index.js'
index.js
$git ls-files ':/index.js'
../index.js
$git ls-files ':(top,glob)**/index?js'
../index.js
ab/index.js
ac/index.js
index.js
```

# 結論: ディレクトリ除外はgit pathspecの独自仕様だった

```shell
$git ls-files ':^src/ab' '**/index.js'
src/ac/index.js
src/index.js
```

# Note: 1系だと使えない可能性あり

会社のレガシー環境だとgit 1.7.1を使っているが、pathspecに否定などは使えなかった（要確認）