我怎么能写出看起来不像C的Perl?

时间:2009-04-10 20:56:54

标签: perl coding-style

我的同事抱怨我的Perl看起来太像C了,这很自然,因为我大部分时间用C编程,而Perl只是一点点。这是我最近的努力。我对易于理解的Perl很感兴趣。我是一个Perl评论家,对于神秘的Perl几乎没有宽容。但考虑到可读性,下面的代码怎么能更多Perlish?

目标是进行流量分析,找出哪些IP地址在文件“ips”中给出的范围内。这是我的努力:

#!/usr/bin/perl -w

# Process the files named in the arguments, which will contain lists of IP addresses, and see if 
# any of them are in the ranges spelled out in the local file "ip", which has contents of the
# form start-dotted-quad-ip-address,end-dotted-quad-ip_address,stuff_to_be_ignored
use English;


open(IPS,"ips") or die "Can't open 'ips' $OS_ERROR";

# Increment a dotted-quad ip address
# Ignore the fact that part1 could get erroneously large.
sub increment {
    $ip = shift;

    my ($part_1, $part_2, $part_3, $part_4) = split (/\./, $ip);
    $part_4++;
    if ( $part_4 > 255 ) {
        $part_4 = 0;
        ($part_3++);
        if ( $part_3 > 255 ) {
            $part_3 = 0;
            ($part_2++);
            if ( $part_2 > 255 ) {
                $part_2 = 0;
                ($part_1++);
            }
        }
   }   
    return ("$part_1.$part_2.$part_3.$part_4");
}

# Compare two dotted-quad ip addresses.
sub is_less_than {
    $left = shift;
    $right = shift;

    my ($left_part_1, $left_part_2, $left_part_3, $left_part_4)     = split (/\./, $left);
    my ($right_part_1, $right_part_2, $right_part_3, $right_part_4) = split (/\./, $right);


    if  ($left_part_1 != $right_part_1 ) { 
        return ($left_part_1 < $right_part_1);
    }   
    if  ($left_part_2 != $right_part_2 ) { 
        return ($left_part_2 < $right_part_2);
    }   
    if  ($left_part_3 != $right_part_3 ) { 
        return ($left_part_3 < $right_part_3);
    }
    if  ($left_part_4 != $right_part_4 ) {
        return ($left_part_4 < $right_part_4);
    }
    return (false);  # They're equal
}

my %addresses;
# Parse all the ip addresses and record them in a hash.   
while (<IPS>) {
    my ($ip, $end_ip, $junk) = split /,/;
    while (is_less_than($ip, $end_ip) ) {
        $addresses{$ip}=1;
        $ip = increment($ip);
    }
}

# print IP addresses in any of the found ranges

foreach (@ARGV) {
    open(TRAFFIC, $_) or die "Can't open $_ $OS_ERROR";
    while (<TRAFFIC> ) {
        chomp;
        if (defined $addresses{$_}) {
            print "$_\n";
        }
    }
    close (TRAFFIC);

}

15 个答案:

答案 0 :(得分:24)

多年来看到C程序员编写的Perl代码,这里有一些通用的建议:

使用哈希。使用列表。使用哈希!使用LISTS!使用列表操作(map,grep,split,join),尤其适用于小循环。不要使用花式列表算法;流行,拼接,推,转移和不移动更便宜。不要使用树木;哈希更便宜。哈希很便宜,制作它们,使用它们并把它们扔出去!使用迭代器for循环,而不是3-arg。不要调用$ var1,$ var2,$ var3;改为使用列表。不要调用$ var_foo,$ var_bar,$ var_baz;请改用哈希。使用$foo ||= "default"。如果必须输入,请不要使用$_

不要使用原型,这是一个陷阱!!

使用正则表达式,而不是substr()index()。喜欢正则表达。使用/x修饰符使其可读。

在需要无块条件时写statement if $foo。几乎总有一种更好的方法来编写嵌套条件:尝试递归,尝试循环,尝试散列。

在需要时声明变量,而不是在子程序的顶部。用严格。使用警告,并解决所有问题。使用诊断。写测试。写POD。

使用CPAN。使用CPAN!使用CPAN!有人可能已经做得更好了。

运行perlcritic。使用--brutal运行它只是为了踢。运行perltidy。想想你为什么要做所有事情。改变你的风格。

