第一章 引言
你应该已经知晓哪些概念?
- 在你的平台如何运行Perl
- Perl的三种变量类型:标量,数组和哈希
- 控制结构的语法如:if,while,for和foreach
- 子程序
- Perl的操作符:grep,map,sort和print
- 文件操作的语法
第二章 进阶基础
2.1 列表操作符
你可能已经知道Perl的一些列表操作符,但并没有想过他们是怎么同列表一起工作的。最常用的列表操作符应该是print了。我们给它一些参数,然后它把他们合在一起显示出来。
print 'Two castaways are', 'Gilligan', 'and', 'Skipper', "n";
在Learning Perl这本书里,你可能知道了另外一些列表操作符。如sort操作符将输入的列表按顺序列出。在Gilligan’s Island的主题歌中的那些求生者没有按字母次序出场,sort可以为我们修正这一点。
my @castaways = sort qw(Gilligan Skipper Ginger Professor Mary-Ann);
my @castaways = reverse qw(Gilligan Skipper Ginger Professor Mary-Ann);
# reverse操作符返回反向排序的列表
Perl 还有其它与列表打交道的操作符。而且一旦你使用他们,你会发现这些语句会使你表达得更清楚,写更少的代码。
2.1.1 使用grep操作符过滤列表
grep操作符取一个列表和一个“测试表达式”。它一个一个地从列表中把元素取出来放到_变量送到输出列表中。
my @lunch_choices = grep &is_edible($), @gilligans_posessions;
my @results = grep EXPR, @input_list;
my $count = grep EXPR, @input_list;
# 在一个列表上下文中,grep操作符会返回所有被选出元素的列表。而在一个标量上下文中,grep返回被选出元素的个数。
my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
my @bigger_than_10 = grep $ > 10, @input_numbers; # 输出结果当然是:16,32 和64。
2.1.2 使用map列表转换
map操作符的语法同grep操作符非常相像,他们有相同的操作步骤。例如它们都是把输入列表中的元素临时地放到$变量中去,而且他们的语法中都有表达式形式和代码块形式。
然而,grep中的测试表达式在map中变成了映射表达式。map操作符在列表环境中为表达式求值(而不是像grep那样在标量环境下求值)。每次表达式求值都成为整个输出结果的一部分。为各个元素求值结果连在一起成为完整全部的输出。在标量环境下,map返回在输入列表里多少个元素被处理。但是map应该总是用在列表环境下,很少用在标量环境下。我们由一个简单的例子开始:
my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
my @result = map $ + 100, @input_numbers; # 对七个元素中的每一个,map将它放到变量$中,然后我们获得单个输出结果:得到各自大于100的数值。所以@result的值是: 101, 102, 104, 108, 116, 132, and 164。
但我们不应该止步于为每个输入仅有一个输出结果。让我们看看为每个输入产生两个输出:
my @result = map { $, 3 * $ } @input_numbers;
# 现在为每个输入项都产生两个输出:1, 3, 2, 6, 4, 12, 8, 24, 16, 48, 32, 96, 64, and 192. 如果需要一个散列展示一个数和它的三倍值的数值对,我们可以将结果成对放到一个散列里面。
my %hash = @result;
或者,在map中不用中间的数组变量:
my %hash = map { $, 3 * $ } @input_numbers;
这里我们可以看到map是非常多才多艺的;我们可以为每个输入元素产生任何数量的输出。而且我们不一定总是为每一个元素产生相同的输出数量。让我们来看一个例子,它把一个数字折成数位。
my @result = map { split //, $ } @input_numbers;
内联的代码块把每个数值拆分为数位。对于1,2,4和8,我们得到单个输出。对于16,32,和64,我们为每个数值得到两个输出。当然我把结果连接起来,我们最终得到1, 2, 4, 8, 1, 6, 3, 2, 6, 和4。
如果一个特别的调用返回一个空的列表,map会把空列表连接起来到一个大的列表中,然后扔掉。我们可以利用这个特点选择和丢弃结果。例如,可能我们要以4结尾的数值:
my @result = map {
my @digits = split //, $_;
if ($digits[-1] = = 4) {
@digits;
} else {
( );
}
} @input_numbers;
如果结尾数字为4,我们返回拆分开的数位(在列表环境)。如果最后个数字不是4,我们则返回一个空的列表,有效地去除了不要的输出。所以,我们总是可以用map来替换grep,但相反则不成。
当然,所有map和grep能做的事,我们也可以显式的用foreach loop来实现。但是,再说一次,我们也可以用汇编或在打卡带来实现同样的编程[*]。要点是正确的运用grep和map可以帮你降低程序的复杂度,让我们可以把注意力集中在高层次的问题上,而不是在细节上纠缠。
2.2 用eval捕捉错误
有一些代码看上去很平常,但是却含有潜在的危险,一旦某种条件不对就会使程序中断,过早地结束程序。
my $average = $total / $count; # divide by zero? 若除零呢?
print "okayn" unless /$match/; # illegal pattern? 若传过来的正则表达式不对呢?
open MINNOW, ‘>ship.txt’
or die "Can’t create 'ship.txt' : $!"; # user-defined die? 若打开失败呢?
&implement($) foreach @rescue_scheme; # die inside sub? 若在子程序里出错退出程序呢?
可是,不能因为代码的某一片断出错而使我们的整个程序崩溃。Perl用eval操作符来实现捕捉错误的机制。
eval { $average = $total / $count } ;
如果在eval块里代码发生错误,系统会退出这个块。但是,尽管退出块,Perl会继续执行eval块之外的代码。我们在eval块的后面一般做法是检查一下$@变量,这个变量要么是空(表示没有出错)或者代码出错时系统返回的“遗言”,多半是“除零错误”之类云云。
2.3 用eval动态编译代码
eval有另外一种用法,其参数是作为一个字串表达式,而不是代码块。在运行时,它将字串临时编译成代码并且执行。这很易用,但也很危险,因为有可能会把具有危害性的代码放到字串里。除了极少数值得一提的例外情况,我们建议你尽量避免这种用法。稍后我们会用这种用法,然后我们就不用这种用法了,我们只是展示它是怎么工作的。
eval '$sum = 2 + 2';
print "The sum is $sum";
Perl在词法环境中执行那段代码,这意味着我们好像在执行的时候输入这些代码的一样。eval的结果就是最后一个表达式求值的值,所以我们不必在eval中输入整个语句。
#!/usr/bin/perl
foreach my $operator ( qw(+ – * /) ) {
my $result = eval "2 $operator 2";
print "2 $operator 2 is $resultn";
}
第三章 使用模块
3.1 使用模块
几乎所有的Perl模块都带有文档说明。所以尽管我们可能不知道那些模块背后的戏法是怎么变的,如果我们知道如何使用接口,我们就不必去担心那些细节。这就是在这里介绍接口的原因,毕竟:它屏蔽了复杂性。
在我们的本机当中,我们可以用perldoc命令来调出模块文档。我们输入我们要查的模块的名字,然后perldoc打印出文档内容:
$ perldoc File::Basename
NAME
fileparse - split a pathname into pieces
basename - extract just the filename from a path
dirname - extract just the directory from a path
SYNOPSIS
use File::Basename;
($name,$path,$suffix) = fileparse($fullname,@suffixlist)
fileparse_set_fstype($os_string);
$basename = basename($fullname,@suffixlist);
$dirname = dirname($fullname);
3.2 函数接口
为了调用一个模块,我们可以用Perl内置的use语句。 这里我们不打算更深入的了解细节问题,我们会在第10章和第15章来说这个问题。 目前,我们只要能调用模块就可以了。 我们就发行版的核心模块中的File::Basename模块开始说吧。要把它调入我们的脚本,我们用:
use File::Basename;
当我们写上如上的代码后,File::Basename 向你的脚本引入了三个子例程[+]:fileparse,basename和dirname.[*] 自此之后,我们就可以用如下语句了:
my $basename = basename( $some_full_path );
my $dirname = dirname( $some_full_path );
3.3 选择性地引入函数
很幸运,我们可以告诉use操作符,通过只导入需要的子例程来限制它的行为。称为“函数导入清单”,如:
use File::Basename ('fileparse', 'basename');
use File::Basename qw( fileparse basename );
3.4 安装从CPAN下载的模块
$ wget
$ tar -xzf HTTP-Cookies-Safari-1.10.tar.gz
$ cd HTTP-Cookies-Safari-1.10s
$ perl Makefile.PL
$ make
$ make test
$ make install
如果你因为没有权限而不能在系统级的目录里建立目录,[*]我们可以用PREFIX参数告诉Perl安装在你另外指定的路径:
$ perl Makefile.PL PREFIX=/Users/home/Ginger
$ export PERL5LIB=/Users/home/Ginger
3.5 处理模块依赖
我们刚才看到如果我们要安装一个模块,并且这个模块要引用Module::Build模块的话, 我们要事先装好Module::Build模块。这就是个稍稍让人头痛的有关一般模块依赖性的例子。 那我们的castaways岛的所有的椰子应该如何处理呢?我们要安装另一些模块,而这些模块各自又依赖更多的其它不同的模块。
幸而,我们有工具来助一臂之力。自从Perl 5.004版开始,CPAN.pm模块成为核心发布的一部份。它给我们提供了一个交互式的模块安装环境。
$ perl -MCPAN -e shell
cpan>
cpan> install CGI::Prototype
第四章 介绍引用
引用是复杂的数据结构、面向对象的编程和花哨的子程序处理的基础。它们是Perl版本4和5之间添加的魔术,使其成为可能。
Perl标量变量保存一个值。数组包含一个有序的标量列表。散列以字符串作为值保存标量的无序集合。虽然标量可以是任意字符串,它允许我们在数组或散列中编码复杂数据,但这三种数据类型都不适合复杂的数据相互关系。这是一项参考工作。引用的重要性,从一个例子开始。
4.1 用多个数组来完成一个简单任务
在Minnow开始一个旅程之前(比如一个三小时的远足), 我们应该事先检查一下每个乘客和乘务人员的行李,保证他们带了旅行所需要的东西。比如说吧,水上安全救生装备。在Minnow船上的每个乘客要生命维持系统,太阳镜和水瓶以及雨衣。我们来写段代码来检查船长的装备:
my @required = qw(preserver sunscreen water_bottle jacket);
my %skipper = map { $_, 1 }
qw(blue_shirt hat jacket preserver sunscreen);
foreach my $item (@required) {
unless ( $skipper{$item} ) { # not found in list?
print "Skipper is missing $item.\n";
}
}
由于我们希望检查某个特定项是否在Skiper的列表中,所以最简单的方法是使散列中的所有项键,然后使用exists来检查哈希。在这里,我们给了每个键一个真值(1),所以我们不需要exists。不要完全输入散列,而是使用map从项目列表中创建它。
当然,如果我们想查一个Gilligan和Professor的,我们可能要写如下的代码:
my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
for my $item (@required) {
unless (grep $item eq $, @gilligan) { # not found in list?
print "gilligan is missing $item.\n";
}
}
my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
for my $item (@required) {
unless (grep $item eq $, @professor) { # not found in list?
print "professor is missing $item.n";
}
}
当我们像这样编程时,我们开始在这里实现大量重复的代码,并认为我们应该将它重构成一个可以重用的公共子程序:
sub check_required_items {
my $who = shift;
my %whos_items = map { $_, 1 } @_;
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
unless ( $whos_items{$item} ) { # not found in list?
print "$who is missing $item.\n";
}
}
}
my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
check_required_items('gilligan', @gilligan);
到目前为止进展顺利。我们可以检查船长和教授的装备,只用如下一点代码:
my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
check_required_items('skipper', @skipper);
check_required_items('professor', @professor);
对于另外两个乘客,我们可以如法泡制。尽管以上代码符合最初的要求,我们还是要有两个问题要解决:为了建立数组@, Perl会拷贝整个数据内容。对于少量数据还可以,但如果数组庞大,这看上去多少有些浪费时间在拷贝数组到子例程。
要解决这些问题,我们需要传引用而不是传值给子例程
4.2 建立一个对数组的引用
相对于其它方法,反斜杠(\)符号被用来当作“取址”操作符。比如,哪我们把它放在一个数组前面时:\@skipper,其结果就是取这个数组的地址。引用这个数组就是一个指针:指向这个数组,但并不是这个数组本身。
标量合适的操作对于引用都一样合适。 它可以是数组或散列中的一个元素,或简单就是一个标量变量,像下面所示:
my $reference_to_skipper = \@skipper;
my $second_reference_to_skipper = $reference_to_skipper; #引用可以被复制
my $third_reference_to_skipper = \@skipper;
我们可以互换这三个引用。 我们甚至说他们是相同的,因为,实际上他们指的是同一地址。
因为我们可以复制一个引用,并且将一个参数传递给一个子例程实际上就是别名,所以我们可以使用这段代码将对数组的引用传递到子例程中:
#! /usr/bin/perl
my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
check_required_items("The Skipper", @skipper);
sub check_required_items {
my $who = shift;
my $items = shift;
my @required = qw(preserver sunscreen water_bottle jacket);
…
}
现在子例程中的$items变量保存的是指向数组@skipper的引用。但我们如何时把一个引用变回一个原始数组呢?当然,我们可以还原一个引用.
4.3 还原一个指向数组的引用
我们看一下@skipper, 你会发现它包括两部份:@符号和数组名。相似地,语法$skipper[1](包括当中的数组名和围绕在周围的语法符号表示取这个数组的第二个元素(索引1表示取第二个元素,因为索引起始值是0)。
这里有一个小戏法:我们可以用在外面套上大括号的指向数组的引用,来替换数组的名字,其结果就是访问原始的数组。换句话说,就是我们写sipper数组名字的地方,可以用大括号包起来的指向数组的引用来代替:{items }
同样,下面两行同指这个数组的第二个元素:
{$items}[1]
运用引用的形式,我们已经可以分离数组名字和从实际数组中访问数组的方法。我们来看看子例程的余下部分:
sub check_required_items {
my $who = shift;
my $items = shift;
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
unless (grep $item eq $, @{$items}) { # not found in list?
print "$who is missing $item.n";
}
}
}
我们做的仅仅就是把@(供应清单的拷贝)替换成@{$items}, 对一个引用的还原操作来取得原始的供应清单数组,现在我们调用子例程次数相比以前少多了。
my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
check_required_items('The Skipper', \@skipper);
my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
check_required_items('Professor', \@professor);
my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
check_required_items('Gilligan', \@gilligan);
以上每个例子中,$items指向一个不同的数组。如此,同样的代码每次调用的时候可以应用到不同的数组。这就是引用的一个最重要的用法之一:把代码同其操作的具体数据结构分离开,这样我们可以重用代码并使其更可读。
通过引用传递数组修复了前面提到的两个问题中的第一个问题。
sub check_required_items {
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
unless (grep $item eq $_, @{$_[1]}) { # not found in list?
print "$_[0] is missing $item.\n";
}
}
}
在子例程的开头,我们能去除两个shift。当然,但我们牺牲了清晰性:
sub check_required_items {
my %whos_items = map {$_, 1} @{$_[1]};
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
unless ( $whos_items{$item} ) { # not found in list?
print "$_[0] is missing $item.\n";
}
}
}
我们在@_中仍然有两个元素。第一个元素是乘客或机组人员的名称,我们在错误消息中使用它。第二个元素是对正确规定数组的引用,我们在grep表达式中使用它。
4.4 把大括号去掉
一般来说,还原对数组的引用大多是一个简单的标量变量,比如:@{{items或$$items[1]这样的形式并不会引起歧义。
但是有一点,如果大括号里的内容不是简单的标量变量的话,我们就不能把大括号去掉。比如,对于前面最后一个改写过的子例程中的@{S。不过,我们可以从@{Item}中删除大括号。
这个规则也方便我们知道哪里丢了大括号。比如我们看到$$items[1]的时候,知道这会有些语法上的麻烦,我们会意识到必须在简单标量变量items必须是一个指向数组的引用。
sub check_required_items {
my $who = shift;
my $items = shift;
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
unless (grep $item eq $, @$items) { # not found in list?
print "$who is missing $item.n";
}
}
}
与前例惟一的区别就是去掉了大括号:@$items。
4.5 修改数组
你已经看到了如何用一个指向数组的引用来解决大量拷贝带来的问题。 现在我们来看看如何修改原始数组。
对于每个遗忘的的装备,我们把它放到另一个数组里,要求乘客关注这些装备:
sub check_required_items {
my $who = shift;
my $items = shift;
my @required = qw(preserver sunscreen water_bottle jacket);
my @missing = ( );
for my $item (@required) {
unless (grep $item eq $, @$items) { # not found in list?
print "$who is missing $item.n";
push @missing, $item;
}
}
if (@missing) {
print "Adding @missing to @$items for $who.n";
push @$items, @missing;
}
}
注意我们另外增加了一个@missing数组。 如果我们在扫描数组的时候发现有遗忘的装备,我们就把它放到@missing数组里。 在扫描结束后,如果发现@missing里有内容,我们就把这个数组加在供应清单后面。
关键就在于那个子例程的最后一行。 我们把指向数组的引用$items还原成数组,访问还原后的数组,并且把@missing数组中的元素加进去。
此外,@Items})在一个双引号字符串中工作,并像一个普通的名为array的字符串一样进行内插。我们不能在@和后面的字符之间包含任何空格,尽管我们可以在花括号中包含任意空格,就好像它是普通的perl代码一样。
4.6. 数据结构嵌套
在下一个例子中,数组@_包含两个元素,其中一个是数组引用。如果我们引用一个也包含对数组的引用的数组呢?我们最终得到了一个复杂的数据结构,这可能非常有用。
举个例子,我们首先用个更大点儿的数据结构包含skipper,Gilligan和Professor供应清单的整个列表。
my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
my @skipper_with_name = ('Skipper', @skipper);
my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
my @professor_with_name = ('Professor', @professor);
my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
my @gilligan_with_name = ('Gilligan', @gilligan);
此时,@SKIPPER_WITH_NAME有两个元素,第二个元素是类似于我们传递给子例程的数组引用。
my @all_with_names = (
\@skipper_with_name,
\@professor_with_name,
\@gilligan_with_name,
);
现在,@skipper_with_name有两个元素,第二个元素就是指向数组的引用,就是上例中我们传给子例程的那个。现在,我们把它们组织起来:
my @all_with_names = (
@skipper_with_name,
@professor_with_name,
@gilligan_with_name,
);