内容简介:shell里面补全的影子无处不在,输入命令的时候可以有补全,敲打选项的时候可以有补全,选择文件的时候可以有补全。有些shell甚至支持通过补全来切换版本控制的分支。
如果你是一个重度 shell 用户,一定会关注所用的shell的补全功能。某款shell的补全强弱,也许就是决定你的偏好的第一要素。
shell里面补全的影子无处不在,输入命令的时候可以有补全,敲打选项的时候可以有补全,选择文件的时候可以有补全。有些shell甚至支持通过补全来切换版本控制的分支。由于shell里面可以运行的程序千差万别,shell一般不会内置针特定对某个 工具 的补全功能。与之相对的,shell提供了一些补全用的API,交由用户编写对应的补全脚本。
在这里,我想向大家介绍如何利用提供的API,来编写一个shell补全脚本。由于需要覆盖的内容较多,所以分为Bash和Zsh两篇。也许有fish用户会抱怨,fish又一次被忽略了:D。之所以只有Bash和Zsh的内容,是因为:1. 这两种shell的用户占了shell用户的绝大多数。2. 我没有用过fish,所以对这方面也不了解。希望有人能够锦上添花,写一个fish版本的补全脚本教程。
既然想要写一个shell补全脚本,那么接下来要决定待补全的对象了。这里我选择pandoc作为目标。pandoc
是文档转换器中的瑞士军刀,支持主流的各种标记语言,甚至对于PDF和MS Word也有一定程度上的支持。pandoc
支持的选项琳琅满目,如果都要实现确实很花时间。所以这里就只实现General options,Reader options,General writer options大部分的内容。不管怎么说,这将会是一个“既不至于简单到让人丧失兴趣,又不至于困难到让人丧失信心”的任务。
安装pandoc
的方式见官网上的说明,这里就不赘述了。安装完了之后,man pandoc
就能看到各个选项的说明。大体上我们需要实现以下几个目标:
- 支持主选项(General options)
- 支持子选项(Reader options/General writer options)
- 支持给选项提供参数值来源。比如在敲
pandoc -f
之后,能够补全FORMAT
的内容。
好,让我们开始给pandoc
写补全脚本吧!
支持主选项
先列出实现了第一阶段目标的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 以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 |
运行程序的方式:
1 2 |
shell$ . ./pandoc # 加载上面的程序 $ pandoc -[Tab][Tab] # 试一下补全能用不 |
现在我来解释下这个程序。
1 |
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
都注释掉了。其实
1 2 |
pre="$3" cur="$2" |
和
1 2 |
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
了,那么就需要避免重复添加。
目前的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
_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语句已经没有意义了,这里直接让它退出函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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语句,变成这样子:
1 2 3 4 5 6 |
case "$cur" in -* ) COMPREPLY=( $( compgen -W "$(complete_options)" -- $cur ) );; * ) COMPREPLY=( $( compgen -A file )) esac |
只有在没有找到对应的补全时,才会调用对路径的补全。
最终版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
_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补全脚本。
以上所述就是小编给大家介绍的《跟我一起写shell补全脚本(Bash篇)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 脚本文件里的 Hybrid Script(混合式脚本)
- 脚本错误量极致优化-定位压缩且无 SourceMap 文件的脚本错误
- 如何从PHP脚本(如批处理文件)中运行多个PHP脚本?
- 荐 python脚本如何监听终止进程行为,如何通过脚本名获取pid
- 在新的,干净的PowerShell实例中调用PowerShell脚本(在另一个脚本中)
- 云服务商封杀AI客户:因判定其Python脚本是恶意脚本
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Transcending CSS
Andy Clarke、Molly E. Holzschlag / New Riders / November 15, 2006 / $49.99
As the Web evolves to incorporate new standards and the latest browsers offer new possibilities for creative design, the art of creating Web sites is also changing. Few Web designers are experienced p......一起来看看 《Transcending CSS》 这本书的介绍吧!