はじめに
calコマンド
は、Linuxの基本的なコマンドで、カレンダーを表示するコマンドです。デフォルトでは今月の1ヶ月分のカレンダーが表示され、オプションを指定すると年や月などを指定することができます。
今回は、こちらをrubyのプログラムを実行して同様の結果を得られるようにしたいと思います。
また、optparseを使ってオプションを指定できるようにして指定した月のカレンダーを表示できるようにします。
optparseとは?
Rubyのoptparseライブラリは、コマンドラインオプションの解析をサポートするためのものです。このライブラリを使用すると、コマンドラインツールやアプリケーションでオプションとその引数を簡単に扱うことができます。
以下参考記事です
library optparse - Ruby3.2リファレンスマニュアル
optparseを使う場合は以下の流れになります。
- OptionParserオブジェクト
opt
を生成する - オプションを取り扱うブロックをoptに登録する
- opt.parse(ARGV)でコマンドラインを実際にparseする
「parseする」とは、特定の構文や形式を持つ文字列やデータを読み取り、それをプログラムが利用しやすい形式やデータ構造に変換することを意味します。今回の文脈では、コマンドライン引数として与えられた文字列を読み取って、それをRubyのデータ構造やオブジェクトに変換するという意味になります。
簡単なサンプル
以下のサンプルでオプションを定義して実行してみます
require 'optparse'
opt = OptionParser.new
opt.on("-m") {|v| p v}
# パターン①
opt.parse(ARGV)
p ARGV
# パターン②
opt.parse!(ARGV)
p ARGV
実行結果は以下のようになります(ARGVの中身が異なります)
$ ruby sample.rb -m hoge
true
["-m", "hoge"] #オプションと引数がARGVに格納されている(ARGVは変更されない)
true
["hoge"] #オプションが削除され残りの引数だけがARGVに格納されている(ARGVは変更されている)
opt.parse(ARGV)
あるいは、opt.parse!(ARGV)
の行で実際にコマンドライン引数を解析(parse)しています
今回は挙動の違いを比較するため、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配列からこれらの要素が取り除かれ空になります)
require 'optparse'
opt = OptionParser.new
opt.on("-m value") {|v| p v}
# パターン①
opt.parse(ARGV)
p ARGV
# パターン②
opt.parse!(ARGV)
p ARGV
実行結果は以下のようになります
$ ruby sample.rb -m hoge
"hoge"
["-m", "hoge"]
"hoge"
[] #オプションとその引数の両方が取り除かれ、ARGV配列は空になります
カレンダープログラムの作成
実行するプログラムは以下になります
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コマンドと同じ実行結果になってます)
$ 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
引数指定した実行結果になります
$ 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
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)
で、コマンドラインの引数を解析し、定義されたオプションに対応する処理を実行します(今回だと上記のブロック内の処理を指します)
opt.on("-m month", Integer) { |month| options[:month] = month }
上記のブロックの記法で{}
を使用していますが、do~end
でも書き換え可能です
opt.on("-m month", Integer) do |month|
options[:month] = month
end
参考記事
library optparse - Ruby3.2リファレンスマニュアル
Rubyのoptparseについて掘り下げてみる