フューチャー技術ブログ

Grep, Git grepの便利オプション

春の入門祭り🌸の第9弾です

はじめに

こんにちは、TIGメディアユニット岸本卓也です。普段の業務ではアプリ領域を担当しており、JavaやJSと戯れつつ日々を過ごしています。

私は最近プロジェクトが変わり、キャッチアップを進めています。その中でソースコードを検索するために、IDEの検索機能以外に grepgit grep を使いました。そこでよく使った・または便利だったオプションを紹介します。

なお、Windowsでは初期状態ではgrepコマンドがありませんが、インストールする方法はいくつかあります。gitを使うためにインストールするGit for WindowsのGit Bashでもgrepコマンドが使えますので、Windowsユーザーの方はお試しください。

grepの基本

grepは指定された検索パターンに一致する部分を検索するコマンドラインプログラムです。次の形式で実行します。

# コマンド実行形式
grep [オプション...] [検索パターン] [検索対象のファイル...]

# コマンド例: sample.txtファイルの中からFutureという文字列を含む行を検索する。
grep 'Future' sample.txt

このように検索パターン (≒検索したい文字列) と検索対象のファイルを指定して実行すると、検索パターンにマッチする行がコンソールに出力されます。

検索対象のファイルが複数ある場合は、次のようにファイルを列挙します。

# コマンド例: 複数のファイルを対象に検索する。
grep 'Future' sample.txt sample2.txt

# コマンド例: ワイルドカードで複数ファイルを指定する。
grep 'Future' *

検索パターン

grepの検索パターンには正規表現を使うことができ、正規表現を使うことで非常に強力な検索ができます。正規表現は検索パターンを表現する特殊な文字列で、ワイルドカードのようなものです。

正規表現には普通の文字とメタ文字 (=特別な意味を持つ文字) があるので、ここではメタ文字の内よく使うものを紹介します。よく使う順に記載します。

  • 任意の1文字: . (ドット)
  • 量指定子: その直前の文字が何回繰り返すかを指定します。
    • 0回以上: *
    • 1回以上: +
    • 0回または1回: ?
    • n回: {n}
  • アンカー: 行頭または行末にマッチします。
    • 行頭: ^
    • 行末: $
  • エスケープ: \ メタ文字を普通の文字として指定したい場合にメタ文字の直前に指定します。
  • ブラケット式: [] []で囲んだ中に指定した文字のいずれか1文字を表します。文字を列挙する以外に - (ハイフン) を使った範囲指定も可能です。
  • 部分式 (グループ)、後方参照
    • 部分式 (グループ): () 正規表現の一部を () で囲むと部分式 (グループ) を定義できます。
    • 後方参照: \n (grepの場合、nは1~9までの数字) n 番目の部分式にマッチした文字列と同じ文字列にマッチします。
  • 単語の境界: 単語の境界にマッチします。
    • 単語の境界: \b
    • 単語の始まり: \<
    • 単語の終わり: \>

これらのメタ文字のいくつかを使った正規表現の例は次のとおりです。

  • Hack.+Future: Hack という文字列に続いて任意の文字が1個以上あり、それに続いて Future という文字列がある部分にマッチします。例えば Hack to the Future にマッチします。
  • BT{2}F: BTTF という文字列にマッチします。
  • Future$: 行末の Future という文字列にマッチします。
  • [0-9]{4}: 任意の数字が4個続く部分にマッチします。例えば 1989 にマッチします。
  • Dr\. [a-zA-Z]+: Dr. という文字列に続いてアルファベットが1文字以上続く部分にマッチします。例えば Dr. Emmett にマッチします。
  • (\<[bB]uffalo\> ?){8}: buffalo という単語が8回続く文字列にマッチします。例えば Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo にマッチします。

正規表現はgrepに限らずエディタやプログラミング言語など様々な場所で使うことができますが、正規表現の細かな仕様は実装によって結構異なります。したがって、正規表現の詳細は各実装のドキュメントを参照する必要があります。

