想定読者
- rubyを触ったことがある
- rubyを触ったことがなくても、LL言語(perl, pythonなど)をある程度触っている
ゴール
- rubyの標準ライブラリ
optparse
を使えるようになる - 簡単なテキスト処理を書けるようになる
※ 今回の要点は上記の2点なので、rubyの細かな文法や、正規表現については詳細に解説しない
背景
いままで自分のツール用+軽く内輪に配る用では、perlを使って作成していた。
なんとなく、rubyをLL言語らしく使ってみたかったのと、よく比較されるperlやpythonと比べて、
どんな書き味なのかも気になったので、テキスト処理をするCLIをツールを題材に作成してみた。
ruby
- まつもとゆきひろ(Matz)によって開発されたプログラミング言語
- 1993年に誕生し、1995年に公開が開始された
- 国産のプログラミング言語としては唯一、ISO/ICEとして承認されている
- 設計思想は"enjoy programming"
optparse
- 標準で搭載されている、コマンドラインオプションを取り扱うライブラリ
- 使い方は簡単3ステップ
- OptionParser オブジェクト opt を生成する
opt = OptionParser.new
- オプションを取り扱うブロックを opt に登録する
opt.on('-o', '-option', 'description of option') {|o|} options[:option] = o}
- opt.parse(ARGV) でコマンドラインを実際に parse する
opt.parse(ARGV)
- Usageなどのバナーは
on.banner = "Usage: usage of command"
で登録 - helpは
puts opt
で出力することができる
- OptionParser オブジェクト opt を生成する
できたツール
こんなログファイルから、欲しいフィールドを取り出したり、情報を絞り込んだり、情報を追加し、標準出力へ出力する。
date Thu Apr 11 04:41:25 pm 2013
base hex timestamps absolute
internal events logged
// version 8.0.0
Begin Triggerblock Thu Apr 11 04:41:25 pm 2013
0.000000 Start of measurement
0.001316 CAN 1 Status:chip status error active
0.001399 1 1F3 Rx d 3 00 10 00 Length = 146000 BitCount = 77 ID = 499
0.002763 1 1E5 Rx d 8 4C 00 21 10 00 00 00 B9 Length = 228000 BitCount = 118 ID = 485
0.003009 1 710 Rx d 8 00 5F 00 00 00 00 13 BE Length = 238000 BitCount = 123 ID = 1808
0.003175 1 C7 Rx d 4 00 38 26 9B Length = 158000 BitCount = 83 ID = 199
0.003349 1 1CC Rx d 4 00 00 00 00 Length = 165883 BitCount = 87 ID = 460
0.003586 1 F9 Rx d 8 00 DA 40 33 D0 63 FF 1C Length = 228000 BitCount = 118 ID = 249
0.003738 1 1CF Rx d 3 00 00 05 Length = 144000 BitCount = 76 ID = 463
0.003976 1 711 Rx d 8 00 23 00 7E FF EB FC 6F Length = 230000 BitCount = 119 ID = 1809
0.004148 1 1D0 Rx d 4 00 00 00 00 Length = 164000 BitCount = 86 ID = 464
0.004382 1 C1 Rx d 8 30 14 F6 08 32 B4 F7 70 Length = 226000 BitCount = 117 ID = 193
0.004615 1 C5 Rx d 8 31 27 F8 44 32 B0 F8 5C Length = 224121 BitCount = 116 ID = 197
0.004825 1 BE Rx d 6 00 00 4D 00 00 00 Length = 202242 BitCount = 105 ID = 190
0.005051 1 D1 Rx d 7 80 00 BF FE 00 FE 00 Length = 218121 BitCount = 113 ID = 209
0.005292 1 C9 Rx d 8 80 2C 5A 60 00 00 18 00 Length = 232242 BitCount = 120 ID = 201
0.005538 1 1C8 Rx d 8 80 00 00 00 FF FE 3F FE Length = 238121 BitCount = 123 ID = 456
0.005774 1 18E Rx d 8 00 00 00 84 78 46 08 45 Length = 228242 BitCount = 118 ID = 398
- オプションなし
- 欲しいフィールドだけを出力する
$ruby canlogfilter.rb log.txt
0.001399 1 1F3 Rx 3 00 10 00
0.002763 1 1E5 Rx 8 4C 00 21 10 00 00 00 B9
0.003009 1 710 Rx 8 00 5F 00 00 00 00 13 BE
0.003175 1 C7 Rx 4 00 38 26 9B
0.003349 1 1CC Rx 4 00 00 00 00
0.003586 1 F9 Rx 8 00 DA 40 33 D0 63 FF 1C
0.003738 1 1CF Rx 3 00 00 05
0.003976 1 711 Rx 8 00 23 00 7E FF EB FC 6F
0.004148 1 1D0 Rx 4 00 00 00 00
0.004382 1 C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.004615 1 C5 Rx 8 31 27 F8 44 32 B0 F8 5C
0.004825 1 BE Rx 6 00 00 4D 00 00 00
0.005051 1 D1 Rx 7 80 00 BF FE 00 FE 00
0.005292 1 C9 Rx 8 80 2C 5A 60 00 00 18 00
0.005538 1 1C8 Rx 8 80 00 00 00 FF FE 3F FE
0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
-
--difftime
(-d
)オプション- 時間のフィールドから差分時間を算出し、左端のフィールドに付加する
$ruby canlogfilter.rb -d log.txt
0.000000 0.001399 1 1F3 Rx 3 00 10 00
0.001364 0.002763 1 1E5 Rx 8 4C 00 21 10 00 00 00 B9
0.000246 0.003009 1 710 Rx 8 00 5F 00 00 00 00 13 BE
0.000166 0.003175 1 C7 Rx 4 00 38 26 9B
0.000174 0.003349 1 1CC Rx 4 00 00 00 00
0.000237 0.003586 1 F9 Rx 8 00 DA 40 33 D0 63 FF 1C
0.000152 0.003738 1 1CF Rx 3 00 00 05
0.000238 0.003976 1 711 Rx 8 00 23 00 7E FF EB FC 6F
0.000172 0.004148 1 1D0 Rx 4 00 00 00 00
0.000234 0.004382 1 C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.000233 0.004615 1 C5 Rx 8 31 27 F8 44 32 B0 F8 5C
0.000210 0.004825 1 BE Rx 6 00 00 4D 00 00 00
0.000226 0.005051 1 D1 Rx 7 80 00 BF FE 00 FE 00
0.000241 0.005292 1 C9 Rx 8 80 2C 5A 60 00 00 18 00
0.000246 0.005538 1 1C8 Rx 8 80 00 00 00 FF FE 3F FE
0.000236 0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
-
--focus
(-f
)オプション- 特定のフィールドを持つ行のみを出力する
$ruby canlogfilter.rb -f 1CC,C1,1C8 log.txt
0.003349 1 1CC Rx 4 00 00 00 00
0.004382 1 C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.005538 1 1C8 Rx 8 80 00 00 00 FF FE 3F FE
-
--pass
(-p
)オプション- 特定のフィールドを持つ行を除いた行を出力する
$ruby canlogfilter.rb -p 1CC,C1,1C8 log.txt
0.001399 1 1F3 Rx 3 00 10 00
0.002763 1 1E5 Rx 8 4C 00 21 10 00 00 00 B9
0.003009 1 710 Rx 8 00 5F 00 00 00 00 13 BE
0.003175 1 C7 Rx 4 00 38 26 9B
0.003586 1 F9 Rx 8 00 DA 40 33 D0 63 FF 1C
0.003738 1 1CF Rx 3 00 00 05
0.003976 1 711 Rx 8 00 23 00 7E FF EB FC 6F
0.004148 1 1D0 Rx 4 00 00 00 00
0.004615 1 C5 Rx 8 31 27 F8 44 32 B0 F8 5C
0.004825 1 BE Rx 6 00 00 4D 00 00 00
0.005051 1 D1 Rx 7 80 00 BF FE 00 FE 00
0.005292 1 C9 Rx 8 80 2C 5A 60 00 00 18 00
0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
- オプションの組み合わせ(
--difftime
と--focus
,--pass
のいずれか)
$ruby canlogfilter.rb -d -f 1CC,C1,1C8 log.txt
0.000000 0.003349 1 1CC Rx 4 00 00 00 00
0.001033 0.004382 1 C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.001156 0.005538 1 1C8 Rx 8 80 00 00 00 FF FE 3F FE
-
--help
(-h
)
$ruby conlogfilter.rb -h
Usage: ruby canlogfilter.rb [-options] logfilename
-d, --difftime add difftime to leftside of realtime
-f, --focus ID1,ID2,ID3 print choiced id
-p, --pass ID1,ID2,ID3 print without choiced id
-h, --help show help
コード
canlogfilter.rb
# classes
class CliOption
require 'optparse'
def initialize
@options = {}
OptionParser.new do |o|
o.banner = "Usage: ruby canlogfilter.rb [-options] logfilename"
o.on('-d', '--difftime', 'add difftime to leftside of realtime') {|v| @options[:difftime] = v}
o.on('-f', '--focus ID1,ID2,ID3', Array, 'print choiced id') {|v| @options[:focusid] = v}
o.on('-p', '--pass ID1,ID2,ID3', Array, 'print without choiced id') {|v| @options[:passid] = v}
o.on('-h', '--help', 'show help') {|v| puts o; exit}
o.parse!(ARGV)
end
end
def has(name)
@options.include?(name)
end
def get(name)
@options[name]
end
def get_filename
ARGV[0]
end
end
class Function
def self.calc_difftime(current, last)
if last.to_f == 0 then
return 0
else
return current.to_f - last.to_f
end
end
def self.set_lasttime(logobj)
logobj.last = logobj.current
end
def set_matchdata(mdobj, logobj)
logobj.current = mdobj[1]
logobj.diff = Function.calc_difftime(logobj.current, logobj.last)
logobj.ch = mdobj[2]
logobj.id = mdobj[3]
logobj.dir = mdobj[4]
logobj.drv = mdobj[5]
logobj.dlc = mdobj[6]
logobj.data = mdobj[7]
Function.set_lasttime(logobj)
end
def compareids_with_array(id, arr)
for a in arr do
if id == a
return true
end
end
return false
end
def print_log_general(logobj)
puts "%3.6f %s %s %s %s %s" % [logobj.current, logobj.ch, logobj.id, logobj.dir, logobj.dlc, logobj.data]
end
def print_log_with_difftime(logobj)
puts "%3.6f %3.6f %s %s %s %s %s" % [logobj.diff, logobj.current, logobj.ch, logobj.id, logobj.dir, logobj.dlc, logobj.data]
end
end
# main
Log = Struct.new(:current, :last, :diff, :ch, :id, :dir, :drv, :dlc, :data)
log = Log.new(0.000000, 0.000000, 0.000000, 1, 0x000, "Rx", "d", "1", "00")
option = CliOption.new
function = Function.new
if(!option.get_filename)
STDERR.puts "Error:filename must be designated\n"
exit 1
end
if(option.has(:focusid) && option.has(:passid))
STDERR.puts "Error:--focus and --pass options cant' be using pararell\n"
exit 1
end
File.foreach(option.get_filename) do |line|
/^\s+(.{8,10})\s+(.{1})\s+(.{1,3}?)\s+(.{2})\s+(.{1})\s+(.{1})\s+((..\s){1,8})/ =~ line
if ($~ != nil)
if option.has(:focusid) then
if function.compareids_with_array($3, option.get(:focusid))
function.set_matchdata($~.to_a, log)
if option.has(:difftime) then
function.print_log_with_difftime(log)
else
function.print_log_general(log)
end
end
elsif option.has(:passid) then
if !function.compareids_with_array($3, option.get(:passid))
function.set_matchdata($~.to_a, log)
if option.has(:difftime) then
function.print_log_with_difftime(log)
else
function.print_log_general(log)
end
end
else
function.set_matchdata($~.to_a, log)
if option.has(:difftime) then
function.print_log_with_difftime(log)
else
function.print_log_general(log)
end
end
end
end
-
CliOption
クラス-
optparse
ライブラリを使用して、オプションを定義しコマンドライン引数をparseするクラス - 前述の解説の通り、クラスをnew()する際、3ステップでオプション定義と引数の定義を行う
- その他、必要なメソッドを実装
- オプションが存在するかを返す
has()
- オプションの中身を返す
get()
- オプションの後に渡されるコマンドライン引数を返す
get_filename()
-
-
Function
クラス- ツールに必要な機能を提供するクラス
-
calc_difftime()
ログを受ける構造体のフィールドから、差分時間を算出して返すメソッド -
set_lasttime()
構造体の現在時間の値を最終時間へと格納するメソッド -
set_matchdata()
正規表現にマッチしたオブジェクトから構造体のフィールドを設定するメソッド -
print_log_general()
--difftime
オプションが無いときのフォーマットで標準出力へ出力 -
print_log_withdifftime()
--difftime
オプションがあるときのフォーマットで標準出力へ出力
-
メイン処理
- Structクラスから、サブクラスを作成し、構造体を定義
- 上記のクラスのインスタンスを生成する
- File.foreach()メソッドでファイルから一行ずつ読みだす
- 正規表現にマッチした行のみ、オプションに従い出力する
まとめ・TODO
- まとめ
- perlとpythonが少しさわれるくらいの筆者でも、rubyは違和感なく書けて、ストレスフリーなプログラミングができた
- オブジェクト指向が初心者でも、rubyの中でのオブジェクト指向は、わかりやすいように感じた(時にString.to_fのようなメソッドの使い方において)
- TODO
- rubyの例外処理機構を使って、上手いエラー処理を書きたい
- rubyのテスト手法に従って、テストを書きたい