Perl简介
翻译来源 Learn Perl in about 2 hours 30 minutes
Perl是一种动态的、动态类型的、高级的、脚本(解释)语言,与PHP和Python最为相似。Perl的语法在很大程度上要归功于古老的shell脚本工具,而且它以过度使用混乱的符号而闻名,其中大部分符号都无法在Google上找到。Perl的shell脚本传统使它非常适合编写胶水代码:将其他脚本和程序连接在一起的脚本。Perl非常适合处理文本数据并产生更多的文本数据。Perl是广泛的、流行的、高度可移植的和良好支持的。Perl的设计理念是 There’s More Than One Way To Do It(TMTOWTDI)(与Python形成鲜明对比,Python: here should be one - and preferably only one - obvious way to do it.)。
Perl有令人讨厌之处,但它也有一些伟大的可取之处。在这一点上,它和其他所有的编程语言一样。
本文件的目的是提供教程,而不是向你推荐。它的适合对象是像这样的人:
- 不喜欢Perl官方文档http://perl.org/。因为它的技术性太强,而且占用太多篇幅去处理非常不寻常的边缘情况。
- 通过公理和例子最快速地学习新的编程语言。
- 已经懂得一般的编程,除了完成工作所需之外,不需要使用Perl。
本文档已尽量简短。
一、初步说明
本文件中的几乎每一个陈述性声明都可以适用以下内容:严格解释上,实际上的情况要复杂得多。如果你看到一个严重的错误,请指出来,但我保留对某些关键错误解释的权利。
在整个文档中,我都在使用示例打印语句来输出数据,但是没有追加换行符。这样做的目的是为了防止混乱,更多地关注于每个例子中被打印的实际字符串更重要。如果实际运行代码,本文中很多例子会导致很多字都挤在一行。请尽量忽略这一点。
二、Hello world
Perl脚本是一个扩展名为.pl
的文本文件。
下面是helloworld.pl
的内容:
use strict; |
Perl脚本由Perl解释器perl
或perl.exe
解释。
perl helloworld.pl [arg0 [arg1 [arg2 ...]]] |
一些额外的说明: Perl的语法是高度自由的,它会允许你做一些看起来模棱两可的语句和不可预测的行为。我没有在这里解释这些行为是什么,但你要做的就是尽量避免它们。避免它们的方法是在你创建的每一个Perl脚本或模块的最上面加上use strict; use warnings;
。像use foo;
这样的语句就是预指令。预指令是给perl.exe
的一个信号,它在程序开始运行之前,在进行初始语法验证时生效。当解释器在运行时遇到这些语句时,它们不在会发挥作用。
;
是语句的结束符。#
是注释的开始,一条注释一直持续到行结束,Perl没有块注释语法。
三、变量
Perl变量有三种类型:标量、数组和哈希。每种类型都有自己的符号:$
、@
和%
。变量是用my
声明的,并且作用在一定范围内,直到括号中的块或文件结束。
1. 标量
一个标量可以包含:
undef
(在Python中对应于None
,在PHP中对应于null
)- 数值(Perl不区分整数和浮点数)
- 字符串
- 对其他任意变量的引用
my $undef = undef; |
使用.
操作符进行字符串连接(与PHP相同):
print "Hello ".$string; # "Hello world" |
2. 布尔值
Perl中没有布尔数据类型 if
语句中的标量只有在以下情况下才会为被认为false
:
undef
- number 0
- string
""
- string
"0"
Perl文档中反复宣称,在某些情况下,函数会返回true
或false
值。实际上,当一个函数声称返回true
时,它通常返回1,当它声称返回false
时,它通常返回空字符串""
。
3. 弱类型
无法确定一个标量是包含number
还是string
更准确地说,永远都不应该去区分。一个标量的行为是像一个数字还是一个字符串,取决于使用它的运算符。当作为字符串使用时,一个标量将表现得像一个字符串。当作为数字使用时,标量将表现得像一个数字(如果使用错误,会发出警告)。
my $str1 = "4G"; |
在正确的情况下请始终使用正确的运算符,有单独的运算符用于比较作为数字的标量和作为字符串的标量。
# Numerical operators: <, >, <=, >=, ==, !=, <=>, +, * |
4. 数组
数组变量是一个以0开头的整数为索引的标量列表,在Python中称为列表,在PHP中称为数组。数组是用一个括号中的标量列表来声明的。
my @array = ( |
你必须使用$
来访问一个数组中的值,因为被访问的值是一个标量而不是一个数组。
print $array[0]; # "print" |
您可以使用负数索引来访问从末尾开始并向前移动的条目。
print $array[-1]; # "me" |
在标量$var
和包含标量条目$var[0]
的数组@var
之间不存在冲突,但是可能会引起读者的混淆,所以要避免这种情况。
数组长度:
print "This array has ".(scalar @array)."elements"; # "This array has 6 elements" |
调用原始Perl脚本的参数存储在内置的数组变量@ARGV
中。
变量可以插值成字符串:
print "Hello $string"; # "Hello world" |
请注意 有一天你会把某人的电子邮件地址放在一个字符串里面,比如"jeff@gmail.com"
。这将导致Perl寻找一个名为@gmail
的数组变量来插值到字符串中,没有找到会导致运行时错误。有两种方法可以防止插值:使用反斜杠转义,或者使用单引号代替双引号。
print "Hello \$string"; # "Hello $string" |
5. 哈希值
哈希值是一个由字符串索引的标量列表。在Python中,它被称为一个字典;在PHP中,它被称为一个数组。
my %scientists = ( |
请注意这个声明与数组声明是多么的相似。事实上,双箭头符号=>
被称为胖逗号,因为它只是逗号分隔符的同义词。哈希是用一个元素数为偶数的列表来声明的,其中偶数元素(0,2,…)都被当作字符串。
你必须使用$
来访问哈希值,因为被检索的值是标量而不是哈希值。
print $scientists{"Newton"}; # "Isaac" |
注意这里使用的括号,同样,在标量$var
和包含标量条目$var{"foo"}
的哈希%var
之间不存在冲突。
你可以将一个哈希值直接转换为一个有两倍条目的数组,在键和值之间交替进行(反之同样简单):
my @scientists = %scientists; |
然而,与数组不同,哈希的键没有基本的顺序。它们将以任何更有效的顺序被返回。所以请注意,在转换后的数组中,键被重新排列了顺序,但保留了键值对。
print "@scientists"; # something like "Einstein Albert Darwin Charles Newton Isaac" |
总之,你必须使用方括号从数组中获取一个值,但你必须使用大括号从哈希中获取一个值。方括号实际上是一个数字运算符,而大括号实际上是一个字符串运算符。提供的索引是数字还是字符串则不影响操作:
my $data = "orange"; |
6. Lists
Perl中的列表和数组或者哈希又是两回事。你刚才已经看到了几个列表:
( |
列表不是一个变量 列表是一个短暂的值,它可以被分配给一个数组或哈希。这就是为什么声明数组和哈希变量的语法是相同的。在很多情况下,list
和array
这两个术语可以互换使用,但同样有很多情况下,list
和array
表现出了微妙的不同,而且非常容易混淆。
记住=>
只是一个符号,看这个例子:
("one", 1, "three", 3, "five", 5) |
使用=>
表示这些列表中的一个是数组声明,另一个是哈希声明。但就它们本身而言,它们都不是任何东西的声明,它们只是列表,相同的列表。比如:
() |
这里甚至没有任何提示。这个列表可以用来声明一个空数组或空哈希,而perl解释器显然没有办法判断这两种情况。一旦你理解了Perl的这个奇怪的方面,你也会明白为什么下面的事实一定是真的:列表值不能被嵌套。尝试下面的例子:
my @array = ( |
Perl无法知道("inner", "list", "several", "entry")
是一个内部数组还是一个内部哈希。因此,Perl假设两者都不是,并将list
扁平化为一个长的list
:
print $array[0]; # "apples" |
无论使不使用=>
,都是一样的:
my %hash = ( |
当然,这确实可以方便地将多个数组连在一起:
my @bones = ("humerus", ("jaw", "skull"), "tibia"); |
后面再详细讲述。
四、Context
Perl最突出的特点是它的代码是上下文敏感的。Perl中的每一个表达式都是在标量上下文或列表上下文中被评估的,这将决定它是要产生一个标量还是列表。如果不知道一个表达式是在什么样上下文中,就不能确定它是什么类型。
一个标量赋值语句my $scalar =
,在标量上下文中评估其表达式,在这里它被赋值为"Mendeleev"
。
my $scalar = "Mendeleev"; |
给一个数组或哈希赋值,比如my @array =
或my %hash =
在上下文中评估其表达式,被赋值为("Alpha", "Beta", "Gamma", "Pie"
)(或("Alpha"=>"Beta", "Gamma"=>"Pie"
),两者是等价的):
my @array = ("Alpha", "Beta", "Gamma", "Pie"); |
一个在列表上下文中的标量表达式会被默默地转换为一个单元素列表:
my @array = "Mendeleev"; # same as 'my @array = ("Mendeleev");' |
在标量上下文中评估的数组表达式返回数组的长度:
my @array = ("Alpha", "Beta", "Gamma", "Pie"); |
在标量上下文中评估的列表表达式(列表不同于数组,还记得吗?)不是返回列表的长度,而是返回列表中的最后一个标量。
my $scalar = ("Alpha", "Beta", "Gamma", "Pie"); |
print
内置函数在列表上下文中评估所有的参数。事实上,print
可以接受一个无限的参数列表,并逐一打印,这意味着它可以直接用于打印数组:
my @array = ("Alpha", "Beta", "Goo"); |
请注意 许多Perl表达式和内置函数根据其评估的上下文显示出截然不同的行为。最突出的例子是函数reverse
。在 list 上下文中,reverse
把它的参数当作一个list
,并将这个list
反转。在标量上下文中,reverse
将整个列表连接在一起,然后将其反转为一个词。
print reverse "hello world"; # "hello world" |
您可以使用scalar
内置函数强制任何表达式在标量上下文中被评估:
print scalar reverse "hello world"; # "dlrow olleh" |
还记得我们之前如何使用scalar
,来获取数组的长度吗?
五、引用和嵌套数据结构
与列表不能包含列表作为元素一样,数组和哈希也不能包含其他数组和哈希作为元素。它们只能包含标量。看看我们尝试后会发生什么:
my @outer = ("Sun", "Mercury", "Venus", undef, "Mars"); |
$outer[3]
是一个标量,所以它需要一个标量值。当你试图给它赋一个像@inner
这样的数组值时,@inner
是在标量上下文中评估的。这和赋值标量@inner
是一样的,也就是数组@inner
的长度,也就是2。
然而,一个标量变量可以包含对任何变量的引用,包括数组变量或哈希变量。这就是在Perl中创建更复杂的数据结构的方式。
使用反斜杠创建一个引用。
my $colour = "Indigo"; |
任何时候,你都可以使用一个变量的名称,你可以在其中加入一些括号,然后,在括号中加入一个变量的引用。
只要结果不含糊,你也可以省略括号。
print $$scalarRef; # "Indigo" |
如果你的引用是对数组或哈希变量的引用,你可以使用大括号或使用更流行的箭头操作符->
来获取数据:
my @colours = ("Red", "Orange", "Yellow", "Green", "Blue"); |
1. 声明数据结构
这里有四个例子,但在实践中,最后一个是最常用的:
my %owner1 = ( |
这样的定义方式看起来很麻烦,但你可以把它简化为:
my %owner1 = ( |
也可以使用不同的符号来声明匿名数组和哈希。对于匿名数组使用方括号,对于匿名哈希使用大括号。每种情况下返回的值都是对相关匿名数据结构的引用。仔细观察,这样做的结果和上面的%account
完全一样:
# Braces denote an anonymous hash |
或者下面的简化版(这是你在行内声明复杂数据结构时真正应该使用的形式):
my %account = ( |
2. 从数据结构取值
现在,让我们假设你有上面的%account
。在上面的每种情况下,你都可以通过反转同样的过程将信息打印出来。所以这里有四个例子,同样其中最后一个是最常用的:
my $ownersRef = $account{"owners"}; |
可以简化为:
my @owners = @{ $account{"owners"} }; |
或者使用引用和->
操作符:
my $ownersRef = $account{"owners"}; |
而如果我们完全跳过所有的中间值:
print "Account #", $account{"number"}, "\n"; |
3. How to shoot yourself in the foot with array references
上面那句话怎么翻译,求大佬指教~~
下面的数组有五个元素:
my @array1 = (1, 2, 3, 4, 5); |
然而,这个数组只有一个元素(恰好是对一个匿名的五元素数组的引用)。
my @array2 = [1, 2, 3, 4, 5]; |
这个标量是对一个匿名的五元素数组的引用。
my $array3Ref = [1, 2, 3, 4, 5]; |
六、条件语句
1. if ... elsif ... else ...
没有特别的地方,除了elsif
的拼写方法:
my $word = "antidisestablishmentarianism"; |
Perl提供了一个更短的if
语法,强烈推荐在短语句中使用:
print "'", $word, "' is actually enormous" if $strlen >= 20; |
2. unless .. else ...
my $temperature = 20; |
一般来说,最好像避免新冠一样避免使用unless
块,因为它们非常容易混淆。一个unless [... else]
代码块可以简单地重构为if[... else]
代码块。幸运的是,没有elsunless
关键字。
相比之下,强烈推荐使用下面这个,因为它很容易阅读:
print "Oh no it's too cold" unless $temperature > 15; |
3. 三元运算符
三元运算符?:
允许在语句中嵌入简单的if
语句。其规范用法是单数/复数形式。
my $gain = 48; |
建议:单数和复数最好在两种情况下都拼全。不要做一些像下面这样的聪明事,因为任何人在代码库中搜索too
或tee
来代替的话,都不会找到这一行:
my $lost = 1; |
三元运算符支持嵌套使用:
my $eggs = 5; |
if
语句在标量上下文中评估它们的条件。例如,if(@array)
当且仅当@array
有1个或更多元素时才返回true
,这些元素是什么并不重要(它们可能包含undef
或其他false
值)。
七、循环
有不只一种语法。
Perl有传统的while
循环:
my $i = 0; |
Perl也提供until
关键字:
my $i = 0; |
这些do
循环几乎等同于上面的内容(如果@array
为空,会发出警告):
my $i = 0; |
和
my $i = 0; |
基本的C风格for
循环也是可用的。请注意我们是如何在for
语句中加入my
的,声明$i
只在循环的范围内:
for(my $i = 0; $i < scalar @array; $i++) { |
这种for
循环被认为是老式的,应该尽量避免,在列表上进行本地迭代要好得多。注意:与PHP不同,for
和foreach
关键字是同义词,只要使用任何看起来最易读的词就可以了:
foreach my $string ( @array ) { |
如果你确实需要下表值,范围运算符..
,可以创建一个匿名的整数列表。
# $#array 返回数组最后一个元素的下标 空数组返回-1 |
你不能直接在哈希上迭代,但是你可以在它的键上迭代。使用keys
内置函数来检索一个包含哈希所有键的数组。然后使用我们用于数组的foreach
方法:
foreach my $key (keys %scientists) { |
由于哈希没有基本的顺序,所以键可以以任何顺序返回。使用内置的sort
函数对键数组进行事先的字母排序:
foreach my $key (sort keys %scientists) { |
如果你没有提供一个显式迭代器,Perl会使用一个默认的迭代器$_
。$_
是内置变量中第一个也是最友好的变量:
foreach ( @array ) { |
如果使用默认的迭代器,并且你只想在你的循环里面放一条语句,你可以使用超短循环语法:
print $_ foreach @array; |
1. 循环控制语句
next
和last
可以用来控制一个循环的进度。在大多数编程语言中,这两个词分别被称为continue
和break
。我们可以为任何循环提供一个标签。按照惯例,标签是用ALLCAPITALS(大写)
书写的。给循环贴上标签后,next
和last
可以针对这个标签进行操作。这个例子用于找到100以下的质数:
CANDIDATE: for my $candidate ( 2 .. 100 ) { |
八、数组函数
1. 修改原数组内容
我们将使用@stack
来演示:
my @stack = ("Fred", "Eileen", "Denise", "Charlie"); |
pop
提取并返回数组的最后一个元素:
print pop @stack; # "Charlie" |
push
将额外的元素添加到数组的末尾:
push @stack, "Bob", "Alice"; |
shift
提取并返回数组的第一个元素:
print shift @stack; # "Fred" |
unshift
则在数组的开头插入新元素:
unshift @stack, "Hank", "Grace"; |
pop
、push
、shift
和unshift
都是split
的特殊情况,split
删除并返回一个数组分片,用一个不同的数组分片替换:
print splice(@stack, 1, 4, "<<<", ">>>"); # "GraceEileenDeniseBob" |
2. 创建新数组
Perl提供了以下函数,这些函数可以作用于数组来创建其他数组。
join
函数将许多字符串连接成一个:
my @elements = ("Antimony", "Arsenic", "Aluminum", "Selenium"); |
在列表上下文中,reverse
函数以反向顺序返回一个列表。在标量上下文中,reverse
将整个列表连接在一起,然后将其反转为一个单词:
print reverse("Hello", "World"); # "WorldHello" |
map
函数接收一个数组作为输入,并对这个数组中每个标量$_
进行操作,然后从结果中构造一个新的数组。要执行的操作是以大括号内的单个表达式的形式提供的:
my @capitals = ("Baton Rouge", "Indianapolis", "Columbus", "Montgomery", "Helena", "Denver", "Boise"); |
grep
函数将一个数组作为输入,并返回一个过滤后的数组作为输出,语法与map
类似。这次,第二个参数对输入数组中的每个标量$_
进行判断。如果返回布尔值为真,则将标量放入输出数组中,否则不放入:
print join ", ", grep { length $_ == 6 } @capitals; |
显然,结果数组的长度就是成功匹配的数量,这意味着你可以使用grep
来快速检查一个数组是否包含一个元素:
print scalar grep { $_ eq "Columbus" } @capitals; # "1" |
grep
和map
可以结合起来对列表操作,这是一个非常强大的功能,但在许多其他编程语言中是不存在的。
默认情况下,sort
函数返回输入数组,并按词法(字母)排序:
my @elevations = (19, 1, 2, 100, 3, 98, 100, 1056); |
然而,与grep
和map
类似,你可以提供一些自己的代码。排序总是使用两个元素之间的一系列比较来进行。你的代码块接收$a
和$b
作为输入,如果$a
“小于”$b
,则返回-1,如果它们相等则返回0,如果$a
“大于”$b
则返回1。
对于字符串,cmp运算符正是这样做的:
print join ", ", sort { $a cmp $b } @elevations; |
宇宙飞船运算符<=>
,对数字也是如此:
print join ", ", sort { $a <=> $b } @elevations; |
$a
和$b
不一定总是标量,它们可能是相当复杂的对象的引用,很难进行比较。如果你需要进行比较,你可以创建一个单独的子程序,并提供函数名字来代替:
sub comparator { |
对于grep
或map
操作,无法这样操作。
请注意子程序和代码块是如何从不明确地提供$a
和$b
的。就像$_
一样,$a
和$b
实际上是全局变量,每次都用一对值来填充比较。
九、内置函数
现在你至少已经看到了十几个内置函数:print
、sort
、map
、grep
、keys
、scalar
等等。内置函数是Perl最大的优势之一:
- 众多
- 是非常有用的
- 有大量的文献记载
- 在语法上有很大的不同,所以请仔细阅读文档
- 有时接受正则表达式作为参数
- 有时接受整个代码块作为参数
- 有时不需要在参数之间使用逗号
- 有时会消耗任意数量的逗号分隔的参数,而有时不会
- 如果提供的参数太少,有时会自己填入参数
- 除非在模棱两可的情况下,一般不需要在调用时加括号
关于内置函数,最好的建议是知道它们的存在。仔细阅读文档以备将来参考。如果你在执行一项任务时,感觉它很低级,而且很普通,以前已经做过很多次了,那么很有可能内置函数已经实现了。
十、用户定义子程序
子程序使用sub
关键字来声明。与内置函数不同,用户定义的子程序总是接受相同的输入:一个标量列表。当然,这个列表可以只有一个元素,也可以是空的。单个标量被认为是一个只有一个元素的列表,一个有N
个元素的哈希被视为一个有2N
个元素的列表。
虽然括号是可选的,但子程序应该总是使用括号来调用,即使是在没有参数的情况下调用。这样可以清楚地表明子程序的调用正在发生。
一旦你进入一个子程序,就可以使用内置的数组变量@_
来获取参数。例子:
sub hyphenate { |
1. Perl通过引用调用
与几乎所有其他主要的编程语言不同,Perl是通过引用来调用的。这意味着在子程序的主体中的变量不是复制的值,它们就是原值:
my $x = 7; |
如果你尝试这样:
reassign(8); |
那么就会发生错误并停止执行,因为reassign()
的第一行相当于:
8 = 42; |
这显然是错的。
我们要吸取的教训是,在子程序的主体中,你一定要在处理前提取参数。
2. 提取参数
有不止一种方法可以提取@_
,但有些方法更方便。
下面的例子中子程序left_pad
使用提供的pad
字符将一个字符串填充到所需长度。(x
函数在一行中连接多个相同字符串的副本。) (注意:为了简洁起见,这些子程序都缺乏一些基本的错误检查,即确保pad字符只有1个字符,检查宽度是否大于或等于现有字符串的长度,检查所有需要的参数是否被传递。)
left_pad
的调用方法如下:
print left_pad("hello", 10, "+"); # "+++++hello" |
- 逐条解读
@_
是有效的,但不是非常漂亮:
sub left_pad { |
- 通过使用
shift
从@_
中删除数据来提取@_
,建议最多使用4个参数:
sub left_pad { |
如果没有向shift函数提供数组,那么它默认对@_
进行操作。这种方法非常常见:
sub left_pad { |
超过4个参数,就很难掌握什么东西被分配到哪里。
- 你可以使用多个同时标量赋值的方式,一次性将
@_
全部提取。同样,这对于最多4个参数也是可以的:
sub left_pad { |
- 对于有大量参数的子程序,或者有些参数是可选的,或者不能与其他参数组合使用,最好的做法是要求用户在调用子程序时提供一个参数哈希,然后将
@_
解包回该参数哈希中。对于这种方法,我们的子程序调用看起来会有些不同:
print left_pad("oldString" => "pod", "width" => 10, "padChar" => "+"); |
而子程序本身是这样的:
sub left_pad { |
3. 返回值
像其他Perl表达式一样,子程序调用可能会显示上下文行为。你可以使用wantarray
函数(应该叫wantlist
,但不要紧)来检测子程序是在什么上下文中被评估的,并返回一个适合该上下文的结果:
sub contextualSubroutine { |
十一、系统调用
假设你已经知道以下与Perl无关的事实。在Windows或Linux系统上,每次进程结束时(我想,在大多数其他系统上也是如此),它都会以一个16位的状态字结束。最高的8位构成了一个介于0到255之间的返回码,0通常代表无条件的成功,而其他值则代表不同程度的失败。其他8位则较少被检查,它们反映了失败的模式,如信号死亡和核心转储信息。
你可以用exit
退出Perl脚本,并选择返回代码(从0到255)。
Perl 提供了不止一种方法。在一次调用中,会生成一个子进程,暂停当前脚本,直到子进程完成,然后恢复对当前脚本的解释。无论使用哪种方法,你都会发现,内置的标量变量$?
已经被填充了该子进程终止时返回的状态字。你可以只取这16位中最高的8位来获得返回代码:$? >> 8
。
system
函数可以用来调用另一个程序,其参数列举如下。system
返回值与$?
填充值相同:
my $rc = system "perl", "anotherscript.pl", "foo", "bar", "baz"; |
另外,你也可以使用反引号在命令行运行一个实际的命令,并捕获该命令的标准输出。在标量上下文中,整个输出以单个字符串的形式返回。在列表上下文中,整个输出以一个字符串数组的形式返回,每个字符串代表一行输出:
my $text = `perl anotherscript.pl foo bar baz`; |
例如,如果anotherscript.pl
包含以下内容,就会出现这种行为:
use strict; |
十二、文件和文件句柄
一个标量可以包含一个文件句柄,而不是数字、字符串、引用或undef
。文件句柄本质上是对特定文件中特定位置的引用。
使用open
将一个标量变成一个文件句柄。open
必须提供一个模式。<
表示我们希望打开文件来读取它:
my $f = "text.txt"; |
如果成功,open
会返回一个真值。否则,它将返回false
,并将错误信息塞进内置变量$!
如上所述,你应该始终检查open
操作是否成功完成。这种检查是相当繁琐的,一个常见的语句是:
open(my $fh, "<", $f) || die "Couldn't open '".$f."' for reading because: ".$!; |
请注意open
调用的参数周围需要加括号。
要从文件柄中读取一行文本,使用内置的readline
函数。readline
返回一个完整的文本行,并在其结尾处有一个完整的换行符(可能除了文件的最后一行),如果你已经到达文件的结尾,则返回undef
:
while(1) { |
要截断可能的换行符,使用chomp
:
chomp $line; |
请注意,chomp
作用于$line
的位置。$line = chomp $line
可能不是你想要的。
你也可以使用eof
来检测文件是否已经到达终点:
while(!eof $fh) { |
但是要注意只使用while(my $line = readline $fh)
,因为如果$line
结果是"0"
,循环就会提前结束。如果你想写一些类似的东西,Perl提供了<>
操作符,它用一种更安全的方式包装readline
。这是很常见的,也是非常安全的:
while(my $line = <$fh>) { |
甚至:
while(<$fh>) { |
写入一个文件需要首先以不同的模式打开它。>
表示我们希望打开文件向其写入。
(如果目标文件已经存在并且有内容,那么>
将把目标文件的内容删除。
如果只是追加到一个现有的文件,请使用模式>>
)。
然后,只需提供filehandle作为print
函数的第0个参数:
open(my $fh2, ">", $f) || die "Couldn't open '".$f."' for writing because: ".$!; |
请注意$fh2
和下一个参数之间没有逗号。
当文件句柄退出作用域时,文件句柄实际上会被自动关闭,但除此之外:
close $fh2; |
有三个文件柄作为全局常量存在,分别是STDIN
、STDOUT
和STDERR
。
当脚本启动时,这些文件柄会自动打开。
要读取用户输入的单行内容:
my $line = <STDIN>; |
只是等待用户按下回车键:
<STDIN>; |
在没有文件句柄的情况下调用<>
,会从STDIN
中读取数据,或者从调用Perl脚本时参数中命名的任何文件中读取数据。
正如你所了解的那样,如果没有命名文件柄,默认情况下打印到STDOUT
。
1. 文件校验
函数-e
是一个内置函数,用于测试命名文件是否存在。
print "what" unless -e "/usr/bin/perl"。 |
函数-d
是一个内置函数,用于测试命名的文件是否是一个目录。
函数-f
是一个内置函数,用于测试命名的文件是否是一个普通文件。
这些只是-X
形式的一大类函数中的三个,其中X
是一些小写或大写字母。这些函数被称为文件测试。注意前面的减号。在Google查询中,减号表示要排除包含这个搜索词的结果。这使得文件测试很难在Google上搜索到!只要搜索perl文件测试
就可以了。
十三、正则表达式
正则表达式出现在Perl以外的许多语言和工具中。Perl的核心正则表达式语法与其他地方基本相同,但Perl的全部正则表达式功能却复杂得可怕,难以理解。我能给你的最好的建议是尽可能避免这种复杂性。
使用=~ m//
进行匹配操作。在标量上下文中,=~ m//
成功时返回true
,失败时返回false
。
my $string = "Hello world"; |
括号中执行子匹配。在执行成功的匹配操作后,子匹配会被塞进内置变量$1
,$2
,$3
,…
my $string = "colourless green ideas sleep furiously"; |
使用=~ s///
进行替换操作:
my $string = "Good morning world"; |
请注意$string
的内容是如何改变的。你必须在=~ s///
操作的左侧传递一个标量变量。如果你传递的是一个字面字符串,你会得到一个错误。
/g
标志表示全局匹配。
在标量上下文中,每一次=~ m//g
调用都会在前一次调用之后找到另一个匹配,成功时返回true
,失败时返回false
。之后就可以按照通常的方式访问$1
等。例如:
my $string = "a tonne of feathers or a tonne of bricks"; |
在列表上下文中,=~ m//g
调用一次返回所有的匹配:
my @matches = $string =~ m/(\w+)/g; |
=~ s///g
调用执行全局搜索和替换,并返回匹配数。这里,我们用字母r
替换所有元音。
# Try once without /g. |
/i
标志使匹配和替换不区分大小写。
/x
标志允许您的正则表达式包含空格(例如,换行符)和注释。
"Hello world" =~ m/ |
十四、模块和包
在Perl中,模块和包是不同的东西。
1. 模块
模块是一个.pm
文件,你可以把它包含在另一个Perl文件(脚本或模块)中。模块是一个文本文件,其语法与Perl脚本完全相同。一个模块的例子可能在/foo/bar/baz/Demo/StringUtils.pm
找到,内容如下:
use strict; |
因为一个模块在加载时是从上到下执行的,所以你需要在最后返回一个真值,以显示它已经成功加载。
为了让Perl解释器能够找到它们,在调用perl之前,应该在环境变量PERL5LIB
中列出包含Perl
模块的目录。列出包含模块的根目录,不要列出模块目录或模块本身:
set PERL5LIB="C:\foo\bar\baz;%PERL5LIB%" |
或
export PERL5LIB="/foo/bar/baz:$PERL5LIB" |
一旦创建了Perl模块,并且perl知道在哪里寻找它,你就可以在Perl脚本中使用内置的require
函数来搜索并执行它。例如,调用require Demo::StringUtils
会使Perl解释器依次搜索PERL5LIB
中列出的每个目录,寻找一个名为Demo/StringUtils.pm
的文件。在模块被执行后,在那里定义的子程序主脚本就可调用。我们的示例脚本可能叫做main.pl
,内容如下:
use strict; |
注意使用双冒号::
作为目录分隔符。
现在有一个问题浮出水面:如果main.pl
包含了许多require
调用,而每一个被加载的模块都包含了更多的require
调用,那么就很难追踪到zombify()
子程序的原始声明。解决这个问题的方法是使用包。
2. 包
包是一个可以声明子程序的命名空间。你声明的任何子程序都会隐含在当前包中。在执行开始时,你是在主包中,但你可以使用包的内置函数切换包:
use strict; |
注意使用双冒号::
作为命名空间的分隔符。
当你调用一个子程序时,你就隐含地调用了当前包中的一个子程序。另外,你也可以显式地提供一个包。看看如果我们继续上面的脚本会发生什么。
subroutine(); # "kingedward" |
所以上述问题的解决方案是修改/foo/bar/baz/Demo/StringUtils.pm
为:
use strict; |
并将main.pl
修改为:
use strict; |
现在仔细阅读下面的说明:
包和模块是Perl编程语言中两个完全独立的、不同的特性。事实上,它们都使用相同的双冒号分隔符是一个巨大的红线。在一个脚本或模块的过程中,可以多次切换包,也可以在多个文件的多个位置使用同一个包声明。调用require Foo::Bar
并不会在文件中寻找并加载一个包含Foo::Bar
声明的文件,也不一定会加载Foo::Bar
命名空间的子程序。调用require Foo::Bar
只是加载一个名为Foo/Bar.pm
的文件,这个文件里面根本不需要任何形式的包声明,事实上它可能会声明包Baz::Qux
之类的废话。
同样的,一个叫Baz::Qux::processThis()
的子程序不一定要在一个叫Baz/Qux.pm
的文件里声明。它可以被声明在任何地方。
将这两个概念分开是Perl最愚蠢的特性之一,将它们作为单独的概念来处理总是会导致混乱的、令人发狂的代码。幸运的是,大多数Perl程序员都遵守以下两个定律。
- 一个Perl脚本 (
.pl
文件) 必须始终包含零个包声明。 - 一个Perl模块 (
.pm
文件) 必须总是包含正好一个包声明,与它的名称和位置完全对应。例如,模块Demo/StringUtils.pm
必须以包Demo::StringUtils
开头。
正因为如此,在实践中你会发现,大多数由可靠的第三方制作的包和模块都可以互换使用。然而,重要的是,你不要认为这是理所当然的,因为有一天你会遇到一个疯子制作的代码。
十五、面向对象的Perl
Perl并不是一个很好的面向对象编程语言。Perl的OO能力是事后嫁接的,这一点就可以看出。
- 一个对象只是一个引用(即一个标量变量),它恰好知道它的引用属于哪个类。要告诉一个引用它的引用属于一个类,请使用 bless。要找出一个引用的引用属于哪个类(如果有的话),请使用 ref.
- 方法是一个简单的子程序,它的第一个参数是一个对象 (或者,在类方法的情况下,一个包名)。对象方法使用
$obj->method()
来调用;类方法使用Package::Name->method()
来调用。 - 一个类只是一个刚好包含方法的包。
一个简单的例子可以让这个问题更清楚。一个包含Animal
类的示例模块Animal.pm
的内容如下:
use strict; |
而我们可以这样利用这个类:
require Animal; |
注意:任何引用都可以被放到到任何类中。这取决于你是否确保:
- 引用可以作为这个类的实例使用
- 这个类是否存在并且已经被加载
你仍然可以用通常的方式来处理原始哈希。
print "Animal has ", $animal->{"legs"}, " leg(s)"; |
但现在你也可以使用相同的->
操作符来调用对象上的方法,就像这样。
这个最后的调用相当于Animal::eat($animal, "insects", "curry", "eucalyptus")
。
1. 构造
构造函数是一个返回新对象的类方法。如果你想要使用,就声明一个。你可以使用任何你喜欢的名字。对于类方法,传递的第一个参数不是一个对象,而是一个类名。在本例中,Animal
。
use strict; |
然后这样使用:
my $animal = Animal->new(); |
2. 继承
要创建一个继承自父类的类,use parent
。假设我们将Animal
与位于Koala.pm
的Koala
子类化。
use strict; |
还有一些示例代码:
use strict; |
这个最后的方法调用试图调用Koala::eat($koala, "insects", "curry", "eucalyptus")
,但是在Koala
包中并没有定义子程序eat()
。然而,由于Koala
有一个父类Animal
,Perl解释器尝试调用Animal::eat($koala, "insects", "curry", "eucalyptus")
,结果成功了。请注意Animal
类是如何被Koala.pm
自动加载的。
由于use parent
接受了一个父类名的列表,Perl支持多重继承,这带来了所有的好处和恐怖。
十六、BEGIN
块
一旦perl完成了对该块的解析,BEGIN块就会被执行,甚至在它解析文件的其他部分之前,但是在执行时被忽略。
use strict; |
BEGIN
块总是先被执行。如果你创建了多个BEGIN
块(尽量不要这样做),当编译器遇到它们时,它们会按照从上到下的顺序执行。BEGIN
块总是先执行,即使它被放在脚本的一半或最后。不要打乱代码的自然顺序。把BEGIN
块放在开头!
BEGIN
块一经解析就会被执行。一旦完成,解析工作就会在BEGIN
块的末尾恢复。只有当整个脚本或模块被解析后,才会执行BEGIN
块之外的任何代码。
use strict; |
因为它们是在编译时执行的,所以放置在条件块中的BEGIN
块仍然会被首先执行,即使条件评估为false
,而且事实上条件还没有被评估,事实上可能永远不会被评估。
if(0) { |
不要把BEGIN
块放在条件中! 如果你想在编译时做一些有条件的事情,你需要把条件放在BEGIN
块中。
BEGIN { |
十七、use
好的。现在你已经理解了包、模块、类方法和BEGIN
块的复杂行为和语义,我可以解释一下极其常见的使用函数。
以下三种用法:
use Caterpillar ("crawl", "pupate"); |
分别相当于:
BEGIN { |
- 不,这三个例子的顺序没有错。只是Perl太笨了。
- 使用调用是一个变相的
BEGIN
块。同样的警告也是适用的,使用语句必须总是放在文件的顶部,而绝不能放在条件语句中。 import()
不是一个内置的Perl函数。它是一个用户定义的类方法。定义或继承import()
的责任在Caterpillar
包的程序员身上,理论上该方法可以接受任何东西作为参数,并对这些参数做任何事情。 使用Caterpillar;
可以做任何事情。请查阅Caterpillar.pm
的文档来了解到底会发生什么。- 请注意,
require Caterpillar
如何加载一个名为Caterpillar.pm
的模块,而Caterpillar->import()
则调用在Caterpillar
包内定义的import()
子程序。希望模块和包是一致的吧!
十八、Exporter
最常见的定义import()
方法的方式是从Exporter
模块中继承。Exporter
是一个核心模块,也是Perl编程语言事实上的核心功能。在Exporter
的import()
实现中,你传递进来的参数列表被解释为子程序名称的列表。当一个子程序被import()
后,它就会在当前包和自己的原始包中变得可用。
这个概念用一个例子最容易理解。下面是Caterpillar.pm
的样子。
use strict; |
包变量@EXPORT_OK
应该包含一个子程序名称的列表。
然后,另一段代码可以通过名称import()
这些子程序,通常使用一个use
语句。
use strict; |
在本例中,当前包是main
,所以crawl()
的调用实际上是对main::crawl()
的调用,而main::crawl()
(因为它是被导入的)映射到Caterpillar::crawl()
。
注意:不管@EXPORT_OK
的内容是什么,每个方法都可以一直调用。
use strict; |
1. @EXPORT
Exporter
模块还定义了一个名为@EXPORT
的包变量,它也可以用子程序名称的列表来填充。
use strict; |
如果调用import()
时没有任何参数,那么在@EXPORT
中命名的子程序就会被导出,这里就是这样。
use strict; |
但是请注意,我们又回到了这样一个情况:如果没有其他线索,可能不容易判断出crawl()
最初的定义。这个故事的寓意是双重的。
当创建一个使用
Exporter
的模块时,千万不要使用@EXPORT
来默认导出子程序。总是让用户手动调用子程序,或者显式地import()
子程序(例如使用Caterpillar("crawl")
,这是一个强烈的提示,可以在Caterpillar.pm
中查找crawl()
的定义)。当使用一个使用
Exporter
的模块时,一定要明确地命名你要import()
的子程序。如果你不想import()
任何子程序,并希望单独引用它们,你必须提供一个显式的空列表:使用Caterpillar()
。
十九、其他说明
核心模块
Data::Dumper
可以用来向屏幕输出一个任意标量。这是一个必不可少的调试工具。还有一种替代语法,
qw{ }
,用于声明数组。这经常出现在使用声明中。
use Account qw{create open close suspend delete}; |
还有很多类似于引号的运算符。
在
=~ m//
和=~ s///
操作中,你可以使用大括号代替斜杠作为regex
的定界符。如果您的regex
中包含大量的斜杠,这就非常有用,否则就需要用反斜杠来转义。例如,=~ m{///}
可以匹配三个正向斜杠,而=~ s{^https?://}{}
可以删除 URL 的协议部分。Perl中确实有
CONSTANTS
。现在已经不鼓励使用了,但并不总是这样。常量实际上只是省略了括号的子程序调用。有时人们会在哈希键周围省略引号,写
$hash{key}
而不是$hash{"key"}
。他们之所以能逃过一劫,是因为在这种情况下,键是以字符串key
的形式出现的,而不是子程序调用key()
。如果你看到一块未格式化的代码块被双方格符包裹着,比如
<<EOF>>
,谷歌的神奇词是here-doc
。警告! 许多内置函数可以在没有参数的情况下被调用,导致它们对
$_
进行操作。希望这能帮助你理解这样的形式。
print foreach @array; |
和
foreach ( @array ) { |
我不喜欢这种阵型,因为在重构的时候会导致问题。