よんちゅBlog

― このブログは自分用のメモや日々の問題などを共有するためのものです ―

20121005185841 お知らせ:  2013/07/17 ブログデザインをリニューアルしました。

zsh の補完関数で キャッシュ機能(cache-policy) を使う

今回は grunt コマンドの補完関数を例として キャッシュ機能(cache-policy) について解説していきます。
grunt については知らなくても大丈夫だと思います。

キャッシュ機能(cache-policy) についての解説がメインなので、zsh 補完関数の基本的な説明は省略します。
その代わり、コード内にコメントで簡単な説明を書きましたのでそちらを参考にして下さい。

※ とても長い記事なので、誤字脱字があるかもしれません。ご容赦下さい。m(_ _)m
(見つけたら教えてくれると助かります)

はじめに

grunt には標準で bash の補完がついています。
しかし、zsh 用の補完関数は無く、bash の補完と同じ方法で実装したのではオプションやタスクの説明が表示されません。
オプションの説明はベタ書きでいいとしても、タスクの説明はそうはいきません。

ということで作成したのが、grunt-zsh-completion です。

grunt のタスクは、"grunt --version --verbose" または "grunt --help" で取得することができますが、前者は実行が早い代わりにタスクの説明が取得できません、後者は実行が遅いかわりにタスクの説明も取得することができます。

bash の補完では前者が採用されており、今回作成した grunt-zsh-completion では後者を採用しました。

さらに、"grunt --help" の遅さをカバーするために キャッシュ機能(cache-policy) を実装しました。

今回は grunt-zsh-completion を例として解説するわけですが、実際のコードではちょっと複雑すぎるので解説用に余分な部分を省いたコードで見ていくことにします。

キャッシュ機能を有効にする

解説の前に、まずはキャッシュを有効にしましょう。

いくら補完関数がキャッシュ機能を実装していても、それを使用するユーザがキャッシュを有効にしていなければ意味がありません。

最低限以下の設定を .zshrc に書いておいて下さい。

zstyle ':completion:*' use-cache yes

補完関数を作る予定のない方でも、キャッシュを有効にしておいた方が良いかと思います。
(といってもキャッシュに対応した補完関数はあまり多くありませんが)

キャッシュの保存場所について

キャッシュは ~/.zcompcache ディレクトリ配下に、キャッシュ毎にファイルが作られます。

今回の場合、grunt という名前でキャッシュファイルが作られるはずです。
キャッシュファイル名は、実装者が自由に決めることができます。

$ ls ~/.zcompcache
grunt

キャッシュファイルの中には、変数名とその値が書かれていて、
ファイルをそのまま source すればキャッシュを復元できるような形式になっています。
キャッシュの復元は専用の関数が用意されているので自身で source する必要はありません

補完関数の全体コード

それでは実装の説明です。
まずは全体のコードを見てみましょう。

以下のコードは実際に動かして動作確認を行うこともできます。
ただし、補完されるオプションやタスクは解説用のダミーになっています。

それでは、コード内の番号が振ってあるところを中心に解説していきたいと思います。

実装解説

1.cache-policy を設定
###############################
#    1.cache-policy を設定
###############################
zstyle -s ":completion:${curcontext}:" cache-policy update_policy
if [[ -z $update_policy ]]; then
    zstyle ":completion:${curcontext}:" cache-policy __grunt_caching_policy
fi

まず先頭の、

zstyle -s ":completion:${curcontext}:" cache-policy update_policy

という構文は、コンテキスト ":completion:${curcontext}:" の スタイル cache-policy の値を
update_policy 変数に設定するという意味になります。
-s は文字列として取得するという意味です。

そして if 文の中にある、

zstyle ":completion:${curcontext}:" cache-policy __grunt_caching_policy

という構文は、cache-policy__grunt_caching_policy という文字列を設定しています。

コンテキストスタイル といった用語は、ここではあまり深く考えずにそういう用語だと思って下さい。

コンテキスト名前空間スタイル は キー だと考えると分かりやすいと思います。
難しそうに見えますが、一度自分で使ってみるとすぐに理解できます。

例えば以下のように自分で使用することもできます。
(コンテキストが既存のものと被っていなければ自由に使えます)

# ":hoge:hoge" fuga に something という文字列を設定
$ zstyle ":hoge:hoge" fuga something

# 変数 var に設定されている値を取得する
$ zstyle -s ":hoge:hoge" fuga var

$ echo "$va"
something

このように、zstyle を使用することでグローバル空間を汚染することなく値を保存することができます。

zsh のプラグインを作る場合などに、設定値を保持するのに利用すると良いでしょう。
グローバルなシェル変数や環境変数を使用するよりスマートです。

話を戻します。

cache-policy に設定した __grunt_caching_policy という文字列は、キャッシュが有効かどうか
判別するのに使用する関数の名前です。自分で定義した関数の名前を設定しています。

この関数の中身やこれがいつ呼ばれるかなどについては、また後で説明します。

