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

spacewander

上一篇里我们定下了给pandoc写补全脚本的计划:

  1. 支持主选项(General options)
  2. 支持子选项(Reader options/General writer options)
  3. 支持给选项提供参数值来源。比如在敲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] # 试一下补全能用不

现在我来解释下这个程序。

bashcomplete -F _pandoc -A file pandoc

是这段代码中最为关键的一行。其实该程序起什么名字都不重要,重要的是要有上面这一行。上面这一行指定bash在遇到pandoc这个词时,调用_pandoc这个函数生成补全内容。(叫_pandoc其实只是出于惯例,并不一定要在前面加下划线)。complete -F后面接一个函数,该函数将输入三个参数:要补全的命令名、当前光标所在的词、当前光标所在的词的前一个词,生成的补全结果需要存储到COMPREPLY变量中,以待bash获取。-A file表示默认的动作是补全文件名,也即是如果bash找不到补全的内容,就会默认以文件名进行补全。

假设你在键入pandoc -o sth后,连击两下Tab触发了补全,_pandoc会被执行,其中:

  1. $1的值为pandoc
  2. $2的值为sth
  3. $3的值为-o
  4. 由于COMPREPLY为空(只有cur-开头时,COMPREPLY才会被填充),所以补全的内容是当前路径下的文件名。

你应该看到了,这里我把$2$3都注释掉了。其实

bashpre="$3"
cur="$2"

bashpre=${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的文档,基本上就两类参数:FORMATFILE。(其它琐碎的我们就不管了,嘿嘿)

FILE好办,默认就可以补全路径嘛。那就看看FORMATFORMAT分两种,一种是读的时候支持的FORMAT,另一种是写的时候支持的FORMAT,这个把文档里面的复制一份,改改就能用了。我们把读操作支持的FORMAT叫做READ_FORMAT,相对的,写操作支持的FORMAT叫做WRITE_FORMAT

补全的来源有了,想想什么时候把它放到COMPREPLY里去。前面补全选项的时候,是通过case语句中-*来匹配的。但是这里的FORMAT参数,只在特定选项后面才有意义。所以前面一直坐冷板凳的pre变量可以上场了。

pre中存储着光标前一个词。我们就用一个case语句判断前面是否是-f-r,还是-t-w。如果符合前面两个组合之一,用compgen配合READ_FORMATWRITE_FORMAT生成补全候选词列表,一切就跟处理opts时一样。由于此时继续参与下一个判断cur的case语句已经没有意义了,这里直接让它退出函数:

bashREAD_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语句,变成这样子:

bashcase "$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补全脚本。

阅读 8.7k

spacewander
这个专栏什么都有,大部分都是关于Linux或后端开发的

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.

5.4k 声望
484 粉丝
0 条评论

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.

5.4k 声望
484 粉丝
宣传栏