Zsh 自动补全脚本入门

入门基础

zsh 如何补全一个命令

命令的补全方法保存在以下划线 _ 开头的补全文件中,这些文件通常保存在 $fpath 变量所指定的路径下。当然,我们也通过可以在 ~/.zshrc 文件中增加如下一行代码,从而给 $fpath 新增一个搜索路径。

1
fpath=(~/newdir $fpath)

补全文件的第一行定义如下所示:

1
#compdef foobar

上述这行代码表示本文件定义了为 foobar 命令进行自动补全的代码。

我们还可以直接使用 compdef (比如:在 ~/.zshrc 中)命令指定将哪个方法作为自动补全的补全方法,如下所示:

1
> compdef _function foobar

除此之外,我们还可以同时为多个命令指定同一个补全方法。

1
> compdef _function foobar goocar hoobar

甚至,还可以传递参数至补全方法中。

1
> compdef '_function arg1 arg2' foobar

更多细节参见 传送门

通用 GNU 命令补全

很多 GNU 命令都有着一套标准的方式来对选项的描述进行罗列显示(当使用 --help 选项时)。对于这些命令,我们可以使用 _gnu_generic 方法自动创建补全方法,如下所示:

1
> compdef _gnu_generic foobar

当然,也可以同时为多个命令指定 _gnu_generic 补全方法。

1
> compdef _gnu_generic foobar goocar hoodar

这行代码可以直接添加到 ~/.zshrc 中。

复制补全功能

假设我们希望一个命令 cmd1 具有与 cmd2 一样的补全功能,并且 cmd2 的自动补全功能已经定义好了,那么我们可以使用如下方式进行复制。

1
> compdef cmd1=cmd2

当我们为一个命令创建了一个 alias 时,使用这种方式可以完整复制原始命令的补全功能!

如何编写补全方法

那么,如何编写属于我们自己的补全方法呢?照葫芦画瓢是一种非常好的上手方式。我们可以阅读一些别人写的补全方法。在本机的文件系统下,我们可以通过 $fpath 环境变量去搜索已有的补全功能文件,比如:/usr/local/share/zsh/site-functions

我们可以看到有一个 _arguments 方法在这些补全功能文件中被大量使用。_arguments 是一个可以快速实现简单补全功能的工具方法。本质上,_arguments 方法是对内置方法 compadd 进行了封装。内置方法 compadd 是将补全单词添加至命令行,并控制补全行为的关键方法。不过,在大多数情况下,我们都不需要使用 compadd 方法,因为 zsh 提供了大量类似 _arguments_describe 这样的工具方法,简单易用。

对于非常简单的补全功能,_describe 方法甚至都够用了。

工具方法

本文仅罗列一部分常用的工具方法。完整的工具方法及其使用说明,参见 传送门

本节我们将简单介绍一部分常用的工具方法,下一节我们将介绍如何使用这些工具方法。

用于整体补全功能的工具方法

方法名称 功能描述
_alternative 用于生成补全候选列表
_arguments 用于指定如何对一个命令进行选项补全和参数补全,采用 UNIX 风格选项
_describe 用来创建仅由单词、描述信息组成的简单补全(不包含 Action)。比 _arguments 方法更简单
_gnu_generic 用来为能够响应 --help 选项的命令进行补全
_regex_arguments 创建一个方法,能够使用正则表达式匹配命令行参数,从而执行 Action 或补全方法

为单个单词进行复杂补全的方法

方法名称 功能描述
_values 用于为任意关键词(值)及其参数进行补全,或者此类组合的逗号分隔列表
_combination 用于补全值的组合,比如:hostnameusername 的组合
_multi_parts 用于对单词的多个部分进行补全,其中每个部分使用某个字符分隔,例如对部分文件路径进行补全:/u/i/sy 补全为 /usr/include/sys
_sep_parts 类似 _multi_parts,不过它允许不同的补全部分使用不同的分隔符
_sequence 用于封装另一个补全方法,从而对另一个补全方法生成的匹配项进行补全

为指定类型的对象进行补全的方法

方法名称 功能描述
_path_files 用于补全文件路径。有多个选项可以控制其行为
_files 使用除了 -g-/ 以外的所有选项调用 _path_files 方法。这些选项取决于文件模式样式设置
_net_interfaces 用于补全网络接口名称
_users 用于补全用户名
_groups 用于补全用户组名称
_options 用于补全 shell 选项的名称
_parameters 用于补全 shell 参数、变量的名称(可以限定为与模式匹配的名称)