使用不用于语言和调试内存分配的时间来改进代码。

提问。慷慨地对您的代码进行风格评论。去参加Perl Mongers会议。转到perlmonks.org。去YAPC或Perl Workshop。您的Perl知识将实现跨越式发展。

答案 1 :(得分:20)

大多数编写代码为“Perlish”都会利用Perl中的内置函数。

例如,这个:

my ($part_1, $part_2, $part_3, $part_4) = split (/\./, $ip);
$part_4++;
if ( $part_4 > 255 ) {
    $part_4 = 0;
    ($part_3++);
    if ( $part_3 > 255 ) {
        $part_3 = 0;
        ($part_2++);
        if ( $part_2 > 255 ) {
            $part_2 = 0;
            ($part_1++);
        }
    }
}   

我会改写像:

my @parts = split (/\./, $ip);

foreach my $part(reverse @parts){
  $part++;
  last unless ($part > 255 && !($part = 0));
}

这可以解决上面发布的代码所做的事情但是更清洁。

你确定代码能做到你想要的吗?对我来说,如果之后的那个“移动到IP的前一部分”,那么看起来有点奇怪。 255。

答案 2 :(得分:15)

有时,Perlish最常做的事情是转向CPAN而不是编写任何代码。

以下是使用Net::CIDR::LiteNet::IP::Match::Regexp

的快速而肮脏的示例
#!/path/to/perl

use strict;
use warnings;

use English;
use IO::File;
use Net::CIDR::Lite;
use Net::IP::Match::Regexp qw(create_iprange_regexp match_ip);


my $cidr = Net::CIDR::Lite->new();

my $ips_fh = IO::File->new();

$ips_fh->open("ips") or die "Can't open 'ips': $OS_ERROR";

while (my $line = <$ips_fh>) {

    chomp $line;

    my ($start, $end) = split /,/, $line;

    my $range = join('-', $start, $end);

    $cidr->add_range($range);

}

$ips_fh->close();

my $regexp = create_iprange_regexp($cidr->list());

foreach my $traffic_fn (@ARGV) {

    my $traffic_fh = IO::File->new();

    $traffic_fh->open($traffic_fn) or die "Can't open '$traffic_fh': $OS_ERROR";

    while (my $ip_address = <$traffic_fh>) {

        chomp $ip_address;

        if (match_ip($ip_address, $regexp)) {
            print $ip_address, "\n";
        }     

    }

    $traffic_fh->close();

}

免责声明:我刚刚说出来,它的测试很少,没有基准测试。省略完整性检查,错误处理和注释以保持行数减少。不过,我没有在这个空白处吝啬。

至于您的代码:在使用它们之前无需定义您的功能。

答案 3 :(得分:14)

另一个重写示例:

sub is_less_than {
    my $left = shift; # I'm sure you just "forgot" to put the my() here...
    my $right = shift;

    my ($left_part_1, $left_part_2, $left_part_3, $left_part_4)     = split (/\./, $left);
    my ($right_part_1, $right_part_2, $right_part_3, $right_part_4) = split (/\./, $right);


    if  ($left_part_1 != $right_part_1 ) { 
        return ($left_part_1 < $right_part_1);
    }   
    if  ($left_part_2 != $right_part_2 ) { 
        return ($left_part_2 < $right_part_2);
    }   
    if  ($left_part_3 != $right_part_3 ) { 
        return ($left_part_3 < $right_part_3);
    }
    if  ($left_part_4 != $right_part_4 ) {
        return ($left_part_4 < $right_part_4);
    }
    return (false);  # They're equal
}

对此:

sub is_less_than {
    my @left = split(/\./, shift);
    my @right = split(/\./, shift);

    # one way to do it...
    for(0 .. 3) {
        if($left[$_] != $right[$_]) {
            return $left[$_] < $right[$_];
        }
    }

    # another way to do it - let's avoid so much indentation...
    for(0 .. 3) {
        return $left[$_] < $right[$_] if $left[$_] != $right[$_];
    }

    # yet another way to do it - classic Perl unreadable one-liner...
    $left[$_] == $right[$_] or return $left[$_] < $right[$_] for 0 .. 3;

    # just a note - that last one uses the short-circuit logic to condense
    # the if() statement to one line, so the for() can be added on the end.
    # Perl doesn't allow things like do_this() if(cond) for(0 .. 3); You
    # can only postfix one conditional. This is a workaround. Always use
    # 'and' or 'or' in these spots, because they have the lowest precedence.

    return 0 == 1; # false is not a keyword, or a boolean value.
    # though honestly, it wouldn't hurt to just return 0 or "" or undef()
}

