You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
20 KiB

2 years ago
# 第三章 Vim 常用命令
## 3.5\* 自动命令与事件
前面章节介绍了自定义快捷键(`:map`)与自定义命令(`:command`),这都是响应玩家
的主动输入而快速做些有用的工作。这也算是对 Vim 的 UI 设计吧。谁说只有图形界面
才算 UI 呢,况且在 gVim 中的自定义菜单,也确实与自定义命令或映射很相似呀。
本节要介绍的自动命令,却是让 Vim 在某些事件发生时自动做些工作,而不必再手动激
活命令了。当然了,自动命令在生效前,也是需要定义的。
### 自动命令的定义语法
自动命令用 `:autocmd` 这个内置命令定义,它至少要求三个参数:
```vim
: 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` ,就如定义自定义模式一样:
```vim
: autocmd! {event} {pat} {cmd}
```
它表示先将满足事件 `{event}` 与模式 `{pat}` 的所有自动命令删除,然后添加自动命
`{cmd}` 。因此这是覆盖式的定义自动命令,此后,在满足相应事件与模式时,就只
会执行这一个自动命令了。依前文介绍,在定义命令与函数时建议用覆盖式的叹号修饰命
`:command!``:function!`。但对于自动命令,还是慎重用覆盖式的 `:autocmd!`
,因为可能无法从本条语句判断会覆盖掉什么自动命令。
### 自动命令组
自动命令组 `augroup` 是组织管理自动命令的有效手段。为理解自动命令组是有必要的
,先回顾上一小节所介绍的自动命令机制,在未利用命令组的情况下,会发生什么不良后
果。
因为 `:autocmd` 定义自动命令时是将其添加到自动命令列表末尾的,所以如果在脚本如
`vimrc` 中定义了自动命令,随后又重新加载了该脚本,那自动命令列表中就会出现两项
重复的自动命令了。对于某些“安全”的自动命令,重复执行不外是浪费效率而已,但有些
自动命令在第二次执行却有可能引发错误呢。
其次,用 `:autocmd!` 删除自动命令时,它是删除*所有*自动命令。即使加了事件与模
式两个限制条件,也无法避免影响扩大化,因为别的插件或 Vim 官方插件也可能为相同
的事件与模式定义的一些有用的自动命令啊。
为了解决这个管理问题,引入了自动命令组的概念。自动命令组名字是用以标记一个自动
命令组的符号,取名规则就按 VimL 变量名的规范吧(虽然帮助文档中说似乎可以用任意
字符串作为组名,除了空白字符),不要用奇怪的字符,同时也是大小写敏感的。然后两
个特殊的自动命令组名 `END``end` 是保留的,有着特殊意义。
在不发生理解歧义下,我们就用自动命令组名表示一个自动命令组吧,且在本节中,不妨
用“组名”来作为自动命令组名的简写吧。
于是,在定义自动命令的 `:autocmd` 命令中,还支持一个可选的组名参数,它紧接命令
之后,而在 `{envent}` 事件之前:
```vim
: autocmd [group] {event} {pat} {cmd}
```
正因为组名是 `:autocmd` 的第一个参数,可有可无,当省略时,第一个参数就是事件名
了。所以我们选取组名时,还要避免与事件名(这是 Vim 预设的范围集)重名,以避免歧
义。
在定义自动命令时,如果指定了 `[group]` 组名参数,就表示将所定义的自动命令添
加到这个自动命令组中。你可以认为每个组都为不同事件维护了不同的自动命令列表,同
一事件在不同组内关联着各自不同的命令列表。
对于删除自动命令的 `:autocmd!` 变异命令,也同样支持在第一个参数中插入可选的组
名。在指定组名后,就表示只删除该组内的自动命令(当然可再限定事件与模式)。
那么,在缺省组名参数时,`:autocmd` 与 `:autocmd!` 又怎样工作的呢。其实它是针对
当前组添加或删除自动命令的。那么当前组又是什么东西呢?它是用 `:augroup` 命令选
定的:
```vim
: augroup {name}
```
在执行这个命令之后,`{name}` 就是当前组名了。当 `{name}` 组名此前尚不存在时,
也会自动创建一个组,然后再选择这个组作为当前组。此后 `:autocmd``:autocmd!`
若不指定组名参数,就用 `{name}` 替代了。
那么,在第一次使用 `:augroup` 选定当前组名之前,当前组又是什么呢?那就是默认组
(default group)了。默认组没有名字,你要把它想象为空字符串也行。或者形式地说
,默认组名是 `END``end`,因为在以下命令表示选择默认组名:
```vim
: augroup END
```
因此,在脚本中定义自动命令的一般规范是这样的:
```vim
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`
举些例子。为了方便,直接在命令行中定义自动事件了,只为简单测试。不过首先也创建
一个组吧,比如:
```vim
: augroup TEST
: augroup END
```
在这里,先是创建并选定 `TEST` 为当前组,然后什么也没干又用 `END` 选回默认组。
此后我们定义自动命令时都将显式地指这在 `TEST` 组上操作。你也可以先不用
`:augroup END`,保持当前组为 `TEST`,只为了想在之后的 `:autocmd` 缺省组名?但
是在命令行操作中说不定会触发加载其他插件,这样就会改变当前组名了。所以为了原子
操作的独立性,还是先选回默认组吧,也避免后来忘了执行 `:augroup END`
然后定义一个自动命令:
```vim
: autocmd TEST BufNewFile,BufRead * echomsg 'hello world!'
```
这里显式指定在 `TEST` 组内定义自动命令,`:autocmd` 只能使用已存在的组,所以我
们之前才要用 `:augroup TEST` 然后又 `:augroup END` 的“空操作”。`BufNewFile` 与
`BufRead` 经常同时用,这样不管是打开编辑已存在的文件,还是新建文件都能触发。在
`{pat}` 部分我们先简单用 `*` 表示匹配所有。最后的 `{cmd}` 部分仅是打印一条消息
现在请试试打开另一个文件,或切换另一个 buffer,看看会不会打出“Hello World!”的
消息。如果消息被其他后续消息覆盖而看不到,请用 `:message` 打开消息区(可能还须
`G` 翻到最后)再看是否有这个记录。
再定义另一个自动命令,在打开 vim 脚本文件中显示不同的消息:
```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`。关于模式的细节,下一小
节再详叙。
为了避免消息太多,我们先把刚才两个自动命令删除了,再定义另外一个自动命令:
```vim
: autocmd! TEST
: autocmd TEST BufNewFile,BufRead * echomsg 'hello ' . expand('<afile>')
```
这里,`<afile>` 表示在触发自动命令时,所匹配的那个文件名(一般是当前文件名)。
再试试打开文件,会打印什么欢迎消息?
切记:在用 `autocmd!` 删除命令时,要加上组名 `TEST`,否则可能会删去一些定义在
默认组的自动命令。
写文件事件也一样定义自动命令:
```vim
: autocmd TEST BufWrite * echomsg 'bye ' . expand('<afile>')
```
然后随便编辑一个文件,用 `:w` 写入,是否能预期的“bye ...”消息。很可能看不到的
。因为 `BufWrite` 事件是在开始写的时刻触发,然后写完后 vim 一般会自动再打印另
一条消息显示写入多少字节。消息被覆盖了!但用 `:message` 再翻到末尾应该就能看到
了。那么我们把事件改为写之后试试:
```vim
: autocmd TEST BufWritePost * echomsg 'goodbye ' . expand('<afile>')
```
再看看写文件时会提示什么消息。顺便说一下,`BufWritePre` 事件与 `BufWrite` 其实
是等效的。如果没有特殊需要,建议用 `BufWrite` 比较简便。
然后再举个切换窗口的自动事件:
```vim
: autocmd TEST WinEnter * echomsg 'Enter Window: ' . winnr()
: autocmd TEST WinLeave * echomsg 'Leave Window: ' . winnr()
```
这里 `winnr()` 函数将取得当前窗口编号。定义完这两个自动事件后,请将你的 vim 分
裂出多个窗口,在窗口间切换,以及关闭多余窗口,看看会有什么消息提示(用
`:message` `G` 确认消息)。由此你应该能得到结论,切换窗口时先触发 `WinLeave`
事件,再触发 `WinEnter` 事件。
其他事件就不一一举例了,请自行对感兴趣的事件进行测试。然后在实际写插件或脚本时
,若想实现某个自动功能,先查阅文档,找个合适的事件,理解它的触发时机。如果 Vim
没有提供合适的事件,可能自动命令就无能为力了。不过幸运的是,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` 在这次写入过程中,不会触发写事件。