有没有更好的方法用/ x编写Perl正则表达式,所以代码仍然易于阅读?

时间:2009-06-12 15:48:14

标签: regex perl perl-critic

我在我的一个脚本上运行了Perl :: Critic,并收到了这条消息:

Regular expression without "/x" flag at line 21, column 26. See page 236 of PBP.

我查看了政策信息here,我理解在扩展模式下编写正则表达式可以帮助任何正在查看代码的人。

但是,我被困在如何将我的代码转换为使用/ x标志。

CPAN示例:

# Match a single-quoted string efficiently...

m{'[^\\']*(?:\\.[^\\']*)*'};  #Huh?

# Same thing with extended format...

m{
    '           # an opening single quote
    [^\\']      # any non-special chars (i.e. not backslash or single quote)
    (?:         # then all of...
        \\ .    #    any explicitly backslashed char
        [^\\']* #    followed by an non-special chars
    )*          # ...repeated zero or more times
    '           # a closing single quote
}x;

如果你只看正则表达式,这是有道理的。

我的代码:

if ($line =~ /^\s*package\s+(\S+);/ ) {

我不确定如何在if语句中使用扩展正则表达式。我可以这样写:

    if (
        $line =~ /
        ^\s*    # starting with zero or more spaces
        package
        \s+     # at least one space
        (\S+)   # capture any non-space characters
        ;       # ending in a semi-colon
        /x
      )
    {

这有效,但我认为这比原版更难阅读。有没有更好的方法(或最佳实践方式)来写这个?我想我可以用qr //.

创建一个变量

我不是在寻找重写这个特定正则表达式的建议(虽然如果我可以改进它,我会接受建议) - 我更关心如何扩展if中的正则表达式言。

我知道Perl :: Critic只是一个指南,但跟随它会很好。

提前致谢!

修改 因此,在收到几个答案之后,我很清楚,制作带注释的正则表达式多行并不总是必要的。理解基本正则表达式的人应该能够理解我的例子在做什么 - 我添加的评论可能有点不必要和冗长。我喜欢使用扩展正则表达式标志的想法,但仍然在正则表达式中嵌入空格以使正则表达式的每个部分更清晰。 感谢所有的投入!

5 个答案:

答案 0 :(得分:12)

永远不要写评论说明代码说的是什么。评论应该告诉你为什么代码会说出它的内容。看看这个怪物,没有评论就很难看到发生了什么,但是评论清楚地表明正在尝试匹配

require 5.010;
my $sep         = qr{ [/.-] }x;               #allowed separators    
my $any_century = qr/ 1[6-9] | [2-9][0-9] /x; #match the century 
my $any_decade  = qr/ [0-9]{2} /x;            #match any decade or 2 digit year
my $any_year    = qr/ $any_century? $any_decade /x; #match a 2 or 4 digit year

#match the 1st through 28th for any month of any year
my $start_of_month = qr/
    (?:                         #match
        0?[1-9] |               #Jan - Sep or
        1[0-2]                  #Oct - Dec
    )
    ($sep)                      #the separator
    (?: 
        0?[1-9] |               # 1st -  9th or
        1[0-9]  |               #10th - 19th or
        2[0-8]                  #20th - 28th
    )
    \g{-1}                      #and the separator again
/x;

#match 28th - 31st for any month but Feb for any year
my $end_of_month = qr/
    (?:
        (?: 0?[13578] | 1[02] ) #match Jan, Mar, May, Jul, Aug, Oct, Dec
        ($sep)                  #the separator
        31                      #the 31st
        \g{-1}                  #and the separator again
        |                       #or
        (?: 0?[13-9] | 1[0-2] ) #match all months but Feb
        ($sep)                  #the separator
        (?:29|30)               #the 29th or the 30th
        \g{-1}                  #and the separator again
    )
/x;

#match any non-leap year date and the first part of Feb in leap years
my $non_leap_year = qr/ (?: $start_of_month | $end_of_month ) $any_year/x;

#match 29th of Feb in leap years
#BUG: 00 is treated as a non leap year
#even though 2000, 2400, etc are leap years
my $feb_in_leap = qr/
    0?2                         #match Feb
    ($sep)                      #the separtor
    29                          #the 29th
    \g{-1}                      #the separator again
    (?:
        $any_century?           #any century
        (?:                     #and decades divisible by 4 but not 100
            0[48]       | 
            [2468][048] |
            [13579][26]
        )
        |
        (?:                     #or match centuries that are divisible by 4
            16          | 
            [2468][048] |
            [3579][26]
        )
        00                      
    )
/x;

my $any_date  = qr/$non_leap_year|$feb_in_leap/;
my $only_date = qr/^$any_date$/;

答案 1 :(得分:11)

嗯,我真的不认为你应该浪费垂直屏幕房地产。另一方面,如果我将这个模式写在几行上,我会使用大括号并缩进模式:

if ($line =~ m{
        \A \s*
        package
        \s+
        (\S+)
        \s* ;
    }x 
) {

恕我直言,以下版本完全没问题:

if ( $line =~ m{ \A \s* package \s+ (\S+) \s* ; }x  ) {

获得m//x的好处。

在这种情况下,评论是完全没有必要的,因为你没有做任何棘手的事情。我确实在分​​号之前添加了\s*,因为有时人们将分号设置为与包名不同,并且不应该丢掉你的匹配。

答案 2 :(得分:8)

这几乎是你对这些额外信息所增加的价值的呼吁。

有时你是对的,它没有添加任何东西来解释发生了什么,只是让代码看起来很乱,但是对于复杂的正则表达式,x标志可以是一个恩惠。

实际上,关于附加信息的附加价值的“打电话”可能非常困难。

我记不清有多少次我见过遗留代码,其中没有保留格式精美的评论,因此偏离代码所做的事情。事实上,当我经验不足时,我完全走错了道路,因为与一段代码相关的评论很旧并且没有被维护。

编辑:在某些方面,CPAN示例实际上并不那么有用。当使用x标志添加注释来描述复杂的正则表达式时,我倾向于描述正则表达式试图匹配的组件,而不仅仅是描述正则表达式“位”本身。例如,我写的内容如下:

  • 英国邮政编码的第一个组成部分(区域和地区),或
  • 英国的国际区号,或
  • 任何英国手机号码。

告诉我的不仅仅是

  • 一个或两个字母,后跟一个数字,后面跟一个字母,或
  • 两个四位数字,或
  • 一个零,后跟四个十进制数字,一个破折号,然后六个十进制数字。

我的感觉是在这种情况下留下正则表达式的评论。你的直觉是对的!

答案 3 :(得分:6)

看到这个主题是关于编写正则表达式的替代方法,有些方法可以编写没有变量的复杂正则表达式,没有注释,并且它仍然有用。

我将Chas Owens日期验证正则表达式转换为Perl-5.10中提供的新声明形式,这有很多好处。

  • 正则表达式中的标记是可重用的
  • 以后打印正则表达式的任何人都会看到整个逻辑树。

它可能不是每个人的水壶鱼,但对于非常复杂的事情,如日期验证它可以很方便(ps:在现实世界中,请使用模块日期的东西,不要DIY,这只是一个例子来学习)

#!/usr/bin/perl 
use strict;
use warnings;
require 5.010;

#match the 1st through 28th for any month of any year
my $date_syntax = qr{
    (?(DEFINE)
        (?<century>
            ( 1[6-9] | [2-9][0-9] )
        )
        (?<decade>
            [0-9]{2} (?!\d)
        )
        (?<year>
            (?&century)? (?&decade)(?!\d)
        )
        (?<leapdecade> (
            0[48]       | 
            [2468][048] |
            [13579][26]
            )(?!\d)
        )
        (?<leapcentury> (
            16          | 
            [2468][048] |
            [3579][26]
            )
        )   
        (?<leapyear>
            (?&century)?(?&leapdecade)(?!\d)
            |
            (?&leapcentury)00(?!\d)
        )
        (?<monthnumber>      ( 0?[1-9] | 1[0-2] )(?!\d)                  )
        (?<shortmonthnumber> ( 0?[469] | 11     )(?!\d)                  )
        (?<longmonthnumber>  ( 0?[13578] | 1[02] )(?!\d)                 )
        (?<nonfebmonth>      ( 0?[13-9] | 1[0-2] )(?!\d)                 )
        (?<febmonth>         ( 0?2 )(?!\d)                               )
        (?<twentyeightdays>  ( 0?[1-9] | 1[0-9] | 2[0-8] )(?!\d)         )
        (?<twentyninedays>   ( (?&twentyeightdays) | 29 )(?!\d)          )
        (?<thirtydays>       ( (?&twentyeightdays) | 29 | 30 )(?!\d)     )
        (?<thirtyonedays>    ( (?&twentyeightdays) | 29 | 30 | 31 )(?!\d))
        (?<separator>        [/.-]                              )               #/ markdown syntax highlighter fix
        (?<ymd>
            (?&leapyear) (?&separator) (?&febmonth) (?&separator) (?&twentyninedays) (?!\d)
            |
            (?&year) (?&separator) (?&longmonthnumber) (?&separator) (?&thirtyonedays) (?!\d)
            |
            (?&year) (?&separator) (?&shortmonthnumber) (?&separator) (?&thirtydays) (?!\d)
            |
            (?&year) (?&separator) (?&febmonth) (?&separator) (?&twentyeightdays) (?!\d)
        )
        (?<mdy>
            (?&febmonth) (?&separator) (?&twentyninedays) (?&separator) (?&leapyear)  (?!\d)
            |
            (?&longmonthnumber) (?&separator) (?&thirtyonedays) (?&separator) (?&year) (?!\d)
            |
            (?&shortmonthnumber) (?&separator) (?&thirtydays) (?&separator) (?&year) (?!\d)
            |
            (?&febmonth) (?&separator) (?&twentyeightdays) (?&separator) (?&year) (?!\d)
        )
        (?<dmy>
            (?&twentyninedays) (?&separator) (?&febmonth) (?&separator) (?&leapyear)  (?!\d)
            |
            (?&thirtyonedays) (?&separator) (?&longmonthnumber) (?&separator)(?&year) (?!\d)
            |
            (?&thirtydays) (?&separator) (?&shortmonthnumber) (?&separator) (?&year) (?!\d)
            |
            (?&twentyeightdays) (?&separator) (?&febmonth) (?&separator)  (?&year) (?!\d)
        )
        (?<date>
            (?&ymd) | (?&mdy) | (?&dmy)
        )
        (?<exact_date>
           ^(?&date)$
       )
    )
}x;

my @test = ( "2009-02-29", "2009-02-28", "2004-02-28", "2004-02-29", "2005-03-31", "2005-04-31", "2005-05-31", 
    "28-02-2009","02-28-2009",        
);

for (@test) {
  if ( $_ =~ m/(?&exact_date) $date_syntax/x ) {
    print "$_ is valid\n";
  }
  else {
    print "$_ is not valid\n";
  }

  if ( $_ =~ m/^(?&ymd) $date_syntax/x ) {
    print "$_ is valid ymd\n";
  }
  else {
    print "$_ is not valid ymd\n";
  }


  if ( $_ =~ m/^(?&leapyear) $date_syntax/x ) {
    print "$_ is leap (start)\n";
  }
  else {
    print "$_ is not leap (start)\n";
  }

  print "\n";
}

请注意添加(?!\d)代码段,这些代码段已添加到

“45”与~= m{(?&twentyeightdays) $syntax}不匹配,因为'4'匹配0?[4]

答案 4 :(得分:1)

似乎这更像是一个如何一致地缩进多线条的问题,如果条件......有很多答案。真正重要的是一致性。 如果您使用perltidy或其他格式化程序,请与其提供的内容保持一致(使用您的配置)。不过,我会从分隔符中缩进正则表达式的一个级别。

你的帖子显示了运行现有代码的一个主要缺陷,比如Perl :: Critic - CPAN示例从原始正则表达式中省略了*。如果你做了很多“清理”,你可能会引入bug,所以我希望你有一个很好的测试套件。