← Vim研究所

Vimで複数ファイルを一括処理する方法 windo/bufdo/tabdoの使い分け

2026-08-03 公開 ・ 更新: 2026-09-03

開いているファイルすべてに同じ変更を加えたいとき、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ファイルに対して両方の実機で実行した結果です。

目次(8項目)

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.txtc.txtの2つだけでした。

よく使うパターン

一括置換と保存

使用頻度がいちばん高いのは複数ファイルの一括置換です。:bufdoに続けて通常の:sコマンドを書くだけで、巡回先のバッファそれぞれで置換が走ります。下は分割編集していたうちの1ファイルぶんの見え方です。

BEFORE
I use emacs on weekdays.
emacs has good extensibility.
:bufdo %s/emacs/vim/g
AFTER
I 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.txtE486: Pattern not foundが出て停止し、c.txtには手が付きませんでした。止まったあとのカレントバッファはb.txtなので、どこまで進んだかは立っている場所を見れば分かります。

一致が無くても止めたくないなら、:seフラグを使うのが素直です。:bufdo %s/a1/QQ/geと書けば、一致の無いバッファはエラーを出さずに素通りします。:s以外なら:silent!で握りつぶします。:vimgrepが止まる例はbufdoとvimgrepを組み合わせた全バッファ検索で扱っています。

!を付けられるのは3つのうち:bufdoだけです。:windo! set number:tabdo! set numberE477: 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つ移るたびにFileTypeBufEnterのautocommandが走ります。Syntaxイベントだけは:bufdo自身が'eventignore'に足して止めますが、それ以外は素通りしません。300個の.pyファイルを載せ、syntax onfiletype 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つに絞れます。

後始末は、終着点が最後の巡回先になることと、エラーが出ればその場で打ち切られることの2つです。| updateを付ける行だけは確認を挟まずにディスクへ書くので、流す前に:lsで対象を数える癖を付けておくと安心して使えます。

関連記事

← Vim研究所 トップへ戻る