Ruby Debugging William H. Mitchell Last Updated: Saturday, 03/10/18

Low-tech debugging in Ruby

A very simple tool for Ruby debugging is Kernel.p, the p method of the Kernel class. The expression p obj invokes the inspect method of the object obj and prints the resulting string.

Let's contrast p and puts:

>> s = "  a\ntest\there... "

>> puts s
  a
test    here... 
=> nil

>> puts [s, s.size.to_s]
  a
test    here... 
17
=> nil

>> p s
"  a\ntest\there... "

>> p [s, s.size.to_s]
["  a\ntest\there... ", "17"]
Note that inspect applies recursively—inspecting [s, s.size.to_s] causes s.inspect and s.size.to_s.inspect to be done.

Here's tac.rb, which we wrote in class, with a call to Kernel.p added at the bottom of the loop:

$ cat tac.rb
reversed = ""
while line = gets
    reversed = line + reversed
    p ["bottom of loop; l,r", line, reversed]
end
puts reversed
Here's an input file:
$ cat lines
line 1
second line
the end
Let's run it:
$ ruby tac.rb < lines
["bottom of loop; l,r", "line 1\n", "line 1\n"]
["bottom of loop; l,r", "second line\n", "second line\nline 1\n"]
["bottom of loop; l,r", "the end\n", "the end\nsecond line\nline 1\n"]
the end
second line
line 1
We see that the bottom of the loop was reached three times. The values of line and reversed are shown each time. The string "bottom of loop; l,r" is simply a label to show me where the output is coming from; the "l,r" reminds me that the values of line and reversed follow. I concisely aggregate them by putting them in an array.

I could produce nicer output with a puts like this,

puts "bottom of loop; line = #{line.inspect}, reversed = #{reversed.inspect}"
but throwing values of interest into an array with a string as a label is usually good enough for debugging.

I personally find that adding a p [...whatever...] in a place or two is sufficient for most of my debugging needs, especially if I've tested my methods individually.

byebug

byebug is a simple but useful Ruby debugger. There's a nice introduction to it section 16.4 in The Ruby Way, 3rd edition by Hal Fulton. Here's a link to that section on Safari.

One very handy byebug capability that Fulton doesn't mention is that if the text you enter at the (byebug) prompt isn't recognized as a byebug command, it's evaluated as a Ruby expression and the result is printed.

In the example below I first enter c 3, a byebug command to run to line three. Then I evaluate the expressions line, line.chomp, and line[-1].ord.

$ byebug tac.rb 

[1, 6] in /Users/whm/372/ruby/tac.rb
=> 1: reversed = ""
   2: while line = gets
   3:     reversed = line + reversed
   4:     # p ["bottom of loop", line, reversed]
   5: end
   6: puts reversed
(byebug) c 3
testing
Stopped by breakpoint 1 at /Users/whm/372/ruby/tac.rb:3

[1, 6] in /Users/whm/372/ruby/tac.rb
   1: reversed = ""
   2: while line = gets
=> 3:     reversed = line + reversed
   4:     # p ["bottom of loop", line, reversed]
   5: end
   6: puts reversed
(byebug) line
"testing\n"
(byebug) line.chomp
"testing"
(byebug) line[-1].ord
10

byebug vs. reading standard input

Programs that rely on redirection of standard input, like tac.rb and xfield.rb aren't handled well by byebug. With a command like

byebug tac.rb < somefile
byebug interprets the contents of somefile as byebug commands!

I haven't found a simple way to direct byebug to read commands from the console (i.e., /dev/tty) instead of standard input, but here's a technique that seems to work:

  1. Add code at the top of the program to look for an environment variable named, for example, stdin. If found, open the file it names, and assign the File object to the predefined global $stdin. Then, calls to Kernel.gets, Kernel.readlines, et al. will read from the file. Here's tac.rb with that modification:
    if stdin_fname = ENV["stdin"] then # ENV is a Ruby Hash, like a Java Map
        $stdin = open(stdin_fname)
    end
    
    reversed = ""
    while line = gets
        reversed = line + reversed
    end
    puts reversed
    
  2. Normal operation is unaffected:
    $ ruby tac.rb < lines
    the end
    second line
    line 1
    
  3. For debugging, start byebug like this:
    $ stdin=lines byebug tac.rb
    
    That stdin=lines construct tells Bash to create an environment variable named stdin that is to endure only for the the command that follows.

    Let's try it:

    $ stdin=lines byebug tac.rb
    
    [1, 9] in /p1/hw/whm/372/tac.rb
    => 1: if stdin_fname = ENV["stdin"] then # ENV is a Ruby Hash, like a Java Map
       2:     $stdin = open(stdin_fname)
       3: end
       4: 
       5: reversed = ""
       6: while line = gets
       7:     reversed = line + reversed
       8: end
       9: puts reversed
    (byebug) c 9
    Stopped by breakpoint 1 at /p1/hw/whm/372/tac.rb:9
    
    [1, 9] in /p1/hw/whm/372/tac.rb
       1: if stdin_fname = ENV["stdin"] then # ENV is a Ruby Hash, like a Java Map
       2:     $stdin = open(stdin_fname)
       3: end
       4: 
       5: reversed = ""
       6: while line = gets
       7:     reversed = line + reversed
       8: end
    => 9: puts reversed
    (byebug) reversed
    "the end\nsecond line\nline 1\n"
    (byebug) reversed.size
    27
    (byebug) c
    the end
    second line
    line 1
    $
    
    For more on byebug see also https://github.com/deivid-rodriguez/byebug. Googling turns up lots of stuff, too, of varying correctness and merit.

    RubyMine

    A very good full-blown Ruby IDE is RubyMine. It's not free but there is a free 30-day trial. Like all IDEs however, there's a big learning curve, especially if you haven't used a JetBrains IDE before. It's not clear to me that it's worth the trouble for the little Ruby programs we'll be writing.