Perl的I/O操作符<>需要注意的一些问题
2015-03-27 17:36:28 阿炯

本站赞助商链接,请多关照。 Perl钻石操作符<>是在Perl中主要用作从文件句柄中读取数据,它的另一个用处是从指定文件夹返回所有匹配的文件名列表。和许多其他Perl语言元素一样,<>在不同的环境中可以表现出截然不同的特性,如果不加熟悉很容易就吃闷头亏。

文件结束判断应该用:defined($var=<$file_handle>)

<>读取文件内容时会在文件结束的时候(EOF)返回undef,其他的时候都会返回读取到的数据,通常是一行文本以及附加在文本后面的换行符\n。所以,通常情况下,直接使用<$file_handle>的返回值来判断是否读入了有效数据也是可以的。但是,当一个文本文件最后一行内容是0且没有回车符时,如果使用<$file_handle>的返回值来判断是否读入了有效数据便会出现问题。所以判断文件结束应当使用defined($var = <$file_handle>)而不应直接使用<>的返回值。

在特殊的循环结构中使用<>操作符时,Perl会自动添加defined判断,这使得我们利用这种“语法糖”写出简单轻便的代码,但也会在一定的时候造成一定的困惑。比如,下面的各行代码都是等价的。

while (defined($_ = <STDIN>)) { print; }
while ($_ = <STDIN>) { print; }
while (<STDIN>) { print; }
for (;<STDIN>;) { print; }
print while defined($_ = <STDIN>);
print while ($_ = <STDIN>);
print while <STDIN>;

但是,单纯的`defined($_=<STDIN>)`和`$_=<STDIN>`或`<STDIN>`并不等价。这可以通过在命令行运行`perl -MO=Deparse -e '代码行'`来验证。因此,如果确实需要在上述的循环结构中验证读入的数据是否是`0`,应该显示地使用下面的语法。

while(($_ = <STDIN>) ne '0'){ ... }
while(<STDIN>){ last unless $_; ... }

使用<>读取@ARGV中的文件

当<>第一次被求值时,Perl检查@ARGV数组,如果数组为空,则向其中添加一个'-'(代表了Unix下的标准输入:STDIN),然后将@ARGV当作一组文件名。在<>的后续读取中,Perl会逐个地打开这些文件,并依次地读取其中的内容。<>和<ARGV>是等价的,ARGV是Perl中的一个特殊的文件句柄,在<>的读取过程中,它会依次地指向实际被打开的文件。

需要注意的是,使用<>读取@ARGV中的文件内容时,Perl内部使用两个参数的open函数打开@ARGV中的各个文件,这使得<>的使用具有一定的不安全性,如果不能保证传入文件名的合法性,就不要轻易使用<>来操作。比如别的用户可能传入这样的文件名yes | rm -rf /usr |,这种时候,在使用<>之前需要对@ARGV中的文件名列表进行一定的验证。

此外,<>在操作@ARGV中的文件时不会关闭前一个文件,这使得在操作一系列的文件时,当前操作的行号$.将会累加,不能反映当前读取的行在当前文件中的正确行号。可以在前一个文件结束时手动关闭该文件来重置行号记录。

# 模拟 grep -ne 'pattern'
while (<>){
 print "$ARGV:$.:$_\n" if /$pattern/;
}
continue{
 close ARGV if eof;
}

标量上下文和列表上下文

<>在标量上下文中返回下一行数据,而在列表上下文中返回由文件中剩余的所有行组成的列表。有些时候,可以使用这种方法将文件数据全部读取到内存中减少磁盘IO;在读取大文件时,不慎在列表上下文中使用<>将会造成大量的内存消耗。

# 读取一行
my $line = <$file_handle>;

# 读取全部的行,然后将第一行赋值给$line
my ($line) = <$file_handle>;

# 一次读取一行然后输出
while (<$file_handle>){ print; }

# 读取文件中的全部内容行然后逐行输出
for (<$file_handle>){ print; }

行的分隔符$/

<>在标量上下文中返回文件中的下一行文本,而行的结束标志由$/指定。$/的默认值是\n,但需要注意的是,\n在Perl中不同于C中的\n,它并不一定是\x0A,在某些系统中,\n可能是其他的值,比如,在Mac OS中(不是Mac OS X),\n的值是\x0D。一般情况下,这种默认的断行策略是可用的,但处理UTF-16这种多字节编码的文件时,可能需要将$/的值进行一定的设置,比如\x0A\x00。一种常见的用法是将$/设置为undef,然后便可以将文件的剩余数据全部读入到一个变量中,为了不影响其他的文件读取操作,一般使用一个语句块来限制对$/的修改的作用范围。

{
 local $/;
 $contents = <$file_handle>;
}

readline和glob的功能区别

如果<...>中的既不是一个文件句柄,也不是一个包含文件句柄的名字、类型团或类型团引用的简单变量,那这个<...>将被解释为glob ...,而这种判断是在语法层面的,在编译时即被确定。因此<$var>始终解释为readline($var),但是类似于<$hask{$key}>的用法却被解释为glob($hash{$key}),即使$hash{$key}中存放的是一个文件句柄的引用($hask{$key}不是一个“简单变量”)。并且<$var >将被解释为glob("$var"),因为$var后面有个不起眼的空格。


Perl读取标准输入<STDIN>

<STDIN>表示从标准输入中读取内容,如果没有,则等待输入。<STDIN>读取到的结果中,如果没有意外,都会自带换行符。例如freeoa.plx文件内容:
$line=<STDIN>;
if($line eq "\n"){
    print "blank line\n";
}else{
    print "not blank: $line"
}

