Rubyの新しいデバッガの機能を先行紹介(翻訳)
debugはRubyの新しいデバッガで、Ruby 3.1に同梱される予定です。近頃はこのデバッガにコントリビューションしつつ自分でも使ってきたので、1.0が正式にリリースされる前にそろそろ皆さんにいち早く紹介するときが来たと感じています。
(本記事執筆時点のdebugはまだ正式リリースではないため、本記事で紹介する機能は正式リリースまでに変更または削除される可能性があります)
(追記: 本プロジェクトのリード開発者@ko1氏が以下のデバッガ記事連載を始めましたので、こちらもチェックよろしくお願いします😉)
🔗 あらまし
上述のように、このデバッガはRuby 3.1の標準ライブラリになる予定です。現時点では以下のようにgemとしてインストールできます。
$ gem install debug --pre
または
# Gemfile
# 開発が活発なので、可能ならGitHubをソースとして指定するのがおすすめ
gem "debug", github: "ruby/debug"
debugは、機能面では有名なGDBデバッガやRubyのbyebug gemと似ています。豊富なデバッグコマンドの他に、独自の機能をいくつも備えています。
READMEより引用します。
debug.rbには以下のような多くの利点がある。
- 高速性: ノンステップモードやノンブレークポイントでパフォーマンスを損なわない
- リモートデバッグをネイティブでサポート
- UNIXドメインソケット
- TCP/IP
- VSCode/DAP統合(VSCode rdbg Ruby Debugger - Visual Studio Marketplace)
- 拡張性: 以下のようにさまざまな方法でアプリケーションにデバッグを導入可能
rdbg
コマンド- コマンドラインで
-r
オプションを指定- 明示的なRubyメソッド呼び出し
- その他
- スレッド(ほぼ完了)とRactor(ToDo)のサポート
- ほぼいつでも
Ctrl-C
でコンソールデバッグのオンオフを切り替え可能- バックトレースコマンドでのパラメータ表示
以下は私が気に入っている機能です。
- カラー表示
backtrace
コマンドでバックトレースを表示するとメソッドの「引数」「ブロック引数」「戻り値」も表示される
=>#0 Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
#1 block {|ten=10|} in second_call at target.rb:8
binding.break
でデバッグコマンドをスクリプト化して手動操作を軽減できる(後述の組み合わせセクションのサンプルを参照)break
やcatch
やwatch
などのコマンドを用いてブレークポイントをさまざまな条件で起動できる
🔗 binding.break
(エイリアス: binding.b
)
私のようなpryのヘビーユーザーなら、pryで慣れ親しんだbinding.break
(binding.b
だけでもよい)を用いてデバッグセッションを開始できます。
ただし、実際のbinding.break
には以下のようにコマンドを渡せるので、binding.pry
よりも強力です。
binding.b(do: "catch CustomException")
: デバッガーはcatch customExeption
コマンドを実行してプログラムを続行する-
binding.b(pre: "catch CustomException")
: デバッガーはcatch customExeption
コマンドを実行してその行で停止する
(コマンドを複数実行したい場合は"cmd1 ;; cmd2 ;; cmd3"
のように;;
で区切ります)
🔗 よく使うコマンド
新しいデバッガには強力なコマンドが多数搭載されています。その中でも私が最も多用しているものを以下に紹介します。
🔗 break
コマンド(エイリアス: b
)
class A
def foo; end
def self.bar; end
end
class B < A; end
class C < A; end
B.bar
C.bar
b1 = B.new
b2 = B.new
c = C.new
b1.foo
b2.foo
c.foo
基本の用法
b A#foo
:b1.foo
やb2.foo
やc.foo
が呼び出されると停止する-
b A.bar
:B.bar
やC.bar
が呼び出されると停止する -
b B#foo
:b1.foo
やb2.foo
が呼び出されると停止する -
b B.bar
:B.bar
が呼び出されると停止する -
b b1.foo
:b1.foo
が呼び出されると停止する
コマンド
-
b b1.foo do: cmd
:b1.foo
が呼び出されるとcmd
を実行する(ただし停止しない) -
b b1.foo pre: cmd
:b1.foo
が呼び出されるとcmd
を実行して停止する
🔗 catch
コマンド
class FooException < StandardError; end
class BarException < StandardError; end
def raise_foo
raise FooException
end
def raise_bar
raise BarException
end
raise_foo
raise_bar
-
catch StandardError
:StandardError
(FooException
やBarException
も含む)のインスタンスがraiseされると停止 -
catch FooException
:FooException
がraiseすると停止
🔗 backtrace
コマンド(エイリアス: bt
)
以下は出力例です。
=>#0 Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
#1 block {|ten=10|} in second_call at target.rb:8
#2 Foo#third_call_with_block(block=#<Proc:0x00007f9283101568 target.rb:7>) at target.rb:15
#3 Foo#second_call(num=20) at target.rb:7
#4 Foo#first_call at target.rb:3
#5 <main> at target.rb:23
bt
: スタックの全フレームを表示-
bt 10
: スタックの最初の10フレームのみを表示 -
bt /my_lib/
:my_lib
にマッチするパスのフレームのみを表示
🔗 outline
コマンド(エイリアス: ls
)
outline
コマンドは、irbやpryのls
コマンドと同様に使えます。
🔗 binding.b
とコマンドの組み合わせ
🔗 binding.b(do: "b Foo#bar do: bt")
この機能を使うと、メソッド定義に手を加えたりコマンドを手動で入力したりせずにメソッド呼び出しのバックトレースをinspectできます。
以下はスクリプト例です。
binding.b(do: "b Foo#bar do: bt")
class Foo
def bar
end
end
def some_method
Foo.new.bar
end
some_method
以下は出力です。
DEBUGGER: Session start (pid: 75555)
[1, 10] in target.rb
=> 1| binding.b(do: "b Foo#bar do: bt")
2|
3| class Foo
4| def bar
5| end
6| end
7|
8| def some_method
9| Foo.new.bar
10| end
=>#0 <main> at target.rb:1
(rdbg:binding.break) b Foo#bar do: bt
uninitialized constant Foo
#0 BP - Method (pending) Foo#bar do: bt
DEBUGGER: BP - Method Foo#bar at target.rb:4 do: bt is activated.
[1, 10] in target.rb
1| binding.b(do: "b Foo#bar do: bt")
2|
3| class Foo
=> 4| def bar
5| end
6| end
7|
8| def some_method
9| Foo.new.bar
10| end
=>#0 Foo#bar at target.rb:4
#1 Object#some_method at target.rb:9
# and 1 frames (use `bt' command for all frames)
Stop by #0 BP - Method Foo#bar at target.rb:4 do: bt
(rdbg:break) bt
=>#0 Foo#bar at target.rb:4
#1 Object#some_method at target.rb:9
#2 <main> at target.rb:12
🔗 binding.b(do: "b Foo#bar do: info")
メソッドが呼び出されたときのメソッドの環境(引数など)をinspectできます。
以下はスクリプト例です。
binding.b(do: "b Foo#bar do: info")
class Foo
def bar(a)
a
end
end
def some_method
Foo.new.bar(10)
end
some_method
以下は出力です。
DEBUGGER: Session start (pid: 75924)
[1, 10] in target.rb
=> 1| binding.b(do: "b Foo#bar do: info")
2|
3| class Foo
4| def bar(a)
5| a
6| end
7| end
8|
9| def some_method
10| Foo.new.bar(10)
=>#0 <main> at target.rb:1
(rdbg:binding.break) b Foo#bar do: info
uninitialized constant Foo
#0 BP - Method (pending) Foo#bar do: info
DEBUGGER: BP - Method Foo#bar at target.rb:4 do: info is activated.
[1, 10] in target.rb
1| binding.b(do: "b Foo#bar do: info")
2|
3| class Foo
4| def bar(a)
=> 5| a
6| end
7| end
8|
9| def some_method
10| Foo.new.bar(10)
=>#0 Foo#bar(a=10) at target.rb:5
#1 Object#some_method at target.rb:10
# and 1 frames (use `bt' command for all frames)
Stop by #0 BP - Method Foo#bar at target.rb:4 do: info
(rdbg:break) info
%self = #<Foo:0x00007fdac491c200>
a = 10
私はRails開発者なので、以下のようなコードをコントローラやアクションの冒頭に置いて使うことがよくあります。
class SomeController < ApplicationController
def index
binding.b(pre: "b User#buggy_method do: info")
# 他のコード
end
end
後はデバッガーでコマンドを実行するもよし、見たいメソッドでデバッガーを停止するもよしです。
おかげで、binding.pry
やputs
を追加してファイルからファイルへジャンプする必要がなくなりました😎
🔗 小さな問題点(その後解消)
しかし新しいデバッガーは(まだ)完璧ではありません。byebugやpryのようにRubyの式を直接評価できません。
(rdbg) 1 + 1
unknown command: 1 + 1
式を評価するには、以下のようにp
かpp
を追加する必要があります。
(rdbg) p 1 + 1
=> 2
ただし本プロジェクトのメンテナーである@ko1のコメントによると、1.0の正式リリースまでに式の評価機能をサポートするかもしれないとのことです。
続報
その後#227がマージされたおかげで、この問題は解消されました😉
最後に
新しいデバッガーはまだ公式にはリリースされていませんが、私は日々の業務で使い始めています。近い将来、すべてのRubyistのツールボックスに常備されると信じています。新しいデバッガーの機能に興味をお持ちでしたら、ぜひお試しください😉
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。なお翻訳記事公開時点のデバッガバージョンはv1.0.0rc2になりました。