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.