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

絕大部分平常使用Linux和OS X的程序員都會選擇zsh做爲本身的shell環境,畢竟對比於bash,zsh的便利性/可玩性要勝出不少,同時它又能兼容bash大多數的語法。不過相對而言,zsh補全腳本要比bash補全腳本要難寫。zsh提供了很是多的補全的API,並且這些API功能有很多重疊的地方,掌握起來並不容易。不像bash,你只需記住三個API(compgencompletecompopt)就能實現整個補全腳本。php

這篇的任務跟上一篇的同樣,須要實現一個針對pandoc的補全腳本,囊括下面三個目標:html

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

何處安放腳本

在開始以前,須要說明下放置zsh腳本的地方,這樣咱們才能讓接下來寫的補全腳本發揮效力。
zsh在啓動時會加載$fpath路徑下的腳本文件。試試echo $fpath來看看這個變量的值。接下來咱們能夠把補全腳本放到$fpath的路徑下,或者建立一個新的在$fpath路徑中的目錄:html5

  1. mkdir ~/.fpath
  2. ~/.zshrc中添加fpath=($HOME/.fpath $fpath)
  3. 重啓zsh

當咱們把本身寫的補全腳本放好後,每次zsh一啓動,就會加載它。不過總不能每次修改完腳本後,都重啓一次zsh吧。若是隻是單純更新補全腳本,能夠執行unfunction _pandoc && autoload -U _pandoc,zsh就會從新加載補全腳本了。(其中_pandoc是補全腳本的名字)git

支持主選項

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

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的補全函數。github

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就夠用了。shell

_arguments接受一連串的選項字符串,每一個字符串表明一個選項。另外你還能夠經過一些選項指定補全上的細節。舉-s爲例:假設你的工具支持-a -b兩個選項,也支持-ab的方式來同時指定兩個選項。若是沒給_arguments提供-s的選項,那麼zsh是不會補全出-ab,由於並不存在選項-ab。而提供了-s後,_arguments才容許你在已經輸入-a的狀況下,補全出-abjson

選項字符串的格式是這樣的:-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'表示,若是找不到匹配的候選詞,就補全文件名。
到目前爲止,實現第一階段目標的腳本所需的知識點已經講解完畢。bash

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

_alternative支持的選項字符串格式跟_arguments很像,好比

_arguments \
    {-t,-w}'[-t FORMAT, -w FORMAT, Specify output format]'

等價於

_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的語法知識了:

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差很少,並且它接受的選項列表是添加到原有的選項列表中的,而不是覆蓋。因此最後的代碼是這樣的:

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參數的部分提到補全子選項的前面,並在補全後直接退出程序的執行。

最終完成的代碼以下:

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補全腳本的資料,若是對這方面有興趣能夠繼續跳坑:

  1. zsh-completions項目上的教程。這是我見過的最詳盡的zsh補全腳本教程。
  2. 官方文檔
  3. /usr/share/zsh/functions/Completion 也許你能從類似的命令的補全腳本中汲取靈感。

順便一提,在查找資料的時候發現有人寫了一個完整的pandoc的zsh補全腳本,感興趣的話能夠看一下:
https://github.com/srijanshetty/zsh-pandoc-completion/blob/master/_pan...

相關文章
相關標籤/搜索