Perl闻名已久,这次终于下定决心好好学习一番。前两天从晓攀那里搜刮了一堆Perl的文档,不过仔细一看全是动辄三、四百页的大块头,感觉还真不适合像我这样想最短时间内能够了解这门语言的人看。我比较喜欢简短精炼的教程,比如我是通过A Byte of Python入门学习Python,通过A Byte of Vim入门学习Vim,通过Shell十三问对Shell的熟悉与理解更上一层楼的。

所幸地是,像我这样的人不在少数,我从StackOverflow网站的一个回答上找到一些教程,从中我挑了Learning Perl the Hard Way作为入门读物(从主页上下载了PDF版本,好像更新日期是2003年,不知道是不是过旧了?)。

昨天看完书中的第一章,整个过程还是比较纠结的。Perl给我一种熟悉又陌生的感觉,熟悉的是有些概念和Bash、Python是类似的,比如在双引号内部变量会展开(Variable interpolatio):

print "hello, $name\n";

比如shift语句,可以移动函数的参数列表:

my @first = shift;

比如List assignment,和Python中的用法也是大同小异:

my ($first, $second) = @pair;  # 这里假设pair数组

当然,更多的是一些不同,比如标量与数组的表示$/@(我也一直会搞混掉):

my $scalar = 1;   # 定义一个scalar var,使用$符号标记
my @array = (1, 2, 3); # 定义一个array var,使用@符号标记
my $first_element = $array[0]; # 获取数组的第一个元素,数组的元素是scalar var,所以用$;
my @slice = @array[0, 2];   # 重新定义一个数组,新数组的元素来自旧数组的第0和2个位置;
my @slice_2 = @array[0..2];  # 同上,不过取自旧数组的第0到2个位置;

比如一直强调的"The way a variable is evaluated depends on context!",不禁让人觉得Perl好复杂啊,针对这个原则最好的一个例子就是获取数组的大小:

my $len=@arr;
print "The length of arr is $len, items are: @arr\n";

还有一个例子是赋值操作的右侧是list literal时,在不同上下文场景下的行为令人诧异的不一致:

my @list = (1, 2, 3);  # 这里将(1, 2, 3)赋值给数组变量,数组的值为列表的元素,这里是1, 2, 3
my $scalar = (1, 2, 3); # 这里将(1, 2, 3)的最后一个元素,即3,赋值给标量scalar
my $scalar = @list;  # 这里将数组的大小赋值给scalar

Perl中函数的定义也是与很多类C的语言不一样,它是用sub来定义函数的,例如书中的一个例子:

sub echo {
    print "@_\n";
}
echo @ARGV

函数定义中没有指明参数列表,在函数内部通过@_这个特殊的数组来获取传入的参数。

当然,最神奇的莫过于open打开文件的语法了:

open FILE, "/etc/hosts"; # 方法1
open my $fh, "/etc/hosts"; # 方法2 

比起第二种方法,第一种方法里面的FILE(FileHandle,文件句柄)比较难以理解,这是一种特殊的变量类型吗?更神奇的还在后面,读取文件内容的方法是用angle operator:

my $first_line = <FILE>;
my $first_line = <$fh>;

下面是书中实现的一个简单的cat版本,用于输出文件内容:

use strict;
use warnings;

sub print_file {
    my $file = shift;
    open FILE, "$file";
    
    while (<FILE>) {  # 默认将读入的行保存在$_命令中
        print;  # 如果print后面为空,默认打印$_,因此这里等价于print $_
    }
}

sub cat {
    foreach (@_) {  # foreach获取第一个参数,即文件名
        print_file $_;  # 打印文件的内容,这里不要遗漏$_
    }
}

cat @ARGV;

上面是一个很好的例子,其中用到了很多Perl的语法和特性。最开始的"use strict"和"use warnings"是让脚本的编译执行更加严格,把一些隐晦的错误和警告在执行时提示出来,推荐写在脚本的最前面。

在调用函数的时候,可以有两种书写格式:

func_name 1, 2, 3;
func_name(1, 2, 3);

Perl的正则表达式是一个比较突出的特性,从它衍生出来的一个比较有名的正则表达式流派叫做PCRE(Perl Compatible Regular Expression),像Python的re模块也有对pcre的支持,这个流派的特征是像\d, \s, \w等标记。

在Perl中,字符串是否匹配正则表达式是用"=~"这个操作符来完成的,例如:

my $url = 'http://kodango.com';

if ($url =~ m/http:\/\/(.*)/) {
    print "\$1=$1\n";  # print $1=kodango.com
}

不过,比较特殊的是,分组的内容是保存在全局变量中的,比如上面例子中的$1。Perl的正则表达式是支持懒惰匹配的,只要在*/+等符号后面加一个?即可,例如:

if ('http://kodango.com/2013/05' =~ m/http:\/\/(.*?)\/.*/) {
    print "\$1=$1\n";
}

上面的正则表达式比较简单(一般情况下正则表达式就应该比较简单),但是如果有时候正则表达式比较长,那么换个人来阅读代码可能就不是那么容易了。Perl中支持另外一种扩展的书写格式,这种格式下,可以为正则表达式增加注释:

if ($url=~ m{
              (ftp|http) # protocol
              ://
              (.*?)      # domain
              /
              (.*)
            }x) {
    print "\$1=$1, \$2=$2, \$3=$3\n";
}

Perl中的运算符与其它类C语言差不多,Perl中也有类似shell中的gt, lt, eq等运算符,不过与shell不一样的是,gt/lt/eq等是用于字符串比较的,而>/</=等是用于数字比较的,例如:

if (10 gt 2) {
    print "'10' greater than '2'\n";
} else {
    print "'10' less than '2'\n";
}

if (10 > 2) {
    print "10 greater than 2\n";
} else {
    print "10 less than 2\n";
}

输出结果是:

'10' less than '2'
10 greater than 2

Perl中还有一个比较特殊的运算符:spaceship operator,即<=>,它是用于比较两个数字的:

  • 如果左边的比右边大,则返回1;
  • 如果左边的和右边相等,则返回0;
  • 如果左边的比右边小,则返回-1;

这个运算符是不是比较形象,相当于Python中的cmp函数。

与或的运算符分别为||或者or,&&或者and。其中符号形式的优先级比字母形式的优先级高,所以比较下下面几行代码的区别:

open FILE, 'not_exist.pl' or die "$0: open file failed: $!";  # 正常执行
open FILE, 'not_exist.pl' || die "$0: open file failed: $!";  # 语义错误
open(FILE, 'not_exist.pl') || die "$0: open file failed: $!"; # 正常执行

在第二种方法中,'not_exist.pl' || die "$0: open file failed: $!"先执行,这个表达式的结果作为open函数的第二个参数,因为'not_exist.pl'为true,所以也不会执行||后面的表达式。

除了or、||的区别外,上面还用到了$0和$!两个特殊的变量,$0表示脚本的名称,而$!记录最近一次错误信息。在Shell中$!返回的是子进程的进程pid。

除了Perl语言的学习外,书中的一些观点我也是比较认可的,例如学习新语言的时候多犯些错误,可以加深对不同错误信息的理解。