For a software engineer, even the basic use of a debugger can save a lot of pain: adding breakpoints (places in the code the program will stop at and expose the current context) is very easy, and navigating from one breakpoint to another isn't difficult either.
And with just that, you can say goodbye to a program's many puts
and runs. Just add one or more breakpoints and run your program. Then you're able to access not only the variables and objects you might have thought of, but also anything accessible from that point in the code.
In this article, we'll focus on pry-byebug
, a gem that adds debugging and stack navigation to pry
using byebug
. We will see how to set up and use pry-byebug
, how it integrates with Ruby programs, and a few advanced techniques.
Let's get started!
Set Up pry-byebug
for Ruby
The setup is really simple. Just add the pry-byebug
gem to your Gemfile, and then run bundle install
. I'd advise you to add it to the development
and test
groups to debug tests as well.
Let's now cover some simple debugging using breakpoints.
Basic Debugging with Breakpoints
The whole principle of debuggers is to get rid of printf debugging
by giving you direct access to a running program's stack. So, we need to add breakpoints to tell the interpreter where to stop.
Let's take the example of a simple bookstore with software that manages the titles it offers.
class Book
attr_accessor :title, :author, :price
def initialize(title, author, price)
@title = title
@author = author
@price = price
end
end
class BookStore
def initialize
@books = []
end
def add_book(book)
@books << book
end
def remove_book(title)
@books.delete_if { |book| book.title == title }
end
def find_by_title(title)
@books.find { |book| book.title.include?(title) }
end
end
# Sample Usage:
store = BookStore.new
book1 = Book.new("Dune", "Frank Herbert", 20.0)
book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
store.add_book(book1)
store.add_book(book2)
store.add_book(book3)
puts store.find_by_title("Hobbit").title
This program is simple enough. Of course, there is no storage, but that's not needed to demonstrate what we want to do.
The last call will probably list "The Hobbit", but that's not certain and might not be the one we want. To debug this, it's best to "jump" into the find_by_title
method.
So, add the following lines to the top of the file:
require 'pry'
require 'pry-byebug'
We will also add a breakpoint to the method using binding.pry
:
def find_by_title(title)
binding.pry
@books.find { |book| book.title.include?(title) }
end
Let's then launch the program and get to the breakpoint:
From: test-pry-byebug/app.rb:29 BookStore#find_by_title:
27: def find_by_title(title)
28: binding.pry
=> 29: @books.find { |book| book.title.include?(title) }
30: end
[1] pry(#<BookStore>)>
The first thing you should see is that we know exactly where we are: there's no need to guess which part of the program that line in STDOUT comes from. Of course, we have only one breakpoint here, but you can easily understand how useful this might be if many breakpoints are used.
We can then check the value of the title
variable. There likely isn't much to be surprised by here. The issue is with the use of find
(instead of select
, or something more complex) to find and return a list of books instead of just one book.
By being right in the context, you can experiment with both find
and select
and decide on a course of action.
Here might be a good point to reflect on the expected behavior of the program you are building and this piece of code. It might be good to clarify what the code should do through RSpec tests.
Once you are ok with that step or want to see what's happening until the next breakpoint, type continue
, and the program execution will start again.
Making Small Steps from Breakpoints
With pry-byebug
, you can take smaller steps from breakpoints to see what happens after each line (particularly helpful for multiple calls using a method). Once you have stepped into the program and are at a breakpoint, use the next
command to execute the next line.
Let's take the case of adding a breakpoint within the add_book
method just before the first three books are created in the bookstore:
def add_book(book)
binding.pry
@books << book
end
store = BookStore.new
binding.pry
book1 = Book.new("Dune", "Frank Herbert", 20.0)
book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
Upon execution, the program will first stop just before the book1
variable is instantiated. We might want to know the result of that call. To do so, we don't have to stop the program and add another breakpoint. We just have to type next
in the pry console and hit Enter
.
From: test-pry-byebug/app.rb:37 :
32: end
33:
34: # Sample Usage:
35: store = BookStore.new
36: binding.pry
=> 37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
40:
[1] pry(main)> next
From: test-pry-byebug/app.rb:38 :
33:
34: # Sample Usage:
35: store = BookStore.new
36: binding.pry
37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
=> 38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
We then have access to book1
.
[1] pry(main)> book1
=> #<Book:0x00007fad37f964d8 @author="Frank Herbert", @price=20.0, @title="Dune">
This beats a lot of back-and-forth insertion and tweaking puts
calls.
Even Smaller Steps
We might want to step into the initialize
method (called through new
) to see more details. We can do so by using step
instead of next
.
From: test-pry-byebug/app.rb:8 Book#initialize:
7: def initialize(title, author, price)
=> 8: @title = title
9: @author = author
10: @price = price
11: end
[1] pry(#<Book>)> title
=> "The Hobbit"
This allows us to step into a method when it's called rather than skip over it as we do with next
.
Free from puts
By now, you should see how this approach saves time and frees us from using puts
or its equivalent to debug a program. Right from the debugger shell, we can look into the values of objects and variables to figure out why a program behaves like it does. Furthermore, you can save a lot of time from the shell by adding breakpoints on the fly, thus avoiding the "quit, move binding.pry
, restart" loop.
Finishing a Debugging Session
You can call continue
to go to the next breakpoint or the end of a program. finish
executes and finishes the current step, returning just after that step. In the previous example, we end up at the following line:
[2] pry(#<Book>)> finish
From: test-pry-byebug/app.rb:39 :
34: # Sample Usage:
35: store = BookStore.new
36: binding.pry
37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
=> 39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
Finally, !!!
exits the debugger directly.
Advanced Use Cases of pry-byebug
for Ruby
Now let's dive into a few more advanced use cases, including rewinding and replaying, adding breakpoints on the fly, and conditional breakpoints.
Rewinding and Replaying
Can we move back and forth between two points? Yes, totally. Given our previous case, we could move up and down from the initialize
method to its call. Just use up
and down
commands after step
to get into the method.
From: test-pry-byebug/app.rb:37 :
34: # Sample Usage:
35: store = BookStore.new
36: binding.pry
=> 37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
[1] pry(main)> step
From: test-pry-byebug/app.rb:8 Book#initialize:
7: def initialize(title, author, price)
=> 8: @title = title
9: @author = author
10: @price = price
11: end
[1] pry(#<Book>)> up
From: test-pry-byebug/app.rb:37 :
35: store = BookStore.new
36: binding.pry
=> 37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
[1] pry(main)> down
From: test-pry-byebug/app.rb:8 Book#initialize:
7: def initialize(title, author, price)
=> 8: @title = title
9: @author = author
10: @price = price
11: end
If we need details about the stack we are in, we can call the backtrace
method:
[1] pry(#<Book>)> backtrace
--> #0 Book.initialize(title#String, author#String, price#Float) at test-pry-byebug/app.rb:8
ͱ-- #1 Class.new(*args) at test-pry-byebug/app.rb:37
#2 <main> at test-pry-byebug/app.rb:37
From: /mnt/data/Code/Pier22/courses/raw-courses/rspec/git-repo/test-pry-byebug/app.rb:8 Book#initialize:
7: def initialize(title, author, price)
=> 8: @title = title
9: @author = author
10: @price = price
11: end
The list of frames is in the stack at the top, numbered from 0 to 2. A little cursor shows us which frame we are currently in.
Add Breakpoints On the Fly
To avoid additional debugger exits, we can add breakpoints on the fly from the debugger console, using the break
command.
You can add breakpoints in the current file or another file, to a specific line or the start of a specific method.
For example, let's add a breakpoint to the start of the find_by_title
method of the BookStore
class:
[2] pry(main)> break BookStore#find_by_title
Breakpoint 2: BookStore#find_by_title (Enabled)
28: def find_by_title(title)
29: @books.find { |book| book.title.include?(title) }
30: end
To add it to line 38 of the current file:
[1] pry(main)> break 38
Breakpoint 1: /mnt/data/Code/Pier22/courses/raw-courses/rspec/git-repo/test-pry-byebug/app.rb @ 38 (Enabled)
35: binding.pry
36: book1 = Book.new("Dune", "Frank Herbert", 20.0)
37: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
=> 38: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
39:
40: store.add_book(book1)
A call to break
lists all the breakpoints in place:
[4] pry(main)> break
# Enabled At
-------------
1 Yes /mnt/data/Code/Pier22/courses/raw-courses/rspec/git-repo/test-pry-byebug/app.rb @ 38
2 Yes BookStore#find_by_title
Note that they are numbered, so you can actually delete them using the --delete
argument to break
:
[4] pry(main)> break --delete 1
Conditional Breakpoints
You might want conditional breakpoints, too.
Imagine that your code only misbehaves if a user
object's role
attribute is set to :admin
.
Well, you might want to add the following breakpoint, and a condition to trigger it so that the debugger only stops if that condition is set:
[1] pry(main)> break BookStore#add_book if book.title.match /Dune/
Breakpoint 1: BookStore#add_book (Enabled) Condition: book.title.match /Dune/
Using continue
, the debugger executes the addition of Dune
without stopping, until the next call to add_book
.
This is a great feature that will save you a lot of time.
Wrapping Up
By now, you should have fewer reasons to rely on printf()
debugging when facing issues with your Ruby code. You now know how to:
- Install
pry-byebug
and some of its plugins - Add breakpoints from your favorite code editor with
binding.pry
- Start a debugger session (starting the app)
- Look at the value of variables and objects from the debugger session (simply calling the object or variable from the debugger console)
- Navigate within the execution frames from the debugger console (with
up
,down
,next
, andframe
) - Add breakpoints on the fly from the debugger console
- Add conditional breakpoints from the debugger console (with
break ClassName#method_name if var.nil?
, for example) - List and remove breakpoints (with
break
,break --delete <number>
, andbreak --disable-all
) - Conclude your debugging session with
finish
,continue
, or!!!
.
Finally, you can also configure pry-byebug
through the ~/.pryrc
file to add aliases and a few custom behaviors. See the main pry-byebug README for more details.
pry-byebug
is a tool that will help you a lot over the years, saving you time and headaches. It's definitely worth practicing its use so that you can confidently open it the next time you're unsure how your code is behaving.
Happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
P.P.S. Use AppSignal for Ruby for deeper debugging insights.
Top comments (0)