Есть ли простой способ сделать замену текста файла на месте? - PullRequest
12 голосов
/ 30 октября 2008

Я пытался закодировать 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 ...

Ответы [ 6 ]

13 голосов
/ 30 октября 2008

Если назначить @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 $/; перед <>, чтобы Perl обрабатывал весь файл за раз вместо построчной.

7 голосов
/ 30 октября 2008

Вас может заинтересовать Файл :: Транзакция :: Атомная или Файл :: Транзакция

ОПИСАНИЕ для F :: T :: A очень похоже на то, что вы пытаетесь сделать:

  # 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, который вы уже написали, и вам будет хорошо.

6 голосов
/ 30 октября 2008

Вы можете использовать Tie :: File для масштабируемого доступа к большим файлам и их изменения на месте. Смотрите man-страницу (man 3perl Tie :: File).

4 голосов
/ 30 октября 2008

Изменение

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 отредактированы один раз (если вы уроните chomp, измените print $out "$line\n"; на print $out $line;).

Аналогично, вы можете изменить open my $out, '>', "$f.out"; на open my $out, '>', undef;, чтобы открыть временный файл, а затем скопировать этот файл поверх оригинала после завершения замены. На самом деле, и особенно, если вы выплескиваете весь файл, вы можете просто произвести подстановку в памяти, а затем записать поверх исходного файла. Но я сделал достаточно ошибок, делая то, что всегда записываю в новый файл и проверяю содержимое.


Примечание , у меня изначально был оператор if в этом коде. Это было, скорее всего, неправильно. Это скопировало бы только те строки, которые соответствовали регулярному выражению "thisgoesout" (заменив его, конечно, "thisgoesin"), в то же время молча поглощая остальные.

2 голосов
/ 30 октября 2008

Вы можете использовать find:

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

Это будет рекурсивно перечислять все имена файлов, затем xargs прочитает его стандартный ввод и запустит оставшуюся часть командной строки с именами файлов, добавленными в конце. Одна приятная вещь в xargs заключается в том, что она будет запускать командную строку более одного раза, если создаваемая командная строка становится слишком длинной для запуска за один раз.

Обратите внимание, что я не уверен, что find полностью понимает все методы оболочки для выбора файлов, поэтому, если приведенное выше не работает, возможно, попробуйте:

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

При использовании подобных конвейеров мне нравится создавать командную строку и запускать каждую часть отдельно, прежде чем продолжить, чтобы убедиться, что каждая программа получает требуемый ввод. Таким образом, вы можете запустить часть без xargs, чтобы проверить ее.

Мне просто пришло в голову, что, хотя вы этого не сказали, вы, вероятно, работаете в Windows из-за искомых суффиксов файлов. В этом случае вышеуказанный конвейер может быть запущен с использованием Cygwin. Можно написать сценарий Perl, чтобы сделать то же самое, что вы начали делать, но вам придется выполнять редактирование на месте самостоятельно, потому что вы не можете воспользоваться переключателем -i в этой ситуации.

1 голос
/ 31 октября 2008

Благодаря ответу на этот вопрос и на этот ответ , я получил это:

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!

...