Perl语法总结

中文原文
英文原文

Hello World

Perl脚本是带有.pl后缀的文本文件,由Perl解释器解释执行,perl或者perl.exe:
perl helloworld.pl [arg0 [arg1 [arg2 ...]]]

#helloworld.pl
use strict;
use warnings;

print "Hello world";
  • 编译指示:编译指示(use foo; 格式)是给perl.exe的一个提示,在程序开始执行之前的语法验证阶段会发挥作用,脚本语句实际执行的时候这些编译指示对于运行结果没有影响。 由于Perl的语法非常宽容,为避免写出有歧义或者不可预期的代码,在每个perl脚本或者模块的开头加上编译指示 use strictuse warnings
  • 分号;是语句结束的标志
  • 井号#表示注释的开始。Perl没有块注释的语法。

变量

Perl的变量有三种类型:标量(scalar)、数组(array)和哈希(hashes)。每种类型都有属于自己的符号:分别是$、@和%。变量定义使用my关键字,生命期直到其所在的代码块结束或者文件的末尾。

Scalar变量

一个scalar变量能包含:

  • undef(未初始化的变量初值默认为undef,打印空字符串”“)
  • 数值(Perl不区分整形和浮点类型)
  • 字符串(用.运算符进行字符串连接)
  • 其他变量的引用。

“布尔类型”(“Boolean”)

Perl没有内置的布尔类型。if语句中的scalar变量仅在以下情况下被认为是“false”:

  • undef
  • 数值0
  • 空字符串””
  • 字符串”0”。

实际上,当一个函数声称它返回“true”,返回值往往是1,而当一个函数声称它返回“false”,返回值往往是一个空字符串”“。

弱类型

无法判定一个scalar包含的是一个数值还是字符串。更准确的来说,我们没有必要知道这个信息。一个scalar按照数值还是字符串的方式参与运算,是完全取决于运算符的。(如果无法转换成数值则会抛出一个警告):

my $str1 = "4G";
my $str2 = "4H";
print $str1 .  $str2; # "4G4H"
print $str1 +  $str2; # "8" 并且抛出两个警告
print $str1 eq $str2; # "" (空字符串,也就是false)
print $str1 == $str2; # "1" 并且抛出两个警告

# 经典错误
print "yes" == "no"; # "1" 并且抛出两个警告,按数值方式参与运算,两边求值结果都是0
#教训是应该总是在恰当的情况下使用正确的运算符,对于比较数值和字符串有两套不同的运算符:
# 数值运算符:  <,  >, <=, >=, ==, !=, <=>, +, *
# 字符串运算符:    lt, gt, le, ge, eq, ne, cmp, ., x

Array变量

