這裡蒐集一些適用於 Perl 程式的基本除錯技巧,可以幫助程式設計師降低程式出問題的機率。

使用 strictwarnings

如果我們需要對 Perl 的程式進行除錯時,可以在程式碼的開頭加上這兩行:

use strict;
use warnings;

這樣對於程式的除錯會很有幫助。以下介紹這兩個 pragma 的作用。

strict

strict 這個 pragma 的作用是強迫程式設計者使用比較嚴謹的 Perl 寫法,避開一些比較容易出錯的撰寫方式,讓程式的除錯會更容易。

加入 strict 之後,在程式的撰寫上會有許多的限制,最常用的就是變數在使用前一定要先宣告:

use strict;
my $foo = 7;                # 宣告一個變數
print "foo is $fooo\n";     # Perl 會顯示 $fooo 沒有宣告的錯誤

在這段程式碼中,如果沒有加上 strict 的話,Perl 會正常執行它,不會有任何錯誤訊息,但是執行的結果會讓程式設計師感到很困惑:「為什麼 $foo 輸出時老是沒有任何東西?」,而加上 strict 之後,就可以直接避免掉這類打錯字的問題。

除此之外,strict 也會限制裸字(barewords)的使用,在一般的 Perl 程式中,如果要指定一個字串給一個變數,可以這樣寫:

no strict;       # 明確指明不使用 strict
$foo = Lorem;    # 使用裸字指定字串
print "$foo\n";  # 輸出 "Lorem"

加上 strict 之後,會對限制大部份的裸字使用:

use strict;
my $foo = ipsum;        # 使用裸字會產生錯誤訊息
$foo = (
  Lorem => 'ipsum'      # 沒問題,在 => 左邊允許使用裸字
);

$SIG{PIPE} = handler;   # 產生錯誤訊息
$SIG{PIPE} = \&handler; # 沒問題
$SIG{PIPE} = "handler"; # 這樣也可以,但是建議使用上面這個方式

在啟用 strict 之後,如果使用 symbolic reference 也會產生錯誤:

no strict;
$name = "foo";
$$name = "bar";           # 設定變數 $foo 為 "bar"
print "$name $$name\n";   # 輸出 "foo bar"

use strict;
my $name = "foo";
$$name = "bar";           # 不可使用 symbolic reference,會產生錯誤

warnings

warnings 這個 pragma 的作用是啟用所有 Perl 警告訊息,讓開發者容易察覺程式可能會出問題的地方。

use warnings;
my $foo = 1;
$foo += 3;
my $foo = 1;            # 產生警告:重複宣告

my $bar = '12fred34';
my $baz = $bar + 1;     # 產生警告:"12fred34" 不是數值
                        # 產生警告:$baz 只使用一次

警告訊息太多?

有些經驗不足的程式設計者會感覺程式原本好好的,在加入 strictwarnings 之後,卻出現一大堆的警告甚至錯誤,會干擾程式設計師開發程式,所以又將這兩行去掉,誤認為沒有任何警告與錯誤產生就表示整個程式沒問題。

但把所有的警告與錯誤訊息「關閉」是完全錯誤的做法,實際上加上 strictwarnings 產生的一大堆訊息所代表的意義是:您的程式非常有可能有問題!正確的做法是依照這些訊息,逐一將每個問題點修正,盡可能讓這些訊息減少,這樣才能讓程式的錯誤產生率降到最低。

檢查 open 的傳回值

在一般的 Perl 程式中,開檔、讀檔是很常見的動作,我們常常會寫出類似這樣的程式碼:

open( my $file, $filename );
while ( <$file> ) {
  # ...
}

但是這樣的程式碼若是遇到欲開啟的檔案不存在時,完全不會有任何警告或錯誤訊息,程式設計者只會發現這個 while 迴圈被跳過,這樣的小錯誤若發生在比較大型的程式裡面,在完全沒有任何訊息的狀況下,有時候是很難被找出來的。

解決的方法是在開啟檔案時,加上檢查傳回值的程式碼,在 Perl 中加上這樣的判斷其實很方便:

open( my $file, '<', $filename ) or die "Can't open $filename: $!";

這樣當程式找不到 $filename 這個檔案時,它就會停止執行並輸出一行錯誤訊息,這對於除錯非常有幫助!

使用 diagnostics

有時候 Perl 所自動產生的錯誤訊息並沒有提供足夠的資訊給程式開發者,例如這樣的訊息可能就會讓程式開發者一頭霧水:

Use of uninitialized value in string eq at /Library/Perl/5.8.6/WWW/Mechanize.pm line 695.

這時候我們可以使用 diagnostics,在程式的開頭加上這一行:

use diagnostics;

這樣一來,Perl 就會輸出比較詳細的訊息:

Use of uninitialized value in string eq at /Library/Perl/5.8.6/WWW/Mechanize.pm
    line 695 (#1)
(W uninitialized) An undefined value was used as if it were already
defined.  It was interpreted as a "" or a 0, but maybe it was a mistake.
To suppress this warning assign a defined value to your variables.

To help you figure out what was undefined, perl tells you what operation
you used the undefined value in.  Note, however, that perl optimizes your
program and the operation displayed in the warning may not necessarily
appear literally in your program.  For example, "that $foo" is
usually optimized into "that " . $foo, and the warning will refer to
the concatenation (.) operator, even though there is no . in your
program.

這樣可以讓程式設計者更容易看出問題在哪裡。

加上 diagnostics 之後,產生的訊息雖然很詳細,不過我們可能不是每次都需要看這麼詳細的內容,有時候訊息太多反而很麻煩,建議可以在平常開發時將這行註解起來,當出現看不懂的訊息時,再將這行打開。

另外如果不想更動原始的程式碼的話,我們也可以透過命令列的參數來啟用 diagnostics

perl -Mdiagnostics mycode.pl

若使用這樣的方式的話,mycode.pl 的程式碼就不需要另外加上 use diagnostics;

呼叫堆疊(Call Stack)

有時候程式會出現一些訊息,但是我們可能看不出來到底程式是怎麼樣執行到這裡的,例如:

Use of uninitialized value in string eq at /Library/Perl/5.8.6/WWW/Mechanize.pm line 695.

我們可以開啟 Mechanize.pm 這個檔案,找到第 695 行看看問題出現的位置,但是這樣還是無法得知我們的程式是怎麼樣執行的,我們需要的是一個函數呼叫的呼叫堆疊。

當一個 Perl 程式呼叫到 diewarn 時,它會進而呼叫 $SIG{__DIE__}$SIG{__WARN__} 所指定的函數(預設是 CORE::dieCORE::warn),我們可以拿一些更有用的函數把預設的函數替換掉,這樣對於除錯會更有幫助。

Carp 模組所提供的 confess 函數就是一個很好的替代方案,我們可以在程式的開頭加上這幾行:

use Carp qw( confess );
$SIG{__DIE__} =  \&confess;
$SIG{__WARN__} = \&confess;

這樣一來,當 Perl 呼叫到 diewarn 時,就會交給 Carp::confess 來負責處理,以這個例子來說,confess 會輸出原本的訊息,外加一個 stack trace 的資訊,然後停止程式的執行:

Use of uninitialized value in string eq at /Library/Perl/5.8.6/WWW/Mechanize.pm line 695.
 at /Library/Perl/5.8.6/WWW/Mechanize.pm line 695
        WWW::Mechanize::find_link('WWW::Mechanize=HASH(0x180e5bc)', 'n', 'undef') called at foo.pl line 17
        main::go_find_link('http://www.cnn.com') called at foo.pl line 8

這樣我們就有更多的除錯資訊,包含被呼叫的函數與被傳遞的參數值,從這裡我們可以看到 find_link 的第三個參數是 undef,這是一個很可疑的地方,從這裡開始檢查是一個不錯的出發點。

Carp::Always

如果您不想在每個程式中都用手動的方式更改 signal handlers,可以改用 Carp::Always,但這個 Perl 模組不是內建的,要從 CPAN 上面下載。

安裝好之後,只要在 Perl 程式的一開始加上一行:

use Carp::Always;

這樣就可以使用了,除了這個方式之外,也可以用命令列的方式來使用:

perl -MCarp::Always script.pl

如此一來,只要程式呼叫 diewarn 時,就會自動輸出呼叫堆疊。

參考資料:Perl 101