← Vim研究所

Vimのautocmdとaugroupの使い方 二重登録を防ぐ書き方とnvim_create_autocmd

2026-11-09 公開 ・

vimrcにautocmdを1行書いて保存し、:source $MYVIMRCで読み直す。設定を試すときによくやる操作ですが、そのたびに同じautocmdがもう1つ登録されます。3回読み直せば3つになり、ファイルを保存するたびに同じ処理が3回走ります。画面には何も出ないので、気づくのは処理が重くなったか、メッセージが重複して出たときです。

この記事では、二重登録が実際に起きる様子を:autocmdの出力で見せ、Vim scriptのaugroupとautocmd!、Neovimのnvim_create_augroup()でそれを止めるところまでを、前後の出力を並べて確かめます。後半では++onceや++nested、ファイルを開いたときのイベントの順番、登録元の調べ方も扱います。

検証環境:Vim 9.1.1752とNVIM v0.12.6(macOS)。一時ディレクトリに検証用のVim script・Luaファイルを置き、vim -Nu NONEとnvim --cleanで読ませて:autocmdと:messagesの出力を取りました。:verboseを手で打った場合はtmuxの中で対話起動して確かめています。掲載している出力はすべて実行結果です。

目次(10項目)

autocmd とは何か(イベント・パターン・コマンドの3つ)

autocmdは「何かが起きたら、このコマンドを実行する」という予約です。書式は:autocmd {イベント} {パターン} {コマンド}で、3つの部品から成ります。

autocmd BufWritePre *.txt echom 'pre-save'
部品上の例意味
イベントBufWritePreバッファをファイルへ書き込む直前
パターン*.txt対象のファイル名。FileTypeのときはファイルタイプ名
コマンドechom 'pre-save'実行するExコマンド

この1行を読ませたあとで.txtを:wすると、書き込みの直前にpre-saveがメッセージ欄に出ます。:autocmd BufWritePreと打つと登録内容が一覧で見えます。ここで1つ紛らわしいのが、Vimでは一覧の見出しがBufWritePreではなくBufWriteと表示されることです。BufWriteはBufWritePreの別名で、Vimは別名のほうで表示します。NeovimはBufWritePreのまま出します。以降の出力でVimだけBufWriteになっているのはこのためです。

基本の書き方と、よく使うイベント

パターンの意味はイベントによって変わります。BufReadPostやBufWritePreのようにバッファに関するイベントはファイル名と照らし合わせ、FileTypeはファイルタイプ名と照らし合わせます。Pythonのファイルだけに効かせたいなら、BufEnter *.pyよりFileType pythonのほうが、拡張子のないスクリプトにも効くので漏れがありません。

イベント起きるときパターンと照らす相手
BufReadPost既存のファイルを読み込んだ後ファイル名
BufNewFile存在しないファイルを開いた時ファイル名
FileTypefiletypeが決まった時ファイルタイプ名
BufEnterバッファに入った時(切り替えのたび)ファイル名
BufWritePre書き込む直前ファイル名
TextYankPostヤンクした直後なし(*を書く)

TextYankPostを使った実例がヤンクした範囲をハイライトする設定で、変更のたびに保存するファイルの自動保存もInsertLeaveやTextChangedのイベントに書き込みをぶら下げて作ります。どちらも次の節の二重登録の問題をそのまま抱えるので、書くときは必ずグループで囲みます。

vimrc を読み直すと autocmd が二重に登録される

次の1行だけのファイルt1.vimを用意し、:source t1.vimを3回実行してから:autocmd BufWritePreを見ました。vimrcを編集して:sourceで反映させる操作を3回繰り返したのと同じです(:sourceの使い方はファイルとvimrcを再読み込みする方法にまとめています)。

autocmd BufWritePre *.txt echom 'pre-save'

Vim 9.1の出力です。

--- Autocommands ---
BufWrite
    *.txt     echom 'pre-save'
              echom 'pre-save'
              echom 'pre-save'

