Rubyとopt parseライブラリでコマンドラインツールを作成する

# 想定読者
* rubyを触ったことがある
* rubyを触ったことがなくても、LL言語(perl, pythonなど)をある程度触っている

# ゴール
* rubyの標準ライブラリ`optparse`を使えるようになる
* 簡単なテキスト処理を書けるようになる

※ 今回の要点は上記の2点なので、rubyの細かな文法や、正規表現については詳細に解説しない

# 背景
いままで自分のツール用+軽く内輪に配る用では、perlを使って作成していた。
なんとなく、rubyをLL言語らしく使ってみたかったのと、よく比較されるperlやpythonと比べて、
どんな書き味なのかも気になったので、テキスト処理をするCLIをツールを題材に作成してみた。

# ruby
* まつもとゆきひろ(Matz)によって開発されたプログラミング言語
* 1993年に誕生し、1995年に公開が開始された
* 国産のプログラミング言語としては唯一、ISO/ICEとして承認されている
* 設計思想は"enjoy programming"

# optparse
* 標準で搭載されている、コマンドラインオプションを取り扱うライブラリ
* 使い方は簡単３ステップ
 1. OptionParser オブジェクト opt を生成する  `opt = OptionParser.new`   
 2. オプションを取り扱うブロックを opt に登録する `opt.on('-o', '-option', 'description of option') {|o|} options[:option] = o}`
 3. opt.parse(ARGV) でコマンドラインを実際に parse する `opt.parse(ARGV)`
 4. Usageなどのバナーは`on.banner = "Usage: usage of command"`で登録 
 5. helpは`puts 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
```

# コード

```ruby: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のテスト手法に従って、テストを書きたい