12

我一直在尝试编写一个 Perl 脚本来替换我项目的所有源文件中的一些文本。我需要类似的东西:

perl -p -i.bak -e "s/thisgoesout/thisgoesin/gi" *.{cs,aspx,ascx}

但这会递归地解析目录的所有文件。

我刚刚开始了一个脚本:

use File::Find::Rule;
use strict;

my @files = (File::Find::Rule->file()->name('*.cs','*.aspx','*.ascx')->in('.'));

foreach my $f (@files){
    if ($f =~ s/thisgoesout/thisgoesin/gi) {
           # In-place file editing, or something like that
    }
}

但现在我被困住了。有没有一种简单的方法可以使用 Perl 编辑所有文件?

请注意,我不需要保留每个修改文件的副本;我把他们都颠覆了=)

更新:我在Cygwin上试过这个,

perl -p -i.bak -e "s/thisgoesout/thisgoesin/gi" {*,*/*,*/*/*}.{cs,aspx,ascx

但看起来我的参数列表已爆炸到允许的最大大小。事实上,我在 Cygwin 上遇到了非常奇怪的错误......

4

6 回答 6

13

如果您@ARGV在使用之前分配*ARGV(又名菱形<>),$^I/-i将处理这些文件而不是命令行上指定的文件。

use File::Find::Rule;
use strict;

@ARGV = (File::Find::Rule->file()->name('*.cs', '*.aspx', '*.ascx')->in('.'));
$^I = '.bak';  # or set `-i` in the #! line or on the command-line

while (<>) {
    s/thisgoesout/thisgoesin/gi;
    print;
}

这应该完全符合您的要求。

如果您的模式可以跨越多行,请undef $/;在 the 之前添加一个,<>以便 Perl 一次对整个文件进行操作,而不是逐行操作。

于 2008-10-29T23:46:25.967 回答
7

您可能对File::Transaction::AtomicFile::Transaction感兴趣

F::T::A 的 SYNOPSIS 看起来与您尝试执行的操作非常相似:

  # In this example, we wish to replace 
  # the word 'foo' with the word 'bar' in several files, 
  # with no risk of ending up with the replacement done 
  # in some files but not in others.

  use File::Transaction::Atomic;

  my $ft = File::Transaction::Atomic->new;

  eval {
      foreach my $file (@list_of_file_names) {
          $ft->linewise_rewrite($file, sub {
               s#\bfoo\b#bar#g;
          });
      }
  };

  if ($@) {
      $ft->revert;
      die "update aborted: $@";
  }
  else {
      $ft->commit;
  }

再加上 File::Find 你已经写好了,你应该很高兴。

于 2008-10-29T23:19:24.273 回答
6

您可以使用 Tie::File 可扩展地访问大文件并就地更改它们。请参阅联机帮助页(man 3perl Tie::File)。

于 2008-10-29T23:28:14.053 回答
4

改变

foreach my $f (@files){
    if ($f =~ s/thisgoesout/thisgoesin/gi) {
           #inplace file editing, or something like that
    }
}

foreach my $f (@files){
    open my $in, '<', $f;
    open my $out, '>', "$f.out";
    while (my $line = <$in>){
        chomp $line;
        $line =~ s/thisgoesout/thisgoesin/gi
        print $out "$line\n";
    }
}

这假设该模式不跨越多行。如果模式可能跨行,您需要在文件内容中啜饮。(“slurp”是一个非常常见的 Perl 术语)。

chomp 实际上并不是必需的,我只是被那些没有被chomped 一次的线条咬了太多次(如果你放弃chomp,更改print $out "$line\n";print $out $line;)。

同样,您可以更改open my $out, '>', "$f.out";open my $out, '>', undef;打开一个临时文件,然后在替换完成后将该文件复制回原始文件。事实上,特别是如果你在整个文件中啜饮,你可以简单地在内存中进行替换,然后覆盖原始文件。但是我犯了足够多的错误,我总是写入一个新文件,并验证内容。


请注意,我最初在该代码中有一个 if 语句。那很可能是错误的。那只会复制与正则表达式“thisgoesout”匹配的行(当然用“thisgoesin”替换),同时默默地吞噬其余部分。

于 2008-10-29T23:19:08.313 回答
2

你可以使用find

find . -name '*.{cs,aspx,ascx}' | xargs perl -p -i.bak -e "s/thisgoesout/thisgoesin/gi"

这将递归列出所有文件名,然后xargs将读取其标准输入并运行命令行的其余部分,并在末尾附加文件名。一件好事xargs是,如果它构建的命令行太长而无法一次性运行,它将多次运行命令行。

请注意,我不确定是否find完全理解选择文件的所有 shell 方法,所以如果上述方法不起作用,那么也许尝试:

find . | grep -E '(cs|aspx|ascx)$' | xargs ...

当使用这样的管道时,我喜欢在继续之前构建命令行并单独运行每个部分,以确保每个程序都能获得它想要的输入。因此,您无需xargs先检查即可运行该部件。

我突然想到,尽管您没有这么说,但由于您要查找的文件后缀,您可能在 Windows 上。在这种情况下,可以使用 Cygwin 运行上述管道。可以编写一个 Perl 脚本来做同样的事情,就像您开始做的那样,但是您必须自己进行就地编辑,因为在这种情况下您无法利用-i开关。

于 2008-10-29T22:35:36.093 回答
1

感谢 ehemient on this question 和 on this answer,我得到了这个:

use File::Find::Rule;
use strict;

sub ReplaceText {
    my $regex = shift;
    my $replace = shift;

    @ARGV = (File::Find::Rule->file()->name('*.cs','*.aspx','*.ascx')->in('.'));
    $^I = '.bak';
    while (<>) {
        s/$regex/$replace->()/gie;
        print;
    }
}

ReplaceText qr/some(crazy)regexp/, sub { "some $1 text" };

现在我什至可以遍历包含 regexp=>subs 条目的散列!

于 2008-10-30T23:30:56.573 回答