同じ定義が3行並びました。Vimは「同じイベント・同じパターン・同じコマンド」でも重複を検出せず、読まれた回数だけ追加します。表示だけの問題ではなく、実際に処理も重複します。2回:sourceした状態でa.txtを開いて:wすると、:messagesはこうなりました。

"a.txt" 1L, 6B
pre-save
pre-save
"a.txt" 1L, 6B written

Neovim 0.12.6でも同じで、3回読ませると3行、:doautocmd BufWritePre x.txtで発火させるとpre-saveが3回出ました。Luaのvim.api.nvim_create_autocmd()をグループなしで書いた場合も変わらず、2回:sourceしたあとにnvim_get_autocmds()で数えると2が返りました。書く言語の問題ではなく、登録の仕組みの問題です。

echomなら目で気づけますが、フォーマッタを走らせる処理や、外部コマンドを呼ぶ処理が重なると、保存のたびに同じ整形が何度もかかって遅くなるだけで、原因がautocmdだとは分かりにくくなります。

augroup と autocmd! で二重登録を防ぐ

止める方法は、autocmdを名前付きのグループに入れ、グループの先頭でそのグループの中身を消すことです。

augroup MyGroup
  autocmd!
  autocmd BufWritePre *.txt echom 'pre-save'
augroup END

このファイルを3回:sourceしてから:autocmd MyGroupを見た結果です。

--- Autocommands ---
MyGroup  BufWrite
    *.txt     echom 'pre-save'

何度読ませても1行のままでした。読まれるたびにautocmd!がMyGroupを空にし、そのあとで1つだけ登録し直すからです。augroupだけでautocmd!を書かないと、グループに名前が付くだけで中身は増え続けます。2つは必ず組で書きます。

注意したいのは、autocmd!が消す範囲です。次のファイルで、グループなしの1つとG1の1つを登録してから、消し方を4通り試しました。

autocmd BufWritePre *.txt echom 'nogroup'
augroup G1
  autocmd!
  autocmd BufWritePre *.txt echom 'g1'
augroup END
実行したコマンド残ったもの
autocmd!G1の'g1'
autocmd! BufWritePreG1の'g1'
autocmd! G1グループなしの'nogroup'
autocmd! G1 BufWritePre *.txtグループなしの'nogroup'

グループの外で打ったautocmd!は、すべてを消すのではなく、グループに属さないものだけを消しました。逆に言えば、vimrcの先頭にautocmd!を1行だけ置く古い書き方は、グループなしの登録を一掃するだけで、プラグインが作ったグループには触れません。グループ名を指定すると、そのグループだけが対象になります。

Neovim の Lua で書く(nvim_create_augroup / nvim_create_autocmd)

Neovimではaugroup+autocmd!の組に当たるのがnvim_create_augroup()のclearです。

local g = vim.api.nvim_create_augroup('MyLua', { clear = true })
vim.api.nvim_create_autocmd('BufWritePre', {
  group = g,
  pattern = '*.txt',
  command = "echom 'lua-pre'",
})

これを:luafileで3回読ませた結果と、clear = falseに書き換えて同じことをした結果を並べます。

-- clear = true
MyLua  BufWritePre
    *.txt     echom 'lua-pre'

-- clear = false
MyLua2  BufWritePre
    *.txt     echom 'lua-pre'
              echom 'lua-pre'
              echom 'lua-pre'

clear = falseは、グループがすでにあればそれを返すだけで中身を消しません。autocmd!を書かなかったaugroupと同じ状態です。オプションを省略してnvim_create_augroup('Def', {})と書いた場合も試したところ、3回読ませて1行だったので、既定はtrueです。clear = falseは、複数のファイルから同じグループに少しずつ足したいときに使うもので、普段の設定では書く場面がありません。

