あきろぐ

いろいろめもするよ🐈🐈🐈

Rubyのoptparseについて掘り下げてみる

これは、「フィヨルドブートキャンプ Part 2 Advent Calendar 2021」2日目の記事です。

adventar.org

part1もあるので、こちらもどうぞ!

adventar.org

はじめに

何かしらのプログラミング言語を使ってコマンドを実装する場合、付属するオプションを自前で解析するのは時間がかかりますよね。 Rubyではオプション解析を良い感じにやってくれるライブラリが用意されており、それがoptparseです。

スクリプトを書くときにoptparseを何回か使ったことがあるのですが、いまいち理解しきれていないと感じているので、この機会にoptparseライブラリについて少し深掘りしたいと思います。

optparseとは

  • Rubyには、最初から数多くのライブラリが標準ライブラリとして用意されている
  • 特に使用頻度が高いものは組み込みライブラリとして提供されているため、requireしなくても呼び出すことが可能
  • optparseは標準ライブラリの1つで、コマンドラインのオプションを取り扱うために使われる
  • optparseを使うときは基本的に以下のステップを踏む
    • OptionParserオブジェクトoptを生成
    • オプションを扱うブロックを登録
    • opt.parse(ARGV)でコマンドラインをパースする

基本的な使い方

まずは、Rubyのリファレンスに記載されているサンプルコードを使って挙動を確認していきます。

require 'optparse'

# create opt object
opt = OptionParser.new

# register options
opt.on('-a') {|v| p v }
opt.on('-b') {|v| p v }

# check arguments
p ARGV

# parse
opt.parse!(ARGV)
p ARGV

実行結果

$ ruby sample.rb -a foo -b bar
true
true
["-a", "foo", "-b", "bar"]
["foo", "bar"]

onメソッドは、optオフジェクトに登録するオプションを定義しています。ARGVにはコマンドラインで渡されたオプション、引数が配列として渡されます。パースすることで引数だけ取り出すことができます。

ARGVからオプションを取り除く必要がない場合は、破壊的メソッドは使わずにopt.parse(ARGV)すればOKです。

上記のサンプルプログラムでは、オプションa,bを定義しましたが、ヘルプオプションhはデフォルトで定義されています。

$ ruby sample.rb -h
["-h"]
Usage: sample [options]
    -a
    -b

ここから、このサンプルプログラムをベースに少しずつ拡張していきます。

ロングオプションを定義する

例えば、ショートオプションだけでななく、ロングオプションを定義したい場合は、onメソッドでオプション定義するときにショートオプションと一緒にロングオプションとして渡したい値を指定します。

以下のように指定することで、ショート、ロングバージョンのどちらでもオプションを指定できるようになります。

require 'optparse'
opt = OptionParser.new

opt.on('-a', '--all') {|v| p v }
opt.on('-b', '--before') {|v| p v }
opt.parse(ARGV)
p ARGV
$ ruby sample.rb --all foo --before bar
true
true
["--all", "foo", "--before", "bar"]

ヘルプを見てみてもショート、ロングバージョンのどちらも表示されていることがわかります。

$ ruby sample.rb -h
Usage: sample [options]
    -a, --all
    -b, --before

オプションの説明を追記する

ヘルプを表示したときに使えるオプションだけではなく、そのオプションの説明を追加したいと思います。 その場合は、ロングオプション同様にonメソッドに説明文を渡してあげればOKです。

require 'optparse'
opt = OptionParser.new

opt.on('-a', '--all', 'description:a') {|v| p v }
opt.on('-b', '--before', 'description:b') {|v| p v }
opt.parse(ARGV)
p ARGV

ヘルプオプションを指定すれば説明文が表示されるようになります。

$ ruby sample.rb -h
Usage: sample [options]
    -a, --all                        description:a
    -b, --before                     description:b

どのオプションが指定されたか記憶する

OptionParserはどのオプションが指定されたかは記憶していません。そのため、どのオプションがコマンドラインで渡されてきたか覚えておく必要がある場合は、コンテナに格納しておきます。

require 'optparse'
opt = OptionParser.new

option = {}
opt.on('-a', '--all', 'description:a') {|v| option[:a] = v }
opt.on('-b', '--before', 'description:b') {|v| option[:b] = v }
opt.parse(ARGV)
p ARGV
p option

オプションが指定された場合は、コンテナにkey,valueが保存されます。オプションが指定されなかった場合は、nilが返ります。

$ ruby sample.rb -a foo -b bar
["-a", "foo", "-b", "bar"]
{:a=>true, :b=>true}

$ ruby sample.rb -a foo
["-a", "foo"]
{:a=>true}

オプションに引数が必須か否か指定する

引数を必須にする場合は、onメソッドでオプション登録するときに末尾に何かしら文字列を追加します。

require 'optparse'
opt = OptionParser.new

