f

アーカイブ

2016-06-11

How to exclude binary file or directory in Vim internal/external grep

テキストエディターVimでは,起動中に内部grep(:vimgrep)と外部grep(:grep)により複数のファイルから文字列を検索できる。この検索からバイナリーファイルや特定のディレクトリを除外する方法をまとめる。

大量のファイルから特定文字列を探していると,検索に時間がかかり,不要なマッチがあると重要なマッチが埋もれてしまうので,無関係のファイルは検索対象から除外したい。特に,バイナリーファイルや,.git.svnなどのバージョン管理のためのディレクトリは,文字列を検索する文字列を検索する意味がないので検索対象から除外したい。

調べてみたが,あまりまとまった情報がなかったのでまとめてみた。設定一覧は最後のまとめの節に掲載している。

目次

  1. 内部grepと外部grep
  2. 内部grep(:vimgrep)
    1. 検索対象外ファイル・ディレクトリの設定
    2. wildignore設定時の注意点
    3. Vim script構文を使ったスマートな設定方法
    4. vimgrepで気づいたこと
  3. 外部grep(:grep)
    1. findstr(Windows)の設定
    2. GNU grepコマンドのオプション
    3. Unixでのgrepprgの既定値の/dev/null
    4. Vimの外部grep(:grep)とシェルのgrepコマンドのオプションの共通化
  4. まとめ
内部grepと外部grep

まず,Vimの内部grep(:vimgrep)と外部grep(:grep)について簡単に説明する。

内部grep(internal grep,:vimgrep)とは,Vimの内部コマンドによるgrep検索のことである。利点と欠点は以下となる。

利点
  • Vimで直接ファイルを一つずつ開いて検索をかけるため,Vimがもつ文字エンコーディングの自動判別や正規表現が使える。
  • Vim標準機能であるため,どのOSでも利用可能で,操作や結果も同じ。
  • 検索後の問い合わせがなく検索結果にシームレスにアクセスできる。
欠点
  • ファイル数が増えると検索に時間がかかる。

外部grep(external grep,:grep)とは,Vimから外部のコマンドを使用するgrep検索のことである。既定ではUnix系OSではgrepコマンドが使われ,Windowsだとfindstrコマンドが検索に使われる。利点と欠点は以下となる。

利点
  • 外部の専用検索コマンドを使うことで,ファイル数が多くても検索速度が早い。
欠点
  • 外部コマンドに依存するため,OSによりコマンドや操作・結果に若干の違いが生じる。
  • 文字エンコーディングの自動判定などの機能が内部grepよりは貧弱。
  • 検索実行後,Press Enter or type commmand to continue問い合わせメッセージが表示されるので,頻繁に検索を実行する場合煩わしい。

使ってみた印象だと,検索対象ファイル数が数十ファイルを越えるなら,確実に外部grepを使ったほうがよい。ただ,外部grepを使うと,検索後に検索結果一覧を表示してメッセージが表示される。頻繁に検索を繰り返す場合,これが煩わしく感じるかもしれないので,検索対象ファイル数が少なければ,内部grepでもよい。

Vimからgrepを使うことで,Vimを離れることなく検索したファイルにアクセスができる。QuickFix-windowと組み合わせることで,大量のファイルから該当文字列を含むファイルだけをつぎつぎとアクセスでき,とても作業効率がよくなる。

内部grep(:vimgrep)
検索対象外ファイル・ディレクトリの設定

Vimの内部grep(:vimgrep)でバイナリーファイルやディレクトリを検索対象外にするには,wildignoreで設定する(参照:5.1 Vimの内部grepの使い方 - quickfix - Vim日本語ドキュメント)。

設定はVimの正規表現の形式で設定する。例えば,ファイルを対象外にするには以下の通りに拡張子で指定したり,ファイル名で指定する。

set wildignore=*.dll,*.exe,tags

:vimgrepではファイルを自動的に判別して,バイナリーファイルを検索対象外にすることはできないようなので,考えられるバイナリーファイルの拡張子を複数指定して検索対象外にする。

ディレクトリを対象外にするには,vimやbash,zshで特有の正規表現である**を使い,ディレクトリ配下の全ファイルを無視するように指定する(参考:vim - How to exclude file patterns in vimgrep? - Stack Overflow)。

