Perl 5:如何在正则表达式模式中禁止@var插值

iyr7buue  于 2023-02-16  发布在  Perl
关注(0)|答案(3)|浏览(125)

这个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脚本(在“replace”中使用“@”转义)按预期进行替换:

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 {}

看起来“replace”模式中的“@RunWith”被视为Perl @变量,并被插值为空字符串。
有没有比转义模式中的“@”字符更好的方法来处理这个问题?如果我们必须这样做,其他类似“@”的字符需要转义吗?
(Note:这与使用\Q\E来抑制regex meta字符的魔力无关。请不要因为存在这种性质的问题而关闭它。)

myss37ts

myss37ts1#

您可以使用正前瞻来匹配{,而不用在$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;

Demonstration .

omvjsjqw

omvjsjqw2#

您似乎希望从配置文件中加载与语言无关的搜索和替换模式,然后通过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 {}
p8h8hvxi

p8h8hvxi3#

下面是一个有效的版本:直接在替换端使用$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-艾德(或者用/ee运行regex),这就是字符串形式的eval-- input从外部输入的问题:eval将评估(运行)任何东西,* 任何 * 代码。我们不需要在配置文件中的文本成为代码的恶意行为,只是考虑打字错误。
至于很好地转义(仅)需要转义的内容,我们可以为此做好准备,例如散列:

my %esc_char = ( at => '\@' );  # etc

并在用替换字符串组合变量时使用它。
如果模式和替换都必须来自配置文件 * 并且 * 必须不是Perl特有的,正如一条评论所说,那么我不确定如何改进问题中提供的代码,除非它应该受到严格的保护以防止运行(意外地)坏代码。

相关问题