Advanced Debugging with Pry
This tutorial helps broaden your basic knowledge of debugging by providing you with a different approach and tool for debugging in Ruby.
Before we begin I will like us to establish some common ground. A basic understanding of the following is required:
Introduction
Pry was and is still being developed by John Mair (project founder; aka banisterfiend; twitter; blog), Conrad Irwin and Ryan Fitzgerald. It is also aided by a healthy number of contributors.
Simply put, Pry is IRB on steroids. With Pry, you get all the features of IRB, plus some more. Besides being able to intercept sessions and perfom REPL-like actions in your terminal, you are also able to use your command shell integrations.
Pry can do quite a number of things; however, for the sake of this tutorial, we will focus on using pry to debug.
To set up pry on your machine, run the gem install pry
command. This would fetch pry
from rubygems.org
Basic Usage
As mentioned above, you can use pry to intercept your session and evaluate each method call during the interception ,
Here we have a class, Greet
, that greets people in several languages as shown below:
class Greet
def self.english(username)
"Hello #{username}"
end
def self.spanish(username)
"Holla #{username}"
end
def self.deutch(username)
"Hallo #{usernme}"
end
def self.aragonese(username)
"ola #{username}"
end
end
You probably noticed that we have a typo in the Deutch method.
If we try to get Deutch greeting by calling the method using Greet.deutch("John Doe")
, you'll get an undefined local variable or method 'usernme'
error.
If we put pry here, we would be able to evaluate each method individually.
In order to use pry, we must put it into the file we're working with and "bind" it at the specific point we want to intercept.
In this case, we will have
require 'pry';
class Greet
def self.english(username)
"Hello #{username}"
end
def self.spanish(username)
"Holla #{username}"
end
def self.deutch(username)
binding.pry
"Hallo #{usernme}"
end
def self.aragonese(username)
"ola #{username}"
end
end
When we run the file again using the Deutch Greeting, Greet.deutch("John Doe")
, a pry session is initiated. We would be able to access all the methods and arguments in the class.
From: /Users/Ruby/tutorial.rb @ line 15 Greet.deutch:
14: def self.deutch(username)
=> 15: binding.pry
16: "Hallo #{usernme}"
17: end
# Checking the argument being passed to the method
[1] pry(Greet)> username
=> "John Doe"
# Making method calls in pry
[2] pry(Greet)> aragonese(name)
=> ola John Doe
# Running individual blocks of code
# In an attempt to debug our error we will run the content of your `deutch` method
[3] pry(Greet)> "Hallo #{usernme}"
NameError: undefined local variable or method `usernme' for Greet:Class
If we look closely we'll find that we have argument name and the one in our code block dont match
We will simply fix this error by adding the missing character in our code block
[4] pry(Greet)> "Hallo #{username}"
=> Hallo John Doe
The application of this are not limited to method calls or accessing variables alone. To exit your pry session, you can enter the command exit
or ctrl + D
.
Doing More with Pry
What if we could inspect all the local variables in our code snippet within our pry session?
From: /Users/Ruby/tutorial.rb @ line 15 Greet.deutch:
14: def self.deutch(name)
=> 15: binding.pry
16: end
# lets see the methods and local variables available
# We do this by running the 'ls' command
[1] pry(Greet)> ls
Greet.methods: aragonese deutch english spanish
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ _pry_ name
## Then we run the 'cd' command against our name variable
[2] pry(Greet)> cd name
And we run the 'ls' command to see all the methods for our name variable whose data type is a string
[3] pry("John Doe"):1> ls
Comparable#methods: < <= > >= between?
String#methods:
% [] capitalize! chr downcase encode gsub intern next! rindex setbyte squeeze! sum to_str unicode_normalized?
* []= casecmp clear downcase! encode! gsub! length oct rjust shellescape start_with? swapcase to_sym unpack
+ ascii_only? center codepoints dump encoding hash lines ord rpartition shellsplit strip swapcase! tr upcase
<< b chars concat each_byte end_with? hex ljust partition rstrip size strip! to_c tr! upcase!
<=> bytes chomp count each_char eql? include? lstrip prepend rstrip! slice sub to_f tr_s upto
== bytesize chomp! crypt each_codepoint force_encoding index lstrip! replace scan slice! sub! to_i tr_s! valid_encoding?
=== byteslice chop delete each_line freeze insert match reverse scrub split succ to_r unicode_normalize
=~ capitalize chop! delete! empty? getbyte inspect next reverse! scrub! squeeze succ! to_s unicode_normalize!
self.methods: __pry__
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ _pry_
It's a lot right? With the above, you do not neccesarily have to know all of Ruby's string methods by heart. However, I recommend you to look into each one of the methofs to know when and when not to use them. Ruby Doc is very useful in this regard. Kudos to you if you already know all these methods by heart! I honestly wouldnt mind buying you coffee if you're in the neighbourhood!
You can use the approach above to look into the internals of both your File and Exception classes.
To do that, we must modify our pry session for this hedgecase:
From: /Users/Ruby/tutorial.rb @ line 15 Greet.deutch:
14: def self.deutch(name)
=> 15: binding.pry
16: end
# We will trigger an exception by attempting to divide a string by an integer
[1] pry(Greet)> name/2
NoMethodError: undefined method `/' for "John Doe":String
from (pry):1:in `deutch'
# This exception is stored in the _ex_ local variable we saw above.
# We can access the exception message at any point in time by looking into the variable
[2] pry(Greet)> _ex_.message
=> "undefined method `/' for \"John\Doe":String"
# Then we can run a backtrace also using the _ex_ variable
[3] pry(Greet)>_ex_.backtrace
["(pry):1:in `deutch'",
"/Users/Admin/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/pry-0.10.4/lib/pry/pry_instance.rb:355:in `eval'",
"/Users/Admin/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/pry-0.10.4/lib/pry/pry_instance.rb:355:in `evaluate_ruby'",
"/Users/Admin/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/pry-0.10.4/lib/pry/pry_instance.rb:323:in `handle_line'",
....
The above is extremely useful when you're trying to debug complex projects and scripts. You can also check last exception message by using the wtf
command in pry. The backtrace will help seek out the origination of your bug.
Using pry in Loops
Imagine a simple method that takes an array argument and outputs a hash with the names of students and an assigned serial number to each students.
Such a method will look like the one we have below:
def student_serial(i)
new_hash = {}
i.each_with_index do |value, index|
new_hash["#{value}"] = index
end
new_hash
end
Using the conditional, you can specify at what point you want to intercept your code and initiate a pry session.
def student_serial(i)
new_hash = {}
i.each_with_index do |value, index|
# we will add a conditional pry to
# start a session when our value is nil
binding.pry if value.nil?
new_hash["#{value}"] = index
end
new_hash
end
We only covered some of the basic things pry can do — imagine how much more you can accomplish when you use Pry with Rails.
Yes, pry is awesome, but it doesn't just end here! In the second part of this series, we will combine pry with steering and pry with rails.