A Sneak Peek of Ruby's New Debugger!

st0012

Stan Lo

Posted on July 29, 2021

A Sneak Peek of Ruby's New Debugger!

GitHub logo ruby / debug

Debugging functionality for Ruby

debug is Ruby's new debugger and will be included in Ruby 3.1. Since I've been both contributing to and using it for a while, I feel it's time to give you guys a sneak peek before its 1.0 release 🙂

(Since it's not officially released yet, any feature mentioned in this article could still be modified/removed in the released version)

(Update: The project's lead developer @ko1 has started a blog series about the debugger. Please also check it 😉)

Introduction

As I have mentioned, it's planned to be a standard library of Ruby 3.1. And currently, you can install it as a gem, like:

$ gem install debug --pre
Enter fullscreen mode Exit fullscreen mode

or

# Gemfile
# it's under active development, so I suggest using GitHub as source when possible
gem "debug", github: "ruby/debug" 
Enter fullscreen mode Exit fullscreen mode

Functionality-wise, debug is similar to the famous GDB debugger and Ruby's byebug gem. It provides a rich set of debug commands and has some unique features.

Quoted from its README:

New debug.rb has several advantages:

-   Fast: No performance penalty on non-stepping mode and non-breakpoints.
-   Remote debugging: Support remote debugging natively.
    -   UNIX domain socket
    -   TCP/IP
    -   VSCode/DAP integration (VSCode rdbg Ruby Debugger - Visual Studio Marketplace)
-   Extensible: application can introduce debugging support with several ways:
    -   By `rdbg` command
    -   By loading libraries with `-r` command line option
    -   By calling Ruby's method explicitly
-   Misc
    -   Support threads (almost done) and ractors (TODO).
    -   Support suspending and entering to the console debugging with `Ctrl-C` at most of timing.
    -   Show parameters on backtrace command.
Enter fullscreen mode Exit fullscreen mode

And these are my favorite features:

  • It's colorized.

Colorize Example

  • When showing backtrace with the backtrace command, it also shows method arguments, block arguments, and the return value.
=>#0    Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
  #1    block {|ten=10|} in second_call at target.rb:8
Enter fullscreen mode Exit fullscreen mode
  • It's possible to script your debug commands with binding.break and reduce manual operations. (See the combinations section for examples)
  • There are several commands to set breakpoints that trigger under different conditions, like break, catch, and watch.

binding.break (alias: binding.b)

If you're a heavy pry user like me, you can use a familiar binding.break (or just binding.b) to kick off the debug session as usual.

But binding.break is actually more powerful than binding.pry, because it can take commands!

For example:

  • binding.b(do: "catch CustomException") - debugger will execute the command (catch customExeption) and continue the program.
  • binding.b(pre: "catch CustomException") - debugger will execute the command (catch customExeption) and stop at the line.

(To execute multiple commands, use ;; as the separator: "cmd1 ;; cmd2 ;; cmd3")

Fequently Used Commands

The new debugger has many powerful commands. And here are the ones I use the most:

break (alias: 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
Enter fullscreen mode Exit fullscreen mode

Basic Usages

  • b A#foo - stops when b1.foo, b2.foo, and c.foo is called
  • b A.bar - stops when B.bar and C.bar is called
  • b B#foo - stops when b1.foo and b2.foo is called
  • b B.bar - stops when B.bar is called
  • b b1.foo - stops when b1.foo is called

Commands

  • b b1.foo do: cmd - executes cmd when b1.foo is called but doesn't stop
  • b b1.foo pre: cmd - executes cmd when b1.foo is called and stops

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
Enter fullscreen mode Exit fullscreen mode
  • catch StandardError - stops when any instance of StandardError is raised, including FooException and BarException
  • catch FooException - stops when FooException is raised

backtrace (alias bt)

Example Output:

=>#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
Enter fullscreen mode Exit fullscreen mode
  • bt - shows all frames on the stack
  • bt 10 - only shows the first 10 frames
  • bt /my_lib/ - only shows the frames with path that matches my_lib

outline (alias ls)

Similar to the ls command in irb or pry.

binding.b + Command Combinations

binding.b(do: "b Foo#bar do: bt")

It allows you to inspect a method call's backtrace without touching the method definition or typing commands manually.

Script:

binding.b(do: "b Foo#bar do: bt")

class Foo
  def bar
  end
end

def some_method
  Foo.new.bar
end

some_method
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

binding.b(do: "b Foo#bar do: info")

It allows you to inspect a method's environment (e.g. argument) when called:

Script:

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
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

I'm a Rails developer, so I usually put the combination code at the beginning of a controller action, like:

class SomeController < ApplicationController
  def index
    binding.b(pre: "b User#buggy_method do: info")
    # other code
  end
end
Enter fullscreen mode Exit fullscreen mode

And then the debugger would execute the command and/or stops at the method I expected.
I don't need to jump between multiple files for adding binding.pry or puts anymore 😎

A Small Drawback

However, the new debugger isn't all perfect (yet). Unlike in byebug or pry, you can't directly evaluate a Ruby expression in the debug session:

(rdbg) 1 + 1
unknown command: 1 + 1
Enter fullscreen mode Exit fullscreen mode

To evaluate an expression, you need to use p or pp command:

(rdbg) p 1 + 1
=> 2
Enter fullscreen mode Exit fullscreen mode

But according to the project's maintainer @ko1's 'comment, expression evaluation may be supported before the official 1.0 release.

Update

With https://github.com/ruby/debug/pull/227 being merged, this problem doesn't exist anymore 😉

Final Thoughts

Although it's not officially released yet, I've started using it at work daily. And I believe it'll soon become an must-have tool in every Rubyists' toolbox. So if you're curious about its capability, I encourage to give it a try 😉

💖 💪 🙅 🚩
st0012
Stan Lo

Posted on July 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related