注意上面的else语句中,$line后面没有加换行符,因为<STDIN>自带换行符。

下面的命令,将等待输入和回车。如果直接回车,则if条件为真。
perl freeoa.plx

下面是和bash shell交互。
echo "hello" | perl freeoa.plx
echo -e "haha\nheihei" | perl freeoa.plx

注意上面第二条语句中,heihei会被忽略,因为上面的操作是标量上下文,在发现换行符的时候,结束输入的读取,所以看到haha后面的\n就结束了。

因为<STDIN>读取的是标准输入,所以如果要通过它读取文件内容,需要使用shell的重定向功能。例如,读取a.txt文件的内容到<STDIN>:
perl freeoa.plx <a.txt

另外<STDIN>在标量上下文中返回的是某一行,可以使用while来遍历多行,但在遍历时要书写准确:
# 错误遍历
$line = <STDIN>;
while(defined($line)){
    print $line;
}

# 正确遍历
while(defined($line = <STDIN>)){
    print $line;
}

第一种写法会无限循环输出读取的第一行(\n前面的行),因为$line被赋值为该第一行,且不再改变,由于defined()返回真,会使得while无限循环。

第二种写法每次读取一行,每读取一行做下读取的位置标记方便下次读取(也就是说<STDIN>是可迭代对象),直到读取完最后一行返回undef使得defined返回false,结束循环。

由于<STDIN>在读取到最后一行后会返回undef,所以可以简写上面的第二种方式:
# (建议写法)
while(<STDIN>){
    print $_;
}

# 或(但不建议使用此写法)
foreach(<STDIN>){
    print $_;
}

只是需要注意上面的两种简写方式,每次读取的行并不是直接赋值给$_,而是在每次迭代过程中赋值的。换句话说,上面的<STDIN>和$_没有直接关系,不像$line=<STDIN>这种赋值,直接在读取的时候就赋值给$line,也正因为不是直接赋值给变量,才能循环读取下去。

这里涉及到上下文和数组的概念可能要先行理解,上面的while和foreach有巨大的差别,while后面是标量上下文,foreach后面是列表上下文。这意味着while是每次从文件中读取一行,打上位置标记以便下次读取,然后进入循环体。而foreach因为操作目标是列表,它会一次性将文件中所有行读取到内存,然后当作列表进行遍历,所以性能非常差,例如400M的文件,也许需要1G的内存,因为perl会预估先分配足够多的内存以便后续不再因为分配内存的事而中断进程。上面的过程,使用下面的形式描述,就很容易理解了:
$line = <STDIN>; # 一次读一行,性能好
@lines = <STDIN>; # 一次读所有,性能差

<STDIN>会带有换行符,通常都会加上chomp()操作符去掉换行符。

读取文件输入<>

perl中使用两个尖括号符号表示读取来自文件的输入,例如从命令行中传递文件作为输入源。这个符号被称为”钻石操作符”。

例如,freeoa.plx程序内容如下:
while(defined($line = <>)){
    print $_;
}

或者简写的:
while(<>){
    print $_;
}

然后在perl程序的命令行中指定输入文件:
$ ./freeoa.plx freeoa.log

或者使用@ARGV数组指定输入文件:
@ARGV=qw(freeoa.log);

如果想要读取标准输入的数据,则在命令行中使用短横线-。
$ echo -e "haha\nheihei" | ./freeoa.plx -

一般来说,while循环中使用<STDIN>或<>读取输入后(也包括open关键字打开文件再读取行的情况),第一行就是去除行尾的换行符,所以大多数都采用如下通用格式:
while(<>){
    chomp;
    COMMANDS;
}

while(<STDIN>){
    chomp;
    COMMANDS;
}

上面的chomp没有参数,所以采用默认变量$_,也就是迭代中的每一行。

关于<>的机制,请继续阅读下文的<<>>。


<<>>

实际上,除了<>还有<<>>。它们之间有区别,这会涉及到文件句柄和ARGV变量。

<>会隐式打开来自@ARGV数组中的参数文件,打开方式是两参数格式的open,类似于open "FH","$ARGV[N]"。正常情况下这没什么问题,但可能会成致命的危险。例如Perl脚本文件a.pl内容如下:
while(<>){print}

如果执行该脚本的方式为:
$ perl a.pl "riskcmd -rfv * |"

其中riskcmd -rfv * |被当作一个参数收集到@ARGV数组中,因为<>以两参数模式的方式隐式打开文件,这等价于:
open FH,"riskcmd -rfv *|";

这表示先打开一个管道,再执行riskcmd -rfv *命令,并将该命令的输出通过管道传递供<>读取。

再看下面的示例,它们是等价的:
$ perl a.pl "ls /tmp |"
$ ls /tmp | perl a.pl

这在写一行式perl程序的时候比较方便:
perl -pe '' "ls /tmp |"
ls /tmp | perl -pe ''

所以以<>的方式打开@ARGV中的文件时,因为无法保证这个数组中的参数一定是文件,所以可能会出现危险操作。而<<>>则能避免这个问题,它隐式地以三参数的open打开@ARGV中的文件。类似于:
open FH,"<","$ARGV[N]";

它限定了第二个参数是输入重定向操作,所以它保证了@ARGV中的参数必须是文件,否则就会报错。
perl -e 'while(<<>>){print}' /etc/passwd
perl -e 'while(<<>>){print}' "ls /tmp |" # 报错

这时想要从管道读取数据,只能将管道放在perl命令的前面。
ls /tmp | perl -e 'while(<<>>){print}'


参考来源:
Perl文件操作入门
perl-chomp与chop操作函数