第三章 Vim 常用命令 #
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事件。 - 标签页事件。类似窗口事件有
TabNewTabEnterTabLeave。 - 整个编辑器启动与离开事件:
VimEnterVimLeave。 - 文件类型事件,当
&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在这次写入过程中,不会触发写事件。