Method#parameters で optparse を覚えやすく

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:自由度が高すぎるって問題もあるでしょうね。もっと定番の使い方を押し付けてほしい。