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

4

我正在尝试匹配其中之一

(\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' => %s\n\n", Dumper(\@v));
    } elsif(@v = $k =~ m/^(\S+)(!)$/) {
      printf STDERR ("clownMatch = '$k' => %s\n\n", 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' => %s\n\n", 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',
          '!'
        ];


===

代码注释中有更多细节。输出在代码部分底部。'!'字符只是一个字符,我没有将其与其他字符混淆。

更新时间:2023年4月10日23:15:40 PDT

在几位读者的智慧输入下,似乎这个问题可以分解成几个较小的问题。

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

我还没有听说过。

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

没有充分的理由就不应该使用。

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

不应该。我之前使用正则表达式进行语法检查时有些过头了。

虽然如此,我学到了很多东西。我希望管理员能够将这篇文章作为一则警示故事。

每个人都应该得到积分,并声称他们被剥夺了,引用本段文字即可。@Schwern因为第一个回答而获得了积分。谢谢。


1
一个有三个捕获,一个有两个捕获。它们应该如何组合? - Schwern
我希望 @v 可以是长度为2或3,具体取决于哪个子正则表达式匹配,就像输出中的 clownMatch 示例一样。 - Erik Bennett
1
由于您正在匹配两个不同的东西,因此拥有两个不同的匹配似乎是完全合理的。为什么您想要将它们合并? - Schwern
2
使用分支重置而不是*undef.*尝试例如^(\S+)(?|(=)([fisuo])|(!)())$ - bobble bubble
1
“使用不包含未定义的分支重置” - 我不认为这会改善情况;仍然有一个额外的捕获。 - zdim
显示剩余10条评论
4个回答

4
在一个交替中,返回所有捕获的值,即使那些没有匹配的也会被返回。
一个简单的解决办法是从返回列表中过滤掉 undef。
if ( my @v = grep { defined } $s =~ /^(?: (\S+)(=)([fisuo]) | (\S+)(!) )$/x )

还有其他方法来构建正则表达式,但直接的选择也是可以的。
这个问题特别问如何将两个(可选的)正则表达式模式合并成一个,以便只捕获实际匹配的内容,而不包含额外的undef。在我看来,这是一个很好的问题,因为通常情况下我们不想再进行清理工作。
通常的选择(p1 | p2)会返回(在列表上下文或@{^CAPTURE}中)所有指定的捕获组,如上所述。如果p1定义了三个捕获组,p2定义了两个,最后我们会得到五个;对于匹配的分支来说是捕获,对于其他分支则是undef。
简而言之,我发现要使用纯正则表达式获取一个“干净”的真实捕获集合,我们需要使用语法解析。虽然内置的支持(参见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

这是一种方法。我希望能学到更多关于正则表达式的方法。尽管如此,我还是点了赞,因为我以前没有用过 grep 这种方式,而且我喜欢学习新东西。 - Erik Bennett
2
@ErikBennett "正则表达式方式" -- 我想不出一种通用且简单的方法,它不会返回所有引入的捕获组,即使它们是 undef 因为它们没有匹配(在另一个分支中)。分支重置模式 接近但仍然返回 更长 的捕获组(这里是 3),即使它匹配了较短的分支。 (因此,如果它匹配 verbose!,它仍然返回三个长的捕获列表之一是 undef)。然后有许多方法来制作一个正则表达式以避免交替,但这取决于特定的模式。 - zdim
这需要我花些时间来学习。从外观上看,它可能会捕捉到foo!s,但我真的需要仔细阅读所有内容。我会继续观察和提问。这比我预期的一行代码要复杂得多。我喜欢它。 - Erik Bennett
它可能也会捕获bar=。但这只是一些简单更改的问题。我自从上学以来就没有用过这些东西了。冒着泄露年龄的风险,那是在“骆驼书”之前。 - Erik Bennett
@ErikBennett 实际的正则表达式取决于确切的用例,这是选择的问题。使用\S+可以捕获“所有内容”,并依赖于随后出现的回溯(= 等)。使用\w+肯定缺少“-”,而使用[\w-]+可能还会有其他一些在选项名称中出现的字符。不错的选择是列出那些“不可能”出现的内容,这就是我所做的——但这可能需要微调。(在名称后面可以使用哪些其他符号?例如有冒号“:”,因此应添加:[^=!:]+等。)但这只是简单的部分,以精确的子模式将其打造成型。 - zdim
显示剩余2条评论

3
我们可以使用以下单一的正则表达式模式:
^(\S+)([!=])((?<==)[fisuo])?$

这段代码的意思是匹配:

  • ^ 匹配字符串的开头
  • (\S+) 匹配并捕获一个非空白字符,保存在$1
  • ([!=]) 匹配并捕获!=,保存在$2
  • ((?<==)[fisuo])? 然后可选地捕获来自fisuo的字母,保存在$3中。回顾先行断言(?<==)确保只有在=的情况下才会匹配
  • $ 匹配字符串的结尾

演示


回顾后发现!我一直在尝试使用前瞻,但不想发布我的失败尝试。这仍然会在第二个组(verbose!)上留下一个尾随的 undef,但它肯定会起作用。开玩笑的是,有没有办法让它(或任何正则表达式)返回2个或3个(或可变数量)的组? - Erik Bennett
如果你想要一个空字符串而不是 undef,可以尝试将模式内部变为可选项,例如通过在结尾处 移除 ? 并添加 OR nothing |) - bobble bubble
1
@bobblebubble "如果你想要一个空字符串而不是未定义的值..." -- 但这仍然不能满足问题所要求的,即匹配分支中实际捕获的列表(在这种情况下为2或3)。不是未定义的值或虚假的空字符串。这并没有回答那个问题。 - zdim

3
我所有的尝试都导致了额外的、不需要的捕获。
我会选择像@bobble_bubble(仅作为评论)建议的"分支重置"(?| pattern1 | pattern2 | ... )
这是一种通用的解决方案,可以将不同的模式与组合在一起,同时重置捕获计数。
遗憾的是,与他链接的文档相反,对于具有较少组的模式,您仍将在返回的LISTs末尾获得undef插槽。
但是,如果这真的让你烦恼——个人而言,我会保留它们——你可以安全地使用像@zdim建议的grep {defined}过滤它们。
这是安全的,因为undef表示不匹配,不能与空匹配""混淆。
这里是覆盖您测试用例的代码。
use v5.12.0;
use warnings;
use Data::Dump qw/pp ddx/;
use Test::More;

# https://dev59.com/y9dqpIgBRmDukGFEl13O

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插槽,因为您仍然需要单独处理不同的情况。

而且,有一天您可能还想在分支之后添加模式。如果分支重置确实跳过了缺失的组,那么这将彻底改变尾随组的编号。因此,从设计角度来看,这样做得很好。


我会选择“分支重置” (?|模式1|模式2|...),就像@bobble_bubble(仅在评论中)已经建议的那样。但是,正如我在我的答案下面的评论中所提到的,它仍然返回最长分支的长度列表,因此当较短的分支匹配时,它明确地返回undef值。这并没有回答问题。顺便说一句,这与我链接的文档并不矛盾。 - zdim
1
@zdim,我详细地回答了这个问题,请仔细阅读整个答案。并且我没有参考你提供的文档。 - LanX
那么你“不明白消除undef的意义”...好吧,也许这确实是错位的,但“那就是问题所在”。这里有一些其他答案似乎忽略了这一点,我要重申我的观点:这是一个很好的问题。我不明白为什么它被认为是无关紧要的——获取那些undef是一件烦人的事情,我们通常需要做一些处理。如果不必这样做,那将是很好的。至于他们的评论,那是针对我的原始评论,他们确实链接到了不同的文档。我的错。我建议像你一样参考perldoc。 - zdim
1
  1. 分支重置(Branch-reset)值得拥有一个显著的回答,而不是隐藏在某些评论中。这里的其他搜索可能只是想要那个。
- LanX
分支重置是一个关键部分,我注意到可以省略 grepping。 - LanX
显示剩余6条评论

2

由于您正在匹配两个不同的东西,拥有两个不同的匹配似乎是完全合理的。

但是,如果您确实想要将它们结合起来,可以这样做:

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

$1 是名称。$2 是开关,如果存在的话。$3 是感叹号,如果存在的话。

对于任何更复杂的情况,请使用命名捕获Regexp::Assemble

演示


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接