第三章 Vim 常用命令
3.3 自定义命令
命令语法
定义命令与定义映射的用法其实很相似:
:command {lhs} {rhs}
只不过在使用自定义命令时,{lhs}
是直接输入到命令行中的,当你按下回车时,vim 就将 {lhs}
替换为 {rhs}
再执行。所以这在形式上与下面这个映射等效:
: nnoremap :{lhs}<CR> :{rhs}<CR>
当然,由于 :command
所支持的参数与 :map
大相径庭,并不期望你真的按这方式将 自定义命令改成映射。实际上,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
联用才有意义。它要说明的是当命令的地址参数 使用 .
(当前)$
(最后)%
(所有)是参照什么集合而言的。例如定义如下命令:
: 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
命令按<Tab>
后会补 全文件名那样。-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 不会自动对参数前缀过滤,可能要求用户自己在函数中过滤。
在这两种情况,补全函数的定义都是类似的,它应该接收三个参数:
a:ArgLead
光标之前的部分参数前缀,a:CmdLine
整个命令行文本,a:CursorPos
当前光标在命令行的位置(按字节计,从1开始)。
当用户按下补全键(一般是<Tab>
),Vim 会自动将这三个参数传给自定义补全函数。 用户在这个函数实现可利用这三个参数所提供的信息(也许不一定要用到全部),返回合 适的候选补全项。
命令实现
我们将自定义名之后的 {rep}
参数部分称为命令实现。它可以是一串简单的替换文本 ,但真正有趣的是它可用一些特殊标记来表示特殊的或动态的内容。这里的特殊标记也用 尖括号 <>
括起,所支持的有意义的标记可能依赖于前面的的命令属性。
<line1>
<line2>
分别表示地址参数的两个数字(一般是第一行与最后一行)。含-range
属性的命令才能接收这两个参数。<count>
就是由-count
属性提供的数字参数。<bang>
支持-bang
属性的命令,如果使用时加了!
修饰,则在{rep}
中的<bang>
标记转换为!
字符,否则就没任何效果。<register>
或简写为<reg>
,支持-register
属性的命令,表示可选的寄存器参 数;否则也没任何效果(加上引号"<reg>"
才表示空字符串)。<lt>
代表左尖括号<
,避免尖括号的特殊意义。比如想在{rep}
中字面地呈现<bang>
这几个字符串,而不是转化为!
字符,就可用<lt>bang>
。
先举个简单的例子,我们已经知道 :map!
命令是列出某类映射。虽然上文说过应该忘 记这个命令,不过正因为它安全无害,不妨再拿来作为演示讲解。首先定义这个命令:
: command! MAP map
这个自定义命令似乎很无趣,不过用大写版的 :MAP
代替内置的 :map
。请试试在命 令中输入 :MAP
并回车执行,其结果与直接使用 :map
是一样的。试试 :MAP!
呢 ?Vim 会报错,说这个命令不支持 !
。那么重定义一下这个命令:
: command! -bang MAP map<bang>
现在,应该 :MAP
与 :MAP!
命令都可以使用了,并且分别与 :map
与 :map!
等 价。这就是 <bang>
用于命令实现参数 {rep}
中的代表意义。同时,如果你没有定 义其他以 MA
开头的命令,那么我们这个自定义命令简写成 :MA
或 :MA!
也是可 以的。
由于这个自定义没有加 -nargs
属性,默认是不能接收参数的,所以若试图用 :MAP lhs rhs
来定义映射会失败。但是,加了参数属性后,又如何在 {rep}
中使用相应的 参数呢?这就是 <args>
标记的用途,同时这有多个变种:
<args>
将用户在自定义命令后输入的参数原样替换到{rep}
中。不过若命令还有-count
或-register
属性的话,前面的属性应该由<count>
或<reg>
捕获 ,而<args>
只表示剩余的参数。<q-args>
与<args>
一样,先捕获所有参数,然后将所有参数用引号括起来作为 一个字符串表达式参数。如果没有参数,这将是一个空字符串(包含引号如""
)。<f-args>
也与<q-args>
一样,只不过将捕获的参数分隔成适用于函数调用时小括 号内的参数列表,所以是将每个参数分别引起,并用逗号分隔。这在{rep}
实现中 调用一个函数中非常有的。如果没有参数,则所调用函数的小括号内也没有任何东西, 即以空参数调用。
现在继续来改造我们的自定义命令 MAP
:
: command! -bang -nargs=* MAP map<bang> <args>
这样,:MAP
与 :MAP!
可以继续用,而且也可以用它来定义映射了,例如:
: MAP <buffer> x dd
这里,用自己的 :MAP
来定义一个映射,将 x
删除一个字符的功能改为删一行 。不过由于只为试验,所以加 <buffer>
定义成局部映射(注意区别,定义局部命令用 -buffer
语法)。
由于我们在定义 MAP
时允许它接收任意个参数 -nargs=*
。所以在 :MAP <buffer> x dd
这个使用场合下,:MAP
的所有参数 <buffer> x dd
替换在定义 MAP
时 <args>
的位置上,也就相当于执行 :map <buffer> x dd
。可以试下执行完,再按 x
是不是实现了预期效果,同时也可以用 :MAP x
或 :map x
查看下将 x
定义 成啥样的映射了。
在这个示例中,如果将定义 MAP
时的 <args>
改成 <q-args>
或 <f-args>
的 话,结果就不正确了,不能仿拟 :map
命令了。在实现复杂命令时,后两个参数变种标 记才更有用,作为函数调用的参数。不过这较为复杂,留待下一小再论。这里先探讨一下 <register>
参数的使用,假设继续为 MAP
命令添加这个属性:
: command! -bang -register -nargs=* MAP <register>map<bang> <args>
先将原来定义的 x
映射删除::unmap <buffer> x
。然后再用新的 :MAP
命令定义 x
映射,不过在参数 <buffer>
前额外加个参数 n
:
: MAP n <buffer> x dd
结果是相当于只定义了普通模式下的映射 :nmap <buffer> x dd
。你可以用 :map x
查看一下 x
的映射定义确认。并且对比一下 :MAP <buffer> X dd
不加 n
的用法 。
结论就是 <register>
不过是捕获了第一个参数,<args>
捕获其他参数。而 MAP
的定义 <register>map<bang> <args>
表明是将第一个参数直接拼在 map
之前作为 映射命令的模式前缀限定,而将其他参数用空格分开后作为 :map
命令的参数了。
这样看来,<register>
似乎很名符实呀。那么我们再尝试下将 un
作为 :MAP
的 第一个参数,看它会不会变成 :unmap
用于删除映射:
: MAP un <buffer> x
: MAP un <buffer> X
然而,这次 vim 报错了,提示 umap n <buffer> x
不是一个命令。由些可见, <register>
只捕获的第一个字母 u
,然后将剩余的东西都当成 <args>
了。因为 寄存器名都是一个字母啊。
vim 有些内置命令如 :del
:yank
:put
支持后面接一个寄存器名(比如 a
), 表示对相应的寄存器操作,相当于普通模式的命令 "ad
"ay
"ap
。自定义命令就可 用 <regsiter>
实现类似的特性,使得自定义命令能像内置命令一样使用。只不过, <register>
只能捕获参数中的第一个字母,把它当成是寄成器名,传给 {rep}
实现 部分,却无法控制 {rep}
如何处理这个字母。因为 :map
命令的模式前缀限定恰好 也只是一个字母,所以我们的 :MAP
就可以用 <register>
进行伪装了。你可以自行 尝试 :MAP i
:MAP c
等用法应该也是有效的。
上一节也提前,使用映射命令,尽量使用更安全的 :noremap
,所以再重定义命令:
: command! -bang -register -nargs=* MAP <register>noremap<bang> <args>
要测试这个命令是否有效,可定义如下映射:
: MAP n <buffer> x xx
再按 x
看看是否能正确只删除两个字符,还是会发生无尽循环故障(如果有这问题, 按 <Ctrl-c>
中断即可)。
再次提醒:这里讨论不断“优化” :MAP
命令,只为说明 :command
自定义命令的用法 与机制。正常使用 vim 下,应该没必要定义这么个命令呀。
自定义命令调用函数
除了很简单的命令,可以调用 vim 既有的内置命令(可能进行必要的包装修饰)外,大 多实用的自定义命令,都是通过调用函数来实现命令要求的功能。这并仅可以实现很复杂 的功能,也容易扩展,还使得用法简明易记,因为它一般如下的形式结构之一:
:command! {cmd} call WorkFunc(<f-args>)
:command! {cmd} call WorkFunc(<q-args>)
当使用自定义命令 {cmd}
时,它后面的命令行参数就会传入实际工作的函数 WorkFunc()
中。<f-args>
按空格分隔多个参数,然后分别引为字符串参数传入,如 果要在参数中包含空格,要用 \
转义,要传入 \
就要用两个反斜杠即 \\
。而 <q-args>
则简单粗暴,将 {cmd}
的所有参数,也就是其后跟着的所有内容当空一个 字符串参数传入。在 {cmd}
之后没有任何参数时,<q-args>
也至少传入一个空字符 串参数(WorkFunc("")
),但 <f-args>
就不传入任何参数了(WorkFunc()
)。
注意:传入 WorkFunc()
的参数必定是字符串类型,但由于 VimL 弱类型与自动转换, 如果一个参数像数字,那么在函数体内将它当作数字处理也完全没有问题。
按 <f-args>
方式调用函数更为常见。<q-args>
可能只用于比较特殊的需要,然后 要自己在函数体内解析字符串参数。另外,<f-args>
只适用于函数调用参数,用在其 他地方的意义不明显,且易出错。而 <q-args>
用于函数参数之外也可能是有意义的。 本小节暂时不讨论 <q-args>
的使用。
使用 range
首先我们需要一个工作函数。不妨复用在 2.4 节讲述函数时使用的给文本行编号的示例 函数吧,取那个支持 range
特性的版本,并改名为 NumberLine
重贴于下:
" 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
,用以调用该函数,命令名与函数不需要相同,只 是懒得另起名字,同时也想说明,命令与函数重名完全没问题,因为它们是完全不是同类 概念:
: command! -range=% NumberLine <line1>,<line2>call NumberLine()
注意到 NumberLine()
函数不支持显式参数,但可接收隐式的地址参数。而命令 :NumberLine
正好定义为支持 -range
属性,这就要将捕获的地址参数 <line1>,<line2>
放在 call
之前,由 call
把地址参数传给 NumberLine()
函 数的 a:firstline
与 :lastline
。
现在我们就可以来试用这个自定义命令了。如果直接在命令行输入 :NumberLine
回车 执行,它会对当前 buffer 的所有文本行编号。因为 -buffer
属性的默认值 %
就表 示所有行,相当于 1,$
。如果我们按行可视模式 V
选择几行,再按 :NumberLine
,命令行中实际输入的是 :'<,'>NumberLine
,它就只会对选择的行进行编号。
使用 count
接着讨论下与 -range
相似但互斥的 -count
属性。<count>
只有一个数字参数, 即可放在命令之前,也可以放在命令之后(甚至对是否有空格分隔不敏感)。很多 vim 内置命令的数字表示重复次数,不过在自定义命令中,<count>
只负责捕获传递这个数 字参数,并无法控制后续命令如何使用这个数字,就如 <register>
一样。
我们另外写个函数,用于对当前行及后面若干行进行相对编号,即当前行号是 0
,下一 行是 1
等(类似 :set relativenumber
)。
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(<count>)
同时也定义一个相应的命令。试试效果?如果直接运行 :NumberRelate
,由于 -count
的默认值是 0,所以只对当前行编号为 0。如果对选区运行 :'<,'>NumberRalate
,给命令提供了两个地址参数?但该命令只接收一个数字参数啊, vim 只会将后面那个地址参数 '>
当作数字参数 <count>
传给函数 NumberRelate()
的参数。同时也可以手动输入数字如 :3NumberRelate
或 :NumberRelate3
都会对当前行及后面3行编号。其中 NumberRelate3
的写法可能会 有歧义,如果恰好还有个自定义命名叫叫 NumberRelate3
。所以最好用 :NumberRelate 3
来调用。也正是这个原因,不建议在命令名中混入数字。
至于 Vim 为什么允许命令与数字参数粘在一起使用,主要是因为要快捷输入。很多最常 用的命令都是有单字母缩写的,而与数字参数的组合使用又极频繁。在这种情况情况下多 敲一个空格的性价比太低了(我的命令才一个字母呢),所以就把空格吃了吧。
这个示例也说明,自定义命令调用函数时,参数不一定要用 <f-args>
或 <q-args>
,混入其他任何特殊标记也是可以的,只要展开替换后符号函数调用语法即可。再比如, call WorkFunc(<bang>)
是非法的,因为展开是 call WorkFunc(!)
,但 call WorkFunc("<bang>")
是合法的,因为展开后是 call WorkFunc("!")
。而 <count>
(其实也包括 <line1>
<line2>
)可直接放入函数括号内,是因为它们会展开成一个 数字。
使用 f-args
前面两例所用的函数都不接收参数,如果函数要求参数,就用 <f-args>
传入吧。假设 更改为文本行编号的需求,在数字编号后还允许加个后缀字符,像 1.
1)
之类的, 同时可以定制分隔编号与原文本之间的空格数量。我们重写 NumberLine
函数,让它接 收两个参数:
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 <line1>,<line2>call NumberLine(<f-args>)
然后也重定义命令 :NumberLine
,为其增加 -nargs
属性,然后用 <f-args>
传给 函数调用。注意虽然可以用 -nargs=1
限定允许一个参数,但不支持 -nargs=2
限定 恰好两个参数,只能用不定数量的 -nargs=*
或 -nargs=+
。此时若只用 :NumberLine
命令执行,会报错说参数太少,加上两个命令行参数后如 :NumberLine ) 4
就能正常工作了,这表示编号样式为 1)
然后接 4 个空格。
注意到 NumberLine()
函数虽然也有个 count
参数。但与上例不同,不能用 -count
属性与 <count>
参数。首先是因为 -count
与 -range
属性只能用一个 ,不能共存。其次这里的 count
参数与大多 vim 内置命令对数字参数的解释很有些不 同,只是恰好用了这个形参名而已。因此不要滥用 <count>
参数,能直接用 <f-args>
是最简洁明了的。
如果工作函数 WorkFunc()
没有 range
属性,不处理地址范围的话,那么自定义命 令时,也不要加 -range
属性,而后面的调用函数写法也更加简单。
另外,如果工作函数是脚本作用域的函数,如 s:WorkFunc()
,则在 {rep}
部分中调 用写成 <SID>WorkFunc()
,高版本的 vim 也可以直接用 s:WorkFunc()
。不过上节的 映射命令 :map
,却只能用 <SID>
而不能用 s:
。
*微命令实例
本节内容所用的命令示例,主要为阐述概念,也许并无实用性。我在大量使用映射后,也 开始对命令有所偏爱了。为了使命令输入尽可能方便,我将常用命令也定义很短的几个大 写字母,并称之为“微命令”。实现脚本放在了 github 上,有兴趣的可以参考,传送门在 此:https://github.com/lymslive/autoplug/tree/master/autoload/microcmd
如果命令名较长,输入不便时,也可以继续使用映射来触发命令,甚至可以将最常用的命 令参数也一并包含在映射中。