2.キャッシュの復元と更新を行う
########################################
#    2.キャッシュの復元と更新を行う
########################################
# グローバル変数 __grunt_tasks にタスク情報が設定される
if __grunt_retrieve_update_cache; then
    update_msg='(cache updated)'
fi

__grunt_retrieve_update_cache 関数を実行すると、 グローバル変数 __grunt_tasks にタスク情報が設定されるようになっています。

詳しくは後述しますが、まずは全体の流れを知ってもらいたいので、ここでは __grunt_tasks 変数にタスク情報が設定される、ということだけ理解しておいて下さい。

また、この関数はキャッシュを更新した場合に戻り値が真になるようになっています。

update_msg 変数は、キャッシュが更新されたことをユーザに知らせるため、補完を表示するときに使用します。
必須ではありませんが、デバッグにも役立つのでおすすめです。
(ホントはデバッグ用に使ってたんだけど、便利だったので本実装にも残したってだけなんですけど)

3.タスク補完
######################
#    3.タスク補完
######################
case $state in
    tasks)
        ### 3.タスク補完
        # __grunt_tasks 変数と _describe 関数を使用してタスクを補完
        _describe "grunt task$update_msg" __grunt_tasks || return 1
    ;;
esac

ここでは、__grunt_tasks 変数と _describe 関数を使用してタスクを補完します。

_describe 関数は zsh が提供している関数で、第1引数に補完グループ名を、第2引数に補完情報の入った変数の変数名を指定します。

グループ名は自由につけることができます。

$ grunt [Tab]
 -- grunt task (cache updated) --     << ここに補完グループ名が表示される
clean  -- Clean files and folders
fuga   -- fuga task
hoge   -- hoge task
4.キャッシュの復元と更新

続いて、後述すると言った __grunt_retrieve_update_cache 関数の説明をします。
おそらく一番重要な部分です。
キャッシュの種類が増えたり、キャシュの更新/復元条件が複雑になると、実装が難解になってきます。

############################################################
#    4.キャッシュの復元と必要ならキャッシュの更新を行う
############################################################
#     キャッシュファイル名: grunt
#     キャッシュ変数名: __grunt_tasks
function __grunt_retrieve_update_cache() {
    if ( ! (( $+__grunt_tasks )) \
        || _cache_invalid 'grunt' ) \
        && ! _retrieve_cache 'grunt'; then
        ### キャッシュの更新が必要
        # 新たに grunt のタスク情報を取得
        __grunt_tasks=(${(f)"$(__grunt_get_tasks)"})
        ### キャッシュを保存
        _store_cache 'grunt' __grunt_tasks
        return 0
    fi
    return 1
}

まず、

