LoginSignup
2
3

More than 5 years have passed since last update.

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

Posted at

想定読者

  • 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ステップ
    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

コード

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3