処理をLuaの関数で書くときはcommandの代わりにcallbackを渡します。関数には引数が1つ渡され、中身をvim.inspect()で表示するとbuf・event・file・group・id・matchの6つのキーが入っていました。FileTypeで使うとmatchとfileの違いがよく分かります。

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function(a) print(a.event, a.match, a.file, a.buf) end,
})
FileType python x.py 1

matchはパターンと照らし合わせた文字列で、FileTypeならファイルタイプ名のpython、fileはファイル名です。BufEnterで同じことをすると、matchとfileはどちらもファイルのパスになりました。バッファごとにキーマップを張るような処理はargs.bufを使うと、どのバッファの話かを取り違えません。

++once・++nested と callback の戻り値

++onceを付けたautocmdは、1回実行されると自分を消します。

autocmd BufEnter *.txt ++once echom 'once: ' .. expand('<afile>')

a.txt、b.txtの順に開くと、メッセージはonce: a.txtの1回だけで、その後の:autocmd BufEnterは空の一覧になりました。Luaではonce = trueが同じ働きをします。起動後に一度だけ重い初期化をしたい、といった用途に向きます。

++nestedはもう少し分かりにくい指定です。autocmdの中で実行したコマンドは、既定ではほかのautocmdを発火させません。次の2行で確かめました。

autocmd BufReadPost *.txt echom 'BufReadPost ' .. expand('<afile>')
autocmd BufEnter a.txt edit b.txt

:e a.txtを実行すると、BufEnterの中の:editで最終的にb.txtが開きますが、メッセージはBufReadPost a.txtだけで、b.txtのBufReadPostは走りませんでした。2行目をautocmd BufEnter a.txt ++nested edit b.txtに変えると、BufReadPost b.txtも出ます。Vim 9.1とNeovim 0.12.6で同じ結果でした。autocmdの中でファイルを開いたのにファイルタイプの設定が効かない、という症状はたいていこれです。

Luaのcallbackには、もう1つ消し方があります。関数がtrueを返すと、そのautocmdは消えます。呼ばれた回数を数えて2回目でtrueを返す関数をBufEnter *.txtに登録し、a.txt→b.txt→a.txtと移動したところ、count=1とcount=2が出たあと3回目は呼ばれず、一覧からも消えていました。条件を満たしたら外す、という処理をonceより細かく書けます。うっかりreturnに真の値を返す式を書くと黙って消えるので、戻り値のある関数をそのまま渡すときは気を付けます。

ファイルを開いたときのイベントの発火順

BufNew・BufAdd・BufNewFile・BufReadPre・BufReadPost・FileType・BufEnter・BufWinEnterのそれぞれでechomするautocmdを登録し、既存のx.pyと、まだ無いnew.pyを開きました。登録したあとでfiletype onを実行した場合の、Vim 9.1の:messagesです(ファイル情報の行は省きました)。

BufNew x.py
BufAdd x.py
BufReadPre x.py
BufReadPost x.py
FileType x.py
BufEnter x.py
BufWinEnter x.py
BufNew new.py
BufAdd new.py
BufNewFile new.py
FileType new.py
BufEnter new.py
BufWinEnter new.py

存在しないファイルではBufReadPreとBufReadPostの代わりにBufNewFileが1つ走ります。BufReadPostだけに書いた処理は新規ファイルでは動かないので、両方に効かせたいならBufReadPost,BufNewFileとカンマで並べます。

意外だったのはBufReadPostとFileTypeの順番が固定ではないことです。filetype onを先に実行してからautocmdを登録すると、同じファイルでFileType x.pyがBufReadPost x.pyより前に出ました。Neovimをnvim --cleanで起動し、-cでautocmdを読ませた場合も同じくFileTypeが先です。

理由は、ファイルタイプの判定そのものがfiletypedetectというグループのBufReadPost *のautocmdだからです。Neovimで:autocmd filetypedetect BufReadPostを打つと、filetype.luaから登録された定義が出てきます。同じイベントのautocmdは登録した順に実行されるので、判定が先に登録されていれば、自分のBufReadPostより前にFileTypeが発火します。init.vimに同じautocmdを書いて普通に起動した場合は、Neovimがinit.vimを読み終えてからファイルタイプ判定を有効にするため、BufReadPostが先でした。BufReadPostで値を用意してFileTypeで使う、という順番に頼った書き方は、設定の読み込み順が変わると壊れます。

