跟我一起寫shell補全指令碼(Zsh篇)

發表於2016-09-12

絕大部分日常使用Linux和OS X的程式設計師都會選擇zsh作為自己的shell環境,畢竟對比於bash,zsh的便利性/可玩性要勝出很多,同時它又能相容bash大多數的語法。不過相對而言,zsh補全指令碼要比bash補全指令碼要難寫。zsh提供了非常多的補全的API,而且這些API功能有不少重疊的地方,掌握起來並不容易。不像bash,你只需記住三個API(compgencompletecompopt)就能實現整個補全指令碼。

這篇的任務跟上一篇的一樣,需要實現一個針對pandoc的補全指令碼,囊括下面三個目標:

  1. 支援主選項(General options)
  2. 支援子選項(Reader options/General writer options)
  3. 支援給選項提供引數值來源

何處安放指令碼

在開始之前,需要說明下放置zsh指令碼的地方,這樣我們才能讓接下來寫的補全指令碼發揮效力。
zsh在啟動時會載入$fpath路徑下的指令碼檔案。試試echo $fpath來看看這個變數的值。接下來我們可以把補全指令碼放到$fpath的路徑下,或者建立一個新的在$fpath路徑中的目錄:

  1. mkdir ~/.fpath
  2. ~/.zshrc中新增fpath=($HOME/.fpath $fpath)
  3. 重啟zsh

當我們把自己寫的補全指令碼放好後,每次zsh一啟動,就會載入它。不過總不能每次修改完指令碼後,都重啟一次zsh吧。如果只是單純更新補全指令碼,可以執行unfunction _pandoc && autoload -U _pandoc,zsh就會重新載入補全指令碼了。(其中_pandoc是補全指令碼的名字)

支援主選項

還是跟上一篇一樣,先解釋一個實現第一個目標的程式,帶各位入門:

就像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是等價的寫法。

  1. -x是選項的名字
  2. [description]是該選項的描述,可選
  3. message這一項我也不知道是什麼意義……不過它是可選的,除非你需要指定action
  4. action用於生成複雜的補全。在這裡你可以使用許多補全語法。一個常見的例子是使用輔助函式,比如_files表示補全當前路徑下的檔名。詳見:
    1. https://github.com/zsh-users/zsh-completions/blob/master/zsh-completio…
    2. https://github.com/zsh-users/zsh-completions/blob/master/zsh-completio…

最後一行'*:files:_files'表示,如果找不到匹配的候選詞,就補全檔名。
到目前為止,實現第一階段目標的指令碼所需的知識點已經講解完畢。

_arguments有一個限制,它要求選項的名字元合某些特殊格式,比如以-+=等字元開頭(所以才叫_arguments嘛)。如果你的工具接受addremove之類的子命令,就需要用到_alternative

_alternative支援的選項字串格式跟_arguments很像,比如

等價於

支援子選項

所謂的支援子選項,就是在某些選項存在的情況下,增加多一些選項。所以,我們所要做的,就是檢查當前輸入的命令列引數中是否存在某些引數,如果存在,增加新的選項。這一步可以分解成兩個步驟,第一個是檢查某些引數是否存在,第二個是增加新的選項。

之前寫bash補全指令碼的時候,是通過遍歷某個儲存有當前輸入的常量陣列,來檢查某些引數是否存在。在網上搜尋一番後,我發現zsh也有同樣的常量陣列,就叫做words,正好是bash那個的小寫哈。那麼接下來就是zsh的語法知識了:

這裡用到一點zsh特有的下標語法,相當於index()

那麼下面是第二步,該怎麼修改補全候選列表呢?如果直接用_arguments指定新的補全列表,會覆蓋掉前面指定的補全列表。當然也可以把前面的補全列表複製一份,並新增新的選項,用它覆蓋掉原來的補全列表。不過這麼一來程式碼就不好看了。

想來zsh應該提供了對應的API的。果不其然,有一個_values可以用來幹這事。_values功能跟_arguments差不多,而且它接受的選項列表是新增到原有的選項列表中的,而不是覆蓋。所以最後的程式碼是這樣的:

支援給選項提供引數值來源

最後一步是給-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引數的部分提到補全子選項的前面,並在補全後直接退出程式的執行。

最終完成的程式碼如下:

後話

由於zsh的補全功能實在強大,而這篇文章只是簡略地講講如何寫出一個zsh補全指令碼,有許多zsh的補全機制都沒能提到。所以補充一些寫zsh補全指令碼的資料,如果對這方面有興趣可以繼續跳坑:

  1. zsh-completions專案上的教程。這是我見過的最詳盡的zsh補全指令碼教程。
  2. 官方文件
  3. /usr/share/zsh/functions/Completion 也許你能從相似的命令的補全指令碼中汲取靈感。

順便一提,在查詢資料的時候發現有人寫了一個完整的pandoc的zsh補全指令碼,感興趣的話可以看一下:
https://github.com/srijanshetty/zsh-pandoc-completion/blob/master/_pan…

相關文章