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