Vimで複数ファイルを一括処理する方法 windo/bufdo/tabdoの使い分け
開いているファイルすべてに同じ変更を加えたいとき、1枚ずつ手作業でやっていては時間がかかります。:windo・:bufdo・:tabdoを使えば、開いている範囲に対してまとめてコマンドを実行できます。3つは書き方がまったく同じで、違うのは「何を順に巡るか」と「巡り終わったあとどこに立っているか」の2点だけです。
検証環境:macOS 26.4.1 + Vim 9.1.1752 / Neovim 0.12.4。本文の巡回順・終了位置・エラーの止まり方・所要時間は、一時ディレクトリに用意したa.txt b.txt c.txtと、300個の.pyファイルに対して両方の実機で実行した結果です。
3つが巡る対象の違い
3つとも構文は{コマンド} {実行したいコマンド}で共通です。違うのは巡回の対象で、ここを取り違えると「効くはずのファイルに効かない」という形で表に出ます。
| コマンド | 巡る対象 | 巡る順 | ! |
|---|---|---|---|
:windo | 現在のタブページのウィンドウ | 左上から最後のウィンドウまで | 付けられない |
:bufdo | バッファ一覧に載っているバッファ | バッファ番号の若い順 | 付けられる |
:tabdo | 各タブページのカレントウィンドウ | 左のタブから順に | 付けられない |
:bufdoが巡るのは:lsに出るバッファだけです。:bdeleteしたバッファは:ls!にしか出なくなり、巡回から抜け落ちます。4つを開いて:bdelete d.txtしてから回すと、訪問先は前の3つだけで、d.txtは素通りでした。ヘルプにも1行で書かれています。
Unlisted buffers are skipped.
:windoは分割していなくてもエラーにはならず、ウィンドウが1つなら1回だけ実行されます。:tabdoもタブが1枚なら1回走って終わります。「分割していないのに効いた気がしない」のは、対象が1つしかないためです。
意外と効くのが:tabdoの対象です。巡るのは各タブページのカレントウィンドウ1つだけで、そのタブが分割されていても残りのウィンドウは対象外になります。2枚目のタブを:splitで2分割して流すと、訪問したのはタブ1とタブ2のウィンドウ1だけで、タブ2のもう1枚には届きませんでした。全タブの全ウィンドウに効かせたいときは入れ子にします。
:tabdo windo set number
この形は手元でも期待どおりに動き、タブ1の1枚とタブ2の2枚、合わせて3つのウィンドウすべてを訪問しました。3つとも範囲指定を受け取れるので、:2,3bufdoのように番号で絞ることもできます。実際に流すと訪問先はb.txt・c.txtの2つだけでした。
よく使うパターン
一括置換と保存
使用頻度がいちばん高いのは複数ファイルの一括置換です。:bufdoに続けて通常の:sコマンドを書くだけで、巡回先のバッファそれぞれで置換が走ります。下は分割編集していたうちの1ファイルぶんの見え方です。
I use emacs on weekdays. emacs has good extensibility.
:bufdo %s/emacs/vim/gI use vim on weekdays. vim has good extensibility.
:bufdoそのものに保存の機能は無いので、ディスクまで反映するには| updateをつなげます。:updateと:wの使い分けは引数リストへの一括処理と同じです。
:bufdo %s/emacs/vim/g | update
表示・比較に使う
分割した各ウィンドウの見え方をそろえたいときは:windoです。行番号や折り返しのように、ウィンドウごとに持つ設定を一度に変えられます。
:windo set number
:windo diffthis
差分表示は同じタブページの中で'diff'が立っているウィンドウ同士を突き合わせる仕組みなので、配る相手はウィンドウになります。:tabdo diffthisでは各タブに1枚ずつ差分ウィンドウができるだけで、比較になりません。
マクロを全体に適用する
記録したマクロを巡回先すべてに流す形も同じ書き方です。ただしマクロはカーソル位置から動き出すので、開始位置を先にそろえておく必要があります。
:bufdo normal! gg@x | update
@xはカーソル位置から動き出すので、各バッファのカーソルがどこにあるかで結果が変わります。ggを挟んで開始位置を明示しておくと、どのバッファでも同じ処理になります。
外部コマンドを実行する
シェルコマンドの実行を巡回先ごとに走らせることもできます。%がそのバッファのファイル名に展開されるので、開いているファイルの行数をその場で数えるような使い方ができます。
:bufdo !wc -l %
ただし出力は1バッファごとに画面へ出て、そのたびにPress ENTER or type command to continueで止まります。3バッファで試すとEnterを3回押すことになったので、数が多いときはシェル側でループを書いたほうが速く終わります。
エラーで止まる位置と ! の意味
巡回の途中でエラーが出ると、そこで打ち切られます。止まった位置がそのままカレントになるので、どこまで進んだかは画面を見れば分かります。
When an error is detected on one buffer, further buffers will not be visited. The last buffer (or where an error occurred) becomes the current buffer.
a.txtだけにa1がある3ファイルで:bufdo %s/a1/QQ/を流すと、a.txtは置換され、b.txtでE486: Pattern not foundが出て停止し、c.txtには手が付きませんでした。止まったあとのカレントバッファはb.txtなので、どこまで進んだかは立っている場所を見れば分かります。
一致が無くても止めたくないなら、:sのeフラグを使うのが素直です。:bufdo %s/a1/QQ/geと書けば、一致の無いバッファはエラーを出さずに素通りします。:s以外なら:silent!で握りつぶします。:vimgrepが止まる例はbufdoとvimgrepを組み合わせた全バッファ検索で扱っています。
!を付けられるのは3つのうち:bufdoだけです。:windo! set numberも:tabdo! set numberもE477: No ! allowedになりました。ウィンドウやタブを移るだけならバッファを離れずに済むので、未保存かどうかを問う場面が無いという理由です。
その:bufdo!は「未保存の変更を捨てて進む」ではなく、「未保存のまま次のバッファへ移ることを許す」という指定でした。'hidden'が無効なVim 9.1.1752で:bufdo! %s/foo/bar/geを流したあとの:ls!は、3つともhと+が並びます。隠れた状態で変更を保持していて、置換の結果は消えていません。
1 h + "a.txt" line 1
2 #h + "b.txt" line 1
3 %a + "c.txt" line 1
危ないのは変更が消えることではなく、変更を抱えたバッファが画面の外に積み上がることのほうです。この状態で:qa!すると、置換した内容がまとめて失われます。
設定ファイルを分割したリポジトリで、複数ファイルにまたがるリネームを:bufdoで一気に片付けたことがあります。sedでも同じことはできますが、Vimで開いた状態のまま実行結果をその場で確認できるのが地味に便利で、以来ちょっとした一括置換はVim側で済ませることが増えました。
実行後にカーソルが戻らない
紛らわしいのが:windoです。3分割した状態でウィンドウ3にいるときに流したところ、巡回はウィンドウ1から始まってウィンドウ3で終わりました。開始位置と終着点がたまたま一致するので戻ったように見えますが、これは枚数が3だったからで、4枚あればウィンドウ4に置き去りにされます。
戻したいなら、流す前に居場所を控えておきます。ここで|の扱いに注意が要ります。:bufdoは行末までを自分が実行するコマンドとして受け取るので、同じ行に|でつないだ後始末は巡回のたびに実行されてしまいます。手元で後ろに1つ足したところ、3バッファぶん3回実行されました。戻す処理は必ず次の行に書きます。
:let b = bufnr('%') | let v = winsaveview()
:bufdo %s/foo/bar/ge | update
:execute 'buffer' b | call winrestview(v)
bufnr('%')でバッファ番号を、winsaveview()でカーソルの行・桁と画面のスクロール位置を控えておき、巡回が終わってから戻します。b.txtの4行目にいる状態でこの3行を流すと、終わったあともb.txtの4行目に立っていました。
戻り先を:edit #で済ませたくなりますが、当てになりません。#が指すのは直前に居たバッファなので、巡回後は最後から2番目に訪問したバッファになります。a.txtから3つを巡った直後の#はb.txtでした。
:windoも考え方は同じで、winnr()を控えてから戻します。ウィンドウ2にいる状態で試すと、:windoのあとはウィンドウ3に立っていて、次の1行でウィンドウ2に帰れました。
:let w = winnr() | windo set number
:execute w . 'wincmd w'
大量のバッファに流すとき
数バッファでは気にならない話が、数十から数百になると効いてきます。効いてくるのは設定が2つです。1つ目が'hidden'です。:bufdoは次へ移るたびに今のバッファを離れるので、未保存のまま離れられない設定だとE37で止まります。対処は2通りで、set hiddenを先に入れておくか、| updateを挟んで都度保存するかです。既定値がVimとNeovimで違うことと:cfdoでも同じ食い違いが起きることは検索結果からの一括置換のほうで書いています。
set hidden " 未保存のまま切り替えられるようにする
:bufdo %s/foo/bar/ge | update " あるいは都度保存する
2つ目が速度です。バッファを1つ移るたびにFileTypeやBufEnterのautocommandが走ります。Syntaxイベントだけは:bufdo自身が'eventignore'に足して止めますが、それ以外は素通りしません。300個の.pyファイルを載せ、syntax onとfiletype plugin indent onだけを入れた設定で測ると、素の:bufdoが0.32秒、:set eventignore=allを挟むと0.02秒でした。Neovimでは0.38秒と0.02秒で、実行順を入れ替えても数字は変わりません。
:set eventignore=all
:bufdo %s/foo/bar/ge | update
:set eventignore=
ただし保存時のautocommandまで止まります。BufWritePreを仕掛けて上の3行を流すと、書き込みは3ファイルとも起きたのにBufWritePreは1度も発火しませんでした(eventignoreなしなら3回発火します)。保存時に自動整形を掛けている環境では、整形されないまま書き出されることになります。
開いていないファイルは対象にならない
3つが扱うのは、すでに開いているものだけです。ディレクトリの中のファイルをまとめて処理したいなら、先に引数リストへ入れてから:argdoを使います。対象の決め方と:bufdoとの違いは引数リストと:argdoの使い方にまとめています。
:args **/*.js " 対象を引数リストに入れる
:argdo %s/foo/bar/ge | update
検索でヒットしたファイルだけを対象にしたいなら:cfdoです。:vimgrepの結果に含まれるファイルを1回ずつ巡るので、無関係なファイルを触らずに済みます。1件ごとに実行する:cdoとの使い分けも含めて、横断検索とquickfixの記事で扱っています。
まとめ
覚えることは対象と後始末の2つに絞れます。
:windo:分割中のウィンドウの見え方をそろえる、差分表示を配る:bufdo:開いているファイルの中身を書き換える、全バッファを検索する:tabdo:タブごとに1枚ずつ設定を変える:argdo・:cfdo:まだ開いていないファイルを対象にする
後始末は、終着点が最後の巡回先になることと、エラーが出ればその場で打ち切られることの2つです。| updateを付ける行だけは確認を挟まずにディスクへ書くので、流す前に:lsで対象を数える癖を付けておくと安心して使えます。