よく使うオプション

grepには様々なオプションがあり、オプションを指定することで検索動作を変えることができます。ここではそのオプションの内よく使うものを紹介します。よく使う順に記載します。

大文字/小文字の違いを無視: -i

grepはデフォルトでは大文字/小文字も含めて厳密に一致する文字を検索するため、検索パターン future は文字列 Future にマッチしません。 -i を指定すると大文字/小文字の違いを無視して検索するため、 Future にもマッチします。

# コマンド例: futureにマッチするが、Futureにはマッチしない。
grep 'future' sample.txt

# コマンド例: future, Futureどちらにもマッチする。
grep -i 'future' sample.txt

大文字/小文字の違いを無視して広めに検索したい時によく使います。

フォルダを再帰的に検索: -r

前述の通り、検索対象ファイルは複数列挙できますが、フォルダ階層が切られたソースを検索する場合にファイルを1個ずつ指定するのは面倒です。そのような場合に -r オプションとともに検索対象ファイルではなくフォルダを指定すると、指定されたフォルダに存在するファイルと、サブフォルダがあればその中に存在するファイルも再帰的に検索します。

# コマンド例: some_dirフォルダから再帰的に、配下に存在するファイルを対象に検索する。
grep -r 'Future' some_dir/

なお、フォルダ指定を省略するとカレントディレクトリから検索するため、私はよく grep -ir 'future' といった形で使います。

特定のファイルだけ検索: --include

フォルダ指定で検索する場合は指定したフォルダ配下のすべてのファイルが検索されますが、これを限定したいことがあります。そのような場合に --include オプションで検索対象とするファイル名の条件を指定できます。ファイル名の条件にはglobパターンを使えます。

# コマンド例: some_dirフォルダから再帰的に、配下に存在する拡張子.vueのファイルを対象に検索する。
grep -r --include='*.vue' 'Future' some_dir/

# コマンド例: 検索対象にしたいファイル名の条件が複数ある場合はオプションを複数回指定する。
grep -r --include='*.vue' --include='*.js' 'Future' some_dir/

特定のファイル/フォルダを除外: --exclude, --exclude-dir

上記とは逆に、検索対象から除外するファイルを指定することもできます。除外するファイル名は --exclude オプション、フォルダ単位で除外するには --exclude-dir オプションで条件を指定でき、どちらもglobパターンを使えます。

# コマンド例: some_dirフォルダから再帰的に、配下に存在する拡張子.log以外のファイルを対象に検索する。
grep -r --exclude='*.log' 'Future' some_dir/

# コマンド例: some_dirフォルダから再帰的に、logフォルダ以外の配下に存在するファイルを対象に検索する。
grep -r --exclude-dir='log' 'Future' some_dir/

拡張正規表現を使う: -E

