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

Perl: Need assistance converting if-elsif-else to something simpler

我一直在阅读有关 dispatch tables 的文章,我大致了解了它们的工作原理,但是我在将我在网上看到的内容应用到一些代码中时遇到了一些麻烦最初写成一堆丑陋的 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([=10=]);
    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};
}

我想把上面的代码变成一个 dispatch table,但是不知道该怎么做。

感谢任何帮助。

您不应在此处使用 elsif,因为多个条件可能为真。由于多个条件可能为真,因此不能使用 dispatch table。您的代码仍然可以大大简化。

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 是不正确的。

GetOptions returns 为 false 时创建以下名为 usage 的子项以使用(不带参数),并在出现其他使用错误时显示错误消息:

use File::Basename qw( basename );

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

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

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

我不确定 dispatch table 会有什么帮助,因为您需要通过特定可能性的成对组合,因此无法通过一次查找触发 suitable 操作。

这是另一种组织方式

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。 其他几个特定的​​调用最好一一检查。

没有使用 returning $OPTIONS 因为它是作为参考传递的,所以任何更改都适用于原始结构(虽然它也不意味着要更改)。也许你可以跟踪是否有错误和 return 如果它可以在调用者中使用,或者只是 return 1.

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


评论可能存在的多个冲突选项

目前的答案不会打印所有冲突的选项,如果有两个以上的选项,正如ikegami在评论中提出的那样;它确实捕获了任何冲突,因此 运行 被中止。

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

  • 在检测到冲突时设置一个标志并跳出循环,然后打印那些不能相互使用的列表(values %opt_excl)或指向以下用法消息

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

  • 或者,在

  • 中查看不同的方法

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

考虑到冲突选项的数量,使用消息应该对此有一个显着的注释。还要考虑这么多相互冲突的选项可能表明存在设计缺陷。

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

如果有很多选项,您可以使用分派 table。我会以编程方式构建 table。它可能不是这里的最佳选择,但它可以工作并且配置比您的 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函数的输出是调度table。

{
    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"
}

如您所见,我们已将所有选项按 ascii-betical 方式排序,以将它们用作键。这样,理论上您可以构建各种组合,例如独占选项。

让我们看一下配置本身。

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 中,我们检查它是否以数组引用作为参数调用。如果是这种情况,调用是为了构建 table,我们将其转换为散列引用,以便我们有一个合适的映射来查找内容。我们知道 %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 是否调度 table 并显示相应的错误消息。


这种方法的优点

如果要添加更多参数,只需将它们包含在@opts中即可。在一个完整的实现中,可能会从 Getopt 调用的配置中自动生成。密钥将在后台更改,但由于它已抽象化,因此您不必关心。

此外,这很容易阅读。除了 create_key ,实际的 dispatch table 语法非常简洁,甚至具有纪实性。

这种方法的缺点

只需一次调用即可进行大量编程生成。这当然不是最有效的方法。


要更进一步,您可以编写为特定场景自动生成条目的函数。

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