正在浏览 Perl 里的文章

system 函数

system函数是Perl中启动子进程最简单的方法。如下调用linux的date命令:

system "date"
它会创建一个子进程来运行date命令,并且这个子进程继承了Perl的标准输入/输出/错误。也就是说date的输出日期时间字符串会送到目前Perl的STDOUT指向的地方。

system函数可以使用单个,或多个参数的形式,其中单个函数的形式很简单,经需要运行的命令直接写在双引号即可,但如果要用到shell下的变量就不能用双引号了,这时需要用单引号代替如下:
system 'ls -l $HOME';

多个参数的形式就更好理解了,将各个参数和变量用逗号隔开即可,如下:

my $tarfile = "something*wicked.tar";
my @dirs = qw(fred|flintstone  betty );
system "tar", "cvf", $tarfile, @dirs;

说明:
1、利用system函数运行外部命令时,Perl会等待外部命令执行之后才能继续进行,否则就一直等待下去,当然可以在外部命令中使用 &符合将命令扔到后台,但这样一来Perl就不会得到程序的输出了。
2、在unix/linux系统下的命令执行后会有一个返回值,如果成功返回0,否则返回非零。这在Perl中恰恰相反,如果要判断系统中的命令是否执行成功,需要在system函数前加个!即可,如下:
!system "rm -rf files_to_delete" or die "something went wrong";

exec函数

exec函数与上面讲的system函数的使用语法一致,只不过perl在使用exec函数调用外部程序时不会等待程序完成,而是调用起来就OK,至于这个程序是否正常执行,是否有错则不会理睬了。很多时候Perl这样做主要功能是为另一个程序执行设定运行的环境而已。一般情况下我们都是配合fork一起使用,这稍后会介绍。

环境变量

Perl中的%ENV哈希存放了父进程,也就是shell中的环境变量,在Perl中可以修改这些变量,然后在启动其它外部的程序,这样外部的程序就会使用我修改后的变量内容,如下:

1
2
3
$ENV{'PATH'} = "/home/rootbeer/bin:$ENV{'PATH'}";
delete $ENV{'IFS'};
my $make_result = system "make";

说明:在Perl修改%ENV哈希中的值并不会影响shell或者其它父进程。

用反引号捕捉输出结果

不管system还是exec函数调用,被调用程序的输出都会定向到Perl的标准输出。如果我们对调用程序的输出感兴趣且要进行下一步处理的话,可以使用反引号来代替单引号或双引号:

my $now = `date`;             # 捕获date的输出
print "The time is now $now"; # 这里不需要换行符,因为date的输出里已经包含

这个反引号就是ESC键下面的那个按键。如果懂得shell脚本的话就明白了,它和shell脚本中的应用类似,但有一点不同,shell会将输出的换行符去掉,但Perl则不会。因此我们经常的做法是使用chomp命令做一下处理:

chomp(my $no_newline_now = `date`);
print "A moment ago, it was $no_newline_now, I think.\n";

Perl解释反引号里面的值的方式类似于system的单参数形式,并且在解释器中会以双引号字符串形式展开,这意味着反斜线转移与变量内插都会正常处理。下面的例子要去的一系列Perl函数的说明文档,可以重复执行perldoc命令,每次使用不同的参数:

1
2
3
4
5
my @functions = qw{ int rand sleep length hex eof not exit sqrt umask };
my %about;
foreach (@functions) {
$about{$_} = `perldoc -t -f $_`;
}

说明:每次循环执行时$_的值都会不同,这样每次就可以执行不同的命令并得到他的输出。

在列表上下文中使用反引号

如果命令会输出很多行,在标量上下文中使用反引号会得到一个很长的文本串,其中包含换行符,然而在列表上下文中同样的反引号调用则返回输出的文本行列表。
例如 Uinx/linux下的who命令通常会用多行列出当前登录系统的每个用户:
merlyn tty/42 Dec 7 19:41
rootbeer console Dec 2 14:15
rootbeer tty/12 Dec 6 23:00
最左边的列式用户名称,中间列是tty名,其余的列则是登录的日期与时间。在标量上希望中我们在一个变量中得到所有的输出,后续工作需要自行拆开:
my $who_text = `who`;
但是在列表上下文中,则会自动取得拆成多行的数据:
my @who_text = `who`;
现在@who_lines里会有多个拆分好的以换行结尾的字符串。对这个结果调用chomp就可以删除所有元素末尾的换行符。换个思路考虑,只要用foreach就可以逐行处理,循环中默认使用$_作为控制变量:

1
2
3
4
5
my %ttys;
foreach (`who`) {
my($user, $tty, $date) = /(\S+)\s+(\S+)\s+(.*)/;
$ttys{$user} .= "$tty at $date\n";
}

●这里用三次循环覆盖以上的数据库,这里用了正则表达式进行匹配,但并没有明确使用绑定操作符(=~),而是直接针对$_进行匹配。这样写更简洁。
●另外请注意,这个正则表达式会寻找一个非空白的单词、数个空白、一个非空白的单词、数个空白,接着是剩余的所有字符,但不包括换行符。这个模式能够匹配$_代表的数据。例如处理第一行的时候,$_会是merlyn,$2是tty/42,$3则是Dec 7 19:41,这是成功的捕获。
●循环中的第二条语句只是用来存储tty与日期信息,之所以对哈希值进行追加是考虑到同一个用户可能多次登录的情况。

将进程视为文件句柄

到目前为止我们看到的都是由Perl同步控制子进程:启动一个命令,然后等着它结束,或者获得其输出,实际上Perl可以启动一个异步运行的子进程,并和它保持通信,直到子进程终止。
要启动并发运行的子进程,需要将命令放在open调用的文件名部分,并在它前面或后面加上竖线(管道符号)。这种调用也称作打开管道。

open DATE, "date|" or die "cannot pipe from date: $!";
open MAIL, "|mail merlyn" or die "cannot pipe to mail: $!";

其实上例很好理解,如果管道符放在后面代表命令的输出放到了文件句柄中(该句柄只读),管道符在后面则代表需要往句柄中写入信息(该句柄只写)。如果无法创建子进程open就会失败,但不会立即报错,只有在关闭文件句柄时才会报错。
那么如何读取和写入这些句柄呢?对于以读取模式打开的文件句柄,如下形式读即可:
my $now = ;
只写模式打开的文件句柄用print命令向其写入:
print MAIL "The time is now $now";

总之可以假设这些文件句柄都连接了一种虚幻的文件,一个包含了date命令的输出内容,另一个则可以自动用mail命令发送邮件。

注意:

1、如果进程连接到某个以读取模式打开的文件句柄,然后它结束运行了,则文件句柄就会返回文件结尾,就跟读取正常文件一样
2、当关闭用来写入数据到某个进程的文件句柄时,该进程会读到文件结尾。所以,要提交邮件并发送,直接关闭这个句柄即可:

close MAIL;
die "mail:non-zero exit of $?" if $?;

注意这里的$?变量和shell中的是一致的,0为正常退出非零为错误。它指示上一条命令是否正常退出。

下面要考虑的问题:为什么要使用文件句柄的方式来和进程打交道呢?如果就像得到进程的输出来做进一步的处理,那用反引号无疑是最简洁快速的方式。然而如果子进程的输出是间断性的是一个输出的过程的话,那就不需要用这种文件句柄的方式了。另外还有一点就是子进程的异步,这个在前文已经说过了。来看一下使用find命令查找指定文件,再用perl格式化输出的例子:

#!/usr/bin/perl -w
use strict;
open F, "find . -atime +90 -size +1b -print|" or die "fork: $!";
while () {
chomp;
printf "%s size %.2fK last accessed on %s\n",
$_, (-s $_)/1024, -A $_;
}

--find命令这次运行时要查找那些90天内未被访问过超过1kb的文件,这个find过程是一个连续的过程,如果用反引号来运行的话,程序要等到都查找完了之后才能放到一个变量中然后使用循环读取,而使用文件句柄就可以找到一个文件,然后就格式化输出文件。改程序中利用了Perl笔记10中介绍的文件测试命令。

发送及接收信号

linux/unix中针对进程的信号有很多,如果对shell了解的话大家都应该清楚。下面是man kill看到的说明,只挑选了简单几个常用的:

Name Num Action Description
ALRM 14 exit
HUP 1 exit
INT 2 exit
KILL 9 exit cannot be blocked
PIPE 13 exit
TERM 15 exit

信号可以是系统发给进程的,也可以是进程发给进程的。因此我么可以编写Perl程序来给别的进程发信号,前提就是必须知道目标进程的ID。假如知道了目标进程的ID为4201,则可以通过下面的语句来发送信号:
kill 2, 4201 or die "Cannot signal 4201 with SIGINT: $!";
发送信号的命令使用kill,因为2信号就是SIGINT,也可以是简写的INT,如果进程已经不存在了,就会报错。这个技巧也可以判断指定的进程是否存在,通过发送信号零来判断。如下:
[cce_perl]
unless (kill 0, $pid) {
warn "$pid has gone away!";
}
上面介绍了发送信号,接下来讲一下接收信号,为什么要接收信号呢?假设我有一个程序需要临时写入一些文件,等到程序结束时再删除之,但当程序执行一半时我按下了Ctrl+C键终止了程序,这样在程序末尾的清理临时文件的部分没有执行,垃圾文件就没有正常清除。那么如果我们能捕获这个Ctrl+C信号,针对这个信号编写一个清理程序不就解决问题了吗。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my $temp_directory = "/tmp/myprog.$$"; # 这里定义临时目录
mkdir $temp_directory, 0700 or die "Cannot create $temp_directory: $!";
sub clean_up {
unlink glob "$temp_directory/*";
rmdir $temp_directory;
}
sub my_int_handler {
&clean_up;
die "interrupted, exiting...\n";
}
$SIG{'INT'} = 'my_int_handler';
#.
#.   do some things
#.
# 这里是正常的退出
&clean_up;

对特殊哈希%SIG进行赋值就能设置信号处理程序。哈希键是信号名称,这里不用写固定的前缀SIG。哈希的值是子程序名,注意,子程序不用写“与号”。现在只要收到SIGINT信号,Perl就会将程序转到信号处理程序,这里的子程序会清理临时文件并退出。程序的末尾也有调用了清理子程序。

如果INT信号的处理程序没有退出操作,而是直接返回,那么用户按的Ctrl+C 就没有效果了。程序仍然会运行下去。有些时候我们需要的就是这种操作,也就是说我们利用信号处理程序处理一些任务之后,不退出,然后接着做下面的工作。下面的例子,假设处理文件里的每行都要花费很长时间,而你想要在收到信号时停止处理,却不想让等待中的这一行中断,这时,只要在信号处理程序中设置一个标记,然后在每行处理结束时检查它即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
my $int_count;
sub my_int_handler { $int_count++ }
$SIG{'INT'} = 'my_int_handler';
...
$int_count = 0;
while () {
... some processing that takes a few seconds ...
if ($int_count) {
# interrupt was seen!
print "[processing interrupted...]\n";
last;
}
}

本章练习

1、写一个程序,它会进入某个特定的目录,比如系统根目录。然后执行ls -l命令获得该目录内容的详细报告。如果非unix系统,请使用该系统上相应命令去的详细列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/perl -w

use strict;

my $dir = $ARGV[0];

open DIR,"ls -l $dir|" or die "can't open $dir:$!";

while (

){
if (/^d/){
print "\e[32m$_\e[0m";
}else{
print "$_";
}
}

