絕大部分日常使用Linux和OS X的程式設計師都會選擇zsh作為自己的shell環境,畢竟對比於bash,zsh的便利性/可玩性要勝出很多,同時它又能相容bash大多數的語法。不過相對而言,zsh補全指令碼要比bash補全指令碼要難寫。zsh提供了非常多的補全的API,而且這些API功能有不少重疊的地方,掌握起來並不容易。不像bash,你只需記住三個API(compgen
,complete
,compopt
)就能實現整個補全指令碼。
這篇的任務跟上一篇的一樣,需要實現一個針對pandoc
的補全指令碼,囊括下面三個目標:
- 支援主選項(General options)
- 支援子選項(Reader options/General writer options)
- 支援給選項提供引數值來源
何處安放指令碼
在開始之前,需要說明下放置zsh指令碼的地方,這樣我們才能讓接下來寫的補全指令碼發揮效力。
zsh在啟動時會載入$fpath
路徑下的指令碼檔案。試試echo $fpath
來看看這個變數的值。接下來我們可以把補全指令碼放到$fpath
的路徑下,或者建立一個新的在$fpath
路徑中的目錄:
mkdir ~/.fpath
- 在
~/.zshrc
中新增fpath=($HOME/.fpath $fpath)
- 重啟zsh
當我們把自己寫的補全指令碼放好後,每次zsh一啟動,就會載入它。不過總不能每次修改完指令碼後,都重啟一次zsh吧。如果只是單純更新補全指令碼,可以執行unfunction _pandoc && autoload -U _pandoc
,zsh就會重新載入補全指令碼了。(其中_pandoc
是補全指令碼的名字)
支援主選項
還是跟上一篇一樣,先解釋一個實現第一個目標的程式,帶各位入門:
1 2 3 4 5 6 7 8 9 10 |
zsh#compdef pandoc # 把它命名為_pandoc,儲存在$fpath路徑下 _arguments {-f,-r}'[-f FORMAT, -r FORMAT, Specify input format]' {-t,-w}'[-t FORMAT, -w FORMAT, Specify output format]' {-o,--output}'[-o FILE, --output=FILE, Write output to FILE instead of stdout]' {-h,--help}'[Show usage message]' {-v,--version}'[Print version]' '*:files:_files' |
就像bash的complete
,zsh也有一個相對的表示補全的API,就是compdef
。zsh補全指令碼以#compdef tools
開頭,表示該檔案是針對tools
的補全指令碼。當然你也可以像bash一樣,直接compdef _function tools
來指定tools
的補全函式。
zsh補全API的第一梯隊是_alternative
、_arguments
、_describe
、_gnu_generic
、_regex_arguments
。它們直接提供補全的來源。這些API的概述見https://github.com/zsh-users/zsh-completions/blob/master/zsh-completio…。由於_describe
能做的_arguments
也能做,_gnu_generic
是為GNU擴充的命令引數準備的,_regex_arguments
就是正則匹配版的_arguments
,所以只要記住_arguments
和_alternative
就夠用了。
_arguments
接受一連串的選項字串,每個字串代表一個選項。另外你還可以通過一些選項指定補全上的細節。舉-s
為例:假設你的工具支援-a -b
兩個選項,也支援-ab
的方式來同時指定兩個選項。如果沒給_arguments
提供-s
的選項,那麼zsh是不會補全出-ab
,因為並不存在選項-ab
。而提供了-s
後,_arguments
才允許你在已經輸入-a
的情況下,補全出-ab
。
選項字串的格式是這樣的:-x[description]:message:action
。你也可以寫做{-x,-y}[description]:message:action
形式,表示-x
和-y
是等價的寫法。
-x
是選項的名字[description]
是該選項的描述,可選message
這一項我也不知道是什麼意義……不過它是可選的,除非你需要指定actionaction
用於生成複雜的補全。在這裡你可以使用許多補全語法。一個常見的例子是使用輔助函式,比如_files
表示補全當前路徑下的檔名。詳見:
最後一行'*:files:_files'
表示,如果找不到匹配的候選詞,就補全檔名。
到目前為止,實現第一階段目標的指令碼所需的知識點已經講解完畢。
_arguments
有一個限制,它要求選項的名字元合某些特殊格式,比如以-
、+
、=
等字元開頭(所以才叫_arguments
嘛)。如果你的工具接受add
、remove
之類的子命令,就需要用到_alternative
。
_alternative
支援的選項字串格式跟_arguments
很像,比如
1 2 |
_arguments {-t,-w}'[-t FORMAT, -w FORMAT, Specify output format]' |
等價於
1 2 |
_alternative 'writer:writer options:((-t:"-t FORMAT, -w FORMAT, Specify output format" -w:"-t FORMAT, -w FORMAT, Specify output format"))' |
支援子選項
所謂的支援子選項,就是在某些選項存在的情況下,增加多一些選項。所以,我們所要做的,就是檢查當前輸入的命令列引數中是否存在某些引數,如果存在,增加新的選項。這一步可以分解成兩個步驟,第一個是檢查某些引數是否存在,第二個是增加新的選項。
之前寫bash補全指令碼的時候,是通過遍歷某個儲存有當前輸入的常量陣列,來檢查某些引數是否存在。在網上搜尋一番後,我發現zsh也有同樣的常量陣列,就叫做words
,正好是bash那個的小寫哈。那麼接下來就是zsh的語法知識了:
1 2 3 4 5 6 7 8 9 |
zshif [[ ${words[(i)-f]} -le ${#words} ]] || [[ ${words[(i)-r]} -le ${#words} ]] then # 修改補全候選列表 fi if [[ ${words[(i)-t]} -le ${#words} ]] || [[ ${words[(i)-w]} -le ${#words} ]] then # 修改補全候選列表 fi |
這裡用到一點zsh特有的下標語法,相當於index()
。
那麼下面是第二步,該怎麼修改補全候選列表呢?如果直接用_arguments
指定新的補全列表,會覆蓋掉前面指定的補全列表。當然也可以把前面的補全列表複製一份,並新增新的選項,用它覆蓋掉原來的補全列表。不過這麼一來程式碼就不好看了。
想來zsh應該提供了對應的API的。果不其然,有一個_values
可以用來幹這事。_values
功能跟_arguments
差不多,而且它接受的選項列表是新增到原有的選項列表中的,而不是覆蓋。所以最後的程式碼是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
zshif [[ ${words[(i)-f]} -le ${#words} ]] || [[ ${words[(i)-r]} -le ${#words} ]] then _values 'reader options' '-R[Parse untranslatable HTML codes and LaTeX as raw]' '-S[Produce typographically correct output]' '--filter[Specify an executable to be used as a filter]' '-p[Preserve tabs instead of converting them to spaces]' fi if [[ ${words[(i)-t]} -le ${#words} ]] || [[ ${words[(i)-w]} -le ${#words} ]] then _values 'writer options' '-s[Produce output with an appropriate header and footer]' '--template[Use FILE as a custom template for the generated document]' '--toc[Include an automatically generated table of contents]' fi |
支援給選項提供引數值來源
最後一步是給-f
和-r
這兩個選項提供讀操作支援的FORMAT引數,給-t
和-w
這兩個選項提供寫操作支援的FORMAT引數。
在Bash篇的實現中,我們檢查上一個詞的值,如果它是-f
或-r
,那麼對當前詞補全讀操作的FORMAT引數。對寫操作的選項也同理。
在zsh中,我們可以用一個特殊的Action:->VALUE
來實現。
->VALUE
這樣的Action會把$state
變數設定成VALUE
,接下來靠一個case語句塊就能根據當前陷入的狀態進行對應的引數補全。
那麼該如何補全FORMAT引數列表呢?這裡可以用上_multi_parts
。
_multi_parts
第一個引數是分隔符,之後接受一組候選詞或一個候選詞陣列作為候選詞列表。例如_multi_parts , a,b,c
,就會生成a b c
這個補全候選列表。
這裡的FORMAT變數直接使用上一章的$READ_FORMAT
和$WRITE_FORMAT
。
我試了一下,如果把FORMAT變數當做字串傳遞過去的話,其間的空格會被轉義,導致無法分隔開來,於是就把它們改寫成陣列的形式。
另外,由於補全FORMAT引數時,不再需要補全選項了。所以把補全FORMAT引數的部分提到補全子選項的前面,並在補全後直接退出程式的執行。
最終完成的程式碼如下:
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 |
zsh#compdef pandoc local READ_FORMAT WRITE_FORMAT 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)' _arguments {-f,-r}'[-f FORMAT, -r FORMAT, Specify input format]: :->reader' {-t,-w}'[-t FORMAT, -w FORMAT, Specify output format]: :->writer' {-o,--output}'[-o FILE, --output=FILE, Write output to FILE instead of stdout]' {-h,--help}'[Show usage message]' {-v,--version}'[Print version]' '*:files:_files' case "$state" in reader ) _multi_parts ' ' $READ_FORMAT && return 0 ;; writer ) _multi_parts ' ' $WRITE_FORMAT && return 0 esac if [[ ${words[(i)-f]} -le ${#words} ]] || [[ ${words[(i)-r]} -le ${#words} ]] then _values 'reader options' '-R[Parse untranslatable HTML codes and LaTeX as raw]' '-S[Produce typographically correct output]' '--filter[Specify an executable to be used as a filter]' '-p[Preserve tabs instead of converting them to spaces]' fi if [[ ${words[(i)-t]} -le ${#words} ]] || [[ ${words[(i)-w]} -le ${#words} ]] then _values 'writer options' '-s[Produce output with an appropriate header and footer]' '--template[Use FILE as a custom template for the generated document]' '--toc[Include an automatically generated table of contents]' fi |
後話
由於zsh的補全功能實在強大,而這篇文章只是簡略地講講如何寫出一個zsh補全指令碼,有許多zsh的補全機制都沒能提到。所以補充一些寫zsh補全指令碼的資料,如果對這方面有興趣可以繼續跳坑:
- zsh-completions專案上的教程。這是我見過的最詳盡的zsh補全指令碼教程。
- 官方文件
- /usr/share/zsh/functions/Completion 也許你能從相似的命令的補全指令碼中汲取靈感。
順便一提,在查詢資料的時候發現有人寫了一個完整的pandoc的zsh補全指令碼,感興趣的話可以看一下:
https://github.com/srijanshetty/zsh-pandoc-completion/blob/master/_pan…