RubyのOptionParserの底力を知る

  • コマンドは、以下のような書式でオプションと引数を設定して、実行する仕組みになっている。

例:

コマンド名 オプション オプション オプション引数 オプション コマンド引数
optparser_test.rb -a -b VALUE --foo FILE_PATH
  • 実際にコマンドを作ろうとすると、オプションの解析には手間がかかると気付く。
  • そのため、多くの言語環境にはオプション解析用のライブラリが用意されている。

どうしたら苦労最小限でオプションを解析できるのか、調べてみた。

基本

require 'optparse'

OptionParser.new do |opt|
  opt.parse!(ARGV)
end
  • オプション定義が何もない状態でも、opt.parse!(ARGV)さえ実行しておけば、-hと--helpオプションが利用できる。
$ ruby optparse_test.rb -h
Usage: optparse_test [options]

$ ruby optparse_test.rb --help
Usage: optparse_test [options]

1文字オプション

  • onメソッド(opt.on)でオプションを定義する。
  • オプション定義とオプション説明を引数に設定する。
  • 1文字オプションは、-で始まる1文字のオプション名。
  • オプション引数がある場合は、続けてオプション引数を象徴する任意の変数名*1を書く。
  • [変数名]のように[]で囲うと、オプション引数は省略可能になる。
require 'optparse'

option={}
OptionParser.new do |opt|
  opt.on('-a',         '1文字オプション 引数なし')         {|v| option[:a] = v}
  opt.on('-b VALUE',   '1文字オプション 引数あり(必須)')   {|v| option[:b] = v}
  opt.on('-c [VALUE]', '1文字オプション 引数あり(省略可能)'){|v| option[:c] = v}
  
  opt.parse!(ARGV)
end
p option
  • ヘルプ表示とオプションの解析結果は、以下のようになった。
$ ruby optparse_test.rb -h
Usage: optparse_test [options]
    -a                               1文字オプション 引数なし
    -b VALUE                         1文字オプション 引数あり(必須)
    -c [VALUE]                       1文字オプション 引数あり(省略可能)

$ ruby optparse_test.rb -a -b 必須 -c
{:a=>true, :b=>"必須", :c=>nil}

ロングオプション

  • ロングオプションは、--で始まる2文字以上のオプション名。
  • 定義の仕方は、基本的に1文字オプションと同じなのだが、
  • オプション名と変数名をスペースか=で区切っておく必要がある。
    • どこまでがオプション名で、どこからが変数名かを明示するのだ。
    • 以下の例では、慣例に習って=で区切って定義してみた。
require 'optparse'

option={}
OptionParser.new do |opt|
  opt.on('--along',         '1文字オプション 引数なし')         {|v| option[:along] = v}
  opt.on('--blong=VALUE',   '1文字オプション 引数あり(必須)')   {|v| option[:blong] = v}
  opt.on('--clong=[VALUE]', '1文字オプション 引数あり(省略可能)'){|v| option[:clong] = v}
  
  opt.parse!(ARGV)
end
p option
  • ヘルプ表示してみた。
$ ruby optparse_test.rb -h
Usage: optparse_test [options]
        --along                      1文字オプション 引数なし
        --blong=VALUE                1文字オプション 引数あり(必須)
        --clong=[VALUE]              1文字オプション 引数あり(省略可能)
  • コマンド引数を=で区切って定義しても、コマンド実行時にはスペース区切りも使える。
$ ruby optparse_test.rb --along --blong=必須 --clong
{:along=>true, :blong=>"必須", :clong=>nil}

$ ruby optparse_test.rb --along --blong 必須 --clong
{:along=>true, :blong=>"必須", :clong=>nil}
  • ロングオプションは必要最小限の入力で指定可能だ。
  • その他のオプション名と区別可能であれば1文字でもOK。
$ ruby optparse_test.rb --a --b=必須 --c
{:along=>true, :blong=>"必須", :clong=>nil}

1文字オプションとロングオプション

  • よくあるコマンドインタフェースとして、ロングオプションに対応する1文字オプションが用意されていることがある。
  • ロングオプションのショートカット的な役割の1文字オプションを定義したい。
  • そのような場合、ロングオプションと1文字オプションは、同時に定義できる。
require 'optparse'

option={}
OptionParser.new do |opt|
  opt.on('-a', '--along',         '1文字オプション 引数なし')         {|v| option[:along] = v}
  opt.on('-b', '--blong=VALUE',   '1文字オプション 引数あり(必須)')   {|v| option[:blong] = v}
  opt.on('-c', '--clong=[VALUE]', '1文字オプション 引数あり(省略可能)'){|v| option[:clong] = v}
  
  opt.parse!(ARGV)