set wildignore+=obj/**,.git/**,.svn/**
wildignore設定時の注意点

wildignoreで値を設定するときは,help: wildignoreで以下のとおりにすることが推奨されている。

リストにパターンを追加するときにはコマンド |:set+=|、リストからパターンを除くときにはコマンド |:set-=| を使うのがよい。こうすると将来のバージョンで異なった既定値が使われるようになったときに、問題が起きるのを防げる。

wildignore - options - Vim日本語ドキュメント

上記の通り,設定するときは既定の設定を壊さないように,:set+=で追加し,:set-=で解除するのがよいだろう。

併せて,「内部grepでバイナリファイルを対象外にする - 座敷牢日誌」で言及されているように,wildignoreの設定は,Vimのgrep以外にも影響を与えるので,そのまま設定するのは危険だ。

前述の記事でなされている通り,Vimでgrep検索するときに発生するイベントであるquickfixをautocmdで捕捉する。:vimgrepを使うときだけwildignoreで検索対象外ファイルをsetlocalで追加設定し,grep検索が終了したらすぐにwildignoreで追加した項目を除外するのがよいだろう。

例えば,以下のように設定することになるだろう。

autocmd QuickFixCmdPre *
\  setlocal wildignore+=*.o,*.obj,*.exe,*.dll,*.bin,*.so,*.a,*.out,*.jar,*.pak
\| setlocal wildignore+=*.zip,*gz,*.xz,*.bz2,*.7z,*.lha,*.deb,*.rpm
\| setlocal wildignore+=*.pdf,*.png,*.jpg,*.gif,*.bmp,*.doc*,*.xls*,*.ppt*

autocmd QuickFixCmdPost *
\  setlocal wildignore-=*.o,*.obj,*.exe,*.dll,*.bin,*.so,*.a,*.out,*.jar,*.pak
\| setlocal wildignore-=*.zip,*gz,*.xz,*.bz2,*.7z,*.lha,*.deb,*.rpm
\| setlocal wildignore-=*.pdf,*.png,*.jpg,*.gif,*.bmp,*.doc*,*.xls*,*.ppt*
Vim script構文を使ったスマートな設定方法

上記で一応設定できたが,この書き方だと,Vimのgrepでの検索前後であるQuickFixCmdPreQuickFixCmdPostとで,2回wildignoreを設定する必要がある。片方への書き忘れがあったり,検索対象外ファイルが増えてきたら行数が増えるので,賢いやり方ではないだろう。Vim scriptの構文を少し使えばスマートに設定できる。

以下の通りに,スクリプトローカル変数s:ignore_listに検索対象外ファイルを記述し,executeで文字列を実行してやれば検索対象外ファイルの記述の重複がなくなる。それに加えて,古いVimではwildignroeが使えないこともあるので,Vimのwildignore機能が有効なときだけwildignoreを設定するようにすれば完璧だろう。

"" vim grep
""" ignored files in vimgrep
let s:ignore_list  = ',.git/**,.svn/**,obj/**'
let s:ignore_list .= ',tags,GTAGS,GRTAGS,GPATH'
let s:ignore_list .= ',*.o,*.obj,*.exe,*.dll,*.bin,*.so,*.a,*.out,*.jar,*.pak'
let s:ignore_list .= ',*.zip,*gz,*.xz,*.bz2,*.7z,*.lha,*.lzh,*.deb,*.rpm,*.iso'
let s:ignore_list .= ',*.pdf,*.png,*.jp*,*.gif,*.bmp,*.mp*'
let s:ignore_list .= ',*.od*,*.doc*,*.xls*,*.ppt*'

if exists('+wildignore')
  autocmd QuickFixCmdPre  * execute 'setlocal wildignore+=' . s:ignore_list
  autocmd QuickFixCmdPost * execute 'setlocal wildignore-=' . s:ignore_list
endif
vimgrepで気づいたこと

Vimの内部grepである:vimgrepを試していて気づいたことがあるので参考までに記載しておく。

Linuxのgrepコマンドでは,とくに指定なければ,検索対象ファイルに隠しファイル.*を含めて再帰的に検索すると,1階層上を示す..が検索対象ディレクトリにヒットしてしまい,ファイルシステム全体を検索することになって危険だった。

grep -r "pattern" .* *

これを防ぐには,正規表現で..を除外させるか,オプション--exclude-dir..を除外すれば回避できた(参考:My Future Sight for Past: How to match grep command for all files including hidden files)。

grep -r "pattern" .[!.]* *
grep -r "pattern" .* * --exlucde-dir=..

しかし,:vimgrepでは以下の通りに検索してもヒットするのは1階層上のディレクトリ配下までであり,ファイルシステム全体を検索してしまうようなことはなかった。:vimgrepの方がgrepコマンドより安全にできているのかもしれない。

:vimgrep 'pattern' .**/
外部grep(:grep)

外部grep(:grep)でバイナリーファイルやディレクトリを検索対象外にするには,grepprgを設定する。grepprgは外部grepを使うときに実際に実行されるコマンドとオプションとなっている(参考:grepprg - options - Vim日本語ドキュメント)。

grepprgの既定値はOSごとに以下となっている。

grepprgの既定値
OS既定値
Unix"grep -n $* /dev/null"
Win32"findstr /n""grep -n"