处理已缓存补全逻辑的方法

如果我们有大量的补全,可以将它们保存在缓存文件中,以便快速加载。

方法名称 功能描述
_cache_invalid 设置指定的缓存标识符对应的补全逻辑是否需要重建
_retrieve_cache 从缓存文件中检索补全逻辑信息
_store_cache 将指定的缓存标识符对应的补全逻辑信息存储在缓存文件中

其他方法

方法名称 功能描述
_message 用于在无法补全时显示帮助信息
_regex_words 用于为 _regex_arguments 命令生成调用参数。比手动编写参数更简单
_guard 可以在 _arguments 的调用参数的 ACTION 部分中使用,也可以在类似的方法用来检查单词是否已补全

Action

类似 _arguments_regex_arguments_alternative_value 这样的工具方法,它都都可以有一系列的调用参数。不过这些参数都遵循了一些预定义格式(或规范)的字符串,我们称之为 参数格式,每个参数格式的最后一个参数/选项是 Action 部分。Action 表示如何补全相应的参数。Action 可以有多种类型:

方法名称 功能描述
() 参数是必须的,但是没有匹配项。相当于空白占位符
(ITEM1 ITEM2) 可能的匹配列表
((ITEM1\:'DESC1' ITEM2\:'DESC2')) 可能的匹配列表,附带描述信息。注意:Action 中的引号不能和其所在的参数格式字符串的引号相同
->STRING $state 设置为 STRING 并继续后续逻辑(当工具方法被调用后,可以使用 case 语句检查 $state
FUNCTION 将要调用的方法名称,该方法能够生成匹配项或调用其他 Action,如:_file_message
{EVAL-STRING} 将字符串作为 Shell 代码执行,可以生成匹配项。也可以调用工具方法并传入参数,如:_values_describe
=ACTION 在不改变补全位置节点的情况下,向补全命令行中插入一个伪单词

注意:并不是所有的工具方法都可以使用所有类型的 Action。比如:_regex_arguments_alternative 方法不能使用 ->STRING 类型。

基于 _describe 的简单补全功能

_describe 方法可用于 选项/参数的顺序和位置并不重要 的简单补全功能。我们只需要创建一个数组参数来持有选项及其描述信息,然后将这个数组参数作为一个参数传递给 _describe。下面的例子中,创建了两个补全候选项 cd,并提供了描述信息(注意:代码应该定义在名为 _cmd 文件中,并位于 $fpath 的路劲下):

1
2
3
4
#compdef cmd
local a -subcmds
subcmds=('c:description for c command' 'd:description for d command')
_describe 'command' subcmds

除此之外,我们还可以向 _describe 方法传入多个不同的列表,并使用 -- 进行分隔。

1
2
3
4
local -a subcmds topics
subcmds=('c:description for c command' 'd:description for d command')
topics=('e:description for e help topic' 'f:description for f help topic')
_describe 'command' subcmds -- topics

如果两个候选项的描述信息相同,_describe 会将两者合并到同一行,并确保所有候选项的描述信息列对齐。_describe 方法可以用在 _alternative_arguments_regex_arguments 的参数格式中的 ACTION 部分。在这种情况下,我们必须将其与参数放在一起,如:'TAG:DESCRIPTION:{_describe 'value' options'}

基于 _alternative 的补全功能

_describe 方法类似,_alternative 方法可用于 选项/参数的顺序和位置并不重要 的简单补全功能。与 _describe 方法不同,_alternative 方法并不使用固定的匹配项,而是调用其他方法来生成候选项。此外,_alternative 允许混合使用不同类型的补全候选项。

_alternative 方法的参数格式是 'TAG:DESCRIPTION:ACTION'。其中,TAG 指定补全匹配项的类型;DESCRIPTION 表示补全候选项列表的描述信息;ACTION 则是前文所描述的一种 Action 类型(_alternative 不支持 ->STRING=ACTION 类型)。

1
_alternative 'arguments:custom arg:(a b c)' 'files:filename:_files'

_alternative 方法的第一个参数中加入了三个候选项 abc;第二个参数调用了 _files 方法来对文件路径进行补全。

我们可以将 _alternative 方法参数分成多行,使用 \ 进行换行。如下所示,为每个自定义参数添加了描述信息:

1
2
3
_alternative \
'args:custom arg:((a\:"description a" b\:"description b" c\:"description c"))' \
'files:filename:_files'

如果我们想把参数传递给 _files 方法,可以像下面一下直接包含进来:

