perlfaq5 - 文件和格式
版本 5.20210520
本节介绍 I/O 和 "f" 相关问题:文件句柄、刷新、格式和页脚。
(由 brian d foy 贡献)
您可能想阅读 Mark Jason Dominus 的 "Suffering From Buffering",地址为 http://perl.plover.com/FAQs/Buffering.html 。
Perl 通常会缓冲输出,因此它不会为每一点输出都进行系统调用。通过保存输出,它可以减少昂贵的系统调用次数。例如,在这段代码中,您希望为处理的每一行在屏幕上打印一个点,以观察程序的进度。但是,您不会看到每行都出现一个点,因为 Perl 会缓冲输出,您需要等待很长时间才能看到一排 50 个点同时出现。
# long wait, then row of dots all at once
while( <> ) {
print ".";
print "\n" unless ++$count % 50;
#... expensive line processing operations
}
为了解决这个问题,您必须取消缓冲输出文件句柄,在本例中为 STDOUT
。您可以将特殊变量 $|
设置为真值(助记符:使您的文件句柄“热起来”)。
$|++;
# dot shown immediately
while( <> ) {
print ".";
print "\n" unless ++$count % 50;
#... expensive line processing operations
}
$|
是每个文件句柄的特殊变量之一,因此每个文件句柄都有其自身的值副本。例如,如果您想合并标准输出和标准错误,则必须取消缓冲每个文件句柄(尽管 STDERR 可能默认情况下是取消缓冲的)。
{
my $previous_default = select(STDOUT); # save previous default
$|++; # autoflush STDOUT
select(STDERR);
$|++; # autoflush STDERR, to be sure
select($previous_default); # restore previous default
}
# now should alternate . and +
while( 1 ) {
sleep 1;
print STDOUT ".";
print STDERR "+";
print STDOUT "\n" unless ++$count % 25;
}
除了 $|
特殊变量之外,您还可以使用 binmode
为您的文件句柄提供 :unix
层,该层是取消缓冲的。
binmode( STDOUT, ":unix" );
while( 1 ) {
sleep 1;
print ".";
print "\n" unless ++$count % 50;
}
有关输出层的更多信息,请参阅 binmode
和 open 在 perlfunc 中的条目,以及 PerlIO 模块文档。
如果您使用的是 IO::Handle 或其子类之一,则可以调用 autoflush
方法来更改文件句柄的设置。
use IO::Handle;
open my( $io_fh ), ">", "output.txt";
$io_fh->autoflush(1);
IO::Handle 对象还具有 flush
方法。您可以随时刷新缓冲区,而无需自动缓冲。
$io_fh->flush;
(由 brian d foy 贡献)
从文本文件中插入、更改或删除一行的基本思想是读取和打印文件到您要进行更改的位置,进行更改,然后读取和打印文件的其余部分。Perl 不提供对行的随机访问(尤其是因为记录输入分隔符 $/
是可变的),尽管诸如 Tie::File 之类的模块可以模拟它。
执行这些任务的 Perl 程序采用打开文件、打印其行,然后关闭文件的基本形式。
open my $in, '<', $file or die "Can't read old file: $!";
open my $out, '>', "$file.new" or die "Can't write new file: $!";
while( <$in> ) {
print $out $_;
}
close $out;
在此基本形式内,添加您需要插入、更改或删除行的部分。
要将行追加到开头,请在进入打印现有行的循环之前打印这些行。
open my $in, '<', $file or die "Can't read old file: $!";
open my $out, '>', "$file.new" or die "Can't write new file: $!";
print $out "# Add this line to the top\n"; # <--- HERE'S THE MAGIC
while( <$in> ) {
print $out $_;
}
close $out;
要更改现有行,请在 while
循环内插入修改行的代码。在本例中,代码查找所有小写版本的“perl”并将它们转换为大写。这会对每一行都发生,因此请确保您应该对每一行都这样做!
open my $in, '<', $file or die "Can't read old file: $!";
open my $out, '>', "$file.new" or die "Can't write new file: $!";
print $out "# Add this line to the top\n";
while( <$in> ) {
s/\b(perl)\b/Perl/g;
print $out $_;
}
close $out;
要仅更改特定行,输入行号 $.
很有用。首先读取并打印您要更改的行之前的行。接下来,读取您要更改的单行,更改它并打印它。之后,读取其余的行并打印它们。
while( <$in> ) { # print the lines before the change
print $out $_;
last if $. == 4; # line number before change
}
my $line = <$in>;
$line =~ s/\b(perl)\b/Perl/g;
print $out $line;
while( <$in> ) { # print the rest of the lines
print $out $_;
}
要跳过行,请使用循环控制。此示例中的 next
会跳过注释行,而 last
会在遇到 __END__
或 __DATA__
时停止所有处理。
while( <$in> ) {
next if /^\s+#/; # skip comment lines
last if /^__(END|DATA)__$/; # stop at end of code marker
print $out $_;
}
通过使用 next
跳过您不想在输出中显示的行,执行相同的操作来删除特定行。此示例跳过每五行。
while( <$in> ) {
next unless $. % 5;
print $out $_;
}
如果出于某种奇怪的原因,您确实想一次查看整个文件,而不是逐行处理,您可以将其全部读取(只要您可以在内存中容纳整个文件!)。
open my $in, '<', $file or die "Can't read old file: $!"
open my $out, '>', "$file.new" or die "Can't write new file: $!";
my $content = do { local $/; <$in> }; # slurp!
# do your magic here
print $out $content;
诸如 Path::Tiny 和 Tie::File 之类的模块也可以提供帮助。但是,如果可以,请避免一次读取整个文件。Perl 不会将该内存返回给操作系统,直到进程结束。
您也可以使用 Perl 单行命令来修改文件。以下命令将 inFile.txt 中的所有 "Fred" 替换为 "Barney",并用新内容覆盖文件。使用 -p
开关,Perl 会将您使用 -e
指定的代码包装在一个 while
循环中,而 -i
会开启就地编辑。当前行位于 $_
中。使用 -p
,Perl 会在循环结束时自动打印 $_
的值。有关更多详细信息,请参阅 perlrun。
perl -pi -e 's/Fred/Barney/' inFile.txt
要备份 inFile.txt
,请为 -i
提供一个要添加的文件扩展名
perl -pi.bak -e 's/Fred/Barney/' inFile.txt
要仅更改第五行,您可以添加一个测试来检查 $.
(输入行号),然后仅在测试通过时执行操作
perl -pi -e 's/Fred/Barney/ if $. == 5' inFile.txt
要在特定行之前添加行,您可以在 Perl 打印 $_
之前添加一行(或多行)
perl -pi -e 'print "Put before third line\n" if $. == 3' inFile.txt
您甚至可以在文件开头添加一行,因为当前行在循环结束时打印
perl -pi -e 'print "Put before first line\n" if $. == 1' inFile.txt
要在文件中的现有行之后插入一行,请使用 -n
开关。它与 -p
类似,只是它不会在循环结束时打印 $_
,因此您必须自己执行此操作。在这种情况下,先打印 $_
,然后打印要添加的行。
perl -ni -e 'print; print "Put after fifth line\n" if $. == 5' inFile.txt
要删除行,只需打印您想要保留的行。
perl -ni -e 'print if /d/' inFile.txt
(由 brian d foy 贡献)
从概念上讲,计算文件中的行数最简单的方法是简单地读取它们并进行计数
my $count = 0;
while( <$fh> ) { $count++; }
不过,您实际上不必自己计数,因为 Perl 已经使用 $.
变量完成了此操作,该变量是上次读取的文件句柄的当前行号
1 while( <$fh> );
my $count = $.;
如果您想使用 $.
,您可以将其简化为一个简单的单行命令,例如以下命令之一
% perl -lne '} print $.; {' file
% perl -lne 'END { print $. }' file
不过,这些方法可能效率不高。如果它们的速度不够快,您可能只需要读取数据块并计算换行符的数量
my $lines = 0;
open my($fh), '<:raw', $filename or die "Can't open $filename: $!";
while( sysread $fh, $buffer, 4096 ) {
$lines += ( $buffer =~ tr/\n// );
}
close $fh;
但是,如果行尾不是换行符,这将不起作用。您可能需要将 tr///
更改为 s///
,以便您可以计算输入记录分隔符 $/
出现的次数
my $lines = 0;
open my($fh), '<:raw', $filename or die "Can't open $filename: $!";
while( sysread $fh, $buffer, 4096 ) {
$lines += ( $buffer =~ s|$/||g; );
}
close $fh;
如果您不介意使用 shell 命令,wc
命令通常是最快的,即使存在额外的进程间开销。但是,请确保您拥有一个未受污染的文件名
#!perl -T
$ENV{PATH} = undef;
my $lines;
if( $filename =~ /^([0-9a-z_.]+)\z/ ) {
$lines = `/usr/bin/wc -l $1`
chomp $lines;
}
(由 brian d foy 贡献)
最简单的概念解决方案是计算文件中的行数,然后从开头开始,将行数(减去最后 N 行)打印到一个新文件中。
大多数情况下,真正的问题是如何删除最后 N 行,而无需对文件进行多次遍历,或者如何在不进行大量复制的情况下进行操作。当您的文件可能包含数百万行时,简单的概念就变成了现实的难题。
一个技巧是使用 File::ReadBackwards,它从文件末尾开始。该模块提供了一个对象,该对象包装了真正的文件句柄,使您能够轻松地在文件中移动。到达所需位置后,您可以获取实际的文件句柄并像平常一样使用它。在这种情况下,您将在要保留的最后一行末尾获取文件位置,并将文件截断到该位置。
use File::ReadBackwards;
my $filename = 'test.txt';
my $Lines_to_truncate = 2;
my $bw = File::ReadBackwards->new( $filename )
or die "Could not read backwards in [$filename]: $!";
my $lines_from_end = 0;
until( $bw->eof or $lines_from_end == $Lines_to_truncate ) {
print "Got: ", $bw->readline;
$lines_from_end++;
}
truncate( $filename, $bw->tell );
File::ReadBackwards 模块还具有将输入记录分隔符设置为正则表达式的优势。
您还可以使用 Tie::File 模块,该模块允许您通过绑定数组访问行。您可以使用正常的数组操作来修改您的文件,包括设置最后一个索引和使用 splice
。
-i
选项? -i
设置 Perl 的 $^I
变量的值,这反过来会影响 <>
的行为;有关更多详细信息,请参阅 perlrun。通过直接修改相应的变量,您可以在更大的程序中获得相同的效果。例如
# ...
{
local($^I, @ARGV) = ('.orig', glob("*.c"));
while (<>) {
if ($. == 1) {
print "This line should appear at the top of each file\n";
}
s/\b(p)earl\b/${1}erl/i; # Correct typos, preserving case
print;
close ARGV if eof; # Reset $.
}
}
# $^I and @ARGV return to their old values here
此代码块修改当前目录中的所有 .c
文件,并在新的 .c.orig
文件中保留每个文件的原始数据的备份。
(由 brian d foy 贡献)
使用 File::Copy 模块。它随 Perl 一起提供,可以跨文件系统进行真正的复制,并且以可移植的方式执行其操作。
use File::Copy;
copy( $original, $new_copy ) or die "Copy failed: $!";
如果您无法使用 File::Copy,则必须自己完成工作:打开原始文件,打开目标文件,然后在读取原始文件时打印到目标文件。您还必须记住将权限、所有者和组复制到新文件。
如果您不需要知道文件的名字,您可以使用 `open()` 函数,并在文件名位置使用 `undef`。在 Perl 5.8 或更高版本中,`open()` 函数会创建一个匿名临时文件。
open my $tmp, '+>', undef or die $!;
否则,您可以使用 File::Temp 模块。
use File::Temp qw/ tempfile tempdir /;
my $dir = tempdir( CLEANUP => 1 );
($fh, $filename) = tempfile( DIR => $dir );
# or if you don't need to know the filename
my $fh = tempfile( DIR => $dir );
File::Temp 模块从 Perl 5.6.1 开始成为标准模块。如果您没有安装足够新的 Perl 版本,请使用 IO::File 模块的 `new_tmpfile` 类方法来获取一个打开以供读写操作的文件句柄。如果您不需要知道文件的名字,请使用它。
use IO::File;
my $fh = IO::File->new_tmpfile()
or die "Unable to make new temporary file: $!";
如果您坚持要手动创建临时文件,请使用进程 ID 和/或当前时间值。如果您在一个进程中需要创建多个临时文件,请使用计数器。
BEGIN {
use Fcntl;
use File::Spec;
my $temp_dir = File::Spec->tmpdir();
my $file_base = sprintf "%d-%d-0000", $$, time;
my $base_name = File::Spec->catfile($temp_dir, $file_base);
sub temp_file {
my $fh;
my $count = 0;
until( defined(fileno($fh)) || $count++ > 100 ) {
$base_name =~ s/-(\d+)$/"-" . (1 + $1)/e;
# O_EXCL is required for security reasons.
sysopen $fh, $base_name, O_WRONLY|O_EXCL|O_CREAT;
}
if( defined fileno($fh) ) {
return ($fh, $base_name);
}
else {
return ();
}
}
}
最有效的方式是使用 pack() 和 unpack() 函数。与使用 substr() 函数相比,这在处理大量字符串时更快。但对于少量字符串来说,它会更慢。
以下是一段示例代码,用于拆分和重新组合一些固定格式的输入行,在本例中,这些输入行来自一个普通的 Berkeley 风格的 ps 命令的输出。
# sample input line:
# 15158 p5 T 0:00 perl /home/tchrist/scripts/now-what
my $PS_T = 'A6 A4 A7 A5 A*';
open my $ps, '-|', 'ps';
print scalar <$ps>;
my @fields = qw( pid tt stat time command );
while (<$ps>) {
my %process;
@process{@fields} = unpack($PS_T, $_);
for my $field ( @fields ) {
print "$field: <$process{$field}>\n";
}
print 'line=', pack($PS_T, @process{@fields} ), "\n";
}
我们使用了哈希切片,以便轻松地处理每一行的字段。将键存储在一个数组中,可以方便地将它们作为一个组进行操作,或者使用 `for` 循环遍历它们。它还可以避免污染程序中的全局变量和使用符号引用。
从 perl5.6 开始,如果您传递一个未初始化的标量变量给 open() 函数,它会自动将文件和目录句柄作为引用进行创建。然后,您可以像传递其他标量一样传递这些引用,并在需要命名句柄的地方使用它们。
open my $fh, $file_name;
open local $fh, $file_name;
print $fh "Hello World!\n";
process_file( $fh );
如果您愿意,可以将这些文件句柄存储在数组或哈希中。如果您直接访问它们,它们不是简单的标量,您需要在 `print` 函数中添加一些帮助,将文件句柄引用放在大括号中。只有当文件句柄引用是一个简单的标量时,Perl 才能自行识别它。
my @fhs = ( $fh1, $fh2, $fh3 );
for( $i = 0; $i <= $#fhs; $i++ ) {
print {$fhs[$i]} "just another Perl answer, \n";
}
在 perl5.6 之前,您需要处理各种类型全局变量的习惯用法,您可能会在旧代码中看到这些习惯用法。
open FILE, "> $filename";
process_typeglob( *FILE );
process_reference( \*FILE );
sub process_typeglob { local *FH = shift; print FH "Typeglob!" }
sub process_reference { local $fh = shift; print $fh "Reference!" }
如果您想创建多个匿名句柄,您应该查看 Symbol 或 IO::Handle 模块。
间接文件句柄是指在需要文件句柄的地方使用符号以外的东西。以下是一些获取间接文件句柄的方法。
$fh = SOME_FH; # bareword is strict-subs hostile
$fh = "SOME_FH"; # strict-refs hostile; same package only
$fh = *SOME_FH; # typeglob
$fh = \*SOME_FH; # ref to typeglob (bless-able)
$fh = *SOME_FH{IO}; # blessed IO::Handle from *SOME_FH typeglob
或者,您可以使用来自 IO::* 模块的 new
方法来创建一个匿名文件句柄,并将该句柄存储在一个标量变量中。
use IO::Handle; # 5.004 or higher
my $fh = IO::Handle->new();
然后像使用普通文件句柄一样使用它们。在 Perl 期望文件句柄的任何地方,都可以使用间接文件句柄。间接文件句柄只是一个包含文件句柄的标量变量。像 print
、open
、seek
或 <FH>
菱形运算符这样的函数将接受命名文件句柄或包含文件句柄的标量变量。
($ifh, $ofh, $efh) = (*STDIN, *STDOUT, *STDERR);
print $ofh "Type it: ";
my $got = <$ifh>
print $efh "What was that: $got";
如果您要将文件句柄传递给函数,您可以用两种方式编写该函数。
sub accept_fh {
my $fh = shift;
print $fh "Sending to indirect filehandle\n";
}
或者它可以局部化一个类型全局变量,并直接使用文件句柄。
sub accept_fh {
local *FH = shift;
print FH "Sending to localized filehandle\n";
}
两种风格都适用于真实文件句柄的对象或类型全局变量。(它们也可能在某些情况下适用于字符串,但这很冒险。)
accept_fh(*STDOUT);
accept_fh($handle);
在上面的示例中,我们在使用文件句柄之前将其分配给了一个标量变量。这是因为只有简单的标量变量,而不是表达式或哈希或数组的下标,才能与 print
、printf
或菱形运算符等内置函数一起使用。使用简单的标量变量以外的东西作为文件句柄是非法的,甚至无法编译。
my @fd = (*STDIN, *STDOUT, *STDERR);
print $fd[1] "Type it: "; # WRONG
my $got = <$fd[0]> # WRONG
print $fd[2] "What was that: $got"; # WRONG
对于 print
和 printf
,您可以通过使用块和表达式来解决这个问题,您可以在其中放置文件句柄。
print { $fd[1] } "funny stuff\n";
printf { $fd[1] } "Pity the poor %x.\n", 3_735_928_559;
# Pity the poor deadbeef.
该块是一个与其他块一样的正常块,因此您可以在其中放置更复杂的代码。这会将消息发送到以下两个地方之一。
my $ok = -x "/bin/cat";
print { $ok ? $fd[1] : $fd[2] } "cat stat $ok\n";
print { $fd[ 1+ ($ok || 0) ] } "cat stat $ok\n";
这种将 print
和 printf
视为对象方法调用的方法不适用于菱形运算符。这是因为它是一个真正的运算符,而不仅仅是一个带有无逗号参数的函数。假设您已将类型全局变量存储在您的结构中,如上所示,您可以使用名为 readline
的内置函数来读取记录,就像 <>
一样。鉴于上面为 @fd 显示的初始化,这将起作用,但仅因为 readline() 需要一个类型全局变量。它不适用于对象或字符串,这可能是一个我们尚未修复的错误。
$got = readline($fd[0]);
需要注意的是,间接文件句柄的不可靠性与它们是字符串、类型全局变量、对象还是其他任何东西无关。这是基本运算符的语法问题。玩对象游戏在这里对你没有任何帮助。
(由 Peter J. Holzer 贡献,[email protected])
从 Perl 5.8.0 开始,可以通过将指向该字符串的引用而不是文件名传递给 open 函数来创建指向字符串的文件句柄。然后可以使用此文件句柄读取或写入字符串。
open(my $fh, '>', \$string) or die "Could not open string for writing";
print $fh "foo\n";
print $fh "bar\n"; # $string now contains "foo\nbar\n"
open(my $fh, '<', \$string) or die "Could not open string for reading";
my $x = <$fh>; # $x now contains "foo\n"
对于旧版本的 Perl,IO::String 模块提供了类似的功能。
没有内置的方法可以做到这一点,但 perlform 提供了一些技巧,使勇敢的程序员能够实现它。
(由 brian d foy 贡献)
如果你想将内容写入字符串,你只需要对字符串打开一个文件句柄,Perl 从 5.6 版本开始就支持这种操作。
open FH, '>', \my $string;
write( FH );
由于你希望成为一名优秀的程序员,你可能希望使用词法文件句柄,即使格式被设计为与裸字文件句柄一起使用,因为默认的格式名称采用文件句柄名称。但是,你可以使用一些 Perl 特殊的每个文件句柄变量来控制这一点:$^
,它命名页首格式,$~
显示行格式。你必须更改默认文件句柄才能设置这些变量。
open my($fh), '>', \my $string;
{ # set per-filehandle variables
my $old_fh = select( $fh );
$~ = 'ANIMAL';
$^ = 'ANIMAL_TOP';
select( $old_fh );
}
format ANIMAL_TOP =
ID Type Name
.
format ANIMAL =
@## @<<< @<<<<<<<<<<<<<<
$id, $type, $name
.
虽然 write 可以与词法变量或包变量一起使用,但无论你使用什么变量,它们都必须在格式中作用域。这很可能意味着你需要局部化一些包变量。
{
local( $id, $type, $name ) = qw( 12 cat Buster );
write( $fh );
}
print $string;
还有一些技巧可以与 formline
和累加器变量 $^A
一起使用,但你将失去格式的大部分价值,因为 formline
不会处理分页等操作。最终,在使用格式时,你将重新实现它们。
(由 brian d foy 和 Benjamin Goldberg 贡献)
你可以使用 Number::Format 来分隔数字中的位数。它处理区域设置信息,对于那些想要插入句点(或任何他们想使用的其他字符)的人来说非常有用。
这个子程序将为你的数字添加逗号。
sub commify {
local $_ = shift;
1 while s/^([-+]?\d+)(\d{3})/$1,$2/;
return $_;
}
这个来自 Benjamin Goldberg 的正则表达式将为数字添加逗号。
s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g;
带注释更容易理解。
s/(
^[-+]? # beginning of number.
\d+? # first digits before first comma
(?= # followed by, (but not included in the match) :
(?>(?:\d{3})+) # some positive multiple of three digits.
(?!\d) # an *exact* multiple, not x * 3 + 1 or whatever.
)
| # or:
\G\d{3} # after the last group, get three digits
(?=\d) # but they have to have more digits after them.
)/$1,/xg;
使用 <> (glob()
) 运算符,在 perlfunc 中有说明。低于 5.6 版本的 Perl 需要安装一个能够识别波浪号的 shell。更高版本的 Perl 内置了此功能。 File::KGlob 模块(可在 CPAN 上获取)提供了更可移植的 glob 功能。
在 Perl 中,你可以直接使用它。
$filename =~ s{
^ ~ # find a leading tilde
( # save this in $1
[^/] # a non-slash character
* # repeated 0 or more times (0 means me)
)
}{
$1
? (getpwnam($1))[7]
: ( $ENV{HOME} || $ENV{LOGDIR} )
}ex;
因为你使用了类似这样的代码,它会先截断文件,然后才给你读写权限。
open my $fh, '+>', '/path/name'; # WRONG (almost always)
糟糕。你应该使用这种方式,如果文件不存在,它会失败。
open my $fh, '+<', '/path/name'; # open for update
使用 ">" 总是覆盖或创建文件。使用 "<" 永远不会做这两件事。"+" 不会改变这一点。
以下是各种文件打开方式的示例。使用 sysopen
的所有示例都假设你已经从 Fcntl 中引入了常量。
use Fcntl;
以只读模式打开文件
open my $fh, '<', $path or die $!;
sysopen my $fh, $path, O_RDONLY or die $!;
以写入模式打开文件,如果需要则创建新文件,否则截断旧文件
open my $fh, '>', $path or die $!;
sysopen my $fh, $path, O_WRONLY|O_TRUNC|O_CREAT or die $!;
sysopen my $fh, $path, O_WRONLY|O_TRUNC|O_CREAT, 0666 or die $!;
以写入模式打开文件,创建新文件,文件必须不存在
sysopen my $fh, $path, O_WRONLY|O_EXCL|O_CREAT or die $!;
sysopen my $fh, $path, O_WRONLY|O_EXCL|O_CREAT, 0666 or die $!;
以追加模式打开文件,如果需要则创建
open my $fh, '>>', $path or die $!;
sysopen my $fh, $path, O_WRONLY|O_APPEND|O_CREAT or die $!;
sysopen my $fh, $path, O_WRONLY|O_APPEND|O_CREAT, 0666 or die $!;
以追加模式打开文件,文件必须存在
sysopen my $fh, $path, O_WRONLY|O_APPEND or die $!;
以更新模式打开文件,文件必须存在
open my $fh, '+<', $path or die $!;
sysopen my $fh, $path, O_RDWR or die $!;
以更新模式打开文件,如果需要则创建文件
sysopen my $fh, $path, O_RDWR|O_CREAT or die $!;
sysopen my $fh, $path, O_RDWR|O_CREAT, 0666 or die $!;
以更新模式打开文件,文件必须不存在
sysopen my $fh, $path, O_RDWR|O_EXCL|O_CREAT or die $!;
sysopen my $fh, $path, O_RDWR|O_EXCL|O_CREAT, 0666 or die $!;
以非阻塞模式打开文件,如果需要则创建
sysopen my $fh, '/foo/somefile', O_WRONLY|O_NDELAY|O_CREAT
or die "can't open /foo/somefile: $!":
请注意,在 NFS 上,文件的创建和删除操作不保证是原子操作。也就是说,两个进程可能都成功创建或删除了同一个文件!因此,O_EXCL 并不像你希望的那样排他。
另请参阅 perlopentut。
<>
运算符执行一个通配符操作(见上文)。在 Perl v5.6.0 之前的版本中,内部 glob() 运算符会 fork csh(1) 来执行实际的通配符扩展,但 csh 无法处理超过 127 个项目,因此会给出错误消息 Argument list too long
。将 tcsh 安装为 csh 的用户不会遇到这个问题,但他们的用户可能会对此感到意外。
要解决这个问题,要么升级到 Perl v5.6.0 或更高版本,要么使用 readdir() 和模式自己执行通配符操作,要么使用像 File::Glob 这样的模块,它不使用 shell 来执行通配符操作。
(由 Brian McCauley 贡献)
Perl 的 open() 函数的特殊双参数形式会忽略文件名中的尾随空格,并从某些前导字符(或尾随 "|")推断出模式。在旧版本的 Perl 中,这是 open() 的唯一版本,因此它在旧代码和书籍中很常见。
除非您有特殊原因需要使用两个参数的形式,否则您应该使用三个参数形式的 open(),它不会将文件名中的任何字符视为特殊字符。
open my $fh, "<", " file "; # filename is " file "
open my $fh, ">", ">file"; # filename is ">file"
如果您的操作系统支持适当的 mv(1) 实用程序或其功能等效项,则此方法有效
rename($old, $new) or system("mv", $old, $new);
使用 File::Copy 模块可能更便携。您只需将文件复制到新名称(检查返回值),然后删除旧文件。从语义上讲,这与 rename()
并不完全相同,rename()
会保留元信息,如权限、时间戳、inode 信息等。
Perl 的内置 flock() 函数(有关详细信息,请参见 perlfunc)将在存在的情况下调用 flock(2),在不存在的情况下调用 fcntl(2)(在 perl 版本 5.004 及更高版本中),如果前两个系统调用都不存在,则调用 lockf(3)。在某些系统上,它甚至可能使用其他形式的本机锁定。以下是 Perl 的 flock() 的一些注意事项
如果三个系统调用(或其等效关闭)都不存在,则会产生致命错误。
lockf(3) 不提供共享锁定,并且要求文件句柄以写入(或追加,或读写)方式打开。
某些版本的 flock() 无法锁定网络上的文件(例如 NFS 文件系统),因此您需要在构建 Perl 时强制使用 fcntl(2)。但这充其量也是可疑的。有关构建 Perl 以执行此操作的信息,请参见 perlfunc 中的 flock 条目以及源代码分发中的 INSTALL 文件。
两个可能不明显但传统的 flock 语义是它会无限期地等待直到授予锁定,并且它的锁定是仅仅是建议性的。这种可选择的锁定更灵活,但提供的保证更少。这意味着使用 flock() 锁定的文件可能会被不使用 flock() 的程序修改。遵守红灯的汽车彼此相处融洽,但与不遵守红灯的汽车相处不融洽。有关详细信息,请参见 perlport 手册页、您端口的特定文档或您的系统特定的本地手册页。如果您正在编写可移植程序,最好假设传统行为。(如果您不是,您应该始终可以自由地为自己的系统的特性(有时称为“特性”)编写代码。对可移植性问题的奴隶般遵守不应妨碍您完成工作。)
有关文件锁定的更多信息,如果您有的话,请参阅 "perlopentut 中的文件锁定"(5.6 版新增)。
一段常见的不应使用的代码是
sleep(3) while -e 'file.lock'; # PLEASE DO NOT USE
open my $lock, '>', 'file.lock'; # THIS BROKEN CODE
这是一个典型的竞争条件:您需要执行两个步骤才能完成一项必须一步完成的操作。这就是为什么计算机硬件提供原子测试和设置指令的原因。理论上,这“应该”起作用
sysopen my $fh, "file.lock", O_WRONLY|O_EXCL|O_CREAT
or die "can't open file.lock: $!";
但遗憾的是,文件创建(和删除)在 NFS 上不是原子的,因此这在网络上不会起作用(至少,并非每次都会起作用)。有人建议使用各种涉及 link() 的方案,但这些方案往往涉及忙等待,这也是不太理想的。
难道没有人告诉过你网页点击计数器毫无用处吗?它们不能统计点击次数,它们是浪费时间,而且它们只用来满足作者的虚荣心。最好选择一个随机数;它们更现实。
无论如何,如果您无法控制自己,可以这样做。
use Fcntl qw(:DEFAULT :flock);
sysopen my $fh, "numfile", O_RDWR|O_CREAT or die "can't open numfile: $!";
flock $fh, LOCK_EX or die "can't flock numfile: $!";
my $num = <$fh> || 0;
seek $fh, 0, 0 or die "can't rewind numfile: $!";
truncate $fh, 0 or die "can't truncate numfile: $!";
(print $fh $num+1, "\n") or die "can't write numfile: $!";
close $fh or die "can't close numfile: $!";
这是一个更好的网页点击计数器
$hits = int( (time() - 850_000_000) / rand(1_000) );
如果这个计数器没有给您的朋友留下深刻印象,那么代码可能会给您留下深刻印象。:-)
如果您在正确实现 flock
的系统上,并且您使用“perldoc -f flock”中的示例追加代码,即使您所在的 OS 没有正确实现追加模式(如果存在这样的系统),一切都会正常。因此,如果您乐于将自己限制在实现 flock
的 OS 上(这实际上并不是什么限制),那么您应该这样做。
如果您知道您只打算使用正确实现追加功能的系统(即不是 Win32),那么您可以从上一个答案中的代码中省略 seek
。
如果您知道您只编写在实现追加模式的 OS 和文件系统(例如现代 Unix 上的本地文件系统)上运行的代码,并且您将文件保持在块缓冲模式,并且您在每次手动刷新缓冲区之间写入的输出少于一个缓冲区,那么几乎可以保证每个缓冲区负载都将以一个块写入文件末尾,而不会与其他人的输出混合。您还可以使用 syswrite
函数,它只是系统 write(2)
系统调用的包装器。
在系统级 write()
操作完成之前,仍然存在一个很小的理论可能性,即信号会中断该操作。还有一种可能性是,某些 STDIO 实现可能会调用多个系统级 write()
,即使缓冲区最初是空的。在某些系统上,这种可能性可能会降至零,并且在使用 :perlio
而不是系统的 STDIO 时,这不会成为问题。
如果你只是想修补一个二进制文件,在很多情况下,像这样简单的操作就足够了
perl -i -pe 's{window manager}{window mangler}g' /usr/bin/emacs
但是,如果你有固定大小的记录,那么你可能需要做一些更类似于这样的操作
my $RECSIZE = 220; # size of record, in bytes
my $recno = 37; # which record to update
open my $fh, '+<', 'somewhere' or die "can't update somewhere: $!";
seek $fh, $recno * $RECSIZE, 0;
read $fh, $record, $RECSIZE == $RECSIZE or die "can't read record $recno: $!";
# munge the record
seek $fh, -$RECSIZE, 1;
print $fh $record;
close $fh;
锁定和错误检查留给读者作为练习。不要忘记它们,否则你会后悔的。
如果你想检索文件最后一次被读取、写入或元数据(所有者等)被修改的时间,你可以使用 -A、-M 或 -C 文件测试操作,如 perlfunc 中所述。这些操作会检索文件的年龄(以你的程序启动时间为基准),以天为单位,并以浮点数形式表示。某些平台可能并不支持所有这些时间。有关详细信息,请参阅 perlport。要检索自纪元以来的“原始”时间(以秒为单位),你可以调用 stat 函数,然后使用 localtime()
、gmtime()
或 POSIX::strftime()
将其转换为人类可读的格式。
以下是一个示例
my $write_secs = (stat($file))[9];
printf "file %s updated at %s\n", $file,
scalar localtime($write_secs);
如果你更喜欢更易读的格式,可以使用 File::stat 模块(在版本 5.004 及更高版本中是标准发行版的一部分)
# error checking left as an exercise for reader.
use File::stat;
use Time::localtime;
my $date_string = ctime(stat($file)->mtime);
print "file $file updated at $date_string\n";
POSIX::strftime() 方法的优点是,理论上它独立于当前区域设置。有关详细信息,请参阅 perllocale。
你可以使用 "utime" in perlfunc 中记录的 utime() 函数。例如,以下是一个小程序,它将第一个参数的读取和写入时间复制到所有其他参数中。
if (@ARGV < 2) {
die "usage: cptimes timestamp_file other_files ...\n";
}
my $timestamp = shift;
my($atime, $mtime) = (stat($timestamp))[8,9];
utime $atime, $mtime, @ARGV;
错误检查,像往常一样,留给读者作为练习。
utime 的 perldoc 还提供了一个示例,该示例对已存在的文件具有与 touch(1) 相同的效果。
某些文件系统对以预期精度存储文件时间的能力有限。例如,FAT 和 HPFS 文件系统无法创建日期精度低于两秒的文件。这是文件系统本身的限制,而不是 utime() 的限制。
要将一个文件句柄连接到多个输出文件句柄,可以使用 IO::Tee 或 Tie::FileHandle::Multiplex 模块。
如果只需要执行一次,可以分别打印到每个文件句柄。
for my $fh ($fh1, $fh2, $fh3) { print $fh "whatever\n" }
处理文件中所有行的Perl惯用方法是逐行处理。
open my $input, '<', $file or die "can't open $file: $!";
while (<$input>) {
chomp;
# do something with $_
}
close $input or die "can't close $file: $!";
这比将整个文件读入内存作为行数组,然后逐个元素处理要高效得多,这通常(如果不是几乎总是)是错误的方法。每当你看到有人这样做
my @lines = <INPUT>;
你应该认真思考为什么需要一次加载所有内容。这不是一个可扩展的解决方案。
如果使用CPAN上的File::Map模块对文件进行“mmap”,你可以在不实际存储在内存中的情况下,将整个文件虚拟地加载到一个字符串中。
use File::Map qw(map_file);
map_file my $string, $filename;
映射后,你可以像对待任何其他字符串一样对待$string
。由于你不必加载数据,因此mmap-ing速度非常快,并且可能不会增加内存占用。
你可能还会发现使用标准的 Tie::File 模块或 DB_File 模块的$DB_RECNO
绑定更有趣,这些绑定允许你将数组绑定到文件,以便访问数组的元素实际上访问文件中的对应行。
如果你想加载整个文件,可以使用 Path::Tiny 模块在一个简单高效的步骤中完成。
use Path::Tiny;
my $all_of_it = path($filename)->slurp; # entire file in scalar
my @all_lines = path($filename)->lines; # one line per element
或者你可以将整个文件内容读入一个标量,如下所示
my $var;
{
local $/;
open my $fh, '<', $file or die "can't open $file: $!";
$var = <$fh>;
}
这会暂时取消定义你的记录分隔符,并在块退出时自动关闭文件。如果文件已经打开,只需使用以下代码
my $var = do { local $/; <$fh> };
你也可以使用本地化的@ARGV
来消除open
my $var = do { local( @ARGV, $/ ) = $file; <> };
使用$/
变量(有关详细信息,请参阅 perlvar)。你可以将其设置为""
以消除空段落(例如,"abc\n\n\n\ndef"
被视为两个段落,而不是三个),或者设置为"\n\n"
以接受空段落。
请注意,空行中不能包含空格。因此 "fred\n \nstuff\n\n"
是一段,但 "fred\n\nstuff\n\n"
是两段。
您可以对大多数文件句柄使用内置的 getc()
函数,但它不能(轻松地)在终端设备上使用。对于 STDIN,可以使用 CPAN 上的 Term::ReadKey 模块或使用 "perlfunc 中的 getc" 中的示例代码。
如果您的系统支持可移植操作系统编程接口 (POSIX),您可以使用以下代码,您会注意到它还关闭了回显处理。
#!/usr/bin/perl -w
use strict;
$| = 1;
for (1..4) {
print "gimme: ";
my $got = getone();
print "--> $got\n";
}
exit;
BEGIN {
use POSIX qw(:termios_h);
my ($term, $oterm, $echo, $noecho, $fd_stdin);
my $fd_stdin = fileno(STDIN);
$term = POSIX::Termios->new();
$term->getattr($fd_stdin);
$oterm = $term->getlflag();
$echo = ECHO | ECHOK | ICANON;
$noecho = $oterm & ~$echo;
sub cbreak {
$term->setlflag($noecho);
$term->setcc(VTIME, 1);
$term->setattr($fd_stdin, TCSANOW);
}
sub cooked {
$term->setlflag($oterm);
$term->setcc(VTIME, 0);
$term->setattr($fd_stdin, TCSANOW);
}
sub getone {
my $key = '';
cbreak();
sysread(STDIN, $key, 1);
cooked();
return $key;
}
}
END { cooked() }
CPAN 上的 Term::ReadKey 模块可能更容易使用。最近的版本还包括对非可移植系统的支持。
use Term::ReadKey;
open my $tty, '<', '/dev/tty';
print "Gimme a char: ";
ReadMode "raw";
my $key = ReadKey 0, $tty;
ReadMode "normal";
printf "\nYou said %s, char number %03d\n",
$key, ord $key;
您应该做的第一件事是查看 CPAN 上的 Term::ReadKey 扩展。正如我们之前提到的,它现在甚至对非可移植(读:不是开放系统,封闭的,专有的,不是 POSIX,不是 Unix 等)系统提供有限的支持。
您还应该查看 comp.unix.* 中的常见问题解答列表,以了解类似的问题:答案基本相同。它非常依赖于系统。以下是一个在 BSD 系统上有效的解决方案
sub key_ready {
my($rin, $nfd);
vec($rin, fileno(STDIN), 1) = 1;
return $nfd = select($rin,undef,undef,0);
}
如果您想找出有多少字符正在等待,还可以查看 FIONREAD ioctl 调用。与 Perl 一起提供的 h2ph 工具试图将 C 包含文件转换为 Perl 代码,可以 require
。FIONREAD 最终在 sys/ioctl.ph 文件中定义为一个函数
require './sys/ioctl.ph';
$size = pack("L", 0);
ioctl(FH, FIONREAD(), $size) or die "Couldn't call ioctl: $!\n";
$size = unpack("L", $size);
如果 h2ph 未安装或对您不起作用,您可以手动 grep 包含文件
% grep FIONREAD /usr/include/*/*
/usr/include/asm/ioctls.h:#define FIONREAD 0x541B
或者使用冠军编辑器编写一个小的 C 程序
% cat > fionread.c
#include <sys/ioctl.h>
main() {
printf("%#08x\n", FIONREAD);
}
^D
% cc -o fionread fionread.c
% ./fionread
0x4004667f
然后对其进行硬编码,将移植作为一项练习留给您的继任者。
$FIONREAD = 0x4004667f; # XXX: opsys dependent
$size = pack("L", 0);
ioctl(FH, $FIONREAD, $size) or die "Couldn't call ioctl: $!\n";
$size = unpack("L", $size);
FIONREAD 需要连接到流的文件句柄,这意味着套接字、管道和 tty 设备可以工作,但不能工作文件。
tail -f
? 首先尝试
seek($gw_fh, 0, 1);
语句 seek($gw_fh, 0, 1)
不会改变当前位置,但它确实清除了句柄上的文件结束条件,因此下一个 <$gw_fh>
使 Perl 再次尝试读取内容。
如果这不起作用(它依赖于您的 stdio 实现的功能),那么您需要类似这样的东西
for (;;) {
for ($curpos = tell($gw_fh); <$gw_fh>; $curpos =tell($gw_fh)) {
# search for some stuff and put it into files
}
# sleep for a while
seek($gw_fh, $curpos, 0); # seek to where we had been
}
如果这仍然不起作用,请查看 IO::Handle 中的 clearerr
方法,它重置句柄上的错误和文件结束状态。
CPAN 上还有一个 File::Tail 模块。
如果你查看 "perlfunc 中的 open",你会发现几种调用 open() 的方法可以做到。例如
open my $log, '>>', '/foo/logfile';
open STDERR, '>&', $log;
或者甚至使用文字数字描述符
my $fd = $ENV{MHCONTEXTFD};
open $mhcontext, "<&=$fd"; # like fdopen(3S)
请注意,"<&STDIN" 会创建副本,而 "<&=STDIN" 会创建别名。这意味着如果你关闭了别名句柄,所有别名都将变得不可访问。对于副本来说,情况并非如此。
错误检查,一如既往,留作读者的练习。
如果出于某种原因,你拥有的是文件描述符而不是文件句柄(也许你使用了 POSIX::open
),你可以使用 POSIX 模块中的 close()
函数
use POSIX ();
POSIX::close( $fd );
这应该很少有必要,因为 Perl 的 close()
函数用于 Perl 自己打开的东西,即使它是像上面的 MHCONTEXT
那样对数字描述符的复制。但如果你真的必须这样做,你可以尝试这样
require './sys/syscall.ph';
my $rc = syscall(SYS_close(), $fd + 0); # must force numeric
die "can't sysclose $fd: $!" unless $rc == -1;
或者,直接使用 open()
的 fdopen(3S) 功能
{
open my $fh, "<&=$fd" or die "Cannot reopen fd=$fd: $!";
close $fh;
}
糟糕!你刚刚在文件名中插入了一个制表符和一个换页符!请记住,在双引号字符串("like\this")中,反斜杠是转义字符。这些字符的完整列表在 "perlop 中的引号和类似引号的运算符" 中。不出所料,你的旧版 DOS 文件系统上没有名为 "c:(tab)emp(formfeed)oo" 或 "c:(tab)emp(formfeed)oo.exe" 的文件。
要么对字符串使用单引号,要么(最好)使用正斜杠。由于从 MS-DOS 2.0 左右开始的所有 DOS 和 Windows 版本在路径中都将 /
和 \
视为相同,因此你最好使用不会与 Perl 冲突的那个——或者 POSIX shell、ANSI C 和 C++、awk、Tcl、Java 或 Python,仅举几例。POSIX 路径也更具可移植性。
因为即使在非 Unix 端口上,Perl 的 glob 函数也遵循标准的 Unix glob 语义。你需要 glob("*")
来获取所有(非隐藏)文件。这使得 glob() 即使在旧版系统上也能保持可移植性。你的端口可能也包含专有的 glob 函数。查看其文档以获取详细信息。
-i
会覆盖受保护的文件?这难道不是 Perl 中的错误吗?这在 http://www.cpan.org/misc/olddoc/FMTEYEWTK.tgz 中的 "Far More Than You Ever Wanted To Know" 集合中的 file-dir-perms 文章中进行了详细而细致的描述。
执行摘要:了解您的文件系统的工作原理。文件上的权限说明了可以对该文件中的数据执行的操作。目录上的权限说明了可以对该目录中的文件列表执行的操作。如果您删除文件,您将从目录中删除其名称(因此操作取决于目录的权限,而不是文件的权限)。如果您尝试写入文件,则文件的权限将决定您是否被允许。
除了将文件加载到数据库或预先索引文件中的行之外,您还可以做几件事。
以下是来自 Camel Book 的蓄水池采样算法
srand;
rand($.) < 1 && ($line = $_) while <>;
与读取整个文件相比,此方法在空间方面具有显着优势。您可以在 Donald E. Knuth 的 The Art of Computer Programming,第 2 卷,第 3.4.2 节中找到此方法的证明。
您可以使用 File::Random 模块,它提供该算法的函数
use File::Random qw/random_line/;
my $line = random_line($filename);
另一种方法是使用 Tie::File 模块,它将整个文件视为一个数组。只需访问随机数组元素。
(由 brian d foy 贡献)
如果您在打印数组时看到数组元素之间有空格,则可能是您在双引号中插值了数组
my @animals = qw(camel llama alpaca vicuna);
print "animals are: @animals\n";
是双引号,而不是 print
,在执行此操作。每当您在双引号上下文中插值数组时,Perl 都会用空格(或 $"
中的内容,默认情况下为空格)连接元素
animals are: camel llama alpaca vicuna
这与在没有插值的情况下打印数组不同
my @animals = qw(camel llama alpaca vicuna);
print "animals are: ", @animals, "\n";
现在输出没有元素之间的空格,因为 @animals
的元素只是成为 print
的列表的一部分
animals are: camelllamaalpacavicuna
当 @array
的每个元素都以换行符结尾时,您可能会注意到这一点。您希望每行打印一个元素,但注意到除第一行之外的每一行都缩进了
this is a line
this is another line
this is the third line
额外的空格来自数组的插值。如果您不想在数组元素之间放置任何内容,请不要在双引号中使用数组。您可以将其发送到 print
,而无需双引号
print @lines;
(由 brian d foy 贡献)
Perl 自带的 File::Find 模块可以帮助你轻松遍历目录结构。你只需要调用 find
子程序,并传入一个回调子程序和要遍历的目录。
use File::Find;
find( \&wanted, @directories );
sub wanted {
# full path in $File::Find::name
# just filename in $_
... do whatever you want to do ...
}
你可以从 CPAN 下载 File::Find::Closures 模块,它提供了许多可与 File::Find 一起使用的预制子程序。
你也可以从 CPAN 下载 File::Finder 模块,它可以帮助你使用类似于 find
命令行工具的语法来创建回调子程序。
use File::Find;
use File::Finder;
my $deep_dirs = File::Finder->depth->type('d')->ls->exec('rmdir','{}');
find( $deep_dirs->as_options, @places );
从 CPAN 下载的 File::Find::Rule 模块具有类似的接口,但它还会为你完成遍历操作。
use File::Find::Rule;
my @files = File::Find::Rule->file()
->name( '*.pm' )
->in( @INC );
(由 brian d foy 贡献)
如果你有一个空目录,可以使用 Perl 内置的 rmdir
函数。如果目录不为空(包含文件或子目录),你需要手动清空它(非常繁琐),或者使用模块来帮助你。
Perl 自带的 File::Path 模块提供了一个 remove_tree
函数,可以为你完成所有繁琐的工作。
use File::Path qw(remove_tree);
remove_tree( @directories );
File::Path 模块还提供了一个与旧版 rmtree
子程序兼容的接口。
(由 Shlomi Fish 贡献)
要实现类似于 cp -R
的功能(即递归复制整个目录树),你需要自己编写代码,或者使用 CPAN 上的优秀模块,例如 File::Copy::Recursive。
版权所有 (c) 1997-2010 Tom Christiansen、Nathan Torkington 和其他作者(如注释所示)。保留所有权利。
本文档是免费的;你可以根据与 Perl 本身相同的条款重新发布和/或修改它。
无论其分发方式如何,此处的所有代码示例均属于公有领域。你被允许并鼓励在自己的程序中使用此代码及其任何衍生作品,无论是出于娱乐目的还是为了盈利,这取决于你的意愿。在代码中添加一个简单的注释,以表明对 FAQ 的贡献,将是礼貌的,但不是必需的。