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

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

  • ?は任意の一文字
  • *は任意の文字列(空文字も含む)
  • [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にはこの一文はなかった

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

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

$git --help gitglossary

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に否定などは使えなかった(要確認)