Perl 5: Как подавить интерполяцию @var в шаблонах регулярных выражений

Этот 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 для подавления магии метасимволов регулярных выражений. Пожалуйста, не закрывайте это из-за существующих вопросов такого рода.)

TL;DR\@RunWith(JUnit4.class)\\n\\1
Gilles Quénot 14.02.2023 19:18

Какова ваша конечная цель здесь? Вы пытаетесь поддерживать строки замены, которые могут содержать не только специальные символы, такие как символы новой строки, но и escape-последовательности, такие как \n, но не переменные? Надежны ли эти строки замены?

amon 14.02.2023 19:43

@амон. Это верно. Строки замены являются допустимыми шаблонами регулярных выражений, а «@» — это обычный символ, и его не следует рассматривать как переменную Perl.

Joe Smith 14.02.2023 20:21
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Веб-скрейпинг, как мы все знаем, это дисциплина, которая развивается с течением времени. Появляются все более сложные средства борьбы с ботами, а...
Калькулятор CGPA 12 для семестра
Калькулятор CGPA 12 для семестра
Чтобы запустить этот код и рассчитать CGPA, необходимо сохранить код как HTML-файл, а затем открыть его в веб-браузере. Для этого выполните следующие...
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
О тренинге HTML JavaScript :HTML (язык гипертекстовой разметки) и CSS (каскадные таблицы стилей) - две основные технологии для создания веб-страниц....
Как собрать/развернуть часть вашего приложения Angular
Как собрать/развернуть часть вашего приложения Angular
Вам когда-нибудь требовалось собрать/развернуть только часть вашего приложения Angular или, возможно, скрыть некоторые маршруты в определенных средах?
Запуск PHP на IIS без использования программы установки веб-платформы
Запуск PHP на IIS без использования программы установки веб-платформы
Установщик веб-платформы, предлагаемый компанией Microsoft, перестанет работать 31 декабря 2022 года. Его закрытие привело к тому, что мы не можем...
Оптимизация React Context шаг за шагом в 4 примерах
Оптимизация React Context шаг за шагом в 4 примерах
При использовании компонентов React в сочетании с Context вы можете оптимизировать рендеринг, обернув ваш компонент React в React.memo сразу после...
2
3
76
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вот вариант, который работает: используйте $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 и {, дайте мне знать

zdim 14.02.2023 20:20

Это работает. Мне интересно, есть ли лучший способ. Попытка избежать поиска ответов на вопрос «Если мы экранируем '@', какие еще символы нам нужно экранировать?» Шаблоны «найти» и «заменить» загружаются из файла конфигурации в реальном сценарии и могут быть любыми допустимыми шаблонами регулярных выражений.

Joe Smith 14.02.2023 20:26

@JoeSmith Дело не в @, с этим относительно легко справиться - дело в этом $1! Потому что мы хотим быть переменной в замене стороны, с тем, что было захвачено. Но если вы ставите его заранее (в $replace), то в это время он не может быть переменной. Тогда вам нужно перейти к eval (или /ee), и это сразу намного хуже. Итак... можно ли ввести $1 в самом регулярном выражении? Или это тоже должно быть в строке замены, загруженной из файла конфигурации?

zdim 14.02.2023 20:29

Чтобы избежать путаницы, в строке замены "\1" совпадает с "$1". Оба работают (я знаю, что perl предпочитает "$1" вместо "\1"). Он должен быть частью строки замены.

Joe Smith 14.02.2023 20:32

@JoeSmith Что касается обработки @, можно подготовить список экранированных символов (например, my %esc_repl = ( at => '\@' ); и т. д.) и использовать его при составлении строк замены. А все остальное остается нераскрытым.

zdim 14.02.2023 20:32

«Я знаю, что perl предпочитает $1» — верно, и уже много лет он сильно предпочитает его. Я бы посоветовал всегда использовать это. Старый \1 теперь выглядит слишком близко к другим вещам

zdim 14.02.2023 20:33

Файл конфигурации, содержащий шаблоны регулярных выражений «найти» и «заменить», не зависит от Perl. («\1» — допустимая ссылка на совпадающие группы вне Perl). Не знакомый с "=>", можете ли вы более подробно рассказать о том, как обновить рассматриваемый код, чтобы использовать то, что вы предложили?

Joe Smith 14.02.2023 20:49

@ДжоСмит О. Если эта конфигурация должна использоваться языками, отличными от Perl, то это другое дело. Вы все равно можете обработать строку замены, как только она будет прочитана в Perl, чтобы экранировать @ (и что угодно еще), как вы это делаете. Но если вы должны остаться с формой \1, то проблема в том, что в Perl используется устаревшая форма. Если это должно использоваться другими языками (так ли это?), то я не вижу, как это улучшить. Какие языки? \\n и \\1 действительно в порядке? Весь этот дизайн кажется потенциально шатким, что произвольные языки должны иметь возможность использовать одно и то же нетривиальное регулярное выражение?

zdim 14.02.2023 21:13

@JoeSmith «как обновить рассматриваемый код» - если они (шаблон + замена) должны быть полностью установлены в файлах конфигурации, которые не должны быть специфичными для Perl, то это должно быть указано в вопросе. Я определенно хотел бы знать, какие другие языки могут быть связаны с ними.

zdim 14.02.2023 21:14

Файл конфигурации — это обычный файл json, который определяет применяемые преобразования. На самом деле я начал со сценария Apple. Позже нужно будет использовать его в Linux и, таким образом, портировать на Perl (поскольку это единственный язык, который имеет механизм регулярных выражений, столь же мощный, как у сценария Apple). Сам сценарий имеет версию Perl и версию сценария Apple. Файлы конфигурации создаются пользователями для своих задач, таких как перенос тестов JUnit 3 на JUnit 4, перенос тестов EasyMock на Mockito. Реальность такова, что большинство пользователей знают регулярное выражение, но не обязательно знают Perl.

Joe Smith 15.02.2023 05:22

@JoeSmith Хорошо, спасибо за объяснение. Я не уверен, можно ли ожидать надежной системы, которая принимает в основном произвольное регулярное выражение, которое не зависит от языка и хорошо работает в Perl. Реализации регулярных выражений действительно различаются - если эти пользователи «знают регулярное выражение» ... какой вариант они знают? Я подумаю еще о возможно более общем способе

zdim 15.02.2023 21:59

Это справедливое заявление. Извините, что вопрос может быть неуместным из-за моего плохого знания языка Perl. Большое спасибо за помощь.

Joe Smith 15.02.2023 23:48

Вы можете использовать положительный прогноз, чтобы сопоставить {, не фиксируя его в 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;

Демонстрация.

Спасибо. Это действительно решило проблему образца в вопросе. Я искал более общее решение (применяется к любому шаблону замены регулярного выражения). С другой стороны, я понимаю, что это может быть нереально. Узнал некоторые трюки из вашего кода. Ценится.

Joe Smith 15.02.2023 23:45
Ответ принят как подходящий

Кажется, вы хотите загрузить не зависящий от языка поиск и заменить шаблоны из файла конфигурации, а затем применить их через сценарий 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 {}

Спасибо за указание «что, если этот символ уже экранирован обратной косой чертой?» Я понял, что то, что я хочу, вероятно, неосуществимо. Удивительно, что вы попытались решить проблему, которая кажется невозможной, и приготовили решение. Я рад принять это как честный ответ. Я очень уважаю вас.

Joe Smith 15.02.2023 23:42

Другие вопросы по теме