Perl switch/case 对包含非捕获组 '?' 的文字正则表达式字符串失败

Perl switch/case Fails on Literal Regex String Containing Non-Capturing Group '?'

我有包含以下行的文本文件:

2/17/2018 400000098627 =2,000.0 .0994 ,387.75
3/7/2018 1)0000006043 2,000.0 .0731 ,332.78
3/26/2018 4 )0000034242 2,000.0 .1729 ,541.36
4/17/2018 2)0000008516 2,000.0 .219 ,637.71

我将它们与 /^\s*(\S+)\s+(?:[0-9|\)| ]+)+\s+([0-9|.|,]+)\s+$/ 匹配,但我也有一些文件的行格式完全不同,我用不同的正则表达式匹配它们。当我打开一个文件时,我确定哪种格式并在 switch/case 块中分配 $pat = '<regex-string>';

$pat = '/^\s*(\S+)\s+(?:[0-9|\)| ]+)+\s+([0-9|.|,]+)\s+$/'

但是引入我用来匹配的非捕获组的?字符在日期之后和第一个货币金额之前重复导致Perl解释器无法编译脚本,报告中止:

syntax error at ./report-dates-amounts line 28, near "}continue "

如果我删除 ? 字符,或将 ? 替换为 \? 转义字符,或者先分配 $q = '?' 然后将 ? 替换为 $q" 字符串赋值(即 $pat = "/^\s*(\S+)\s+($q:[0-9|\)| ]+)+\s+([0-9|.|,]+)\s+$/"; )中,脚本编译并运行。如果我在 switch/case 块之外分配正则表达式字符串也可以正常工作。 Perl v5.26.1 .

我的代码中也没有任何 }continue,正如编译失败中所报告的那样,这可能是 Switch.pmswitch/case 代码的某种转换本机编译器阻塞。这是 Switch.pm 中的某种错误吗?即使我以完全相同的方式使用 given/when,它也会失败。

#!/usr/local/bin/perl

use Switch;

# Edited for demo
switch($format)
{
    # Format A eg:
    #     2/17/2018 400000098627 =2,000.0 .0994 ,387.75
    #     3/7/2018 1)0000006043 2,000.0 .0731 ,332.78
    #     3/26/2018 4 )0000034242 2,000.0 .1729 ,541.36
    #     4/17/2018 2)0000008516 2,000.0 .219 ,637.71
    #
    case /^(?:april|snow)$/i
    { # This is where the ? character breaks compilation:
        $pat = '^\s*(\S+)\s+(?:[0-9|\)| ]+)+\s+\D?(\S+)\s+$';

      # WORKS:
      # $pat = '^\s*(\S+)\s+(' .$q. ':[0-9|\)| ]+)+\s+\D' .$q. '(\S+)\s+$';
    }

    # Format B
    case /^(?:umberto|petro)$/i
    {
        $pat = '^(\S+)\s+.*Think 1\s+(\S+)\s+';
    }
}

不要使用 Switch。正如@choroba 在评论中提到的那样,Switch 使用源过滤器,这会导致神秘且难以调试的错误,正如您所说的那样。

模块的文档本身说:

In general, use given/when instead. It were introduced in perl 5.10.0. Perl 5.10.0 was released in 2007.

然而,given/when 不一定是一个好的选择,因为它是实验性的并且将来可能会改变(似乎这个特性是来自 Perl v5.28 的 almost removed;所以你肯定如果可以避免,现在不想开始使用它)。一个好的替代方法是使用 for:

for ($format) {
    if (/^(?:april|snow)$/i) {
       ...
    } 
    elsif (/^(?:umberto|petro)$/i) {
       ...
    }
}

一开始可能看起来很奇怪,但一旦你习惯了,我认为它实际上是合理的。或者,当然,您可以使用此选项的 none 并执行:

sub pattern_from_format {
    my $format = shift;

    if ($format =~ /^(?:april|snow)$/i) {
       return qr/^\s*(\S+)\s+(?:[0-9|\)| ]+)+\s+\D?(\S+)\s+$/;
    } 
    elsif ($format =~ /^(?:umberto|petro)$/i) {
        return qr/^(\S+)\s+.*Think 1\s+(\S+)\s+/;
    }
    # Some error handling here maybe
 }

如果出于某种原因您仍想使用 Switch:请使用 m/.../ 而不是 /.../

我不知道为什么会出现这个错误,但是,documentation 说:

Also, the presence of regexes specified with raw ?...? delimiters may cause mysterious errors. The workaround is to use m?...? instead.

我一开始看错了,因此尝试使用 m/../ 而不是 /../,这解决了问题。

另一个替代 if/elsif 链的选项是遍历哈希,它将您的正则表达式映射到应分配给 $pat 的值:

#!/usr/local/bin/perl

my %switch = (
  '^(?:april|snow)$'    => '^\s*(\S+)\s+(?:[0-9|\)| ]+)+\s+\D?(\S+)\s+$',
  '^(?:umberto|petro)$' => '^(\S+)\s+.*Think 1\s+(\S+)\s+',
);

for my $re (keys %switch) {
  if ($format =~ /$re/i) {
    $pat = $switch{$re};
    last;
  }
}

对于更一般的情况(即,如果您不仅仅是将字符串分配给标量),您可以使用相同的通用技术,但使用 coderefs 作为散列值,从而允许它根据匹配项执行任意 sub

这种方法可以涵盖相当广泛的通常与 switch/case 构造相关的功能,但请注意,因为条件是从散列的键中提取的,所以它们'将以随机顺序进行评估。如果您有可能匹配多个条件的数据,则需要采取额外的预防措施来处理它,例如使用具有正确顺序的条件的并行数组或使用 Tie::IxHash 而不是常规哈希。