【问题标题】:Passing a regex substitution as a variable in Perl在 Perl 中将正则表达式替换作为变量传递
【发布时间】:2023-03-18 14:44:01
【问题描述】:

我需要将正则表达式替换作为变量传递:

sub proc {
    my $pattern = shift;
    my $txt = "foo baz";

    $txt =~ $pattern;
}

my $pattern = 's/foo/bar/';
proc($pattern);

这当然行不通。我尝试评估替换:

eval("$txt =~ $pattern;");

但这也没有用。我在这里遗漏了什么可怕的明显的东西?

【问题讨论】:

    标签: regex perl


    【解决方案1】:

    我需要将正则表达式替换作为变量传递

    你呢?为什么不传递代码参考?示例:

    sub modify
    {
      my($text, $code) = @_;
      $code->($text);
      return $text;
    }
    
    my $new_text = modify('foo baz', sub { $_[0] =~ s/foo/bar/ });
    

    一般来说,当您想将“做某事的事情”传递给子例程(在您的问题中是“正则表达式替换”)时,答案是传递对一段代码的引用。 Higher Order Perl 是一本关于该主题的好书。

    【讨论】:

    • 这很有效,并且最接近我的想法。但是,根据我的口味,生成的代码有点古怪和复杂,我通常认为是时候重新考虑我的整体方法了。
    • 我是 Perl 新手,请您解释一下操作符 -> 是做什么的? $code->($text) 的目的是什么?谢谢。
    • ->() 部分取消引用 $code 作为代码引用并执行它,将 () 的内容作为参数传递。更多信息:perldoc.perl.org/perlref.html#Using-References
    【解决方案2】:
    sub proc {
        my($match, $subst) = @_;
        my $txt = "foo baz";
        $txt =~ s/$match/$subst/;
        print "$txt\n";
    }
    
    my $matcher = qr/foo/;
    my $sub_str = "bar";
    
    proc($matcher, $sub_str);
    

    这相当直接地回答了您的问题。你可以做更多 - 但是当我使用 qr// 术语而不是 $sub_str 作为简单文字时,扩展的正则表达式被替换了。

    我最近需要为具有一些特殊(方言)SQL 类型的语句创建一个解析器(测试解析器),识别这样的行,将其拆分为三个类型名称:

    input: datetime year to second,decimal(16,6), integer
    

    我用来演示的脚本使用了引用的正则表达式。

    #!/bin/perl -w
    use strict;
    while (<>)
    {
        chomp;
        print "Read: <$_>\n";
        my($r1) = qr%^input\s*:\s*%i;
        if ($_ =~ $r1)
        {
            print "Found input:\n";
            s%$r1%%;
            print "Residue: <$_>\n";
            my($r3) = qr%(?:year|month|day|hour|minute|second|fraction(?:\([1-5]\))?)%;
            my($r2) = qr%
                            (?:\s*,?\s*)?   # Commas and spaces
                            (
                                (?:money|numeric|decimal)(?:\(\d+(?:,\d+)?\))?   |
                                int(?:eger)?  |
                                smallint      |
                                datetime\s+$r3\s+to\s+$r3
                            )
                        %ix;
            while ($_ =~ m/$r2/)
            {
                print "Got type: <$1>\n";
                s/$r2//;
            }
            print "Residue 2: <$_>\n";
        }
        else
        {
            print "No match:\n";
        }
        print "Next?\n";
    }
    

    我们可以争论 $r1 等名称的使用。但它确实起到了作用……它不是,现在也不是生产代码。

    【讨论】:

    • 多么丑陋的代码,为什么要为每一行一次又一次地创建正则表达式值?
    • 这是演示代码 - 这意味着“未优化”和“不用于生产用途”。组装它是为了表明它有效,仅此而已。
    【解决方案3】:

    好吧,您可以使用 qr// 运算符预编译正则表达式。但是你不能传递一个操作符(s///)。

    $pattern = qr/foo/;
    
    print "match!\n" if $text =~ $pattern;
    

    但如果你必须传递替换运算符,你只能传递代码或字符串:

    proc('$text =~ s/foo/bar');
    
    sub proc {
       my $code = shift;
    
       ...
    
       eval $code;
    }
    

    或者,代码:

    proc(sub {my $text = shift;  $text =~ s/foo/bar});
    
    sub proc {
       my $code = shift;
    
       ...
    
       $code->("some text");
    }
    

    【讨论】:

      【解决方案4】:

      s/// 不是正则表达式。因此,您不能将其作为正则表达式传递。

      我不喜欢eval。它非常脆弱,有很多边框。

      我认为最好采用类似于 JavaScript 的方法:传递正则表达式(在 Perl 中,即 qr//)和代码引用以进行替换。比如传递参数得到同样的效果

      s/(\w+)/\u\L$1/g;
      

      你可以打电话

      replace($string, qr/(\w+)/, sub { "\u\L$1" }, 'g');
      

      请注意,“g”修饰符实际上并不是正则表达式的标志(我认为将其附加到正则表达式是 JavaScript 中的设计错误),因此我选择将它传递给第三个参数。

      一旦确定了 API,接下来就可以进行实施了:

      sub replace {
          my($string, $find, $replace, $global) = @_;
          unless($global) {
              $string =~ s($find){ $replace->() }e;
          } else {
              $string =~ s($find){ $replace->() }ge;
          }
          return $string;
      }
      

      让我们试试吧:

      print replace('content-TYPE', qr/(\w+)/, sub { "\u\L$1" }, 'g');
      

      结果:

      内容类型

      我觉得不错。

      【讨论】:

      • CPAN 上的 Data::Munge 有一个与此类似的“替换”函数,尽管它将子字符串匹配传递给函数,或将替换解析为字符串。
      【解决方案5】:
      eval "$txt =~ $pattern";
      

      这就变成了

      eval "\"foo baz\" =~ s/foo/bar/"
      

      并且替换不适用于文字字符串。

      这可行:

      eval "\$txt =~ $pattern"
      

      但这不是很令人愉快。 eval 几乎从来都不是正确的解决方案。

      zigdon's solution 可以做任何事情,如果替换字符串是静态的,Jonathan's solution 非常适合。如果您想要比第一个更结构化且比第二个更灵活的东西,我建议您使用混合:

      sub proc {
          my $pattern = shift;
          my $code = shift;
          my $txt = "foo baz";
          $txt =~ s/$pattern/$code->()/e;
          print "$txt\n";
      }
      
      my $pattern = qr/foo/;
      proc($pattern, sub { "bar" });   # ==> bar baz
      proc($pattern, sub { "\U$&" });  # ==> FOO baz
      

      【讨论】:

        【解决方案6】:

        也许你可能会重新考虑你的方法。

        您想将正则表达式替换传递给函数,可能是因为该函数将从其他来源(从文件、套接字等读取)派生要操作的文本。但是您将正则表达式与正则表达式替换混为一谈。

        在表达式s/foo/bar/ 中,您实际上有一个正则表达式 ("/foo/") 和一个替换 ("bar") 应该替换表达式匹配的内容。在您迄今为止尝试的方法中,您在尝试使用 eval 时遇到了问题,主要是因为表达式中的特殊字符可能会干扰 eval 或在评估过程。

        因此,请尝试向您的例程传递两个参数:表达式和替换:

        sub apply_regex {
            my $regex = shift;
            my $subst = shift || ''; # No subst string will mean matches are "deleted"
        
            # Some setup and processing happens...
        
            # Time to make use of the regex that was passed in:
            while (defined($_ = <$some_filehandle>)) {
                s/$regex/$subst/g; # You can decide if you want to use /g etc.
            }
        
            # The rest of the processing...
        }
        

        这种方法有一个额外的好处:如果你的正则表达式模式没有有任何特殊字符,你可以直接传入它:

        apply_regex('foo', 'bar');
        

        或者,如果是这样,您可以使用qr// quoting-operator 创建一个正则表达式对象并将其作为第一个参数传递:

        apply_regex(qr{(foo|bar)}, 'baz');
        apply_regex(qr/[ab]+/, '(one or more of "a" or "b")');
        apply_regex(qr|\d+|); # Delete any sequences of digits
        

        最重要的是,您真的不需要eval 或使用代码引用/闭包来完成此任务。这只会增加复杂性,可能会使调试变得比需要的更难。

        【讨论】:

          【解决方案7】:

          我找到了一种可能更好的方法:

          sub proc {
              my ($pattern, $replacement) = @_;
              my $txt = "foo baz";
          
              $txt =~ s/$pattern/$replacement/g;  # This substitution is global.
          }
          
          my $pattern = qr/foo/;  # qr means the regex is pre-compiled.
          my $replacement = 'bar';
          
          proc($pattern, $replacement);
          

          如果替换的标志必须是可变的,你可以使用这个:

          sub proc {
              my ($pattern, $replacement, $flags) = @_;
              my $txt = "foo baz";
          
              eval('$txt =~ s/$pattern/$replacement/' . $flags);
          }
          
          proc(qr/foo/, 'bar', 'g');
          

          请注意,您不需要在替换字符串中转义 /

          【讨论】:

          • 这可能适用于特定输入(固定字符串),但如果模式是“(\w+)”而替换是“\u\L$1”怎么办?
          【解决方案8】:

          我有一个非常简单的批量文件重命名脚本,它使用了这个技巧:

          #!/opt/local/bin/perl
          sub oops { die "Usage : sednames s/old/new [files ..]\n"; }
          oops if ($#ARGV < 0);
          
          $regex = eval 'sub { $_ = $_[0]; ' . shift(@ARGV) . '; return $_; }';
          sub regex_rename { foreach (<$_[0]>) {
              rename("$_", &$regex($_));
          } }
          
          if ($#ARGV < 0) {  regex_rename("*");  }
          else {  regex_rename(@ARGV);  }
          

          任何修改$_ 的Perl 命令(如s/old/new)都可以用来修改文件。

          我决定使用eval,这样正则表达式只需要编译一次。 eval$_ 有一些奇怪的地方让我无法简单地使用:

          eval 'sub { ' . shift(@ARGV) . ' }';
          

          虽然这个&amp;$regex 确实修改了$_,但要求"$_" 在调用rename 之前评估$_。是的,eval 很脆弱,就像其他人所说的那样。

          【讨论】:

            【解决方案9】:

            你是对的 - 你非常接近:

            eval('$txt =~ ' . "$pattern;");
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2016-08-12
              • 1970-01-01
              • 1970-01-01
              • 2015-07-24
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多