optparseを噛み砕いて説明してみた

# 初めに

今回は、`Ruby`の`optparse`ライブラリについて自分なりに噛み砕いて説明していきます。
噛み砕いたところで公式となんら変わらないところは飛ばしたり、とてもさらっといきます。

まず、どういうものなのかのイメージを持ってもらうため、下記を見てください。
（※全て記載すると不要に場所を取るので削ってます）

```
ZumiAir ruby % ruby -h
Usage: ruby [switches] [--] [programfile] [arguments]
  -v              print the version number, then turn on verbose mode
  -w              turn warnings on for your script
  -W[level=2|:category]     set warning level; 0=silence, 1=medium, 2=verbose
  -x[directory]   strip off text before #!ruby line and perhaps cd to directory
  --jit           enable JIT for the platform, same as --mjit (experimental)
  --mjit          enable C compiler-based JIT compiler (experimental)
  -h              show this message, --help for more info
  ```

何かしら、ターミナルでコマンドを打つ場合にオプション指定できるものがほとんどだと思います。
例であげたものだと、`-h`を指定していますね。
これを自分が作成したプログラムにも楽に定義するためのライブラリになります。

いくつかの定義方法を解説していきますが、どれが一番直感的に使えるのかというと
[色々書いたけど簡単なのは](#色々書いたけど簡単なのは)
が個人的にはいいかなと思います。
とりあえずオプションを受け付けるようにしたい！という方はこの項を見ていただくとよいかなと。

## オプションを受け付けるようにしてみよう

オプションの登録ですが、`on`メソッドを使用します。
下記は、`-a`オプションが指定されて実行されるとtrue、なければfalseが表示される想定です。
```ruby:onメソッド
require 'optparse'
opt = OptionParser.new

opt.on('-a') {|v| p v}
opt.on('-b') {|v| p v}
```
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -a 1 2 -b 3
```
...何も表示されません。

[公式ドキュメント(optparse)](https://docs.ruby-lang.org/ja/latest/library/optparse.html "Ruby Document optparse")を見ると、

> Enumerator#each などと違い、OptionParser#on メソッドが呼ばれた時点ではブロックは実行されません。あくまで登録されるだけです。OptionParser#parse が呼ばれた時に、コマンドラインにオプションが指定されていれば実行されます。

とあります。

つまり、上記の例では`-a`や`-b`オプションの登録はされているけれど、`{|v| p v}`は実行されないということ。

では、`parse`メソッドを追記してみます。
```ruby:parseメソッド
require 'optparse'
opt = OptionParser.new

opt.on('-a') {|v| p v}
opt.on('-b') {|v| p v}
argv = opt.parse(ARGV)
p ARGV
p argv
```

```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -a 1 2 -b 3
true
true
["-a", "1", "2", "-b", "3"]
["1", "2", "3"]
```

期待通り表示されました！

ちなみに、定義していないオプションを指定すると例外が発生します。
普段使うコマンドでも無いオプションを指定すると怒られると思いますが、同じですね。
```:未定義オプション指定
ZumiAir ruby % ruby optparse_sample.rb -c
optparse_sample.rb:6:in `<main>': invalid option: -c (OptionParser::InvalidOption)
optparse_sample.rb:6:in `<main>': invalid option: c (OptionParser::InvalidOption)
```

そして、実はこの時点で`-h`オプションは使えます。
共通認識である、そのコマンドのヘルプが表示されます。
```:ヘルプオプション
ZumiAir ruby % ruby optparse_sample.rb -h
Usage: optparse_sample [options]
    -a
    -b
```
説明はありませんが、定義したオプションは表示されていますね。
後でこちらについても記載しています。

### オプションの順番
公式ドキュメントに下記の記載がある通り、直後である必要はありません。
> オプションの指定はコマンドの直後である必要はありません(上の例で、-b はオプションとして認識されている)。

これは、順番を入れ替えても認識してくれるということになります。
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -b 1 2 -a 3
true
true
["-b", "1", "2", "-a", "3"]
["1", "2", "3"]
```

しかし、次の文に
> ただし、環境変数 POSIXLY_CORRECT が定義してあるとこの挙動は変更されます。

とあります。
正直、何を言ってるかわかりませんでした。笑

自分がいくらか手元で試してわかったのは、
* 公式ドキュメントは`POSIXLY_CORRECT=1`で定義していたが、定義されているかどうかだけなので`1`の数字には特に意味はない。
* 実行時の１つ目の引数がオプションでなければ一つもオプションとして認識されない
    ```
    ZumiAir ruby % env POSIXLY_CORRECT='' ruby optparse_sample.rb a -b -a a -b test baz
    ["a", "-b", "-a", "a", "-b", "test", "baz"] ⇦ parse後
    ["a", "-b", "-a", "a", "-b", "test", "baz"] ⇦ parse前
    ```
* 実行時の１つ目の引数がオプションの場合、１つ目だけをオプションと認識する
    ```
    ZumiAir ruby % env POSIXLY_CORRECT='' ruby optparse_sample.rb -b a -b test baz
    ["a", "-b", "test", "baz"] ⇦ parse後
    ["-b", "a", "-b", "test", "baz"] ⇦ parse前
    ```
* 実行時の１つ目の引数がオプションの場合かつ、オプションが続く限りはオプションと認識し、オプション以外が入った時点以降からはオプションとして認識されない
    ```
    ZumiAir ruby % env POSIXLY_CORRECT='' ruby optparse_sample.rb -b -a a -b test baz
    ["a", "-b", "test", "baz"] ⇦ parse後
    ["-b", "-a", "a", "-b", "test", "baz"] ⇦ parse前
    ```

というのが確認できました。

使用頻度はおそらくほぼないので、`POSIXLY_CORRECT`を定義すると順番を意識する必要がある。
と認識しておく程度で良いと思います。

## どうやって値を取り出そうか

### 配列で取得する

先ほどの例に、しれっと[ARGV](https://docs.ruby-lang.org/ja/latest/method/Object/c/ARGV.html "Ruby Document ARGV")が登場しましたが、これは与えられた引数を表す配列です。
そのため、出力を見ると`ARGV`には引数に記載した全てが配列で格納されているのが見えますね。

また、次の行には`parse`メソッドで解析後の配列が格納されています。
オプションが取り除かれてますね。

配列になっているので、下記のように取り出すことはできます。
```ruby:配列から取り出す
require 'optparse'
opt = OptionParser.new

opt.on('-a') {|v| p v}
opt.on('-b') {|v| p v}
parse = opt.parse(ARGV)
p parse
arg = parse[1]
p arg
```

が、これでは使いにくいですよね。
簡単に期待通りの値が取れなくなりそうです。

それならば、明示的に別の入れ物に格納してあげましょう。

```ruby:明示的に格納
require 'optparse'
opt = OptionParser.new

params = {}
opt.on('-a') {|v| params[:a] = v}
opt.on('-b') {|v| params[:b] = v}
parse = opt.parse(ARGV)
p parse
p params
p params[:a]
```
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -a a_arg -b b_arg
["a_arg", "b_arg"]
{:a=>true, :b=>true}
true
```

これであれば、一定に各オプションの値を得られそうです。
ただ、現時点では定義したオプション指定の有無しか取れていません。

#### オプションの引数を取得する（必須）

非常に簡単に取得することができます。
「オプション定義時に末尾に何かしらを記述する」だけです。
```ruby:オプションの引数（必須）
require 'optparse'
opt = OptionParser.new

params = {}
opt.on('-a val') {|v| params[:a] = v}
opt.on('-b') {|v| params[:b] = v}
parse = opt.parse(ARGV)
p parse
p params
p params[:a]
```
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -a a_arg -b b_arg
["b_arg"]
{:a=>"a_arg", :b=>true}
"a_arg"
```

`-a`オプションには引数を取るように記述したことで、期待通り取得できています。

これは、引数が必須になる書き方なので、
```:-aの引数なし（後続のオプション有り）
ruby optparse_sample.rb -a -b b_arg
["b_arg"]
{:a=>"-b"}
"-b"
```
のように`-b`オプションのつもりで指定したものが`-a`オプションの引数として認識されます。
上記例は期待通りでないにしろ、`-a`オプションの引数があるとズレて認識されるので例外は発生していません。
```:-aの引数なし（後続のオプションなし）
ZumiAir ruby % ruby optparse_sample.rb -a         
optparse_sample.rb:7:in `<main>': missing argument: -a (OptionParser::MissingArgument)
```
のように代わりに取れるものもないと、例外が発生します。

#### オプションの引数を取得する（任意）

オプションの引数を任意（有無を問わない）とする場合、先ほどオプション定義時に末尾に記述したものを`[]`で囲みます。
```ruby:オプションの引数（任意）
opt.on('-a [val]') {|v| params[:a] = v}
```
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -a
[]
{:a=>nil}
nil
ZumiAir ruby % ruby optparse_sample.rb -a -b b_arg
["b_arg"]
{:a=>nil, :b=>true}
nil
```

すると、`-a`オプションに引数がなくても例外は発生せず`nil`となりました。
また`-a`、`-b`オプションを続けて記述するケースでも、`-a`オプションの引数は`nil`と正しく認識されています。

### ハッシュで取得する

先ほどは配列に取得した引数ですが、ハッシュで受け取ることもできます。
`parse`メソッドの引数に`into`オプションを指定することで自動でハッシュに格納してくれます。

```ruby:ハッシュに格納
require 'optparse'
opt = OptionParser.new

params = {}
opt.on('-a [val]') {|v| p v}
opt.on('-b', '--long-b') {|v| p v}
parse = opt.parse(ARGV, into: params)
p parse
p params
p params[:b]
```
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -a -b b_arg
nil
true
["b_arg"]
{:a=>nil, :"long-b"=>true}
true
```
こちらでは明示的に格納しているわけでなく、自動でハッシュへ格納しています。
ハッシュのキーはロングオプションが定義されていればロングオプションの値を、ショートオプションのみの場合はショートオプションの値が使われます。


## オプションの種類

### ロングオプション / ショートオプション
馴染みがあるところだと思いますので、さらっといきます。
```:ロングオプション
ZumiAir ruby % ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23]
```
```:ショートオプション
ZumiAir ruby % ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23]
```
のように、`--`で始まるものがロングオプション、`-`で始まるものがショートオプションです。

１つの機能にロング、ショートのオプションがあるというのが多くあるケースでは無いでしょうか。

### 否定型オプション
ロングオプションにだけ、`--`の後ろに`no-`を挟んでからオプション文字を入れると否定型オプションを定義することもできます。
```ruby:否定型オプション
require 'optparse'
opt = OptionParser.new

