Posted at

Ruby: 色付き pp

More than 3 years have passed since last update.


はじめに

pp は オブジェクトを人間に見やすく出力してくれます。

しかし、色付けはされません。

(拙作 prolog_parser.rb (参考)はオブジェクトを pp で出力しています)

$ ruby prolog_parser.rb 'f(x,y) :- a(b(c(d,e,f),!),[1,2,X|Y])'

{:":-"=>
[{:f=>[:x, :y]},
{:a=>[{:b=>[{:c=>[:d, :e, :f]}, :!]}, {[1, 2, {nil=>:X}]=>[{nil=>:Y}]}]}]}

複雑なオブジェクト出力だと、ちょっとツラいです。

そこで、色付き pp が欲しくなりました。


Ripper

Ripper は Ruby プログラムを解析するためのライブラリです。

字句解析を行うメソッド Ripper::Lexer#lex を試してみます。


t.rb

require 'pp'

require 'ripper'

pp Ripper.lex $<.read


: お試し(その1)

$ ruby t.rb < t.rb
[[[1, 0], :on_ident, "require"], # 出力情報は[位置情報,シンボル,値]
[[1, 7], :on_sp, " "],
[[1, 8], :on_tstring_beg, "'"],
[[1, 9], :on_tstring_content, "pp"],
[[1, 11], :on_tstring_end, "'"],
[[1, 12], :on_nl, "\n"],
[[2, 0], :on_ident, "require"],
[[2, 7], :on_sp, " "],
[[2, 8], :on_tstring_beg, "'"],
[[2, 9], :on_tstring_content, "ripper"],
[[2, 15], :on_tstring_end, "'"],
[[2, 16], :on_nl, "\n"],
[[3, 0], :on_ignored_nl, "\n"],
[[4, 0], :on_ident, "pp"],
[[4, 2], :on_sp, " "],
[[4, 3], :on_const, "Ripper"],
[[4, 9], :on_period, "."],
[[4, 10], :on_ident, "lex"],
[[4, 13], :on_sp, " "],
[[4, 14], :on_gvar, "$<"],
[[4, 16], :on_period, "."],
[[4, 17], :on_ident, "read"],
[[4, 21], :on_nl, "\n"]]

: お試し(その2)

$ echo ':a' | ruby t.rb
[[[1, 0], :on_symbeg, ":"], [[1, 1], :on_ident, "a"], [[1, 2], :on_nl, "\n"]]

: お試し(その3)

$ echo ':A' | ruby t.rb
[[[1, 0], :on_symbeg, ":"], [[1, 1], :on_const, "A"], [[1, 2], :on_nl, "\n"]]

: お試し(その4)

$ echo ':":-"' | ruby t.rb
[[[1, 0], :on_symbeg, ":\""],
[[1, 2], :on_tstring_content, ":-"],
[[1, 4], :on_tstring_end, "\""],
[[1, 5], :on_nl, "\n"]]


Racc

Ruby コードのシンタクスハイライトをするコマンドを作って、コマンドラインでフィルタする戦略にしようと思います。

Racc で簡単なパーサを作ってみます。

ここでは、シンボルリテラルを緑色に色付けするパーサにします。

シンボルリテラルは上の「お試し(その2〜4)」のパターンです。


try.ry

class HighlightParser

rule
program : { result = "" }
| program token { result << val[1] }

token : ANY
| symbol { result = color(val, GREEN) }
| ON_IDENT
| ON_CONST
| ON_TSTRING_CONTENT
| ON_TSTRING_END
| ON_SYMBEG

symbol : ON_SYMBEG ON_IDENT { result = val.join }
| ON_SYMBEG ON_CONST { result = val.join }
| ON_SYMBEG ON_TSTRING_CONTENT ON_TSTRING_END { result = val.join }
end

---- header
require 'pp'
require 'ripper'

---- inner
GREEN = 32

def parse(src)
selection = %i(on_symbeg on_ident on_const on_tstring_content on_tstring_end)
conv = -> sym { selection.include?(sym) ? sym.upcase : :ANY }

@q = Ripper.lex(src).map {|token| token.drop(1) } # 位置情報は捨てる
.map {|sym,val| [conv.(sym), val] } # 興味あるトークン以外は ANY に変換する
.reverse # pop で取れるように反転
#pp @q

do_parse
end

private
def next_token
@q.pop
end

def color(val, code) # 色付けは、ANSIエスケープシーケンスで画面制御
"\e[#{code}m#{val.join}\e[m"
end

---- footer
if __FILE__ == $0
puts HighlightParser.new.parse ARGF.read
end


: Racc ソースのコンパイル

$ racc -o try.rb try.ry # try.rb ができあがる
3 shift/reduce conflicts # これは「シフト/還元衝突」の警告です。ここでは無視...

実行。

いろいろ作りが粗いですが、これはこれで OK。

...しかし、ふと気付いたことが。。。


Pry::ColorPrinter

ところで、pry では pp が色付けされます。

もしや、と思い調べたら Pry::ColorPrinter というのがありました。

早速、トライ。


try-color-printer.rb

require 'pry'

obj = [:foo,1,{true=>(10..20)},"hello"]

Pry::ColorPrinter.pp obj


pry で、こんなことができたとは知りませんでした。

自作パーサのように自分でプログラミングする必要なくていいです。


おわりに

結論。

Pry::ColorPrinter が便利そうなんで、これからは使ってみようと思います。

(パーサ作る前に気づきたかった...)


本稿内容の動作確認は以下の環境で行っています。


  • Ruby 2.1.5 p273