gitconfig の alias だけで git grep の exclude を指定する。

結論

[alias]
    grep-with-exclude = !git grep $@ -- ':!*.svg' && :

実際に使っている設定

[alias]
    grep-with-exclude = !git grep --line-number -I --show-function --heading --break $@ -- ':!*.svg' ':!*/package-lock.json' ':!*/yarn.lock' ':!*/Gemfile.lock' && :

    g = grep-with-exclude --ignore-case
    G = grep-with-exclude

    # fgrep
    f = grep-with-exclude --fixed-strings --ignore-case
    F = grep-with-exclude --fixed-strings

    # egrep
    e = grep-with-exclude --extended-regexp --ignore-case
    E = grep-with-exclude --extended-regexp

動機

「確かバージョン 1.0 指定したはずだな…」と思って git grep '1\.0' とか打つと、大抵 svg ファイルや Gemfile.lock や package-lock.json などから大量に不要な情報がヒットする現象、発生しがちじゃないですか。これ伝わると思う。

けど git grep に除外パターンを指定するのは結構しんどくて、 git grep '1\.0' -- ':!*.svg' ':!*/package-lock.json' ':!*/yarn.lock' ':!*/Gemfile.lock' などと打つ必要があり、とにかくダルい。なんとかできないかなとずっと (15年くらい) 思ってたけど、なんとかしたという話。

解説

  • gitconfig の alias は ! で始めて書くとシェルコマンドとして扱われる。
  • git grep に渡している -- は、シェルにおいてこれ以降はオプションではなく引数として扱ってね、と明示するためのもの。
    • 例えば -f というファイルがあったとして、それを消したいときに rm -- -f という使い方をする。
  • $@grep-with-exclude に渡した引数の位置をここに持ってくる。
  • このままだとコマンドの一番最後にも引数が渡されてしまうので、全てを無視する : というコマンドを && で繋げてやる。
    • : というコマンドはシェルスクリプトを書くときにも重宝するやつで、例えば set -e してる環境で終了コードが 0 以外になるものを取りたいときに && : で繋げてやると、スクリプトの終了は抑制しつつ終了コードを取ることができる。

実用としては

git grep のオプションで

  • --line-number
    • 行番号を表示。
  • -I
    • バイナリファイルの除外。
  • --show-function
    • メソッド名とか関数名を出せるらしい。
  • --heading
    • 横じゃなくて上にファイル名を表示.
  • --break
    • ファイルの境界で空行を出力。

あたりは付けたいので付けておいて、 git ggit grep するようにしつつ、大文字小文字の区別や egrep, fgrep を使うときの alias まで書くと上のような感じになるかな、というところ。

確認

GIT_TRACE 環境変数に 1 とか true を放り込みながら git コマンドを実行すると alias がどう解決されて、どう実行されているのかデバッグすることができる。

$ GIT_TRACE=1 git g '1\.0'
17:09:31.602057 git.c:749               trace: exec: git-g '1\.0'
17:09:31.602774 run-command.c:659       trace: run_command: git-g '1\.0'
17:09:31.603284 git.c:407               trace: alias expansion: g => grep-with-exclude
17:09:31.603527 git.c:749               trace: exec: git-grep-with-exclude '1\.0'
17:09:31.603533 run-command.c:659       trace: run_command: git-grep-with-exclude '1\.0'
17:09:31.604084 run-command.c:659       trace: run_command: 'git grep --line-number -I --show-function --heading --break $@ -- '\'':'\!'*.svg'\'' '\'':'\!'*/package-lock.json'\'' '\'':'\!'*/yarn.lock'\'' '\'':'\!'*/Gemfile.lock'\'' && :' '1\.0'
17:09:31.622676 run-command.c:659       trace: run_command: unset GIT_PAGER_IN_USE; LV=-c less
17:09:31.625115 git.c:463               trace: built-in: git grep --line-number -I --show-function --heading --break '1\.0' -- ':'\!'*.svg' ':'\!'*/package-lock.json' ':'\!'*/yarn.lock' ':'\!'*/Gemfile.lock'

こういう感じになり、 git g に渡した '1\.0' 引数がちゃんと $@ で指定した位置に渡されていて、末尾に渡されるものに関しては && : '1\.0' となって無事無視されており、期待通りに動いていることが分かる。

ちなみに GIT_TRACE 以外にも GIT_TRACE_SETUPGIT_CURL_VERBOSE 環境変数もあるらしい。

締め

前回ブログ書いたのが 1 年前で吃驚した。