Этот perl-скрипт не заменяет ввод text_str, как ожидалось:
my $text_str ='public class ZipUtilTest extends TestCase {}';
my $find = '^(public class \\w+) extends TestCase \\{';
my $replace = '@RunWith(JUnit4.class)\\n\\1 {';
eval '$text_str =~ ' . "s#$find#$replace#mg";
say "$text_str";
Вывод (неправильный):
(JUnit4.class)
public class ZipUtilTest {}
Этот пересмотренный сценарий Perl (с экранированным символом @ в замене) заменяет, как и ожидалось:
my $text_str ='public class ZipUtilTest extends TestCase {}';
my $find = '^(public class \\w+) extends TestCase \\{';
my $replace = '@RunWith(JUnit4.class)\\n\\1 {';
$replace =~ s/@/\\@/g; # Escape '@' to avoid Perl @var interpolation
eval '$text_str =~ ' . "s#$find#$replace#mg";
say "$text_str";
Выход (правильный):
@RunWith(JUnit4.class)
public class ZipUtilTest {}
Похоже, что «@RunWith» в шаблоне «заменить» обрабатывается как @переменная Perl и интерполируется в пустую строку.
Есть ли лучший способ справиться с этим, чем экранирование символа «@» в шаблонах? Если нам нужно сделать это, нужно ли экранировать любые другие символы, подобные '@'?
(Примечание: это не имеет ничего общего с использованием \Q\E для подавления магии метасимволов регулярных выражений. Пожалуйста, не закрывайте это из-за существующих вопросов такого рода.)
Какова ваша конечная цель здесь? Вы пытаетесь поддерживать строки замены, которые могут содержать не только специальные символы, такие как символы новой строки, но и escape-последовательности, такие как \n, но не переменные? Надежны ли эти строки замены?
@амон. Это верно. Строки замены являются допустимыми шаблонами регулярных выражений, а «@» — это обычный символ, и его не следует рассматривать как переменную Perl.
Вот вариант, который работает: используйте $1 непосредственно в замещающей стороне, а не в заранее созданной для нее переменной. Это избавляет нас от некоторых хлопот.
use warnings;
use strict;
use feature 'say';
my $text_str = 'public class ZipUtilTest extends TestCase {}';
#say $text_str;
my $re = '^(public class \w+) extends TestCase \{';
#say $re;
my $replace = "\@RunWith(JUnit4.class)\n";
#say $replace;
$text_str =~ s/$re/${replace}$1 {/;
say $text_str;
Обновление с комментариями
Переменные для шаблона и строки замены считываются из файла конфигурации. Тогда «хлопоты», о которых я упоминаю, становятся более серьезными.
Если $1 должен быть подготовлен в переменной строки замены, это должна быть простая строка (символов $ и 1), в то время как она должна стать переменной и быть оценена в регулярном выражении.
Это означает, что переменная должна быть eval-ed (или регулярное выражение должно запускаться с /ee), и это проблема со строковой формой eval -- ввод извне: eval будет оценивать (запускать) что угодно, любой код. Нам не нужны злонамеренные действия в отношении текста, чтобы стать кодом в файлах конфигурации, просто учтите опечатки.
Что касается красивого экранирования (только) того, что нужно экранировать, к этому можно подготовиться, например, хэш:
my %esc_char = ( at => '\@' ); # etc
И используйте это при составлении переменной со строкой замены.
Если и шаблон, и замена должны исходить из файлов конфигурации и не должны быть специфичными для Perl, как говорится в комментарии, то я не уверен, как улучшить код, предложенный в вопросе. За исключением того, что он должен быть сильно защищен от запуска (скажем, случайно) плохого кода.
Если все должно быть упаковано в $replace, включите $1 и {, дайте мне знать
Это работает. Мне интересно, есть ли лучший способ. Попытка избежать поиска ответов на вопрос «Если мы экранируем '@', какие еще символы нам нужно экранировать?» Шаблоны «найти» и «заменить» загружаются из файла конфигурации в реальном сценарии и могут быть любыми допустимыми шаблонами регулярных выражений.
@JoeSmith Дело не в @, с этим относительно легко справиться - дело в этом $1! Потому что мы хотим быть переменной в замене стороны, с тем, что было захвачено. Но если вы ставите его заранее (в $replace), то в это время он не может быть переменной. Тогда вам нужно перейти к eval (или /ee), и это сразу намного хуже. Итак... можно ли ввести $1 в самом регулярном выражении? Или это тоже должно быть в строке замены, загруженной из файла конфигурации?
Чтобы избежать путаницы, в строке замены "\1" совпадает с "$1". Оба работают (я знаю, что perl предпочитает "$1" вместо "\1"). Он должен быть частью строки замены.
@JoeSmith Что касается обработки @, можно подготовить список экранированных символов (например, my %esc_repl = ( at => '\@' ); и т. д.) и использовать его при составлении строк замены. А все остальное остается нераскрытым.
«Я знаю, что perl предпочитает $1» — верно, и уже много лет он сильно предпочитает его. Я бы посоветовал всегда использовать это. Старый \1 теперь выглядит слишком близко к другим вещам
Файл конфигурации, содержащий шаблоны регулярных выражений «найти» и «заменить», не зависит от Perl. («\1» — допустимая ссылка на совпадающие группы вне Perl). Не знакомый с "=>", можете ли вы более подробно рассказать о том, как обновить рассматриваемый код, чтобы использовать то, что вы предложили?
@ДжоСмит О. Если эта конфигурация должна использоваться языками, отличными от Perl, то это другое дело. Вы все равно можете обработать строку замены, как только она будет прочитана в Perl, чтобы экранировать @ (и что угодно еще), как вы это делаете. Но если вы должны остаться с формой \1, то проблема в том, что в Perl используется устаревшая форма. Если это должно использоваться другими языками (так ли это?), то я не вижу, как это улучшить. Какие языки? \\n и \\1 действительно в порядке? Весь этот дизайн кажется потенциально шатким, что произвольные языки должны иметь возможность использовать одно и то же нетривиальное регулярное выражение?
@JoeSmith «как обновить рассматриваемый код» - если они (шаблон + замена) должны быть полностью установлены в файлах конфигурации, которые не должны быть специфичными для Perl, то это должно быть указано в вопросе. Я определенно хотел бы знать, какие другие языки могут быть связаны с ними.
Файл конфигурации — это обычный файл json, который определяет применяемые преобразования. На самом деле я начал со сценария Apple. Позже нужно будет использовать его в Linux и, таким образом, портировать на Perl (поскольку это единственный язык, который имеет механизм регулярных выражений, столь же мощный, как у сценария Apple). Сам сценарий имеет версию Perl и версию сценария Apple. Файлы конфигурации создаются пользователями для своих задач, таких как перенос тестов JUnit 3 на JUnit 4, перенос тестов EasyMock на Mockito. Реальность такова, что большинство пользователей знают регулярное выражение, но не обязательно знают Perl.
@JoeSmith Хорошо, спасибо за объяснение. Я не уверен, можно ли ожидать надежной системы, которая принимает в основном произвольное регулярное выражение, которое не зависит от языка и хорошо работает в Perl. Реализации регулярных выражений действительно различаются - если эти пользователи «знают регулярное выражение» ... какой вариант они знают? Я подумаю еще о возможно более общем способе
Это справедливое заявление. Извините, что вопрос может быть неуместным из-за моего плохого знания языка Perl. Большое спасибо за помощь.
Вы можете использовать положительный прогноз, чтобы сопоставить {, не фиксируя его в 1 долларе. Тогда строка замены не обязательно должна содержать $1.
При построении регулярного выражения лучше использовать оператор заключения в кавычки регулярного выражения qr{}, чем строки; он будет цитироваться как регулярное выражение, а не как строка. Это может избежать тонких ошибок.
use v5.10;
my $text_str = 'public class ZipUtilTest extends TestCase {}';
# Use a positive-look ahead to match, but not capture, the {
# Quote as regex to avoid subtle quoting issues.
my $find = qr'^(public class \w+) extends TestCase(?>\s*\{)';
# Use double-quotes to interpolate the \n, but escape the \@.
my $replace = "\@RunWith(JUnit4.class)\n";
# Add the $1 to the end of the replacement.
$text_str =~ s{$find}{$replace$1};
say $text_str;
Демонстрация.
Спасибо. Это действительно решило проблему образца в вопросе. Я искал более общее решение (применяется к любому шаблону замены регулярного выражения). С другой стороны, я понимаю, что это может быть нереально. Узнал некоторые трюки из вашего кода. Ценится.
Кажется, вы хотите загрузить не зависящий от языка поиск и заменить шаблоны из файла конфигурации, а затем применить их через сценарий Perl.
Если это ваша цель, то использование eval не подходит, так как Perl имеет синтаксис, который вы не хотите поддерживать, как вы узнали.
Неразумно пытаться обойти эти специфичные для Perl части, пытаясь избежать их, так как это может стать довольно сложным. Например, вы рассматривали экранирование вхождений @, поскольку они могут представлять имя массива, но что, если этот символ уже экранирован обратной косой чертой? Чтобы справиться с этим должным образом, потребовалась бы почти полная повторная реализация синтаксиса строковых литералов Perl, что звучит не очень весело.
Что бы я сделал, так это определил собственный синтаксис строки замены, чтобы мы были полностью независимы от синтаксиса Perl.
Например, мы можем определить синтаксис замещающей строки как полностью дословный, за исключением того, что мы поддерживаем определенные символы обратной косой черты. Предположим, что синтаксис '\' DIGIT, такой как \1, заменяет захват, и что поддерживаются обычные переходы обратной реакции (\b \t \n \v \f \r \" \' \\ \x0A), которые являются общим подмножеством строковых литералов JavaScript , строковых литералов Python 3 и Perl. побеги, минус восьмеричные побеги. Обратите внимание, что эти языки не согласны с синтаксисом символов Unicode.
Мы можем реализовать интерпретатор для этого языка замены строк следующим образом: мы разбираем строку замены в массив опкодов, чередуя литеральную строку с номером захвата. Например, шаблон замены abc\1def будет разобран на ['abc', 1, 'def']:
sub parse_replacement_pattern {
my ($pattern) = @_;
my @ops = (''); # init with empty string
# use m//gc style parsing which lets us anchor patterns at the current "pos"
pos($pattern) = 0;
while (pos $pattern < length $pattern) {
if ($pattern =~ /\G([^\\]+)/gc) {
$ops[-1] .= $1;
}
elsif ($pattern =~ /\G\\n/gc) {
$ops[-1] .= "\n";
}
... # and so on for the basic escapes
elsif ($pattern =~ /\G\\x([0-9a-fA-F]{2})/gc) {
$ops[-1] .= chr $1;
}
elsif ($pattern =~ /\G\\([1-9])/gc) {
push @ops, $1, ''; # add replacement opcode + empty string
}
else {
die "invalid syntax";
}
}
return \@ops;
}
Мы можем применить такой шаблон замены, прокручивая операции, добавляя литеральную строку или содержимое захвата по мере необходимости.
sub apply_replacement_pattern {
my ($ops) = @_;
my $output = '';
my $is_capture = 0;
for my $op (@$ops) {
if ($is_capture) {
# we know that $op must be the number of a capture buffer
$output .= ${^CAPTURE}[$op - 1]; # like eval "\$$op"
}
else {
# we know that $op must be a literal string
$output .= $op;
}
$is_capture = !$is_capture;
}
return $output;
}
Теперь мы можем использовать эти функции в вашем тестовом примере:
my $text_str ='public class ZipUtilTest extends TestCase {}';
my $find = '^(public class \\w+) extends TestCase \\{';
my $replace = '@RunWith(JUnit4.class)\\n\\1 {';
my $replace_ops = parse_replacement_pattern($replace);
$text_str =~ s{$find}{apply_replacement_pattern($replace_ops)}mge;
say $text_str;
Это дает ожидаемый результат
@RunWith(JUnit4.class)
public class ZipUtilTest {}
Спасибо за указание «что, если этот символ уже экранирован обратной косой чертой?» Я понял, что то, что я хочу, вероятно, неосуществимо. Удивительно, что вы попытались решить проблему, которая кажется невозможной, и приготовили решение. Я рад принять это как честный ответ. Я очень уважаю вас.
TL;DR
\@RunWith(JUnit4.class)\\n\\1