合并两个具有可变数量的捕获组的正则表达式



我正在尝试匹配任何一个

(S+)(=)([fisuo])

(S+)(!)

然后将结果放在列表(捕获组)中。 我所有的尝试都会导致额外的、不需要的捕获。

下面是一些代码:

#!/usr/bin/perl
#-*- cperl -*-
# $Id: test7,v 1.1 2023/04/10 02:57:12 bennett Exp bennett $
#
use strict;
use warnings;
use Data::Dumper;
foreach my $k ('debugFlags=s', 'verbose!') {
my @v;
# Below is the offensive looking code.  I was hoping for a regex
# which would behave like this:
if(@v = $k =~ m/^(S+)(=)([fisuo])$/) {
printf STDERR ("clownMatch = '$k' => %snn", Dumper(@v));
} elsif(@v = $k =~ m/^(S+)(!)$/) {
printf STDERR ("clownMatch = '$k' => %snn", Dumper(@v));
}
@v = ();
# This is one of my failed, aspirational matches.  I think I know
# WHY it fails, but I don't know how to fix it.

if(@v = $k =~ m/^(?:(S+)(=)([fisuo]))|(?:(S+)(!))$/) {
printf STDERR ("hopefulMatch = '$k' => %snn", Dumper(@v));
}
printf STDERR "===n";
}
exit(0);
__END__

输出:

clownMatch = 'debugFlags=s' => $VAR1 = [
'debugFlags',
'=',
's'
];

hopefulMatch = 'debugFlags=s' => $VAR1 = [
'debugFlags',
'=',
's',
undef,
undef
];

===
clownMatch = 'verbose!' => $VAR1 = [
'verbose',
'!'
];

hopefulMatch = 'verbose!' => $VAR1 = [
undef,
undef,
undef,
'verbose',
'!'
];

===

代码注释中有更多详细信息。 输出位于代码部分的底部。 而"!"字符就是这样。 我不会将其与其他一些不混淆。

更新周一 4 月 10 日 23:15:40 PDT 2023:

在几位读者的明智投入下,这个问题似乎分解成几个较小的问题。

正则表达式可以返回可变数量的捕获组吗?

我没有听说过一种或另一种方式。

如果可以的话,是否应该以这种方式使用正则表达式?

并非没有令人信服的理由。

出于我的目的,我应该使用正则表达式来创建真正的词法分析器/解析器吗?

不。 我正在使用正则表达式进行语法检查,然后得意忘形。

不过,我学到了很多东西。 我希望版主认为适合将这篇文章作为一个警示故事。

每个人都应该在这一点上得到分数,并且可以声称他们被抢劫了,引用这一段。 @Schwern获得第一名的分数。 谢谢。

在交替中,将返回所有捕获的值,即使对于不匹配的值也是如此。

一个简单的方法是从返回列表中过滤掉undef

if ( my @v = grep { defined } $s =~ /^(?: (S+)(=)([fisuo]) | (S+)(!) )$/x )

还有其他方法可以构建正则表达式,但直接的交替就可以了。


这个问题特别询问如何将两种(替代)正则表达式模式以这种方式合并为一个,以便捕获实际匹配的内容,而无需额外的undef。 在我看来,这是一个很好的问题,因为不必清理通常是件好事。

通常的交替(p1 | p2)返回(在列表上下文中或在@{^CAPTURE}中)所有指示的捕获组,如上所述。如果p1定义了三个捕获组,p2两个捕获组,则最终我们得到五个;捕获匹配的分支,undefs 捕获另一个分支。

简而言之,我发现要仅使用纯正则表达式获得一组"干净"的真实捕获,我们需要使用语法进行解析。虽然内置支持(参见DEFINE)只能匹配("识别")模式,但Regexp::Grammars支持更多。 一个简单的例子是合适的

use warnings;
use strict;
use feature 'say';
use Data::Dump qw(dd);  # Data::Dumper is in the core
my $grammar = do {
use Regexp::Grammars;
qr{ 
<word> <symb> <val>?
<nocontext:>
<token: word>  [^=!]+  # or use w in a character class with chars
# that are also allowed, like [w-.] etc
<token: symb>  = | !
<token: val>   [fisuo]
}x;
};
for my $s (qw(debugFlags=s verb!)) {
if ($s =~ $grammar) { 
dd %/;              # hash %/ is populated with results
say for values %/;   # just the results
say '-'x60;
}   
}

这打印

{ symb => "=", val => "s", word => "debugFlags" }
s
=
debugFlags
------------------------------------------------------------
{ symb => "!", word => "verb" }
!
verb
------------------------------------------------------------

结果不会排序,因此可能需要为哈希添加所需的排序标准,或者遍历各个哈希元素。