このように,OSごとに使われるコマンドが異なるので,設定を分岐させる必要がある。grepコマンドはPOSIXでも規定されており,LinuxやMac,WindowsにおいてもCygwinやMSYS2など使える可能性が高い。そこで,grepコマンドが存在すれば,grepコマンドのオプションとして設定し,そうでなければWindowsとみなしてfindstrコマンドのオプションとして設定するのがよいだろう。

if executable('grep')
  set grepprg=grep\ -n
else
  set grepprg=findstr\ /n\ /p
endif
findstr(Windows)の設定

Windowsでは標準で外部grepコマンドにfindstrコマンドが使われる。

このコマンドはWindowsにおけるgrepコマンドに相当するもので,正規表現を使ってファイル内の文字列を検索できる。しかし,grepと異なり検索対象外のファイルやディレクトリやを設定することはできない。

しかし,/pオプションをつけることで,バイナリファイルを検索対象外ファイルに指定できる。したがって,grepprgには以下のとおりに設定するのが最善だろう。

set grepprg=findstr\ /n\ /p

その他,findstrコマンドではディレクトリを再帰的に検索するのに/sオプションを使うなど,grepとはコマンドの体系が異なっている。ヘルプを確認して使い方を習熟しておく必要があるだろう。

個人的には,findstrコマンドを使うくらいなら,コマンド体系を統一できる内部grepである:vimgrepコマンドを優先したい。

GNU grepコマンドのオプション

Windows以外であれば,基本的には外部grep(:grep)にはgrepコマンドが使われる。POSIXで規定されているgrepには検索対象外のファイルやディレクトリを指定できないが,多くの環境で使われていると思われるGNU grepであれば,オプションが用意されている(参照:Man page of GREP)。

GNU grepで検索対象外に関するオプション
オプション説明
--binary-files=without-match, -Iバイナリーファイルを無視する。
--exclude-dir指定したディレクトリを無視する。
--exclude指定したファイルを無視する。ファイルはパスなしのファイル名であるベースネームのみで判定する。

--exclude-dirと--exludeではマッチングにglobまたはワイルドカードとして,*?[...]が使える。また,一度に複数の項目を設定するには,値を波括弧{}で囲み,項目はカンマ,で区切る。または,--exclude-dirなどのオプションを複数回繰り返して指定してもよい。ただし,波括弧で囲めるのは値が複数個あるときだけ。1個しかないのに波括弧を使うと,その設定は無視される。値はでも有効なので,をつけよう。なお,波括弧を引用符で囲むと波括弧自体が文字列として解釈されるので注意しよう。

grep "pattern" * --exclude-dir={..,.git}
grep "pattern" * --exclude-dir=.. --exclude-dir=.git
grep "pattern" * --exclude-dir={..} # これは無視される
grep "pattern" * --exclude-dir={,..} # これはOK
grep "pattern" * --exclude-dir="{,..}" # これはNG

例えば,以下のように設定すればよいだろう。

set grepprg=grep\ -n\ -I\ --exlucde-dir={..,.git,.svn,.obj} --exclude={tags,GTAGS,RTAGS,GPATH}

なお,--exclude-dirオプションはGNU grep 2.5.2から導入されたらしく,それ以前のgrepでは--excludeを使えば代用できるとの情報を見かけた(参考:grepで.svnディレクトリを除外して再帰検索 - 日々の報告書)。しかし,--exclude=*.svnなどとしてもディレクトリを除外できなかったので,2.5.2以降のバージョンでは--excludeオプションの仕様が変わったのかもしれない。

Unixでのgrepprgの既定値の/dev/null

Unixでのgrepprgの既定値は,以下となっている(参考:grepprg - options - Vim日本語ドキュメント)。

grepprg=grep\ -n\ $*\ /dev/null

grepでは検索対象を指定しなければ,標準入力から読み込まれるとみなされ動作が停止してかのようにみえてしまう。Unixだけ後ろに$*\ /dev/nullが付いているのは,検索対象の指定を忘れたときの予防のためだろう。検索対象に/dev/nullが指定されるようにすることで,標準入力からの入力を待ち続けることを回避できる。

しかし,この指定があると通常のgrepと挙動が変わってくる。それは,-rオプションで再帰的にディレクトリを指定するときだ。

echo "backup" > a.sh
## これはヒット
grep -r --include=*.sh backup

## vim
vim
## これはヒットしない
:grep -r --include=*.sh backup

grepprg=grep\ -n\ $*\ /dev/nullとなっていれば,ディレクトリに/dev/nullが指定されたとみなされ,現在ディレクトリを探しにいかない(参考: Differences when using grep in terminal and :grep in vim - Stack Overflow)。

この問題を避けるためにも,grepprgの既定値から$*\ /dev/nullを削除した。また,次の節で説明するが:grepの実行後のQuickFix-Windowにおいて,ステータスラインに表示されるスペースを省略するためにも有効だ。
Vimの外部grep(:grep)とシェルのgrepコマンドのオプションの共通化

