如何编写一个 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
变量输入的子命令,然后调用对应的补全函数来为子命令提供补全项。