bash和zsh的自动补全

最近在给一个开源项目贡献代码,想要给他加上相应的自动补全功能

BGmi起初只是个 cli 程序,前端单纯的展示已经下载的剧集,后来给前端加了一些订阅功能,但是 cli 的使用频率还是很高,cli 没有自动补全功能总是说不过去,所以就花了一些时间加上了这个功能.

分析一下需求

BGmi 的命令都是同样的结构,bgmi action1 --opt1 arg1 --opt2 arg2,那么我们需要补全的就是所有的 action 和每个 action 相应的选项了.在此之前,是直接add_parseradd_argument相应的 action 和选项.这样是没法进行下一步的,所以首先花了一些时间,所以首先把所有的action和相应的opts存在了一个变量中

actions_and_arguments = [
{
'action': ACTION_ADD,
'help': 'Subscribe bangumi.',
'arguments': [
{'dest': 'name',
'kwargs': dict(metavar='name', type=unicode_, nargs='+',
help='Bangumi name'), },
{'dest': '--episode',
'kwargs': dict(metavar='episode',
help='Add bangumi and mark it as specified episode.',
type=int), },
]
},
{
'action': ACTION_DELETE,
'help': 'Unsubscribe bangumi.',
'arguments': [
{'dest': '--name',
'kwargs': dict(metavar='name', nargs='+', type=unicode_,
help='Bangumi name to unsubscribe.'), },
{'dest': '--batch',
'kwargs': dict(action='store_true', help='No confirmation.'), },
]
}]

一个list中储存了多个dict,每个dict对应一个action,每个action的选项存在arguments字段中.这里的命名可能有些混乱,写的时候没太注意.

无论是在 bash 还是 zsh 中,要让 bgmi 有自动补全的功能,都需要一个相应的函数来给 bgmi 命令提供自动补全功能,也就是说,我们是要把上面的一个dict转换成一个字符串. 这种事情,当然就该模板出马了.因为 BGmi 的 api 是由 tornado 提供的,所以就直接用tornado.template了.

先从 Bash 的自动补全开始

参考的跟我一起写 shell 补全脚本(Bash 篇)

最终的模板_bgmi_completion_bash.sh

先说下 bash 的语法

基本上会用到的数据类型就是字符串和数字了,字符串两边需要加单引号的双引号,或者是反引号.而单引号和双引号还有一些不同.双引号允许转义,而单引号不允许

shell 的语法跟编程语言的语法有一些不同,感觉 shell 的语法在故意混淆字符串和命令.语句中的一个单词又可以做为命令又可以做为字符串.所以为了避免歧义,需要加上单引号或者双引号.而单引号和双引号又有一些不同.单引号是没有转义的,双引号是有转义的.比如说

export var=1
echo "$var" # 1
echo "$var 233" # 1 233
echo '$var' # $var
echo "`ls`" # 输出ls命令的输出

在双引号字符串中,以$开头的会被替换成对应的变量,用反引号包起来的内容会视为命令,运行之后把输出替换为字符串的一部分

然后是具体的代码

bash 用来提供自动补全的命令是complete

complete --help
complete: complete [-abcdefgjksuv] [-pr] [-DE] [-o option] [-A action]
[-G globpat] [-W wordlist] [-F function] [-C command] [-X filterpat]
[-P prefix] [-S suffix] [name ...]
Specify how arguments are to be completed by Readline.

For each NAME, specify how arguments are to be completed. If no options
are supplied, existing completion specifications are printed in a way that
allows them to be reused as input.

Options:
-p print existing completion specifications in a reusable format
-r remove a completion specification for each NAME, or, if no
NAMEs are supplied, all completion specifications
-D apply the completions and actions as the default for commands
without any specific completion defined
-E apply the completions and actions to "empty" commands --
completion attempted on a blank line

When completion is attempted, the actions are applied in the order the
uppercase-letter options are listed above. The -D option takes
precedence over -E.

Exit Status:
Returns success unless an invalid option is supplied or an error occurs.

本来complete是支持用另一个命令来进行自动补全的,但是试了试实在是太慢了,所以还是生成了一个 bash 函数.

因为我是编写了一个_bgmi函数来进行bgmi命令的自动补全,所以此处就应该complete -F _bgmi bgmi

然后就是_bgmi函数本体了. config 太多,只贴了一部分.

_bgmi() {
local pre cur action
local actions bangumi config
actions="add delete update cal config filter fetch download list mark search source complete"
config="BANGUMI_MOE_URL SAVE_PATH DOWNLOAD_DELEGATE MAX_PAGE TMP_PATH DANMAKU_API_URL"
COMPREPLY=()

pre=${COMP_WORDS[COMP_CWORD-1]}
cur=${COMP_WORDS[COMP_CWORD]}
if [ $COMP_CWORD -eq 1 ]; then
COMPREPLY=( $( compgen -W "$actions" -- $cur ) )
else
action=${COMP_WORDS[1]}

case "$action" in

update )
local opts
opts="--download -d --not-ignore"
COMPREPLY=( $( compgen -W "$opts" -- $cur ) )
return 0
;;

