DEV Community

Cover image for A Sneak Peek of Ruby's New Debugger!
Stan Lo
Stan Lo

Posted on • Updated on

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 😉

Latest comments (3)

Collapse
 
svoop profile image
Sven Schwyn • Edited

Hi Stan, nice work! I'm giving it a spin (instead of pry) on a pet project which parses third party documents and opens a debug console when something goes wrong – to speed up adapting the parser on upstream changes. With pry, I've used pry-rescue for this. Is there a way to do something similar with the new debugger? I've tried to add binding.b(do: "catch StandardError") but that doesn't do the trick. Or maybe an equivalent to Pry.start of sorts?

Collapse
 
naftali_marcus_3429f62547 profile image
Naftali Marcus

Do you expect this to be used in production or only in development and testing environments?

Collapse
 
st0012 profile image
Stan Lo

I suppose only in dev and testing environments.
The debugger "listens" to your program (e.g. whether you started a new thread) once it's required, so it'll more or less cause some extra computation even if you don't enter any debug session. The performance impact may not be noticeable in dev, but you probably don't want to try it in production.
However, if you only require it manually outside the web server process (e.g. a Rails console), that may be fine (I haven't tried it so can't be 100% sure).