Stan Lo
Posted on July 29, 2021
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 😉
Posted on July 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.