如何编写一个 zsh 补全脚本
本文对 zsh 补全系统进行了简单的介绍,然后分析了一个完整的示例,该示例可以作为一个新的补全脚本的起点。剩余内容对示例补全脚本进行了简要的分析和介绍。
zsh completion system
zsh completion system(compsys
)是 zsh
的重要组成部分,当我们在 shell 中输入命令时可以通过制表符(tab
键)进行补全。我们可以在此处找到完整的文档,也可以查看源代码。这里,_main_complete
函数非常关键,由于它比较冗长,这里我会简单进行介绍一下。
补全系统需要激活。如果你使用了
oh-my-zsh
,那么它已经被激活了,否则需要在~/.zshrc
中增加以下代码来进行激活。
1 | autoload -U compinit |
当我们在 shell 中输入 foobar <tab>
时,zsh
会调用针对 foobar
的补全函数。补全函数通过调用一系列
compsys 内建函数来为 zsh 提供了补全项。
补全函数可以通过直接调用 compdef
函数来手动进行注册,如:compdef <function-name> <program>
。更为常规方法是将补全函数定义在一个独立的文件(也称
补全脚本)中。按照惯例,定义了补全函数的文件的命名通常是以下划线
"_" 为前缀,拼接目标程序的名称。当通过 compinit
初始化补全系统时,zsh 会查找 fpath
变量指定路径下的所有文件,并读取第一行。因此,我们只需要将补全脚本放在
fpath
变量所指定的路径下即可,当然,还需要确保文件的第一行包含了
compdef
命令,如:#compdef _foobar foobar
.
fpath
变量类似于PATH
变量,指定了一系列的路径。zsh 根据fpath
来查找函数。我们可以通过echo $fpath
来查找fpath
变量所指定的值。如果想要新增一个路径,只需要重新设置fpath
变量即可,如:fpath=($fpath <path-to-folder>)
补全脚本示例
假设我们有一个程序 hello
,其调用接口如下所示:
1
2
3hello -h | --help
hello quietly [--slient] <message>
hello loudly [--repeat=<number>] <message>
hello
有两个子命令 quietly
和
loudly
,两者各自有着不同的参数。理想情况下,当没有提供任何命令时,我们希望补全脚本能够补全
-h
,--help
,quietly
,loudly
。一旦输入
quietly
或
loudly
,补全脚本会根据上下文提供特定的补全项。
如下所示的补全脚本实现了上述的补全功能。本文的其余部分,我将对该补全脚本进行解释,并深入探讨其他一些内容。
1 | #compdef _hello hello |
这里有几个需要注意的地方,特别是传递给 _arguments
函数的参数,以及 local
的使用。不过,让我们先来看一下补全脚本的整体结构。
整体结构
事实上,zsh 补全脚本并没有什么特别的。它只不过是一个使用了
#compdef <function> <program>
命令来将自己注册给 program
的普通 zsh
脚本,因此我们可以自由选择合适的脚本结构,不过我发现以下的结构很有帮助。
定义一个函数,并将其命名为
_<program>
,提供默认的补全项。对于每一个子命令,为其定义一个
_<program>_<sub-command>
函数,提供子命令的补全项。以我的实践经验来看,这样写会非常直观。
_arguments
的使用
脚本通过调用 _arguments
函数来向 zsh
提供可能的补全项。当然,zsh 还提供了很多其他函数可以达到该目的。查看更多。
在上述这个例子中,关于 _arguments
函数的使用,有两个有趣的地方。字符串参数被称为
specs
,当我们第一次用时会觉得有点捉摸不透——在 zsh
中我们没有太多抽象方式,因此所有有点复杂的内容都在字符串中进行编码,这使得我们能够在更小的范围内学习这种
DSL。在上述这个例子中,specs
使用了两种形式:
- option specs:
OPT[DESCRIPTION]:MESSAGE:ACTION
- command specs:
N:MESSAGE:ACTION
。N
表示第 N 个参数。
ACTION
部分还是其自己的 DSL。在官方文档中搜索
specs: overview
可以查看有全部的内容。
-C
标志位和 spec 为 "*::arg:->args
" 的
ACTION
进行组合也很有趣。下面是文档中关于 -C
标志位的描述:
在这种格式中,
_arguments
处理参数和选项,用参数更新状态以表示处理完毕,然后返回控制流返回给调用者(函数);然后,调用者自行生成补全项。
这里面涉及到的参数包括: 1
2local context state state_descr line
typeset -A opt_args
我们可以认为是 _arguments
函数放回了多个值——事实上,_arguments
函数只是修改了全局变量,但由于使用了 typeset -A
和局部变量
local
,因此只是在当前作用域中进行了修改。typeset
的 -A
选项告诉 zsh 参数是一个关联数组。
因此,-C
标志位使得我们能够检查补全状态,并根据用户提供特定于上下文的补全项。在上述这个例子中,我们使用
switch
语句来匹配 line
变量输入的子命令,然后调用对应的补全函数来为子命令提供补全项。