end
p option
  • ヘルプ表示してみた。
$ ruby optparse_test.rb -h
Usage: optparse_test [options]
    -a, --along                      1文字オプション 引数なし
    -b, --blong=VALUE                1文字オプション 引数あり(必須)
    -c, --clong=[VALUE]              1文字オプション 引数あり(省略可能)
  • ロングオプションも1文字オプションも、問題なく使える。
  • オプション引数を指定する時、
    • ロングオプションでは、スペースか=で区切る必要がある。
    • 1文字オプションでは、区切りなし、あるいはスペースで区切る。
$ ruby optparse_test.rb --along --blong=必須 --clong
{:along=>true, :blong=>"必須", :clong=>nil}

$ ruby optparse_test.rb -a -b 必須 -c
{:along=>true, :blong=>"必須", :clong=>nil}

$ ruby optparse_test.rb -a -b必須 -c
{:along=>true, :blong=>"必須", :clong=>nil}
  • 1文字オプションでは、=で区切ってオプション引数を指定してしまうとNG。
  • =も含んだ文字列を指定したことになってしまう。
$ ruby optparse_test.rb -a -b=必須 -c
{:along=>true, :blong=>"=必須", :clong=>nil}

noオプション

  • no-で始まるロングオプション名は、falseを返す。
  • [no-]と括弧で囲うと、no-を省略したロングオプション名も可能になる。
require 'optparse'

option={}
OptionParser.new do |opt|
  opt.on('--no-only', 'onlyは無効'){|v| option[:only] = v}
  opt.on('--[no-]both', 'bothも有効'){|v| option[:both] = v}
  
  opt.parse!(ARGV)
end
p option
  • ヘルプ表示してみた。
$ ruby optparse_test.rb -h
Usage: optparse_test [options]
        --no-only                    onlyは無効
        --[no-]both                  bothも有効
  • no-は、falseを返す。
$ ruby optparse_test.rb --no-only --no-both
{:only=>false, :both=>false}
  • --[no-]bothだと、--bothも可能。trueを返す。
$ ruby optparse_test.rb --both
{:both=>true}
  • --no-onlyでは、--onlyはエラーになる。
$ ruby optparse_test.rb --only
optparse_test.rb:8:in `block in 
': invalid option: --only (OptionParser::InvalidOption) from optparse_test.rb:4:in `new' from optparse_test.rb:4:in `
'

オプション引数を数値や配列として受け取る

  • 通常、オプション引数に指定した値は、文字列として受け取る。
  • クラスを指定すると、そのクラスに変換されたものを受け取る。
  • 指定可能なクラスは、instance method OptionParser#onデフォルトで利用可能な引数クラスを参照。
require 'optparse'

option={}
OptionParser.new do |opt|
  opt.on('--long=V,V,...', '通常のオプション'){|v| option[:only] = v}
  opt.on('--long_array=V,V,...', Array, 'クラス指定あり'){|v| option[:long_array] = v}
  
  opt.parse!(ARGV)
end
p option
  • ヘルプ表示してみた。
$ ruby optparse_test.rb -h
Usage: optparse_test [options]
        --long=V,V,...               通常のオプション
        --long_array=V,V,...         クラス指定あり
  • 通常は文字列として受け取る。
$ ruby optparse_test.rb --long=1,2,3
{:only=>"1,2,3"}
  • Arrayを指定すると配列になる。
$ ruby optparse_test.rb --long_array=1,2,3
{:long_array=>["1", "2", "3"]}

オプション引数の選択肢

  • オプション引数に、いくつかの選択肢を設定したい場合がある。
  • 選択肢の配列やハッシュを与えておくと、それが選択肢となる。
require 'optparse'

option={}
OptionParser.new do |opt|
  opt.on('--long=VALUE', 
            'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS'){|v| option[:long] = v}
  
  opt.on('--long_array=VALUE', 
            ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'], 
            'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS'){|v| option[:long_array] = v}
  
  opt.on('--long_hash=VALUE', 
            {'1'=>'WHITESPACE_ONLY', '2'=>'SIMPLE_OPTIMIZATIONS', '3'=>'ADVANCED_OPTIMIZATIONS'}, 
            '1 | 2 | 3'){|v| option[:long_hash] = v}

  opt.on('--long_both=VALUE', 
            ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'], 
            {'1'=>'WHITESPACE_ONLY', '2'=>'SIMPLE_OPTIMIZATIONS', '3'=>'ADVANCED_OPTIMIZATIONS'}, 
            '1:WHITESPACE_ONLY | 2:SIMPLE_OPTIMIZATIONS | 3:ADVANCED_OPTIMIZATIONS'){|v| option[:long_both] = v}

  opt.parse!(ARGV)
