Perl快速入门

这里是我学习Perl的笔记,需要你了解Linux Bash脚本语言,Python语言的语法和常用的数据结构。

如果你没有这些知识,也可以点进来看看我推荐的Perl书籍(在开头就有)。

Perl 的介绍

什么是Perl

Perl是一门广泛运用在Linux系统上的脚本语言。在Python出现之前Linux系统上大家基本上都使用Perl进行出现编写。比起其他语言,Perl的优势在于文本和表格的处理。Perl也一度在网络编程中占据重要优势,以前的CGI程序大部分都是Perl写的,以前的运维也必须学习Perl。

虽然现在很多的语言抢走了Perl的工作,但是学习Perl无疑可以让我们在Linux系统上工作的更好。Perl和C语言一样,是大多数Linux系统自带的语言。

Perl适合做什么?

Perl非常适合编写一次性程序,如果你现在有个问题,但是又不想太花时间使用C/C++的话,Perl是个好选择(我甚至觉得在这方面Perl比Python的表现还要好)。

Perl也很适合编写快速的原型和文字处理程序。文字处理是Perl的强项。

其实除了文字处理,Perl也可以干很多事情。和Python一样,Perl有被称为CPAN的包社区,里面有处理各种情况的包。

Perl不适合做什么

首先和Python一样,由于是脚本语言,Perl没办法生成二进制文件,要想发布程序,必须连同源码发布出去。

其次,大程序和工程可能并不适合使用Perl来编写,因为Perl的语法问题。。。

使用的Perl版本

这里使用的是Perl5版本。6版本听说不受待见,而且据说改的像一门新语言,所以从5版本开始学习。

Perl的语法特点

Perl是很多语言语法的混合体,你可以在里面看到Bash脚本,C/C++,Python的影子。所以如果你语言见得比较多,学习Perl会非常容易。

学习Perl的资料

零基础入门推荐Perl三部曲,即骆驼书:

  • 《Learning Perl》初级入门(中文《Perl语言入门》)
  • 《Immediate Perl》进阶
  • 《Mastering Perl》高级Perl

实践方面可以看《Perl最佳实践》

口袋书的话推荐《Perl 5 Pocket Reference》

Unix网络方面的话可以看《Perl网络编程》。如果你觉得使用C/C++进行Unix网络编程太麻烦的话(比如直接看《Unix网络编程》系列),那么我十分推荐看这本。因为Perl的网络编程API几乎和Unix的API一模一样,但是Perl使用起来却很方便,这有利于你免去复杂的结构体配置,快速熟悉Unix网络方面的API和编程技巧,等学完之后再去看《Unix网络编程》会更加得心应手。

另外,十分推荐在Linux系统上学习Perl。

Perl自带的帮助文档

如果你安装了Perl,那么你应该也安装过了perldoc工具。这个工具的功能和Linux下的man差不多,也就是perl语言的帮助文档。有什么不会的可以查一查。

开始Perl的学习

HelloWorld

1
2
#!/usr/local/bin/perl
print "Hello World!\n";

和Python2很像对吧,第一行和Bash脚本类似,指定Perl的解释器在哪里。

第二行的print是函数。你也可以加上括号:print("Hello World!\n"),但是如果解释器可以明确无歧义的判断参数类型话也可以不加括号。

我要怎么运行?

很简单,如果你写了第一行,那么直接将这个源代码文件变为可执行的,然后运行即可。

如果没有第一行,你就只能通过Perl解释器来运行,和Python一样:

1
perl helloworld.pl

Perl的文件可以不加后缀名,但是如果你要加的话,可以加pl,这也是大多数编辑器默认的Perl文件后缀。

分号是必须的吗?

对,是必须的。

区别大小写吗

区别

Perl的注释

Perl只有行注释,使用#

变量

创建变量

和Python一样,变量拿来就可以用,不需要声明,但是需要在前面加上$

1
$a = 32;	#显然是Bash的语法习惯

这里要注意的是:当你在块里面(即花括号里面)和函数里面,甚至循环,判断结构中直接使用变量的话,这些遍历全部都是全局变量!

要想使用局部变量,必须使用my关键字:

1
2
3
4
if(expression){
	my $a = 20;	#局部变量
	$b = 30; #全局变量,出了if语句还能用
}

当然,变量仍然遵循就近原则:如果在内部声明了局部变量,那么全局变量会被隐藏。

使用warning(见下)会帮助你在局部块中新创建了全局变量时给你一个警告⚠️。

或者如果你想让Perl强制你使用my的话,可以使用严格模式:

1
use strict;

这样即使在全局环境中你都得使用my关键字创建变量。

基本数据类型

标量数据

标量数据指数字,字符串,undef

数字

无论是小数还是整数,Perl一律视为双精度浮点数

