您似乎对在 perl 中如何完成文件 IO 操作感到困惑,所以我建议您阅读一下。
什么地方出了错?
你正在做的是:
- 打开文件进行阅读
- 读取整个文件并关闭它
- 使用 STDOUT 文件句柄打开同一个文件进行覆盖(org 文件被截断)。
- 调整默认打印句柄,以便在您显示的代码中甚至未打开的文件句柄上设置自动刷新。
- 对所有行执行替换并打印它们
- 关闭 STDOUT,然后在一切完成后打印一条消息。
您最大的主要错误是尝试重新打开默认输出文件句柄 STDOUT。我认为这是因为您不知道如何print
工作,即您可以提供一个文件句柄来打印到print FILEHANDLE "text"
. 或者您不知道 STDOUT 是预定义的文件句柄。
您的其他错误:
- 你没有使用
use strict; use warnings;
. 您编写的任何程序都不应该没有这些。它们将防止您做坏事,并为您提供有关错误的信息,并为您节省数小时的调试时间。
- 除非你真的需要,否则你永远不应该“slurp”一个文件(将整个文件读取到一个变量中),因为这是无效且缓慢的,并且对于大文件会导致你的程序由于内存不足而崩溃。
- 永远不要重新分配默认文件句柄 STDIN、STDOUT、STDERR,除非 A) 你真的需要,B) 你知道你在做什么。
select
设置打印的默认文件句柄,阅读文档。这很少是您需要关心的事情。该变量为当前选定的文件句柄$|
设置自动刷新(如果设置为真值)。所以你所做的实际上什么也没做,因为 OUTPUT_HANDLE 是一个不存在的文件句柄。如果您跳过了这些语句,它将为 STDOUT 设置自动刷新。(但你不会注意到任何区别)select
print
使用打印缓冲区,因为它很有效。我假设您正在尝试自动刷新,因为您认为您的打印被捕获在缓冲区中,这是不正确的。一般来说,这不是您需要担心的事情。程序结束时会自动刷新所有打印缓冲区。
- 在大多数情况下,您不需要显式关闭文件句柄。文件句柄在超出范围或程序结束时会自动关闭。
- 推荐使用词法文件句柄,例如,
open my $fh, ...
而不是全局的,例如open FILE, ..
,因为前面的语句,并且因为避免全局变量总是一个好主意。
- 建议使用三参数 open:
open FILEHANDLE, MODE, FILENAME
. 这是因为否则您可能会冒着文件名中的元字符损坏您的open
语句的风险。
快速修复:
现在,正如我在评论中所说,这 - 或者更确切地说,你想要-p
的,因为这段代码是错误的 - 与命令行开关的惯用用法几乎相同:
perl -pi.bak -e 's/(.*?xsl.*?)xsl/$1xslt/' file.txt
这个简短的小片段实际上完成了您的程序所做的所有事情,但做得更好。解释:
-p
switch 自动假定您提供的代码在while (<>) { }
循环内,并在您的代码执行后打印每一行。
-i
switch 告诉 perl 对文件进行就地编辑,将备份副本保存在“file.txt.bak”中。
因此,该单行相当于这样的程序:
$^I = ".bak"; # turns inplace-edit on
while (<>) { # diamond operator automatically uses STDIN or files from @ARGV
s/(.*?xsl.*?)xsl/$1xslt/;
print;
}
这相当于:
my $file = shift; # first argument from @ARGV -- arguments
open my $fh, "<", $file or die $!;
open my $tmp, ">", "/tmp/foo.bar" or die $!; # not sure where tmpfile is
while (<$fh>) { # read lines from org file
s/(.*?xsl.*?)xsl/$1xslt/;
print $tmp $_; # print line to tmp file
}
rename($file, "$file.bak") or die $!; # save backup
rename("/tmp/foo.bar", $file) or die $!; # overwrite original file
inplace-edit 选项实际上创建了一个单独的文件,然后将其复制到原始文件上。如果使用备份选项,则首先备份原始文件。您不需要知道这些信息,只需知道使用-i
开关将导致-p
(and -n
) 选项实际对原始文件执行更改。
-i
不需要在激活备份选项的情况下使用交换机(Windows 除外),但建议使用。一个好主意是先在没有选项的情况下运行单行,因此输出会打印到屏幕上,然后在看到输出正常后添加它。
正则表达式
s/(.*?xsl.*?)xsl/$1xslt/;
您搜索包含“xsl”的字符串两次。的用法.*?
在第二种情况下很好,但在第一种情况下不好。每当您发现自己使用通配符字符串开始正则表达式时,您可能做错了什么。除非你试图捕捉那部分。
但是,在这种情况下,您将其捕获并删除它,然后将其放回原处,这完全没有用。因此,首要任务是将那部分去掉:
s/(xsl.*?)xsl/$1xslt/;
现在,移除某些东西并将其放回去实际上只是一个根本不移除它的魔术。我们不需要这样的魔术,因为我们一开始就不能删除它。使用环视断言,您可以实现这一点。
在这种情况下,由于你有一个可变长度的表达式并且需要一个后向断言,我们必须使用\K
(mnemonic: Keep) 选项,因为没有实现可变长度的后视。
s/xsl.*?\Kxsl/xslt/;
所以,由于我们没有取出任何东西,我们不需要使用$1
. 现在,您可能会注意到,“嘿,如果我将 'xsl' 替换为 'xslt',我根本不需要删除 'xsl'。” 这是真的:
s/xsl.*?xsl\K/t/;
您可以考虑为此正则表达式使用选项,例如/i
,这会导致它忽略大小写,从而也匹配诸如“XSL FOO XSL”之类的字符串。或者/g
允许它每行执行所有可能匹配的选项,而不仅仅是第一个匹配。在perlop中阅读更多内容。
结论
完成的单线是:
perl -pi.bak -e 's/xsl.*?xsl\K/t/' file.txt