optparseを使ってRubyでカレンダープログラムを作ってみた

# はじめに
`calコマンド`は、Linuxの基本的なコマンドで、カレンダーを表示するコマンドです。デフォルトでは今月の1ヶ月分のカレンダーが表示され、オプションを指定すると年や月などを指定することができます。

[【 cal 】コマンド――カレンダーを表示する](https://atmarkit.itmedia.co.jp/ait/articles/1701/11/news016.html)

今回は、こちらをrubyのプログラムを実行して同様の結果を得られるようにしたいと思います。

また、**optparse**を使ってオプションを指定できるようにして指定した月のカレンダーを表示できるようにします。



# optparseとは？
Rubyのoptparseライブラリは、コマンドラインオプションの解析をサポートするためのものです。このライブラリを使用すると、コマンドラインツールやアプリケーションでオプションとその引数を簡単に扱うことができます。

以下参考記事です
[library optparse - Ruby3.2リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/library/optparse.html#argv)

optparseを使う場合は以下の流れになります。

1. OptionParserオブジェクト `opt` を生成する
2. オプションを取り扱うブロックをoptに登録する
3. opt.parse(ARGV)でコマンドラインを実際にparseする


「**parseする**」とは、特定の構文や形式を持つ文字列やデータを読み取り、それをプログラムが利用しやすい形式やデータ構造に変換することを意味します。今回の文脈では、コマンドライン引数として与えられた文字列を読み取って、それをRubyのデータ構造やオブジェクトに変換するという意味になります。

# 簡単なサンプル
以下のサンプルでオプションを定義して実行してみます
```ruby:sample.rb
require 'optparse'

opt = OptionParser.new
opt.on("-m") {|v| p v}

# パターン①
opt.parse(ARGV)
p ARGV 

# パターン②
opt.parse!(ARGV)
p ARGV 
```

実行結果は以下のようになります(ARGVの中身が異なります)

```shell
$ ruby sample.rb -m hoge
true
["-m", "hoge"] #オプションと引数がARGVに格納されている(ARGVは変更されない)
true
["hoge"] #オプションが削除され残りの引数だけがARGVに格納されている(ARGVは変更されている)
```



`opt.parse(ARGV)`あるいは、`opt.parse!(ARGV)` の行で実際にコマンドライン引数を解析(parse)しています

:::note info
今回は挙動の違いを比較するため、2通りの書き方で実行結果を表示しています。
:::

具体的には、`ARGV`という配列に格納されたコマンドライン引数をもとに、**事前に定義されたオプション**(今回の場合だと`-m`)を認識し、後続のブロックの内容が実行されます。

`opt.parse!(ARGV)`は、ARGVを直接変更します。指定されたオプションはARGVから削除され、残りの引数だけがARGVに格納され残ります。そのため、破壊的な挙動となります。

上記のコードで`opt.on("-m") {|v| p v}`と定義した場合は、`-m`オプションは引数を取らないように定義されているので、`-m`オプションが存在するかどうかだけが評価されます。そのため**true**が返っています(ARGV配列にhogeが格納されたままになっている)

また、以下のように`opt.on("-m value") {|v| p v}`としてやると、`-m`オプションの後に指定された引数(hoge)が後続のブロック内のvに渡されてhogeが表示されます(`OptionParser`は`-m`と`hoge`の両方をオプションとして解析するので、ARGV配列からこれらの要素が取り除かれ空になります)



```ruby:sample.rb
require 'optparse'

opt = OptionParser.new
opt.on("-m value") {|v| p v}

# パターン①
opt.parse(ARGV)
p ARGV 

# パターン②
opt.parse!(ARGV)
p ARGV 
```

実行結果は以下のようになります

```shell
$ ruby sample.rb -m hoge
"hoge"
["-m", "hoge"] 
"hoge"
[] #オプションとその引数の両方が取り除かれ、ARGV配列は空になります
```

# カレンダープログラムの作成

実行するプログラムは以下になります

```ruby:calendar.rb
require 'date'
require 'optparse'

def print_calendar(year, month)
    # 指定された月の1日と最後の日を取得
    first_day = Date.new(year, month, 1)
    last_day = Date.new(year, month, -1)

    # カレンダーのヘッダーを表示
    puts "    #{month}月 #{year}    "
    puts "日 月 火 水 木 金 土"

    # 月の1日が始まる位置までスペースを表示
    print "   " * first_day.wday

    # 月の各日を表示
    (first_day..last_day).each do |date|
        print date.day.to_s.rjust(2) + " "
        # 土曜日なら改行
        puts if date.saturday?
    end

    puts
end

# コマンドラインオプションの解析
options = {}
opt = OptionParser.new
opt.on("-m month", Integer) { |month| options[:month] = month } # 処理が1行なので{}のブロック記法を採用
opt.parse!(ARGV)

# 現在の年と月を取得
now = Date.today
year = now.year

# 指定があった月、何も入力がなければ現在の月
month = options[:month] || now.month 

# 月のバリデーション(入力値が1から12の整数以外だとエラーメッセージを表示させる)
if month < 1 || 12 < month
    puts "#{month} is neither a month number (1..12) nor a name"
    exit
end

print_calendar(year, month)
```


以下が実行結果になります(calコマンドと同じ実行結果になってます)

```zsh
$ cal     
      8月 2023         
日 　月 　火 　水 　木 　金 　土  
       1  2  3  4  5  
 6  7  8  9 10 11 12  
13 14 15 16 17 18 19  
20 21 22 23 24 25 26  
27 28 29 30 31        
                      
$ ruby calendar.rb        
    8月 2023    
日 　月 　火 　水 　木 　金 　土
       1  2  3  4  5 
 6  7  8  9 10 11 12 
13 14 15 16 17 18 19 
20 21 22 23 24 25 26 
27 28 29 30 31 
```

引数指定した実行結果になります

```zsh
$ cal -m 6
      6月 2023         
日 　月 　火 　水 　木 　金 　土  
             1  2  3  
 4  5  6  7  8  9 10  
11 12 13 14 15 16 17  
18 19 20 21 22 23 24  
25 26 27 28 29 30     
                      
$ ruby calendar.rb -m 6
    6月 2023    
日 　月 　火 　水 　木 　金 　土
             1  2  3 
 4  5  6  7  8  9 10 
11 12 13 14 15 16 17 
18 19 20 21 22 23 24 
25 26 27 28 29 30 
```

## Point

```ruby
options = {}
opt = OptionParser.new
opt.on("-m month", Integer) { |month| options[:month] = month } 
opt.parse!(ARGV)
```
+ `options`は、オプションの値を保存するためのハッシュで、`opt`にOptionParserの新しいインスタンスを作成して格納します
+ 3行目のブロック内で、`-m`オプションに対する処理を定義します。このオプションの値を正数として受け取り、options[:month]にセットします(今回、引数の入力値は**1から12の整数**として制限しています)
+ また、`opt.on`の第二引数として`Integer`などの**入力値の型**を指定する事もでき、今回の場合だと入力値が整数じゃないとエラーになります
+ `opt.parse!(ARGV)`で、コマンドラインの引数を解析し、定義されたオプションに対応する処理を実行します(今回だと上記のブロック内の処理を指します)


```ruby
opt.on("-m month", Integer) { |month| options[:month] = month }
```
上記のブロックの記法で`{}`を使用していますが、`do~end`でも書き換え可能です

```ruby
opt.on("-m month", Integer) do |month|
    options[:month] = month
end
```

# 参考記事
[library optparse - Ruby3.2リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/library/optparse.html#argv)
[Rubyのoptparseについて掘り下げてみる](https://akng-engineer.hatenablog.com/entry/2021/12/02/225959)