Perl: нужна помощь в преобразовании if-elsif-else во что-то более простое

Я читал таблицы диспетчеризации, и у меня есть общее представление о том, как они работают, но у меня возникли некоторые проблемы с восприятием того, что я вижу в Интернете, и применением концепции к некоторому коду, который я изначально написал как ужасный беспорядок if-elsif еще заявления.

У меня есть параметры разбора настроен с помощью 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 ответа

Решение

Я не уверен, как таблица диспетчеризации могла бы помочь, поскольку вам нужно пройти через парные комбинации определенных возможностей, и, таким образом, вы не можете инициировать подходящее действие одним поиском.

Вот еще один способ организовать это

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 в комментариях ikegami; это действительно ловит любые конфликты так, чтобы запуск был прерван.

Код легко настраивается для этого. Вместо кода в if блокировать либо

  • установить флаг, так как конфликт обнаружен и вырвался из цикла, затем распечатать список тех, которые нельзя использовать друг с другом (values %opt_excl) или укажите на следующее сообщение об использовании

  • собирать конфликты по мере их наблюдения; напечатать их после цикла

  • или увидеть другой подход в ответе икегами

Однако ожидается, что кто-то знает о разрешенных вызовах программы, и любой список конфликтов является любезностью забывчивого пользователя (или средством отладки); в любом случае сообщение об использовании печатается.

Учитывая количество конфликтующих опций, сообщение об использовании должно иметь заметное примечание по этому вопросу. Также учтите, что очень много противоречивых вариантов могут указывать на недостаток дизайна.

Наконец, этот код полностью опирается на тот факт, что эта обработка выполняется один раз за прогон и работает с несколькими опциями; таким образом, он не связан с эффективностью и свободно использует вспомогательные структуры данных.

Вы можете использовать таблицу диспетчеризации, если вариантов много. Я бы построил эту таблицу программно. Возможно, это не лучший вариант, но он работает, и конфигурация более читаема, чем ваша. 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} ],
        # ...
    )
);

Мы используем список ссылок на массивы. Каждая запись в одной строке и содержит две части информации. Использование жирной запятой => облегчает чтение Первая часть, которая очень похожа на ключ в хэше, это комбинация. Это список полей, которые не должны встречаться вместе. Вторым элементом в массиве ref является сообщение об ошибке. Я удалил все повторяющиеся элементы, такие как символ новой строки, чтобы упростить изменение того, как и где может отображаться ошибка.

map вокруг этого списка конфигурации комбинации запускает опции через наш create_key функция, которая переводит его в простую строку растрового стиля. Мы назначаем все это хешу этой карты и сообщению об ошибке.

внутри create_key, мы проверяем, был ли он вызван со ссылкой на массив в качестве аргумента. Если это так, то вызов был для построения таблицы, и мы конвертировали ее в хеш-ссылку, чтобы у нас была правильная карта для поиска вещей. Мы знаем, что %OPTIONS всегда содержит все ключи, которые существуют, и которые предварительно заполнены значениями, которые все оцениваются как ложные. Мы можем использовать, чтобы преобразовать истинность этих ценностей в 1 или же 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 Кроме того, фактический синтаксис таблицы диспетчеризации весьма лаконичен и даже носит документальный характер.

Недостатки этого подхода

Существует много программных поколений для одного звонка. Это, конечно, не самый эффективный способ сделать это.


Чтобы пойти дальше, вы можете написать функции, которые автоматически генерируют записи для определенных сценариев.

Я предлагаю вам взглянуть на вторую главу превосходной книги Марка Джейсона Доминуса " Perl высшего порядка", которую можно бесплатно скачать в формате PDF.

Вы не должны использовать 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 vs Usage Error

  • --help должен предоставить запрошенную помощь STDOUT и не должен приводить к коду выхода из ошибки.
  • Ошибки использования должны быть напечатаны в STDERR, что должно привести к коду ошибки.

призвание HelpMessage равнодушно в обеих ситуациях поэтому неверно.

Создайте следующий подпункт с именем usage использовать (без аргументов), когда GetOptions возвращает false и с сообщением об ошибке, когда возникает какая-либо другая ошибка использования:

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 ) },
Другие вопросы по тегам