すがブロ

sugamasaoのhatenablogだよ

Ruby2.4のoptparseでHashにパース結果をマッピングするintoという便利オプションを調べた

表題の通りなんですが

blog.cognitohq.com

上記のページの「Parse CLI options into a Hash」の項目を見ると、以下のように parse メソッドに :into でハッシュオブジェクトを渡すと自動でマッピングしてくれるという地味に便利な機能が追加された。

上記のURLから抜粋

require 'optparse'
require 'optparse/date'
require 'optparse/uri'

cli =
  OptionParser.new do |options|
    options.define '--from=DATE',    Date
    options.define '--url=ENDPOINT', URI
    options.define '--names=LIST',   Array
  end

config = {}

args = %w[
  --from  2016-02-03
  --url   https://blog.blockscore.com/
  --names John,Daniel,Delmer
]

cli.parse(args, into: config)

config.keys    # => [:from, :url, :names]
config[:from]  # => #<Date: 2016-02-03 ((2457422j,0s,0n),+0s,2299161j)>
config[:url]   # => #<URI::HTTPS https://blog.blockscore.com/>
config[:names] # => ["John", "Daniel", "Delmer"]

:into で便利になるのはわかるものの、このサンプルを見ると define って何だろう、とか短いオプション名(-uとか)も指定した場合どうなるの?ってところが色々と疑問だったので調べてみた。

define is 何

情弱なのでリファレンスマニュアルに載ってないAPIだから define を使ったオプションの定義したことがなかった(実は載っているのかと思ってドキドキしたけどやっぱり載ってない)。

library optparse (Ruby 2.4.0)

というわけでRubyのソースを確認して見るとこんな感じ

https://github.com/ruby/ruby/blob/v2_4_2/lib/optparse.rb#L1465-L1505

抜粋すると以下のように、いつも使っている on メソッドが内部で呼び出しているメソッドなんですね。

  def define(*opts, &block)
    top.append(*(sw = make_switch(opts, block)))
    sw[0]
  end

  def on(*opts, &block)
    define(*opts, &block)
    self
  end

ソース内の雰囲気からすると、 on は公開用で define は内部で使う用っぽい気がするけど*1、普通に呼び出せるしどっちでも良いのかもしれない。まあ、今まで通り on 系を使えば良いかなって気がしますね……。

オプション名 is 何

割り当てるオプション名ってどう決まるのか。

options.on '-a, '--argument=VAL'

みたいに定義した時にどっちが使われるのかなという話でもありますし、オプション名に - が含まれていた場合どうなるのか(ネタバレすると:"foo-bar"みたいになる)など疑問が湧きますよね。

これを知るにはざっくり :into オプションの流れを追う必要があるのですが、結論を先に書いておくと

  • 短いオプション名と長いオプション名があった場合長いオプション名が使用される
  • オプション名の先頭ハイフンを取り除いた文字列を .to_sym した値になる

という動作のようです。

プログラム的には

という流れのようですね。

まとめ

    OptionParser.new do |options|
      options.on '-r', '--ruby=VAL' do |val|
        val.downcase
      end
      options.on '-p', '--perl=VAL' do |val|
        val.upcase
      end
      options.on '-g', '--go-lang' do
        'go'
      end
      options.on '-d' do
        true
      end
      options.on_tail '-h', '--help', 'hi'
    end

こんな定義をした場合、 into オプションでHashを渡した場合は

  {
      ruby: 'ruby',
      perl: 'PERL',
      'go-lang': 'go',
      d: true
  }

こんな値が得られます。便利ですね。

サンプルソースコードとテストケースは以下にあります(主に test/unit を使ってみたかっただけw)。 https://github.com/sugamasao/optparser_ruby24_into_sample

*1:実際のところはどうなんでしょうか?