执行:perl ex_14_1.pl ~
drwxr-xr-x 4 root root 4096 May 13 10:29 bin
lrwxrwxrwx 1 root root 15 Jun 1 16:25 cooper -> /backup/cooper/
drwx------ 2 root root 4096 Jun 9 16:46 Mail
-rw------- 1 root root 3401 Jun 9 16:46 mbox
-rw-r--r-- 1 root root 179316 Feb 4 10:14 MySQL-zrm-2.2.0-1.noarch.rpm
文件夹会显示绿色,其余白色

2、接上面的程序,让它将命令的输出送到当前目录下的ls.out 文件,错误暑促则送到 ls.err文件。

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/perl -w

use strict;

my $dir = $ARGV[0];

open STDOUT,">ls.out" die "can't open file:$!";
open STDERR,">ls.err" die "can't open file:$!";

chdir "$dir" or die "cant change dir to $dir :$!";
exec "ls -l" or die "can't execute ls -l command:$!";

3、写依程序,它会解析date命令的输出以判断今天是星期几。如果是工作日,则输出get to work,否则输出go play。

1
2
3
4
5
6
7
8
9
#!/usr/bin/perl -w

use strict;

if( `date +\%A`=~/^S/){
print "go play\n";
}else{
print "get to work\n";
}

智能匹配操作符

Perl 5.10 版本中的智能匹配操作符(~~)会根据需要选择恰当的方式比较两端的操作数。它只用于判断操作数是否相同,在比较大小时就不能用了,老老实实的用传统的比较操作符吧。智能匹配操作符~~和前面讲的绑定操作符=~很像,不过相对来说~~操作符更强悍一些。他甚至可以代替绑定操作符。如下:

先前用绑定操作符
print "I found Fred in the name!\n" if $name =~ /Fred/;

现在可以使用智能匹配操作符代替,可以完成同样的任务:
use 5.010;
print "I found Fred in the name!\n" if $name ~~ /Fred/;

只能匹配操作符看到左侧是个标量,右侧是个正则表达式,于是它自己推断出应该执行模式匹配操作。这只是一个简单的例子,在处理更复杂的情况时,它就该大显身手了,如,你想在哈希%names中查找任何匹配Fred的键,如果找到就打印输出一条信息。这时你无法使用exists判断,因为它需要给定确切的键,当然你可以用foreach来遍历每个键,尝试用正则表达式匹配,跳过不匹配的,直到发现要找的键,最后用last退出:

1
2
3
4
5
6
7
my $flag = 0;
foreach my $key (keys %names){
    next unless $key =~ /Fred/;
    $flag = $key;
    last;
}
print "I found a key matching 'Fred'. It was %flag\n" if $flag;

这样写很费事,但兼容性是最好的,Perl 5 版本都支持,但相对于智能匹配操作符来说就显得很麻烦了,下面使用智能匹配操作符来搞定它:

use 5.010;
say "I found a key matching 'Fred'" if %names ~~ /Fred/;

--这里智能匹配操作符发现了左侧是一个哈希,右侧是一个正则表达式,因此它聪明的遍历%names的所有键,用给定的正则表达式逐个测试,如果找到就立即停止,并返回真。在这里的匹配和标量的匹配不太一样,它集数种不同的操作于一身,用了一个操作符就解决了各种各样的问题。

两个数组的比较(只考虑长度相同的数组)
如果按照常规的方法可以按数组索引依次遍历,取出相同位置的两个元素比较。如果比较下来两者相抵,则令计数器$equal自增1,循环结束后如果$equal和数组@names1的长度一致则说明两个数组完全相同:

1
2
3
4
5
6
7
my $equal = 0;
foreach my $index ( 0 .. $#names1 ) {
    last unless $names1[$index] eq $names2[$index];
    $equal++;
    }
print "The arrays have the same elements!\n"
    if $equal == @names1;

下面是使用智能匹配操作符来处理

use 5.010;
say "The arrays have the same elements!"
    if @names1 ~~ @names2;

--看到了吧,仍然是这么简单,就一句话,几乎算不上是程序。

使用智能匹配是,对两边操作数的顺序没有要求,倒过来写也可以。就像代数中的“交换律”

1
2
3
use 5.010;
say "I found a name matching 'Fred'" if $name ~~ /Fred/;
say "I found a name matching 'Fred'" if /Fred/ ~~ $name;

智能匹配操作处理方式

下表是智能匹配操作符对不同操作数的处理方式

例子 匹配方式
%a ~~ %b 哈希的键是否一致
%a ~~ @b 至少 %a 中的一个键在列表@b中
%a ~~ /Fred/ 至少一个键匹配给定的模式
%a ~~ 'Fred' 哈希中某一指定键$a{Fred}是否存在 $a{Fred}
@a ~~ @b 数组是否相同
@a ~~ /Fred/ 有一个元素匹配给定的模式
@a ~~ 123 至少有一个元素转化为数字后是123
@a ~~ 'Fred' 至少有一个元素转化为字符串后是'Fred'
$name ~~ undef $name确实尚未定义
$name ~~ /Fred/ 模式匹配
123 ~~ '123.0' 数字和字符串是否相等
'Fred' ~~ 'Fred' 字符串是否相同
123 ~~ 456 数字是否相等

当使用只能匹配操作符时,Perl会按此表自上而下查看使用的操作数。先找到哪一种匹配就选择对应的操作。

注意:当两个标量以字符串的形式存储像123,123.12这些数字的时候,使用智能匹配操作符进行比对时会默认将这些字符串转换为数字,然后在进行比对。

given语句

Perl中的given-when控制结构能够根据given的参数,执行某个条件对应的语句块,其与C语言中的switch语句类似。只不过更具有Perl的色彩。看看下面的例子,其从命令行中取出第一个参数,$ARGV[0],然后依次走一遍when条件判断,看是否找到了Fred。每个when语句对应不同的处理方式,判断的条件从最宽松的开始测试:

1
2
3
4
5
6
7
use 5.010;
given( $ARGV[0] ) {
    when( /fred/i ) { say 'Name has fred in it' }
    when( /^Fred/ ) { say 'Name starts with Fred' }
    when( 'Fred'  ) { say 'Name is Fred' }
    default         { say "I don't see a Fred" }
    }

--given会将参数化为$_,每个when条件都尝试用智能匹配对$_进行测试,实际上可以写成如下形式,这就清除多了:

1
2
3
4
5
6
7
use 5.010;
given( $ARGV[0] ) {
    when( $_ ~~ /fred/i ) { say 'Name has fred in it' }
    when( $_ ~~ /^Fred/ ) { say 'Name starts with Fred' }
    when( $_ ~~ 'Fred'  ) { say 'Name is Fred' }
    default               { say "I don't see a Fred" }
    }

--如果$_不能满足任何when条件,perl就会执行default语句块。下面为运行结果:
$ perl5.10.0 switch.pl Fred
Name has fred in it
$ perl5.10.0 switch.pl Frederick
Name has fred in it
$ perl5.10.0 switch.pl Barney
I don't see a Fred
$ perl5.10.0 switch.pl Alfred
Name has fred in it

看到这里你也许会说,Perl中的if-elsif-else语句不是一样可以完成这个例子吗,为什么还要用given-when语句呢,它还有存在的必要吗?实际上他们两个语句最大的不同在于given-when可以在满足某个条件的基础上,继续测试其他的条件,但if-elsif-else一旦满足了某个条件,就只能执行对应的那个语句块。实际上前面的例子可以写成如下的方式:

1
2
3
4
5
6
7
use 5.010;
given( $ARGV[0] ) {
    when( $_ ~~ /fred/i ) { say 'Name has fred in it'; break }
    when( $_ ~~ /^Fred/ ) { say 'Name starts with Fred'; break }
    when( $_ ~~ 'Fred'  ) { say 'Name is Fred'; break }
    default               { say "I don't see a Fred"; break }
    }

--按照这种写法,因为第一条测试语句如果传来的参数匹配/fred/i,后面的所有语句就没有机会执行了,这时候就会直接跳出控制结构。如果在when语句块的末尾使用continue,Perl就会尝试执行后续的when语句了,这也是if-elsif-else语句块力不能及的地方。当另一个when的条件满足时,会执行对应语句块。在每个when语句块的末尾写上continue,就意味着所有的条件判断都会执行:

1
2
3
4
5
6
7
use 5.010;
given( $ARGV[0] ) {
    when( $_ ~~ /fred/i ) { say 'Name has fred in it'; continue }
    when( $_ ~~ /^Fred/ ) { say 'Name starts with Fred'; continue }
    when( $_ ~~ 'Fred'  ) { say 'Name is Fred'; continue } # 注意!
    default               { say "I don't see a Fred" }
    }

--实际上这样写是有问题的,这里的default总是会运行
$ perl5.10.0 switch.pl Alfred
Name has fred in it
I don't see a Fred

--default块相当于一个测试条件永远为真的when语句。如果在default之前的when语句使用了continue,Per就会继续执行default语句。因此可以说default就是一个特殊的when:

1
2
3
4
5
6
7
use 5.010;
given( $ARGV[0] ) {
    when( $_ ~~ /fred/i ) { say 'Name has fred in it'; continue }
    when( $_ ~~ /^Fred/ ) { say 'Name starts with Fred'; continue }
    when( $_ ~~ 'Fred'  ) { say 'Name is Fred'; continue } # 注意!
    when( 1 == 1        ) { say "I don't see a Fred" } # 相当于default语句块
    }

要解决这个问题,只要拿掉最后一个when的continue就可以了,改写成如下形式:

1
2
3
4
5
6
7
use 5.010;
given( $ARGV[0] ) {
    when( $_ ~~ /fred/i ) { say 'Name has fred in it'; continue }
    when( $_ ~~ /^Fred/ ) { say 'Name starts with Fred'; continue }
    when( $_ ~~ 'Fred'  ) { say 'Name is Fred'; break } # 现在就对了!
    when( 1 == 1        ) { say "I don't see a Fred" }  # 这里的when(1==1)可改写成default
    }

多个项目的when匹配

有些时候需要遍历很多元素,但given只能一次接受一个参数,当然可以将given语句放到foreach里面循环测试。比如要遍历@names,依次将各元素赋值到$name,然后再用given:

1
2
3
4
5
6
use 5.010;
foreach my $name ( @names ) {
    given( $name ) {
        ...
        }
    }

如果使用given-when语句,想必大家首先想到的就是上面的方法,实际上要遍历多个元素时就就不必使用given了,使用foreach的简写形式,让它给当前正在遍历的元素起个化名$_。此外若要用智能匹配,当前元素就只能是$_。

use 5.010;
foreach ( @names ) { # 不要使用命名变量!
    when( /fred/i ) { say 'Name has fred in it'; continue }
    when( /^Fred/ ) { say 'Name starts with Fred'; continue }
    when( 'Fred'  ) { say 'Name is Fred'; }
    default         { say "I don't see a Fred" }
    }

一般在遍历的时候,总希望可以看到当前的工作状态。可以在foreach语句块中写上其他的语句,比如say:

1
2
3
4
5
6
7
8
9
use 5.010;
foreach ( @names ) { # 不要使用命名变量!
    say "\nProcessing $_";
    when( /fred/i ) { say 'Name has fred in it'; continue }
    when( /^Fred/ ) { say 'Name starts with Fred'; continue }
    when( 'Fred'  ) { say 'Name is Fred'; }
    say "Moving on to default...";
    default         { say "I don't see a Fred" }
    }




本章练习
1、用given-when结构写一个程序,根据输入的数字,如果能被3整除,就打印“Fizz”,如果能被5整除就打印“Bin”,如果能被7整除就打印“Sausag”。比如输入数字15,程序打印“Fizz”和“Bin”

1
2
3
4
5
6
7
8
9
10
11
#!/usr/binperl -w

use strict;

use 5.010;

