在上一篇里我们定下了给pandoc
写补全脚本的计划:
- 支持主选项(General options)
- 支持子选项(Reader options/General writer options)
- 支持给选项提供参数值来源。比如在敲
pandoc -f
之后,能够补全FORMAT
的内容。
支持主选项
先列出实现了第一阶段目标的程序:
bash
# 以pandoc的名字保存下面的程序 _pandoc() { local pre cur opts COMPREPLY=() #pre="$3" #cur="$2" pre=${COMP_WORDS[COMP_CWORD-1]} cur=${COMP_WORDS[COMP_CWORD]} opts="-f -r -t -w -o --output -v --version -h --help" case "$cur" in -* ) COMPREPLY=( $( compgen -W "$opts" -- $cur ) ) esac } complete -F _pandoc -A file pandoc
运行程序的方式:
shell
$ . ./pandoc # 加载上面的程序 $ pandoc -[Tab][Tab] # 试一下补全能用不
现在我来解释下这个程序。
bash
complete -F _pandoc -A file pandoc
是这段代码中最为关键的一行。其实该程序起什么名字都不重要,重要的是要有上面这一行。上面这一行指定bash在遇到pandoc
这个词时,调用_pandoc
这个函数生成补全内容。(叫_pandoc
其实只是出于惯例,并不一定要在前面加下划线)。complete -F
后面接一个函数,该函数将输入三个参数:要补全的命令名、当前光标所在的词、当前光标所在的词的前一个词,生成的补全结果需要存储到COMPREPLY
变量中,以待bash获取。-A file
表示默认的动作是补全文件名,也即是如果bash找不到补全的内容,就会默认以文件名进行补全。
假设你在键入pandoc -o sth
后,连击两下Tab触发了补全,_pandoc
会被执行,其中:
-
$1
的值为pandoc
-
$2
的值为sth
-
$3
的值为-o
- 由于
COMPREPLY
为空(只有cur
以-
开头时,COMPREPLY
才会被填充),所以补全的内容是当前路径下的文件名。
你应该看到了,这里我把$2
和$3
都注释掉了。其实
bash
pre="$3" cur="$2"
和
bash
pre=${COMP_WORDS[COMP_CWORD-1]} # COMP_WORDS变量是一个数组,存储着当前输入所有的词 cur=${COMP_WORDS[COMP_CWORD]}
是等价的。不过后者的可读性更好罢了。
最后解释下COMPREPLY=( $( compgen -W "$opts" -- $cur ) )
这一行。opts
就是pandoc
的主选项列表。compgen
接受的参数和complete
差不多。这里它接受一个以IFS
分割的字符串"$opts"
作为补全的候选项(IFS
即shell里面表示分割符的变量,默认是空格或者Tab、换行)。假如没有一项跟当前光标所在的词匹配,那么它返回当前光标所在的词作为结果。(也即是不补全)
实现第一个目标用到的东西就是这么多。接下来就是第二个目标了。
在继续之前,你需要把Bash文档看一遍。若能把其中的一些选项尝试一下就更好了。
支持子选项
接下来的目标是支持Reader options/General writer options。想判断是否需要补全Reader options/General writer options,先要确认输入的词里面是否有-r
和-f
(读),以及-w
和-t
(写)。前面提到的COMP_WORDS
就派上用场了。只需要将它迭代一下,查找里面有没有我们需要确认的词。
假设我们已经确认了需要补全子选项,接下来就应该往原来的补全项中添加子选项的内容。需要补全读选项的添加读方面的选项,需要补全写选项的添加写方面的选项。既然补全选项是一个字符串,那么把要添加的字符串接到原来的opts
后面就好了。这里要注意一点,假如前面的操作里面已经把某类子选项添加到opts
了,那么就需要避免重复添加。
目前的实现代码如下:
bash
_pandoc() { local pre cur COMPREPLY=() #pre="$3" #cur="$2" pre=${COMP_WORDS[COMP_CWORD-1]} cur=${COMP_WORDS[COMP_CWORD]} complete_options() { local opts i opts="-f -r -t -w -o --output -v --version -h --help" for i in "${COMP_WORDS[@]}" do if [ "$i" == "-f" -o "$i" == "-r" ] then opts="$opts"" -R -S --filter -p" break fi done for i in "${COMP_WORDS[@]}" do if [ "$i" == "-t" -o "$i" == "-w" ] then opts="$opts"" -s --template --toc" break fi done echo "$opts" } case "$cur" in -* ) COMPREPLY=( $( compgen -W "$(complete_options)" -- $cur ) ) esac } complete -F _pandoc -A file pandoc
注意跟上一个版本相比,这里把原来的opts
变量替换成了complete_options
这个函数的输出。通过使用函数,我们可以动态地提供补全的来源。比如我们可以在函数里列出符合特定条件的文件名,作为补全的候选词。
支持给选项提供参数值来源
好了,现在是最后一个子任务。大致浏览一下pandoc
的文档,基本上就两类参数:FORMAT
和FILE
。(其它琐碎的我们就不管了,嘿嘿)
FILE
好办,默认就可以补全路径嘛。那就看看FORMAT
。FORMAT
分两种,一种是读的时候支持的FORMAT
,另一种是写的时候支持的FORMAT
,这个把文档里面的复制一份,改改就能用了。我们把读操作支持的FORMAT
叫做READ_FORMAT
,相对的,写操作支持的FORMAT
叫做WRITE_FORMAT
。
补全的来源有了,想想什么时候把它放到COMPREPLY
里去。前面补全选项的时候,是通过case语句中-*
来匹配的。但是这里的FORMAT
参数,只在特定选项后面才有意义。所以前面一直坐冷板凳的pre
变量可以上场了。
pre
中存储着光标前一个词。我们就用一个case语句判断前面是否是-f
或-r
,还是-t
或-w
。如果符合前面两个组合之一,用compgen
配合READ_FORMAT
或WRITE_FORMAT
生成补全候选词列表,一切就跟处理opts
时一样。由于此时继续参与下一个判断cur
的case语句已经没有意义了,这里直接让它退出函数:
bash
READ_FORMAT="native json markdown markdown_strict markdown_phpextra markdown_github textile rst html docbook opml mediawiki haddock latex" WRITE_FORMAT="native json plain markdown markdown_strict markdown_phpextra markdown_github rst html html5 latex beamer context man mediawiki textileorg textinfo opml docbook opendocument odt docx rtf epub epub3 fb2 asciidoc slidy slideous dzslides revealjs s5" case "$pre" in -f|-r ) COMPREPLY=( $( compgen -W "$READ_FORMAT" -- $cur ) ) return 0 ;; -t|-w ) COMPREPLY=( $( compgen -W "$WRITE_FORMAT" -- $cur ) ) return 0 esac
再. ./pandoc
一下,试试看,是不是一切都ok?
诶呀,还有个问题!这次在尝试补全FORMAT
的时候,还会把当前路径下的文件名补全出来。然而这并没有什么意义。所以在补全FORMAT
的时候,得把路径补全关掉才行。
问题在于最后一句:complete -F _pandoc -A file pandoc
。目前不管是什么情况,都会补全文件名。所以接下来得限定某些情况下才补全文件名。
第一步是移除最后一行的-A file
,下一步是修改最底下的case语句,变成这样子:
bash
case "$cur" in -* ) COMPREPLY=( $( compgen -W "$(complete_options)" -- $cur ) );; * ) COMPREPLY=( $( compgen -A file )) esac
只有在没有找到对应的补全时,才会调用对路径的补全。
最终版本:
bash
_pandoc() { local pre cur COMPREPLY=() #pre="$3" #cur="$2" pre=${COMP_WORDS[COMP_CWORD-1]} cur=${COMP_WORDS[COMP_CWORD]} READ_FORMAT="native json markdown markdown_strict markdown_phpextra markdown_github textile rst html docbook opml mediawiki haddock latex" WRITE_FORMAT="native json plain markdown markdown_strict markdown_phpextra markdown_github rst html html5 latex beamer context man mediawiki textileorg textinfo opml docbook opendocument odt docx rtf epub epub3 fb2 asciidoc slidy slideous dzslides revealjs s5" case "$pre" in -f|-r ) COMPREPLY=( $( compgen -W "$READ_FORMAT" -- $cur ) ) return 0 ;; -t|-w ) COMPREPLY=( $( compgen -W "$WRITE_FORMAT" -- $cur ) ) return 0 esac complete_options() { local opts i opts="-f -r -t -w -o --output -v --version -h --help" for i in "${COMP_WORDS[@]}" do if [ "$i" == "-f" -o "$i" == "-r" ] then opts="$opts"" -R -S --filter -p" break fi done for i in "${COMP_WORDS[@]}" do if [ "$i" == "-t" -o "$i" == "-w" ] then opts="$opts"" -s --template --toc" break fi done echo "$opts" } case "$cur" in -* ) COMPREPLY=( $( compgen -W "$(complete_options)" -- $cur) ) ;; * ) COMPREPLY=( $( compgen -A file )) esac } complete -F _pandoc pandoc
最后的问题
现在补全脚本已经写好了,不过把它放哪里呢?我们需要找到这样的地方,每次启动bash的时候都会自动加载里面的脚本,不然每次都要手动加载,那可吃不消。
.bashrc
是一个(不推荐的)选择,不过好在bash自己就提供了在启动时加载补全脚本的机制。
如果你的系统有这样的文件夹:/etc/bash_completion.d
,那么你可以把补全脚本放到那。这样每次bash启动的时候就会加载你写的文件。
如果你的系统里没有这个文件夹,你需要查看下/etc/bash_completion
这个文件。bash启动的时候,会执行. /etc/bash_completion
,你可以把你的补全脚本放在这个地方。
正如许多配置文件一样,凡是有/etc
版本的也对应的~/.
版本。有/etc/bash_completion
,自然也有~/.bash_completion
。如果你只想让自己使用这个补全脚本,或者没有root权限,可以放在~/.bash_completion
。
Bash补全脚本的内容就是这么多……请期待下一篇的Zsh补全脚本。