grepprgの値としてgrepコマンドを使う時の設定例として以下を掲載した。

set grepprg=grep\ -n\ -I\ --exlucde-dir={..,.git,.svn,.obj} --exclude={tags,GTAGS,RTAGS,GPATH}

これには以下2点の問題がある。

  1. :grep実行後のQuickFix-Windowでのステータスラインの逼迫。
  2. 通常のgrepコマンドとVimの外部grepとで設定の重複。

1.の:grep実行後のステータスラインが逼迫されることについて説明する。

:grepを実行後のQuickFix-Windowでは,検索コマンドがステータスラインに表示される。しかし,grepprgが長いと画面に入りきらず,一部が省略されて表示されてしまう。例えば以下の画像のように先頭部分が>で省略されてしまっている。見た感じが悪いのでできれば回避したい。

2.について説明する。基本的には検索対象外ファイルの設定は通常のgrepコマンドでもVimの:grepでも同じ設定になるはずだ。片方の設定忘れなど保守が悪いので,共通化したい。

この2点の問題を解決する方法がある。それは,以下だ。

grepにオプションを追加したaliasを設定しておいて,vimから使えるようにする。

こうすれば,Vimのgrepprgで設定する必要がなくなりbashなどと設定を共通化できる。また,ステータスラインの表示スペースも節約できる。

Vimからシェルのaliasを使う方法は「My Future Sight for Past: Use of alias for Vim external command」に詳しくまとめている。

例えば,~/.bashenvファイル(~/.bashrcでも可)に共通設定を記入を記入しておき,そのファイルをVim起動中に環境変数BASH_ENVに設定する。

## .bashenv

## grep 2.21 later, deprecated GREP_OPTIONS environmental variable GREP_OPTIONS="--color=auto -I --exclude-dir={.,..,.git,.svn,obj}" GREP_OPTIONS="${GREP_OPTIONS} --exclude={tags,GTAGS,GRTAGS,GPATH}" alias grep="grep ${GREP_OPTIONS}"
## .vimrc

if executable('grep') "" シェルと共通化させない場合の設定
" set grepprg=grep\ -n\ -I\ --exlucde-dir={..,.git,.svn,.obj} --exclude={tags,GTAGS,GRTAGS,GPATH}
set grepprg=grep\ -n
else
"" for Windows
set grepprg=findstr\ /n\ /p endif

"" Enable alias for external command if filereadable(glob('~/.bashenv')) | let $BASH_ENV=expand('~/.bashenv') | endif

こうすることで,grepのオプションをVimとシェルとで共通化でき,ステータスラインのスペースも確保できる。

まとめ

Vimの内部grep(:vimgrep)と外部grep(:grep)でバイナリーファイルや特定のディレクトリを検索対象外にする方法をまとめた。これでVimでファイルを検索するのが快適になるだろう。

最後に,今回の設定をまとめて掲載する。

## .vimrc

"" vim grep """ ignored files in vimgrep let s:ignore_list = ',.git/**,.svn/**,obj/**' let s:ignore_list .= ',tags,GTAGS,GRTAGS,GPATH' let s:ignore_list .= ',*.o,*.obj,*.exe,*.dll,*.bin,*.so,*.a,*.out,*.jar,*.pak' let s:ignore_list .= ',*.zip,*gz,*.xz,*.bz2,*.7z,*.lha,*.lzh,*.deb,*.rpm,*.iso' let s:ignore_list .= ',*.pdf,*.png,*.jp*,*.gif,*.bmp,*.mp*' let s:ignore_list .= ',*.od*,*.doc*,*.xls*,*.ppt*' if exists('+wildignore') autocmd QuickFixCmdPre * execute 'setlocal wildignore+=' . s:ignore_list autocmd QuickFixCmdPost * execute 'setlocal wildignore-=' . s:ignore_list endif

if executable('grep') "" シェルと共通化させない場合の設定
" set grepprg=grep\ -n\ -I\ --exlucde-dir={..,.git,.svn,.obj} --exclude={tags,GTAGS,GRTAGS,GPATH}
set grepprg=grep\ -n
else
"" for Windows
set grepprg=findstr\ /n\ /p endif

"" Enable alias for external command if filereadable(glob('~/.bashenv')) | let $BASH_ENV=expand('~/.bashenv') | endif
## .bashenv

## grep 2.21 later, deprecated GREP_OPTIONS environmental variable GREP_OPTIONS="--color=auto -I --exclude-dir={.,..,.git,.svn,obj}" GREP_OPTIONS="${GREP_OPTIONS} --exclude={tags,GTAGS,GRTAGS,GPATH}" alias grep="grep ${GREP_OPTIONS}"

0 件のコメント:

コメントを投稿