另外,这里:

my ($ip, $end_ip, $junk) = split /,/;

$junk可能需要@junk来捕获所有垃圾,您可以将其关闭 - 如果您指定了未知-sized数组到两个元素的“数组”,它将默默地丢弃所有额外的东西。所以

my($ip, $end_ip) = split /,/;

在这里:

foreach (@ARGV) {
    open(TRAFFIC, $_) or die "Can't open $_ $OS_ERROR";
    while (<TRAFFIC> ) {
        chomp;
        if (defined $addresses{$_}) {
            print "$_\n";
        }
    }
    close (TRAFFIC);
}

使用变量来存储文件句柄,而不是TRAFFIC。此外,通常,您应该使用exists()检查哈希元素是否存在,而不是defined() - 它可能存在但设置为undef(这不应该发生在您的程序中) ,但是当你的程序变得更复杂时,这是一个很好的习惯):

foreach (@ARGV) {
    open(my $traffic, $_) or die "Can't open $_ $OS_ERROR";
    while (<$traffic> ) {
        chomp;
        print "$_\n" if exists $addresses{$_};
    }
    # $traffic goes out of scope, and implicitly closes
}

当然,你也可以使用Perl的精彩<>运算符,它打开@ARGV的每个元素进行读取,并作为迭代它们的文件句柄:

while(<>) {
    chomp;
    print "$_\n" if exists $addresses{$_};
}

如前所述,请尽量避免use English use English qw( -no_match_vars );,除非match_vars避免对那些邪恶的use strict;进行重大的性能惩罚。而且还没有被注意到,但应该是......

始终 总是总是use warnings;-w或者Larry Wall会从天而降并破坏你的代码。我看到你有-w - 这已经足够了,因为即使在Unix之外,Perl也会解析shebang行,并会找到你的use warnings;use strict;喜欢它。但是,您需要my。这将在您的代码中捕获许多严重错误,例如不使用false声明变量或使用strict作为语言关键字。

让您的代码在warnings以及strict下工作将导致更清晰的代码从未因您无法理解的原因而中断。您将花费数小时进行调试器调试,最后您可能最终会使用warnings和{{1}}来确定错误是什么。 删除它们,如果(且仅当)您的代码已完成并且您正在发布它并且从不生成任何错误。

答案 4 :(得分:13)

虽然这样做肯定是Perl中的一种方法。

use strict;
use warnings;

my $new_ip;
{
  my @parts = split ('\.', $ip);

  foreach my $part(reverse @parts){
    $part++;

    if( $part > 255 ){
      $part = 0;
      next;
    }else{
      last;
    }
  }
  $new_ip = join '.', reverse @parts;
}

这就是我实际实现它的方式。

use NetAddr::IP;

my $new_ip = ''.(NetAddr::IP->new($ip,0) + 1) or die;

答案 5 :(得分:6)

我不能说这个解决方案会让你的程序更加Perl-ish,但它可能会简化你的算法。

不是将IP地址视为dot-quad,base-256数字,而是需要使用nested-if结构来实现增量函数,而是将IP地址视为32位整数。将a.b.c.d形式的IP转换为整数(未经测试):

sub ip2int {
    my $ip = shift;
    if ($ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) {
        return ($1 << 24) + ($2 << 16) + ($3 << 8) + $4;
    } else {
        return undef;
    }
}

现在很容易确定IP是否落在两个端点IP之间。只需进行简单的整数运算和比较。

$begin = "192.168.5.0";
$end = "192.168.10.255";
$target = "192.168.6.2";
if (ip2int($target) >= ip2int($begin) && ip2int($target) <= ip2int($end)) {
    print "$target is between $begin and $end\n";
} else {
    print "$target is not in range\n";
}

答案 6 :(得分:5)

