フィルタコマンドを作るときなどに Better Errors の様な感じで使えるREPLが欲しくなって、 ライブラリを作ってみたことがあります(Rubyの例外終了時に自動でREPLを起動する)。 *1
その時は、set_trace_funcのオーバーヘッドを避けるためにStandardError#initializeをCレベルで定義して その中でbindingを取得する*2という ややトリッキーな実装をしていたのですが、Ruby 2.0でset_trace_funcの高速版といえるTracePointが導入されたのでそちらを使って書き直してみました。
主な変更点は2点。
gem installしてrequireするだけで利用できます(当然、1.9では動きません)。
$ gem install dexc $ cat t.rb def m(obj) obj.to_s + 1 end m(0) $ ruby -rdexc t.rb 0:lib/dexc.rb:82> tp.enable TracePoint#enable: false 1:lib/dexc.rb:83> end Dexc#start: false 2:lib/ruby/2.0.0/rubygems/core_ext/kernel_require.rb:45> return gem_original_require(path) Kernel#gem_original_require: true 3:lib/ruby/2.0.0/rubygems/core_ext/kernel_require.rb:45> return gem_original_require(path) Kernel#require: true 4:t.rb:0> IO#set_encoding: #<File:t.rb (closed)> 5:t.rb:0> IO#set_encoding: #<File:t.rb (closed)> 6:t.rb:1> def m(obj) Module#method_added: nil 7:t.rb:2> obj.to_s + 1 Fixnum#to_s: "0" 8:t.rb:2> obj.to_s + 1 Exception#initialize: #<TypeError: no implicit conversion of Fixnum into String> 9:t.rb:2> obj.to_s + 1 Class#new: #<TypeError: no implicit conversion of Fixnum into String> 10:t.rb:2> obj.to_s + 1 Exception#exception: #<TypeError: no implicit conversion of Fixnum into String> 11:t.rb:2> obj.to_s + 1 Exception#backtrace: nil TypeError: no implicit conversion of Fixnum into String from t.rb:2:in `+' from t.rb:2:in `m' from t.rb:4:in `<main>' From: t.rb @ line 2 Object#m: 1: def m(obj) => 2: obj.to_s + 1 3: end [1] pry(main)> obj => 0 [2] pry(main)> hist[7] # or dexc_hist[7] => "0"
例外が起きた環境でREPL(例ではPry)が起動されています。 また、`hist'を使うことで例外発生直前の一連のメソッドの返り値を取得できることが分かります。
なお、トレースが取れるようになって便利になった反面パフォーマンスは低下しました。 参考までに、Rubyに含まれているテスト(test/ruby)を走らせた時の所要時間を比較すると以下のようになります。
条件 | 所要時間 |
---|---|
dexcなし | 2:25.83 |
dexcあり(トレース無効(:raiseのみ)) | 2:28.90 |
dexcあり(トレース有効) | 5:34.07 |