given($ARGV[0]){
    when(not $_ % 3){say 'Fizz';continue}
    when(not $_ % 5){say 'Bin';continue}
    when(not $_ % 7){say 'sausage'}
}

# perl ex_13_1.pl 3
Fizz
# perl ex_13_1.pl 15
Fizz
Bin
# perl ex_13_1.pl 105
Fizz
Bin
sausage
2、使用foreach-when写个程序,要求从命令行遍历某个目录下的文件,并报告每个文件的可读、可写和可执行属性状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/perl -w

use strict;

use 5.010;

my $SourcePath = $ARGV[0];
opendir MYDIR,$SourcePath or die "can't open $SourcePath :$!";

chdir $SourcePath;
foreach (readdir MYDIR){
say "\e[32m==============================\e[0m";
when(-x $_){say "$SourcePath/$_ \e[1;32mexecutable!\e[0m";continue}
when(-r $_){say "$SourcePath/$_ \e[1;35mreadable !\e[0m";continue}
when(-w $_){say "$SourcePath/$_ \e[1;33mwritable !\e[0m"}
}
closedir MYDIR

--我这里从命令行读取用户输入的目录,如果目录不存在,报错退出,否则切换当前路径到该目录下,依次遍历目录中的文件,使用when语句判断文件可读写情况。如果是.和..则直接略过,在测试文件属性时用到了前面介绍的一个技巧,即_也就是下划线,它代表了上一个测试的文件。
运行结果:

index 函数

使用方法:index(待检索的字符串,"检索什么",从哪开始检索);
如下:
my $string = "hello world!";
my $where = index($string,"wor");

$where 的结果为6,即wor中的w首次出现的位置,这个位置是从零开始算起的,如果你非要从1算起也可以,但解释就有些不同了,如果从1开始算起,你可以理解为“到达此字符之前经过了多少个字符数”。如果没有找到子串,返回-1。

index还有一个可选的第三个参数,即指定开始搜索的地方,这样index就不会从开始的地方开始查找了:

my $stuff  = "Howdy world!";
my $where1 = index($stuff, "w");               # $where1 得到 2
my $where2 = index($stuff, "w", $where1 + 1);  # $where2 得到 6
my $where3 = index($stuff, "w", $where2 + 1);  # $where3 得到 −1 (没有找到)

与index函数相对应的是rindex函数,它是查找字符串中子串最后出现的位置
my $last_slash = rindex("/etc/passwd", "/"); # 值为 4

用substr处理子串

substr处理较长字符串中的一部分,用法如下:
$part = substr($string, $initial_position, $length);
该函数支持三个参数,分别为:待处理的字符串、从零算起的起始位置、子串的长度。如下面的例子:

my $mineral = substr("Fred J. Flintstone", 8, 5);  # 得到 "Flint"
my $rock = substr "Fred J. Flintstone", 13, 1000;  # 得到 "stone"

第二个例子中,我写的子串长度为1000,但前面给定的字符串总共也没有几个,这样一来perl就会将字符串第13个开始到最后的所有字符都截取出来。当不知道字符串有多少但要想将某个位置开始到最后的所有字符都截取时,可以省略第三个参数,这样也可以达到同样的目的。

字符串中子串的位置可以为负数,表示从字符串结尾开始倒数,看看下面的例子:

my $out = substr("some very long string",3, 2);  # $out 得到"in"

接下来看一个index和substr结合使用的例子:

my $long = "some very very long string";
my $right = substr($long, index($long, "l") );

下面是将选取的字符串部分替换为其他的内容:

my $string = "Hello World!";
substr($string,0,5) = "Goodbye";
# $string 值变为了Goodbye World!

如例子所示,被替换的字符长度不必与选取出的字符相等。甚至可以使用绑定操作符(=~)只对字符串的某部分进行替换:

substr($string,20) =~ s/fred/barney/g;

substr与index做的事情多半也可以使用正则表达式来做到。所以,请选择最适合解决问题的方法。但这里要说一点,通常substr与index的速度会略快一些,因为它们不用载入正则表达式引擎。它们总是区分大小写,不必担心元字符,而且不会设定任何的内存变量。

如果不给substr赋值,可以使用传统的四个参数的形式来使用,第四个参数为要替换的字符串,整个substr函数返回的内容仍然为找到的字符串。

my $previous_value = substr($string, 0, 5, "Goodbye");

用sprintf格式化数据

sprintf函数和printf函数类似,相同点是他们都是格式化字符串的,但sprintf不负责输出,只是格式化字符串然后将格式化后的字符串返回。如下:

my $date_tag = sprintf
"%4d/%02d/%02d %2d:%02d:%02d",
$yr, $mo, $da, $h, $m, $s;

上面的例子中$date_tag会得到类似2010/05/24 5:20:03的结果。格式字符作为sprintf的第一个参数,会在某些格式数值前置零。格式串中的数值字段的前置零,表示必要的时候会在数值前面补零以符合要求的宽度。

下面的例子,会将一个较大的数字格式化为美元习惯的xx,xxx,xxx.xx的格式:

#!/usr/bin/perl -w

use strict;

sub big_money{
my $number = sprintf "%.2f",shift @_;
1 while $number =~ s/^(-?\d+)(\d\d\d)/$1,$2/;
$number =~ s/^(-?)/$1\$/;
$number;
}

my $money = &big_money(@ARGV);

print "$money\n";

# perl sprintf.pl 123456789.1
$123,456,789.10

高级排序

perl中的排序并不是前面章节介绍的sort函数那么简单,下面讲解更灵活,更复杂的排序。

首先要定义排序子程序,在子程序中说明一下我这个排序是按照什么规则排序的。如,数字,字符等。排序子程序的编写也很简单,你没有必要编写很多数字或字符的排序的过程,实际上你只需要声明一下是那种类型的比较就可以了。并在最后给出返回值即可。相等返回0,大于返回1,小于返回-1。直接看例子:

简单的按照数字排序的 排序子程序:

sub by_number {
# a sort subroutine, expect $a and $b
if ($a < $b) {1 } elsif ($a > $b) { 1 } else { 0 }
}

#定义完排序子程序之后通过下面的方式调用
my @result = sort by_number @some_numbers;

实际上排序子程序可以写成如下的简略形式:
sub by_number { $a <=> $b }

这里用到了飞碟操作符(<=>)该操作符只能用来比较两个数值,并返回-1、0或1
与飞碟操作符对应的那肯定是字符比较操作符cmp了,与<=>一样他们都是三路比较操作符。如下定义和调用字符串排序子程序:

sub ASCIIbetically { $a cmp $b }
my @strings = sort ASCIIbetically @any_strings;

你还可以使用cmp操作符定义更高级的字符排序子程序,如不区分大小写的排序:
sub case_insensitive { "\L$a" cmp "\L$b" }
在这个例子中我们强制将$a,$b转换为小写然后再进行比较。要注意的是,我们并没有修改被比较的内容,我们只是那里使用了一下而已。实际上$a,$b并不是数据项的复制,而是原始列表元素的临时名称而已,因此千万不要修改它们的值。否则被比较的信息可能会改变。

如果排序子程序像例子一样简单,那就可以让程序代码更为简单,只要把子程序内嵌到函数名的位置就可以了。如下:
my @numbers = sort { $a <=> $b } @some_numbers;
实际上在Perl高手中几乎不会有人单独的写一个排序子程序。如果要逆序排序,可以直接用reverse函数来轻松解决:
my @descending = reverse sort { $a <=> $b } @some_numbers;
小技巧:<=>与cmp操作符并不知道谁是$a,谁是$b,只要你把$a写在左边那就是正序,否则就是逆序。

哈希按值排序

上文已经介绍了列表的排序,本节介绍哈希值的排序。如有下面的哈希:

my %score = ("barney"=> 195,"fred"=>205,"dino"=>30);

定义排序子程序

sub  by_score {$score{$b}<=>$score{$a}}

这个排序子程序很容易实现。我们要的是积分的数值比较,而不是名字。换句话说,我们不应该比较$a与$b这两个球员的名字,而是比较$score{$a}与$score{$b}这两个积分。如果想到了这一点,那答案就出来了。

按多值排序

接着上面的例子,哈希中含有3个人名,并且相对应的也有3个分数,如果我再加一个人,积分和某一个相同那该如何处理呢?按照常规处理,积分相同的会返回0,本次比较无效,我们可以再加一个比较规则,也即是如果积分相同的话就按照人名将积分相同的人再排序,如下:

my %score = (
"barney" => 195, "fred" => 205,
"dino" => 30, "bamm-bamm" => 195,
);

这里bamm-bamm和barney积分相同。所所以在排序完成后,哪一个应该排在前面是不可预知的,因此我又加入了按照名字的ASCII码排序。如下:

my @winners = sort by_score_and_name keys %score;
sub by_score_and_name {
$score{$b} <=> $score{$a}  # 按积分排序
or
$a cmp $b                  # 名字ASCII码排序
}

自己编写的一个实例:

#!/usr/bin/perl -w

use strict;

my %score = ("zyq"=>100,"wcl"=>210,"zyz"=>98,"pf"=>182);

sub by_score{$score{$b}<=>$score{$a}}

sub by_score_and_name{
$score{$b}<=>$score{$a}
or
$a cmp $b
}

print "sort hash by score:\n";
my @winner = sort by_score keys %score;
foreach (@winner){
print "$_ => $score{$_}\n";
}

print "\nsort hash by scroe and name:\n";
%score=("zyq"=>100,"wcl"=>210,"zyz"=>98,"pf"=>182,"fyr"=>98);
@winner = sort by_score_and_name keys %score;
foreach (@winner){
print "$_ => $score{$_}\n";
}

输出结果

sort hash by score:
wcl => 210
pf => 182
zyq => 100
zyz => 98

sort hash by scroe and name:
wcl => 210
pf => 182
zyq => 100
fyr => 98
zyz => 98

有人可能会问,上面的例子,如果积分和人名都一样那怎么排序?仔细想……我排序的是哈希,哈希的键是不可重复的,因此不会出现人名重复的情况。

这个例子只是涉及到两级比较,如果又需要的话还可以进行更多级别的比较,如下:

@patron_IDs = sort {
&fines($b) <=> &fines($a) or
$items{$b} <=> $items{$a} or
$family_name{$a} cmp $family_name{$a} or
$personal_name{$a} cmp $family_name{$b} or
$a <=> $b
} @patron_IDs;

本章练习

1、编程读入一连串的数字并将它们按值排序,将结果以靠右对其的格式输出。请以下列数据来测试你的程序。
17 1000 04 1.50 3.14159 −10 1.5 4 2001 90210 666
写法1

#!/usr/bin/perl -w

use strict;

my @sort_num = sort {$a<=>$b} @ARGV;

foreach (@sort_num){
printf "%10g\n",$_;
}

运行:perl ex_12_1.pl 17 1000 04 1.50 3.14159 −10 1.5 4 2001 90210 666
结果:

       1.5
       1.5
   3.14159
         4
         4
        10
        17
       666
      1000
      2001
     90210

写法2

#!/usr/bin/perl -w

use strict;

my @number;
push @number,split while <>;
foreach (sort {$a<=>$b} @number){
printf "%10g\n",$_;
}

这种写法不是以参数的形式运行程序的,而是运行程序后输入待排序的字符,输入完成后按Ctrl+D结束输入。
程序的第二行可以写成
while(<>){
push @number,split
}
的格式,while循环么次会读入一行(从用户所要求的输入来源,也就是钻石操作符),接着split会以空白来分隔该行,与乘胜一个数字列表,这样一来while循环就会将其中所有的数字存进@number里。