告诉你的同事他们的perl看起来太像线路噪音了。请不要仅仅为了混淆而混淆你的代码 - 它是asinine的开发目标,就像那些因为不可读而给人们带来如此糟糕的声誉,当它是非常糟糕的程序员(显然,就像你的同事)编写邋code的代码时。结构良好,缩进和逻辑代码是一件好事。 C是件好事。

但是说真的 - 弄清楚如何编写perl的最佳位置是由Damian Conway撰写的O'Reilly“Perl Best Practices”。它告诉你他怎么认为你应该做的事情,他总是给出他的立场的充分理由,偶尔给出不同意的充分理由。我在某些方面不同意他,但他的推理是合理的。你和任何比康威先生更了解perl的人合作的几率非常渺茫,并且拥有一本印刷书籍(或者至少是一个Safari订阅版)可以为你的论点提供更坚实的支持。拿着Perl Cookbook的副本,因为查看解决常见问题的代码示例可以让您走上正确的轨道。我讨厌说“买书”,但这些是任何 perl开发者应阅读的非常好的书。

关于你的具体代码,你使用的是foreach,$_,没有任何分支,转移等等。它看起来很像我的眼睛 - 它已经用perl开发了很长时间而。但需要注意的是 - 我讨厌英语模块。如果您必须使用它,请执行use English qw( -no_match_vars );。 match_vars选项可以显着降低正则表达式解析速度,并且它提供的$PREMATCH / $POSTMATCH变量通常不常用。

答案 7 :(得分:4)

只有一条建议:使用严格。其余部分几乎不相关。

答案 8 :(得分:3)

我确切地知道你的感受。我的第一语言是FORTRAN,就像一个优秀的FORTRAN程序员,我用以下所有语言写FORTRAN:)。

我有这本非常精彩的书Effective Perl Programming,我时不时地重读。特别是一章名为“Idiomatic Perl”。以下是我用来保持Perl看起来像Perl的一些东西:列表运算符,如map和grep,切片和哈希切片,报价运算符。

使我的Perl看起来像FORTRAN / C的另一件事是定期阅读模块源,特别是主人的那些。

答案 9 :(得分:2)

您可以使用Acme::BleachAcme::Morse

答案 10 :(得分:2)

虽然这样可行:

use strict;
use warnings;
use 5.010;

use NetAddr::IP;

my %addresses;
# Parse all the ip addresses and record them in a hash.
{
  open( my $ips_file, '<', 'ips') or die;

  local $_; # or my $_ on Perl 5.10 or later
  while( my $line = <$ips_file> ){
    my ($ip, $end_ip) = split ',', $line;
    next unless $ip and $end_ip;

    $ip     = NetAddr::IP->new( $ip, 0 ) or die;
    $end_ip = NetAddr::IP->new( $end_ip ) or die;
    while( $ip <= $end_ip ){
      $addresses{$ip->addr} = 1;
      $ip++;
    }
  }
  close $ips_file
}

# print IP addresses in any of the found ranges
use English;

for my $arg (@ARGV) {
  open(my $traffic, '<',$arg) or die "Can't open $arg $OS_ERROR";
  while( my $ip = <$traffic> ){
    chomp $ip;
    if( $addresses{$ip} ){
      say $ip
    }
  }
  close ($traffic);
}

如果可能的话我会使用netmasks,因为它变得更简单:

use Modern::Perl;
use NetAddr::IP;

my @addresses;
{
  open( my $file, '<', 'ips') or die;

  while( (my $ip = <$file>) =~ s(,.*){} ){
    next unless $ip;
    $ip = NetAddr::IP->new( $ip ) or die;
    push @addresses, $ip
  }

  close $file
}


for my $filename (@ARGV) {
  open( my $traffic, '<', $filename )
    or die "Can't open $filename";

  while( my $ip = <$traffic> ) {
    chomp $ip;
    next unless $ip;

    $ip = NetAddr::IP->new($ip) or next; # skip line on error
    my @match;
    for my $cmp ( @addresses ){
      if( $ip->within($cmp) ){
        push @match, $cmp;
        #last;
      }
    }

    say "$ip => @match" if @match;

    say "# no match for $ip" unless @match;
  }
  close ($traffic);
}

测试ips文件:

192.168.0.1/24
192.168.0.0
0:0:0:0:0:0:C0A8:0/128

测试traffic文件:

192.168.1.0
192.168.0.0
192.168.0.5

输出:

