22

Zsh 自动补全脚本入门

 3 years ago
source link: http://chuquan.me/2020/10/02/zsh-completion-tutorial/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

3EfYZbB.jpg!mobile

入门基础

zsh 如何补全一个命令

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

fpath=(~/newdir $fpath)

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

#compdef foobar

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

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

> compdef _function foobar

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

> compdef _function foobar goocar hoobar

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

> compdef '_function arg1 arg2' foobar

更多细节参见 传送门

通用 GNU 命令补全

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

> compdef _gnu_generic foobar

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

> compdef _gnu_generic foobar goocar hoodar

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

复制补全功能

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

> 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 语句检查 $stateFUNCTION 将要调用的方法名称,该方法能够生成匹配项或调用其他 Action,如: _file_message {EVAL-STRING} 将字符串作为 Shell 代码执行,可以生成匹配项。也可以调用工具方法并传入参数,如: _values_describe =ACTION 在不改变补全位置节点的情况下,向补全命令行中插入一个伪单词

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

基于 _describe 的简单补全功能

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

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

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

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 类型)。

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

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

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

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

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

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

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

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

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

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

_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] ,如下所示为一个简单示例:

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

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

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

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

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

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

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

_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 方法来创建补全方法,然后再调用该补全方法。如下所示:

_regex_arguments _cmd OTHER_ARGS..
_cmd "$@"

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

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

_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 类型。

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

_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 创建的另一个变量,用于指定当前单词之后的单词,如果当前单词之后没有其他单词,则为空白。如下所示为一个简单的例子:

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

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

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

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

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

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

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

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 文件列表的例子:

_values 'mp3 files' ~/*.mp3

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

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

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

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

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

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

使用 compadd 直接添加补全单词

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

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

compadd foo bar blah

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

compadd -X 'Some completions' foo bar blah

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

compadd -P what_ foo bar blah

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

compadd -S _todo foo bar blah

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

compadd -P _todo -q foo bar blah

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

compadd -a wordsarray

测试与调试

重载补全方法:

> 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 手册, 传送门


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK