這是一篇關於perl腳本測試的總結性文章,其中提到了很多實用的模塊,如果文中介紹的不夠詳細,請到cpan上搜索該模塊並查閱其文檔。
1基本語法檢查
Perl語言的哲學是“There is more than one way to do it”,很多討厭Perl的人總是拿Perl的這個特性來攻擊Perl,而喜歡Perl的人卻又極力推崇它。這里不討論這個特性是好是壞,但不可否認的 是,Perl自由的語法,尤其是靈活特性所帶來的諸多陷阱,再加上濫用這種靈活的coder,perl代碼要做到“難看、難懂、難維護”,的確很容易。
1.1第一步
拿到一份待測試的perl腳本,第一步,我們檢查一下代碼里有沒有加上“use strict;”以及“use warnings;”。這里我大膽斷言一下:如果一個100行以上的腳本從開始書寫到完成,都沒有加上這兩句的話,這個腳本十有八九是有bug的。
之所以我這么說,是因為strict和warnings實在是兩個非常重要的模塊。簡單的說,strict模塊幫助我們“避免犯錯”,warnings模塊幫助我們“發現錯誤”。
1.1.1 use strict;
strict模塊一共有strict "vars"、 strict "refs"、 strict "subs"三部分,以strict "vars"為例,此功能要求腳本在使用變量時必須事先聲明。
對於$_、@_此類全局變量而言,它們已經由Perl語言作了預定義,而其它我們需要聲明的非全局變量,即所謂的詞法變量,我們都必須用my來做聲明。這樣的限制,規范了變量的使用,以免全局變量、局部變量難以區分,從而造成困惑。
為了更好地解釋my,給出如下三點附注:
1、my變量也就是"詞法變量",其作用域最大時為其所在的腳本文件范圍(當my聲明在任何一個{}塊之外時),最小時為其所在的最內層的{}塊。
2、全局變量除了$_此類預定義變量外,還包括用our或use vars來聲明的全局變量。
3、local可以將全局變量臨時地"局部化",但它不創建新的變量,因此你在聲明一個新變量時,請用 my 或 our。
關於strict模塊的具體應用,舉個例子:
{ $tmp = 3; }
此處省略500行代碼
$tmp ++;
即使是兩個不相關的變量,你也很可能用了同一個變量名$tmp進行聲明,如果沒使用 strict 模塊,那么$tmp變量默認將為全局變量,前一個$tmp使用后的值,將污染后一個$tmp,盡管你本來把它們當作兩個完全不同的臨時變量。
使用my可以規范變量作用范圍,把很多潛在的錯誤消滅在代碼最初。而strict模塊的作用(嚴格的來說,是strict "vars"的作用),則是強迫你使用my。
1.1.2 use warnings;
看一個例子,warnings模塊是怎么幫我們發現bug的:
$ cat a.txt
1 a b 2
3 c d 4
4 d f
9 k f 1
$ cat b.pl
open FI,"a.txt" or die "cant open a.txt";
while(<FI>){
@ret = split /\s*/,$_;
print $ret[0]+$ret[3],"\n";
}
$ perl b.pl
3
7
4
10
這段代碼的本意是,從a.txt中按行取出第1、4個字段,打印出加和值。從代碼中,可以看到我們默認a.txt每一行都是至少有4條記錄的,但如果出現一個異常,導致a.txt中,有一行只有3列值,這種錯誤我們希望能夠檢查出來。
然而,在不加warnings模塊檢查的情況下,雖然$ret[3]值為空,perl是不會報警的。
我們使用warnings模塊的命令行形式的參數-w來執行一遍b.pl :
$ perl -w b.pl
3
7
Use of uninitialized value in addition (+) at b.pl line 4, <FI> line 3.
4
10
可以發現,perl解釋器給我們打出了警告,並且指明了代碼和數據文件出錯的具體地址,從而幫助我們發現了bug 。
1.2 用-c參數檢查語法
Perl運行腳本,分成”編譯階段"和"運行階段"兩個過程,在"編譯階段",perl解釋器會分析一遍語法,如果語法有問題,則直接報錯退出,而不進行其它檢查,更不會進入"運行階段"。
有時候我們想檢查語法,卻又不想語法檢查通過后整個腳本被執行,這時候我們可以用perl的 -c 參數,譬如 perl -c test.pl ,這個參數能幫助我們檢查完語法后就直接退出,不管檢查是否通過。
不過,它有如下兩個缺點:
1、 它只能檢查語法,而無法通過-w等參數一並進行告警檢查。
2、它會執行BEGIN塊,並且會檢查use方法加載的模塊,但它不會執行INIT和END塊,也不會執行require方法加載的模塊(因為require方法加載模塊是在"運行階段")。
1.3用-T參數檢查注入式漏洞
我們都知道SQL注入,其實perl代碼測試也需要注意注入式危險。先來看一個例子:
print "${foo()}";
這句代碼的目的是,將用戶輸入的一個指向標量變量的引用的返回值的函數打印出結果來,可假如foo()被替換成system('rm *'),等我們發現打印的結果不對時,危害已經造成了。 解決的辦法是使用污染模式(taint-mode)選項(用-T參數):
$ cat d.pl
sub foo { system 'rm *' };
print ${foo()};
$ perl -T d.pl
Insecure $ENV{PATH} while running with -T switch at d.pl line 1.
1.4其它建議
如果源頭上產出的就是一份風格良好的代碼,這樣代碼的質量和測試的效率就高多了。因此,我們在書寫代碼時,可以參考如下建議(作為要測試perl代碼的QA,我們也可以把這些建議反饋給RD們):
1.4.1制定命名規范
譬如,聲明引用變量時,變量名加“_ref”后綴;定義包時,以大寫字母開頭...等待。Perl允許你同一個變量名,同時聲明為幾種不同的變量類型,譬 如 my $a; my @a; 。這種特性給我們帶來的代碼維護上的困惑遠大於它的價值。此類問題我們都可以制定規范約束,具體規范的例子可以參考《Perl最佳實踐》的相關章節。
1.4.2注釋!注釋!!
其實最重要的還是注釋,注釋雖非多多益善,但函數的輸入輸出及用途說明、復雜邏輯的說明,這些還是很必要的。我在讀perl代碼時,加的基本都是中文注釋--寫起來容易,讀起來也容易;我還喜歡把變量代入值,寫一個示范語句出來,一個簡單的例子比幾句話更容易明白。
1.4.3代碼格式規范
有興趣的可以去cpan上搜一下perltidy,或者參考《Perl最佳實踐》的相關章節,這里不多介紹。
1.4.4使用別名
對於 $@、$"此類全局變量,如果覺得可維護性很差,可以考慮使用其別名來增加可讀性。
譬如代碼 undef $/; $cont = < FILE >; 的作用是通過修改$/,將文件句柄FILE的所有內容讀入一個變量。一個可讀性更強的方法,是將 $/ 用它的別名 $INPUT_RECORD_SEPARATOR 或 $RS 來替代(后者看上去很awk,不是嗎?這是因為當初Larry Wall從awk中借鑒了很多語法。)。
具體的別名列表,可以查閱 perldoc perlvar 文檔。
1.4.5復雜數據結構解引用時,不妨分解一下
先看一段代碼:
%hash = (
a => [ [1,2,3,4,5],
[2,3,4,5,6] ],
b => [ [3,4,5,6,7],
[4,5,6,7,8] ]
);
print ${${$hash{b}}[1]}[4];
為了獲取這個"數組的數組的哈希"數據結構的值"8",我使用了 ${${$hash{b}}[1]}[4] 這個復雜的表達式,當然,這已經可讀性不錯了,畢竟我加了很多大括號,可如果其他人來看,不得不硬着頭皮去翻譯。當數據結構更復雜時,硬頭皮程度指數級增長。
我的建議是把這樣一個復雜的解引用語句,轉換成幾個簡單的解引用語句,譬如:
my $_arrayarray_ref = $hash{b};
my $_array_ref = ${$arrayarray_ref}[2];
print ${$_array_ref}[4];
2測試方法
2.1看
2.1.1最簡單最常用的方法:print函數
最常用的往往是最簡單的,print函數即是如此。在指定位置添加print函數,輸出需要查看的變量內容,能幫助我們解決大部分的調試需求。
2.1.2我在哪?
如果有大量的print函數調用,那么輸出的結果很可能需要我們仔細辨別哪條信息對應哪條print調用。此時,print函數中添加__LINE__、__FILE__信息能幫我們解決這個問題,前者表示當前行號,后者表示當前所在腳本文件名。
2.1.3我是誰?
用print函數,能查看某個變量的內容,但涉及到復雜的數據結構,print就不能滿足需求了。
以下給出兩種更強大的方案。
2.1.3.1復雜數據結構 - Data::Dumper模塊
簡單如一個哈希數據結構,用print函數也沒法方便地輸出全部內容的。這個時候就應該使用Data::Dumper模塊了,使用方法很簡單:
{ require Data::Dumper; print Dumper(%hash); }
再復雜的數據結構,Data::Dumper模塊都能輸出完整內容。
注:為了方便調試代碼的管理,以及盡量不影響原先代碼,在遇到多行代碼時,我會用{}來組成一個代碼塊,並且使用require而非use -- 以免可能將原本代碼中編譯階段的缺少模塊的bug給掩蓋掉。下同。
2.1.3.2深入了解變量屬性 - Devel::Peek
Devel::Peek能幫助我們深入了解變量的細節屬性,舉個例子,我們也許對Perl的上下文環境的處理方法很是困惑,下面這個例子就是利用Devel::Peek幫助我們了解一個"字符串變量"是如何和一個"整數變量"相加的。
$ cat f.pl
use Devel::Peek;
my $a = 5;
my $b = '5';
Dump($a);
Dump($b);
print "\n === equal ===\n\n" if $a == $b;
Dump($a);
Dump($b);
$ perl f.pl
SV = IV(0x67c368) at 0x67c370
REFCNT = 1
FLAGS = (PADMY,IOK,pIOK)
IV = 5
SV = PV(0x65dbc8) at 0x67c430
REFCNT = 1
FLAGS = (PADMY,POK,pPOK)
PV = 0x676670 "5"\0
CUR = 1
LEN = 16
=== equal ===
SV = IV(0x67c368) at 0x67c370
REFCNT = 1
FLAGS = (PADMY,IOK,pIOK)
IV = 5
SV = PVIV(0x6717f0) at 0x67c430
REFCNT = 1
FLAGS = (PADMY,IOK,POK,pIOK,pPOK)
IV = 5
PV = 0x676670 "5"\0
CUR = 1
LEN = 16
其中,SV表示標量,IV表示整型,PV表示字符串型,IOK標志表明對象含有一個可用的整型值,POK標志表明對象含有一個可用的字符串值。
從例子中,可以看到,$a和$b在剛聲明的時候,分別是整型標量和字符串標量。在需要做數值比較時,作為字符串標量的$b將主動地從字符串型轉換成整型, 從而”既是字符串型,又是整型“。同理,如果你用 $a eq $b 進行比較,則整型變量$a將獲取字符串型的屬性。
一般情況下,我們不需要了解這么多。如果說我們的工作是測試一個雞蛋是否變質了,那我們聞和吃一般就夠了,而沒必要在顯微鏡下觀察雞蛋的蛋白質等結構。
Devel::Peek就是我們的顯微鏡。如果的確有興趣用這台顯微鏡研究蛋白質結構,請參考 perldoc perlguts 以及 perldoc Devel::Peek 文檔。
2.1.4我怎么想? - B::Deparse
有時候,對於某些晦澀的語句,我很想知道perl是怎么理解它的。譬如我想知道 while( < FILE > ) 的終止條件是< FILE >返回空值呢,還是undef,這時候可以使用如下命令:
$ perl -MO=Deparse -e "while( <FILE> ) {}"
while (defined($_ = <FILE>)) {
();
}
-e syntax OK
由此看出,終止條件是<FILE>返回undef。
具體的內容,可以查看B::Deparse文檔。
2.1.5用Carp模塊獲取詳細的警告和錯誤信息
這里推薦Carp模塊的carp和croak兩種方法,作為warn和die的替代。推薦的理由在於,carp和croak在具有warn和die的功能外,還能輸出更多的信息,以方便我們調試。 舉例:
$ cat b.pl
use Carp;
sub foo {
unless(defined $_[0]) {
warn "warn message\n";
print " \n";
carp "carp message\n";
}
}
foo();
$ perl b.pl
warn message
carp message
at b.pl line 6
main::foo() called at b.pl line 10
如果還需要獲取堆棧信息,可以進一步用cluck替代carp。相對地,die - croak - confess 也構成一組調試方法。
2.1.6比較
可能在某一處,我們希望做一些復雜變量的比較工作。這里我們可以使用一些Test::Builder類的模塊,譬如:
{ require Test::Deep; require Test; print Test::Deep(\%a,\%b,"cmp ok") }
常用變量比較方法
2.1.7在運行階段隨時進行查看
有時候,程序運行時間很長,而我們希望在運行過程中,隨心所欲地觀察一些信息。這種情況下,我們可以自定義信號處理函數,譬如:
$SIG{INT} = sub {require Data::Dumper; print Dump(%hash)};
INT信號默認行為是終止程序,我們可以把它改成輸出某個我們想了解的值。如此,你可以在每次需要查看處理進度信息時,按ctrl+c就行了。
2.1.8既是注釋,又是debug語句
有一個模塊,它能讓你添加一些語句,使得這些語句在平常情況下是一些注釋,在你需要的時候就是調試信息輸出語句。這個模塊就是Smart::Comments,這里不多介紹,具體可以查看其文檔。
2.2改
2.2.1用POD快速注釋掉一段代碼
我們知道用 # 符號可以注釋掉一行代碼,但這樣效率太低了,對於整塊的代碼,我們可以利用POD來快速注釋,譬如:
=head
此處省略100行需要被注釋的代碼
=cut
注意 “=head”和“=cut”都需要頂格寫。關於POD的詳細資料,可以查看 perldoc pod 文檔。
此外,你還可以用"__END__"或"__DATA__"將所在行之后的內容全部“注釋”掉。
2.2.2修改代碼
直接修改代碼,假定某些輸入條件,測試后續邏輯的正確性,這是我們常用的方法。
這里需要特別提到的是關於正則表達式,如果我們發現正則表達式語句比較復雜,建議你用幾條簡單的正則表達式去替代被測的復雜正則表達式語句,並且在同樣的 輸入條件下,驗證輸出結果是否一致。這種方法尤其適用於輸入是大數據量的情況下,通過大量的實驗,我們很可能會發現一些在新舊正則表達式下輸出不一致的 case。
2.2.3重載庫函數
對於當前腳本中的函數,我們可以直接修改函數實現的代碼。可某些情況下,我們想要修改當前腳本所調用的庫文件里面的函數。這種情況下,直接修改庫文件比較 麻煩,尤其是標准的庫文件,我們想修改它還需要root權限。一個比較簡單的方法是重新實現該函數,從而覆蓋掉庫文件中的同名函數。
舉個例子,我們知道Time::Local模塊的timelocal函數是localtime的倒置,假設我需要修改timelocal函數,使得獲取到的值都是輸入參數的日期的下一年的反轉結果。
具體方法如下:
$ cat g.pl
use Time::Local;
use subs 'timelocal'; # 告訴解釋器,timelocal函數需要重載
*Time::Local::timelocal = sub { # 用typeglob的方法,將timelocal指向新的函數定義
my @tmp = @_;
$tmp[5] += 1;
return timelocal(@tmp); # 調用原先的timelocal函數
};
my $t = time();
print $t,"\n";
my @a = localtime($t);
print Time::Local::timelocal(@a),"\n";
$ perl g.pl
1291188534
1322724534
從例子中看到,重載后的timelocal函數,使得輸出的值比初始值大了一年。
2.3跟蹤和調試
2.3.1用Devel::Trace跟蹤
大家都知道shell中有-x參數,可以設定了跟蹤每一行代碼的執行結果,在perl中,有一個模塊能起到相似的作用,那就是Devel::Trace。
使用方法很簡單,perl -d:Trace test.pl,具體可以CPAN上查看文檔。
2.3.2用debug調試
? 基本的debug命令
Debugging task
Debugger command
To run a program under the debugger > perl -d program.pl
To set a breakpoint at the current line DB<1> b
To set a breakpoint at line 42 DB<1> b 42
To continue executing until the next break-point is reached DB<1> c
To continue executing until line 86 DB<1> c 86
To continue executing until subroutine foo is called DB<1>c foo
To execute the next statement DB<1> n
To step into any subroutine call that's part of the next statement DB<1> s
To run until the current subroutine returns DB<1> r
To print the contents of a variable DB<1> p $variable
To examine the contents of a variable DB<1> x $variable
To have the debugger watch a variable or expression, and inform you whenever it changes DB<1>w$variable
DB<1> wexpr($ess)*$ion
To view where you are in the source code DB<1> v
To view line 99 of the source code DB<1> v 99
To get helpful hints on the many other features of the debugger DB<1> h
上面的debug的基本命令,已經能應付絕大部分情況下的調試需求,除此外,這里還想特別介紹一下 a 命令,通過 a LINE COMMAND 的方法,我們可以設置一個在執行第 LINE 行程序之前的動作,而我們可以利用這個COMMAND,設定一些復雜的操作,以更好地獲取調試信息。
舉個例子:
$cat k.pl
%ha = ( a => [1,2,3],
b => [4,5,6] );
%hb = ( a => [1,2,3],
b => [4,5,7] );
print "1\n";
$ perl -d k.pl
Loading DB routines from perl5db.pl version 1.33
Editor support available.
Enter h or `h h' for help, or `man perldebug' for more help.
main::(k.pl:1): %ha = ( a => [1,2,3],
main::(k.pl:2): b => [4,5,6] );
DB<1> a 5 use Test::More tests => 1; use Test::Differences; print eq_or_diff ${$ha{b}}[2],${$hb{b}}[2],"e or d";
DB<2> n
main::(k.pl:3): %hb = ( a => [1,2,3],
main::(k.pl:4): b => [4,5,7] );
DB<2> n
main::(k.pl:5): print "1\n";
1..1
not ok 1 - e or d
# Failed test 'e or d'
# at (eval 24)[/opt/ActivePerl-5.12/lib/perl5db.pl:638] line 1.
# +---+-----+----------+
# | Ln|Got |Expected |
# +---+-----+----------+
# * 1|6 |7 *
# +---+-----+----------+
0
更多的內容,請參考 perldoc perldebug 文檔。
3測試對象
3.1測試.pl腳本文件
這里的.pl腳本文件,指的是被直接運行的腳本。這樣的腳本,一般的特點是包含了很多庫文件,並且代碼比較零散--不像庫文件那樣絕大部分代碼都是一個個封裝好的函數。
測試這樣的腳本時,除了前面講到的那些測試方法外,這里再介紹一個測試框架:Test::More。
Test::More是perl腳本的測試框架模塊,除了下表整理的一些常用方法,它還有SKIP、TODO等測試內容標記,更強大的地方 是,Test::More由於基於Test::Builder模塊,它能配合其它Test::Builder類的測試模塊一起工作。
具體參考 perldoc Test::More; perldoc Test::Builder;
? 常用Test::Builder類模塊及其方法
模塊名
模塊簡介
方法名
方法簡介
Test::More
強大的測試框架 ok 判斷真假
is/isnt 字符串比較,類似eq/ne
like/unlike 正則比較,匹配/不匹配
cmp_ok 可以指定操作符地比較
can_ok 被測模塊是否導出函數到當前命名空間
isa_ok 對象是否被定義或對象的實例變量確實是已定義的引用
subtest 生成測試子集
pass/fail 直接給出通過/不通過
use_ok 測試加載模塊並導入相應符號是否成功
require_ok 類似use_ok
is_deeply 復雜數據結構的比較,加強版的is
3.2測試.pm庫文件
如前面所說,庫文件一般都是由一個個封裝好的函數組成,這樣的perl腳本,最適合做單元測試了。單元測試推薦Test::Class模塊,具體的內容參考另外一篇文章《perl單元測試》。
3.3性能測試
這里的性能測試主要指的是腳本運行時間的檢測,具體依然參考《perl單元測試》。
3.4覆蓋率測試
覆蓋率測試,具體內容參考《perl單元測試》。。
4 Perl陷阱和缺陷
關於陷阱和缺陷,perl有專門的文檔,具體參考 perldoc perltrap 。以下列舉的內容是對它的一個補充,另外也是實際工作中更易犯的一些錯誤。
4.1基本語法
4.1.1真與假
除了""和"0",所有字符串為真
除了 0,所有數字為真
所有引用為真
所有未定義的值為假
空列表"()"、"undef"均為假
4.1.2數組和哈希的初始化,不要用undef
$ cat b.pl
@a = undef;
@b = ();
%c = undef;
print scalar @a,"\n";
print scalar @b,"\n";
print scalar keys %c,"\n";
perl b.pl
1
0
1
從例子可以看出,用undef作為右值,實際上是給數組或哈希初始化了一個值,該值為undef,而非預想的初始為空。
4.1.3對 $_ 的值,及時存取
$_ 是每個perler初學時最早碰到的讓人困惑的全局變量,它是缺省的輸入和模式搜索空間,很多操作和函數中都會將輸出缺省地賦給它。因此,養成及時存取 $_ 變量的習慣,能夠避免很多陷阱。
while(<FI>){
$tmp_a = $_;
foreach(@arr){
$tmp_b = $_;
......
}
}
4.1.4對全局變量的修改,注意及時恢復
把一個文件句柄對應的文件內容,賦值給一個scalar變量,常用 undef $/; $cont = < FILE_A > 。但這樣將會修改全局變量 $/ ,如果沒有改回去的話,后續的while(< FILE_B >)之類所有對文件句柄的鑽石符操作,均將一次讀入整個文件。
修改的辦法,是將變量賦回原先的值:
undef $/;
$cont = <FILE_A>;
$/ = "\n"; # 重置為初始值
while(<FILE_B>)
更好的辦法,是用 local 函數,將修改局限在最小的閉合塊中:
{
local $/ = undef;
$cont = <FILE_A>;
}
while(<FILE_B>)
4.1.5用foreach、for的兩個注意點
先來看一段測試代碼
$ cat e.pl
$i = 0;
@a = (1,2,3);
foreach $i (@a) {
$i = -$i;
}
print "$i\n";
print "@a\n";
$ perl e.pl
0
-1 -2 -3
從中我們可以看到兩點:
1、 變量 $i 盡管中foreach循環(或for循環)中最后的一次值是3,但退出循環后,其值還是保持進入循環時的值。也就是說,$i 變量在foreach循環中,被local了。
2、 $i 在foreach循環過程中,是@a數組當前值的別名,因此修改$i值,也就是直接修改了@a數組。在for、grep函數中,同樣存在這個問題。
4.1.6子例程傳參時不會拷貝參數,注意不要修改原值
先來看如下一個例子:
$ cat h.pl
#use Devel::Peek;
my @a = qw(1 2 3);
#Dump(\@a);
sub bezero {
# Dump(\@_);
$_[1] = 0;
print "===\n";
}
print "@a\n";
bezero(@a);
print "@a\n";
$ perl h.pl
1 2 3
===
1 0 3
我們看到,數組 @a 在被當作參數傳入函數bezero后,被修改了值。由此,我們可以懷疑,在傳參時,@_其實是@a的別名,而非拷貝。為了證實這個猜測,我們可以利用前面 提到的Devel::Peek模塊觀察這兩個數組的地址是否一致。因此我們打開腳本中的注釋,然后重新運行腳本,觀察結果如下:
$ perl h.pl
SV = IV(0x67bc30) at 0x67bc38
... ....
SV = IV(0x67bc30) at 0x67bc38
... ...
兩個數組的地址完全一致。
通過這個例子,我們的結論是:函數傳參時,如果你不想修改參數值,記得把參數在函數內部拷貝一份后再使用。
4.1.7用each多次遍歷同一hash時,注意及時歸位迭代游標
$ cat a.pl
%h = ( a => 1, b => 2 );
while(($a,$b)=each %h){
print "$a $b\n";
print "======\n";
last;
}
while(($a,$b)=each %h){
print "$a $b\n";
}
$ perl a.pl
a 1
======
b 2
這個例子中,有兩處使用了each函數,由於前面一個while在獲取一次值后,就退出循環,但沒有重置迭代游標,導致第二個while從上一次迭代的位置開始。
如果你期望第二次while中也能用each完整地遍歷哈希,你需要重置迭代游標。重置的方法是完整地讀取一遍該哈希,譬如用keys函數、values 函數、把該哈希當作右值賦值給數組或另一個哈希。當然,你用 while(each %h){} 什么也不做地遍歷一遍,也能重置迭代游標,只是這樣的代碼實在有點丑陋。
而我的做法是, 從來不用each函數,而是用keys函數獲取key后再取value ,我的哈希遍歷方法如下:
foreach my $key (keys %h) {
print "$key $h{$key}\n";
}
雖然多敲了些代碼,但這樣做我不用擔心前面的陷阱,因為keys函數一次就遍歷完了哈希。
你可以 perldoc -f each; perldoc -f keys; perldoc -f values; 了解更多。
? Perl內部是怎么維持這個迭代游標的?
1. 在Perl內部,散列稱為 HV (hash value),並使用 HE 結構體表示鍵/值對,使用 HEK 結構體表示所有關鍵字。
2. Perl用RITER, EITER兩個字段來實現訪問散列所有元素的單向迭代器。RITER 是作為數組下標的整數,而 EITER 是一個指向 HE 的指針。迭代器是從 RITER = -1 和 EITER = NULL 開始的,完成一次迭代后,RITER和EITER的值將會變化。其中RITER在首次迭代后,值變成1,之后每次成功迭代,值都將累加1,直到迭代完成 后,復位為-1。
知道了這些細節,我們就可以用 Devel::Peek 模塊的 Dump 函數更方便地觀察迭代過程了,感興趣的話,你不妨一試。
4.1.8調用函數時,用foo(),而不要用foo或&foo
以如下一段代碼為例:
$ cat a.pl
use strict;
use warnings;
sub double {
return 0 if scalar @_ == 0;
return 2 * $_[0];
}
print double - 3;
$ perl a.pl
-6
這里原意是要打印出 double函數的返回值減去3以后的值,也就是應該打印結果為 -3 ,可結果卻是 -6 。
我們可以利用前面介紹的 B::Deparse 模塊,看一下perl是怎么理解代碼的:
$ perl -MO=Deparse a.pl
sub double {
use warnings;
use strict 'refs';
return 0 if scalar @_ == 0;
return 2 * $_[0];
}
use warnings;
use strict 'refs';
print double(-3);
a.pl syntax OK
很清楚地發現,perl把 double -3 理解成了 double(-3)。為了避免這種問題,在函數調用的時候,我們必須明確地寫成 double() - 3 ,而如果你就是想表達 double(-3) 的話,也要明確地寫成 double(-3) ,以免代碼讓人理解錯。
另外需要注意,不要寫成 &double(), 這種寫法是perl 4版本的語法,雖然目前perl 5兼容了這種寫法,但它會帶來一些困惑,譬如:
$curr_pos = tell &get_mask( ); # means: tell(get_mask( ))
$curr_time = time &get_mask( ); # means: time() & get_mask( )
當然,在作為函數引用的時候,還是需要用的,譬如:
$SIG{PIPE} = \&Plumber; # 定義PIPE信號的處理函數
4.2正則匹配
4.2.1變量內插時,注意特殊字符的轉義
可以使用quotemeta函數,也可以使用\Q..\E,但總之,在需要變量內插從而構成正則表達式時,注意特殊字符的轉義。
4.2.2在用$1獲取匹配結果時,確保匹配是成功的
先來看如下一個例子:
$ cat a.pl
$str = 'a2';
$str =~ /(\d)/;
print $1,"\n";
$str =~ /(3)/;
print $1,"\n";
$ perl a.pl
2
2
$1是全局變量,匹配失敗時並不會修改它的值,也不會重置為undef,因此,每次使用它時,請確保加上匹配成功的判斷,譬如 if( $str =~ /(\d)/ ){ print $1; } 。
4.2.3 /g -- 控制迭代匹配
/g 是全局查找所有匹配的修飾詞。在列表環境下,它能一次性返回所有匹配結果,在標量環境下,它每次返回一個匹配結果。標量環境下進行迭代匹配時,需要了解匹配偏移量的知識,否則很容易出錯。
我的建議是:
1、列表環境下使用/g,大膽用吧,不用擔心。
2、標量環境下使用/g,如果需要對同一個字符串進行多次/g匹配,注意是否需要重置匹配偏移量。
? 與迭代匹配相關的
迭代匹配修飾符: /g、/cg
上次匹配位置斷言: \G
上次匹配位置函數: pos($str)
? 不同上下文環境下的/g
列表環境 標量環境
顯式 @res = m/(\d)/g; $res = m/(\d)/g;
隱式 print m/(\d)/g; m/(\d)/g;
while( m/(\d)/g ) { }
功能 列表環境下,/g一次性返回所有匹配結果列表 標量環境下,/g迭代匹配,每次返回一個匹配結果
? 用/g、/cg和pos來控制迭代
匹配類型
匹配嘗試開始位置
匹配成功時的pos值
匹配失敗時的pos設定
m/…/ 字符串起始位置(忽略pos) 重置為undef 重置為undef
m/…/g 字符串的pos位置 匹配結束位置的偏移值 重置為undef
m/…/gc 字符串的pos位置 匹配結束位置的偏移值 不變
4.2.4正則表達式很強大,但不要濫用
首先來做一個練習:給你5分鍾,讓你寫出一個判斷"年月日"字符串是否合法的正則表達式。
sleep 5 * 60;
好了,坦白說,你是不是不到兩分鍾就放棄了?我相信這樣的正則表達式肯定可以寫出來,因為perl的正則表達式非常強大,它支持"環視"(look around),支持在表達式中嵌入條件判斷、代碼,你可以把一個正則表達式寫成一個由復雜的邏輯判斷語句構成的式子。不過,既然如此,何必還要硬着頭皮 寫正則表達式呢?我們干脆用一些 if...else... 語句去判斷好了。
況且,即使你花了N分鍾寫出來了,這樣的表達式,可讀性也是非常差的。
因此,給一個建議:邏輯復雜的時候,不如用幾條語句去代替一個正則表達式。
除此外:
1、對於定長的記錄,pack/unpack是更好的選擇。
2、分析html、xml等頁面時,尤其是很難找到一個固定的字符串去錨定匹配結果時,推薦使用HTML::Parser、HTML::TreeBuilder、XML::Simple之類的模塊。
4.3與Shell共舞
perl腳本調用shell命令,shell腳本中執行perl命令行語句,這是經常會碰到的場景。作為膠水語言,perl能與shell很好地共舞,但前提是避免落入如下陷阱。
perl腳本調用shell命令,一般有三種方式:``或qx//、system()函數、exec函數。關於這幾種方式的含義和使用方法,可以查看相關文檔。這里的內容都是使用中的陷阱。
注:這里寫”調用shell命令"並不嚴謹,我的確切意思是,"向shell提交對外部命令調用的請求"。
4.3.1區分perl中的$?跟shell中的$?
在shell中,我們知道 $? 表示上一條命令執行后的返回狀態,而在perl中, $? 表示上一次管道關閉,反勾號(``)命令或者 wait,waitpid,或者 system 函數返回的狀態。 它的另一個名字是$CHILD_ERROR。它是一個16位的狀態值,高8位是子進程的退出值,在低位,$? & 127 告訴你該進程是因為哪個信號(如果有)退出的,而 $? & 128 匯報該進程的死亡是否產生一個內核的傾倒。
說具體一點,我們需要注意以下兩點:
? 不要用 $? 去判斷前一條語句是否執行成功
open FI,"a.txt"; die "open a.txt failed" if ($? = 0); 這種寫法是錯誤的,正確的寫法是 open FI,"a.txt" or die "open a.txt failed"; 。原因很簡單,在perl中,$? 並不是“上一條命令”執行后的返回狀態。
? 子進程返回失敗時,$? 的值是256,而非1
舉例:
$ cat p.pl
system("ls file_not_exist") ;
print $?;
$ perl p.pl
ls: file_not_exist: No such file or directory
256
原因前面講了,$?的高8位存的是子進程的退出值,因此子進程退出值是1時,$?的值就是256。
4.3.2調用外部命令並獲取返回值后,注意chomp
譬如 chomp($host=`hostname -i`) ,如果少了chomp,但$host變量獲取到的將是末尾帶有\n的ip值,為后續處理埋下了隱患。
4.3.3不要調用shell的內建命令
shell的內建命令,實際上都是shell的內部函數,這些內建命令,你用 which 是查不到路徑的。內建命令包括 cd、export、umask、source 等等,具體的你可以 man builtins 查看。
對於內建命令,譬如 cd,你直接用 system("cd") 執行會出錯,錯誤是無法找到該文件或目錄(你可以用代碼 system("cd"); print $!; 試一下),在命令前加 sh -c 是個解決方案。
不過調用shell的內建命令,效果可不是你預想的那般,測試代碼如下:
system("sh -c cd ./dir && touch cba");
system("touch abc");
這段代碼試圖在 ./dir 目錄下,建兩個文件,運行后卻發現,兩個文件都被建在了當前目錄,而非 ./dir 目錄。
原因在於perl每次在用system()調用外部命令時,都是先做一個fork,而外部命令的執行是在fork的子進程中進行的,它做的 cd 操作,無法更改父進程(也就是perl腳本進程)的工作路徑。
解決的辦法是用perl自己的函數,譬如用 chdir 替換 system("sh -c cd")、用 umask 替換 system("sh -c umask") 等等。
4.3.4調用外部命令時,注意特殊字符的轉義
簡單不嚴謹地說,perl對'('、'<'之類的特殊字符是不需要轉義的,而shell需要,因此在有可能涉及此類特殊字符時,需要特別留意。
以下是一段測試代碼:
$a = 'abc(123)sdf';
$b = quotemeta $a;
qx/echo $a/;
`echo $a`;
system("echo $a");
exec("echo $a");
試着執行一下,四條語句均會報錯。把$a改成$b,四條語句都改為正確。
4.3.4慎用Shell.pm模塊
Shell.pm是perl的標准模塊,它能夠讓你在perl腳本中直接運行shell命令,但由於它內部實現時對quoting的處理有缺陷,導致在對包含'('、'<'之類字符的參數處理時依然會有錯誤。
測試代碼如下:
$ cat d.pl
use Shell;
$a = 'abc(123)sdf';
$b = quotemeta $a;
echo($a);
print "=============\n";
echo($b);
$ perl d.pl
sh: -c: line 0: syntax error near unexpected token `('
sh: -c: line 0: `echo abc(123)sdf'
=============
sh: -c: line 0: syntax error near unexpected token `('
sh: -c: line 0: `echo abc\\(123\\)sdf'
可以看到,Shell模塊的轉義問題更加使人困惑,不管有沒有quotemeta,都會報轉義相關的錯誤。
查一下perldoc Shell文檔,可以看到:
BUGS
Quoting should be off by default.
文檔告訴我們,轉義的問題其實一直都在。
如果你感興趣,還可以debug到Shell.pm模塊內部,看看該模塊對轉義是怎么處理的,從而了解問題究竟出在哪里。這里先公布一下我的結論:以版本 號為0.72_01的Shell.pm為例,問題出在第109~110行(s/(['\\])/\\$1/g;$_ = $_;),這里對轉義的處理過於簡單了。解決方法是將第110行代碼改成$_ = quotemeta $_; 我的實驗結果是它解決了轉義問題,但我並不確定它是否會帶來其它的bug,聰明的你可以深究一下。
4.3.5 shell調用perl語句,注意變量內插
大家在寫shell腳本時,經常調用awk、sed命令進行一些文本相關的處理,其實perl也有 one-line 模式,也能被shell調用,並且功能遠比awk、sed強大。具體的命令行模式的perl如何使用,請參考 perldoc perlrun 文檔。
這里講一個需要注意的問題(其實類似例子更應該算是shell的”陷阱“,並且在shell腳本調用awk時也同樣需要注意):
$ cat log
key:1us time:11us
key:2us time:12us else:null
key:3us type:arr time:13us
$ cat a.sh
pattern='time'
cat $1 | perl -ne '{$sum += $1 if /$pattern:(\d+)us/;} END{print $sum;}'
$ sh a.sh log
6
腳本a.sh的設計功能是從命令行參數中讀入一個日志文件,利用perl語句匹配出time值,然后計算出time值的加和。根據設計,運行結果應該是36,而非6。
問題出在對$pattern的傳值上,注意到腳本里是用''單引號來引用perl執行語句的,我們知道單引號在shell里是"hard quote",凡在hard quote中的所有meta都會被關閉特殊含義,包括這里的'$'符號。因此這里無法將$pattern的值傳入perl語句,導致$pattern的值 為空,從而錯誤地匹配了 ':(\d+)us' ,並且捕獲了它匹配到的第一個值,也就是"key"后面的數字。
如果把單引號改成雙引號,能解決$pattern變量的傳值問題,但卻導致$sum和$1這兩個原本perl語句中的變量被shell進行了變量內插,這樣的錯誤依然不能接受。
正確的解決辦法是將perl語句的那一段,分情況使用單引號和雙引號,更改后的語句和運行結果如下:
$ cat b.sh
pattern='time'
cat $1 | perl -ne '{$sum += $1 if /'"$pattern"':(\d+)us/;} END{print $sum;}'
sh b.sh log
36