grepの検索パターンはデフォルトでは基本正規表現 (BRE) として解釈されます。BREでは ?, +, {, |, (, ) の文字はメタ文字ではなく普通の文字です。BREでこれらをメタ文字として機能させるには \?, \+, \{, \|, \(, \) と指定する必要があります 1

一方、拡張正規表現 (ERE) ではこれらの文字は単体でメタ文字として機能します。したがって、検索パターンによってEREにした方がスッキリ書ける場合は -E オプションを指定することで検索パターンをEREとして解釈させることができます。

# コマンド例: BRE
grep '[0-9]\{4\}' sample.txt

# コマンド例: ERE
grep -E '[0-9]{4}' sample.txt

マッチした周辺行も出力: -A num, -B num, -num (-C num)

検索パターンがどのような状況で使われているか、知りたいことがあります。そのような場合に、マッチした行に加えてその周辺の行を出力するオプションがあります。

  • マッチした行の後 num 行も出力: -A num
  • マッチした行の前 num 行も出力: -B num
  • マッチした行の前後 num 行も出力: -num (-C num)

行番号を出力: -n

行番号が必要であれば -n オプションでマッチした行の行番号を出力できます。

ファイル名を出力しない: -h

検索対象のファイルが複数ある場合、デフォルトでは検索結果にファイル名が出力されます。単一ファイルの検索時同様にファイル名なしの出力にするには -h オプションを指定します。

検索結果をパイプで繋いで処理したい場合によく使います。

ファイル名だけ出力: -l

デフォルトでは検索結果にはマッチした行テキストが出力されます。これの代わりにマッチするデータがあるファイル名だけ表示するには -l オプションを指定します。

文字コードShift_JISのファイルを検索

grepは文字コードUTF-8のファイルならマルチバイト文字も検索できます。

# コマンド例: 日本語を検索する。
grep 'フューチャー' sample.txt

# コマンド例: 絵文字を検索する。
grep -P '🤠' sample.txt

ただ、Windowsにおいては文字コードShift_JISのファイルを相手にすることもまだまだ多いものの、grepは単純にはShift_JISのファイルを検索できません。そのような場合は、次のように文字コードShift_JISの検索パターンを指定すると検索できます。ただし、正規表現が使えません。

# コマンド例: 文字コードShift_JISのファイルを対象に検索する。
grep -aF $(echo 'フューチャー' | iconv -t SJIS) sample_sjis.txt

# コマンド例: 検索結果の文字化けを解消するには、検索結果の文字コードを変換する。
grep -aF $(echo 'フューチャー' | iconv -t SJIS) sample_sjis.txt | iconv -f SJIS

この例ではコマンド置換 ($(...) の部分、grepではなくシェルの機能) により文字コードShift_JISに変換するコマンドを実行してその結果を検索パターンに指定しています。

各オプションの意味とその意図は次のとおりです。

  • -a: バイナリファイルもテキストとして検索します。
    Shift_JISのファイルはgrepにバイナリファイルと認識され、検索結果にマッチした行テキストが出力されないことがあります。 -a を指定するとバイナリファイルもテキスト扱いで検索してマッチした行テキストが出力されます。
  • -F: 検索パターンを正規表現として解釈せず普通の文字列として扱います。
    Shift_JISの検索文字列の一部はメタ文字として解釈されてしまい、エラーになることがあります。そのような場合は -F オプションを指定して検索パターンをただの文字列として指定するとエラーを解消できます。

マッチしない行を検索: -v

検索パターンを含まない行を検索したい (NOT条件のような) 場合は -v オプションを指定します。

複数の検索パターン: -e

複数の検索パターンのいずれかを含む行を検索したい (OR条件のような) 場合はいくつか方法がありますが、 -e オプションで検索パターンを複数指定する方法が簡単です。

# コマンド例: HackまたはBackという文字列を含む行を検索する。
grep -e 'Hack' -e 'Back' sample.txt

検索パターンをファイルで指定: -f

検索パターンは -f オプションを使ってファイルで指定できます。検索パターンを指定するファイルは1行に1個の検索パターンを記述します。このとき、検索パターンを指定するファイルの改行コードは LF にする必要があります。改行コードに CR が含まれていると意図した検索ができません。

# コマンド例: ファイルから検索パターンを読み取って検索する。
grep -f patterns.txt sample.txt

このオプションは次の例のように、別のコマンドの結果を検索パターンとして使う時に指定することが多いです。

# コマンド例: 別コマンドの結果を検索パターンとして指定し、検索する。
grep -r --include='*.vue' -f <(grep -Eo '[EWI][0-9]+' some-resource.yml) some_dir/

この例ではプロセス置換 (<(...) の部分、grepではなくシェルの機能) により別のコマンドの結果を検索パターンファイルとして指定しています。

最短一致検索

検索パターンで紹介した量指定子の *+ はマッチする部分ができるだけ長くなるようにマッチされます (最長一致検索)。例えば、検索パターン H.*e は文字列 Hack to the Future の全体にマッチします。そうではなく、1個目の e までマッチさせる (最短一致検索) 場合は、 -P オプションを指定し、Perl互換正規表現 (PCRE) のメタ文字 *?+? を使います。検索パターン H.*?e は文字列 Hack to the Future に対して Hack to the の部分にマッチします。

# コマンド例: 最短一致検索する。
grep -P 'H.*?e' sample.txt

git grep

gitのサブコマンドにはgitリポジトリを検索するgit grepコマンドがあります。単にワークツリーを検索するだけであればgrepを使えば良いですが、コミット内を検索したい場合にはgit grepを使う必要があります。git grepはワークツリーとコミット内のどちらも検索できます。

git grepは名前にgrepが含まれている通りgrepと似ており、使い方がgrepとほぼ同じでオプションも同じように使えるものが多いです。ここでは前述したgrepのオプションとは異なる部分と、git grep特有の便利機能を紹介します。

特定のファイルだけ検索

git grepには --include オプションがありません。代わりに、git grepではコマンドライン引数の最後に検索対象のファイルを限定する「パス仕様」を指定できます。

# コマンド例: カレントディレクトリから再帰的に、配下に存在する拡張子.vueのファイルを対象に検索する。
git grep 'Future' -- '*.vue'

この例では '*.vue' の部分がパス仕様で、 -- はそれより後ろの引数がパス仕様であることを明示するために指定します。

なお、git grepは -r オプションの動作がデフォルトのため、オプション無しでもフォルダを再帰的に検索します。

特定のファイル/フォルダを除外

--include と同様にgit grepには --exclude, --exclude-dir オプションが無いため、コマンドライン引数の最後で除外するパス仕様を指定します。除外するパスは先頭に :^ または :! という文字 (どちらも同じ意味) を付けて指定します。

# コマンド例: カレントディレクトリから再帰的に、配下に存在する拡張子.log以外のファイルを対象に検索する。
git grep 'Future' -- ':^*.log'

# コマンド例: カレントディレクトリから再帰的に、logフォルダ以外の配下に存在するファイルを対象に検索する。
git grep 'Future' -- ':^*/log/*'

他ブランチ/全ブランチ検索

gitは機能や修正毎にブランチを分けて開発することが多いため、他ブランチを対象に検索したいことがあります。そのような場合はコマンドライン引数でブランチを指定して検索できます。

# コマンド例: dev/awesome-featureブランチを対象に検索する。
git grep 'Future' dev/awesome-feature

# コマンド例: リモートブランチを対象に検索する。
git grep 'Future' origin/dev/awesome-feature

# コマンド例: 検索対象のブランチを複数指定する。
git grep 'Future' dev/awesome-feature dev/pretty-feature

また、もっと広く検索するために全ブランチを対象に検索したいことがあります。そのような場合、私は次のように検索しています。

# コマンド例: 全リモートブランチを対象に検索する。
git grep 'Future' $(git show-ref | cut -d' ' -f2 | grep '/origin/')

# コマンド例: 全リモートブランチを対象に、特定のパターンのファイルのみ検索する。
git grep 'Future' $(git show-ref | cut -d' ' -f2 | grep '/origin/') -- '*.vue' ':^*.log'

さいごに

当記事では、私がよく使うgrepオプションやgit grepの機能を紹介しました。grepは他のコマンドと組み合わせるとさらに強力に使うことができます。

# コマンド例: 別コマンドの結果を検索する。
hoge | grep 'Future'

# コマンド例: 3日以内に更新されたログファイルを対象に検索する。
find . -maxdepth 2 -type f -name '*.log' -mtime -3 -print0 | xargs -0 grep 'Future'

# コマンド例: grepで検索されたファイルを対象に置換処理する。
perl -pi -e 's/foo([-\/])/bar${1}/g' $(grep -rl 'foo[-/]')

当blog過去記事の grepのLT では、「find, while, cut, …もgrepのオプションだよね?」といった衝撃の発表もされていて面白い内容ですのでよろしければそちらも参照ください。

参考リンク