end
p option
  • ヘルプ表示してみた。
$ ruby optparse_test.rb -h
Usage: optparse_test [options]
        --long=VALUE                 WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS
        --long_array=VALUE           WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS
        --long_hash=VALUE            1 | 2 | 3
        --long_both=VALUE            1:WHITESPACE_ONLY | 2:SIMPLE_OPTIMIZATIONS | 3:ADVANCED_OPTIMIZATIONS
  • 配列もハッシュも与えないと、選択肢以外の指定も可能になってしまう。
$ ruby optparse_test.rb --long=WHITESPACE_ONLY
{:long=>"WHITESPACE_ONLY"}

$ ruby optparse_test.rb --long=ABC
{:long=>"ABC"}
  • 配列を与えておくと、配列以外の文字列ではエラーになる。
$ ruby optparse_test.rb --long_array=WHITESPACE_ONLY
{:long_arrya=>"WHITESPACE_ONLY"}

$ ruby optparse_test.rb --long_array=ABC
optparse_test.rb:21:in `block in 
': invalid argument: --long_array=ABC (OptionParser::InvalidArgument) from optparse_test.rb:4:in `new' from optparse_test.rb:4:in `
'
  • さらに、選択肢を区別できる必要最小限の入力でも指定可能になる。
$ ruby optparse_test.rb --long_array=W
{:long_arrya_hash=>"WHITESPACE_ONLY"}
  • ハッシュを与えておくと、ハッシュのキーで指定可能となる。
$ ruby optparse_test.rb --long_hash=1
{:long_hash=>"WHITESPACE_ONLY"}
  • しかし、ハッシュの値ではエラーとなる。
  • もちろん、存在しないハッシュのキーでもエラーとなる。