1
2
3
4
5
6
7
$a = 5.21;
$b = 3.2e10; #3.2*10^10
$c = 61_289_9129_01	#允许插入下划线,将若干位分开,这样看的清楚
$oct = 0377;	#8进制
$hex = 0xff; #16进制
$binary = 0b11101101; #2进制
$d = 0xff_ee_3a;	#同样可以使用下划线
操作

四则和位运算是和C/C++一样的运算符,但是由于所有数字都视为浮点数,所以不存在整数除整数还是整数的情况。

字符串

使用双引号括起来的字符串内部可以转译。

使用单引号括起来的不能转译。

字符串可以内插,语法和Bash脚本一样:

1
2
3
4
$str1 = "Hello";
$str2 = "World";
$c = 32
$expression = "${str1} ${str2}! ${c}" #"Hello World 32"

这里和Bash一样,${str1}中的花括号在没有歧义的时候也可以不加。

支持UTF8

Perl原生支持Unicode。如果想要支持UTF8编码,需要在代码开头(#!/usr/local/bin/perl下面)加上

1
use utf8;
操作

最主要的操作是连接操作:

1
2
3
4
$str1 = "Hello";
$str2 = "World";
$expression = $str1 . " " . $str2;
$str .= " ${str2}";	#甚至有专门的.=符号(意义和+=意义)

使用.连接

也可以进行重复:

1
2
3
$str1 = 'a';
$multipul = str1 x 4;	#'aaaa'
$multipul = str x 4.6; #'aaaa' 小数会自动舍弃

使用x进行重复。

数字和字符串的相互转换

在Perl中,数字和字符串被视为一个东西。什么时候将值视为字符串,什么时候视为数字,取决于你使用的操作符。

比如3+2显然就是数字之间的操作,直接得到5。然而'3'+2这种情况也可以得到5。因为Perl会在需要数字的时候将字符串自动转换为整数(或者换句话说,因为+操作只能对数字有用,所以字符串会变为数字),而针对字符串的操作则会将数字变为字符串,如'3' . 2得到'32'。当转换字符串到数字时,Perl会从字符串开头的第一个字符开始看,直到遇到字符停止(小数点不算),也就是说:

1
2
3
4
$str1 = "0.322" #转化为0
$str1 = "3.221" #转化为3.221
$str1 = "321ab45" #转化为321
$str1 = "abe123" #转化为0

所以3 x 2将会是'33',因为3和2转换成字符串了。而3.2 x 2会得到3.23.2

而且八进制和十六进制的前置0和0x技巧只对直接量有效,不能用于字符串转换:

1
'0377'	#转化为377

想要知道数字和字符串对应的操作符?请参阅perlop文档(使用perldoc perlop即可)。

害怕转换?请使用警告

如果你担心Perl的这种自动转换会给程序带来风险,你可以使用use warnings;来让Perl在含糊不清的地方报一个警告。这样你就可以针对警告来修改你的代码。但是注意,抛出警告并不意味着Perl会自动帮你修改程序,它该怎么转换还是怎么转换。

也可以使用use diagnostics来抛出更详细的警告。

数字和字符串的逻辑运算符

由于Perl会根据运算符来视值为数字还是字符串,所以对数字和字符串有不同的比较操作符。

数值的话使用常规的方法,字符串则是借鉴了Bash脚本的语法:

等于 不等于 小于 大于 小于等于 大于等于
数值 == != < > <= >=
字符串 eq ne lt gt le ge
undef

undef即未定义,和C的NULL,Python的None差不多。undef会被视为假。

对于undef的操作,主要是判断一个变量或表达式是否是undef。可以使用函数defined()来判断。

列表(数组)和哈希

这两个结构的元素都是异质的,但是哈希的键必须是字符串。

列表(也就是数组啦)

Perl的列表是很宽泛的:

1
2
3
$arr[0] = "value1";
$arr[2] = "value2";
$arr[3] = "value3";

你可以像上面这样直接将普通变量当做数组,只需要在后面加上索引即可,而且索引是可以跳的。如果这个时候你使用$arr[1]会得到undef。

任何可以求值得到数字的表达式都可以作为下标,如果不是整数,会自动舍去小数。

使用$#arr_name来得到数组中最后一个元素的索引值。或者你可以像Python一样直接使用负数来得到最后一个元素。

列表直接量和数组引用

除了像上面那样一个一个费劲地放入元素,还可以使用列表直接量:

1
@arr = (1, 2, 3, 4)

这里括号包裹的一堆东西就是列表。

如果想要使用列表这个整体(也就是引用列表),那么就必须使用@符号放在前面。

使用范围操作符来生成数组

可以使用a..b的形式来生成[a, b]区间的所有整数:

1
@arr = 1..3;	#arr为 (1, 2, 3)
列表之间的赋值

列表和列表之间可以赋值,但是你能否想到这种方式:

1
($value1, $value2, $value3) = qw{a b c};

这里利用了列表赋值的特性,这个语句等价于:

1
2
3
$value1 = "a";
$value2 = "b";
$value3 = "c";

如果你有不想要的元素,可以使用变量$_代替:

1
($value1, $value2, $_, $value3) = @arr
列表的输出

可以通过print函数直接输出列表,但是列表的元素之间不会有空格分开,连在一起很难看。

解决办法是将列表内插到字符串中,这样在输出的时候就可以自动增加空格:

1
print "@arr";
简便的纯字符串列表创造方法

使用qw可以快速创建所有元素都是字符串的列表:

1
2
@a = qw(Berk Slash GQ)
#得到列表("Berk", "Slash", "GQ")

使用qw的好处是在于不用在字符串外面加上双引号了。

qw后面的括号可以换成其他的,比如方括号或者花括号。如果不是成对的字符就直接使用自己即可:

1
2
@a = qw\asd asd asd\;
@b = qw{wqm ewq olk};
将列表当做栈

可以使用poppush函数将列表当做栈,栈顶是列表的最后一位:

1
2
pop @array;
push @array;

也可以使用shiftunshift,这个时候栈顶是列表的第一位。

在列表中间增加移动元素

使用splice可以做到

1
splice @array, pos[, list_len][, @subt_arr];

第一个参数是要控制的列表,第二个参数是从哪个位置开始。

如果只给出上面两个参数,会返回这个列表的开头到pos的子列表。

第三个参数指出要返回的长度(注意并不是结束位置),第四个参数是在拿出元素之后要加的元素,以列表存储。

也就是说:

1
2
3
@arr = qw{A B C};
@remove = splice @arr, 1, 2, qw{D E};
#remove为移出的元素,即(B,C),然后arr为(A, B, D, E)
反转数组

使用reverse,这个在很多语言里面都有了,不讲了。

排序

使用sort函数,排序的时候会将数字转换为字符串,然后按照ASCII码来对字符串排序。默认升序排序。

哈希

哈希和我们常说的哈希表不太一样。这里的哈希更像是Python中的字典,C++中的map。由于Perl中的哈希的内部存储是通过哈希表存储的,所以被称为hash。

哈希的键必须是字符串,值可以是任意数据类型。

访问hash

访问哈希和数组差不多,但是要使用花括号:

1
$hash{'key1'} = "value1";

同时这也是给哈希赋值的一种方式。

和列表一样,你可以手动地给哈希的每一个元素像上面一样赋值。

如果访问了不存在的键,会返回undef。

访问整个hash

哈希的特有前缀是%:

1
%hash = %hash2; # hash之间的赋值
快速创建hash

列表可以转换为哈希,我们可以利用这种特性:

1
2
3
4
%hash = ("key1", "value1", "key2", "value2");
# 相当于
%hash{"key1"} = "value1";
%hash{"key2"} = "value2";

同样地,哈希也可以转换为列表:

1
2
@list = %hash;
#list 就是 ("key1", "value1", "key2", "value2")

但是不能保证列表中元素的顺序(但是相关联的键一定在其值前面)

或者你可以使用更加清晰的赋值方式:胖箭头:

1
2
3
4
5
%hash = (
	key1 => 'value1',
	key2 => 'value2',
	key3 => 'value3'
);

其实胖箭头=>在解释的时候会被解释为逗号,并且会将箭头左边的量自动视为字符串,所以这里的语法其实和上面的语法是一样的,而且你不需要在键上加上引号。当然在列表中你也可以使用:

1
@list = (2 => 3 => 4 => 5);

不理解的话将胖箭头视为逗号即可。

那么这里有人会问了:这里的2,3,4,5是字符串吗?其实这个问题没有意义,因为字符串和数字是一个东西,是不是字符串取决于你怎么使用它。

hash函数

这里有一些常用的hash函数:

  • keys %hash得到hash的键所组成的列表
  • values %hash得到hash的值组成的列表
  • reverse %hash将hash的值变为键,键变为值
  • exists %hash, key检查hash内是否有键key
  • delete %hash, key 删除hash内的key及对应的value。没有key的话不做操作。

和列表一样,你也可以使用each来在迭代中同时得到键和值:

1
2
3
while(my ($key, $value) = each %hash){
	#...
}

环境变量%ENV

%ENV哈希是系统自带的哈希,表示当前的环境变量。

命令行参数变量@ARGV

也就是我们在C/C++中的int main(int argc, char** argv)和Java中的void main(String[] argv)中的argv了。存储命令行给出的参数的列表。

$_变量

Perl中有个很奇特的变量,$_,当你的函数缺少参数,或者循环中缺少循环体的时候,这个变量就会自动补上。比如:

1
2
3
4
@arr = qw{a b c d}
foreach (@arr){
	print;
}

这里并没有指定循环遍历和print函数的参数,那么$_变量就会自动补上。所以上面的代码等价于:

1
2
3
4
@arr = qw{a b c d}
foreach $_ (@arr){
	print $_;
}

固有前缀

Perl中有一个令人迷惑的行为:列表和普通变量的名字可以是一样的!不仅如此,列表,变量,函数,字典等的名字都可以是一样的!那么如何区分它们呢?这就要使用前缀。每一个数据类型都有自己独有的前缀,比如变量就是$,列表就是@,然后字典是%,函数则是&。那么$a就是a变量,而@a就是a列表了。

引用

perl里的引用和C++的引用差不多,本质上就是C语言指针。

使用\来表示引用:

1
2
@arr = qw(a b c d);
$ref_arr = \@arr;

这个时候$ref_arr就指向列表arr了。

引用其他的也是一样的:

1
2
$ref_scalae = \$scala;
$ref_hash = \%hash;

解引用的话只要在引用前面加上固有前缀就可以了:

1
@$ref_arr;	#相当于直接使用@arr;

对于哈希和列表,也可以在不全部解引用的情况下使用内部元素:

1
2
$ref_list->[3];	#使用->取出元素,列表用下标
$ref_hash->{key1};	#哈希用键取出

控制结构

条件判断

If语句

if语句的格式和C差不多:

1
2
3
4
if(expression){
}elsif(expression){
}else{
}

需要注意的有两点:

  • 表示_else if_的语句是elsif
  • 不能像C语言一样,如果只有一条语句的话不加花括号。Perl里面条循环语句,条件语句都必须加花括号。

表示假的东西有:undef,0和空的东西(空字符串,以及空列表,空哈希等)。

这里有一点需要注意:字符串'0'和"0"由于会事先被转换为数字0,所以字符串'0'和"0"也是假!

given-when结构

只能在5.10版本中及以后使用,所以你需要事先指定perl版本:

1
use 5.010;

功能和C的switch差不多,语法如下:

1
2
3
4
5
given(variable){
	when(condition1){ expression1 }
	when(condition2){ expression2 }
	default { default-expression }
}

given会将variable一个一个和condition匹配,如果成功了就会执行后面的代码。

和switch一样,如果你最后不加break,那么会导致继续向下匹配。

循环语句

while循环和unless循环

while循环是当条件为真的时候循环,而unless是当条件为假的时候循环:

1
2
3
4
5
6
7
while(expression){

}

unless(expression){

}

for语句和foreach语句

for语句和C的一样,只不过在变量声明的时候需要遵循Perl的语法:

1
2
3
for(my $i=0;$i<10;$i++){
	print $i;
}

foreach语句必须在5.010版本下才能使用。

foreach可以让你像Python一样遍历可遍历结构,比如遍历列表:

1
2
foreach my $ele (@list){
}

也可以使用each操作符来遍历列表,可以同时得到列表的下标和元素:

1
2
while(my($index, $value) = each @arr){
}

表达式修饰符

Perl有一个我很喜欢的功能,就是可以在表达式后面加一个控制行为的修饰符:

1
print "$n < 0" if $n<0;

像这样,这样的话只有$n小于0的时候前面的语句才会执行。这种方法很简洁易懂。

你也可以在后面加上or:

1
chdir '../awe' or print $!;

这里如果chdir返回了表示假的值的话,会执行后面的print语句。这种语句广泛用于在函数出错的时候输出出错信息。

die和warn关键字

顾名思义,die关键字会让程序die,也就是让程序停止运行。相当于C/C++中的throw和assert(false)。die后面可以接一个字符串来让程序执行到die的时候输出这个字符串。也就是说在人工停止程序的时候给出一些信息:

1
die "I'm died";

配合上前面说的控制修饰符,我们可以这样写:

1
chdir "../era" or die $!;

如果chdir函数出错,那么停止程序并输出出错信息。

warn的用法和die一样,只不过不停止程序,而是给出一个警告。

子程序(函数)

定义子程序

1
2
3
sub function_name{
	#function body
}

采用sub关键字就可以定义。需要注意的是函数是没有参数列表和返回值类型的。

参数在哪里?

Perl使用@_数组来存储参数。第一个参数存储在$_[0],第二个存储在$_[1],以此类推。

这就导致一个情况:你可以给一个函数任意多个参数。参数的不确定性进而引起一个IDE方面的特性:由于没有办法通过函数签名来知道函数有多少个参数,进而也就没办法在自动补全的时候告诉你参数的类型和个数。但有些IDE可以通过分析Perl文档来提示参数,但这也意味着你需要给你的函数写文档。。。

还有一个注意的地方:@_符号是函数内的私有变量,所以出了函数也就没办法使用,除非你自己定义了全局的@_

有了这种得到参数的方法,变长的参数想必也是很容易实现了。

如何返回返回值?

你可以像C/C++等语言一样使用return,但是Perl有一个更懒的方法:函数总会将其最后一行表达式的值返回(如果没有return的话)。这样,一来,不管你有没有return,函数总能返回值,只不过是返回的值有没有意义而已。

如果最后一行是函数调用,那么调用的函数的返回值将会被返回。

函数调用

像C/C++一样调用:

1
print("param");

如果参数的类型不会因为上下文而导致歧义的话,也可以省略括号。

由于函数和变量可以重名,所以在调用无参函数的时候,你要么加上一对括号,要么使用&符号前缀,以和普通变量区别:

1
2
max();
&max;

简单来说,如果Perl可以判断出你调用的函数一定是函数,那么就可以省略&符号。

这里还有一个很重要的情况:用户自定义函数可以和系统自带函数重名!。在这个时候,如果你想要调用系统函数的话,必须在调用函数的前面加上&,不然默认调用的是用户自定义函数。

持久性私有变量

也就是C/C++中的静态变量,Perl里面使用state关键字声明:

1
state $n = 2;

当然只能在函数内部使用。

函数出错了?看一下$!变量吧

如果函数的返回值表示函数出错了,你可以输出$!变量来看看函数的错误信息(如果函数写了错误信息的话)。

后调用的函数会重新将错误信息放入$!中(如果有错误的话),你也可以自己在函数出错的时候将信息写入$!中(但是我们默认不在函数成功的时候将成功信息放入$!中)。

正则表达式

Perl最强大的功能莫过于正则表达式了。虽然Python和C++,Java都带有正则表达式,但是Perl的正则使用方法是他们中最简单的。这也使Perl特别擅长对文字的处理。

如果你不会正则表达式的话,我推荐你去看看《精通正则表达式》(Mastering Regular Expression)。说实话,学Perl不用正则的话,很大程度体会不到Perl的方便。

如果你使用过vim,那么Perl使用正则的方式你会感到很熟悉。

使用正则表达式

和vim一样,使用/将正则括起来,需要转译的字符用\转译:

1
2
3
if(/*abc+/){
	# some operators
}

如果匹配成功,正则会返回真,否则返回假。像上面一样,如果不指定要匹配的字符,默认使用$_里的字符匹配。

其实这是m//的简写。和qw语句一样,你也可以使用m{abc}或者m!abc!、当你不想加m的时候可以直接写//。也就是说,上面的语句和

1
if(m{*abc+}){}

一个道理。

如果想要指定匹配的字符串,需要使用=~符号:

1
if("uiqjabcc0o" =~ /*abc+/){}

模式分组

模式分组是正则里面的一个功能。简单来说就是你可以在正则表达式中使用已经匹配到的内容,比如:

1
/(abc)\1/

这里圆括号表示一个分组,这里的分组会匹配到abc。然后使用\n的方式引用第n个分组(分组从1开始),所以这里的正则表达式相当于:

1
/(abc)abc/

当然你也可以:

1
/(.)\1/

这样会匹配两个连在一起的一模一样的字符,比如abba中的bb。

至于每个括号的分组编号是什么,很简单,从左往右看,第一个左括号代表的分组编号就是1,第二个左括号就是2,以此类推。

模式匹配修饰符

这部分知识属于正则知识,不再赘述,只是说一下Perl中常用的修饰符:

修饰符加载最右边:

1
/ab/[修饰符1][修饰符2][...]
  • i:大小写无关匹配
  • s:匹配任意字符(大部分情况下.号无法匹配换行符,使用这个来让.可以匹配换行符
  • x:加入空白符,加上x后可以在模式里面加入任意空白符(空格,制表符和换行回车符),所以原来的空白符Perl会直接忽略

模式中的内插

没错,正则中甚至可以内插变量:

1
/asc$var1/

当有歧义的时候,可以使用括号将变量单独括起来。

捕获变量

当你使用括号扩起一组正则的时候,Perl会匹配这个正则,并且留下来匹配的内容在$n变量中,以便于你得到匹配的结果:

1
/(*a)(.+?b)asd(cca)/

这个时候有三个组*a,.+?b,cca。当你匹配的时候,匹配到的值会分别存入三个捕获变量$1,$2, $3中来方便你获得。

捕获变量的值会一直存在,除非你手动改变它或有另一个正则匹配成功。注意,失败的匹配不会改变捕获变量,所以你应当只在匹配成功的时候使用捕获变量。

不捕获模式

如果你不想让每一个括号都影响捕获变量呢?可以使用(?:pattern)的方法,在左括号右边紧接上?:即可避免这个括号影响捕获变量。

命名捕获

如果你觉得$n这种捕获变量的名字太难记了,对又对不上号,那么你可以给捕获组命名,匹配成功后会自动放入哈希%+中:

1
/(?<name1>acc*)acs/

使用(?<var_name>pattern)的方式来指定名称,这样,第一个括号就不会影响到$1,而是放入$+{name1}中。

自动捕获变量

有三个系统自带的捕获变量:

  • $&:匹配的部分会被存入
  • `$``匹配区段之前的部分会被存入
  • $':区段之后的部分会被存入

只要你在程序中使用过一次上述变量,Perl机会在所有正则中将对应部分存入这些变量。这将导致正则的效率变慢。所以这些变量最好不要使用。

替换

正则不仅能查找,也能替换。和vim一样,使用s///的方式替换:

1
s/abc/bca/	#将abc替换为bca

和vim一样,在最后加上g表示全局替换:

1
s/abc/cba/g #这样就不仅仅替换找到的第一个字符串,而是所有找到的字符串都会替换

输入和输出

标准输入和输出

<STDIN>,<STDOUT>,<STDERR>就是我们熟悉的标准输入,标准输出和标准错误流。在Unix中,你需要使用open函数打开,然后使用write函数写,read函数读。Perl也差不多,只不过不用打开而已:

1
2
3
while(<STDIN>){
	print $_;
}

上面的程序将用户的所有输入原样打在屏幕上。

print函数其实就是将ASCII写到<STDOUT>中。你也可以使用printf函数来格式化输出(和C语言的一样用)。

这个时候有人会问了:那我在print函数里面可以内插数组,那在printf里面怎么表示数组呢?很简单,假如@items是一个有10个元素的数组,那么你需要:

1
printf "the items are :" . ("%s " x @items), @items;

这里首先使用x将%s重复10次(在标量上下文中,直接引用列表将会得到列表的大小),然后和格式化字符串连接,最后放入items列表当做参数即可。

钻石操作符

<>符号被称为钻石操作符,可以将给入的命令行参数所指的文件内容读入。也就是说:

1
2
3
4
5
while(<>){
	print $_;
}

#然后执行 ./diamond.pl file1.cpp file2.pl file3.java

会输出file1.cpp,file2.pl,file3.java的内容。也就是说钻石操作符首先会从命令行参数中取出第一个参数,然后打开这个参数指向的文件,将文件的内容返回(这里直接返回到$_中)。

在不同的上下文中其返回的结果也是不一样的,:

1
@arr = <>;

在列表上下文中会返回所有文件的内容,以行分割。

文件句柄

和其他语言一样,Perl有文件句柄。有6个文件句柄是Perl保留的:STDIN, STDOUT, STDERR, DATA, ARGV, ARGVOUT

在Perl程序打开的时候 ,print和say会和STDOUT关联,而warn和die会和STDERR关联。

Perl程序员一般将文件 句柄的名字全大写,以和普通变量区别。

文件句柄没有任何固定前缀,也就是说长这样:HANDLE

打开文件句柄(打开文件)

使用open函数打开:

1
open HANDLE, $filename;

oepn不会返回文件句柄,而是将文件句柄赋值给第一个参数。

第二个参数是文件名称,如果你只是给入文件名称,那就是读(已存在)的文件,也就是C语言中的r操作。

如果在文件名前加上>表示以写的方式打开文件(C语言中的w)。

如果在文件名前加上>>表示以追加方式打开(C语言中的a):

1
2
3
4
5
open FHANDL, "<file.txt";	#读方式打开,<可以忽略
open FHANDL, ">file.txt";	#截断文件,以写方式打开
open FHANDL, ">>file.txt";	#追加方式打开
open FHANDL, "+>file.txt";	#截断文件,然后打开用于读写
open FHANDL, "<+file.txt";	#用于读写,不截断

在5.6版本之后,你也可以用更清楚的方式打开:

1
2
open FHANDL, ">", "file.txt";	#可以将打开方式和文件名分开。
open FHANDL, ">:encoding(UTF-8)", "file.txt"; #以写方式,UTF8编码打开。

那么如何用二进制方式打开呢?这需要使用binmode关键字:

1
binmode FHANDL;

也就是说,你先用open打开文件,然后再用binmode将其变为二进制读写。

如果你比较熟悉Unix的openAPI的话,你也可以使用sysopen,这个函数的用法和Unix的open函数完全一样:

1
sysopen FILE_HANLE, $filename, $mode;

复制文件句柄

你也可以使用open来复制文件句柄:

1
2
open FHANDLE2, ">&STDOUT";	#使用>&来复制用于写的文件句柄
open FHANDLE3, "<&STDIN";	#使用<&来复制用于读的文件句柄

关闭文件句柄

使用close函数关闭文件句柄。

读取文件

读取的话分为两种:一行一行读取和按字符读取。

一行一行读取的话和从STDIN读取数据一样:

1
2
3
while(<FHANDL>){
	print $_;
}

上面的语句会将文件里的每一行读到$_中并输出

如果想要按字读的话,那么就使用readsysread函数:

1
2
$read_byte = read FILE_HANDLE, $buffer, $length [, $offset];
$read_byte = sysread FILE_HANDLE, $buffer, $length [, $offset];

read和sysread的使用方法一样,将从开头偏移$offset,长度$length的内容读入$buffer中。而且注意这里的第一个参数后是由逗号的。

区别在于read是阻塞函数,除非读到文件末尾,否则会一直读length长度。而sysread是非阻塞的。这一点在对socket编程的时候由很重要的区别。

写文件

同样也是分一行一行写和写一堆。

一行一行的话直接用print,printf和say均可。具体做法是将文件句柄放在第一个参数的位置:

1
print FHANDL "hello world";

注意中间是没有逗号的。

如果要一次写一堆的话,使用函syswrite就可以了:

1
$write_bytes = syswrite FHANDLE, $data [, $length][, $offset];

这里注意,write函数和syswrite的功能是不一样的,不要混淆。syswrite是非阻塞的。

select函数

select函数有很多种情况,其中一个情况是只有一个参数:

1
$previous = select HANDLE;

这会改变print指向的句柄。也就是说print原本是指向STDOUT的,你可以将其改变为你的文件句柄,然后直接输出,这样内容就会到文件句柄中。

这个函数返回上一个句柄。

检测文件尾

使用eof(FHANDLE)函数来检测文件末尾,如果下一次读取的时候到达了文件尾,那么会返回真。

使用面向对象的方法操作IO

Perl有一些模块可以通过面向对象的方法使用IO,常用的是IO::Handle(对文件句柄进行面向对象)和IO::File(对文件进行面向对象)。IO::FileIO::Handle用途更多。

要使用模块,首先的使用use关键字:

1
use IO::File;

对于IO::File,我们给出一个例子:

1
2
3
4
5
$file = IO::File->new("test.txt");	#新建一个文件对象
while($file->getline()){	#每次循环获得文件的一行
	print $_;
}
$file->close();	#关闭文件

具体的用法请见perl文档。

文件,目录操作

Perl的文件和目录操作基本上都是和Unix系统编程中同名的API函数,只不过需要在Perl的语法下使用而已。

文件测试

Perl可以得到文件的一些信息,以及测试文件的一些属性。

测试的方法和Bash脚本差不多:使用-X(X是一个字符)来表示测试。比如-e表示测试是否存在:

1
if -e $filename;	#如果文件存在返回真

所有的测试符可以见这里测试符的最下面。

在5.10版本前,如果想要测试多个文件符,必须分开操作。在5.10之后可以将其放在一起:

1
if -x -e $filename;

stat和stat函数

stat是Unix系统编程的一个函数,perl里也可以使用这个函数。

这个函数的功能是返回文件的一些信息,包括uid,符号链接的数量等。

stat函数的参数可以是文件句柄,或者是文件名称。其返回一个含13个元素的列表(如果失败返回空列表):

1
文件所在设备的设备编号文件所在的inode编号文件的权限位几何文件或目录的硬链接数uidgid设备标识符文件大小[以byte计]最后使用时间最后修改时间inode编号改变时间最适IO大小分配的系统特定块的实际数目 = stat $filename;

localtime函数

localtime也是和Unix函数一样的。其参数为时间戳,返回值视上下文:

  • 标量上下文:返回一个可读的表示时间的字符串
  • 列表上下文:返回($sec, $min, $hour, $day, $mon, $year, $wday, $yday, $isdst)列表。其中$mon是0-11的月份值,$year是从1900年开始的年份,所以 你得加上1900年才能得到现在的年份。$wday是从0-6的值,代表从周日到周六,$yday表示目前是今年的第几天,从0-364.

目录操作

改变路径

使用chdir即可改变路径:

1
chdir "../";	#到上一层路径

文件名通配符

像Bash一样,可以使用*来通配文件:

1
2
3
4
5
6
#先写下如下程序files.pl:
foreach $filename (@ARGV){
	print $filename;
}

#然后执行: ./files.pl *.cpp

执行之后会打印出目录下的所有cpp文件。也就是说bash会帮助你展开通配符。

在Perl程序中也可以使用通配符,使用glob函数即可:

1
2
glob "*.cpp";	#会得到当前目录中所有的cpp文件
glob "*.cpp *.pl";	#匹配多个

glob的作用和Bash完全一样,所以不会列出开头为.的文件。

文件和目录的操作

这里列举一些常用的操作:

  • 删除文件unlink:使用unlink删除,参数可以是以逗号分隔的文件名,或者直接是列表。返回删除成功的文件数目。

  • 重命名文件rename:第一个参数是原文件名,第二个参数是新文件名

  • 创建和删除目录mkdir/rmdir

  • 修改权限chmod

  • 修改隶属关系chown

  • 修改时间戳utime

这些函数的用法可以在perldoc中找到。

进程,管道和信号

perl也可以操作进程,而且其API函数和Unix的API几乎一致,很容易上手。

进程

进程创建

和Unix一样,使用fork函数创建。

fork函数会返回值,如果返回的是0,代表现在所在的进程是子进程(用fork创建的进程),而如果是大于0的,代表是刚刚创建的子进程的ID,也就是说现在仍然处于父进程:

1
2
3
4
5
6
$pid = fork;
if($pid==0){
	print("我现在是子进程哦");
}else{
	print("我还是父进程,我的pid是${$},我的儿子是${pid}");
}

发生错误返回undef

$$变量里面存储着当前进程的pid,这个变量时只读不可写的。

你也可以用getgpid([$pid])来得到指定进程的进程组id,如果没有参数,返回当前进程的进程组id。

进程运行程序

当你使用fork之后,子进程和父进程还是使用一样的资源和代码。只有当你改变了子进程的代码或者改变了子进程的资源之后,子进程才会在内存中开辟一片内存。这种技术叫做写时复制(copy on write)

然而,和父进程运行一样的代码有什么意思呢?所以我们在创建子进程之后基本上立刻就要使用exec()函数来给子进程新的代码执行。exec的功能是以新的命令替换当前进程。所以exec从不返回,因为替换之后原来的代码已经没了:

1
2
3
if(fork()==0){
	exec("cat", "test.txt");	#相当于这个进程现在要执行cat程序,并且cat程序的参数是test.txt
}

所以现在有两种情况让子进程运行和父进程不一样的代码:

  • 使用if语句判断fork的返回值,如果返回值为0代表是子进程,执行子进程专有代码
  • 使用exec替换子进程原本的程序。

管道

就是Unix下的管道,可以通过Perl来打开。

使用open打开管道

使用open可以打开管道:

1
2
open FHANDL, "|filename";	#打开写管道
open FHANDL, "filename|";	#打开读管道

在文件名前面加|表示打开读管道,在后面加|表示打开写管道。

但是你不能同时打开读写管道:open FHANDL, "|filename|"是错误的。

直接管道

可以像Bash命令一样直接使用\``来打开管道:

1
$context = `ls`;

这样context变量会得到ls命令的输出结果。

pipe函数

这个是Unix下的标准打开管道的函数:

1
$error = pipe RHANDLE, WHANDL;

其两个参数都是返回值参数,第一个参数为打开了的写管道,第二个参数为打开了的读管道。

一般来说管道是让两个程序通信的东西,所以在同一个程序里面打开管道没什么意义,除非你用的多进程程序。如果想要两个进程之间通过管道通信,一般是创建管道后一个进程关闭读/写管道,另一个进程关闭写/读管道,这样让一个进程向另一个进程传输数据。

虽然pipe返回两个管道,但是你不能保留两个管道同时进行读写操作。如果想要既读数据又写数据,必须再开一个管道,然后关闭其中的读/写管道。

管道的判断

使用-p来判断一个句柄是不是管道,用-S判断是不是一个套接字,用-t判断文件句柄是不是又终端打开。

信号

信号种类

信号是Unix系统传输给程序的信息。比如你每次程序死循环的时候,你会按下Ctrl-C来强制关闭程序对吧,这个时候其实是Unix发送了INT信号给程序,INT信号的默认操作是关闭程序,所以程序会被关闭。

但是你也可以自己使用代码接受信号,然后自定义处理信号的方法。POSIX定义了19个信号:INT,QUIT,HUP,KILL,ILL,SEGV,PIPE,ALRM,CHLD,STOP,TTIN,TTOU,TSTP,USR2,USR1,FPE,ABRT,COUNT,TERM.其中最常用的是:

  • INT:来自键盘的中断,默认终止程序
  • KILL,STOP:来自系统的中断,终止程序,不可捕获
  • CHLD:子进程终止
  • HUP:挂起
  • PIPE:写往没有读取者的管道
  • ALRM:来自闹钟的定时信号

其中KILL和STOP是不能够被捕获的,其一旦发出,程序必定被停止。

HUP用于挂起程序,其实很常用:在你用命令行转一个程序的时候,关闭了命令行之后程序并没有死亡,而是被挂起了(其实我以前一直以为是死亡了。。。)。

CHLD则是在程序的子进程终止的时候,其父进程会接收到这个信号。这个信号一般用来帮助我们解决僵死进程。

ALRM则是在使用alarm($second)函数下,经过second描述之后程序会接收到的信号。

截取信号

你可以在%SIG哈希中注册信号函数来截取信号:

1
2
3
%SIG{INT} = sub{
	print "你按下了CTRL-C";
};

这里截取INT信号,所以你在按下CTRL-C的时候不会终止程序,而是输出一行信息。

发送信号

使用kill()函数发送信号:

1
$count = kill($signal, @processes)

第一个参数是要发送的信号,以字符串表示,如'INT'。第二个参数是一组pid,表示你要发送信号的对象。返回能够被发送信号的进程数量。

使用信号处理程序的建议

由于信号可能在程序的任何时候到达,而一旦到达程序就会执行对应的信号处理程序。所以你的信号处理程序最好不要改动内存啊,做IO操作等花里胡哨操作,这样可能和程序原本代码冲突。最安全的方法是设置一个全局变量的状态,然后在主代码中检查这个状态来判断下一步做什么。

updatedupdated2023-06-182023-06-18