问题中的示例非常简单,因此琐碎的语法适用于它,但是如果我们想象它增长到更全面地处理选项,那么语法将需要更加参与/结构化。例如,虽然这仍然很简单

qr{
<option>   # run the matching
# Define the grammar
<nocontext:>
<token: option>     <opt_vals> | <opt_flag>
<token: opt_vals>   <word> <symb_vals> <val>
<token: opt_flag>   <word> <symb_flag>?
<token: word>       [^=!:]+
<token: symb_vals>  = | :
<token: symb_flag>  !
<token: val>        [fisuo]
}x;

它可以更容易地扩展,也更精确。

这个问题中正则表达式的目的是检查Getopt::Long的用法,这是一个用于解析命令行选项的模块,并且!之后没有任何内容(标志类型选项的否定)。 因此,选项名称后面带有值(=:)的符号与!分开。 当然,库的语法中还有更多内容;这是一个演示。

请参阅(看似无穷无尽的)文档,了解许多许多Regexp::Grammars功能,其中几乎没有在这里使用。


其他一切似乎都受到额外undef的影响。 ">分支重置"接近,但仍然返回最长的指示捕获组集(此处为 3),即使它与较短的分支匹配,正如我在下面的评论中提到的;所以我们又得到了undef。请参阅@LanX的答案,了解如何使用它。

条件表达式,我希望它可以躲避子弹,还设置了它看到的所有捕获括号

for (qw(debugFlags=s verb!)) 
{
if ( /^([^=!]+) (?(?==) (=)([fisuo]) | (!))$/x ) {
say "Length: ", scalar @{^CAPTURE};
say $_//'undef' for @{^CAPTURE};
}
}

我们在第二次测试中适当地打印了两undef。 我专门使用条件的展望来避免额外的捕获组,但当然表达式中的所有括号都会获取它们,无论哪个匹配。 所以一个人真的可以做到

if ( /^([^=!]+) (=)? (?(2) ([fisuo]) | (!))$/x )

(具有相同的结果,良好的匹配和捕获,但需要额外的undef秒)

我们可以使用以下单一正则表达式模式:

^(S+)([!=])((?<==)[fisuo])?$

这说要匹配:

  • 从字符串的开头^
  • (S+)匹配和捕获$1非空格术语
  • ([!=])!=$2匹配和捕获
  • 然后((?<==)[fisuo])?$3选择性地捕获一封来自fisuo的信件 "查看"(?<==)确保这仅与=匹配
  • 字符串的末尾$

演示

我所有的尝试都会导致额外的、不需要的捕获。

我会选择"分支重置"(?| pattern1 | pattern2 | ... )就像@bobble_bubble已经建议的那样(仅作为评论)

这是一种通用解决方案,可将不同的模式与组组合在一起,同时重置捕获计数。

唉,与他链接到的文档相反,您仍然会在返回的 LIST 末尾获得undef个插槽,用于组较少的模式。

但是,如果这真的困扰您 - 我个人会保留它们 - 您可以像@zdim建议的那样使用grep {defined}安全地过滤它们。

这是安全的,因为undef意味着不匹配,不能与空匹配""混淆。

这里是涵盖您的测试用例的代码

use v5.12.0;
use warnings;
use Data::Dump qw/pp ddx/;
use Test::More;
# https://stackoverflow.com/questions/75974097/merge-two-regexes-with-variable-number-of-capture-groups
my %wanted =
(
"debugFlags=s" => ["debugFlags", "=", "s"],
"verbose!"     => ["verbose", "!"],
);

while ( my ( $str, $expect) = each %wanted ) {
my @got =
$str =~ / (S+)
(?|
(=) ([fisuo]+)
|
(!)
)
/x;
ddx @got;                          # with trailing undefs
@got = grep {defined} @got;         # eliminate undefs
is_deeply( @got, $expect, "$str => ". pp(@got));
}
done_testing();

-->

# branchreset.pl:25: ["debugFlags", "=", "s"]
ok 1 - debugFlags=s => ["debugFlags", "=", "s"]
# branchreset.pl:25: ["verbose", "!", undef]
ok 2 - verbose! => ["verbose", "!"]
1..2
<小时 />
战略更新

但同样,我认为最后消除undef槽没有意义,因为无论如何您都需要单独处理不同的情况。

有一天,您可能还想在分支之后添加模式。如果分支重置真的跳过了缺失的组,那将改变尾随组的编号,使其无法识别。所以从设计的角度来看,这做得很好。

既然你匹配的是两个不同的东西,那么有两个不同的匹配似乎是完全合理的。

但是,如果您确实想组合它们,则可以这样做:

m{^
(S+)
(?:
=([fisuo]) |
(!)
)
$
}x

$1 是名称。$2 是交换机(如果存在)。$3 是 !(如果存在)。

对于更复杂的内容,请使用命名捕获或 Regexp::Assemble。

示范

最新更新