Vimのautocmdとaugroupの使い方 二重登録を防ぐ書き方とnvim_create_autocmd
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 | 存在しないファイルを開いた時 | ファイル名 |
FileType | filetypeが決まった時 | ファイルタイプ名 |
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! BufWritePre | G1の'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.1 | Neovim 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 イベント名で行数と登録元を見ます。同じ定義が複数並んでいれば二重登録、知らない定義があれば登録元のファイルを辿る、という順で確かめると原因に早く着きます。