コマンドオプションを解析するライブラリ Vital.OptionParser を書いた

この記事はVim AdventCalendar 2012の 343 日目の記事です. 昨日は cohama さんの Vim で Ruby の def end とかを自動入力する vim-endwise を vim-smartinput で実装してみた でした. そろそろ終わりが見えてきて,僕が投稿するのもこれが最後な気がします.

コマンドのオプションを扱うためのライブラリ,Vital.OptionParser の紹介です. コマンドの引数を単純に <f-args> などで取った場合,引数の順序に依存してしまったり,個々のオプションの解析が面倒になり,正規表現などで実装が汚くなることが多々あります. 今までは各プラグインが独自に引数を解析しており,引数のフォーマットなども微妙に違ったりしていました.

そこで Vital.OptionParser という汎用的にオプションを解析できるライブラリを作りました.これを使うと引数や<bang>のようなアトリビュートを統一的に扱え,引数の順序に依存するようなことはありません.このライブラリはvital.vim の一部になっていて,他の vital.vim のモジュールと同様に使えます.

Vital.OptionParser は Ruby の OptionParser のインターフェースを参考にしていて,

  1. パーサオブジェクト作成
  2. パーサがパースするオプションを定義
  3. パース

の3段階に分かれています.

詳しい使い方はドキュメント のほうを見てもらうとして,本記事ではざっくり使い方を把握してもらうために次のような実例で説明します.

:[range]ResolveConflicts[!] [Options...]

Options:
  --file=FILE      : 解決するファイルを指定する
  --[no-]ask       : 解決前に確認する
  --themselves, -t : マージ先の変更優先で取り込む
  --ourselves, -o  : マージ元の変更優先で取り込む

:ResolveConflicts は依存関係を解決するためのコマンドで,範囲と ! を取ることができ,上記のようなオプションを取れます.このコマンドのパースを Vital.OptionParser でやってみます.

パーサオブジェクトの作成

vital#ofimport() を使ってモジュールを取得し,new() で OptionParser オブジェクトを生成します.

let s:OptionParser = vital#of('my_plugin').import('OptionParser')
let s:parser = s:OptionParser.new()

パーサがパースするオプションを定義

parser.on({long option}, [{short-option}, ] {description})

on() でオプションを定義します.

call s:parser.on('--themselves', '-t', 'マージ先の変更優先で取り込む')
call s:parser.on('--ourselves', '-o', 'マージ元の変更優先で取り込む')
call s:parser.on('--[no-]ask', '解決前に確認する')
call s:parser.on('--file=FILE', '解決するファイルを指定する')

--hoge のようなロングオプションの定義とコマンドの説明が必須で,-h のようなショートオプションはオプショナルです. 次のように,ロングオプションのシグネチャによってオプションの種類が決まります.

フォーマット説明
--hogeオーソドックスなオプション
--hoge=VALUE値が必須なオプション
--[no-]hoge否定可能なオプション

なお,on() は自身を返すため,次のようにつなげて書くこともできます.

call s:parser.on('--themselves', '-t', 'マージ先の変更優先で取り込む')
            \.on('--ourselves', '-o', 'マージ元の変更優先で取り込む')
            \.on('--[no-]ask', '解決前に確認する')
            \.on('--file=FILE', '解決するファイルを指定する')

パース

parser.parse({q-args} [, {cmd-attributes}...])

parse() でコマンドの引数を文字列で受け取り,オプションをパースします.

command! -nargs=* -range=% -bang ResolveConflicts
            \ call s:resolve_conflicts(
            \     s:parser.parse(<q-args>, [<line1>, <line2>], <q-bang>)
            \ )

:ResolveConflicts の定義時に s:parse.parse() を使ってオプションを解析し,解析結果を s:resolve_conflicts() に渡しています.

parse() は,第1引数にコマンドの引数を <q-args>[<f-args>] で渡します. さらに,第2引数以降にコマンドのアトリビュートを渡します.第2引数以降の引数の順番は自由ですが,引数の渡し方(たとえば -bang であれば <q-bang> を使うなど)が決まっています.詳しくはドキュメントを参照してください. 今回は範囲と ! をそれぞれ [<line1>, <line2>]<q-bang> として渡しています.

これで次のようなコマンドを呼ぶと

:1,10ResolveConflicts! -t --no-ask --file=~/hoge piyo

s:parser.parse() は次のような辞書を返します.

{
    'file': '~/hoge',
    'ask': 0,
    'themselves': 1,
    '__range__': [1, 10],
    '__bang__': '!',
    '__unknown_args__': ['piyo']
}

キーがオプション名,値がオプションのパース結果になります.= で値を指定するとその値が文字列として入り,値を指定しないと数値の 1 または 0 が入ります. また,_ で囲まれているキーはコマンドのアトリビュートのパース結果です.__unknown_args__ の値には解析対象でない引数が指定されたときにそれらが入ります.

パース後の扱い方

パース結果の辞書は s:resolve_conflicts() に渡されています. s:resolve_conflicts() では解析結果から各オプションに対する処理をします.

function! s:resolve_conflicts(opts)
    if has_key(a:opts, 'themselves')
        " --themselves が指定されたときの処理
    endif
    if has_key(a:opts, 'ourselves')
        " --ourselves が指定されたときの処理
    endif
    if has_key(a:opts, 'ask')
        " --ask または --no-ask が指定されたときの処理
    endif
    if has_key(a:opts, 'file')
        " --file に値が指定されたときの処理"
    endif

    if has_key(a:opts, '__bang__')
        " コマンドに ! が付けられた時の処理
    endif
    if a:opts.__unknown_args__ != []
        " 指定外のオプションが付けられたときの処理
    endif

endfunction

--help オプション

--help オプションは特別なオプションで,自動で定義されます. --help オプションを指定すると,そのコマンドのオプションヘルプが :echo で出力されます.

今後の拡張

これからも Vital.OptionParser は拡張していく予定があり,直近では

  • 引数にデフォルト値をつけられるようにする(work in progress)
  • コマンドの補完関数を自動生成する
  • --hoge="aaa bbb" のようにクオートで囲った値を渡せるようにする

が挙げられます. また,サブコマンド用のモジュール(Ruby の thor のような感じ)を新たに作成することも考えています.実装的には,各サブコマンドが Vital.OptionParser のオブジェクトを持つ感じになると思います.

まとめ

コマンドのオプションを扱うためのライブラリ,Vital.OptionParser を紹介しました. 紹介したコード片をまとめるとこんな感じになります.

option-parser-example.vim

オプションが1,2個のコマンドでも実装がすっきりするので,vimrc などでも積極的に使っていって損は無いと思うので,是非使ってみてほしいです.