複数ファイルを一気にgrepしたり文字列置換したりする方法

コード書いてると、複数ファイルを一気にgrepしたり、特定の単語を一気に置換したりしたい事がある。例えば、リファクタリングの際にクラス名とかメソッド名がそもそも良く無いなーと思って一気にrenameしたくなるとか。そんな時、方法としてはunixのコマンドを使う方法とeditor(例えばvimとか)の機能を使う方法の大きく分けて2つが存在する。

1. unixコマンドを使ってgrep & 置換

まず、1つのファイルに対しての操作を考える。grepなら文字通りgrepコマンドが存在するし、特定の文字列を置換したい場合にはsedコマンドが使える。sedコマンドはファイル名を渡すとその中身を条件に合わせて置換して、結果を標準出力に出力する。

sed s/(元の文字列)/(新しい文字列)/g (入力ファイル名)
$ echo 'my name is panda.' > test.txt
$ cat test.txt
my name is panda.
$ sed s/panda/rabit/g test.txt
my name is rabit.

sedコマンドに-iオプションを渡すと、出力先を入力元のファイルに出来る。つまり、ファイル内の文字列置換を行える事になる。

sed -i '' s/(元の文字列)/(新しい文字列)/g (入力ファイル名)

ただし、-iオプションについてはどのsedでも使える訳では無い様で、振る舞いも微妙に違うっぽい。OSX 10.9.4では使えたが、バックアップファイルを作らない場合でも空文字列('')を-iオプションの後ろにつける必要があった。

参考: http://www.dab.hi-ho.ne.jp/sasa/biboroku/unix/sed-i.html http://open-groove.net/linux-command/sed-edit-files/

複数ファイルに対してgrep & 置換

findコマンドとかgit ls-filesとか使って複数ファイルを用意してから、xargsgrepsedを使えば良い。自分は特にgit管理下のファイルに対して使う事が多かったので、以下のfunctionを.zshrcに書いた。

function grepall() { git ls-files | xargs grep -l $1 }
function sedall()  { grepall $1 | xargs sed -i '' s/$1/$2/g }

これで、grepall (検索したい文字列)とかsedall (元の文字列) (新しい文字列)とかで複数ファイルに対して一気に検索や置換が出来て、けっこう便利だった。こういうのでざっくり置換してから、git diffで変化を見ながら細かい調整とかしたら良いと思う。 ちなみに、grepallは検索した文字列が含まれるファイルの ファイル名だけ を表示するようにしてるんだけど、どこでヒットしたかも見たければ-lオプションを外せば良い。

それから、ファイル名自体のrenameも一気にやりたい時があって、mvしまくるfunctionも書いた。renameall (元の文字列) (新しい文字列)でファイル名を一気に変更出来る。

例えば、user.rbusers_controller.rbがある時に、renameall user super_userと打つとsuper_user.rbsuper_users_controller.rbにrenameされる。

function renameall() { git ls-files | grep $1 | while read LINE; do mv $LINE `echo $LINE | sed s/$1/$2/g`; done }

やってる事はシンプルで、「sedでrenameした文字列」へとmvを行っている。sedはファイル名を標準入力として受け取る事も出来るので、echo $LINEしたファイル名をパイプでsedに渡している。

この3つが結構使えて、お気に入りになっている。

editor(というかvim)の機能でgrep & 置換

grep

自分はeditorとしてはvimを使ってるんだけど、vimにはvimgrepというコマンドが存在していて、:vim[grep] {パターン} {ファイル名}と打つと複数ファイルにまたがってgrepをかける事が出来る。

詳細は以下のリンクを見ると良いと思う。 http://qiita.com/yuku_t/items/0c1aff03949cb1b8fe6b

unixコマンド使った場合と比べるとvim内から使えるというのが最大の利点なんだけど、いかんせん動作は遅いので検索対象はある程度しぼった方が良かったりする。それでもgit管理下のファイル全部にgrepかけたいと思う事はあって、そんな時は

:vim {パターン} `git ls-files`

とかすれば良い。

ただ、git ls-filesとかいちいち打ってられないのでオリジナルでGitGrepとかいうコマンドにしてみた。

285 command! -nargs=* GitGrep call s:GitGrep(<f-args>)
286 function! s:GitGrep(...)
287   execute "vimgrep " . a:1 " `git ls-files`"
288 endfunction

やっぱり動作は重めなので、あまり使いどころは無いかもしれない。ファイル名のパターンにユースケースが多ければ、GitGrep以外のコマンドも増やして行こうと思う。

置換

vim複数ファイルにまたがって置換をしたい場合、よくあるパターンは

:args **/*.txt
:args
:argdo %s/hoge/fuga/g | update

という手順を踏むらしい。詳細は参考リンクを見ていただきたいが、やっているのは:argsコマンドでargument listに置換対象のファイルを登録し、その中身を確認し、その全てに対して:argdo%sという置換コマンドを実行するという事である。

参考: http://archiva.jp/web/tool/vim_grep.html

これは何と言うか結構ダルくて、別の良い方法無いかなーと思ってたらvimgrepした結果に対して編集windowを開くvimプラグインが存在する事を知った。

参考: http://archiva.jp/web/tool/vim_grep2.html

github: https://github.com/thinca/vim-qfreplace

これもvimgrepかけてから編集window開くから2ステップかかるんだけど、変更箇所を目で確かめながら編集したい時には良いかもと思った。

まとめ

複数ファイルに対して一斉にgrepとか置換とかをしたい時、速度を求めるならunixのコマンドで、vimを閉じたく無いならvimの機能を使うと良い。

どうでも良い話

VimScriptについて今まで全然知らなくて~/.vimrcがただのコピペの寄せ集めみたいになってたんだけど、オリジナルコマンド作る為にこのスライド見ながら試行錯誤してみたら大分理解が深まった気がする。昔コピペした時はただの文字列だったものが、きちんと意味を持ったコードに見える様になってて、こういう体験はけっこう楽しい。