$ ruby optparse_test.rb --long_hash=WHITESPACE_ONLY
optparse_test.rb:21:in `block in 
': invalid argument: --long_hash=WHITESPACE_ONLY (OptionParser::InvalidArgument) from optparse_test.rb:4:in `new' from optparse_test.rb:4:in `
' $ ruby optparse_test.rb --long_hash=4 optparse_test.rb:21:in `block in
': invalid argument: --long_hash=4 (OptionParser::InvalidArgument) from optparse_test.rb:4:in `new' from optparse_test.rb:4:in `
'
  • 配列とハッシュ両方を与えておくと、配列の値・ハッシュのキーで指定可能となる。
$ ruby optparse_test.rb --long_both=1
{:long_both=>"WHITESPACE_ONLY"}

$ ruby optparse_test.rb --long_both=WHITESPACE_ONLY
{:long_both=>"WHITESPACE_ONLY"}

オプション引数のパターン指定

  • オプション引数に許可するパターンを設定しておくこともできる。
  • 正規表現を与えることで、その正規表現にマッチする文字列のみ入力可能になる。
    • マッチしない場合はエラーになる。
  • 試しに、0から100までの数値のみにマッチする正規表現を与えてみると...
require 'optparse'

option={}
OptionParser.new do |opt|
  opt.on('--rate=VALUE', /\A\d{1,2}\Z|\A100\Z/, '0-100 Percentage.'){|v| option[:rate] = v}
  
  opt.parse!(ARGV)
end
p option
  • ヘルプ表示してみた。
$ ruby optparse_test.rb -h
Usage: optparse_test [options]
        --rate=VALUE                 0-100 Percentage.
  • 0から100までは指定可能。
  • 101以上はエラーになる。
$ ruby optparse_test.rb --rate=0
{:rate=>"0"}

$ ruby optparse_test.rb --rate=99
{:rate=>"99"}

$ ruby optparse_test.rb --rate=100
{:rate=>"100"}

$ ruby optparse_test.rb --rate=101
optparse_test.rb:7:in `block in 
': invalid argument: --rate=101 (OptionParser::InvalidArgument) from optparse_test.rb:4:in `new' from optparse_test.rb:4:in `
'

ヘルプ表示をデザインする

  • 以下の変数やメソッドを活用しながら、オプション定義しながら、ヘルプ表示をデザインするのだ。
インスタンス変数とデフォルト値
banner=nil, summary_width=32, summary_indent=' ' * 4, version=nil
  • インスタンス変数は、デフォルト値では満足できない時だけ、必要に応じて設定すればいい。
インスタンスメソッド
on, on_head, on_tail, separator
  • on, on_head, on_tailは、すべてほぼ同じ機能を持っている。
  • 若干の違いは、ヘルプ表示する時の表示順序の違い。
    • on_headは、on, separatorより先に表示される。
    • on_tailは、on, separatorより後に表示される。
  • よって、on_headとon_tailの中には、separatorを入れることは出来ない。
require 'optparse'

option={}
OptionParser.new do |opt|
  opt.banner = 'Usage: banner'
  opt.summary_width = 32
  opt.summary_indent = ' ' * 4
  opt.version = '1.0.0'
  
  opt.on_head('on_head 1行目 デフォルト値: banner = nil', 
              'on_head 2行目 デフォルト値: indent = ' ' * 4', 
              'on_head 3行目 デフォルト値: width  = 32', 
              'on_head 4行目 デフォルト値: version= nil')
  opt.separator('')
  opt.on('on 1行目 オプション定義しているonメソッドは、以下のindentとwidthで表示される', 
         'on 2行目 indentとwidthの設定は、自由に変更できる')
  opt.on('|ind|                               |')
  opt.on('|ent|width                          |description')
  opt.on('|<4>|<-------------32-------------->|')
  opt.on('-a', '--along', '1行目', 
                          '2行目', 
                          '3行目'){|v| option[:along] = v}
  opt.on('-b', '--blong', '1行目', 
                          '2行目', 
                          '3行目'){|v| option[:blong] = v}
  opt.separator('')
  opt.on('on 1行目 オプション定義しないonメソッドは、各文字列がインデントなしで表示される', 
         'on 2行目', 
         'on 3行目')
  opt.separator('')
  opt.on_tail('on_tail 1行目', 'on_tail 2行目', 'on_tail 3行目')
  
  opt.parse!(ARGV)
end
p option
  • ヘルプ表示してみた。
$ ruby optparse_test.rb -h # or --help
Usage: banner
on_head 1行目 デフォルト値: banner = nil
on_head 2行目 デフォルト値: indent =  * 4
on_head 3行目 デフォルト値: width  = 32
on_head 4行目 デフォルト値: version= nil

on 1行目 オプション定義しているonメソッドは、以下のindentとwidthで表示される
on 2行目 indentとwidthの設定は、自由に変更できる
|ind|                               |
|ent|width                          |description
|<4>|<-------------32-------------->|
    -a, --along                      1行目
                                     2行目
                                     3行目
    -b, --blong                      1行目
                                     2行目
                                     3行目

on 1行目 オプション定義しないonメソッドは、各文字列がインデントなしで表示される
on 2行目
on 3行目

on_tail 1行目
on_tail 2行目
on_tail 3行目
  • バージョン表示してみた。
$ ruby optparse_test.rb -v # or --version
optparse_test 1.0.0
  • version=nilではこうなる。
$ ruby optparse_test.rb -v # or --version
optparse_test: version unknown

シンプルなgetopts

  • オプションをハッシュに変換するだけで用が足りる時は、getoptsが便利。
  • optparseをrequireすると、ARGVにOptionParser::Arguableの機能が追加されるらしい。
ARGV.getopts('1文字オプション羅列', 'ロングオプション', 'ロングオプション', ...)
  • オプション引数を受け取る場合は、オプション名の次に:を書いておくのだ。
$ ruby -r optparse -e 'p ARGV.getopts("ab:cd:", "foo", "bar:")'
{"a"=>false, "b"=>nil, "c"=>false, "d"=>nil, "foo"=>false, "bar"=>nil}

$ ruby -r optparse -e 'p ARGV.getopts("ab:cd:", "foo", "bar:")' -- -a -b100 --foo --bar=abc
{"a"=>true, "b"=>"100", "c"=>false, "d"=>nil, "foo"=>true, "bar"=>"abc"}
  • ロングオプションには、デフォルト値も設定できる。
    • 残念ながら、1文字オプションには設定できないが...。
  • オプション名:に続けて、デフォルト値を書けばOK。
$ ruby -r optparse -e 'p ARGV.getopts("", "foo:", "bar:100")'
{"foo"=>nil, "bar"=>"100"}

$ ruby -r optparse -e 'p ARGV.getopts("", "foo:", "bar:100")' -- --bar=abc
{"foo"=>nil, "bar"=>"abc"}

*1:オプション引数の内容を想像できる変数名なら何でもいい。目的は代入するのではなく、説明するためである。