使用 Perl 正则表达式捕获 C 风格代码块前后的文本

Capturing text before and after a C-style code block with a Perl regular expression

我正在尝试使用 Perl 正则表达式捕获 C 样式代码块前后的一些文本。到目前为止,这就是我所拥有的:

use strict;
use warnings;

my $text = << "END";
int max(int x, int y)
{
    if (x > y)
    {
        return x;
    }
    else
    {
        return y;
    }
}
// more stuff to capture
END

# Regex to match a code block
my $code_block = qr/(?&block)
(?(DEFINE)
    (?<block>
        \{                # Match opening brace
            (?:           # Start non-capturing group
                [^{}]++   #     Match non-brace characters without backtracking
                |         #     or
                (?&block) #     Recursively match the last captured group
            )*            # Match 0 or more times
        \}                # Match closing brace
    )
)/x;

#  ends up undefined after the match
if ($text =~ m/(.+?)$code_block(.+)/s){
    print ;
    print ;
}

我遇到了第二个捕获组在匹配后未初始化的问题。在 DEFINE 块之后没有办法继续正则表达式吗?我认为这应该可以正常工作。

应该包含代码块下方的注释,但它没有,而且我找不到一个很好的理由说明它不起作用。

你非常接近。

(?(DEFINE)) 将定义您要使用的表达式和部分,但除了定义它们之外,它实际上并没有做任何事情。在定义变量时考虑这个标记(以及它包含的所有内容)。这很好很干净,但定义变量并不意味着变量被使用!

您想在定义代码块后使用它,因此您需要在声明变量后添加表达式(就像在任何编程语言中一样)

(?(DEFINE)
  (?<block>\{(?:[^{}]++|(?&block))*\})
)
(?&block)

这部分定义了您的变量

(?(DEFINE)
  (?<block>\{(?:[^{}]++|(?&block))*\})
)

这部分会调用您的变量

(?&block)

编辑

编辑 1

(?(DEFINE)
  (?<block>\{(?:[^{}]++|(?&block))*\})
)
(?&block)\s*(?:\/\/|\/\*)([\s\S]*?)(?:\r\n|\r|\n|$)

上面的正则表达式将在块后获得注释(正如您已经定义的那样)。

你有一个 . 可以匹配任何字符(换行符除外 - 除非你使用 s 修饰符指定 . 也应该匹配换行符)

编辑 2

(?(DEFINE)
  (?<block>\{(?:[^{}]++|(?&block))*\})
)
(?&block)\s*(?:(?:\/\/([\s\S]*?)(?:\r\n|\r|\n|$))|\/\*([\s\S]*?)\*\/)

此正则表达式在语法上更适合捕获评论。之前的编辑将使用 /* 直到新行或文件末尾。这个将一直工作到结束标记或文件结束。

编辑 3

至于你的代码不起作用,我不太确定。您可以看到您的代码 运行 here,它似乎工作正常。我会改用上面写的正则表达式之一。

编辑 4

我想我终于明白你在说什么了。使用正则表达式,您尝试做的事情是不可能的。你不能在不捕获它的情况下引用一个组,因此,唯一真正的解决方案是捕获它。但是,有一种 hack-around 替代方案适用于您的情况。如果您想在没有第二部分的情况下获取第一部分和最后一部分,您可以使用以下正则表达式,它不会检查正则表达式的第二部分的语法是否正确(缺点)。如果您确实需要检查语法,您将不得不处理额外的捕获组。

(.+?)\{.*\}\s*(?:(?:\/\/([\s\S]*?)(?:\r\n|\r|\n|$))|\/\*([\s\S]*?)\*\/)

这个正则表达式捕获 { 字符之前的所有内容,然后匹配它之后的所有内容,直到它遇到 } 后跟任何空格,最后是 //。但是,如果您在代码块中有注释(在 } 之后)

,这将中断

也有现成的工具可用于此,只需几行代码。

也许第一个要看的模块是核心Text::Balanced

列表上下文中的extract_bracketed returns:匹配的子串,匹配后字符串的剩余部分,匹配前的子串。然后我们可以在余数

中继续匹配
use warnings;
use strict;
use feature 'say';

use Text::Balanced qw/extract_bracketed/;

my $text = 'start {some {stuff} one} and {more {of it} two}, and done';

my ($match, $lead);
while (1) {
    ($match, $text, $lead) = extract_bracketed($text, '{', '[^{]*');
    say $lead // $text;
    last if not defined $match; 
}

打印什么

start 
 and 
, and done

一旦没有匹配,我们需要打印余数,因此 $lead // $text(因为也不可能有 $lead)。代码直接使用$text并修改,直到最后一个余数;如果您想保留原文,请先将其保存。

我在上面使用了一个虚构的字符串,但我也在您的代码示例中对其进行了测试。


这也可以使用 Regexp::Common 来完成。

使用 $RE{balanced} 正则表达式拆分字符串,然后取奇数元素

use Regexp::Common qw(balanced);

my @parts = split /$RE{balanced}{-parens=>'{}'}/, $text;

my @out_of_blocks = @parts[  grep { $_ & 1 } 1..$#parts ];

say for @out_of_blocks;

如果字符串以分隔符开头,则第一个元素是空字符串,与 split 一样。

要清除前导空格和尾随空格,请将其传递给 map { s/(^\s*|\s*$//gr }

捕获组按照它们在正则表达式中出现的顺序从左到右编号,而不是它们匹配的顺序。这是您的正则表达式的简化视图:

m/
  (.+?)  # group 1
  (?:  # the $code_block regex
    (?&block)
    (?(DEFINE)
      (?<block> ... )  # group 2
    )
  )
  (.+)  # group 3
/xs

命名组也可以作为编号组访问。

第二组是block组。但是,该组仅用作命名子模式,而不是捕获。因此,</code> 捕获值是 undef。</p> <p>因此,代码块之后的文本将存储在捕获中 <code>

有两种方法可以解决这个问题:

  • 对于复杂的正则表达式,只使用命名捕获。一旦您从正则表达式对象中 assemble 正则表达式,或者如果捕获是有条件的,就认为正则表达式很复杂。这里:

    if ($text =~ m/(?<before>.+?)$code_block(?<afterwards>.+)/s){
        print $+{before};
        print $+{afterwards};
    }
    
  • 将所有定义放在末尾,这样它们就不会弄乱您的捕获编号。例如,您的 $code_block 正则表达式只会定义一个命名模式,然后您可以显式调用该模式。