2、编程以不区分大小写的字符顺序把下列的哈希数据按姓氏排序后输出。当姓一样时,就以名字排序(同样不区分大小写)。也就是说,输出结果中的第一个名字应该是Fred,最后一个应该是Betty。所有姓相同的人应该排在一起。千万不别更改原始数据。这些名字应该以它原来的大小写形式被显示出来。
my %last_name = qw{
fred flintstone Wilma Flintstone Barney Rubble
betty rubble Bamm-Bamm Rubble PEBBLES FLINTSTONE
};

答:

#!/usr/bin/perl -w

use strict;

my %last_name = qw{
fred flintstone Wilma Flintstone Barney Rubble
betty rubble Bamm-Bamm Rubble PEBBLES FLINTSTONE
};

sub sort_by_name{
"\L$last_name{$a}" cmp "\L$last_name{$b}"
or
"\L$a" cmp "\L$b"
}
my @sort_name = sort sort_by_name keys %last_name;

foreach (@sort_name){
print "$_ $last_name{$_}\n";
}

3、编程在输入字符串中找出特定子串出现的位置并将其输出。例如:输入字符串为“This is a test.”,而子串是“is”,则程序应该会报告位置2和5;如果子串是“a”,程序应该会报告8,如果子串是“t”,程序将报告什么?
答:写法1 程序中指定待检索字符和子串

#!/usr/bin/perl -w

use strict;

my $string="This is a test.";
my $location=-1;

while(1){
$location = index($string,"is",$location+1);

last if $location == -1;
printf "$location\t";
}

print "\n";

写法2:用户命令行输入字符串和子串

#!/usr/bin/perl -w

use strict;

print "pleash in put string:";
chomp(my $string=<STDIN>);

print "pleash input sub string:";
chomp(my $substring=<STDIN>);

my $location = -1;

while(1){
    $location = index($string,$substring,$location+1);

    last if $location==-1;
    print "$location\t";
}

print "\n";

运行/结果:
# perl ex_14_3_1.pl
pleash in put string:this is a best test!
pleash input sub string:t
0 13 15 18

在目录树中切换

程序运行时会以自己的工作目录作为相对路径的起点。可以使用chdir操作符来改变当前的工作目录,这和shell下的cd命令差不多:
chdir "/etc" or die "can't chdir to /etc:$!";
注意这里我用到了$!变量,由于这是对操作系统的调用,所以发生错误时便会设定标量标量$!的值。如果chdir的返回为假,则表示有问题发生,这时通常需要检查一下$!变量的内容。

如果chdir省略参数则跳到当前的目录,这实际上是没有什么意义的。

文件名通配

一般来说shell会将命令行里的文件名模式展开成所有匹配的文件名。这称之为文件名通配。如,假设将*.pm这个文件名模式交给echo命令,shell会将它展开成名成相匹配的文件列表:

$ echo *.pm
barney.pm dino.pm fred.pm wilma.pm

这里的echo命令不必知道该如何展开*.pm,因为shell会将它展开,再交给echo处理,这也同样适合于perl程序:

#!/usr/bin/perl -w
use strict;

foreach  (@ARGV) {
print "one arg is $_\n";
}

$ perl show-args *.txt
one arg is file.txt
one arg is letter.txt
在这里我上文的程序并不了解如何进行文件通配的处理,放在@ARGV里的已经是展开好了的名称。

有些时候在程序内部,也可能会想要用*.txt之类的模式。我们可以使用glob操作符将它展开成相匹配的文件名:
my @all_files = glob "*";
my @txt_files = glob "*.txt";

--其中@all_files会取得当前目录中的所有文件,并按字母顺序排序,但不包含以点号开头的文件。@txt_files得到所有以.txt结尾的文件。
下面是一次匹配多个模式的例子:
 my @all_files_including_dot = glob ".* *";
--其中".*"代表以点开头的文件名,另外注意引号之内两个模式中间的空格是有意的,它分隔了两个要进行文件名通配处理的条目。

文件通配符的另外一种写法

除了上面介绍的glob操作符之外我们可以使用一种简单的方式在perl使用文件通配符,那就是"尖括号写法"(<>):

my @all_files = <*>
# 与 my @my_files = glob "*" 一样