Array变量是包含一个scalar列表的、由从0开始的整形数为下标存取的变量。

  • 用一个()包围的scalar列表来初始化
  • 使用 $ 来存取array中的值,因为取到的值是一个scalar而非array
  • 可以使用 负数 作为下标
  • scalar变量 $var 和 array变量 @var 可以同时存在,但应避免。
  • array 长度 (scalar @array)
  • 最大下标 ($#array)
  • 调用Perl脚本时使用的参数列表被保存在内置的array变量@ARGV中。
  • 变量可以被插入到字符串中被求值
  • 用反斜杠对@,$进行转义,或者将双引号改为单引号,可以避免对字符串中的变量名求值。
my @array = ("print","these","strings","out","for","me");
print $array[0]; # "print"
print $array[6]; # 返回undef,打印""并且抛出一个警告
print $array[-1]; # "me"
print $array[-7]; # 返回undef,打印""并且抛出一个警告
print "This array has ".(scalar @array)."elements"; # "This array has 6 elements"
print "The last populated index is ".$#array;      # "The last populated index is 5"
print "Hello $string"; # "Hello world"
print "@array";        # "print these strings out for me"
print "Hello \$string"; # "Hello $string"
print 'Hello $string';  # "Hello $string"
print "\@array";        # "@array"
print '@array';         # "@array"

Hash变量

Hash变量是包含一个scalar列表的、由字符串为下标存取的变量。

  • 与array初始化相似。双箭头符号=>被称为fat comma(胖逗号), 与逗号完全等价。 Hash变量由偶数个元素组成的列表来声明,其中偶数下标(0、2、……)的元素都被当做字符串使用。
  • 与array一样,用$来存取hash中的值,因为取到的值是scalar而非hash。
  • scalar变量 $var 和hash变量 %var 可以同时存在。
  • hash 和 array 可以相互转化。(细节参考代码注释)
  • 方括号是一个有效的数值运算符,而花括号是一个有效的字符串运算符
my %scientists = (
	"Newton"   => "Isaac",
	"Einstein" => "Albert",
	"Darwin"   => "Charles",
);

print $scientists{"Newton"};   # "Isaac"
print $scientists{"Dyson"};    # 返回undef,打印""并且抛出一个警告

#原先hash中的键和值在转换后的array中交替出现(反向的转换也同样简单):
my @scientists = %scientists;

#然而有一点与array不同,hash中的键没有特定的保存顺序,而是以一种比较高效的方式进行存储。  
#因此,需要注意转换后的array会将hash中的键值对重新排列次序:
print "@scientists"; # 输出可能是"Einstein Albert Darwin Charles Newton Isaac"

#方括号是一个有效的数值运算符,而花括号是一个有效的字符串运算符,  
#因此事实上,作为下标的值是数值还是字符串类型其实并不重要(scalar以什么方式参与运算取决于运算符):
my $data = "orange";
my @data = ("purple");
my %data = ( "0" => "blue");

print $data;      # "orange"
print $data[0];   # "purple"
print $data["0"]; # "purple"
print $data{0};   # "blue"
print $data{"0"}; # "blue"

列表(Lists)

Perl中的列表与array和hash都不一样。 列表不是一个变量, 列表是一个暂存的 ,可以被赋值到一个array或者hash变量。如上文中array和hash的初始化列表所示。
有时,“列表”和“array”这两个词可以混用。有时,两者有微妙的区别。
例子:

("one", 1, "three", 3, "five", 5) 
("one" => 1, "three" => 3, "five" => 5)

列表暗示array的声明,使用=>暗示hash的声明,但就这两个列表自身并没有声明任何东西,它们只是列表,而且是完全相同的列表。 同样的, 空列表()可以用来声明空array或者空hash,但是perl解释器则完全无法知道将会是哪一种。一旦你理解了这一点,也就能理解Perl的这个事实:列表不能嵌套

my @array = (
	"apples",
	"bananas",
	(
		"inner",
		"list",
		"several",
		"entries",
	),
	"cherries",
);

#Perl无法知道("inner", "list", "several", "entries")应该是array还是hash,因此Perl假设两者都不是,而将其扁平化为一个一维长列表:
print $array[0]; # "apples"
print $array[1]; # "bananas"
print $array[2]; # "inner"
print $array[3]; # "list"
print $array[4]; # "several"
print $array[5]; # "entries"
print $array[6]; # "cherries"

#即使使用fat comma也会是同样的情况:
my %hash = (
	"beer" => "good",
	"bananas" => (
		"green"  => "wait",
		"yellow" => "eat",
	),
);

# 上面的代码会抛出一个警告,因为我们尝试用7个元素的列表来初始化这个hash
print $hash{"beer"};    # "good"
print $hash{"bananas"}; # "green"
print $hash{"wait"};    # "yellow";
print $hash{"eat"};     # undef,因此打印""并且抛出一个警告

#当然,这倒让连接数组变得简单了:
my @bones   = ("humerus", ("jaw", "skull"), "tibia");
my @fingers = ("thumb", "index", "middle", "ring", "little");
my @parts   = (@bones, @fingers, ("foot", "toes"), "eyeball", "knuckle");
print @parts;

上下文

Perl最独特的特性在于它的代码对于上下文是敏感的。每个Perl的表达式要么在 scalar上下文中求值,要么在 列表上下文中求值,取决于此处期望产生一个scalar还是列表。

  • Scalar的赋值会在scalar上下文求值。
  • Array或者hash的赋值会在列表上下文求值。
  • 在列表上下文中求值的scalar表达式会被转换成含有一个元素的列表。
  • 在scalar上下文中求值的列表表达式会返回列表中的最后一个scalar。
  • 在scalar上下文中求值的array表达式返回该数组的长度。
  • print内置函数在列表上下文中求对所有的参数求值。
  • 内置函数scalar强制让任何表达式在scalar上下文进行求值(参考上文获取数据长度)
my @array = "Mendeleev"; # 与'my @array = ("Mendeleev");'等价
my $scalar = ("Alpha", "Beta", "Gamma", "Pie"); # $scalar的值现在是"Pie"

my @array = ("Alpha", "Beta", "Gamma", "Pie");
my $scalar = @array; # $scalar的值现在是4

my @array = ("Alpha", "Beta", "Goo");
my $scalar = "-X-";
print @array;              # "AlphaBetaGoo";
print $scalar, @array, 98; # "-X-AlphaBetaGoo98";

引用和嵌套数据结构

  • 列表和array只能包含scalar,无法嵌套列表、array、hash。例如:
my @outer = ("Sun", "Mercury", "Venus", undef, "Mars");
my @inner = ("Earth", "Moon");
$outer[3] = @inner;
print $outer[3]; # "2"

$outer[3] 是个scalar,因此@inner就会在scalar上下文中被求值,得到数组长度。

  • scalar变量可以包含任何变量的引用,包括array和hash。用反斜杠来创建一个引用。具体使用见例子:
my $colour    = "Indigo";
my $scalarRef = \$colour;
print $colour;         # "Indigo"
print $scalarRef;      # 输出可能是 "SCALAR(0x182c180)"
print ${ $scalarRef }; # "Indigo"

#如果结果没有歧义的话,可以省略掉花括号:
print $$scalarRef; # "Indigo"

#对array或者hash的引用,可以用花括号或者更加风靡的箭头运算符`->`:
my @colours = ("Red", "Orange", "Yellow", "Green", "Blue");
my $arrayRef = \@colours;
print $colours[0];       # 直接访问array元素
print ${ $arrayRef }[0]; # 通过引用访问array元素
print $arrayRef->[0];    # 与上一句等价

my %atomicWeights = ("Hydrogen" => 1.008, "Helium" => 4.003, "Manganese" => 54.94);
my $hashRef = \%atomicWeights;
print $atomicWeights{"Helium"}; # 直接访问hash元素
print ${ $hashRef }{"Helium"};  # 通过引用访问hash元素
print $hashRef->{"Helium"};     # 与上一句等价,这种写法相当常见

声明数据结构

用方括号声明匿名array,而用花括号声明匿名hash,这两种方法返回的是声明的匿名数据结构的引用

my %account = (
	"number" => "31415926",
	"opened" => "3000-01-01",
	"owners" => [
		{
			"name" => "Philip Fry",
			"DOB"  => "1974-08-06",
		},
		{
			"name" => "Hubert Farnsworth",
			"DOB"  => "2841-04-09",
		},
	],
);

从数据结构中获取信息

解引用 account:

print "Account #", $account{"number"}, "\n";
print "Opened on ", $account{"opened"}, "\n";
print "Joint owners:\n";
print "\t", $account{"owners"}->[0]->{"name"}, " (born ", $account{"owners"}->[0]->{"DOB"}, ")\n";
print "\t", $account{"owners"}->[1]->{"name"}, " (born ", $account{"owners"}->[1]->{"DOB"}, ")\n";

如何用array的引用作茧自缚

#这个数组有5个元素:
my @array1 = (1, 2, 3, 4, 5);
print @array1; # "12345"

#然而这个array只有1个元素(一个含有5个元素的匿名array的引用):
my @array2 = [1, 2, 3, 4, 5];
print @array2; # e.g. "ARRAY(0x182c180)"

#这个scalar是一个含有5个元素的匿名array的引用:
my $array3Ref = [1, 2, 3, 4, 5];
print $array3Ref;      # e.g. "ARRAY(0x22710c0)"
print @{ $array3Ref }; # "12345"
print @$array3Ref;     # "12345"

条件分支

几种条件分支用法

  • if(){}elsif(){}else{}(注意elsif的拼写)。
  • statement if condition; 短语句强烈推荐这种写法。
  • unless(){}else{} 避免使用,可以通过条件取反转化为if...else
  • statement unless condition; 短语句强烈推荐。

三目运算符

  • 三目运算符 ?: 可以嵌套。
  • if语句在scalar上下文中进行求值。

循环

#while循环
while(condition){}

#until循环
until(condition){}

#do...while循环
do{}while();

#do...until循环
do{}until();

#for循环(已过时,避免使用)
for(init; condition; statement){}

#原生array迭代语法(for,foreach等价)
foreach my $var ( @array ){}

#range运算符 ..
foreach my $i ( 0 .. $#array ){}

#迭代hash的键值,内置函数keys获取hash所有键的array
foreach my $key ( keys %hash ){}

#使用内置函数sort对键的array进行字母序排序
foreach my $key (sort keys %hash){}

#用默认迭代器 $_
foreach ($array){print $_;}

#使用默认迭代器,如果循环里只有一句语句
print $_ foreach @array;

循环控制

nextlast 可以用来控制循环过程,相当于continue和break。
一般约定,行标写成全部大写。在循环里加上行标以后,next和last可以选择指定跳转到某个行标。下面的示例程序能够找出100以内的素数:

CANDIDATE: for my $candidate ( 2 .. 100 ) {
	for my $divisor ( 2 .. sqrt $candidate ) {
		next CANDIDATE if $candidate % $divisor == 0;
	}
	print $candidate." is prime\n";
}

Array函数

原地(In-place)array修改函数

pop、push、shift和unshift都是splice的特例。

  • pop抽取并返回array的最后一个元素
  • push向array末尾添加一个元素
  • shift抽取并返回array的第一个元素
  • unshift向array的头部插入一个元素
  • splice返回删除的一个array的切片,并且用另一个array的切片在原array中替换之

使用示例:

pop @array;
push @array, "Bob", "Alice"; # ...BobAlice
shift @array;
unshift @stack, "Hank", "Grace"; # HankGrace...
splice(@stack, 1, 4, "<<<", ">>>"); # 1 --> index , 4 --> length

从现有的array创建新的array

  • join 函数把多个字符串连接成一个字符串:
  • 在列表上下文,reverse 函数把传入的列表逆序返回,在scalar上下文,reverse 先把字符串列表连接起来,再将这个字符串反转。
  • map 函数接受一个array,并将一个操作应用于这个array中的每一个scalar $_,然后返回用这些scalar创建的array。
  • grep 函数接受一个array,并返回一个经过筛选的array。语法与map类似,而第二个参数会对array中的每个scalar$_求值,如果返回true,这个scalar就会被放到输出array中,否则就不会。
  • sort 函数对输入的array进行排序
my @elements = ("Antimony", "Arsenic", "Aluminum", "Selenium");
print @elements;             # "AntimonyArsenicAluminumSelenium"
print "@elements";           # "Antimony Arsenic Aluminum Selenium"
print join(", ", @elements); # "Antimony, Arsenic, Aluminum, Selenium"

print reverse("Hello", "World");        # "WorldHello"
print reverse("HelloWorld");            # "HelloWorld"
print scalar reverse("HelloWorld");     # "dlroWolleH"
print scalar reverse("Hello", "World"); # "dlroWolleH"

my @capitals = ("Baton Rouge", "Indianapolis", "Columbus", "Montgomery", "Helena", "Denver", "Boise");
print join ", ", map { uc $_ } @capitals; # "BATON ROUGE, INDIANAPOLIS, COLUMBUS, MONTGOMERY, HELENA, DENVER, BOISE"

print join ", ", grep { length $_ == 6 } @capitals; # "Helena, Denver"
print scalar grep { $_ eq "Columbus" } @capitals; # "1"

my @elevations = (19, 1, 2, 100, 3, 98, 100, 1056);
#默认按字母序排序
print join ", ", sort @elevations; # "1, 100, 100, 1056, 19, 2, 3, 98"
#cmp运算符适用于字符串(按字母序比较):
print join ", ", sort { $a cmp $b } @elevations; # "1, 100, 100, 1056, 19, 2, 3, 98"
#这个“宇宙飞船运算符”<=>适用于数值:
print join ", ", sort { $a <=> $b } @elevations; # "1, 2, 3, 19, 98, 100, 100, 1056"
#$a和$b总是scalar,但是它们也许是某个复杂对象的引用,那样就很难直接进行比较。如果你需要更多篇幅来描述这种比较,你可以单独创建一个子程序来描述它,并在用到它的地方提供这个子程序的名字 (你不能对grep或map这样做)。
sub comparator {
	# lots of code...
	# return -1, 0 or 1
}
print join ", ", sort comparator @elevations;

grep和map的组合形成了list comprehensions这种许多其他语言中欠缺的非常强大特性。(对一个列表中满足某个条件的所有元素上应用某个操作,而形成一个新的列表。)
就像$_ 一样,$a$b是当一对值需要比较时被填入的全局变量。

用户自定义的子程序

  • 子程序用 sub 关键字来声明。
  • 相比内置函数,自定义子程序总是接受一种输入:一个scalar的列表
  • 在子程序中,参数被保存在内置array变量 @_ 中。
  • 尽管括号可以省略,但应该总是在调用子程序的时候加上括号,便于阅读。
sub hyphenate {

  # 从array中取出第一个参数,忽略其他
  my $word = shift @_;

  # 聪明过头的list comprehension
  $word = join "-", map { substr $word, $_, 1 } (0 .. (length $word) - 1);
  return $word;
}

print hyphenate("exterminate"); # "e-x-t-e-r-m-i-n-a-t-e"

Perl以引用方式调用

Perl以引用方式调用子程序(以引用方式传递参数,总是应该在使用参数之前将它们提取出来)

提取参数

#逐个抽取@_中的参数
sub left_pad {
	my $oldString = $_[0];
	my $width     = $_[1];
	my $padChar   = $_[2];
	my $newString = ($padChar x ($width - length $oldString)) . $oldString;
	return $newString;
}

#对于不超过4个参数的情况推荐用shift通过移出元素的方法来提取@_中的参数
sub left_pad {
	my $oldString = shift @_;
	my $width     = shift @_;
	my $padChar   = shift @_;
	my $newString = ($padChar x ($width - length $oldString)) . $oldString;
	return $newString;
}

#如果没有给shift函数提供array参数,它就会默认对@_进行操作。这种用法很常见:
sub left_pad {
	my $oldString = shift;
	my $width     = shift;
	my $padChar   = shift;
	my $newString = ($padChar x ($width - length $oldString)) . $oldString;
	return $newString;
}

#一次性把所有@_中的参数提取出来。仍然是适用于少于4个参数的情形:
sub left_pad {
	my ($oldString, $width, $padChar) = @_;
	my $newString = ($padChar x ($width - length $oldString)) . $oldString;
	return $newString;
}

#对于有大量参数的子程序,或者有些参数可选或无法和其他参数组合使用的子程序,最佳实践是要求用户构造参数的hash来调用这个子程序,然后将整个@_放回到一个hash中。
print left_pad("oldString" => "pod", "width" => 10, "padChar" => "+");
sub left_pad {
	my %args = @_;
	my $newString = ($args{"padChar"} x ($args{"width"} - length $args{"oldString"})) . $args{"oldString"};
	return $newString;
}

返回值

就像其他Perl表达式一样,子程序调用也会根据上下文表现出不同的行为。用wantarray函数来检测子程序是在什么上下文中被调用的,这样就可以返回恰当类型的结果:

sub contextualSubroutine {
	# 调用这里需要一个列表,那么就返回一个列表
	return ("Everest", "K2", "Etna") if wantarray;

	# 调用者需要一个scalar,那么就返回一个scalar
	return 3;
}
my @array = contextualSubroutine();
print @array; # "EverestK2Etna"
my $scalar = contextualSubroutine();
print $scalar; # "3"

系统调用

每当一个进程在Windows或Linux系统中结束,它将产生一个16位的状态字,高8位表示返回码,值落在0到255之间,其中0约定俗成地表示无条件的成功,而其他值则表示不同程度的失败,另外8位则少有人关心,它们“表示了错误的原因,比如因为收到了信号或者产生core dump信息”。可以调用exit,用你选择的返回码(0到255之间)退出Perl脚本。

Perl调用语句启动一个子进程、等待子进程执行结束、然后继续解释执行当前的脚本。子进程结束时返回的状态字被填入内置scalar变量$?中。取出16位中的高8位来得到返回码:$? >> 8

  • 用system函数调用另一个程序,并且提供一个参数列表,system的返回值与填入 $? 的值一致:
  • 用反引号``在命令行中运行一条真正的命令,并且捕获它的标准输出。
    在scalar上下文中,整个输出被当做一整个字符串返回。
    在列表上下文中,整个输出按一个字符串的array返回,其中每个字符串是输出中的一行。
my $rc = system "perl", "anotherscript.pl", "foo", "bar", "baz";
$rc >>= 8;
print $rc; # "37"

my $text = `perl anotherscript.pl foo bar baz`;
print $text; # "foobarbaz"

#如果anotherscript.pl包含形如下面这样的代码,你就能看到上面这种结果:
use strict;
use warnings;

print @ARGV;
exit 37;

文件和文件句柄

打开文件

Scalar变量能包含一个 文件句柄
open 打开文件,必须给open提供一个打开模式
读模式<,写模式>, 追加模式>>
成功返回true, 失败返回false,并且错误消息会被填入内置变量$!

my $f = "text.txt";
my $result = open my $fh, "<", $f;

if(!$result) {
	die "Couldn't open '".$f."' for reading because: ".$!;
}

#更常见的写法:(参数列表两边要加上括号)
open(my $fh, "<", $f) || die "Couldn't open '".$f."' for reading because: ".$!;

读文件

内置函数readline从文件句柄中读取一行,包括结尾换行符(除了文件末尾的那行可能例外),如果已经读到文件末尾则返回undef。

while(1) {
	my $line = readline $fh;
	last unless defined $line;
	# 处理$line...
}

#可以用chomp移除末尾可能存在的换行符:
chomp $line;

#你也可以用eof来检测是否已经读到文件末尾:
while(!eof $fh) {
	my $line = readline $fh;
	# 处理$line...
}

#如果$line的内容恰好是'0', while(my $line = readline $fh),循环可能过早结束。  
#如果你想要这样写,Perl提供了<>功能上更安全的运算符,这种写法很常见而且也非常安全
while(my $line = <$fh>) {
	print $line
}
#或者
while(<$fh>) {
	print $_
}

写文件

使用写模式> 打开文件。然后,将文件句柄作为print方法的第0个参数。

open(my $fh2, ">", $f) || die "Couldn't open '".$f."' for writing because: ".$!;
#请注意在$fh2和后面的参数之间没有逗号。
print $fh2 "The eagles have left the nest";

#主动关闭文件句柄
close $fh2;
close $fh;

标准输入输出

有三个文件句柄以全局常量形式存在:STDIN、STDOUT和STDERR,它们在脚本开始时就被自动打开。

#要读取一行用户的输入:
my $line = <STDIN>;
#如果只是等待用户按回车:
<STDIN>;

调用<>而不提供文件句柄参数,表示从STDIN或者在Perl脚本启动时指定的参数指向的文件中读取。

文件检测

  • 内置函数-e用于测试文件是否存在。
  • 内置函数-d用于测试文件是否是目录。
  • 内置函数-f用于测试文件是否是普通文件。 (用表格总结其他检测函数)

正则表达式

建议就是尽可能避免引入不必要的复杂性。

  • =~ m// 运算符进行正则表达式匹配。
#在scalar上下文中,=~ m//在成功时返回true,而失败是返回false。
my $string = "Hello world";
if($string =~ m/(\w+)\s+(\w+)/) {
	print "success";
}

#圆括号表示匹配组,匹配成功以后,匹配组被填入内置变量$1、$2、$3……:
print $1; # "Hello"
print $2; # "world"

#在列表上下文中,=~ m//返回$1、$2……组成的列表。
my $string = "colourless green ideas sleep furiously";
my @matches = $string =~ m/(\w+)\s+((\w+)\s+(\w+))\s+(\w+)\s+(\w+)/;
print join ", ", map { "'".$_."'" } @matches; 
# prints "'colourless', 'green ideas', 'green', 'ideas', 'sleep', 'furiously'"
  • =~ s/// 运算符进行正则表达式替换。
my $string = "Good morning world";
$string =~ s/world/Vietnam/;
print $string; # "Good morning Vietnam"
#必须在 =~ s///运算符左边提供一个scalar变量,如果你提供了字面字符串,会返回一个错误。

#/g标志表示“全局匹配”。
#在scalar上下文中,每次 =~ m//g调用都会匹配下一个匹配项,成功返回true,失败返回false。
my $string = "a tonne of feathers or a tonne of bricks";
while($string =~ m/(\w+)/g) {
  print "'".$1."'\n";
}

#在列表上下文中,=~ m//g一次性返回所有匹配的结果。
my @matches = $string =~ m/(\w+)/g;
print join ", ", map { "'".$_."'" } @matches;

#每次 =~ s///g 调用会进行一次全局的查找/替换,并且返回匹配的次数。
# 先不用/g进行一次替换
$string =~ s/[aeiou]/r/;
print $string; # "r tonne of feathers or a tonne of bricks"

# 再替换一次
$string =~ s/[aeiou]/r/;
print $string; # "r trnne of feathers or a tonne of bricks"

# 用/g全部替换
$string =~ s/[aeiou]/r/g;
print $string, "\n"; # "r trnnr rf frrthrrs rr r trnnr rf brrcks"

#/i标志表示查找替换对于大小写不敏感。
#/x标志允许正则表达式中包含空白符(例如换行符)和注释。

"Hello world" =~ m/
  (\w+) # one or more word characters
  [ ]   # single literal space, stored inside a character class
  world # literal "world"
/x;
# returns true

模块和包

在Perl中,模块(module)和包(package)是不同的东西。

模块

模块是包含在另一个Perl文件(脚本或模块)中的一个.pm文件,是与.pl Perl脚本语法完全相同的文本文件。模块在被加载时会自顶向下执行,在结尾处返回一个true表示加载成功

例如有个模块位于 /foo/bar/baz/Demo/StringUtils.pm,并且有如下内容:

use strict;
use warnings;

sub zombify {
	my $word = shift @_;
	$word =~ s/[aeiou]/r/g;
	return $word;
}

return 1;
  • 设置环境变量PERL5LIB让Perl解释器能够找到。
    export PERL5LIB=/foo/bar/baz:$PERL5LIB
  • 使用内置函数require查找(PERL5LIB目录)并执行模块(加载)。
    require Demo::StringUtils
  • 用双冒号::作为目录的分隔符。
use strict;
use warnings;

require Demo::StringUtils;

print zombify("i want brains"); # "r wrnt brrrns"

如果main.pl包含很多require调用,而且每个被加载的模块又包含更多require调用,那我们要找到zombify()子程序最初的定义就太困难了。解决方案是使用包。

包是用来声明子程序的命名空间。

  • 所有的子程序默认都被声明在当前包中,程序开始执行时,位于main包,使用内置函数package切换包
  • 使用双冒号::作为命名空间的分隔符。
  • 调用子程序时, 默认会调用当前包中的子程序, 可以显示指定包的名字。
use strict;
use warnings;

sub subroutine {
	print "universe";
}

package Food::Potatoes;

# 没有冲突:
sub subroutine {
	print "kingedward";
}

subroutine();                 # "kingedward"
main::subroutine();           # "universe"
Food::Potatoes::subroutine(); # "kingedward"

所以对上面描述的问题的一个符合逻辑的解决方案就是把/foo/bar/baz/Demo/StringUtils.pm改为:

use strict;
use warnings;

package Demo::StringUtils;

sub zombify {
	my $word = shift @_;
	$word =~ s/[aeiou]/r/g;
	return $word;
}

return 1;

然后把main.pl改为:

use strict;
use warnings;

require Demo::StringUtils;

print Demo::StringUtils::zombify("i want brains"); # "r wrnt brrrns"

在Perl语言中包和模块是彼此独立完全不同的两个功能,它们恰好都是用双冒号作为分隔符根本就是个掩人耳目的把戏。在一个脚本或者模块中多次切换包是可行的,在不用位置的多个文件中使用同一个包名也是可行的。调用require Foo::Bar并不会去查找并且加载一个有package Foo::Bar的文件,也不一定会加载定义在Foo::Bar命名空间里的子程序。调用 require Foo::Bar 仅仅表示加载一个名为Foo/Bar.pm的问题,与其中有什么包的声明没有任何关系,也许那个文件中声明了package Baz::Qux和其他乱七八糟的内容。
同样的,调用Baz::Qux::processThis()子程序并不一定要声明在名叫Baz/Qux.pm的文件里,它可能被定义在任何地方
分离这两种功能可能是Perl中最糟糕的一个设计,而如果把它们视作分开的功能,将带来混乱,以及让人抓狂的代码。值得庆幸的是,主流的Perl程序员总是遵循下面两个规则:

  • Perl脚本(.pl文件)不应该包含package声明。
  • Perl模块(.pm文件)必须包含且仅包含一个package声明,且包名与它的文件名、所在的位置一致。
    例如,模块Demo/StringUtils.pm必须由package Demo::StringUtils开头。

因此,你会发现实际工作中,绝大部分由可靠的第三方提供的“包”和“模块”的概念是可以交换混用的。然而,很重要的是,你千万不能把这个当做承诺,因为将来有一天你一定会碰上一个疯子写的代码。

Perl的面向对象

概念

  • 类就是包含一组方法的包。
  • 对象只是一个引用(也就是一个scalar变量),它恰好知道自己属于哪个类。
  • 设置(set)一个引用它所指向的内容属于的类,使用bless
  • 获取(get)引用所指向的内容属于的类(如果有的话),使用ref
  • 方法只是一个子程序,接受对象(或者对于类的方法,就是包名)作为第一个参数。
  • 使用 $obj->method() 可以调用对象的方法,用Package::Name->method()可以调用类的方法。

示例模块Animal.pm包含Animal类,内容如下:

use strict;
use warnings;

package Animal;

sub eat {
	# 第一个参数总是操作所基于的对象
	my $self = shift @_;

	foreach my $food ( @_ ) {
		if($self->can_eat($food)) {
			print "Eating ", $food;
		} else {
			print "Can't eat ", $food;
		}
	}
}

# 就这个参数来说,假设动物可以吃任何东西
sub can_eat {
	return 1;
}

return 1;

然后我们可以这样使用这个类:

require Animal;

my $animal = {
	"legs"   => 4,
	"colour" => "brown",
};                       # $animal是一个普通的hash的引用
print ref $animal;       # "HASH"
bless $animal, "Animal"; # 现在它是"Animal"类的对象
print ref $animal;       # "Animal"

#仍然可以按以前的方式操作这个hash:
print "Animal has ", $animal->{"legs"}, " leg(s)";

#但你也可以同样用->运算符调用这个对象的方法,就像这样:
$animal->eat("insects", "curry", "eucalyptus");
#等价于
Animal::eat($animal, "insects", "curry", "eucalyptus")

注意: 任何引用都可以被转换(bless)成任何类的对象。需要由你来保证:这个引用指向的内容可以被当做这个类的对象来使用,并且被转换成的这个类存在,并且已经被加载了。

构造函数

构造函数是这个类返回新对象的类方法, 任何名字都可以。对于类的方法,第一个参数是类名而不是一个对象,在这个例子里就是”Animal”:

use strict;
use warnings;

package Animal;

sub new {
	my $class = shift @_;
	return bless { "legs" => 4, "colour" => "brown" }, $class;
}

#然后像下面这样使用:
my $animal = Animal->new();

继承

use parent 继承一个基类,如:

use strict;
use warnings;

package Koala;

# 继承自Animal
use parent ("Animal");

# 重载一个方法
sub can_eat {
	my $self = shift @_; # 没有使用,你也可以直接在这里写"shift @_;"
	my $food = shift @_;
	return $food eq "eucalyptus";
}

return 1;

下面是一些示例程序:

use strict;
use warnings;

require Koala;

my $koala = Koala->new();

$koala->eat("insects", "curry", "eucalyptus"); # 只吃eucalyptus

最后那个方法调用尝试执行Koala::eat($koala, "insects", "curry", "eucalyptus"),但子程序eat()并没有在Koala包里定义。然而,因为Koala有父类Animal,Perl解释器会再尝试调用Animal::eat($koala, “insects”, “curry”, “eucalyptus”),这回没问题。请注意Animal类是如何自动被Koala.pm加载的。
因为use parent接受一组父类的名字,所以Perl支持多重继承,当然也就包含了它所带来的所有好处和噩梦。

BEGIN块

BEGIN块在perl解释完这个代码块以后就立即被执行,甚至在文件剩下的部分被解释之前,而这个代码块在运行时则被忽略:

use strict;
use warnings;

print "This gets printed second";

BEGIN {
	print "This gets printed first";
}

print "This gets printed third";

BEGIN块总是首先执行。如果你创建了多个BEGIN块(别这么做),它们将按照解释器解释它们的顺序自上而下执行。BEGIN即使出现在脚本中间(别这么做)或者脚本最后(也别这么做),它也会首先被执行。不要搞乱自然的代码执行顺序,总是把BEGIN块放在开头!

BEGIN块在解释完后立即被执行,执行完毕以后将从这个BEGIN块结束处继续解释剩下的代码。如果BEGIN块以外的任何代码被执行了,那么整个脚本或者模块就已经被解释了一遍,且仅有一遍。

use strict;
use warnings;

print "This 'print' statement gets parsed successfully but never executed";

BEGIN {
	print "This gets printed first";
}

print "This, also, is parsed successfully but never executed";

...because e4h8v3oitv8h4o8gch3o84c3 there is a huge parsing error down here.

(译者注:上面程序的最后一行不是注释,作者写最后一行是构造一个语法错误,因而造成BEGIN块在解释到这里之前就已经被执行,而BEGIN块执行完毕以后继续恢复解释,一旦遇上语法错误,脚本其他部分将不会再被执行。)

因为它们在脚本编译时就执行,BEGIN块即使在条件分支中也仍然会在编译时就运行,哪怕条件将被判定为false,因为在那时条件还根本没有被求值,甚至可能永远不会被求值。

if(0) {
	BEGIN {
		print "This will definitely get printed";
	}
	print "Even though this won't";
}

#不要把BEGIN块放在条件分支里!如果你要在编译时做一些条件判断,把这个条件判断放在BEGIN块里面:
BEGIN {
	if($condition) {
		# etc.
	}
}

use

好,现在让我们来理解一下包、模块、类的方法和BEGIN块那模棱两可的行为以及语义,我会来解释一下超级常见的use函数。

下面三条语句:
use Caterpillar ("crawl", "pupate");
use Caterpillar ();
use Caterpillar;

分别和下面的三段等价:
BEGIN {
	require Caterpillar;
	Caterpillar->import("crawl", "pupate");
}
BEGIN {
	require Caterpillar;
}
BEGIN {
	require Caterpillar;
	Caterpillar->import();
}
  • use只是BEGIN块的伪装,同样的警告对此也适用。
  • use 语句必须总是放在文件开头,并且永远不要放在条件分支里。
  • import()并不是Perl的内置函数,它只是一个用户自定义的类方法。定义或者继承import()函数的重任就落在写Caterpillar这个包的程序员身上了。这个方法理论上可以接受任何东西作为参数,也可以对参数做任何操作。
  • use Caterpillar; 可以做任何事情,你需要查询Caterpillar.pm的文档来判断到底会发生什么。
  • 请注意require Caterpillar是如何加载一个名为Caterpillar.pm的模块的,而Caterpillar->import()则调用定义在Caterpillar包里的子程序import()。我们只能一起期待这里的模块和包是一致的!

Exporter

定义一个import()方法最常见的办法是从Exporter模块继承下来。Exporter是一个核心模块,也是Perl语言中成为事实标准的核心功能。在Exporter的import()实现中,你传入的参数列表将被认为是子程序名字的列表,当一个子程序被import(),它在当前包和原来所在的包里就都可以被使用了。

用一个例子最能帮助理解这个概念。Caterpillar.pm的内容如下:

use strict;
use warnings;

package Caterpillar;

# 继承自Exporter
use parent ("Exporter");

sub crawl  { print "inch inch";   }
sub eat    { print "chomp chomp"; }
sub pupate { print "bloop bloop"; }

our @EXPORT_OK = ("crawl", "eat");

return 1;

包变量 @EXPORT_OK 应该包含子程序名字的列表。

另一块代码就可以通过名字来import()这些子程序,一般使用use语句:

use strict;
use warnings;
use Caterpillar ("crawl");

crawl(); # "inch inch"

在这种情况下,当前包是main所以crawl()实际上是调用了main::crawl(),(因为被导入了)映射到Caterpillar::crawl()。

注意:不管@EXPORT_OK的内容是什么,通过“常规写法”使用这些函数总是可以的:

use strict;
use warnings;
use Caterpillar (); # 没有提供任何子程序名,import()不会被调用

# 然而……
Caterpillar::crawl();  # "inch inch"
Caterpillar::eat();    # "chomp chomp"
Caterpillar::pupate(); # "bloop bloop"

Perl没有私有方法,习惯上在希望私有的方法名前面有一个或者两个下划线。

@EXPORT

Exporter模块还定义了一个包变量叫@EXPORT,也包含一组子程序名。

use strict;
use warnings;

package Caterpillar;

# 继承自Exporter
use parent ("Exporter");

sub crawl  { print "inch inch";   }
sub eat    { print "chomp chomp"; }
sub pupate { print "bloop bloop"; }

our @EXPORT = ("crawl", "eat", "pupate");

return 1;

如果没有给import()传入任何参数,@EXPORT中写出的子程序将全部被导出,就像这样:

use strict;
use warnings;
use Caterpillar; # 调用import()但不提供参数

crawl();  # "inch inch"
eat();    # "chomp chomp"
pupate(); # "bloop bloop"

不过我们又回到了那种情况,没有其他提示的话,我们很难知道crawl()原先是在哪儿定义的。这件事情有两个寓意:

  • 当我们用Exporter创建模块的时候,不要用@EXPORT来导出子程序,总是让调用者以“常规方法”调用子程序,或者显式地import()它们(使用比如:use Caterpillar ("crawl")提供了一条很强的线索,告诉我们可以从Caterpillar.pm中找到crawl()的定义)。
  • 当use一个使用Exporter的模块时,总是显式写明你希望import()的子程序,如果你不想import()任何子程序,而是用常规方法引用它们,你必须显式提供一个空的列表:use Caterpillar ()。(不调用import())

杂项

  • 核心模块Data::Dumper可以被用于输出任意scalar到屏幕上,这是非常有用的调试工具。
  • 还有另一种语法qw{ }可以用来声明array,常常在use语句用到它:
    use Account qw{create open close suspend delete};
    有许多引号一样的运算符
  • =~ m//=~ s///运算符中,你可以用花括号代替斜杠作为正则表达式的分隔符,当你的正则表达式中包含很多斜杠时候就很有用了,要不然你就得使用很多反斜杠来进行跳脱。例如,=~ m{///}将匹配三个斜杠而=~ s{^https?://}{}会移除URL的协议部分。
  • Perl没有CONSTANTS。现在不鼓励使用它们,不过以前不一定。常量实际上就是省略括号的子程序调用。
  • 有时候人们省略hash键两旁的引号,写成$hash{key}而非$hash{“key”}。 当这个孤零零的key恰好表示字符串”key”而不是子程序调用key()的时候,它们才能侥幸成功。
  • 如果你看到一块由两个左尖括号作为分隔符包围起来的没有格式化的代码,就像<<EOF,可以通过在Google中搜索“here-doc”找到它的解释。(译者注:这是再一次吐槽因为Perl滥用符号导致难以搜索。)
  • 警告!许多内置函数调用时都可以不给参数,那样它们就会使用$_代替,希望这可以帮助你理解下面这种写法:
print foreach @array;

#还有
foreach ( @array ) {
	next unless defined;
}

这种写法在代码重构时将会遇到麻烦。