opt.on('--no-along') {|v| p v }
opt.on('-b', '--blong') {|v| p v }

p opt.parse(ARGV)
p ARGV
```
```:実行例
ruby % ruby no_option_sample.rb --no-along -b  
false
true
[]
["--no-along", "-b"]
```
指定されていると`false`を返すようになっています。

その後の処理で`false`で`で返ってきた方が都合がいい場合に使うのかな？とは思うのですが、処理側を工夫すれば回避できるケースが多いので、このようなややこしい指定は使わない方がいいのでは...と個人的には思います。

### ヘルプオプション

最初の方にも少し触れましたが、デフォルトで`-h`、`--help`は定義されています。

試しに、ここまでで記載してきたsampleを`-h`オプションをつけて実行してみます。
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -h
Usage: optparse_sample [options]
    -a [VAL]
    -b, --long-b
```

こちらも見慣れたものだと思いますが、何か足りない気がしますね。
並びとしては、
ショートオプション、ロングオプション、引数、説明文
と続きます。

はい、現状だとオプションの説明文がありません。
追加するには、`on`メソッドの引数に追加してあげます。
```ruby:ヘルプ説明文
require 'optparse'
opt = OptionParser.new

params = {}
opt.on('-a [val]', 'description') {|v| params[:a] = v}
opt.on('-b', '--long-b', '日本語でも書けるけど、英語が無難かな') {|v| params[:b] = v}
parse = opt.parse(ARGV)
```
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -h
Usage: optparse_sample [options]
    -a [val]                         description
    -b, --long-b                     日本語でも書けるけど、英語が無難か
```
このように、自由に説明文を追加できます。