my $dir = "/etc";
my @dir_files = <$dir/* $dir/.*>;
# $dir变量也会替换为相应的内容

注意:尖括号在perl中即代表了从文件句柄读取,又代表了文件名通配操作,那Perl是如何决定是哪一种的呢?因为合理的文件句柄必须是严格意义上的Perl标识符,所以如果尖括号满足Perl标识符条件的,就作为文件句柄来读取,否则它代表的iushi文件名通配操作。举例来说:

my @files = <FRED/*>;  ## glob
my @lines = <FRED>;    ## 从文件句柄读取
my $name = "FRED";
my @files = <$name/*>; ## glob
my @lines = <$name>; ## 对句柄FRED进行间接文件句柄读取

目录句柄

若要从目录中取得文件名列表,那就要用到目录句柄,它和文件句柄类似,可以使用opendir打开,readdir读取,closedir关闭,只不多读取的时候是目中的文件名而不是文件的内容,如下例子:
readdir.pl

#!/usr/bin/perl -w
use strict;

my $dir_to_process = "/etc";
opendir DH, $dir_to_process or die "Cannot open $dir_to_process: $!";
foreach my $file (readdir DH) {
print "one file in $dir_to_process is $file\n";
}
closedir DH;

运行:perl readdir.pl
one file in /etc is .
one file in /etc is ..
one file in /etc is bashrc
one file in /etc is auto.misc
one file in /etc is firmware
one file in /etc is libaudit.conf
...
one file in /etc is netplug.d
--和文件句柄一样,目录句柄会在程序结束或再用这个句柄打开另一个目录前自动关闭。上面的例子中将. 和..文件都输出了,我现在不想显示这些文件,只想显示*.conf的文件,且按照字母顺序排序,那将如何修改之前的程序呢:
readdir_2.pl

#!/usr/bin/perl -w
use strict;

my @file;
my $dir_to_process = "/etc";
opendir DH, $dir_to_process or die "Cannot open $dir_to_process: $!";
foreach  my $file (readdir DH) {
next if $file =~ /^\./;
next unless $file =~ /\.conf$/;
push @file,"$dir_to_process/$file\n";
}

print sort @file;
closedir DH;

--这样输出的文件是按照字母顺序且都是.conf的文件

删除文件

使用unlink操作符删除文件:
unlink "slate", "bedrock", "lava";
既然unlink的参数是列表,glob函数又恰好返回列表,我们融合两者可以一次删除多个文件:
unlink glob "*.o";
unlink的返回值是成功删除了多少个文件,下面的例子可以检查unlink是否执行成功:

my $successful = unlink "slate", "bedrock", "lava";
print "I deleted $successful file(s) just now\n";

重命名文件

使用rename函数来重命名文件,写法: rename "old","new";
下面的例子是将所有的.old文件重命名为.new
rename.pl

foreach my $file (glob "*.old") {
my $newfile = $file;
$newfile =~ s/\.old$/.new/;
if (-e $newfile) {
warn "can't rename $file to $newfile: $newfile exists\n";
} elsif (rename $file, $newfile) {
## 改名成功,什么都不做
} else {
warn "rename $file to $newfile failed: $!\n";

--此程序会先检查$newfile是否村咋,因为只要用户有删除目标文件的权限rename就会毫不忧郁的覆盖现有的文件。加上这项敬爱奶茶可以减低损失数据的几率

建立和删除目录

mkdir DIRNAME,MASK

mkdir执行后成功返回true失败返回false并设置$!的内容。

[ccie lang="perl"]mkdir "fred", 0755 or warn "Cannot make fred directory: $!";[/cce]

注意:MASK最好不要用变量的形式指定,否则perl会认为这个变量时十进制的数字,而非mkdir认为的八进制,因此,在指定MASK最好使用数字,或者用oct函数转换一下变量,如下:

my $name = "fred";
my $permissions = "0755";
mkdir $name, oct($permissions);

修改权限

chmod 0755, "fred", "barney";
chmod函数与shell下的命令是一致的

更改属主

my $user = 1004;
my $group = 100;
chown $user, $group, glob "*.o";

--如果要处理的不是数字,而是像apache这样的字符串该如何处理呢?很简单,使用getpwnam与getgrnam函数来把用户名或组名转成数字即可,如下:

defined(my $user = getpwnam "merlyn") or die "bad user";
defined(my $group = getgrnam "users") or die "bad group";
chown $user, $group, glob "/home/merlyn/*";

--这里我们使用了defined函数来确认返回值不是undef,也就是说验证指定的用户或组是否真的存在。

一、文件测试操作符及其意义:

-r 文件或目录,对目前(有效地)用户或组来说是可读的
-w 文件或目录,对目前(有效地)用户或组来说是可写的
-x 文件或目录,对目前(有效地)用户或组来说是可执行的
-o 文件或目录,由目前(有效的)用户拥有
-R 文件或目录,对实际的用户或组来说是可读的
-W 文件或目录,对实际的用户或组来说是可写的
-X 文件或目录,对实际的用户或组来说是可执行的
-O 文件或目录,由实际的用户拥有
-e 文件或目录,是存在的
-z 文件存在而且没有内容(对目录来说永远为假)
-s 文件或目录存在并且有内容(返回值是以字节为单位的文件大小)
-f 是普通文件
-d 是目录
-l 是符号链接
-S 是socket类型的文件
-p 是命名管道,也就是先入先出(fifo)队列
-b 是块设备文件(如某个可挂载的磁盘)
-c 是字符设备文件(如某个I/O设备)
-u 文件或目录设置了setuid位
-g 文件或目录设置了setgid位
-k 文件或目录设置了sticky位
-t 文件句柄是TTY设备
-T 看起来想文本文件
-B 看起来是二进制文件
-M 最后一次修改后至今的天数
-A 最后一次访问后至今的天数
-C 最后一次文件节点编号(inode)变更后至今的天数

二、同一文件多个测试属性

一般两个测试属性可以用and操作符来连接:
if ( -r $file and -w $file){
...
}
实际上这样的写法是比较浪费资源的,通常情况下可以写成如下的简写方式:
if ( -r $file and -w _ ) {
...
}
注意:这里使用下划线(_) 来代替文件名,这是因为我前面已经用-r测试操作符对$file进行了测试,当第二次测试同一个文件的时候可以使用这种简写方式,当然第三次,第四次也可以使用,但一定是没有另一个文件测试之前。否则就变成了上一个测试的文件了。多说无益,看看下面的例子:

if( -r $file ) {
print "The file is readable!\n";
}
lookup( $other_file );
if( -w _ ) {
print "The file is writable!\n";
}
sub lookup {
return -w $_[0];
}

像上例的情况,-w测试的文件就不是$file,而是$other_file

三、栈式文件测试操作

Perl5.10 以后的版本支持这种写法

use 5.010;
if( -r -w -x -o -d $file ) {
print "dir is readable, writable, and executable!\n";
}

注意:对于返回真假值以外的测试来说,栈式写法并不出色。如下面的例子,我本意想确认小于512字节的目录,但实际上没有做到:

use 5.010;
if( -s -d $file < 512) { # 注意,不要这样写
print "The directory is less than 512 bytes!\n";
}

上面的可以展开写成如下的格式:

if( ( -d $file and -s _ ) < 512 ) {
print "The directory is less than 512 bytes!\n";
}

当-d返回假时,Perl将假值同数字512作比较。比较的结果变为真,因为假等效于数字0,而0永远小于512,为了避免这种问题,还是写成分开的方式比较好:

if( -d $file and -s _ < 512 ) {
print "The directory is less than 512 bytes!\n";
}

四、stat和lstat函数

这两个函数会测试 文件或目录,然后返回一个含有13个元素的列表,详细信息如下:
my($dev, $ino, $mode, $nlink, $uid, $gid, $rdev,
$size, $atime, $mtime, $ctime, $blksize, $blocks)
= stat($filename);
对符号链接符调用stat函数的话,会返回符号连接指向的文件信息,如果要返回符号连接的信息请使用lstat函数

五、localtime函数

前文的stat函数中返回的atime,mtime,ctime的格式类似于1272349300这样的值,该值是从1900年至现在的秒数,可以利用locattime函数将该值格式化一下:

#!/usr/bin/perl -w

use strict;

my $file=$ARGV[0];
my($dev, $ino, $mode, $nlink, $uid, $gid, $rdev,
$size, $atime, $mtime, $ctime, $blksize, $blocks)
= stat($file);

my($sec, $min, $hour, $day, $mon, $year,
$wday, $yday, $isdst) = localtime $atime;

print "$ARGV[0] atime is :$atime\n";

$year+=1900;
print "$ARGV[0] localtime atime is :$year\n";

执行# perl localtime.pl /etc/passwd
后输出结果为:
/etc/passwd atime is :1272349801
/etc/passwd localtime atime is :2010
由于localtime函数获得的year也是1900年到现在经过的年数,因此,要得到现在的年应该加1900。

$wday 代表今天是本周的第几天,0为周日3为周三
$yday 代表今天是今年的第几天,范围是 0..364 (或 0..365 闰年.)
此外还有2个有用的函数:
gmtime 返回世界标准时间
time 从系统时钟取得时间戳

=================本章习题===================

1、写一程序,从命令行取得文件名,并汇报这些文件是否可读、可写、可执行以及是否的确存在。程序可以支持一次传送多个要测试属性的文件。

#!/usr/bin/perl -w

use strict;

foreach my $file (@ARGV){
if (! -e $file){
print "$file not exists!\n";
last;
}
-r $file?print "$file is readable!\n"
:print "$file not readable!\n";
-w $file?print "$file is writeable!\n"
:print "$file not writeable!\n";
-x $file?print "$file is executable!\n"
:print "$file not executable!\n";
-o $file?print "$file is owned by this user!\n"
:print "$file not owned by this user!\n";
}

2、编写程序,从命令行参数指定的文件中找出最旧的文件并且以天数汇报它存在了多久,若命令行参数为空程序改如何处理?

#!/usr/bin/perl -w

use strict;

die "No file names supplied!\n" unless @ARGV;

my $old_file = shift @ARGV;
my $old_age = -M $old_file;

foreach (@ARGV){
my $age = -M;
($old_file,$old_age) = ($_,$age) if
$age>$old_age;
}

printf "the oldest file is %s,and it was %.1f days old\n",$old_file,$old_age;

执行:perl ex_2.pl /etc/*
输出结果:
the oldest file is /etc/hosts.allow,and it was 3757.4 days old

==========================================

unless控制结构

unless控制结构与if控制结构恰恰相反,if是判断表达式为真才执行下面的语句块,unless则判断表达式为假就执行语句块。

如果unless控制结构中加入else子句的时候那就相当于将if...else的代码执行部分反过来写是一致的:

unless ($mon =~ /^Feb/) {
print "This month has at least thirty days.\n";
} else {
print "Do you see what's going on here?\n";
}

if ($mon =~ /^Feb/) {
print "Do you see what's going on here?\n";
} else {
print "This month has at least thirty days.\n";
}

相同

until控制结构

有时也许会想要颠倒while循环的条件。那么可以使用until

until($j>$i){
$j *=2;
}

这个循环回一直执行,直到条件为真。它只不过是个改装过的while循环罢了,两者之前唯一的差别在于until会在条件为假的时候重复执行。

条件修饰词

表达式后面可以接着一个用来控制它的修饰词,例如用if修饰词来模拟一个if快:
print "$n is a negative number.\n" if $n < 0;这一句代码相当于下列的语句块:

if ($n < 0) {
print "$n is a negative number.\n";
}

除了if修饰词外,还有以下的修饰词:
&error("Invalid input") unless &valid($input);
$i *= 2 until $i > $j;
print " ", ($n += 2) while $n < 10;
&greet($_) foreach @person;

裸块控制结构

所谓的裸快控制结构是没有关键字或条件的代码块。不如现在有一个while循环,如下所示:
while (condition) {
body;
body;
body;
}
然后拿走关键字while和条件,就会得到一个裸块:
{
body;
body;
body;
}
裸块像一个while或foreach循环,可它从不循环,只执行一次。也就是一个伪循环!

裸块还有一个好处,就是可以在裸块里使用临时变量,而不会影响到全局变量。

elsif子句

许多情况下,你需要逐项检查一系列的条件表达式,看看其中哪个为真。这可以通过if控制结构的elsif子句来写成如下代码:

if ( ! defined $dino) {
print "The value is undef.\n";
} elsif ($dino =~ /^-?\d+\.?$/) {
print "The value is an integer.\n";
} elsif ($dino =~ /^-?\d*\.\d+$/) {
print "The value is a _simple_ floating-point number.\n";
} elsif ($dino eq '') {
print "The value is the empty string.\n";
} else {
print "The value is the string '$dino'.\n";
}

自增和自减

很简单与C/C++一致

for控制结构

很简单与C/C++一致

循环控制

last操作符

操作符last能立即终止循环。就像在C语言中的break操作符一样。当看到last,循环就会结束,例如:

# 输出所有包含 fred的行,直到碰到 __END__ 标记为止
while (<STDIN>) {
if (/__END__/) {
# No more input on or after this marker line
last;
} elsif (/fred/) {
print;
}
}
## last comes here #

只要输入行中有__END__记号,这个循环就会结束。

在Perl中有5中循环块。也就是for、foreach、while、until以及裸块而if块或者子程序带的花括号不是循环的快。last操作符对整个循环块起作用。

last操作符只会对运行中最内层的循环快发挥作用。

next操作符

next是跳过本次循环,像C中的continue一致。

redo操作符

redo操作符可以将控制返回到本次循环的顶端,不经过任何条件测试,也不会进入下一次循环迭代。而那些用过C语言的人们却对这个操作符感到陌生。因为C语言里没有这个概念。例子如下:

# 打字测试
my @words = qw{ fred barney pebbles dino wilma betty };
my $errors = 0;
foreach (@words) {
## redo 会跳到这里 ##
print "Type the word '$_': ";
chomp(my $try = <STDIN>);
if ($try ne $_) {
print "Sorry - That's not right.\n\n";
$errors++;
redo;  # 跳到循环的顶端
}
}
print "You've completed the test, with $errors errors.\n";

和另外两个操作符一样,redo在这5中循环快里都可以使用,并且在循环快嵌套的情况下只对最内层的循环起作用。

next和redo两者之前最大的区别在于next会正常继续下一次迭代,而redo则会重新执行这次的迭代。下面的例子可以然你体验这三种操作符工作方式的区别:

foreach (1..10) {
print "Iteration number $_.\n\n";
print "Please choose: last, next, redo, or none of the above? ";
chomp(my $choice = <STDIN>);
print "\n";
last if $choice =~ /last/i;
next if $choice =~ /next/i;
redo if $choice =~ /redo/i;
print "That wasn't any of the choices... onward!\n\n";
}
print "That's all, folks!\n";

三目操作符?:

perl里的三目操作符和C语言中的一致。
条件表达式真表达式:假表达式

逻辑操作符

&& / and 逻辑与
|| / or 逻辑或
! / not 逻辑非

if ($dessert{'cake'} && $dessert{'ice cream'}) {
# 两个条件都为真
print "Hooray! Cake and ice cream!\n";
} elsif ($dessert{'cake'} || $dessert{'ice cream'}) {
# 至少一个条件为真
print "That's still good...\n";
} else {
# 两个都为假——什么也不做
}

“定义或”操作符

在Perl 5.10中引入了“定义或”操作符//,在发现左边已定义的值时进行短路,无论作弊的只是真是假。这些定义或(defined-or)操作符会跟那些逻辑或操作符一样,只是它们测试的是是否已经定义,而不是布尔值。

use 5.010;
my $last_name = $last_name{$someone} // '(No last name)';

上面的含义为:如果last_name哈希中没有$someon的键时就是用//后面的值作为$last_name的值。

使用部分求值操作符的控制结构

之前看到的四个操作符 &&、 ||、 // 和?: 都有一个共性:根据左边的求值决定是否计算右侧的表达式。有些情况会执行的表达式,在另外的情况下并不执行。因此被统称为部分求值操作符,部分求值操作符是天然的控制结构,因为不会执行所有的表达式。

如果写过linux 下的shell脚本的话应该能够很快的了解这个部分求值操作符的说法,例如在linux 的shell中你可能写过如下的代码:
[ -e /etc/passwd ] && cat /etc/passwd
实际上在perl中也可以利用前面介绍的操作符来进行部分求值操作:
($m < $n) && ($m = $n); # 只有当$m的值小于$n的值时才会执行后面的操作
($m > 10) || print "why is it not greater?\n"; #当$m的值小于10时才会打印输出后面的内容。
三目操作符同样可以成为控制结构。下面的例子,我们想将$x赋值给两个变量中较小的那个:
($m < $n) ? ($m = $x) : ($n = $x);

“逻辑或”、“逻辑与”操作符的另外一种写法。可以讲他们写成单词or和and。这种单词的形式和标点符号的形式的效果是相同的,但是前者在运算优先级要低得多。既然单词操作符不会紧紧的粘住附近的表达式,它们需要的括号可能会少一些:
$m<$n and $m = $n; #写成相应的if语句版本会更好

另外一个常用的写法是:

open CHAPTER, $filename
or die "Can't open '$filename': $!";
通过使用低优先级的短路or操作符,我们表达了open this file or die的意思。如果文件打开成功,就会返回真,此时or就不必执行了;但如果文件打开失败,or就还得去执行右侧的部分,也就是丢出信息并终止程序。

====================本章习题=====================

1、编写程序,让用户不断猜测范围从1到100的秘密数字,直到猜中为止。程序应该以公式int(1+rand 100)来随进产生秘密数字。当用户猜错时,程序应该响应Too hight或Too low。如果用户键入quie或exit等字样,或者键入一个空白行,程序终止。当然用户猜到了,程序也终止。
答:

#!/usr/bin/perl -w

use strict;

my $num = int(1+rand 100);

while (1){
print "please input number:";
chomp(my $input = <STDIN>);

if ($input =~ /quit|exit|^\s*$/i){
print "You are a person without perseverance!\nbyebye!\n";
last;
}
elsif ($input > $num ){
print "Too hight!\n";
}
elsif($input < $num){
print "Too low!\n";
}
elsif($input == $num ){
print "Ok you are right!\n";
last;
}
}

注意:这里的判断退出的语句一定要放到最前面,否则就会出错。该程序中使用了int和rand函数,rand 100 会生成0以上100以下的随机数,但要注意,并非是随机整数,因此有可能为99.999,因此我这里用 1+rand 100来操作,最后利用int函数取出整数部分。

2、修改迁前一个程序,打印额外的调试信息,例如程序选择的秘密数组。确保修改的部分可以用开关控制,并且调试开关即使关上也不会产生警告。如果使用Perl 5.10版本可以用//操作符,否则请使用三目操作符。
答:这个程序是在之前的解答基础上做了少量的修改。我们需要在开发过程中打印秘密数字,所以在$Debug变量为真的时候调用print。而$Debug的值要么来自于环境变量,要么是默认值1.通过使用//操作符,我们在$ENV{DEBUG}未定义的时候设置它为1:

my $debug = $ENV{DEBUG}?$ENV{DEBUG}:1;

print "this number is :$num\n"  if $debug;

======本章到此结束======

1、用s///替换

$_ = "He's out bowling with Barney tonight.";
s/Barney/Fred/;  # 将 Barney 替换为 Fred
print "$_\n";

当然这里可以使用比较复杂的正则表达式

s/with (\w+)/against $1's team/;
print "$_\n"; 

输出He's out bowling against Fred's team tonight.

再来看看更多的示例:

$_ = "green scaly dinosaur";
s/(\w+) (\w+)/$2, $1/;  # 替换后为 "scaly, green dinosaur"
s/^/huge, /;            # 替换后为"huge, scaly, green dinosaur"
s/,.*een//;             # 将模式内容清空后为 "huge dinosaur"
s/green/red/;           # 匹配失败,结果仍为 "huge dinosaur"
s/\w+$/($`!)$&/;        # 替换后为 "huge (huge !)dinosaur"
s/\s+(!\W+)/$1 /;       # 替换后为 "huge (huge!) dinosaur"
s/huge/gigantic/;       # 替换后为 "gigantic (huge!) dinosaur"

s///返回的是布尔值,替换成功为真,否则为假。

使用/g进行全局替换

s///的模式只会替换到第一个匹配的结果,但如果在后面加上一个/g情况就发生了变化,它会替换所有匹配的内容。

示例:

$_ = "home, sweet home!";
s/home/cave/g;
print "$_\n";  # "cave, sweet cave!"

常见的替换应用是将多个空白替换为一个空格,如下:

$_ = "Input  data\t may have    extra whitespace.";
s/\s+/ /g;  # 结果为 "Input data may have extra whitespace."

删除开头和结尾的空格
s/^\s+//;
s/\s+$//;

也可以写成:s/^\s+|\s+$//g; 

不同的定界符

和m//与qw//一样,我们也可以改变s///的定界符。由于替换运算符要用到三个定界符,因此与先前的例子还有些不同,在使用成对出现的定界符是书写方式略有不同,如果不是成对出现的定界符就无所谓了,如下:

s#^https://#http://#;
s{fred}{barney};
s[fred](barney);
s<fred>#barney#;

第一个例子由于使用的不是成对出现的定界符,因此使用上与/差不多,后面几个例子使用的都是成对出现的定界符,因此在形式上有些不同,你甚至可以在一个替换模式中使用两种定界符!

可选修饰符

不仅是 /g 操作符,替换运算也可以使用我们经常在模式匹配中使用的 /i 、/x 与 /s 修饰符。它们使用时的顺序对结果没有影响。
s#wilma#Wilma#gi; # 将所有的 WiLmA 或 WILMA 替换为 Wilma
s{__END__.*}{}s;   # 将__END__标记之后的所有内容都清除

绑定操作符

像之前m//时提到的,我们可以使用绑定操作符为s///选择不同的目标

$file_name =~ s#^.*/##s; # 将 $file_name变量中所有Unix风格路径全部去掉

