RipperTutorial.TokenStreamInterface

2009-01-30 03:58:14 +0900 (3011d); rev 7

Ripper のトークン指向インターフェイスを 利用したプログラムの作例を示します。

ruby2html.rb

第一段階として、Ruby スクリプトを HTML の <span> でタグ付けするプログラムを示します。 すべてのトークンをタグ付けしても構いませんが、 今回はコメントと文字列だけを <span> でくくることにしました。

ではまずプログラムを見てください。

require 'ripper'
require 'cgi'

class Ruby2HTML < Ripper::Filter
  def on_default(event, tok, f)
    f << CGI.escapeHTML(tok)
  end

  def on_comment(tok, f)
    f << %Q[<span class="comment">#{CGI.escapeHTML(tok)}</span>]
  end

  def on_tstring_beg(tok, f)
    f << %Q[<span class="string">#{CGI.escapeHTML(tok)}]
  end

  def on_tstring_end(tok, f)
    f << %Q[#{CGI.escapeHTML(tok)}</span>]
  end
end

if $0 == __FILE__
  Ruby2HTML.new(ARGF).parse($stdout)
end

今回の主役は Ripper::Filter クラスです。 Ripper::Filter クラスはイベントドリブン インターフェイスを持つクラスなので、 まず Ripper::Filter を継承したクラスを作り、 このクラスに適当にイベントハンドラを定義していきます。

イベントハンドラは「on_XXX」という名前のメソッドです。 「XXX」には "comment" (コメント) や "tstring_beg" (文字列開始) "ident" (識別子) などが入ります。 イベント名のリストは Ripper::SCANNER_EVENTS に格納されています。

今回使うのは on_comment と on_tstring_beg、それに on_tstring_end です。 この三つのメソッドをオーバーライドし、タグ付けを行います。

イベントハンドラメソッドには二つの引数があります。 第一引数は問題のトークンで、第二引数はユーザデータです。 この第二引数は Enumerable#inject の引数と似ていて、 イベントハンドラの値が次のイベントハンドラの第二引数へと、 次々に渡されます。 一番最初のハンドラの引数は Ripper::Filter#parse の引数です。

               Ripper::Filter#parse(data)
                                     ↓
result = Ripper::Filter#on_XXX(tok, data)
  |
  +----------------------------------+
                                     ↓
result = Ripper::Filter#on_XXX(tok, data)
  |
  +----------------------------------+
                                     ↓
result = Ripper::Filter#on_XXX(tok, data)
  |
  +----------------------------------+
                                     ↓
result = Ripper::Filter#on_XXX(tok, data)
  ↓

ちなみに最後のハンドラの値は Ripper::Filter#parse 全体の値になります。

それから、Ripper::Filter で便利なのが on_default イベントです。 このメソッドは、明示的に拾ったイベント以外の すべてのイベントをまとめて渡してくれます。 つまりこの場合ならば、on_comment と on_tstring_beg/end だけが 専用メソッドに渡り、残りのイベントはすべて on_default に渡ります。 ですから、特別な処理をしたいイベントだけ明示的に拾い、 あとは on_default で拾えば、すべてのイベントに対応できるわけです。

Q and A

どのようなイベントが発生するのか?
Ripper::SCANNER_EVENTS にリストされている名前の イベントが発生します。
実際にトークンがどう区切られるのか知りたい
後述する Ripper.lex などを使って調査してください。
どのイベントがどのタイミングで発生するのか、完全な仕様を知りたい
作者でもわかってないことをどうやって書けと言うんですか?

調査に利用できるメソッド

Ripper::Filter に代表されるトークン指向インターフェイスの 挙動を調べるときに役立つメソッドを紹介しておきます。

Ripper.tokenize

Ripper.tokenize(str) → [String]
Ruby プログラム str をトークンに分割し、 そのリストを返す。

使用例

require 'ripper'
p Ripper.tokenize("def m(a) nil end")
    #=> ["def", " ", "m", "(", "a", ")", " ", "nil", " ", "end"]

Ripper.tokenize は最も単純なトークンストリーム API です。 特に説明することはありません。

Ripper.tokenize は空白やコメントも含め、 元の文字列にある文字は 1 バイトも残さずに分割しますが、 そのごく僅かな例外として、__END__ 以降の文字列は黙って捨てられます。 これは現在のところ仕様と考えてください。

Ripper.lex

Ripper.lex(str) → [((Integer,Integer), Symbol, String)]
Ruby プログラム str をトークンに分割し、 そのリストを返す。ただし tokenize と違い、 トークンの種類と位置情報が付帯する。

使用例

require 'ripper'
require 'pp'

pp Ripper.lex("def m(a) nil end")
    #=> [[[1, 0], :on_kw, "def"],
         [[1, 3], :on_sp, " "],
         [[1, 4], :on_ident, "m"],
         [[1, 5], :on_lparen, "("],
         [[1, 6], :on_ident, "a"],
         [[1, 7], :on_rparen, ")"],
         [[1, 8], :on_sp, " "],
         [[1, 9], :on_kw, "nil"],
         [[1, 12], :on_sp, " "],
         [[1, 13], :on_kw, "end"]]

Ripper.lex は分割したトークンを詳しい情報とともに返します。 返り値の配列の要素は 3 要素の配列 (概念的にはタプル) です。 その内訳を以下に示します。

位置情報 (Integer,Integer)
トークンが置かれている行 (1-origin) と桁 (0-origin) の 2 要素の配列。
種類 (Symbol)
「:on_XXX」の形式で表される、トークンの種類。
トークン (String)
トークン文字列。

system revision 1.162