Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rubyの新しいデバッガの機能を先行紹介(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。なお翻訳記事公開時点のデバッガバージョンはv1.0.0rc2になりました。

Rubyの新しいデバッガの機能を先行紹介(翻訳)

ruby/debug - GitHub

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には以下のような多くの利点がある。

  • 高速性: ノンステップモードやノンブレークポイントでパフォーマンスを損なわない
  • リモートデバッグをネイティブでサポート
  • 拡張性: 以下のようにさまざまな方法でアプリケーションにデバッグを導入可能
    • 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でデバッグコマンドをスクリプト化して手動操作を軽減できる(後述の組み合わせセクションのサンプルを参照)
  • breakcatchwatchなどのコマンドを用いてブレークポイントをさまざまな条件で起動できる

🔗 binding.break(エイリアス: binding.b

私のようなpryのヘビーユーザーなら、pryで慣れ親しんだbinding.breakbinding.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.foob2.fooc.fooが呼び出されると停止する

  • b A.bar: B.barC.barが呼び出されると停止する

  • b B#foo: b1.foob2.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: StandardErrorFooExceptionBarExceptionも含む)のインスタンスが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.pryputsを追加してファイルからファイルへジャンプする必要がなくなりました😎

🔗 小さな問題点(その後解消)

しかし新しいデバッガーは(まだ)完璧ではありません。byebugやpryのようにRubyの式を直接評価できません。

(rdbg) 1 + 1
unknown command: 1 + 1

式を評価するには、以下のようにpppを追加する必要があります。

(rdbg) p 1 + 1
=> 2

ただし本プロジェクトのメンテナーである@ko1コメントによると、1.0の正式リリースまでに式の評価機能をサポートするかもしれないとのことです。

続報

その後#227がマージされたおかげで、この問題は解消されました😉

最後に

新しいデバッガーはまだ公式にはリリースされていませんが、私は日々の業務で使い始めています。近い将来、すべてのRubyistのツールボックスに常備されると信じています。新しいデバッガーの機能に興味をお持ちでしたら、ぜひお試しください😉

関連記事

Ruby: 「オブジェクト指向トレース」とtapping_device gemで効率よくデバッグ(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。