大小写转换

这里介绍几个转义字符:
\U 将其后的所有字符转换为大写
\u 将其后的第一个字符转换为大写
\L 将其后所有的字符转为小写
\l 将其后的第一个字符转换为小写
\E 结束大小写转换的影响

示例:

$_ = "I saw Barney with Fred.";
s/(fred|barney)/\U$1/gi;  # $_ 现在成了 "I saw BARNEY with FRED."
s/(fred|barney)/\L$1/gi;  # $_ 现在成了 "I saw barney with fred."
s/(\w+) with (\w+)/\U$2\E with $1/i;  # $_ 替换为 "I saw FRED with barney."
s/(fred|barney)/\u$1/ig;  # $_ 替换后为 "I saw FRED with Barney."
s/(fred|barney)/\u\L$1/ig;  # $_ 这里将首字母变为大写 "I saw Fred with Barney."

这里介绍的转义字符也适用于双引号包含的字符串中,如下:
print "Hello, \L\u$name\E, would you like to play a game?\n";

#!/usr/bin/perl -w
use strict;

$_ = "I saw Barney with Fred.";

s/(barney|fred)/\U$1/gi;

print "$_ \n";

s/(barney|fred)/\L$1/gi;

print "$_\n";

s/(\w+) with (\w+)/\U$2\E with $1/gi;

print "$_\n";

s/(barney|fred)/\u$1/gi;
print "$_\n";

s/(barney|fred)/\L\u$1/gi;
print "$_\n";

输出结果:
I saw BARNEY with FRED.
I saw barney with fred.
I saw FRED with barney.
I saw FRED with Barney.
I saw Fred with Barney.

Split操作符

split会根据分隔符拆分一个字符串,如果分割成功,返回的结果是按照分隔符切分的字段,否则返回空。

写法:@fields = split /separator/,$string;

任何匹配模式的内容都不会出现在返回的字段中。下面就是以冒号作为分隔符的典型split模式:
@fields = split /:/, "abc:def:g:h";  # 得到 ("abc", "def", "g", "h")

如果两个分隔符连在一起,会产生空字段:
@fields = split /:/, "abc:def::g:h";  # 得到 ("abc", "def", "", "g", "h")

split 还有一个不成文的规则:它会保留开头处的空字段,但却会省略结尾处的空字段。
@fields = split /:/, ":::a:b:c:::";  # 得到 ("", "", "", "a", "b", "c")

利用/\s+/模式进行空白分隔也是常见的做法。在此模式下,所有的空白会被当成一个空格来处理:

my $some_input = "This  is a \t        test.\n";
my @args = split /\s+/, $some_input;
# 得到 ("This", "is", "a", "test.")
# 上面的可以简写为下面的方式,因为split操作符默认是以空白分隔$_变量
my @fields = split;  # 与 split /\s+/, $_; 一致

Join函数

join函数不会使用模式,它的功能与split功能恰好相反,split会将字符串分解为数个子字符串,而join会把这些子字符串合并为一个字符串。用法如下:
my $result = join $glue,@pieces;
join函数的第一个参数是各个字符的分隔符,第二个参数就是需要合并的数组。
my $x = join ":",4,5,6,7,8,9,10,11;
结果$x为"4:5:6:7:8:9:10:11"

join与split的用法类似,但要记住join的第一个参数是字符串而非模式!

列表上下文中的m//