1
2
3
4
_alternative \
'args:custom arg:((a\:"description a" b\:"description b" c\:"description c"))'\
'files:filename:_files -/'

如果想使用参数扩展来创建补全列表,我们需要使用双引号来引用 _alternative 方法的调用参数。如下所示为一个简单示例:

1
2
3
_alternative \
"dirs:user directory:($userdirs)" \
"pids:process ID:($(ps -A | awk '{print $1}'))"

在这种情况下,第一个调用参数添加了存储在 $userdirs 变量中的单词,第二个调用参数则执行 ps -A | awk '{print $1}' 来获取一系列的 PID 以作为补全候选项。在实际开发中,我们可以直接使用已有的 _pids 方法。

我们还可以在 ACTION 中使用其他的工具方法(如:_values)来生成实现更加复杂的补全功能,如下所示为一个简单示例:

1
2
3
_alternative \
"directories:user directory:($userdirs)" \
'options:comma-separated opt: _values -s , letter a b c'

上述例子会对 $userdirs 中的项进行补全,同时也会对包含 abc 的逗号分隔列表进行补全。注意:在 _values 方法前需要有一个初始的空格。

_describe 方法一样,_alternative 方法自身也可以作为 _arguments_regex_arguments 等方法的调用参数中的 ACTION 部分。

基于 _arguments 的补全功能

通过调用 _arguments 方法,我们就能够实现复杂的补全功能。_arguments 方法能够处理带有各种选项及常规参数的命令。与 _alternative_arguments 方法相似,它以格式化的字符串作为其调用参数。这些格式化参数能够指定选项及其对应的选项参数(如:-f filename)、命令参数。

_arguments 方法的调用参数的基本参数格式为 -OPT[DESCRIPTION],如下所示为一个简单示例:

1
_arguments '-s[sort output]' '--l[long output]' '-l[long output]'

稍微复杂一点的 _arguments 方法的参数格式可以是 -OPT[DESCRIPTION]:MESSAGE:ACTION。其中,MESSAGEACTION 的含义与上述 _alternative 中描述的一样。如下所示为一个简单示例:

1
_arguments '-f[input file]:filename:_files' 

_arguments 方法的参数格式还可以是 N:MESSAGE:ACTION。其中,N 表示第 N 个命令参数,MESSAGEACTION 还是和之前一样。如果省略 N,则仅表示下一个命令参数(在已指定的参数之后)。如果在 N 的前面或后面加上冒号 :,则表示参数是可选的。如下为一个简单的示例:

1
_arguments '-s[sort output]' '1:first arg:_net_interfaces' '::optional arg:_files' ':next arg:(a b c)'

上面这个例子中,第一个参数是网络接口名,下一个可选参数是一个文件名,最后一个参数是 abc 其中之一,s 选项可以在任意位置进行补全。

_arguments 方法支持上述所有类型的 ACTION。这样的话,我们可以使用 case 分支来调用不同的 Action。

1
2
3
4
5
6
7
8
9
10
11
_arguments '-m[music file]:filename:->files' '-f[flags]:flag:->flags'
case "$state" in
files)
local -a music_files
music_files=( Music/**/*.{mp3,wav,flac,ogg} )
_multi_parts / music_files
;;
flags)
_values -s , 'flags' a b c d e
;;
esac

在这个例子中,music file 的路径调用 _multi_parts 方法来进行目录补全;flags 则可以调用 _value 方法,以逗号 , 分隔列表的形式进行补全。

本节,我们简单地介绍了 _arguments 方法的基本使用方法,当然,我们还可以指定互斥选项、重复选项/参数、以 + 开头而非 - 开头的选项等等。更多的使用方法,可以参见官方教程。此外,还可以参考本文末尾提到的教程。

基于 _regex_arguments_regex_words 的补全功能

如果我们有一个复杂的命令行格式,它有多种可能的参数序列,那么 _regex_arguments 方法可能就是一个比较适合的补全方法。

_regex_arguments 方法会创建一个补全方法,补全方法的名字由第一个调用参数指定。因此,我们需要先调用 _regex_arguments 方法来创建补全方法,然后再调用该补全方法。如下所示:

1
2
_regex_arguments _cmd OTHER_ARGS..
_cmd "$@"

上述例子中, OTHER_ARGS 用于匹配并补全命令行中的单词,其可以是多个调用参数列表。这些调用参数列表可以用 '|' 进行二选一。我们还可以使用括号来指定选择的层级,不过,括号必须使用斜杠 \ 或引号 '' 进行转义, 如:\(\)'('')'