### バージョンオプション

人に使ってもらうケースでもなければ特に要らない気はしますが、`-v`、`--version`オプションもデフォルトで定義されています。

しかし、何もしていないと
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -v    
optparse_sample: version unknown
```
と`unknown`になります。

トップレベルに`Version = 任意のVersion`を追加してあげると、
```:実行結果
ZumiAir ruby % ruby optparse_sample.rb -v
optparse_sample 2.1.1
```
のように表示されるようになります。

## 色々書いたけど簡単なのは

### ARGVの機能を使って定義、取得する
`optparse`ライブラリを`require`した際に`ARGV`が拡張されます。
その中に`getopts`メソッドがあり、これを利用するのが簡単です。

自動で指定されたオプションに応じて解析してくれて、結果をハッシュで返してくれます。

```ruby:ARGV.getopts
require 'optparse'

parse = ARGV.getopts('ab:', 'long-b:', 'default:1')
p parse
```
```:実行結果
ZumiAir ruby % ruby argv_sample.rb -a -b b_arg --long-b lb-arg
{"a"=>true, "b"=>"b_arg", "long-b"=>"lb-arg", "default"=>"1"}
```
ここまで解説した中で最も短く書けました！

`getopts`メソッドは、第一引数にショートオプション、第二引数以降はロングオプションを記述します。
ショートオプション１個しか作れないの？ということはなく、`abc`など連結して書くことで複数定義したことになります。

また、引数を取得するか否かについては`:`をオプション直後に付けます。
`a:bc:`と書けば、`a`と`c`オプションは引数が必要ということになります。

注意点としては、`[]`で囲い、任意にする記述方法はこちらには無いようです。

`--`以降をオプションと認識しないというのは同じになります。

## 補足

### ロングオプション、指定時に全部書かなくてもいい

例えば、`--lllong`と`--llong`のオプションがあったとします。
その時、どちらのオプションかの区別は３文字目が`l`か`o`かで判断できますのよね？

なので、この場合は`--lll`と指定すれば`--lllong`オプションを指定したことになります。
同様に、`--llo`と指定すれば`--llong`を指定したと認識してくれます。

## 最後に

ここまで読んでいただき、ありがとうございました！

公式ドキュメントのサブコマンドについても触れようかと思ったのですが、力尽きました。。