if ( ! (( $+__grunt_tasks )) \
    ....
fi

という構文は、__grunt_tasks 変数が定義されているかどうかを判別しています。
$+__grunt_tasks$+ に続く変数が定義されていれば 非0 を、定義されていなければ 0 を返します。
そして、( ( ... ) ) は算術式評価といって、中の式が 非0 なら "真"、0 なら "偽" になります。

よってこの構文は、__grunt_tasks 変数が未定義なら "真" となります。

続いて、

_cache_invalid 'grunt'

という部分は、キャッシュが無効かどうかを判別しています。
_cache_invalid 関数は zsh が提供する関数で、引数に指定したキャッシュが 無効なら "真"有効なら"偽" を返します。

引数に指定するキャッシュ名は自由に決めることができます。
そして、このキャッシュ名が前述の キャッシュファイル名 として利用されます。

続いて、

! _retrieve_cache 'grunt'

という部分は、名前の通りキャッシュを取得(復元)しています。
引数にキャッシュ名を指定し、キャッシュが有効なら復元され戻り値が "真" となります。

例え _cache_invalid が偽を返したとして(キャッシュが有効でも)、ユーザがキャッシュ機能を無効にしていた場合には、キャッシュの復元に失敗します。
そのため、戻り値のチェックを行なっています。

結局、キャッシュが復元出来なければ if 文の中が実行されるようになっています。
(ちょっとトリッキーな書き方ですが、この書き方が一番簡潔かと思います)

続いて、if 文の中身についてです。

### キャッシュの更新が必要
# 新たに grunt のタスク情報を取得
__grunt_tasks=(${(f)"$(__grunt_get_tasks)"})
### キャッシュを保存
_store_cache 'grunt' __grunt_tasks
return 0

キャッシュが利用できないので grunt のタスク情報を新たに取得します。
そして、取得したタスク情報を、

_store_cache 'grunt' __grunt_tasks

という部分でキャッシュとして保存しています。
第1引数にキャッシュ名を、第2引数以降に保存する変数名を指定します。

キャッシュを保存すると、キャッシュファイル(~/zcompcache/grunt) が作られます。
中身は以下のようになるはずです。

$ cat ~/.zcompcache/grunt
__grunt_tasks=( 'hoge:hoge task' 'fuga:fuga task' 'clean:Clean files and folders' )

そして、前述の _retrieve_cache 関数でキャッシュを復元すると、_store_cache 関数で指定した変数名の変数にキャッシュが復元されます。
(上記キャッシュファイルが source されると考えると分かりやすいです)

また、今回は1変数しか保存しませんでしたが、_store_cache に複数の変数を渡すこともできます。

_store_cache 'grunt' __grunt_tasks hoge fuga

注意すべき点として、_store_cache 関数は常に新しいキャシュを生成するため、複数行に分けて実行することはできません。
複数行に分けて実行した場合は、最後のみ有効になります。(変数名を変えてもダメです)
必要なら別のキャッシュ名を指定しましょう。

最後に、キャッシュを更新したので return 0 で "真" を返しています。

5.cache-policy に設定する関数

cache-poilcy に設定する関数の説明です。

この関数は、_cache_invalid または、_retrieve_cache 関数実行時に、キャッシュが有効かどうかチェックするために呼ばれます。

この関数では、$1 変数にキャッシュファイルのパスが設定されます。
今回の場合は、~/.zcompcache/grunt が設定されるはずです。

また、戻り値が "真" (return 0) ならキャッシュが無効と判断され、"偽" (return 非ゼロ) なら有効と判断されます。

#######################################
#    5.cache-policy に設定する関数
#######################################
#  $1 にキャッシュファイルのパスが入っています
#
#  戻り値:
#    真 (return 0)      : キャッシュ無効
#    偽 (return 非ゼロ) : キャッシュ有効
function __grunt_caching_policy() {
    local -a oldp
    # Nm+7  と指定するとキャッシュの有効期限が 7日 となる
    # Nmw+1 と指定すると 1週間
    # Nmh+1 と指定すると 1時間
    # Nmm+1 と指定すると 1分
    oldp=( "$1"(Nm+7) )
    (( $#oldp ))
}

まず、

oldp=( "$1"(Nm+7) )

という構文についてです。

この中の "$1"(Nm+7) という部分は、グロブ修飾子 (Glob Qualifiers) を用いた ファイル名生成(グロビング) といって、ファイル名の末尾に (Nm+7) のようなグロブ修飾子をつけることで、条件にマッチしたファイル名のみ残すという手法です。

(N) は、条件にマッチするファイルがない場合にエラーとしないグロブ修飾子です。

(m+7) は、ファイルの最終変更日時から7日以上経っているもの、という意味のグロブ修飾子です。

キャッシュファイルが変更されてから7日以上経っていれば、条件にマッチするので "$1" がそのまま残ります。
7日未満なら、条件にマッチしないので空になります。
(このときエラーにしないために、(N) というグロブ修飾子を使っています)

他にも (a+7) でアクセス日時、(c+7) でiノード変更日時をチェックすることができます。

続いて、

oldp=(...)

という部分は、配列を oldp 変数に代入しています。
これは、ファイル名生成(グロビング) を実行するためにこういう書き方をしています。

これで、キャッシュファイルが変更されてから7日以上経っていれば、oldp 配列にキャッシュファイル名が設定されるようになります。

最後に、

(( $#oldp ))

という部分です。
これはすでに紹介した算術式評価の構文で、oldp 配列の length が 非0 なら "真"、0 なら "偽" となります。

つまり、キャッシュファイルが変更されてから7日以上経っていれば、oldp 配列にキャッシュファイル名が設定されるので length が 1 となり、算術式評価の結果が "真" となります。

そして、関数内で return 文を省略すると暗黙の return が実行され、最後に評価した式の戻り値が関数全体の戻り値となります。

これは、単に return と書いた場合や、return $? と書いた場合と同じ意味になります。

実装説明終わり

実装の説明は以上です。

いかがでしたでしょうか?
少しでも理解の助けになれば幸いです。

補完関数を書く上で参考になりそうなリンク

最後に、補完関数の基本を理解するのに役立ちそうな記事のリンクを張っておきます。

他におすすめの記事などあれば教えてください。

それと、以下の書籍 "zshの本" が超絶おすすめです。
キャッシュに関しては残念ながら記述はありませんでしたが、それ以外ならだいたい書いてます。
(索引が弱いのだけが難点)

zshの本 (エッセンシャルソフトウェアガイドブック)

広瀬 雄二 技術評論社 2009-06-17
売り上げランキング : 231694
by ヨメレバ

用語やフラグを調べたい場合は、以下の方法がおすすめです。

おわり

キャッシュ機能が必要になるケースは稀かもしれませんが、もし必要になった時にはこの記事を思い出して頂ければと思います。

あと、実際の grunt 補完をご利用になりたい方は、以下の最新版をご使用下さい。

解説用のサンプルと比べるとかなり複雑になっています。
興味のある方は解析してみて下さい。

以上で解説は終わりです。

質問や、もっとこの辺を詳しく書いて欲しいなどありましたら、お気軽にお尋ねください。