option = {}
# 引数なし
opt.on('-a', '--all', 'description:a') {|v| option[:a] = v }
# 引数必須
opt.on('-b VAL', '--before', 'description:b') {|v| option[:b] = v }
# 引数必省略可能
opt.on('-c [VAL]', '--ccc', 'description:c') {|v| option[:c] = v }
opt.parse!(ARGV)

引数必須な場合に引数を渡さないと以下のようなエラーが発生します。

$ ruby sample.rb -b
sample.rb:8:in `<main>': missing argument: -b (OptionParser::MissingArgument)

また、引数をオプションと一緒に渡すことで引数をそのままHashにkeyとvalueを格納できます。 引数なしのオプションには真偽値が返り、オプションb,cに引数を渡すと引数がHashに入ります。

$ ruby sample.rb -a -b test -c test
{:a=>true, :b=>"test", :c=>"test"}

受け取る引数のクラスを指定する

あるオプションの引数で受け取るクラスを限定したい場合は、以下のように記述します。

require 'optparse'
opt = OptionParser.new

option = {}
# 引数はIntegerのみ
opt.on('-a', '--all=N', Integer, 'description:a') {|v| option[:a] = v }
# 引数はStringのみ
opt.on('-b', '--before=S', String, 'description:b') {|v| option[:b] = v }
opt.on('-c [VAL]', '--ccc', 'description:c') {|v| option[:c] = v }
opt.parse!(ARGV)
p option

Intergerクラスを指定すると、文字列が引数として渡ってきたらエラーを返します。また、Stringクラスを指定すると、数値を引数に渡した場合でも文字列として受け取ります。

$ ruby sample.rb -a 1 -b aaa
{:a=>1, :b=>"aaa"}

$ ruby sample.rb -a aaa
sample3.rb:7:in `<main>': invalid argument: -a aaa (OptionParser::InvalidArgument)

$ ruby sample.rb -b 1
{:b=>"1"}

便利なARGVの機能

今まで紹介した方法でコマンドのオプションや引数を処理することが可能になりますが、もう少し楽にオプションや引数を扱えるのがOptionParser::Arguableモジュールのgetoptsメソッドです。 getoptsメソッド使うとワンラインでコードでオプションと引数を指定することができ、受け取ったものをHashとして受け取ることができます。

getoptsメソッドにはショートオプション、またはロングオプションを指定することができます。引数が必要なオプションにはコロンをオプションの後ろにつけます。オプションはopt.onのときと異なり、1つ1つ指定するのではなくまとめて記述するので、ぱっと見わかりずらいかと思いますが、オプションや引数をシンプルに扱うだけであればgetoptsメソッド使うととても楽になります。

require 'optparse'

# 引数なしのオプションa, 引数ありのオプションbを指定
params = ARGV.getopts("ab:")
p params
$ ruby sample2.rb -h
Usage: sample2 [options]
    -a
    -b VAL

$ ruby sample2.rb -a -b bbb
{"a"=>true, "b"=>"bbb"}

ロングオプションを使いたい場合も同様に引数が必要なオプションにはコロンをオプションの後ろにつけますが、ショートオプションのときのようにまとめてオプションは指定せず、分けて書きます。ショートオプションが不要な場合は、""のように空文字を渡してあげる必要があります。

require 'optparse'
# ショートオプションなし、ロングオプション引数なし or あり
params = ARGV.getopts("", "all", "before:")
p params
$ ruby sample2.rb -h
Usage: sample2 [options]
        --all
        --before=VAL

$ ruby sample2.rb --all --before 1
{"all"=>true, "before"=>"1"}

また、オプションのデフォルト値も指定することが可能です。

require 'optparse'
# ショートオプションなし、ロングオプション引数あり、デフォルト値あり
params = ARGV.getopts("", "all", "before:", "maxsize:1024")
p params
$ ruby sample2.rb -h
Usage: sample2 [options]
        --all
        --before=VAL
        --maxsize=1024

$ ruby sample2.rb
{"all"=>false, "before"=>nil, "maxsize"=>"1024"}

デフォルト値はコマンド実行時に上書きすることも可能です。

$ ruby sample2.rb --maxsize=100
{"all"=>false, "before"=>nil, "maxsize"=>"100"}

まとめ

実際に調べて使ってみた感想としては、こんな感じです。

  • optparse使うと自動的にヘルプオプションついてるの助かる(コマンドのバージョン表示オプションvもついてるらしい)
  • 引数クラス指定できるので、後の処理で引数のクラスどうこう考えなくて良いの助かる
  • 細かくオプション定義する必要がなければ、getoptsメソッドを使うとめっちゃ楽になる

なんとなく理解して使っていたライブラリも調べてみると新しい発見もあり理解度があがるので、また気になるライブラリ等あれば深掘りする時間を取ろうと思いました。

参考文献

docs.ruby-lang.org

docs.ruby-lang.org