filter )
local opts
opts="--subtitle --include --exclude --regex"
COMPREPLY=( $( compgen -W "$opts" -- $cur ) )
return 0
;;

config )
COMPREPLY=( $( compgen -W "$config" -- $cur ) )
return 0
;;

cal )
local opts
opts="--today -f --force-update --download-cover --no-save"
COMPREPLY=( $( compgen -W "$opts" -- $cur ) )
return 0
;;

source )
local source
source="bangumi_moe mikan_project dmhy"
COMPREPLY=( $( compgen -W "$source" -- $cur ) )
return 0
;;

search )
local opts
opts="--count --regex-filter --download --dupe"
COMPREPLY=( $( compgen -W "$opts" -- $cur ) )
return 0
;;

download )
local opts
opts="--list --mark --status"
COMPREPLY=( $( compgen -W "$opts" -- $cur ) )
return 0
;;

esac

fi

}
complete -F _bgmi bgmi

# run `eval "$(bgmi complete)"` in your bash

COMP_WORDS是保存了当前命令行所有输入内容的一个数组,COMP_CWORD是当前正在输入的词的索引.
所以,pre=${COMP_WORDS[COMP_CWORD-1]}是当前正在输入的前一个词,cur=${COMP_WORDS[COMP_CWORD]}是正在输入的词.

(这里用${}包起来跟直接使用$var没有什么区别,只是其他语言的变量前不用加$,用{}包起来个人看起来习惯一点.)

因为bgmi的命令都是bgmi action args这样的形式,所以先判断COMP_WORDS的大小,如果等于 1,说明还没输出对应的 action,需要补全 action. 如果大于 1, 说明已经输入过了 action,只需要补全对应的选项.

在 bash 中,生成对应补全选项的命令是compgen

$ compgen --help
compgen: compgen [-abcdefgjksuv] [-o option] [-A action]
[-G globpat] [-W wordlist] [-F function] [-C command]
[-X filterpat] [-P prefix] [-S suffix] [word]

Display possible completions depending on the options.

Intended to be used from within a shell function generating possible
completions. If the optional WORD argument is supplied, matches against
WORD are generated.

Exit Status:
Returns success unless an invalid option is supplied or an error occurs.

我在这里只用到了compgen -W 根据一个wordlist来生成对应的补全.

接下来只需要把对应的内容根据模板的要求进行修改就可以了.

Zsh 的自动补全

参照的这篇文章https://github.com/spacewander/blogWithMarkdown/issues/32

先放个结果...

_bgmi(){

if [[ ${#words} -le 2 ]]
then
_alternative \
'action:action options:((add\:"Subscribe bangumi." delete\:"Unsubscribe bangumi." list\:"List subscribed bangumi." filter\:"Set bangumi fetch filter." update\:"Update bangumi calendar and subscribed bangumi episode." cal\:"Print bangumi calendar." config\:"Config BGmi." mark\:"Mark bangumi episode." download\:"Download manager." fetch\:"Fetch bangumi." search\:"Search torrents from data source by keyword" source\:"Select date source bangumi_moe or mikan_project" install\:"Install BGmi front / admin / download delegate" upgrade\:"Check update." history\:"List your history of following bangumi" ))'
fi

if [[ ${words[(i)cal]} -le ${#words} ]]
then
_alternative \
'cal:cal options:((--today\:"Show bangumi calendar for today." -f\:"Get the newest bangumi calendar from bangumi.moe." --force-update\:"Get the newest bangumi calendar from bangumi.moe." --download-cover\:"Download the cover to local" --no-save\:"Do not save the bangumi data when force update." ))'
fi

}

compdef _bgmi bgmi

#usage: eval "$(bgmi complete)"
#if you are using windows, cygwin or babun, try `eval "$(bgmi complete|dos2unix)"`

zsh 跟 bash 有几点不同

bash 中的 complete 在 zsh 中是 compdef

zsh 中用来保存目前所有输入的词组是words

zsh 中要生成对应的提醒的话用的是_alternative等命令,而不是把结果赋值给某个变量.

其中有这样一个用法

${words[(i)cal]} 这类似于 js 中的words.indexOf('cal')#a就相当于a.length

因为_alternative的功能是最全的,所以我就只用了_alternative这一个命令
cal:cal options:(( -f\:"Get the newest bangumi calendar from bangumi.moe." --force-update\:"Get the newest bangumi calendar from bangumi.moe." ))

如果有两个选项是同样的意思,直接重复输出就可以了,zsh 会自动把他们合并成一行,就像这样 其中 --force-update-f的帮助信息在我们输入的时候就是相同的.

ubuntu@VM-189-243-ubuntu ~ $ bgmi cal -
--download-cover -- Download the cover to local
--force-update -f -- Get the newest bangumi calendar from bangumi.moe.
--no-save -- Do not save the bangumi data when force update.
--today -- Show bangumi calendar for today.