[Ruby] コマンドラインオプションを簡単に扱うライブラリ optparselet

by tanabe on April 28, 2007

optparse は便利なんだけど、ぱっと使うには少し機能が重たく感じるときがあった。

で、機能削って使うときに簡単なやつを書いてみた。(実装は optparse に依存しまくり。)

使い方はこんなかんじ。

# 1. フラグ扱い(指定されていれば true)したければ :act_as_flag 指定
# 2. ショートネームを独自に指定したければ :short_names 指定
# 3. 数字は整数型扱い
# 4. 少数は浮動小数点型扱い
# 5. ただし 0 から始まる数値は文字列扱い
# 6. 他は文字列扱い
# 7. 省略されたオプションをトラックしたいときはブロック渡してその中で適当に処理
# 8. すべてのオプションはデフォルトで省略可

require 'lib/optparselet'

# 一番単純な使い方

parser = OptionParserlet.new([:ability, :bicycle, :cycle])

argv = %w{ -a foo -b bar -c baz }
parser.parse(argv) # => {:ability => "foo", :bicycle => "bar", :cycle => "baz"}

# 1. フラグ扱い(指定されていれば true)したければ :act_as_flag 指定

parser = OptionParserlet.new([:ability, :bicycle, :cycle], :act_as_flag => [:cycle])

argv = %w{ -a foo -b bar -c }
parser.parse(argv) # => {:ability => "foo", :bicycle => "bar", :cycle => true}

# 2. ショートネームを独自に指定したければ :short_names 指定

parser = OptionParserlet.new([:ability, :bicycle, :cycle], :short_names => [:r, :e, :m])

argv = %w{ -a foo -b bar -c baz } # Long Name の短縮も使える
parser.parse(argv) # => {:ability => "foo", :bicycle => "bar", :cycle => "baz"}

argv = %w{ -r foo -e bar -m baz } # 当然、Short Name も使える
parser.parse(argv) # => {:ability => "foo", :bicycle => "bar", :cycle => "baz"}

# 3. 数字は整数型扱い
# 4. 少数は浮動小数点型扱い
# 5. ただし 0 から始まる数値は文字列扱い

parser = OptionParserlet.new([:ability, :bicycle, :cycle])

argv = %w{ -a 23 -b 43.150 -c 0123 }
parser.parse(argv) # => {:ability => 23, :bicycle => 43.15, :cycle => "0123"}

# 7. 省略されたオプションをトラックしたいときはブロック渡してその中で適当に処理

parser = OptionParserlet.new([:ability, :bicycle, :cycle])

argv = %w{ -a foo -b bar } # -c を省略して呼出し
options = parser.parse(argv) do |options|
  options[:cycle] = "default value"
end
options # => {:ability => "foo", :bicycle => "bar", :cycle => "default value"}

# 8. すべてのオプションはデフォルトで省略可

parser.parse(argv) # => {:ability => "foo", :bicycle => "bar"} (エラーにならない)

オプションの取り回しが野暮ったくて拡張性がつぶされてるとか、実装で修正したいところはあるけど、とりあえず公開。

てことで、コードはこちら。

require 'optparse'

class OptionParserlet

  def self.parse(argv, names, options = {}, &block)
    new(names, options).parse(argv, &block)
  end

  def parse(argv, &block)
    argv = ['--help'] if argv.size.zero?

    options = @options
    names = @names

    params = {}
    @parser = OptionParser.new do |opts|
      opts.banner = options[:help] || options[:banner] || "Usage: #{File.basename($0)} [options]"

      if options[:short_names]
        define_option_with_short_name( name_pair(names, options[:short_names]), opts, options, params )
      else
        define_option(names, opts, options, params)
      end
      opts.parse(argv)
    end

    block.call(params) if block
    params
  end

  def initialize(names, options = {})
    @names = names
    @options = options
  end

  private
  def define_option(names, optparse, options, params)
    names.each do |id|
      if flag_option?(id, options)
        optparse.on("--#{id.to_s}") {|f| params[id] = f }
      else
        optparse.on("--#{id.to_s} [val]") {|v| params[id] = parse_val(v) }
      end
    end
  end

  def define_option_with_short_name(names, optparse, options, params)
    names.each_pair do |id, short_name|
      if flag_option?(id, options)
        optparse.on("-#{short_name.to_s}", "--#{id.to_s}") {|f| params[id] = f }
      else
        optparse.on("-#{short_name.to_s}", "--#{id.to_s} [val]") {|v| params[id] = parse_val(v) }
      end

    end
  end

  def flag_option?(id, options)
    options[:act_as_flag] && options[:act_as_flag].include?(id)
  end

  def name_pair(long_names, short_names)
    alist = [long_names, short_names].transpose
    Hash[*alist.flatten]
  end

  def parse_val(str)
    case str
    when /\A0(?:\d+\.\d+|\d+)\z/
      str.to_s
    when /\A\d+\z/
      str.to_i
    when /\A\d+\.\d+\z/
      str.to_f
    else
      str.to_s
    end
  end
end

ショートカットでクラスメソッドを定義したから、実際はそっちだけ使ってれば十分かと。