前陣子開始研究 rails 原始碼時看到很多關於 debug 的技巧,便有感而發想寫些關於 rails debug 的紀錄。稍微介紹一下我平常在 debug 時常用的兩個 gem,pry 以及 byebug

Pry

pry 大概是我開始寫 rails 之後最常使用的 debug 工具。雖然我會用到的部分只是 pry 強大功能的一小部分,但光是這樣已經幫助我解出大大小小的 bug 了。pry 可以視作一個加強版的 irb,不僅可以跟 irb 一樣直接開啟,也可以直接在 rails 執行到某一行的時候開啟,讓你檢視當時的狀態。

要使用 pry 首先要先在你的 Gemfile 加入:

group :development, :test do
  gem 'pry'
end

然後就可以直接在在你想要開啟 pry 的地方加入 binding.pry 就可以了。例如下面這段程式:

class ArticlesController < ApplicationController
  def index
    @article = Article.first
    binding.pry
  end
end

這時當你執行到該 controller method 的時候,terminal 中就會出現以下畫面:

    2:  def index
    3:    @article = Article.first
 => 4:     binding.pry
    5:   end

[1] pry(#<ArticlesController>)>

這時你就可以使用像在 irb 一樣的操作方式檢視那個時間點所有 variable 的狀態,同時也可以對它們進行你想要的操作。pry 在大部分寫 rails 的時候我覺得已經很夠用了,有這個功能基本上就可以解決大部分的 bug 了。

byebug

byebug 是 rails 內預設的 debug 工具。必須承認我一開始認為它完全是個沒啥用的東西,如果有 pry 就可以刪掉了,那完全是因為我從來沒有好好看過這東西要怎麼用。隨著寫的程式越來越複雜,看過的東西越來越多,我才發現 byebug 的功能之強大,甚至有種可以媲美付費 IDE 的debugger 的感覺。

byebug 的使用方法跟 pry 的道理是差不多的,在你想要檢視的地方放入 byebug 這個 method 即可,但有別於 pry,byebug 除了可以就當時的狀態做操作之外,還可以逐步執行偵錯,甚至在每一步追蹤你關心的變數。

以下面這個例子來說好了:

def factorial(n)
  num = 1

  n.times { |count| num *= count }

  return num
end

puts factorial(6)

這是一個算出階層的簡單程式(1! = 1, 2! = 1 * 2, 3! = 1 * 2 * 3),但如果你跑完會發現這個程式其實有個 bug。factorial(6) 會印出 0 而非 720。這時候可以在 puts factorial(6) 前面加上 byebug

  return num
end

byebug
puts factorial(6)
前面記得加上 require 'byebug'

重新執行之後應該會看到:

[3, 12] in /path/to/code.rb
    3: def factorial(n)
    4:   num = 1
    5:
    6:   n.times { |count| num *= count }
    7:
    8:   return num
    9: end
   10:
   11: byebug
=> 12: puts factorial(6)
(byebug)

byebug 會把時間停留在第 12 行執行之前,這時如果你有想要檢視的 variable 的話可以跟 pry 一樣直接做操作。不過在這個例子裡面前面沒有們好看的,所以我們可以輸入 step 然後按下 enter。這樣畫面就會開始逐步執行:

[1, 10] in /path/to/code.rb
    1: require 'byebug'
    2:
    3: def factorial(n)
=>  4:   num = 1
    5:
    6:   n.times { |count| num *= count }
    7:
    8:   return num
    9: end
   10:
(byebug)

這邊 byebug 就跳到第 4 行執行之前,這裡你可以試著輸入 num 來檢查現在 num 的狀態,你會得到 nil 因爲 num = 1 還沒被執行。

這裡為了 debug 會希望可以一直追蹤 num 的數值,所以我們可以輸入 display num,這樣接下來的每一步,byebug 都會顯示當時 num 的數值。設定完後就可以開始不斷的 step 找尋 bug。

可以使用 s 替代 step,還可以直接按下 enter 來重複上一步。

開始逐步執行後馬上就可以發現,在 n.times 第一次執行的時候 num 就變成 0 了。所以問題是出在 n.times 是從 0 開始而非 1,所以這時候就可以輸入 exit 離開 byebug,然後把程式改成:

def factorial(n)
  num = 1

  n.times { |count| num *= count + 1 }

  return num
end

puts factorial(6)

結果就會正確了。

pry vs. byebug

pry 的特色最直接的就是簡單的操作,因為跟 irb 實在太像了,只要會 binding.pry 就幾乎沒有學習門檻,更重要的是 pry 有內建的上色功能,所以顯示的 code 是彩色的,對於找 bug 方便很多。但是當程式開始複雜起來的時候,byebug 的逐步執行功能有些時候是少不了的,更重要的是當你 bug 是出在別人寫的 library 的時候,byebug 的逐步執行可以直接告訴你是接下來執行的是哪個檔案裡面的哪一行 code,幫你省下很多找原始碼的時間。

如果你想要研究某個 gem 的原始碼,善用 byebug 可以讓你學到更多。

Reference