2

我一直在阅读调度表,并大致了解它们是如何工作的,但是我在将我在网上看到的内容并将这个概念应用到我最初编写的一些代码中遇到了一些麻烦,因为 if-elsif 是一团糟-else 语句。

我通过 using 配置了选项解析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};
}

我想把上面的代码变成一个调度表,但不知道怎么做。

任何帮助表示赞赏。

4

3 回答 3

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,因此任何更改都适用于原始结构(虽然它也不意味着要更改)。也许您可以跟踪是否有错误并返回它是否可以在调用者中使用,或者只是 return 1

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


评论可能的多个冲突选项

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

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

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

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

  • 或者,在ikegami 的回答中看到不同的方法

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

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

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

于 2017-11-15T05:39:41.383 回答
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 ) },
于 2017-11-15T05:22:05.883 回答
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"
}

正如你所看到的,我们已经对所有选项进行了 ascii-betly 排序,以将它们用作键。这样,您理论上可以构建各种组合,例如独占期权。

让我们看一下配置本身。

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

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

围绕这个map组合配置列表通过我们的create_key函数运行选项,将其转换为简单的位图样式字符串。我们将所有这些分配给该映射的哈希和错误消息。

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

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

现在我们如何使用它?

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;
}

我们现在需要做的就是$OPTIONS从 Getopt::Long 中获取哈希引用,并将其传递给我们的create_key函数以将其转换为映射字符串。然后我们可以简单地查看调度表exists中是否有该键%forbidden_combinations并显示相应的错误消息。


这种方法的优点

如果要添加更多参数,只需将它们包含在@opts. 在一个完整的实现中,可能会从 Getopt 调用的配置中自动生成。钥匙会在引擎盖下改变,但由于它被抽象出来,你不必关心。

此外,这很容易阅读。顺便create_key说一句,实际的调度表语法非常简洁,甚至具有纪实性。

这种方法的缺点

只需一次调用,就会进行大量程序化生成。这当然不是最有效的方法。


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

我建议您看一下Mark Jason Dominus 的优秀著作Higher-Order Perl的第二章,该书以 PDF 格式免费提供。

于 2017-11-15T12:47:03.317 回答