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
or
# Gemfile
# it's under active development, so I suggest using GitHub as source when possible
gem "debug", github: "ruby/debug"
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.
And these are my favorite features:
- It's colorized.
- 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
- 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
, andwatch
.
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
Basic Usages
-
b A#foo
- stops whenb1.foo
,b2.foo
, andc.foo
is called -
b A.bar
- stops whenB.bar
andC.bar
is called -
b B#foo
- stops whenb1.foo
andb2.foo
is called -
b B.bar
- stops whenB.bar
is called -
b b1.foo
- stops whenb1.foo
is called
Commands
-
b b1.foo do: cmd
- executescmd
whenb1.foo
is called but doesn't stop -
b b1.foo pre: cmd
- executescmd
whenb1.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
-
catch StandardError
- stops when any instance ofStandardError
is raised, includingFooException
andBarException
-
catch FooException
- stops whenFooException
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
-
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 matchesmy_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
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
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
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
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
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
To evaluate an expression, you need to use p
or pp
command:
(rdbg) p 1 + 1
=> 2
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 😉
Top comments (3)
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 toPry.start
of sorts?Do you expect this to be used in production or only in development and testing environments?
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).