# no match for 192.168.1.0/32
192.168.0.0/32 => 192.168.0.1/24 192.168.0.0/32 0:0:0:0:0:0:C0A8:0/128
192.168.0.5/32 => 192.168.0.1/24

答案 11 :(得分:1)

而不是这样做:


if  ($left_part_1 != $right_part_1 ) { 
    return ($left_part_1 < $right_part_1);
}

你可以这样做:


return $left_part_1 < $right_part_1 if($left_part_1 != $right_part_1);

此外,您可以使用Fatal模块,以避免检查错误。

答案 12 :(得分:1)

我用于“我的代码看起来如何”的唯一标准是阅读和理解代码的目的是多么容易(特别是不熟悉Perl的程序员),而不是它是否遵循特别的风格。

如果Perl语言功能使某些逻辑更容易理解,那么我使用它,如果不是我不使用它 - 即使它可以用更少的代码来完成它。

你的同事可能认为我的代码非常“非常有用”,但我敢打赌他们完全理解代码正在做什么,并且可以修改它以便毫无困难地修复/扩展它:

我的版本:

#******************************************************************************
# Load the allowable ranges into a hash
#******************************************************************************
my %ipRanges = loadIPAddressFile("../conf/ip.cfg");

#*****************************************************************************
# Get the IP to check on the command line
#*****************************************************************************
my ( $in_ip_address ) = @ARGV;

# Convert it to number for comparison
my $ipToCheckNum = 1 * sprintf("%03d%03d%03d%03d", split(/\./, $in_ip_address));

#*****************************************************************************
# Loop through the ranges and see if the number is in any of them
#*****************************************************************************
my $startIp;
my $endIp;
my $msg = "IP [$in_ip_address] is not in range.\n";

foreach $startIp (keys(%ipRanges))
   {
   $endIp = $ipRanges{$startIp};

   if ( $startIp <= $ipToCheckNum and $endIp >= $ipToCheckNum ) 
      {
      $msg = "IP [$in_ip_address] is in range [$startIp] to [$endIp]\n";
      }
   }

print $msg;

#******************************************************************************
# Function: loadIPAddressFile()
#   Author: Ron Savage
#     Date: 04/10/2009
# 
# Description:
# loads the allowable IP address ranges into a hash from the specified file.
# Hash key is the starting value of the range, value is the end of the range.
#******************************************************************************
sub loadIPAddressFile
   {
   my $ipFileHandle;
   my $startIP;
   my $endIP;
   my $startIPnum;
   my $endIPnum;
   my %rangeList;

   #***************************************************************************
   # Get the arguments sent
   #***************************************************************************
   my ( $ipFile ) = @_;

   if ( open($ipFileHandle, "< $ipFile") )
      {
      while (<$ipFileHandle>)
         {
         ( $startIP, $endIP ) = split(/\,/, $_ );

         # Convert them to numbers for comparison
         $startIPnum = 1 * sprintf("%03d%03d%03d%03d", split(/\./, $startIP));
         $endIPnum   = 1 * sprintf("%03d%03d%03d%03d", split(/\./, $endIP));

         $rangeList{$startIPnum} = $endIPnum;
         }

      close($ipFileHandle);
      }
   else
      {
      print "Couldn't open [$ipFile].\n";
      }

   return(%rangeList);
   }

(注意:额外的“#”行是为了保留我的freakin'间距,在这里发布代码总是会受到影响)

答案 13 :(得分:0)

我错过了什么......以上任何一个阵列版本都可以使用吗? mod对for循环的局部变量执行。我认为Brad Gilbert的Net :: IP解决方案将是我的选择。克里斯·卢茨(Chris Lutz)以我的方式对其余部分进行了清理。

顺便说一下 - 关于可读性的一些评论让我感到好奇。是否有关于Erlang / Lisp语法可读性的[强烈]抱怨,因为只有一种方法可以在其中编写代码?

答案 14 :(得分:0)

这可能更像是C,但也更简单:

use Socket qw(inet_aton inet_ntoa);

my $ip = ("192.156.255.255");

my $ip_1 = inet_ntoa(pack("N", unpack("N", inet_aton($ip))+1));
print "$ip $ip_1\n";

更新:我在阅读问题中的所有代码之前发布了这个。这里的代码只是增加了ip地址。