Perl регулярные выражения: замена в ini-файлах для новой версии программы

Сегодня утром соскочил с печки и попал ногами прямо в валенки. Сбегал в уборную, выгнал корову пастись, а сам к компу и думаю: о чём бы это статейку накропать? Подпёр голову обеими руками, думаю, значит. И тут вспомнилось, как один программист-шареварщик писал в конференции Shareware Russia о трудностях при переходе на новую версию программы. Когда внесены исправления в сам код, надо поменять в ini-файлах и документации старую версию программы на новую, а потом создать дистрибутив для закачивания его к себе на сайт. Некоторые шареварщики используют Perl с его регулярными выражениями, чтобы во всех файлах перейти на новую версию программы и вообще сделать новый дистрибутив автоматически, нажатием клавиши.

Вот здесь и возникает задача, которую абстрактно можно сформулировать так: написать оператор подстановки s///, который в тексте во всех строках, где не встречается подстрока aaa, заменит все подстроки bbb на ccc. В общем случае эти образцы подстрок находятся в переменных $a='aaa', $b='bbb' и $c='ccc'. Грубо говоря, надо во всех строках, где нет $a, заменить $b на $c. Тот программист не смог красиво, одним оператором s/// решить эту задачку.

Например, имеем фрагмент ini-файла:
bbb aaa bbb
bbb aa bbb bbb
aaa bbb bbb
aa bbb bbb

На выходе должно получиться:
bbb aaa bbb
ccc aa ccc ccc
aaa bbb bbb
aa ccc ccc

Когда я решил эту задачу, то на некоторых соответствующих форумах предложил её в качестве упражнения на тренировку мышц, которые шевелят извилинами. Народ ответил, что это можно сделать и не одним оператором s///, а в цикле по строкам, а кто-то предложил вариант такого оператора подстановки, который работал только, если в строках aaa встречалось после bbb. Идея была такой: если выражение (bbb)(?=.*?aaa) возвращает истинный результат, то надо $1 заменить на ccc. Но сложность здесь в том, что aaa может встретиться и раньше bbb, тогда замену делать нельзя, а здесь она производилась.

Для того, чтобы зафиксировать факт, что при встрече bbb мы в этой же строке уже встречали aaa, надо его где-то запомнить, и конечно, это будет переменная-флаг (а где же ещё?), ведь Perl регулярные выражения не позволяют заглядывать назад на неопределённое число символов. Тогда в начале каждой строки, т.е. при встрече ^ (с модификатором m), надо установить эту переменную в 1 (т.е. мы пока не встретили подстроку aaa и пока считаем, что замену производить надо). При встрече bbb, после которого где-то в этой строке имеется aaa, мы эту переменную сбросим в 0, также мы должны сбросить её в 0, если наткнёмся в этой же строке на aaa. Но как тогда быть с захватом того, что нам надо заменять? Придётся захватывать всё подряд, т.е. и aaa и bbb, а потом разбираться с ними. В секции замены оператора s///, если окажется, что $1 eq $a, то мы в качестве замены подставим $a, т.к. замену-то всё равно надо делать, даже, если не хочется, вот мы и заменим $a само на себя, чтобы не испортить строку ini-файла. А если окажется, что $1 eq $b, то при значении флага, равного 1, заменим $1 на $c (т.к. в данной строке нет $a), а иначе на $b (опять вернём то же значение, чтобы не испортить файл).

Для того, чтобы искать сразу начало строки ^, фрагмент bbb(?=.*?aaa) (т.е. bbb, после которого есть aaa), и aaa, используем альтернативные шаблоны Perl регулярных выражений: (шаблон1|шаблон2|шаблон3). Вот сама программка:

#!/usr/bin/perl -w
use strict;
use re 'eval';


# В тексте во всех строках, где не встречается строка из $a, заменить содержимое $b на содержимое $c
# Автор Сергей Мельников. Сделано для сайта "Perl регулярные выражения, статьи вебмастеру"
# http://www.cronc.com/ru.shtml

my ($a,$b,$c)=('aaa','bbb','ccc');
$_=<<EOD;
bbb aaa bbb
bbb aa bbb bbb
aaa bbb bbb
aa bbb bbb
EOD

s/(^(?{$^R=1})|
    \Q$b\E(?=.*?\Q$a\E(?{$^R=0}))?|
    \Q$a\E(?{$^R=0}))
 /
  if ($1 eq $a) { $a }
   elsif ($1 eq $b) { $^R ? $c : $b }
 /egmx;
print $_;

На выходе получаем верный результат:

 bbb aaa bbb
 ccc aa ccc ccc
 aaa bbb bbb
 aa ccc ccc
use re 'eval'; надо использовать, чтобы не получить сообщение
 Eval-group not allowed at runtime, use re 'eval' in regex m/(^(?{$^R=1})| и т.д.

Обратите внимание, что значения переменных внутри регулярного выражения берутся в метасимволы \Q...\E, чтобы содержимое переменных подставлялось как литерал, а не как часть регулярного выражения. Если внутри этих переменных вдруг попадутся последовательности символов, которые являются метасимволами Perl регулярных выражений, то они корректно замаскируются символом \.

А теперь для полноты счастья заодно решим и обратную задачку: в тексте во всех строках, где встречается строка из $a, заменить содержимое $b на содержимое $c:

#!/usr/bin/perl -w
use strict;
use re 'eval';

# В тексте во всех строках, где встречается строка из $a, заменить содержимое $b на содержимое $c
# Автор Сергей Мельников. Сделано для сайта "Perl регулярные выражения, статьи вебмастеру"
# http://www.cronc.com/ru.shtml

my ($a,$b,$c)=('aaa','bbb','ccc');
$_=<<EOD;
aa bbb
bbbbbbb aaaaaa bbbbbbbbbbbbb
aaaaaa bbbbbbbbbbbbbbb 
bbbbbbbbbbb aaaaaa
EOD

s/(^(?{$^R=0})|
    \Q$b\E(?=.*?\Q$a\E(?{$^R=1}))?|
    \Q$a\E(?{$^R=1}))
 /
  if ($1 eq $a) { $a }
   elsif ($1 eq $b) { $^R ? $c : $b }
 /egmx;
print $_;

На печать опять выходит верный результат:

 aa bbb
 ccccccb aaaaaa ccccccccccccb
 aaaaaa ccccccccccccccc
 cccccccccbb aaaaaa

У читателя может возникнуть вопрос: а что это за переменная $^R, которая используется как флаг для хранения состояния? Эта переменная хранит результат последнего по времени выполнния встроенного кода Perl (?{ код }), который не находится внутри условной конструкции (? if then[| else]). У нас встроенный код, это (?{$^R=0}) и (?{$^R=1}), Значит, механизм Perl регулярных выражений просто продублирует это присваивание. Вместо этой специальной переменной можно использовать и обычную собственную переменную, а можно было бы, исходя из только что сказанного, просто написать (?{0}) и (?{1}). Я это сделал для наглядности и чтобы оператор s/// обходился без объявления переменных.

Во всех книгах и пособиях по Perl и Perl регулярным выражениям авторы пишут, что переменная $^R, как и все специальные переменные Perl регулярных выражений, доступна только для чтения. Вот так вот пишут все авторы... Салаги! Perl регулярные выражения