在列表上下文中使用的模式匹配操作符(m//)时,如果模式匹配成功,那么返回的是所有捕获变量的列表;如果匹配失败,则返回空列表:

$_ = "Hello there, neighbor!";
my($first, $second, $third) = /(\S+) (\S+), (\S+)/;
print "$second is my $third\n";

输出结果:there is my neighbor!
如此就能给那些匹配变量起即好记又动听的名字。

先前已经在s///的例子中用到了/g修饰符,该修饰符同样也可以用在m//操作符上,其效果就是让模式能够匹配到字符串中的许多地方。下面的例子中具有一对圆括号的模式,它会在每次匹配成功时返回一个捕获串:

my $text = "Fred dropped a 5 ton granite block on Mr. Slate";
my @words = ($text =~ /([a-z]+)/ig);
print "Result: @words\n";

输出结果:Result: Fred dropped a ton granite block on Mr Slate

这就相当于自己动手实现了split的功能,但并不是指定想要去除的部分(split中的第一个参数,也即是分隔符实际上就是要去除的部分),反而是指定想要流行的部分。

如果模式中有多对圆括号,那么每次匹配就能捕获多个串。假设,我想让一个字符串变成哈希,可以这样做:

#!/usr/bin/perl -w

use strict;

my $letter2name = "a apple b banana c carry
d dog e edit f fllow g good h height i idle"
;

my %newhash = ($letter2name =~ /(\w+)\s+(\w+)/g);

while (my ($key,$value) = each %newhash ){
print "$key => $value\n";
}

输出结果:
e => edit
a => apple
d => dog
c => carry
h => height
b => banany
g => good
f => fllow
i => idle

注意:本例中的模式匹配中一定要加上修饰符/g 否则就会匹配字符串中一次,这样的话哈希中也就只有一对值。如果加上/g的话就会对整个字符串进行全面的扫描了,从而将字符串完整的转换为哈希。

非贪婪量词

到目前为止我们碰到的4个量词({} + * ?)都是贪婪量词。也就是说在保证整体匹配的前提下,它们会尽量匹配长字符串。与之相对的就是非贪婪量词。对于每一个贪婪的量词,都有一个非贪婪的量词。以加号(+)为例,它的非贪婪量词为(+?),这表示一次或更多匹配(加号的本意)。但是匹配的字符串却是越短越好,而不是越长越好。举例来说:假如我想处理一个HTML文本,需要将去除<BOLD>跟</BOLD>这样的标记,并保留剩余的内容。如果要处理的字符串是这样:
I’m talking about the cartoon with Fred and <BOLD>Wilma</BOLD>!
那么可以用下面的这个替换表达式把标记去掉。
s#<BOLD>(.*)</BOLD>#$1#g;
但这样会不会有问题呢?实际上这个表达式考虑得太简单了,也可以说这里的星号量词太贪心了,如果换成下面的字符串该怎么办呢?
I thought you said Fred and  <BOLD>Velma</BOLD>, not  <BOLD>Wilma</BOLD>
这种情况下该模式会从第一个<BOLD>匹配直到最后一个</BOLD>,把这中间的部分全部取出来。这就错了,在这里我们就需要使用非贪婪量词。非贪婪版本的星号是*?,所以新的替换表达式应该写成这样:
s#<BOLD>(.*?)</BOLD>#$1#g;
这样就正确无误了!

看到这里你也就知道了剩下的两个量词的非贪婪版本了:
?? 虽然问号已经是匹配零次或一次了,但是这样写的话会优先考虑零次
{5,10}? 优先考虑5次

跨行的模式匹配

传统的正则表达式都是用来匹配单行文本。由于Perl可以处理任意长度的字符串,其模式匹配也可以处理多行文本,与处理单行文本并无差异。看看下面可以表示4行的文本:
$_ = "I'm much better\nthan Barney is\nat bowling,\nWilma.\n";
^和$通常是用来匹配整个字符串的开始和结束的。但当模式加上/m修饰符后,就可以让它们也匹配串内的换行符,这样一来,它们所代表的位置就不再是整个字符串的头尾,而是每行的开头跟结尾。因此,下面的模式就成立了:
print "Found 'wilma' at start of line\n" if /^wilma\b/im;

同样,也可以对多行文本逐个进行替换,参见下面的例子,先把整个文件读进一个变量,然后把文件名前置于每一行的开头:

#!/usr/bin/perl -w

use strict;

my $filename = "test.log";
open MAILLOG,$filename
or die "Can't open $filename:$!";

my $line = join '',<MAILLOG>;

$line =~ s/^/$filename : /gm;

print "$line";

一次更新多个文件

要在Perl中直接修改文件内容可以使用钻石操作符(<>),先看下面的例子:

#!/usr/bin/perl -w
use strict;
chomp(my $date = `date`);
$^I = ".bak";
while (<>) {
s/^Author:.*/Author: Randal L. Schwartz/;
s/^Phone:.*\n//;
s/^Date:.*/Date: $date/;
print;
}

程序一开始使用了系统的date命令,下一行则是对$^I变量的赋值,这个赋值是个扩展名,也就是再修改的时候要先备份一下,以防万一。
钻石操作符会读取命令行参数指定的那些文件。程序的主循环一次会读取、更新及输出一行

===================本章习题======================

1、写个程序来赋值并修改指定的文本文件。在副本里,此程序会把出现字符串Fred(大小写不计)的每一处都换成Larry。输入文件名应该在命令行上指定,输出文件名是本来的文件名加上.out。
答:

#!/usr/bin/perl -w

use strict;

my $in = $ARGV[0];

unless ( defined $in){
die "Usage $0 filename";
}

my $out= $in;
$out=~ (s/(\.\w)?$/.out/);

unless (open IN,"<$in"){
die "Open file $in error:$?";
}

unless (open OUT,">$out") {
die "Write file $out error:$?";
}

while (<IN>) {
s/fred/Larry/gi;
print OUT $_;
}

2、修改前一题的程序,以便把所有的Fred换成Wilma并把所有的Wilma换成Fred。如果输入的是fred&wilma,那么正确的输出应是Wilma&Fred。
答:

#!/usr/bin/perl -w

use strict;

my $in = $ARGV[0];

unless ( defined $in){
die "Usage $0 filename";
}

my $out= $in;
$out=~ (s/(\.\w)?$/.out/);

open IN,"<$in" or die "Open file $in error:$?";

unless (open OUT,">$out") {
die "Write file $out error:$?";
}

while (<IN>) {
s/fred/\0/gi;
s/wilma/Fred/gi;
s/\0/Willma/g;
print OUT $_;
}

注意:这里的替换是不能用正则表达式一步完成的,必须用一个中间量替换一下,然后再替换回来。本例使用\0也就是NULL作为中间量。

3、写个程序,把你目前写过的所有程序都加上版权声明,也就是在第一行的后面加上如下信息:## Copyright (C) 20XX by Yours Truly。你应该直接修改修改文件内容并且做备份。假设你将在命令行指定待修改的文件名。
答:

#!/usr/bin/perl -w

use strict;

$^I=".bak";

while (<>){
if (/^#!/){
$_.= "## Copyright (C) 2010 by Cooper!\n"
}
print;
}

4、修改前一题程序里的模式,如果文件中已经有版权声明,就不在进行修改。提示:你可能需要知道钻石操作符当前正在读取的文件名称,可以在$ARGV里找到。
答:

为了避免重复加上版权声明,我们得分两次处理文件。第一次,我们需要先建立一个哈希,它的键是文件名称,而它的值是什么无关紧要,为了安全起见,此处将值置为1:

my %file;
foreach (@ARGV){
$file{$_}=1;
}

第二次,我们会吧这个哈希当成待办列表逐个处理,并把已经包含版权声明的文件移除。目前正在读取的文件名称可用$ARGV获取,所以可以直接把它拿来当哈希键:

while (<>){
if (/^## Copyright/i) {
delete $file($ARGV);
}
}

最后的部分跟之前所写的程序一样,但我们会会事先把@ARGV的内容改掉:

@ARGV = sort keys %file;
$^I = ".bak";
while (<>){
if (/^#!/){
$_.= "## Copyright (C) 2010 by Cooper!\n"
}
print;
}

----------------本章到此结束----------------

Perl笔记:05、哈希

抢沙发

什么是哈希

哈希是一种数据结构,和数组相比
相同在于:可以容纳很多值(没有上限),并能随机存取。
区别在于:不像数组是以数字来检索,哈希是以名字来检索。也就是说检索用的键不是数字,而是保证唯一的字符串

所谓键其实就是字符串,所以我们不必用数字3来获取数组元素,而是用vilma这个名字来存取哈希元素。

这些键可以是任何字符串……你可以用任意字符串表达式作为哈希键。它们也必须是唯一的字符串,就像数组中只能有一个编号为3的元素一样。

哈希的键与值

哈希的键与值

另一种看待哈希的方法是将它想象成一大桶的数据,其中每个数据都有关联的标签,可以伸手到桶里任意取出一张标签,查看它附着的数据时什么。但是在桶里没有所谓的第一个元素,只有一堆数据。数组中有第一个元素0,但在哈希里没有顺序,只有一些键/值对。如下图:

哈希像一桶数据

这些键和值都是任意的标量,但会被转换成字符串,如50/20为键,那么他会转换成含有三个字符的字符串“2.5”。

根据Perl“去除不必要限制”的原则,哈希可能是任意大小,从没有任何键/值的空哈希到填满整个内存的哈希都可以。由于Perl针对哈希的算法优化,与替他语言不通,Perl在操作含有三个键值的哈希和操作三百万个键值的哈希速度是一样的。

访问哈希元素

要访问哈希元素,需要使用如下语法:
$hash{$some_key}

这和访问数组的做法类似,只是使用了花括号而非方括号来引出索引。而且现在的键表达式是字符串,而非数字:

$family_name{"fred"} = "flintstone";
$family_name{"barney"} = "rubble";

我们可以在程序中这样访问哈希:

foreach $person (qw< barney fred > ){
print "I've heard of $person $family_name{$person}.\n";
}

哈希变量的命名和其它Perl的标识符类似,可以有字母、数字和下划线,但不可以用数字开头。哈希有自己的命名空间,也就是说程序中可以定义同名的哈希,子程序,或数组,但按照正常人的思维方式还是不要这样办的好。

访问整个哈希

要指代整个哈希,可以用百分号(%)作为前缀。因此前面我们使用的哈希应该称之为%family_name

为了方便起见,哈希可以转换成列表,反过来也行。但当列表转为哈希时一定要确保列表中的元素为偶数个,否则的话会产生奇怪的结果。

哈希赋值

哈希的赋值可以用列表或者数组来初始化一个哈希,但这都不是最好的办法,Perl推荐使用使用“胖箭头”(=>)符号来对哈希进行赋值,如下:

my %last_name = (  # 哈希也可以是词法变量
"fred"    => "flintstone",
"dino"    => undef,
"barney"  => "rubble",
"betty"   => "rubble",
);

这样的赋值清晰明了。列表结尾有一个额外的逗号,这种写法不但无伤大雅,而且便于维护。但需要加入更多人的信息时,只要确保每行都有一组键/值对和结尾的逗号就可以了。

哈希函数

keys和values函数
keys函数能返回哈希的键列表,而values函数返回值列表。如果哈希没有任何成员,则两个函数都返回空列表:

my %hash = ("a"=>1,"b"=>2,"c"=3,);
my @k = keys %hash;
my @v = values %hash;

这样,@k包含“a”,“b”,“c”,而@v则会包含1,2,和3。需要注意,顺序可能会不同,别忘了Perl存储哈希的顺序无法预测。但可以确定的是返回的键列表和值列表的顺序是一样的。在标量上下文中这两个函数都会返回哈希中键/值对儿的个数。这个计算过程不必对整个哈希进行遍历,因而非常高效:

my $count = keys %hash;        # 得到 3,也就是说有三对儿键值

偶尔也能看到别人的程序里把哈希当成布尔表达式来判断真假,如下:

if (%hash){
print "That was a true value!\n";
}

只要哈希中有一个键/值对,就返回真。所以,这里的意思是如果哈希不为空就打印输出。这种写法很少见。

each函数
如果要罗列哈希的每个键/值对,可以使用each函数。它可以用两个元素的列表形式返回键/值对。每一次对同一个哈希调用此函数,他就会返回下一组键/值对,直到所有的元素都被访问过。也就是再没有任何新的键/值对,此时each会返回空列表。

实际使用时,唯一适合使用each的地方就是在while循环中,如下:

while ( ($key,$value) = each %hash) {
print "$key => $value\n";
}

这里有很多的操作,首先,each %hash会从哈希中返回一组键/值对,结果是含有两个元素的列表。该列表会赋值给($key,$value)。while循环中每次会从%hash取出一对数据赋值给$key,$value 如果到最后没有内容了,这两个变量的就会赋值为undef,默认情况下undef的值为0,因此这时候while就会终止循环了。当然each返回键/值对的顺序是乱的,也就是说和我先前赋值的时候顺序是不一致的。但它与keys和values返回的顺序相同,也就是哈希的自然顺序。那如果要想对哈希排序怎么办呢,只要对键排序就行了。如下方法:

foreach $key (sort keys %hash) {
$value = $hash{$key};
print "$key=> $value\n";
# 或者可以省略变量$value
# print "$key=>$hash{key}\n";
}

exists函数
若要检查哈戏中是否有某个键,可以使用exists函数,它能返回真或假,分别表示键存在与否,和键对应的值无关:

if ( exists $books{"dino"} ) {
print "Hey, there's a library card for dino!\n"
}

也就是说,exists $books{"dino"}返回真,意味着dino存在于keys %books返回的键列表中。

delete函数
该函数可以删除哈希中指定的键以及其对应的值。如果没有该键,则会直接结束,不会出现仍和的警告信息。

哈希值内插
可以将单一哈希元素内插到双引号引起的字符串中:

foreach $person (sort keys %books){
if($books{$person}){
print "$person has $books{$person} items\n";
}
}

但整个哈希的内插是不支持的,"$books"的结果只是6个字符的串 %books

%EVN哈希
Perl既然是允许在系统中,那么它就有必要知道当前系统的一些环境。Perl是利用%ENV哈希来存储这些信息的。例如在%ENV中存取PATH键的值:

print "PATH is $EVN{PATH}\n";

因此在Perl中可以从$ENV 中得到系统环境中的变量。

----------本章到此结束----------

读取标准输入

$line=<STDIN>;  # 读取下一行
chomp($line);  # 截掉最后的换行符
chomp(my $line = <STDIN>);  # 这一行相当于上面的两行

如果读到文件结尾,“行输入”操作符就会返回undef。这样的设计是为了配合循环使用,可以自然的跳出循环:

while(defined($line=<STDIN>)){
print "I saw $line\n";
}

第一行程序代码做了许多事:读取标准输入,将它存入某个变量、检查变量的值是否被定义,以及我们是否该执行while循环的主体(也就是还没有遇到输入的结尾)。因此在循环主体内,我们会在$line变量里看到各行输入的内容。这是很常见的操作,所以perl顺理成章的为它定义了一个简写形式:

while(){
print "I saw $_\n";
}

上面的代码和下面的是一样的:

 while(defined($_=)){
print "I saw $_\n";
}

钻石操作符输入

还有另外一种读取输入的方法,就是使用钻石操作符 <> 它能让程序在处理调用参数的时候,提供类似于Unix工具程序的功能。

说白了钻石操作符输入和标准的输入唯一区别就是 钻石操作符的来源不是键盘,而是运行命令时给出参数,这个参数一般情况下应该是文件。可以写多个参数。

调用参数

从技术上来说,钻石操作符其实不会去检查调用参数,他的参数其实是来自@ARGV数组。这个数组时由Perl解释 器事先建立的特殊数组,其内容就是由调用参数组成的列表。它和别的数组没有什么不同,只不过在程序开始运 行时,@ARGV里就已经塞满了调用参数。

钻石操作符会查看数组@ARGV以决定改用哪些文件名,如果它找到的是空列表,就会改用标准输入流;否则,就 会使用@ARGV里的文件列表。这表示程序开始运行之后,只要尚未使用钻石操作符,你就可以对@ARGV动手脚。这 样我们可以处理三个特定的文件,不管用户在命令行参数中指定了什么:

@ARGV = qw# larry moe curly #;  强制让钻石操作符读取这三个文件
while(<>){
chomp;
print "It was $_ than I saw in some strage-like file!\n";
}

输出到标准输出

print操作符的使用前文已经用过多次了

使用printf 格式化输出

my($user,$days_to_die) = ("zyq",28);
printf "Hello,%s;your password expires in $d days!\n",$user,$days_to_die;

printf可用的转换格式很多,要输出恰当的数值形式,可以使用%g,它会按需要自动选择浮点数、整数甚至是指数形式。%d格式代表十进制的整数,它会舍去小数点之后的数字。

在Perl里,printf最常用在字段式的数据输出上,因为大多数的转换格式都可以让你指定宽度。如果数据太长,字段会按需要自动扩展:

printf "%6d\n",42 # 输出结果为 ----42 (- 代表一个空格)

%s代表字符串格式,所以它的功能其实就是字符串内插,只是它还能设定字段宽度:
printf "%10s\n","wilma"; # 输出结果为 -----wilma

printf "%-10s\n","wilma"; # 输出结果为 wilma-----

文件句柄

文件句柄是需要进行操作的文件一个标识而已

打开文件句柄

Perl内置了三种文件句柄分别为:STDIN STDOUT STDERR 这三个文件句柄是自动打开的随着程序的终止而自动关闭。

当需要操作其他文件时,我们就需要使用oopen操作符来告诉perl我要打开其它的文件,如下例子:

open CONFIG,"dino";     # 以只读形式打开,此时的CONFIG就像一个指针指向dino文件
open CONFIG,"<fred";        # 同上
open CONFIG,">fred";        # 以复写的形式打开文件
open LOG,">>logfile";       # 以添加的形式打开文件

第一行会打开已经存在的dino文件,文件中的任何内容,都能从文件句柄CONFIG读到我们的程序中来。这和利用shell的重定向(例如<dino这样的写法)将文件内容经STDIN读入程序里的做法相似。实际上第二行正好利用了这样的技巧。它和第一行做所的事实完全相同的。只不过用了小于号来声明“此文件是用来输入的”,而这正是默认的操作。

在相对新的Perl(5.6版以后)中,open另有一种使用三个参数的写法,当然这种写法是规范且一目了然的:
open CONFIG,"<","dino";
open BEDROCK,">",$file_name;
open LOG,">>",&logfile_name();

其优点在于语法上可以很容易的区分模式串(第二个参数)与文件名本身(第三个参数),这种写法在安全方面有些好处。

关闭文件句柄

当不再使用某些文件句柄时要记得使用close操作符来关闭它们,尽管程序结束后会关闭所有的文件句柄,但手工关闭是一个很好的习惯。时刻记得open和close应该是成对出现的

close BEDROCK;

用die处理严重错误

当Perl遇到严重错误时,程序应该终止执行,并用错误信息告知原因。这样的功能用die函数可以实现,它让我们能够自定义“严重错误”信息。

die函数会输出你指定的信息,并且让你的程序立刻终止,并返回不为零的退出码。还有一个很不错的功能,就是可以报告程序出错的行数。

由于有了die命令,那么在上文的打开文件句柄时为了严谨考虑应该判断需要打开的文件是否成功的打开了。如果没有成功打开的话应该报错,如果这个文件很重要的话就必须终止程序的运行。所以上面的例子可以改写成:

if (! open LOG,">>logfile"){
die "Cannot create logfile:$!";
}

如果open失败,die会终止程序的运行,并且告诉我们无法创建日志文件。后面的$!是可读的系统出错信息。一般情况下,当系统拒绝我们所请求的服务时,$!会给我们一个理由。有可能是权限不够或文件找不到等。

使用文件句柄

当文件句柄与只读的形式打开时,perl可以轻而易举的从中读取数据如下:

if (! open PASSWD,"/etc/passwd"){
die "How did you get logged in?($!)"
}

while(<PASSWD>){
chomp;
...
}

正如你所看到的,所谓的“行输入”操作符是由两部分组成的:一对尖括号以及里面用来输入的文件句柄。

以写入或是添加模式打开的文件句柄可以在print或printf函数中使用。使用时,直接将它放在关键字之后、参数列表之前:

print LOG "Captain's log,stardate 3.14159\n";                # 输出到文件句柄LOG
printf STDERR "%d percent complete.\n",$date/$total*100;

注意到文件句柄和好输出的内容之间没有逗号了吗?实际上是可以加个括号的,但这个括号可有可无。

改变默认的文件输出句柄

默认情况下如果不为print或printf指定文件句柄的话,他们的输出就会送到STDOUT。不过可以使用select操作符来改变默认的文件句柄。下面的例子中我们会将输出送到BEDROCK这个文件句柄:

iselect BEDROCK;
print "I hope Mr.Slate doesn't find out about this.\n";
print "Wilma!";

一旦用select操作符切换了默认输出的文件句柄。程序就会一直往那里输出。因此在输出完毕后应该立刻将默认输出切换回来。将文件输出到文件句柄的话默认都会先送到缓冲区内,如果想立刻刷新缓冲区可以将$|变量置为1即将如下代码:

select LOG;
$|=1;            # 不要将LOG的内容保留到缓冲区
select STDOUT;
print LOG "this gets written to the LOG an once!\n";

复用标准文件句柄

如果你要复用文件句柄(换句话说,如果已经打开了某个名为FRED的文件句柄,而现在又要打开同样名为FRED的文件句柄),Perl会自动帮你关闭原有的文件句柄。不过要注意,不要复用Perl内置的6个标准文件句柄,除非你想使用该文件句柄实现特殊的功能。前文说过,来自die,warn的信息以及Perl内部的错误信息会送到STDERR。如果以上三个知识能融会贯通,你就会意识到,错误信息不一定送到程序的标准错误输出流,也可以送到文件里,如下:

if (! open STDERR,">>/home/barney/.error_log"){
die "Can't open error log for append:$!";
}

注意:在成功的打开.error_log文件后STDERR文件句柄才能算是真正意义上的复用了,Perl才会变比默认的系统文件句柄,如果打开文件失败,则不会复用STDERR句柄,Perl会自动返回内置的文件句柄。

使用say来输出

say 函数和print函数类似,都是输出一行信息,但say会自动在末尾加上换行符。其他的使用方法和print一样。

----------本章结束----------

子程序的定义

sub 子程序名{
子程序内容
}

子程序的调用

&子程序名

返回值

子程序中的最后一行即为返回值,如下示例:

#!/usr/bin/perl -w
$fred=5;
$barney=3;
sub sum_of_fred_and_barney{
print "Hey,you called the sum_of_fred_and_barney!\n";
$fred+$barney;    # 这就是返回值
}

$result=&sum_of_fred_and_barney;    # 这里的 $result 的值为15

但如果sum_of_fred_and_barney子程序修改为:

sub sum_of_fred_and_barney{
print "Hey,you called the sum_of_fred_and_barney!\n";
$fred+$barney;
print "Hey,I'm returning a value now!\n";
# 这里会给出结果1,也就是print函数执行成功之意
}

参数

在定义子程序的时候可以不用显示的指出参数个数和类型,但是在调用的时候就一定要给出参数列表。这点与C\C++语言就有些不同了
如下:

sub max{
if($_[0]>$_[1]){
$_[0];
} else {
$_[1];
}
}

上面这段代码中需要注意的是子程序的参数列表。默认情况下参数列表是属于 @_ 数组的,那么$_[0]就代表第一个元素也就是第一个参数,$_[1] 就是第二个参数。

$n = &max(4,5);

这时$n为5

那如果 $n = &max(4,5,6);

这样的话多余的参数会被忽略——反正子程序也不会用到$_[2],所以Perl并不在乎里面是否有值。参数如果不足也会被忽略——如果用到超出@_数组边界的参数,只会得到undef。

子程序中的私有变量

既然每次调用子程序是Perl都会给我们新的@_,难道不能让他产生私有变量吗?当然可以了!

默认情况下Perl中所有的变量都是全局变量。但如果在定义变量的前面使用my操作符的话,这个变量就变成了私有变量。
如下例子:

 sub max{
my ($m,$n)=@_;
if( $m>$n ){ $m; } else { $n; }
}

$m与$n的作用范围只在max子程序内,在程序其他部分的相应变量不会受到子程序变量的影响。反之,别的程序代码也无法访问或修改这些私有变量。

长度可变的参数列表

在真实的Perl代码中,尝尝把更长的(任意长度的)列表作为参数传给子程序。当然,子程序可以很容易的通过检查@_数组的长度来确定参数的个数是否正确。比方说我们可以将max函数完善一下检查参数列表:

 sub max{
if ( @_ !=2){
print "WARNING!&max should get exactly to arguments!\n";
}
my ($m,$n)=@_;
if( $m>$n ){ $m; } else { $n; }
}

更好的&max子程序

#!/usr/bin/perl -w

sub max{
my($max_so_far)=shift @_;    # 数组中的第一个值暂时把它当成最大值
foreach (@_){                # 遍历数组@_ 中的其他元素
if ( $_ > $max_so_far ){   # 判断当前的元素是否比$max_so_far还大
$max_so_far=$_;
}
}
$max_so_far;
}

$maximum = &max(3,4,5,6,7);

空参数列表

上文的&max函数即使超过2个参数,修改后的&max也可以应付,但如果一个参数都没有呢?这听起来似乎有些杞人忧天。毕竟,怎么可能会有人调用&max却不传入任何参数呢?但是,也许会有人写出如下的程序代码:

$maximum = &max(@numbers);

某些时候,数组@numbers或许是一个空的列表。也许数组中的内容是程序从文件里读入的,但文件却是空的。那么这种情况&max会怎么样呢?

子程序的第一行会对参数组@_(现在是空的)进行shift操作,以此作为$max_so_far的值。这并不会出错,因为数组是空的。所以shift会返回undef给$max_so_far

现在foreach循环要遍历@_数组,但是由于@_是空的,所以循环本身不会被执行。

接下来,perl将$max_so_far的值undef作为子程序的返回值。从某种角度来看,那是正确的结果,因为在空列表中没有最大的值。

关于语法(my)变量

事实上,词法变量可使用在任何语句块内,而不仅限于子程序的语句块。如if、while或foreach

每当我们谈到my()操作符是,请注意,在my不适用括号时,只用来声明单个词法变量如:
my $fred,$barney;        # 错!没声明 $barney
my ($fred,$barney);    # 两个都声明了

当然,也可以使用my来创建新的私有数组

my @phone_number;

所有新的变量的值一开始都是空的;标量被设为undef,数组被设为空列表。

use strict编译命令

所谓编译命令,就是对编译器的指示,告诉它关于程序代码的一些信息。这会让perl语法编译器强制执行一些严格的、确保良好程序设计的规则。

根据大部分人的建议,比整个屏幕长的程序都应该加上use strict

return 操作符

return操作符会从子程序中立即返回某个值:

#!/usr/bin/perl -w

use strict;

my @names = qw/zyq wcl zsc pf fyr zyz/;

sub which_element_is{
my($what,@array) = @_;
foreach (0..$#array){
if ( $what eq $array[$_] ){
return $_;
}
}

-1;
}

my $result = &which_element_is("wcl",@names);

print "$result\n";

输出结果为:
1

省略调用子程序时使用的&符号

有两种情况可以省略这个符号

情况1:调用子程序时使用的参数在括号内
my @cards = shuffle(@deck_of_cards);    # shuffle 前没有必要用&

情况2:子程序的定义在调用之前
sub division{
$_[0] / $_[1];
}

my $quotient = division 355,113;    # 这里甚至连括号都省略了。呵呵

特殊情况:当自己定义的子程序与Perl内置的函数同名时。如果少了&符号,就算我们已经定义了子程序&chomp,仍然会调用到内置函数chomp。

因此,真正的省略规则如下:

你知道所有Perl的内置函数名

非标量返回值

子程序不仅可以返回标量值,如果在列表上下文中调用它,他就能返回列表值。如下代码:

#!/usr/bin/perl -w

use strict;

my ($fred,$barney) = (11,6);

sub list_from_fred_to_barney{
if ($fred<$barney){
$fred..$barney;
} else {
reverse $barney..$fred;
}
}

my @c = &list_from_fred_to_barney;

print " @c\n"

输出结果:
11 10 9 8 7 6

持久性私有变量

说白了就是在子程序中的私有变量,但是这个私有变量在每次调用子程序时不会重新初始化。使用state操作符来定义持久性私有变量。

sub marine {
state $n = 0;  # private, persistent variable $n
$n += 1;
print "Hello, sailor number $n!\n";
}

----------本章结束----------