如下所示为一个简单示例:

1
2
_regex_arguments _cmd SEQ1 '|' SEQ2 \( SEQ2a '|' SEQ2b \)
_cmd "$@"

上述例子中,指定了一个与 SEQ1SEQ2(后跟 SEQ2aSEQ2b)相匹配的命令行。这本质上就是使用正则表达式描述命令行的参数。

每个调用参数列表之前必须包含一个 /PATTERN/ 部分,后跟一个可选的 :TAG:DESCRIPTION:ACTION 部分。

每个 PATTERN 是一个正则表达式,用于匹配命令行上的一个单词。这些模式(pattern)会被顺序处理,直到遇到一个不匹配的模式,此时将执行对应的 Action 以得到该单词的补全项。

注意,必须要有一个模式来匹配命令自身。后面,我们将进一步介绍 PATTERN

_regex_arguments 方法参数中 :TAG:DESCRIPTION:ACTION 部分的含义与 _alternative 方法参数类似,不同的地方在于 _regex_arguments 中开头多了一个 :,此外,它还允许调用所有的 Action 类型。

如下所示为一个简单示例:

1
2
3
4
_regex_arguments _cmd /$'[^\0]##\0'/ \( /$'word1(a|b|c)\0'/ ':word:first word:(word1a word1b word1c)' '|'\
/$'word11(a|b|c)\0'/ ':word:first word:(word11a word11b word11c)' \( /$'word2(a|b|c)\0'/ ':word:second word:(word2a word2b word2c)'\
'|' /$'word22(a|b|c)\0'/ ':word:second word:(word22a word22b word22c)' \) \)
_cmd "$@"

在这个例子中,第一个单词可以是 word1(后跟 abc 中的任意一个)或 word11(后跟 abc 中的任意一个);如果第一个单词包含 11,那么第二个单词可以是 word2(后跟 abc 中的任意一个)或文件名。

这个例子看起来太复杂了,有一种简单的方式是使用 _regex_words 方法为 _regex_arguments 创建方法调用参数。

模式

我们可能注意到上面这个示例中, /PATTERN/ 看上去并不像正常的正则表达式。这里的字符串参数采用 $'foo\0' 的形式,通过这种形式将字符串中的 \0 解释为 null 字符,从而对参数内容进行单词分隔。如果我们没有在模式的末尾添加 \0,则可能无法匹配下一个单词。如果要在模式中使用变量的内容,我们可以给它添加双引号,以便对其进行扩展,然后再在后面添加一个含有 null 字符的字符串,如:"$somevar"$'\0'

模式的正则表达式语法似乎与普通的正则表达式有些不同。虽然没有找到相应的说明文档,但是能够总结出以下特殊字符的用法:

字符 使用描述
* 通配符-任意数量的字符
? 通配符-单个字符
# 零个或多个前一个字符(类似于正则表达式中的 *
## 一个或多个前一个字符(类似于正则表达式中的 +

_regex_words

_regex_words 方法使得我们可以更容易地为 _regex_arguments 方法创建调用参数。_regex_words 方法的结果可以存储在一个变量中,从而能够作为 _regex_arguments 方法的调用参数。

使用 _regex_words 创建 _regex_arguments 的调用参数时,需要向其提供一个标签,其次是描述信息,接着是描述各个单词的参数格式。参数格式为 WORD:DESCRIPTION:SPEC,其中 WORD 表示要补全的单词,DESCRIPTION 表示描述信息,SPEC 可以是 _regex_words 创建的另一个变量,用于指定当前单词之后的单词,如果当前单词之后没有其他单词,则为空白。如下所示为一个简单的例子:

1
_regex_words firstword 'The first word' 'word1a:a word:' 'word1b:b word:' 'word1c:c word'

_regex_words 方法执行的结果会被存储在 $reply 数组中,因此我们需要在 $reply 的值改变之前将其保存到其他数组中,如下所示:

1
2
3
local -a firstword
_regex_words word 'The first word' 'word1a:a word:' 'word1b:b word:' 'word1c:c word'
firstword="$reply[@]"

基于此,我们可以这样调用 _regex_arguments 方法:

1
2
_regex_arguments _cmd /$'[^\0]##\0'/ "${firstword[@]}"
_cmd "$@"

这里我们来为初始命令添加了一个其他的模式。

