跟我一塊兒寫shell補全腳本(Bash篇)

上一篇裏咱們定下了給pandoc寫補全腳本的計劃:php

  1. 支持主選項(General options)
  2. 支持子選項(Reader options/General writer options)
  3. 支持給選項提供參數值來源。好比在敲pandoc -f以後,可以補全FORMAT的內容。

支持主選項

先列出實現了第一階段目標的程序:html

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

運行程序的方式:html5

shell$ . ./pandoc # 加載上面的程序
$ pandoc -[Tab][Tab] # 試一下補全能用不

如今我來解釋下這個程序。node

bashcomplete -F _pandoc -A file pandoc

是這段代碼中最爲關鍵的一行。其實該程序起什麼名字都不重要,重要的是要有上面這一行。上面這一行指定bash在遇到pandoc這個詞時,調用_pandoc這個函數生成補全內容。(叫_pandoc其實只是出於慣例,並不必定要在前面加下劃線)。complete -F後面接一個函數,該函數將輸入三個參數:要補全的命令名、當前光標所在的詞、當前光標所在的詞的前一個詞,生成的補全結果須要存儲到COMPREPLY變量中,以待bash獲取。-A file表示默認的動做是補全文件名,也便是若是bash找不到補全的內容,就會默認以文件名進行補全。git

假設你在鍵入pandoc -o sth後,連擊兩下Tab觸發了補全,_pandoc會被執行,其中:github

  1. $1的值爲pandoc
  2. $2的值爲sth
  3. $3的值爲-o
  4. 因爲COMPREPLY爲空(只有cur-開頭時,COMPREPLY纔會被填充),因此補全的內容是當前路徑下的文件名。

你應該看到了,這裏我把$2$3都註釋掉了。其實shell

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

json

bashpre=${COMP_WORDS[COMP_CWORD-1]} # COMP_WORDS變量是一個數組,存儲着當前輸入全部的詞
cur=${COMP_WORDS[COMP_CWORD]}

是等價的。不事後者的可讀性更好罷了。segmentfault

最後解釋下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補全腳本。

相關文章
相關標籤/搜索