3.5* 自动命令与事件

优质
小牛编辑
126浏览
2023-12-01

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 变量名的规范吧(虽然帮助文档中说似乎可以用任意 字符串作为组名,除了空白字符),不要用奇怪的字符,同时也是大小写敏感的。然后两 个特殊的自动命令组名 ENDend 是保留的,有着特殊意义。

在不发生理解歧义下,我们就用自动命令组名表示一个自动命令组吧,且在本节中,不妨 用“组名”来作为自动命令组名的简写吧。

于是,在定义自动命令的 :autocmd 命令中,还支持一个可选的组名参数,它紧接命令 之后,而在 {envent} 事件之前:

: autocmd [group] {event} {pat} {cmd}

正因为组名是 :autocmd 的第一个参数,可有可无,当省略时,第一个参数就是事件名 了。所以我们选取组名时,还要避免与事件名(这是 Vim 预设的范围集)重名,以避免歧 义。

在定义自动命令时,如果指定了 [group] 组名参数,就表示将所定义的自动命令添 加到这个自动命令组中。你可以认为每个组都为不同事件维护了不同的自动命令列表,同 一事件在不同组内关联着各自不同的命令列表。

对于删除自动命令的 :autocmd! 变异命令,也同样支持在第一个参数中插入可选的组 名。在指定组名后,就表示只删除该组内的自动命令(当然可再限定事件与模式)。

那么,在缺省组名参数时,:autocmd:autocmd! 又怎样工作的呢。其实它是针对 当前组添加或删除自动命令的。那么当前组又是什么东西呢?它是用 :augroup 命令选 定的:

: augroup {name}

在执行这个命令之后,{name} 就是当前组名了。当 {name} 组名此前尚不存在时, 也会自动创建一个组,然后再选择这个组作为当前组。此后 :autocmd:autocmd! 若不指定组名参数,就用 {name} 替代了。

那么,在第一次使用 :augroup 选定当前组名之前,当前组又是什么呢?那就是默认组 (default group)了。默认组没有名字,你要把它想象为空字符串也行。或者形式地说 ,默认组名是 ENDend,因为在以下命令表示选择默认组名:

: augroup END

因此,在脚本中定义自动命令的一般规范是这样的:

augroup SPECIFIC_GROUP
    autocmd!
    autocmd {event} {pat} {cmd}
augroup END

首先选定一个组,紧接着用 :autocmd! 删除该组内原来所有旧的自动命令,然后用 :autocmd 重新定义新的自动命令,可能有多条 :autocmd 自动命令,最后用 END 选回默认的(无名)组。这样,即使这个脚本重新加载,这个组内的自动命令也正是在这 块脚本内所能看到的这些自动命令了。

当然了,你的组名不要别的组冲突。建议依据脚本文件名或插件名定义组名,且用大写字 母,因为组名很重要,但其实又不必写很多次,故用大写字母表示合适。而且,尽量把自定 义命令写在一块,不要分散。

这样,在组内定义的自动命令就有了局部特性,相当于局部自动命令,而在组外的(无名 默认组)自动命令,就相当于全局自动命令。在编程的任何时刻,都尽量用局部的东西, 少用全局的东西。就自动命令而言,除了直接在命令行临时测试下什么自动命令,在脚本 插件中,永远不在默认的无名“全局”组定义自动命令。

另外提一点,退化的查询命令 :autocmd 在缺省组名参数时,不是依据当前组,而是列 出所有组内的自动命令。这与定义或删除自动命令时的缺省行为不同。这也好理解,因为 只是查询,还是希望尽可能查出更多,而修改操作,却要尽可能缩小影响范围。

还有,组名只影响定义与删除自动命令的操作,但不影响事件触发自动命令。即不管定义 在哪个组内,事件触发时,并且检测满足模式后,就能执行相应的自动命令。

使用事件

Vim 会监测大量事件,详细列表请查看文档 :help autocmd-events,这里只介绍几种 常用的事件。事件名不分大小写,然而建议按文档中的名字使用事件。

  • 读事件。有很多相似但略有细微差别的事件,BufNewFile 指创建新文件,BufRead 指读入文件。一般用这两个就可以了。若有更多控制需求,可用 BufReadPreBufReadPost,这些事件一般会在 :edit 等命令时触发。若用 :read 命令,可 触发 FileReadPreFileReadPost 事件。
  • 写事件。: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 的“空操作”。BufNewFileBufRead 经常同时用,这样不管是打开编辑已存在的文件,还是新建文件都能触发。在 {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 在这次写入过程中,不会触发写事件。