下面是一个更加复杂的例子,我们为命令行中不同的单词调用 _regex_words 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
local -a firstword firstword2 secondword secondword2
_regex_words word1 'The second word' 'woo:tang clan' 'hoo:not me'
secondword=("$reply[@]")
_regex_words word2 'Another second word' 'yee:thou' 'haa:very funny!'
secondword2=("$reply[@]")
_regex_words commands 'The first word' 'foo:do foo' 'man:yeah man' 'chu:at chu'
firstword=("$reply[@]")
_regex_words word4 'Another first word' 'boo:scare somebody:$secondword' 'ga:baby noise:$secondword'\
'loo:go to the toilet:$secondword2'
firstword2=("$reply[@]")

_regex_arguments _hello /$'[^\0]##\0'/ \( "${firstword[@]}" '|' "${firstword2[@]}" \)"
_hello "$@"

在上面这个例子中,第一个单词可以是 foomanchuboogaloo 其中之一。如果第一个单词是 booga,那么第二个单词可以是 woohoo。如果第一个单词是 loo 那么第二个单词可以是 yeehaa,其他情况下没有第二个单词。

我们可以看一下 _ip 方法的具体实现,其内部很好地运用了 _regex_words 方法。

基于 _values_sep_parts_multi_parts 的复杂补全功能

_values_sep_parts_multi_parts 方法可以单独使用,也可以在 _alternative_arguments_regex_arguments 方法的调用参数的 ACTION 中使用。

如下所示是使用空格分隔 mp3 文件列表的例子:

1
_values 'mp3 files' ~/*.mp3

如下所示是使用逗号分隔 session id 列表的例子:

1
_values -s , 'session id' "${(uonzf)$(awk '{print $1}')}"

如下所示是补全 foo@news:woofoo@news:laabar@news:woo 等的例子:

1
_sep_parts '(foo bar)' @ '(news ftp)' : '(woo laa)'

如下所示是一次补全 MAC 地址的例子:

1
_multi_parts : '(00:11:22:33:44:55 00:23:34:45:56:67 00:23:45:56:67:78)'

使用 compadd 直接添加补全单词

为了更准确地进行控制,我们可以使用内置的 compadd 方法直接添加补全单词。这个方法有很多不同的参数,用于控制如何显示补全以及单词补全后如何更改命令行上的文本。更多信息可以查看官方文档 传送门。这里只给出一些简单的示例。

在可能的补全列表中添加单词:

1
compadd foo bar blah

和上面例子一样,区别在于会额外显示解释信息:

1
compadd -X 'Some completions' foo bar blah

和第一个例子一样,区别在于会在自动补全的单词之前插入前缀 what_

1
compadd -P what_ foo bar blah

和第一个例子一样,区别在于会在自动补全的单词之后插入后缀 _todo

1
compadd -S _todo foo bar blah

和第一个例子一样,区别在于当在后缀之后输入空白字符时,会自动删除 _todo 后缀:

1
compadd -P _todo -q foo bar blah

$wordsarray 数组中的单词添加至可能的补全列表中:

1
compadd -a wordsarray

测试与调试

重载补全方法:

1
2
> unfunction _func
> autoload -U _func

下面这些方法能够提供一些有用的信息。

方法 描述
_complete_help 当对当前光标位置进行补全时,显示相关的上下文名称、标签以及补全方法
_complete_debug 执行普通的补全,会在一个临时文件重保存补全系统所执行的 shell 命令 trace 信息

注意事项

请记住,在包含补全方法的文件的开头应该包含 #compdef 这一行。

请注意对 _arguments_regex_arguments 方法的参数格式使用正确的引号类型:如果在参数格式中需要扩展参数,请使用双引号,否则请使用单引号。

检查 _arguments_alternative_regex_arguments 的参数格式中的冒号 : 数量和位置是否正确。

在使用 _regex_arguments 方法是,请记住要包含一个初始模式以匹配命令自身。

请记住在 _regex_arguments 的任何 PATTERN 参数的末尾加上一个空字符 $'\0'

提示

有时候我们会遇到这样的情况:子命令后面只能有一个选项,而在子命令后输入 tab 键时,zsh 会自动补全此选项。如果我们希望它在补全之前列出其描述信息,则可以向 ACTION 中添加另一个空选项(如:\:),例如:TAR:DESCRIPTION:((opt1\:"description for opt1" \:))。注意,这仅适用于其支持 ACTION 的工具方法(如:_arguments_regex_arguments)。

其他资料

一个关于 _arguments 方法的基本教程,传送门

一个关于 _arguments 方法的高级教程,传送门

zshcompsys 手册,传送门