Perl:需要帮助将if-elsif-else转换为更简单的

时间:2017-11-15 05:12:20

标签: perl dispatch-table

我一直在阅读调度表,我对它们如何工作有了一般的了解,但是我在网上看到并将这个概念应用到我最初写的一些代码时遇到了一些麻烦。一个丑陋的if-elsif-else陈述。

我使用GetOpt::Long配置了解析选项,反过来,这些选项会在%OPTIONS哈希中设置一个值,具体取决于所使用的选项。

以下面的代码为例......(更新详细信息

use     5.008008;
use     strict;
use     warnings;
use     File::Basename qw(basename);
use     Getopt::Long qw(HelpMessage VersionMessage :config posix_default require_order no_ignore_case auto_version auto_help);

my $EMPTY      => q{};

sub usage
{
    my $PROG = basename($0);
    print {*STDERR} $_ for @_;
    print {*STDERR} "Try $PROG --help for more information.\n";
    exit(1);
}

sub process_args
{
    my %OPTIONS;

    $OPTIONS{host}              = $EMPTY;
    $OPTIONS{bash}              = 0;
    $OPTIONS{nic}               = 0;
    $OPTIONS{nicName}           = $EMPTY;
    $OPTIONS{console}           = 0;
    $OPTIONS{virtual}           = 0;
    $OPTIONS{cmdb}              = 0;
    $OPTIONS{policyid}          = 0;
    $OPTIONS{showcompliant}     = 0;
    $OPTIONS{backup}            = 0;
    $OPTIONS{backuphistory}     = 0;
    $OPTIONS{page}              = $EMPTY;

    GetOptions
      (
        'host|h=s'              => \$OPTIONS{host}               ,
        'use-bash-script'       => \$OPTIONS{bash}               ,
        'remote-console|r!'     => \$OPTIONS{console}            ,
        'virtual-console|v!'    => \$OPTIONS{virtual}            ,
        'nic|n!'                => \$OPTIONS{nic}                ,
        'nic-name|m=s'          => \$OPTIONS{nicName}            ,
        'cmdb|d!'               => \$OPTIONS{cmdb}               ,
        'policy|p=i'            => \$OPTIONS{policyid}           ,
        'show-compliant|c!'     => \$OPTIONS{showcompliant}      ,
        'backup|b!'             => \$OPTIONS{backup}             ,
        'backup-history|s!'     => \$OPTIONS{backuphistory}      ,
        'page|g=s'              => \$OPTIONS{page}               ,
        'help'                  => sub      { HelpMessage(-exitval => 0, -verbose ->1)     },
        'version'               => sub      { VersionMessage()  },
      ) or usage;

    if ($OPTIONS{host} eq $EMPTY)
    {
        print {*STDERR} "ERROR: Must specify a host with -h flag\n";
        HelpMessage;
    }

    sanity_check_options(\%OPTIONS);

    # Parse anything else on the command line and throw usage
    for (@ARGV)
    {
        warn "Unknown argument: $_\n";
        HelpMessage;
    }

    return {%OPTIONS};
}

sub sanity_check_options
{
    my $OPTIONS     = shift;

    if (($OPTIONS->{console}) and ($OPTIONS->{virtual}))
    {
        print "ERROR: Cannot use flags -r and -v together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{console}) and ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flags -r and -d together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{console}) and ($OPTIONS->{backup}))
    {
        print "ERROR: Cannot use flags -r and -b together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{console}) and ($OPTIONS->{nic}))
    {
        print "ERROR: Cannot use flags -r and -n together\n";
        HelpMessage;
    }

    if (($OPTIONS->{virtual}) and ($OPTIONS->{backup}))
    {
        print "ERROR: Cannot use flags -v and -b together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{virtual}) and ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flags -v and -d together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{virtual}) and ($OPTIONS->{nic}))
    {
        print "ERROR: Cannot use flags -v and -n together\n";
        HelpMessage;
    }

    if (($OPTIONS->{backup}) and ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flags -b and -d together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{backup}) and ($OPTIONS->{nic}))
    {
        print "ERROR: Cannot use flags -b and -n together\n";
        HelpMessage;
    }

    if (($OPTIONS->{nic}) and ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flags -n and -d together\n";
        HelpMessage;
    }

    if (($OPTIONS->{policyid} != 0) and not ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flag -p without also specifying -d\n";
        HelpMessage;
    }

    if (($OPTIONS->{showcompliant}) and not ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flag -c without also specifying -d\n";
        HelpMessage;
    }

    if (($OPTIONS->{backuphistory}) and not ($OPTIONS->{backup}))
    {
        print "ERROR: Cannot use flag -s without also specifying -b\n";
        HelpMessage;
    }

    if (($OPTIONS->{nicName}) and not ($OPTIONS->{nic}))
    {
        print "ERROR: Cannot use flag -m without also specifying -n\n";
        HelpMessage;
    }

    return %{$OPTIONS};
}

我想将上述代码转换为调度表,但无法弄清楚如何操作。

感谢任何帮助。

3 个答案:

答案 0 :(得分:3)

我不确定调度表如何帮助,因为您需要经历特定可能性的成对组合,因此无法通过一次查找触发合适的操作。

这是组织它的另一种方式

use List::MoreUtils 'firstval';

sub sanity_check_options
{
    my ($OPTIONS, $opt_excl) = @_;

    # Check each of 'opt_excl' against all other for ConFLict
    my @excl = sort keys %$opt_excl;
    while (my $eo = shift @excl) 
    {
        if (my $cfl = firstval { $OPTIONS->{$eo} and $OPTIONS->{$_} } @excl) 
        {
            say "Can't use -$opt_excl->{$eo} and -$opt_excl->{$cfl} together";
            HelpMessage();
            last;
        }
    }

    # Go through specific checks on
    # policyid, showcompliant, backuphistory, and nicName
    ...
    return 1;  # or some measure of whether there were errors
}

# Mutually exclusive options
my %opt_excl = (
    console => 'r', virtual => 'v', cmdb => 'c', backup => 'b', nic => 'n'
); 

sanity_check_options(\%OPTIONS, \%opt_excl);

这将检查%opt_excl中列出的所有选项是否存在冲突,删除涉及互斥的(五个)选项的elsif段。它使用List::MoreUtils::firstval。 其他一些特定的调用最好逐一检查。

没有使用返回$OPTIONS,因为它作为参考传递,因此任何更改都适用于原始结构(虽然它也不意味着更改)。也许您可以跟踪是否存在错误并返回它是否可以在调用者中使用,或者只返回1

这解决了所询问的长elsif链,并没有进入其余的代码。这里有一条评论:不需要{%OPTIONS},它复制哈希以创建一个匿名的哈希;只需使用return \%OPTIONS;

评论可能的多个冲突选项

目前的答案不会打印所有冲突的选项,如果有多于两个,则会在评论中由ikegami引发;它会捕获任何冲突,以便中止运行。

为此,代码很容易调整。而不是if块中的代码

  • 设置一个标志,因为检测到冲突并跳出循环,然后打印那些不能彼此使用的标记列表(values %opt_excl)或指向以下用法消息< / p>

  • 收集观察到的冲突;在循环之后打印它们

  • 或者,请参阅ikegami's answer

  • 中的其他方法

但是,人们应该知道允许的程序调用,任何冲突列表都是对健忘用户(或调试辅助工具)的礼貌;无论如何也会打印使用信息。

鉴于冲突选项的数量,使用信息应该有一个突出的注释。还要考虑到这么多冲突的选项可能表明存在设计缺陷。

最后,这段代码完全依赖于这样的事实:每次运行一次这个处理,并使用少数选项进行操作;因此它不关心效率并自由使用辅助数据结构。

答案 1 :(得分:0)

你不应该在这里使用elsif因为多个条件可能是真的。由于可能存在多个条件,因此无法使用调度表。您的代码仍然可以大大简化。

my @errors;

push @errors, "ERROR: Host must be provided\n"
   if !defined($OPTIONS{host});

my @conflicting =
   map { my ($opt, $flag) = @$_; $OPTIONS->{$opt} ? $flag : () }
      [ 'console', '-r' ],
      [ 'virtual', '-v' ],
      [ 'cmdb',    '-d' ],
      [ 'backup',  '-b' ],
      [ 'nic',     '-n' ];

push @errors, "ERROR: Can only use one the following flags at a time: @conflicting\n"
   if @conflicting > 1;

push @errors, "ERROR: Can't use flag -p without also specifying -d\n"
   if defined($OPTIONS->{policyid}) && !$OPTIONS->{cmdb};

push @errors, "ERROR: Can't use flag -c without also specifying -d\n"
   if $OPTIONS->{showcompliant} && !$OPTIONS->{cmdb};

push @errors, "ERROR: Can't use flag -s without also specifying -b\n"
   if $OPTIONS->{backuphistory} && !$OPTIONS->{backup};

push @errors, "ERROR: Can't use flag -m without also specifying -n\n"
   if defined($OPTIONS->{nicName}) && !$OPTIONS->{nic};

push @errors, "ERROR: Incorrect number of arguments\n"
   if @ARGV;

usage(@errors) if @errors;

请注意,上面修复了代码中的大量错误。

帮助与使用错误

  • --help应该向STDOUT提供所请求的帮助,并且不应该导致错误退出代码。
  • 应将使用错误打印到STDERR,并导致错误退出代码。

因此,在两种情况下无差别地呼叫HelpMessage是不正确的。

usage返回false时,创建以下要使用的子GetOptions(不带参数),并在发生其他一些使用错误时显示错误消息:

use File::Basename qw( basename );

sub usage {
   my $prog = basename($0);
   print STDERR $_ for @_;
   print STDERR "Try '$prog --help' for more information.\n";
   exit(1);
}

继续使用HelpMessage来回复--help,但参数的默认值不适合--help。您应该使用以下内容:

'help' => sub { HelpMessage( -exitval => 0, -verbose => 1 ) },

答案 2 :(得分:0)

如果有很多选项,您可以使用调度表。我会以编程方式构建该表。它可能不是最好的选择,但是它的工作原理和配置比elsif构造更具可读性。

use strict;
use warnings;
use Ref::Util::XS 'is_arrayref';    # or Ref::Util

sub create_key {
    my $input = shift;

    # this would come from somewhere else, probably the Getopt config
    my @opts = qw( host bash nic nicName console virtual cmdb
        policyid showcompliant backup backuphistory page );

    # this is to cover the configuration with easier syntax
    $input = { map { $_ => 1 } @{$input} }
        if is_arrayref($input);

    # options are always prefilled with false values
    return join q{}, map { $input->{$_} ? 1 : 0 }
        sort @opts;
}

my %forbidden_combinations = (
    map { create_key( $_->[0] ) => $_->[1] } (
        [ [qw( console virtual )] => q{Cannot use flags -r and -v together} ],
        [ [qw( console cmdb )]    => q{Cannot use flags -r and -d together} ],
        [ [qw( console backup )]  => q{Cannot use flags -r and -b together} ],
        [ [qw( console nic )]     => q{Cannot use flags -r and -n together} ],
    )
);

p %forbidden_combinations; # from Data::Printer

p函数的输出是调度表。

{
    00101   "Cannot use flags -r and -v together",
    00110   "Cannot use flags -r and -n together",
    01100   "Cannot use flags -r and -d together",
    10100   "Cannot use flags -r and -b together"
}

正如您所看到的,我们已经对所有选项进行了排序,以便将它们用作关键字。这样,你可以在理论上构建各种组合,如独家选项。

让我们来看看配置本身。

my %forbidden_combinations = (
    map { create_key( $_->[0] ) => $_->[1] } (
        [ [qw( console virtual )] => q{Cannot use flags -r and -v together} ],
        # ...
    )
);

我们使用数组引用列表。每个条目都在一行上,包含两条信息。使用胖逗号=>可以轻松阅读。第一部分,就像哈希中的 key 一样,是组合。这是一个不应该一起出现的字段列表。数组ref中的第二个元素是错误消息。我删除了所有重复的元素,比如换行符,以便更改错误的显示方式和位置。

此组合配置列表周围的map通过我们的create_key函数运行选项,该函数将其转换为简单的位图样式字符串。我们将其全部分配给该映射的散列和错误消息。

create_key内,我们检查它是否以数组引用作为参数调用。如果是这种情况,则调用是用于构建表,并将它转换为哈希引用,因此我们有一个适当的映射来查找内容。我们知道%OPTIONS总是包含所有存在的键,并且那些预先填充了所有评估为 false 的值。我们可以利用这些将这些值的真实性转换为10,然后构建我们的密钥。

我们马上就会看到为什么这很有用。

现在我们如何使用它?

sub HelpMessage { exit; }; # as a placeholder

# set up OPTIONS
my %OPTIONS = (
    host          => q{},
    bash          => 0,
    nic           => 0,
    nicName       => q{},
    console       => 0,
    virtual       => 0,
    cmdb          => 0,
    policyid      => 0,
    showcompliant => 0,
    backup        => 0,
    backuphistory => 0,
    page          => q{},
);

# read options with Getopt::Long ...
$OPTIONS{console} = $OPTIONS{virtual} = 1;

# ... and check for wrong invocations
if ( exists $forbidden_combinations{ my $key = create_key($OPTIONS) } ) {
    warn "ERROR: $forbidden_combinations{$key}\n";
    HelpMessage;
}

我们现在需要做的就是从Getopt :: Long获取$OPTIONS哈希引用,并通过我们的create_key函数将其转换为地图字符串。然后,我们只需查看%forbidden_combinations调度表中的密钥exists,并显示相应的错误消息。

此方法的优点

如果您想添加更多参数,您只需将它们包含在@opts中即可。在一个完整的实现中,可能会从Getopt调用的配置中自动生成。密钥将在引擎盖下发生变化,但由于这是抽象的,所以你不需要关心。

此外,这很容易阅读。除create_key外,实际的调度表语法非常简洁,甚至还有文档字符。

此方法的缺点

只需一个电话就可以进行大量的编程生成。这当然不是最有效的方式。

为了更进一步,您可以编写为某些场景自动生成条目的函数。

我建议你看一下Mark Jason Dominus' excellent book Higher-Order Perl中的第二章,它可以PDF格式免费下载。