Edited at

git pathspecを使った高度なgrep

$git --version

git version 2.19.1


tl;dr



  • git grepgit ls-filesなどでのパスの絞り方にはpathspecという共通仕様がある


  • :に続けてmagic word/signatureを使うことができ、ディレクトリの除外など高度な検索ができる

# リポジトリルート上のファイルでtestという文字列を含むものを検索

$git grep -w test ':(top)*' ':(top,exclude)*/*'


きっかけ

@laiso さんの「git-grepで特定のディレクトリを除外する」が便利でよく使うのだが、

git grep word ':!exclude/' .という記法がなかなか覚えられず、都度記事を見てしまう。

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


glob(7)の仕様?

$git grep --help

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

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

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

pathspecで検索してみると、


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の仕様を追ってみる。

#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以外の一文字(先頭にブラケットで否定)



$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 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

!,^

exclude
除外

$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の独自仕様だった

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

src/ac/index.js
src/index.js


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

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