登録した autocmd を確かめる・消す(:autocmd / :verbose autocmd / autocmd!)

一覧は:autocmdに、グループ名・イベント・パターンを後ろから順に足して絞り込みます(:autocmd MyGroup、:autocmd BufWritePre *.txt)。どこで定義したかまで知りたいときは:verboseを前に付けます。Vimでt2.vimを:sourceした場合の出力です。

--- Autocommands ---
MyGroup  BufWrite
    *.txt     echom 'pre-save'
        Last set from ~/t2.vim line 3

ファイル名と行番号まで出ます。ただし、いつも出るわけではありません。確かめた範囲では次のように違いました。

定義した方法Vim 9.1Neovim 0.12.6
Vim scriptのファイルを:sourceファイル名と行番号ファイル名と行番号
-c引数-c argument-c argument
コマンドラインで手入力行が出ない行が出ない
Luaファイルを:source(callback)なしファイル名のみ
Luaファイルを:luafileなしLuaとだけ出る
Luaでnvim -V1起動なしファイル名と行番号

Neovimでnvim_create_autocmd()を使った定義は、通常の起動ではLast set from Lua (run Nvim with -V1 for more details)やLast set from ~/l3.lua (run Nvim with -V1 for more details)のように、行番号の代わりに-V1で起動し直すよう促す一文が出ます。言われたとおりnvim -V1で起動するとLast set from ~/l3.lua line 1まで出ました。callbackで登録したものは一覧のコマンド欄が<Lua 108: ~/l3.lua:1>のような表示になり、descを付けておくとその後ろに[my callback]と説明が並ぶので、こちらでも見当が付けられます。どのファイルが読まれているか自体が怪しいときは、先に:scriptnamesで読み込まれた設定ファイルを確かめるほうが早いです。

消すときは、Vim scriptなら前の節の表のとおりautocmd! グループ名、グループごと無くすならaugroup! グループ名です。Luaではvim.api.nvim_clear_autocmds({ group = 'MyLua' })で中身を空にし、vim.api.nvim_del_augroup_by_name('MyLua')でグループを削除します。どちらも実行後の:autocmdに該当の行が残らないことを確かめました。Vimでaugroup! MyGroupのあとに:autocmd MyGroupを打つとE216: No such group or event: MyGroupになり、グループ自体が無くなったことが分かります。

使ってみて

発火順を調べるとき、最初はnvim --clean -c 'so order.vim'で手早く済ませようとして、FileTypeがBufReadPostより先に出る結果を見ました。思い込んでいた順番と逆なので、しばらく自分のスクリプトの書き間違いを疑ってechomの文字列を何度も見直しています。-cで読ませた時点ではファイルタイプ判定がもう登録済みだ、と気づいたのは:autocmd filetypedetectを打ってからでした。検証のための近道が、そのまま検証対象の条件を変えていたことになります。

まとめ

autocmdは読み込まれた回数だけ登録され、Vimは重複を見つけて捨ててくれません。vimrcやinit.luaを読み直す運用をするなら、Vim scriptではaugroup 名前の直後にautocmd!、Luaではnvim_create_augroup(名前, { clear = true })(既定のままでも同じ)でグループを作り、その中に入れます。グループの外のautocmd!はグループなしの登録しか消さない点も覚えておくと、消したはずの処理が残る理由が分かります。

おかしいと思ったら:verbose autocmd イベント名で行数と登録元を見ます。同じ定義が複数並んでいれば二重登録、知らない定義があれば登録元のファイルを辿る、という順で確かめると原因に早く着きます。

関連記事

← Vim研究所 トップへ戻る