"perl -i" 的实现方式是什么?

7
在Perl的-i[extension]特性描述中,网址为http://perldoc.perl.org/perlrun.html,与使用perl -pi.orig ...相当的代码如下所示:
#!/usr/bin/perl

use strict;
use warnings;

my $extension = '.orig';
my $oldargv = '';
my $backup;
LINE: while (<>) {
    if ($ARGV ne $oldargv) {
        if ($extension !~ /\*/) {
            $backup = $ARGV . $extension;
        } else {
            ($backup = $extension) =~ s/\*/$ARGV/g;
        }
        rename($ARGV, $backup);
        open(ARGVOUT, ">$ARGV");
        select(ARGVOUT);
        $oldargv = $ARGV;
    }
    # Don't change anything; just copy.
}
continue {
    print;
}
select(STDOUT);

$extension eq '.orig'时,这个程序可以正常工作;然而,Perl也定义了没有扩展名的-i(即$extension eq '')。

如果没有提供扩展名,并且您的系统支持它,则原始文件保持打开状态,没有名称,同时输出被重定向到具有原始文件名的新文件中。当perl退出时,无论是干净地还是不干净地,原始文件都将被取消链接。

也许我的系统(Mac OS X Yosemite 10.10.3)不支持它。

如果我在此程序中设置$extension = '',则该代码对小于STDIN块的文件(在AcivePerl 5.10中为4096字节,但在ActivePerl 5.16中为8192字节)可以正常工作,但对于大于一个块的文件则不会正常工作。

在我的系统中,如果$ARGV$backup具有相同的值(如果$extension eq '',则它们将是相同的),那么第17行的open(ARGVOUT, ">$ARGV")调用会在读取一个块后破坏输入文件。

当然,我可以通过先写入临时文件,然后在最后重命名来解决这个问题。但是,经过几个小时的调试后,我有点失望,因为perlrun中的示例并不像我预期的那样通用。

  1. 是否有一种标准的惯用方法来处理$extension eq ''的情况?

  2. 这个$extension eq ''的用例是否足够重要,以至于需要编辑perlrun?当然,“如果您的系统支持它”这个条件意味着示例不是不正确的,但如果也涵盖了这种情况,那么示例将更有用。


3
示例程序之所以正确,是因为示例程序涉及到的是-i.orig而不是没有扩展名的-i。如果您仅仅删除程序中的.orig,就期望示例程序表现得像-i一样,我不知道您为什么会有这个期望。(这就像看到5 * 5 == 25,然后假设8 * 8 == 28)它的正确性与“你的系统支持它”这个条件无关,后者是指您的系统是否支持保持对已取消链接文件的打开句柄。 - ruakh
1
顺便说一下,不是-i'',而是没有参数的裸-i。字符串比较使用eq而不是== - ruakh
虽然你可以写成-i'' - ikegami
1
@ruakh,我理解你关于5 * 5的观点,但这只是一个展示Perl工作原理的例子,我认为对这个例子感到有些误导并不是不合理的。按照你的比喻,这就好像你告诉我一些函数f(a, b)(a, b) = (5, 5)时“等同于”代码a * b,但没有提到当ab具有其他可允许值时代码是不同的。 - Cary Millsap
你是想在脚本中实际使用这段代码,还是只是好奇 -i 是如何工作的?如果是前者,你可以在 shebang 行上包含标志,例如 #!/usr/bin/perl -pi - ThisSuitIsBlackNot
3
不,它的目的是展示 perl -p -i.orig -e 's/foo/bar/;' 的工作原理。 - ikegami
1个回答

12

Perl 5.28 更新了-i。本答案适用于Perl的早期版本。


当提供扩展名时:

open(my $fh_in,  '<', $qfn);
rename($qfn, "$qfn$ext");
open(my $fh_out, '>', $qfn);

这可以通过使用strace来查看。
$ strace perl -i~ -pe1 a
...
open("a", O_RDONLY)                     = 3
rename("a", "a~")                       = 0
open("a", O_WRONLY|O_CREAT|O_EXCL, 0600) = 4
...

当没有提供扩展名时:
open(my $fh_in,  '<', $qfn);
unlink($qfn);
open(my $fh_out, '>', $qfn);

这可以使用strace来查看。
$ strace perl -i -pe1 a
...
open("a", O_RDONLY)                     = 3
unlink("a")                             = 0
open("a", O_WRONLY|O_CREAT|O_EXCL, 0600) = 4
...

像Mac一样的Unix系统支持匿名文件,而Windows不支持,所以在Windows上使用-i需要添加一个扩展名。

>perl -i.bak -pe1 a

>perl -i -pe1 a
Can't do inplace edit without backup.

如果我们将这些知识融入到您发布的代码中,我们将得到以下内容:
#!/usr/bin/perl

use strict;
use warnings;

my $extension = '.orig';
my $oldargv = '';
my $backup;
LINE: while (<>) {
    if ($ARGV ne $oldargv) {
        if (length($extension)) {
            if ($extension !~ /\*/) {
                $backup = $ARGV . $extension;
            } else {
                ($backup = $extension) =~ s/\*/$ARGV/g;
            }
            rename($ARGV, $backup);
        } else {
            die("Can't do inplace edit without backup.\n") if $^O eq 'MSWin32';
            unlink($ARGV);
        }
        open(ARGVOUT, ">$ARGV");
        select(ARGVOUT);
        $oldargv = $ARGV;
    }
    # Don't change anything; just copy.
}
continue {
    print;
}
select(STDOUT);

1
而Cygwin则默认将bare -i 静默地设为 -i.bak(推测是为了更好地支持现有的shell脚本)。 - ysth
1
@ysth,奇怪的是在cygwin中unlink技巧是有效的。(我不知道为什么。)也许以前没有起作用?或者可能有一些限制? - ikegami
1
可能记错了,因为已经很久没做了,但大概是通过Win32 API设置一个在关闭时删除的标记,或者在退出处理程序中实际删除它,或者两者兼而有之?也许还有一些限制(比如其他进程也在使用该文件?)。 - ysth
@ysth,那样行不通。文件需要在关闭之前被删除。我猜它会将其重命名为磁盘上的其他位置。 - ikegami
我认为以前不支持,例如http://permalink.gmane.org/gmane.os.cygwin/79103。但是那里引用的cygwin源文件已经不存在了,所以也许现在Windows实际上支持它了? - ysth
@ysth,如果它被使用了,那就解释了Perl中的黑客攻击。 - ikegami

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接