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 cccuse 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 регулярных выражений, доступна только для чтения. Вот так вот пишут все авторы... Салаги!