optparse って、どうしても使い方が覚えられません。"--option-name [OPTION]" みたいな文字列が内部でパースされて、その結果挙動が変わるというインターフェイスが気持ち悪いせいだと思うんです。気持ち悪いインターフェイスは覚えられない *1 。
そこで、最近 trunk に入った Method#parameters を使えばもっと覚えやすくわかりやすい記述ができるんじゃないかなと考えました。
Method#parameters というのは、こんな感じに、Method オブジェクトから仮引数の名前や種類を知ることができるメソッドです。:req は必須の引数、:opt はオプションの引数をあらわします。
def foo(x, y, z = :foo) end p method(:foo).parameters #=> [[:req, :x], [:req, :y], [:opt, :z]]
これを使って、optparse の超いい加減ラッパを作ってみました。それを使って書いてみたコマンドライン引数のパーサがこちら。
require "optparse-proxy" class FooOptionParser < OptionParserProxy desc "description of -a" def _a puts "option -a" end alias __long_aaa _a desc "description of -b" def _b(val) puts "option -b with #{ val }" end desc "description of -c" def _c(val = "default") puts "option -c with #{ val }" end end FooOptionParser.parse(ARGV)
どうでしょう。def _a が -a オプションの処理を、def _b(val) が -b オプション (引数必須) の処理をする感じです。慣れ親しんだ Ruby の記法そのままなので、覚えやすいような気もします。
--help はこちら。
$ ruby19 sample.rb --help Usage: sample [options] -a, --long-aaa description of -a -b VAL description of -b -c [VAL] description of -c
仮引数の名前 (val) や種別が反映されています。
実行例。
$ ruby19 sample.rb -a option -a $ ruby19 sample.rb -a -b foo option -a option -b with foo $ ruby19 sample.rb -c option -c with default $ ruby19 sample.rb -c foo option -c with foo
以下、optparse-proxy.rb の中身。エラーチェックとかぜんぜんしてないので、proof-of-concept だと思ってください。
require "optparse" class OptionParserProxy def self.desc(desc) @desc = desc @descs ||= {} end def self.method_added(name) @descs[name] = @desc if @desc @desc = nil end def self.option_parser op = OptionParser.new proxy = new opts = {} defs = {} instance_methods(false).each do |name| mhd = instance_method(name) (opts[mhd.to_s] ||= []) << name defs[mhd.to_s] = mhd.parameters end opts.each do |s, names| args = names.sort.reverse.map do |name| str = name.to_s.tr("_", "-") unless defs[s].empty? type, arg = defs[s].first arg = arg.to_s.upcase str << " " << (type == :req ? arg : "[#{ arg }]") end str end args << @descs[names.first] if @descs[names.first] op.on(*args) do |*args| args = [] if defs[s].empty? proxy.send(names.first, *args.compact) end end op end def self.parse(argv) option_parser.parse(argv) end end
*1:自由度が高すぎるって問題もあるでしょうね。もっと定番の使い方を押し付けてほしい。