在上一篇裡我們定下了給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補全指令碼。