3.5* 自动命令与事件
3.5* 自动命令与事件
前面章节介绍了自定义快捷键(:map
)与自定义命令(:command
),这都是响应玩家 的主动输入而快速做些有用的工作。这也算是对 Vim 的 UI 设计吧。谁说只有图形界面 才算 UI 呢,况且在 gVim 中的自定义菜单,也确实与自定义命令或映射很相似呀。
本节要介绍的自动命令,却是让 Vim 在某些事件发生时自动做些工作,而不必再手动激 活命令了。当然了,自动命令在生效前,也是需要定义的。
自动命令的定义语法
自动命令用 :autocmd
这个内置命令定义,它至少要求三个参数:
: autocmd {event} {pat} {cmd}
{event}
就是 Vim 预设的可以监测到的事件,比如读写文件,切换窗口等。{pat}
这是模式条件的意思,一般指是否匹配当前文件。{cmd}
就是事件发生且满足条件时,要自动执行的命令。
在一个命令中可以有多个事件,事件名用逗号分开,且逗号前后不能有空格。模式也可能 以逗号分隔为多个模式。因为{event}
与 {pat}
都相当于是 :autocmd
的单个参 数,其内不能有空格。但最后部分 {cmd}
可以有空格。
一般情况下,{cmd}
就是合法的 ex 命令,将它拷贝到命令行也能手动执行那种。不过 {cmd}
中可能含有一些特殊标记 <>
,在执行前会替换成实际值,这才大大增加了自 动命令的灵活性,而非只能执行静态命令。
在 vim 内部,相当于为每个事件 {event}
维护了一个列表,每当用 :autocmd
为该 事件定义了一个自动命令,就将这个命令加到列表中。然后每当事件发生,就遍历这个命 令列表,如果它满足相应的 {pat}
条件,就会执行这个 {cmd}
命令。
因此,每发生一个事件,vim 都可能自动执行许多命令。就比如文件类型检测与语法高亮 着色,就是通过自动命令实现的。当你安装一些复杂插件,可能会自动执行更多的命令。 而我们自己用 :autocmd
定义的自动命令,只是添加在原来的命令列表之后,做些自定 义的额外工作。
与此前的 :map
与 :command
一样,退化的 :autocmd
是查询功能:
:autocmd {event} {pat}
列出与事件及模式相关的自动命令。:autocmd * {pat}
列出满足某个模式的所有事件的自动命令。:autocmd {event}
列出与某事件相关的所有自动命令,不论模式。:autocmd {event} *
与:autocmd {event}
等效,*
就表示匹配所有。:autocmd
列出所有自动命令。
叹号修饰的 :autocmd!
命令用于删除自动命令,参数意义与退化命令一样:
:autocmd! {event} {pat}
根据事件与模式删除自动命令。:autocmd! * {pat}
只根据模式条件删除自动命令。:autocmd! {event}
只根据事件删除命令。:autocmd! {event} *
只根据事件删除命令。:autocmd!
删除所有自动命令。
但是,叹号也可以修饰完整的非退化的 :autocmd
,就如定义自定义模式一样:
: autocmd! {event} {pat} {cmd}
它表示先将满足事件 {event}
与模式 {pat}
的所有自动命令删除,然后添加自动命 令 {cmd}
。因此这是覆盖式的定义自动命令,此后,在满足相应事件与模式时,就只 会执行这一个自动命令了。依前文介绍,在定义命令与函数时建议用覆盖式的叹号修饰命 令 :command!
与 :function!
。但对于自动命令,还是慎重用覆盖式的 :autocmd!
,因为可能无法从本条语句判断会覆盖掉什么自动命令。
自动命令组
自动命令组 augroup
是组织管理自动命令的有效手段。为理解自动命令组是有必要的 ,先回顾上一小节所介绍的自动命令机制,在未利用命令组的情况下,会发生什么不良后 果。
因为 :autocmd
定义自动命令时是将其添加到自动命令列表末尾的,所以如果在脚本如 vimrc
中定义了自动命令,随后又重新加载了该脚本,那自动命令列表中就会出现两项 重复的自动命令了。对于某些“安全”的自动命令,重复执行不外是浪费效率而已,但有些 自动命令在第二次执行却有可能引发错误呢。
其次,用 :autocmd!
删除自动命令时,它是删除所有自动命令。即使加了事件与模 式两个限制条件,也无法避免影响扩大化,因为别的插件或 Vim 官方插件也可能为相同 的事件与模式定义的一些有用的自动命令啊。
为了解决这个管理问题,引入了自动命令组的概念。自动命令组名字是用以标记一个自动 命令组的符号,取名规则就按 VimL 变量名的规范吧(虽然帮助文档中说似乎可以用任意 字符串作为组名,除了空白字符),不要用奇怪的字符,同时也是大小写敏感的。然后两 个特殊的自动命令组名 END
与 end
是保留的,有着特殊意义。
在不发生理解歧义下,我们就用自动命令组名表示一个自动命令组吧,且在本节中,不妨 用“组名”来作为自动命令组名的简写吧。
于是,在定义自动命令的 :autocmd
命令中,还支持一个可选的组名参数,它紧接命令 之后,而在 {envent}
事件之前:
: autocmd [group] {event} {pat} {cmd}
正因为组名是 :autocmd
的第一个参数,可有可无,当省略时,第一个参数就是事件名 了。所以我们选取组名时,还要避免与事件名(这是 Vim 预设的范围集)重名,以避免歧 义。
在定义自动命令时,如果指定了 [group]
组名参数,就表示将所定义的自动命令添 加到这个自动命令组中。你可以认为每个组都为不同事件维护了不同的自动命令列表,同 一事件在不同组内关联着各自不同的命令列表。
对于删除自动命令的 :autocmd!
变异命令,也同样支持在第一个参数中插入可选的组 名。在指定组名后,就表示只删除该组内的自动命令(当然可再限定事件与模式)。
那么,在缺省组名参数时,:autocmd
与 :autocmd!
又怎样工作的呢。其实它是针对 当前组添加或删除自动命令的。那么当前组又是什么东西呢?它是用 :augroup
命令选 定的:
: augroup {name}
在执行这个命令之后,{name}
就是当前组名了。当 {name}
组名此前尚不存在时, 也会自动创建一个组,然后再选择这个组作为当前组。此后 :autocmd
或 :autocmd!
若不指定组名参数,就用 {name}
替代了。
那么,在第一次使用 :augroup
选定当前组名之前,当前组又是什么呢?那就是默认组 (default group)了。默认组没有名字,你要把它想象为空字符串也行。或者形式地说 ,默认组名是 END
或 end
,因为在以下命令表示选择默认组名:
: augroup END
因此,在脚本中定义自动命令的一般规范是这样的:
augroup SPECIFIC_GROUP
autocmd!
autocmd {event} {pat} {cmd}
augroup END
首先选定一个组,紧接着用 :autocmd!
删除该组内原来所有旧的自动命令,然后用 :autocmd
重新定义新的自动命令,可能有多条 :autocmd
自动命令,最后用 END
选回默认的(无名)组。这样,即使这个脚本重新加载,这个组内的自动命令也正是在这 块脚本内所能看到的这些自动命令了。
当然了,你的组名不要别的组冲突。建议依据脚本文件名或插件名定义组名,且用大写字 母,因为组名很重要,但其实又不必写很多次,故用大写字母表示合适。而且,尽量把自定 义命令写在一块,不要分散。
这样,在组内定义的自动命令就有了局部特性,相当于局部自动命令,而在组外的(无名 默认组)自动命令,就相当于全局自动命令。在编程的任何时刻,都尽量用局部的东西, 少用全局的东西。就自动命令而言,除了直接在命令行临时测试下什么自动命令,在脚本 插件中,永远不在默认的无名“全局”组定义自动命令。
另外提一点,退化的查询命令 :autocmd
在缺省组名参数时,不是依据当前组,而是列 出所有组内的自动命令。这与定义或删除自动命令时的缺省行为不同。这也好理解,因为 只是查询,还是希望尽可能查出更多,而修改操作,却要尽可能缩小影响范围。
还有,组名只影响定义与删除自动命令的操作,但不影响事件触发自动命令。即不管定义 在哪个组内,事件触发时,并且检测满足模式后,就能执行相应的自动命令。
使用事件
Vim 会监测大量事件,详细列表请查看文档 :help autocmd-events
,这里只介绍几种 常用的事件。事件名不分大小写,然而建议按文档中的名字使用事件。
- 读事件。有很多相似但略有细微差别的事件,
BufNewFile
指创建新文件,BufRead
指读入文件。一般用这两个就可以了。若有更多控制需求,可用BufReadPre
与BufReadPost
,这些事件一般会在:edit
等命令时触发。若用:read
命令,可 触发FileReadPre
与FileReadPost
事件。 - 写事件。
:w
写入当前文件时触发BufWrite
事件,部分写入(如'<,'>w file
)则触发FileWrite
事件。 - 窗口事件。新建窗口触发
WinNew
,进入窗口触发WinEnter
,离开窗口前触发WinLeave
事件。 - 标签页事件。类似窗口事件有
TabNew
TabEnter
TabLeave
。 - 整个编辑器启动与离开事件:
VimEnter
VimLeave
。 - 文件类型事件,当
&filetype
选项被设置时触发FileType
。
举些例子。为了方便,直接在命令行中定义自动事件了,只为简单测试。不过首先也创建 一个组吧,比如:
: augroup TEST
: augroup END
在这里,先是创建并选定 TEST
为当前组,然后什么也没干又用 END
选回默认组。 此后我们定义自动命令时都将显式地指这在 TEST
组上操作。你也可以先不用 :augroup END
,保持当前组为 TEST
,只为了想在之后的 :autocmd
缺省组名?但 是在命令行操作中说不定会触发加载其他插件,这样就会改变当前组名了。所以为了原子 操作的独立性,还是先选回默认组吧,也避免后来忘了执行 :augroup END
。
然后定义一个自动命令:
: autocmd TEST BufNewFile,BufRead * echomsg 'hello world!'
这里显式指定在 TEST
组内定义自动命令,:autocmd
只能使用已存在的组,所以我 们之前才要用 :augroup TEST
然后又 :augroup END
的“空操作”。BufNewFile
与 BufRead
经常同时用,这样不管是打开编辑已存在的文件,还是新建文件都能触发。在 {pat}
部分我们先简单用 *
表示匹配所有。最后的 {cmd}
部分仅是打印一条消息 。
现在请试试打开另一个文件,或切换另一个 buffer,看看会不会打出“Hello World!”的 消息。如果消息被其他后续消息覆盖而看不到,请用 :message
打开消息区(可能还须 用 G
翻到最后)再看是否有这个记录。
再定义另一个自动命令,在打开 vim 脚本文件中显示不同的消息:
: autocmd TEST BufNewFile,BufRead *.vim echomsg 'hello vim!'
然后用 :e $MYVIMRC
打开你的启动配置文件,看看有什么欢迎消息?似乎仍是打印“ Hello World!”,而不是“Hello vim!”?那么请用 :echo $MYVIMRC
查看下你的配置文 件是哪个文件,一般应该是 ~/.vimrc
或 ~/.vim/vimrc
,它并不是以 .vim
作为 后缀的文件名呢。所以不能匹配 *.vim
这个模式。
那么手动打开一个确实以 .vim
为后缀的文件再试试看吧,或者新建一个 vim 文件 :e none.vim
。不出意外的话,你应该会看到两条消息,“Hello World!”与“Hello vim!”都 打印了,因为它确实同时满足刚才定义的两个自动命令啊,所以两个都执行了。然后再试 试 :e none.VIM
,新建一个文件以大写的 .VIM
为后缀名。这也不会触发“Hello vim!”,可见文件模式是区别大小写的,它未能匹配到 .VIM
。关于模式的细节,下一小 节再详叙。
为了避免消息太多,我们先把刚才两个自动命令删除了,再定义另外一个自动命令:
: autocmd! TEST
: autocmd TEST BufNewFile,BufRead * echomsg 'hello ' . expand('<afile>')
这里,<afile>
表示在触发自动命令时,所匹配的那个文件名(一般是当前文件名)。 再试试打开文件,会打印什么欢迎消息?
切记:在用 autocmd!
删除命令时,要加上组名 TEST
,否则可能会删去一些定义在 默认组的自动命令。
写文件事件也一样定义自动命令:
: autocmd TEST BufWrite * echomsg 'bye ' . expand('<afile>')
然后随便编辑一个文件,用 :w
写入,是否能预期的“bye ...”消息。很可能看不到的 。因为 BufWrite
事件是在开始写的时刻触发,然后写完后 vim 一般会自动再打印另 一条消息显示写入多少字节。消息被覆盖了!但用 :message
再翻到末尾应该就能看到 了。那么我们把事件改为写之后试试:
: autocmd TEST BufWritePost * echomsg 'goodbye ' . expand('<afile>')
再看看写文件时会提示什么消息。顺便说一下,BufWritePre
事件与 BufWrite
其实 是等效的。如果没有特殊需要,建议用 BufWrite
比较简便。
然后再举个切换窗口的自动事件:
: autocmd TEST WinEnter * echomsg 'Enter Window: ' . winnr()
: autocmd TEST WinLeave * echomsg 'Leave Window: ' . winnr()
这里 winnr()
函数将取得当前窗口编号。定义完这两个自动事件后,请将你的 vim 分 裂出多个窗口,在窗口间切换,以及关闭多余窗口,看看会有什么消息提示(用 :message
G
确认消息)。由此你应该能得到结论,切换窗口时先触发 WinLeave
事件,再触发 WinEnter
事件。
其他事件就不一一举例了,请自行对感兴趣的事件进行测试。然后在实际写插件或脚本时 ,若想实现某个自动功能,先查阅文档,找个合适的事件,理解它的触发时机。如果 Vim 没有提供合适的事件,可能自动命令就无能为力了。不过幸运的是,Vim 已经提供了大量 的事件,应该能满足绝大部分需求了。或者,当你功夫足够深时,可以从近似的事件入手 进而曲线救国。
再次提醒,如果是在脚本中定义自动命令,请按以下规范写:
" save in somefile.vim
augroup TEST
autocmd!
autocmd BufNewFile,BufRead * echomsg 'hello ' . expand('<afile>')
autocmd BufWrite * echomsg 'bye ' . expand('<afile>')
autocmd BufWritePost * echomsg 'goodbye ' . expand('<afile>')
augroup END
在 :augroup
块内不必再指定 TEST
组名了,虽然也可以在每个 :autocmd
命令重 复加上这个组名,但是建议省略。因为万一以后因为某种原因要改组名,却忘记了同步修 改里面的每个组名,那就麻烦了。
所以,把 :augroup
与 :augroup END
当作像 :function!
与 :endfunction
一 样的独立单元块吧。只不过里面的命令不是由显式的 :call
调用,而是 vim 根据事件 自动调用了。于是,很显然地,自动命令组名应像(全局)函数名一样,不要与其他组名 冲突。
在实用的自动命令中,{cmd}
部分一般是调用一个工作函数,以简化 :autocmd
的语 法,而把复杂的逻辑实现放在函数中。特殊标记如 <afile>
表示匹配的文件名,在触 发自动命令时才展开。但有个例外,<sfile>
表示的是定义该自动命令时所在脚本文件 (假设你不是把自动命令放在函数中定义,一般应该是这样)。同时,在 {cmd}
部分 也可以用 <SID>
表示当前定义脚本范围的元素,比如 s:Function
。
文件模式
定义自动命令时 :autocmd
的第二参数(可选组名除外),即 {pat}
是文件模式的 意思。它不同于正则表达式,而像是操作系统的文件名通配符。即 *
表示任意字符, ?
表示单个字符。详细符号意义请查看 :help file-pattern
。这里只强调几点需要 注意的地方:
- 逗号表示多个模式的或意义。如
*.c,*.h,*cpp
表示c/c++
文件。 - 如果模式中没有路径分隔符
/
,则只匹配文件名。 - 如果模式中包含
/
则要匹配文件全路径名。如/vim/src/*.c
只匹配位于/vim/src/
目录下的 c 文件,这可能是 Vim 源代码的工程文件。而*/src/*.c
则匹配任意目录下的子目录src/
内的 c 文件,可能表示任意一 c 语言工程内的源 文件。 - 一些命令如
:edit
会将其参数内的环境变量(如$MYVIMRC
)与特殊寄存器(如%
与#
)展开,则在将实际文件名展开后再匹配自动命令中的文件模式。
如果文件模式 {pat}
用一个特殊参数 <buffer>
代替,则表示定义了一个只局部于 特定 buffer 的自动命令。这又有几个变种:
<buffer>
所定义的自动命令影响当前 buffer,即只有在当前 buffer 才能触发。<buffer=N>
这里N
是一个数字,表示只影响编号为N
的 buffer。用:ls
命令或bufnr()
函数可以查看 buffer 的编号,那算是唯一不变的 id。<buffer=abuf>
这里的<abuf>
是在触发自动命令时的特殊标记,如同<afile>
表示触发的文件,而<abuf>
表示触发的 buffer 编号。这个参数只在当自动命令中 定义另一个自动命令时有用。
例如,:autocmd BufNewFile * autocmd CursorHold <buffer=abuf> echo 'hold'
表 示每当新建一个文件(BufNewFile
事件)时,就为该文件 buffer 定义一个自动命令, 该自动命令的意图是每当 CursorHold
事件触发(光标停留一段时间),就打印一个消 息。
相当之下,<buffer>
参数更简单易懂,如该参数能满足局部自动命令的要求,优先使 用这个吧。例如,将 :autocmd {event} <buffer>
命令放在某个函数内,先通过其他 命令切换到正确的 buffer 内,再调用这个函数为该 buffer 定义局部自动命令。由于这 已经是局部自动命令了,加不加组名的影响都不那么大了。
其他提示
- 自动命令是相对高级的功能,可用
has('autocmd')
判断你的 Vim 版本是否已编译 了这个功能,或:version
看输出是否有+autocmd
。 - 文件类型检测的自动命令定义在
filetypedetect
组内,当你想创造新文件类型时, 也可往这个组内添加自动命令,如:autocmd filetypedetect *.xyx setfiletype xfile
。但没事不要误用:autocmd!
删除这个组内的其他自动命令。 - 嵌套的自动命令。默认情况下,自动命令中使用的命令如
:e
:w
不再继续触发读 写事件,但是加上nested
可选参数,可允许嵌套。如:autocmd {event} {pat} nested {cmd}
使得在执行{cmd}
时有可能继续触发自动命令(不过有最大嵌套层 数限制,除非必要,慎用)。nested
可选参数应位于{cmd}
之前,只有保持{cmd}
在最后部分,才方便在自动命令使用必要的空格啊。 - 自动命令也可以手动调用,当你觉得有这需求时再去查文档吧,
:doautocmd
与:doautoall
。 - 太多自动命令有可能降低效率,因此有个选项
&eventignore
可以指定忽略某些事件 。这不会删除自动命令,但有些事件不会触发了,相应自动命令也就不会执行了。在一 个命令之前附加:noautocmd {cmd}
可临时使得本次执行{cmd}
时不会触发自动命 令。如:noautocmd w
在这次写入过程中,不会触发写事件。