# 第三章 Vim 常用命令 ## 3.3 自定义命令 ### 命令语法 定义命令与定义映射的用法其实很相似: ```vim :command {lhs} {rhs} ``` 只不过在使用自定义命令时,`{lhs}` 是直接输入到命令行中的,当你按下回车时,vim 就将 `{lhs}` 替换为 `{rhs}` 再执行。所以这在形式上与下面这个映射等效: ```vim : nnoremap :{lhs} :{rhs} ``` 当然,由于 `:command` 所支持的参数与 `:map` 大相径庭,并不期望你真的按这方式将 自定义命令改成映射。实际上,Vim 的帮助文档中这样描述自定义命令的语法的: ```vim :command {cmd} {rep} ``` `:command!` 加个叹号修饰则表示重新定义命令 `{cmd}`,否则若之前已定义 `{cmd}` 命令,`:command` 原版会报错。这是为了保护已定义不被覆盖,当你确实要覆盖时,请 加 `!` 后缀。在实践中,一般都是在脚本中定义命令,建议只用 `!` 即可,尤其是在开 发阶段需要调试脚本时,加上 `!` 方便很多。 大部分命令的 `!` 修饰版都是表示强制执行,忽略错误的意思。但上一节介绍的 `:map!` 的意义太奇葩,建议直接忘记 `:map!` 的用法。 `:command` 命令的退化用法是一致的: * `:command {cmd}` 列出以 `{cmd}` 开头的自定义命令; * `:command` 列出所有自定义命令; Vim 的内置命令都是小写的(除了 `:Next` 与 `:X` `:Print`),所以要求自定义命令 名 `{cmd}` 只能以大写字母开头,其后就类似 VimL 变量名的要求了。然而也不建议在 命令名中使用数字,因为这可能与数字参数混淆。 内置命令可以缩写(这与上节的缩写映射不是同个东西),在没有歧义时,只要输入命令 名的前几个字母就可以了。自定义命令 `{cmd}` 同样可获得此基本福利。不过内置命令 还有更好的福利,就是钦定的缩写,比如 `s` 是替换命令 `substitute` 的缩写,但它 不会与 `set` 发生歧义,而 `set` 的缩写是 `se`。自定义命令却无此特性,只能按基 本规则,输入尽可能多的前缀字符来达到唯一确定命令名的目的。不过缩写只建议在命令 行中使用,在脚本中尽量使用全名。 ### 命令属性 在自定义命令时,可支持多种属性,就像 `:map` 的特殊参数(用 `<>` 括起来的)。但 是在 `:command` 中,以一个 `-` 引导一个属性(更像 shell 命令行的选项)。所有属 性必须出现在命令名 `{cmd}` 之前。 * `-buffer` 局部命令,只能用于当前 buffer。 * `-bang` 该自定义命令允许有 `!` 后缀修饰。 * `-register` 第一个参数允许是寄存器名。 * `-bar` 该自定义命令后面允许用 `|` 分隔,接续另一个命令。在这种情况下,`{rep}` 参数内就不能有 `|` 了,否则会出现解析歧义。 以上这几个属性,只有 `-buffer` 是常用的,并且建议能局部化时尽量局部化。其他 的属性则较少用到。`-bang` 与 `-register` 只相当于某种特殊参数,而在同一行中用 `|` 使用多个语句(命令)的骚操作,能不用尽量不用。 然后,命令还支持几个复杂的属性,用 `-attribute=value` 表示,允许为属性指定值, 要注意的是等号前后没有空格,而将整体当作 `:command` 命令的一个参数。 * 参数个数,自定义命令 `{cmd}` 允许多少个参数: - `-nargs=0` 这是默认行为,不指定该属性就表示命令不接受参数; - `-nargs=1` 仅接受一个参数; - `-nargs=*` 接受 0 或多个参数; - `-nargs=?` 接受 0 或 1 个参数; - `-nargs=+` 接受 1 或多个参数。 按常规用法,多个参数用空格分隔(或制表符)。但如果只有一个参数,末尾的空格会被 认为是参数的一部分。否则若要参数中包含空格,请用 `\` 转义。 * 范围数字释义,是否允许在命令之前加上一个或两个(以逗号分隔)数字: - `-range` 允许两个地址参数或一个数字参数。不加该属性时,自定义命令默认不接 收数字或地址参数。但这只是允许,可选加或不加,也不提供默认数字或地址。 - `-range=%` 允许地址参数,且默认是全 buffer,相当于 `1,$`。 - `-range=N` 允许一个数字参数,默认是 `N`,只能用在命令名之前。 - `-count=N` 与 `-range=N` 类似,不过数字参数不仅可以出现在命令名之前,也可 以出现在命令名之后(相当于第一个参数)。`-count` 与 `-count=0` 等效。不过 注意,`-range` 属性与 `-count` 属性是互斥的,最好只用其中一个属性。 * 特殊地址`.` `$` `%` 所表示的范围(在允许 `-range` 时): - `-addr=lines` 这也是默认行为,取当前 buffer 文本行的范围。 - `-addr=arguments` 指打开 vim 时命令行的文件名参数(其实也可以更改)。 - `-addr=buffers` 指所有打开过的 buffer。 - `-addr=loaded_buffers` 仅指当前加载的 buffer,在某个窗口中显示的 buffer。 - `-addr=windows` 取所有窗口列表的范围,仅限当前标签页。 - `-addr=tabs` 取所有标签页范围。 注意,`-addr` 属性必须要与 `-range` 联用才有意义。它要说明的是当命令的地址参数 使用 `.`(当前)`$`(最后)`%`(所有)是参照什么集合而言的。例如定义如下命令: ```vim : command -range CmdA {rhs} : command -range=% -addr=buffers CmdB {rhs} : command -range=% -addr=tabs CmdT {rhs} ``` 则使用命令时,`:.,$CmdA` 表示用命令 `CmdA` 处理当前 buffer 内当前行到最后一行 之间的文本行。`:CmdB` 表示处理所有 buffer,因为 `-range` 的默认范围是 `%` 表示 所有,而 `-addr` 表示所有的集合是指所有 buffer。同样,`:.,$CmdT` 表示处理从当 前标签页到最后一个标签页,虽然 `-range=%` 表示默认所有,但使用时可以自己加个特 定的地址参数呀。 ### 命令补全 自定义命令还有个最复杂的属性,是有关补全特性的。值得单独拿出来讨论。 Vimer 初学者倾向于使用映射,可能较少用到自定义命令。但是随着对 Vim 深入使用与 理解,可能就会发觉键盘的映射资源是有限的,尤其是要有规律地组织许多容易记住的映 射会有瓶颈。这时不妨将眼光投入到自定义命令中。虽然使用命令没有映射那么快,但只 不过多加冒号与回车,就几乎有了无限的扩展可能。而且,在命令行中,不仅命令名可以 补全,命令参数也可以补全,这就大大减少了记忆负担。 `-complete` 属性就是用于指定命令如何补全参数的,其取值范围非常广,这里仅介绍几 种主要的补全行为,全部列表请参考 `:help :command-complete`: * `-complete=file` 按文件(包含目录)补全,就像 `:edit ` 命令按 `` 后会补 全文件名那样。 * `-complete=option` 补全选项名。 * `-complete=help` 补全帮助主题。 * `-complete=shellcmd` 补全外部 shell 可用的命令。 * `-complete=tag` 补全标签,类似 `:tag ` 所需的参数。 * `-complete=filetype` 补全文件类型名。 总之,如果自定义命令期望它的参数是某一类意义上的参数,就可以指定 `-complete` 属性为相应的值,以方便输入参数。当然,如果你定义的某个命令要实现比较复杂的功能 ,vim 预设提供的补全行为都不满足要求的话,还可以指定一个函数来实现补全。 * `-complete=custom,{func}` * `-complete=customlist,{func}` 这也叫做自定义补全。要注意的是,`=` 与 `,` 前后都没有空格,在 `custom,` 或 `customlist,`后直接接一个函数名。 当 `-complete` 属性值是 `custom` 时,函数要求返回一个以回车 `\n` 分隔的字符串 ,每一行是一个候选补全项。且 vim 会自动匹配比较光标前已经输入的部分参数前缀, 进行一些过滤。 当 `-complte` 属性值是 `customlist` 时,函数要求返回一个列表,每个元素是候选补 全项。但 Vim 不会自动对参数前缀过滤,可能要求用户自己在函数中过滤。 在这两种情况,补全函数的定义都是类似的,它应该接收三个参数: 1. `a:ArgLead` 光标之前的部分参数前缀, 2. `a:CmdLine` 整个命令行文本, 3. `a:CursorPos` 当前光标在命令行的位置(按字节计,从1开始)。 当用户按下补全键(一般是``),Vim 会自动将这三个参数传给自定义补全函数。 用户在这个函数实现可利用这三个参数所提供的信息(也许不一定要用到全部),返回合 适的候选补全项。 ### 命令实现 我们将自定义名之后的 `{rep}` 参数部分称为命令实现。它可以是一串简单的替换文本 ,但真正有趣的是它可用一些特殊标记来表示特殊的或动态的内容。这里的特殊标记也用 尖括号 `<>` 括起,所支持的有意义的标记可能依赖于前面的的命令属性。 * `` `` 分别表示地址参数的两个数字(一般是第一行与最后一行)。含 `-range` 属性的命令才能接收这两个参数。 * `` 就是由 `-count` 属性提供的数字参数。 * `` 支持 `-bang` 属性的命令,如果使用时加了 `!` 修饰,则在 `{rep}` 中的 `` 标记转换为 `!` 字符,否则就没任何效果。 * `` 或简写为 ``,支持 `-register` 属性的命令,表示可选的寄存器参 数;否则也没任何效果(加上引号 `""` 才表示空字符串)。 * `` 代表左尖括号 `<`,避免尖括号的特殊意义。比如想在 `{rep}` 中字面地呈现 `` 这几个字符串,而不是转化为 `!` 字符,就可用 `bang>`。 先举个简单的例子,我们已经知道 `:map!` 命令是列出某类映射。虽然上文说过应该忘 记这个命令,不过正因为它安全无害,不妨再拿来作为演示讲解。首先定义这个命令: ```vim : command! MAP map ``` 这个自定义命令似乎很无趣,不过用大写版的 `:MAP` 代替内置的 `:map`。请试试在命 令中输入 `:MAP` 并回车执行,其结果与直接使用 `:map` 是一样的。试试 `:MAP!` 呢 ?Vim 会报错,说这个命令不支持 `!`。那么重定义一下这个命令: ```vim : command! -bang MAP map ``` 现在,应该 `:MAP` 与 `:MAP!` 命令都可以使用了,并且分别与 `:map` 与 `:map!` 等 价。这就是 `` 用于命令实现参数 `{rep}` 中的代表意义。同时,如果你没有定 义其他以 `MA` 开头的命令,那么我们这个自定义命令简写成 `:MA` 或 `:MA!` 也是可 以的。 由于这个自定义没有加 `-nargs` 属性,默认是不能接收参数的,所以若试图用 `:MAP lhs rhs` 来定义映射会失败。但是,加了参数属性后,又如何在 `{rep}` 中使用相应的 参数呢?这就是 `` 标记的用途,同时这有多个变种: * `` 将用户在自定义命令后输入的参数原样替换到 `{rep}` 中。不过若命令还有 `-count` 或 `-register` 属性的话,前面的属性应该由 `` 或 `` 捕获 ,而 `` 只表示剩余的参数。 * `` 与 `` 一样,先捕获所有参数,然后将所有参数用引号括起来作为 一个字符串表达式参数。如果没有参数,这将是一个空字符串(包含引号如 `""`)。 * `` 也与 `` 一样,只不过将捕获的参数分隔成适用于函数调用时小括 号内的参数列表,所以是将每个参数分别引起,并用逗号分隔。这在 `{rep}` 实现中 调用一个函数中非常有用。如果没有参数,则所调用函数的小括号内也没有任何东西, 即以空参数调用。 现在继续来改造我们的自定义命令 `MAP`: ```vim : command! -bang -nargs=* MAP map ``` 这样,`:MAP` 与 `:MAP!` 可以继续用,而且也可以用它来定义映射了,例如: ```vim : MAP x dd ``` 这里,用自己的 `:MAP` 来定义一个映射,将 `x` 删除一个字符的功能改为删一行 。不过由于只为试验,所以加 `` 定义成局部映射(注意区别,定义局部命令用 `-buffer` 语法)。 由于我们在定义 `MAP` 时允许它接收任意个参数 `-nargs=*`。所以在 `:MAP x dd` 这个使用场合下,`:MAP` 的所有参数 ` x dd` 替换在定义 `MAP` 时 `` 的位置上,也就相当于执行 `:map x dd`。可以试下执行完,再按 `x` 是不是实现了预期效果,同时也可以用 `:MAP x` 或 `:map x` 查看下将 `x` 定义 成啥样的映射了。 在这个示例中,如果将定义 `MAP` 时的 `` 改成 `` 或 `` 的 话,结果就不正确了,不能仿拟 `:map` 命令了。在实现复杂命令时,后两个参数变种标 记才更有用,作为函数调用的参数。不过这较为复杂,留待下一小节再论。这里先探讨一下 `` 参数的使用,假设继续为 `MAP` 命令添加这个属性: ```vim : command! -bang -register -nargs=* MAP map ``` 先将原来定义的 `x` 映射删除:`:unmap x`。然后再用新的 `:MAP` 命令定义 `x` 映射,不过在参数 `` 前额外加个参数 `n`: ```vim : MAP n x dd ``` 结果是相当于只定义了普通模式下的映射 `:nmap x dd`。你可以用 `:map x` 查看一下 `x` 的映射定义确认。并且对比一下 `:MAP X dd` 不加 `n` 的用法 。 结论就是 `` 不过是捕获了第一个参数,`` 捕获其他参数。而 `MAP` 的定义 `map ` 表明是将第一个参数直接拼在 `map` 之前作为 映射命令的模式前缀限定,而将其他参数用空格分开后作为 `:map` 命令的参数了。 这样看来,`` 似乎很名副其实呀。那么我们再尝试下将 `un` 作为 `:MAP` 的 第一个参数,看它会不会变成 `:unmap` 用于删除映射: ```vim : MAP un x : MAP un X ``` 然而,这次 vim 报错了,提示 `umap n x` 不是一个命令。由些可见, `` 只捕获的第一个字母 `u`,然后将剩余的东西都当成 `` 了。因为 寄存器名都是一个字母啊。 vim 有些内置命令如 `:del` `:yank` `:put` 支持后面接一个寄存器名(比如 `a`), 表示对相应的寄存器操作,相当于普通模式的命令 `"ad` `"ay` `"ap`。自定义命令就可 用 `` 实现类似的特性,使得自定义命令能像内置命令一样使用。只不过, `` 只能捕获参数中的第一个字母,把它当成是寄存器名,传给 `{rep}` 实现 部分,却无法控制 `{rep}` 如何处理这个字母。因为 `:map` 命令的模式前缀限定恰好 也只是一个字母,所以我们的 `:MAP` 就可以用 `` 进行伪装了。你可以自行 尝试 `:MAP i` `:MAP c` 等用法应该也是有效的。 上一节也提前,使用映射命令,尽量使用更安全的 `:noremap`,所以再重定义命令: ```vim : command! -bang -register -nargs=* MAP noremap ``` 要测试这个命令是否有效,可定义如下映射: ```vim : MAP n x xx ``` 再按 `x` 看看是否能正确只删除两个字符,还是会发生无尽循环故障(如果有这问题, 按 `` 中断即可)。 再次提醒:这里讨论不断“优化” `:MAP` 命令,只为说明 `:command` 自定义命令的用法 与机制。正常使用 vim 下,应该没必要定义这么个命令呀。 ### 自定义命令调用函数 除了很简单的命令,可以调用 vim 既有的内置命令(可能进行必要的包装修饰)外,大 多实用的自定义命令,都是通过调用函数来实现命令要求的功能。这不仅可以实现很复杂 的功能,也容易扩展,还使得用法简明易记,因为它一般是如下的形式结构之一: ```vim :command! {cmd} call WorkFunc() :command! {cmd} call WorkFunc() ``` 当使用自定义命令 `{cmd}` 时,它后面的命令行参数就会传入实际工作的函数 `WorkFunc()` 中。`` 按空格分隔多个参数,然后分别引为字符串参数传入,如 果要在参数中包含空格,要用 `\ ` 转义,要传入 `\` 就要用两个反斜杠即 `\\`。而 `` 则简单粗暴,将 `{cmd}` 的所有参数,也就是其后跟着的所有内容当一个 字符串参数传入。在 `{cmd}` 之后没有任何参数时,`` 也至少传入一个空字符 串参数(`WorkFunc("")`),但 `` 就不传入任何参数了(`WorkFunc()`)。 注意:传入 `WorkFunc()` 的参数必定是字符串类型,但由于 VimL 弱类型与自动转换, 如果一个参数像数字,那么在函数体内将它当作数字处理也完全没有问题。 按 `` 方式调用函数更为常见。`` 可能只用于比较特殊的需要,然后 要自己在函数体内解析字符串参数。另外,`` 只适用于函数调用参数,用在其 他地方的意义不明显,且易出错。而 `` 用于函数参数之外也可能是有意义的。 本小节暂时不讨论 `` 的使用。 #### 使用 range 首先我们需要一个工作函数。不妨复用在 2.4 节讲述函数时使用的给文本行编号的示例 函数吧,取那个支持 `range` 特性的版本,并改名为 `NumberLine` 重贴于下: ```vim " File: ~/.vim/vimllearn/fcommand.vim function! NumberLine() abort range for l:line in range(a:firstline, a:lastline) let l:sLine = getline(l:line) let l:sLine = l:line . ' ' . l:sLine call setline(l:line, l:sLine) endfor endfunction ``` 然后定义一个命令也叫 `NumberLine`,用以调用该函数,命令名与函数不需要相同,只 是懒得另起名字,同时也想说明,命令与函数重名完全没问题,因为它们是完全不是同类 概念: ```vim : command! -range=% NumberLine ,call NumberLine() ``` 注意到 `NumberLine()` 函数不支持显式参数,但可接收隐式的地址参数。而命令 `:NumberLine` 正好定义为支持 `-range` 属性,这就要将捕获的地址参数 `,` 放在 `call` 之前,由 `call` 把地址参数传给 `NumberLine()` 函 数的 `a:firstline` 与 `a:lastline`。 现在我们就可以来试用这个自定义命令了。如果直接在命令行输入 `:NumberLine` 回车 执行,它会对当前 buffer 的所有文本行编号。因为 `-buffer` 属性的默认值 `%` 就表 示所有行,相当于 `1,$`。如果我们按行可视模式 `V` 选择几行,再按 `:NumberLine` ,命令行中实际输入的是 `:'<,'>NumberLine` ,它就只会对选择的行进行编号。 #### 使用 count 接着讨论下与 `-range` 相似但互斥的 `-count` 属性。`` 只有一个数字参数, 即可放在命令之前,也可以放在命令之后(甚至对是否有空格分隔不敏感)。很多 vim 内置命令的数字表示重复次数,不过在自定义命令中,`` 只负责捕获传递这个数 字参数,并无法控制后续命令如何使用这个数字,就如 `` 一样。 我们另外写个函数,用于对当前行及后面若干行进行相对编号,即当前行号是 `0`,下一 行是 `1` 等(类似 `:set relativenumber`)。 ```vim function! NumberRelate(count) abort let l:cursor = line('.') let l:eof = line('$') for l:count in range(0, a:count) let l:line = l:cursor + l:count if l:line > l:eof break endif let l:sLine = getline(l:line) let l:sLine = l:count . ' ' . l:sLine call setline(l:line, l:sLine) endfor endfunction command! -count NumberRelate call NumberRelate() ``` 同时也定义一个相应的命令。试试效果?如果直接运行 `:NumberRelate` ,由于 `-count` 的默认值是 0,所以只对当前行编号为 0。如果对选区运行 `:'<,'>NumberRalate`,给命令提供了两个地址参数?但该命令只接收一个数字参数啊, vim 只会将后面那个地址参数 `'>` 当作数字参数 `` 传给函数 `NumberRelate()` 的参数。同时也可以手动输入数字如 `:3NumberRelate` 或 `:NumberRelate3` 都会对当前行及后面3行编号。其中 `NumberRelate3` 的写法可能会 有歧义,如果恰好还有个自定义命名叫叫 `NumberRelate3`。所以最好用 `:NumberRelate 3` 来调用。也正是这个原因,不建议在命令名中混入数字。 至于 Vim 为什么允许命令与数字参数粘在一起使用,主要是因为要快捷输入。很多最常 用的命令都是有单字母缩写的,而与数字参数的组合使用又极频繁。在这种情况情况下多 敲一个空格的性价比太低了(我的命令才一个字母呢),所以就把空格吃了吧。 这个示例也说明,自定义命令调用函数时,参数不一定要用 `` 或 `` ,混入其他任何特殊标记也是可以的,只要展开替换后符号函数调用语法即可。再比如, `call WorkFunc()` 是非法的,因为展开是 `call WorkFunc(!)`,但 `call WorkFunc("")` 是合法的,因为展开后是 `call WorkFunc("!")`。而 `` (其实也包括 `` ``)可直接放入函数括号内,是因为它们会展开成一个 数字。 #### 使用 f-args 前面两例所用的函数都不接收参数,如果函数要求参数,就用 `` 传入吧。假设 更改为文本行编号的需求,在数字编号后还允许加个后缀字符,像 `1.` `1)` 之类的, 同时可以定制分隔编号与原文本之间的空格数量。我们重写 `NumberLine` 函数,让它接 收两个参数: ```vim function! NumberLine(postfix, count) abort range let l:sep = repeat(' ', a:count) " 生成含 count 个空格的字符串 for l:line in range(a:firstline, a:lastline) let l:sLine = getline(l:line) let l:sLine = l:line . a:postfix . l:sep . l:sLine call setline(l:line, l:sLine) endfor endfunction command! -range=% -nargs=+ NumberLine ,call NumberLine() ``` 然后也重定义命令 `:NumberLine`,为其增加 `-nargs` 属性,然后用 `` 传给 函数调用。注意虽然可以用 `-nargs=1` 限定允许一个参数,但不支持 `-nargs=2` 限定 恰好两个参数,只能用不定数量的 `-nargs=*` 或 `-nargs=+`。此时若只用 `:NumberLine` 命令执行,会报错说参数太少,加上两个命令行参数后如 `:NumberLine ) 4` 就能正常工作了,这表示编号样式为 `1)` 然后接 4 个空格。 注意到 `NumberLine()` 函数虽然也有个 `count` 参数。但与上例不同,不能用 `-count` 属性与 `` 参数。首先是因为 `-count` 与 `-range` 属性只能用一个 ,不能共存。其次这里的 `count` 参数与大多 vim 内置命令对数字参数的解释很有些不 同,只是恰好用了这个形参名而已。因此不要滥用 `` 参数,能直接用 `` 是最简洁明了的。 如果工作函数 `WorkFunc()` 没有 `range` 属性,不处理地址范围的话,那么自定义命 令时,也不要加 `-range` 属性,而后面的调用函数写法也更加简单。 另外,如果工作函数是脚本作用域的函数,如 `s:WorkFunc()`,则在 `{rep}` 部分中调 用写成 `WorkFunc()`,高版本的 vim 也可以直接用 `s:WorkFunc()`。不过上节的 映射命令 `:map`,却只能用 `` 而不能用 `s:`。 ### \*微命令实例 本节内容所用的命令示例,主要为阐述概念,也许并无实用性。我在大量使用映射后,也 开始对命令有所偏爱了。为了使命令输入尽可能方便,我将常用命令也定义很短的几个大 写字母,并称之为“微命令”。实现脚本放在了 github 上,有兴趣的可以参考,传送门在 此:https://github.com/lymslive/autoplug/tree/master/autoload/microcmd 如果命令名较长,输入不便时,也可以继续使用映射来触发命令,甚至可以将最常用的命 令参数也一并包含在映射中。