perl 合并两个捕获组数目可变的正则表达式

htrmnn0y  于 2023-04-12  发布在  Perl
关注(0)|答案(4)|浏览(130)

我在试着匹配

(\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',
          '!'
        ];

===

在代码注解中有更多的细节。输出在代码部分的底部。'!'字符就是这样。我没有把它和其他的not混淆。

更新于2023年4月10日星期一23:15:40 PDT:

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

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

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

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

除非有个令人信服的理由

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

没有。我当时在用正则表达式进行语法检查,有点忘乎所以了。
不过,我学到了很多东西,我希望版主们能把这篇文章当作一个警示故事。
每个人都应该在这一点上得到分数,并且可以引用这一段来声称他们被抢劫了。

omqzjyyz

omqzjyyz1#

在一个交替中,all 捕获的值被返回,即使是那些不匹配的捕获。
一个简单的方法是从返回列表中过滤掉undef

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

也有其他的方法来构建正则表达式,但是直接的交替就可以了。
这个问题特别询问了如何将两个(可选的)正则表达式模式合并为一个,以便只捕获实际匹配的内容,而不需要额外的undef。在我看来,这是一个很好的问题,因为它通常不需要清理。
通常的交替(p1 | p2)返回(在alist上下文中或在@{^CAPTURE}中)all 指示的捕获组,如上所述。如果p1定义了三个捕获组,p2定义了两个,最终我们得到五个;捕获匹配的分支,undef s捕获另一个分支。
简而言之,我发现要获得一组“干净”的真捕获,我们需要用语法解析pure-regex。虽然内置支持(参见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{
    <line>       # match

    # Define the grammar
    <nocontext:>
    <token: line>  <p1> | <p2>

    <token: p1>    <word><symb><val>
    <token: p2>    <word><symb>

    <token: word>  [^=!]+

    <token: symb>  = | !
    <token: val>   [fisuo]

}x;

它可以更容易地扩展。请参阅(似乎无穷无尽的)文档的许多许多功能。
所有其他的似乎都受到了额外的undef s的影响。“* 分支reset*”很接近,但仍然返回最长的一组指示的捕获组(这里是3),即使它匹配一个较短的分支,正如我在下面的评论中提到的;所以我们又得到了undef s。关于如何使用这个,请参阅@LanX的答案。
条件表达式(我希望它能躲过这一劫)也设置了它看到的所有捕获括号

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

在第二个测试中,我们及时地得到了两个undef。我使用了一个lookahead的条件,特别是为了避免额外的捕获组,但是当然,表达式中更远的 all 括号会得到它们,而不管哪些匹配。所以我们真的可以这样做

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

ecr0jaav2#

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

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

这说匹配:

  • ^从字符串的开始
  • (\S+)$1中匹配并捕获非空白项
  • ([!=])$2中匹配和捕获!=
  • ((?<==)[fisuo])?然后可选地在$3中捕获来自fisuo的字母,(?<==)的lookbehind确保这仅与=匹配
  • $字符串结尾


∮ ∮ ∮ ∮

rsaldnfx

rsaldnfx3#

我所有的尝试都会导致额外的、不必要的捕获。
我会选择“分支重置”(?| pattern1 | pattern2 | ... ),就像@bobble_bubble已经建议的那样(仅作为评论)
这是一个通用的解决方案,将不同的模式与组组合在一起,同时重置捕获计数。
遗憾的是,与他所链接的文档相反,对于组较少的模式,您仍然会在返回的列表末尾获得undef插槽。
但如果这真的困扰你-我个人会保留它们-你可以安全地用grep {defined}过滤掉它们,就像@zdim建议的那样。
这是安全的,因为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插槽有什么意义,因为无论如何您都需要单独处理不同的情况。
有一天你可能也想在分支后添加模式。如果分支重置真的跳过了缺失的组,那将使尾随组的编号变得无法识别。所以从设计的Angular 来看,这做得很好。

vngu2lb8

vngu2lb84#

既然你要匹配两个不同的东西,那么有两个不同的匹配似乎是完全合理的。
但是,如果你想把它们结合起来,你可以这样做:

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

$1是名称。$2是开关(如果存在)。$3是!(如果存在)。
对于更复杂的内容,请使用命名捕获或Regexp::Assemble
Demonstration

相关问题