Softpanorama

May the source be with you, but remember the KISS principle ;-)
Home Switchboard Unix Administration Red Hat TCP/IP Networks Neoliberalism Toxic Managers
(slightly skeptical) Educational society promoting "Back to basics" movement against IT overcomplexity and  bastardization of classic Unix

Perl options processing

Old News
;-)

Perl options processing

Recommended Books Recommended Links

Getopt::Std

Getopt::Long

Perl Language

Generating Help screen     Perl Tips History Humor Etc

Under modern command shells, including those on Unix and Windows, options can be either letters or words. Programs that accept single letters might be invoked like program -a -b -c. Or, they might look be invoked as program -abc, meaning the same thing. If the options take values, you can bundle them together: -aw80L24x is equivalent to -a -w 80 -L 24 -x. With option words, you sacrifice brevity for clarity: program --all --width=80 --length 24 --extend.

In either case, options precede other program arguments (the /tmp in ls -l /tmp) and the parsing stops as soon as a non-option argument is encountered. A double dash by itself immediately terminates option parsing.

These conventions aren't universal but they are sign of good style and it is preferable to adhere to them.

Parsing options in Perl isn't very hard, but reinventing the bycile does not make much sense unless Perl provided solution suiffers from acute overcomplexiry.

There is. In fact, there are several ways to process options ion Perl without writing your own code:

-s Switch

Perl has a built-in support the single-character style options if invoked with the -s switch. For example:

perl -s script.pl -f -b myfile.dat

Perl will remove anything that looks like an option (-f and -b) from the command line and set the variables ($f and $b) to true. Here the options should be preceded with a single dash. When Perl encounters an argument without the dash, it finishes options proccessing.

Getopt::Std

Standard module Getopt::Std parse single letter options. It allow bungling ( -abc) provides two subroutines, getopt() and getopts(). Each expects a single dash before option letters and stops processing options when the first non-option is detected.

Getopt::Long

Getopt::Long provides the GetOptions() function, which gives you more control over command line options. It provides support for:

Other features:

Option Words. In its standard configuration, GetOptions() handles option words, ignoring case. Options may be abbreviated, as long as the abbreviations are unambiguous. Options and other command line arguments can be mixed; options will be processed first, and the other arguments will remain in @ARGV.

This call to GetOptions() allows a single option, -foo.

GetOptions ('foo' => \$doit);

When the user provides -foo on the command line, $doit is set to 1. In this call, -foo is called the option control string, and \$doit is called the option destination. Multiple pairs of control strings and destinations can be provided. GetOptions() will return true if processing was successful, and false otherwise, displaying an error message with warn().

The option word may have aliases, alternative option words that refer to the same option:

GetOptions ('foo|bar|quux' => \$doit);

If you want to specify that an option takes a string, append =s to the option control string:

GetOptions ('foo=s' => \$thevalue);

When you use a colon instead of an equal sign, the option takes a value only when one is present:

GetOptions ('foo:s' => \$thevalue, 'bar' => \$doit);

Calling this program with arguments -foo bar blech places the string 'bar' in $thevalue, but when called with -foo -bar blech, something different happens: $thevalue is set to an empty string, and $bar is set to 1.

These options can also take numeric values; you can use =i or :i for integer values, and =f or :f for floating point values.

Using and Bundling Single-Letter Options. Using single-letter options is trivial; bundling them is a little trickier. Getopt::Long has a Configure() subroutine that you can use to fine-tune your option parsing. For bundling single-letter options, you would use Getopt::Long::Configure ('bundling'). Now GetOptions() will happily accept bundled single-letter options:

Getopt::Long::Configure ('bundling');
GetOptions ('a' => \$all,
            'l=i' => \$length,
            'w=i' => \$width);

This allows options of the form -a -l 24 -w 80 as well as bundled forms: -al24w80. You can mix these with option words:

GetOptions ('a|all' => \$all,
            'l|length=i' => \$length,
            'w|width=i' => \$width);		

However, the option words require a double dash: --width 24 is acceptable, but -width 24 is not. (That causes the leading w to be interpreted as -w, and results in an error because idth isn't a valid integer value.

Getopt::Long::Configure('bundling_override') allows option words with a single dash, where the words take precedence over bundled single-letter options. For example:

Getopt::Long::Configure ('bundling_override');
GetOptions ('a' => \$a, 'v' => \$v,
            'x' => \$x, 'vax' => \$vax);

This treats -axv as -a -x -v, but treats -vax as a single option word. 

Advanced destinations. You don't need to specify the option destination. If you don't, GetOptions() defines variables $opt_xxx (where xxx is the name of the option), just like getopt() and getopts(). Similarly, GetOptions() also accepts a reference to a hash (as its first argument) and places the option values in it.

If you do specify the option destination, it needn't be a scalar. If you specify an array reference, option values are pushed into this array:

GetOptions ('foo=i' => \@values);

Calling this program with arguments -foo 1 -foo 2 -foo 3 sets @values to (1,2,3).

The option destination can also be a hash reference:

my %values;
GetOptions ('define=s' => \%values);

If you call this program as program -define EOF=-1 -define bool=int, the %values hash will have the keys EOF and bool, set to -1 and 'int' respectively.

Finally, the destination can be a reference to a subroutine. This subroutine will be called when the option is handled. It expects two arguments: the name of the option and the value.

The special option control string '<>' can be used in this case to have a subroutine process arguments that aren't options. This subroutine is then called with the name of the non-option argument. Consider:

GetOptions ('x=i' => \$x, '<>' => \&doit);

When you execute this program with -x 1 foo -x 2 bar this invokes doit() with argument 'foo' (and $x equal to 1, and then calls doit() with argument 'bar' (and $x equal to 2).

Other Configurations. GetOptions() supports several other configuration characteristics. For a complete list, see the Getopt::Long documentation.

Getopt::Long::Configure ('no_ignore_case') matches option words without regard to case.

Getopt::Long::Configure ('no_auto_abbrev') prevents abbreviations for option words.

Getopt::Long::Configure ('require_order') stops detecting options after the first non-option command line argument.

Generating Help screen

 People often ask me why GetOptions() doesn't provide facilities for help message. There are two reasons. The first reason is that while command line options adhere to conventions, help messages don't. Any style of message would necessarily please some people and annoy others, and would make calls to GetOptions() much lengthier and more confusing.

The second reason is that Perl allows a program to contain its own documentation in POD (Plain Old Documentation) format, and there are already modules that extract this information to supply help messages. The following sub-routine uses Pod::Usage for this purpose (and demonstrates how Pod::Usage can be loaded on demand):

sub options () {
    my $help = 0;       # handled locally
    my $ident = 0;      # handled locally
    my $man = 0;        # handled locally
	
    # Process options.
    if ( @ARGV > 0 ) {
        GetOptions('verbose'=> \$verbose,
                           'trace' => \$trace,
                           'help|?' => \$help,
                           'manual' => \$man,
                           'debug' => \$debug)
            or pod2usage(2);
    }
    if ( $man or $help ) {
        # Load Pod::Usage only if needed.
        require "Pod/Usage.pm";
        import Pod::Usage;
        pod2usage(1) if $help;
        pod2usage(VERBOSE => 2) if $man;
    }
}

Pod::Usage is available at http://www.perl.com/CPAN/modules/authors/Brad_Appleton. The latest version of Getopt::Long (2.17 as of this writing) can be found in authors/Johan_Vromans. This kit also contains a script template that uses both Getopt::Long and Pod::Usage.

OTHER OPTION HANDLING MODULES

A few other option handling modules can be found on the CPAN. The following modules can be downloaded from http://www.perl.com/CPAN/modules/by-category/12_Option_Argument_Parameter_Processing.


Top Visited
Switchboard
Latest
Past week
Past month

NEWS CONTENTS

Old News ;-)

[Sep 30, 2020] How to process command line arguments in Perl using Getopt--Long by Gabor Szabo

Oct 30, 2014 | perlmaven.com

When a Perl script is executed the user can pass arguments on the command line in various ways. For example perl program.pl file1.txt file2.txt or perl program.pl from-address to-address file1.txt file2.txt or, the most common and most useful way:

perl program.pl -vd --from from-address --to to-address file1.txt file2.txt

How can we deal with this information?

When the scripts starts to run, Perl will automatically create an array called @ARGV and put all the values on the command line separated by spaces in that variable. It won't include perl and it won't include the name of our script ( program.pl in our case), that will be placed in the $0 variable. @ARGV will only include the values located after the name of the script.

In the above case @ARGV will contain: ('-vd', '--from', 'from-address', '--to', 'to-address', 'file1.txt', 'file2.txt')

We can access @ARGV manually as described in the article about @ARGV , but there are a number of modules that will handle most of the work for you. In this article we'll see Getopt::Long a module that also comes with the standard installation of Perl.

Explain the command line

Just before doing that, let's see what is really our expectation from the command line processing.

There can be lots of other requirements and Getopt::Long can handle quite a few of them, but we'll focus on the basics.

Getopt::Long

Getopt::Long exports a function called GetOptions , that can process the content of @ARGV based on the configuration we give to it. It returns true or false indicating if the processing was successful or not. During processing it removes the items from @ARGV that have been successfully recognized. We'll take a look at possible errors later on. For now, let' see a small example we save in cli.pl :

  1. use strict ;
  2. use warnings ;
  3. use 5.010 ;
  4. use Getopt :: Long qw ( GetOptions );
  5. my $source_address ;
  6. GetOptions ( 'from=s' => \$source_address ) or die "Usage: $0 --from NAME\n" ;
  7. if ( $source_address ) {
  8. say $source_address ;
  9. }

After loading the module we declare a variable called $source_address where the value of the --from command line flag will be stored. We call GetOptions with key-value pairs. The keys (in this case one key) is the description of the flag. In this case the from=s declares that we are expecting a command line parameter called --from with a string after it. Because in Perl numbers can also be seen as strings, this basically means "pass me any value". This declaration is then mapped to the variable we declared earlier. In case the syntax is unclear => is a "fat arrow" you might be familiar from hashes and the back-slash \ in-front of the variable indicates that we are passing a reference to the variable. You don't need to understand references in order understand this code. Just remember that the variables on the right hand side of the "fat comma" operators need to have a back-slash when calling GetOptions .

We can run this program in several ways: perl cli.pl --from Foo will print "Foo". The value passed after the -from flag is assigned to the $source_address variable. On the other hand running perl cli.pl will not print anything as we have no passed any value.

If we run it perl cli.pl Foo it won't print anything either, as GetOptions only deals with options that start with a dash ( - ). (This is actually configurable, but let's not get there now.)

Failures

So when will the short circuit or die kick-in?

Unknown option

If we run the script passing something that looks like a parameter name, but which has not been declared when calling GetOptions . Something that starts with a dash - . For example:

perl cli.pl --to Bar

Unknown option: to
Usage: cli.pl --from NAME

The first line is a warning printed by GetOptions , the second line is the string we generated using die .

Option requires an argument

Another case is when we run the script, pass --from , but without passing any value after it:

perl cli.pl --from

In that case the output will look like this:

Option from requires an argument
Usage: cli.pl --from NAME

Here too, the first line was from GetOptions and the second line from our call to die . When we called GetOptions we explicitly said =s that we are expecting a string after the --from .

Default values

Often we would like to give a default value to one of the options. For example in the case of the --from field we might want it to default to the word 'Maven'. We can do it by assigning this value to the $source_address variable before calling GetOptions . For example, at the time we declare it using my .

  1. my $source_address = 'Maven' ;
  2. GetOptions ( 'from=s' => \$source_address ) or die "Usage: $0 --from NAME\n" ;
  3. if ( $source_address ) {
  4. say $source_address ;
  5. }

If the user does not pass the --from flag then GetOptions will not modify the value in the $source_address variable. Running perl cli.pl will result in "Maven".

Flags without value

In addition to parameters that require a value, we also would like to allow flags. Names, that by their presence make a difference. These things are used when we want to allow the users to turn on debugging, or to set the verbosity of the script.

  1. use strict ;
  2. use warnings ;
  3. use 5.010 ;
  4. use Getopt :: Long qw ( GetOptions );
  5. my $debug ;
  6. GetOptions ( 'debug' => \$debug ) or die "Usage: $0 --debug\n" ;
  7. say $debug ? 'debug' : 'no debug' ;

Originally the $debug variable contained undef which is considered to be false in Perl. If the user passes the --debug flag, the corresponding variable will be set to some true value. (I think it is the number one, but we should only rely on the fact that it evaluates to true.) We then use the ternary operator to decide what to print.

The various ways we call it and the output they produce:

$ perl cli.pl 
no debug

$ perl cli.pl --debug
debug

$ perl cli.pl --debug hello
debug

The last example shows that values placed after such name are disregarded.

Multiple flags

Obviously, in most of the scripts you will need to handle more than one flag. In those cases we still call GetOptions once and provide it with all the parameters:

Combining the above two cases together we can have a larger example:

  1. use strict ;
  2. use warnings ;
  3. use 5.010 ;
  4. use Getopt :: Long qw ( GetOptions );
  5. my $debug ;
  6. my $source_address = 'Maven' ;
  7. GetOptions (
  8. 'from=s' => \$source_address ,
  9. 'debug' => \$debug ,
  10. ) or die "Usage: $0 --debug --from NAME\n" ;
  11. say $debug ? 'debug' : 'no debug' ;
  12. if ( $source_address ) {
  13. say $source_address ;
  14. }

Running without any parameter will leave $debug as undef and the $source_address as 'Maven':

$ perl cli.pl 
no debug
Maven

Passing --debug will set $debug to true, but will leave $source_address as 'Maven':

$ perl cli.pl --debug
debug
Maven

Passing --from Foo will set the $source_address but leave $debug as undef :

$ perl cli.pl  --from Foo
no debug
Foo

If we provide parameters, they will both set the respective variables:

$ perl cli.pl --debug --from Foo
debug
Foo

The order of the parameters on the command line does not matter:

$ perl cli.pl  --from Foo --debug
debug
Foo
Short names

Getopt::Long automatically handles shortening of the option names up to ambiguity. We can run the above script in the following manner:

$ perl cli.pl --fr Foo --deb
debug
Foo

We can even shorten the names to a single character:

$ perl cli.pl --f Foo --d
debug
Foo

and in that case we can even use single-dash - prefixes:

$ perl files/cli.pl -f Foo -d
debug
Foo

These however are not really single-character options, and as they are they cannot be combined:

$ perl cli.pl -df Foo
Unknown option: df
Usage: cli.pl --debug  --from NAME
Single-character options

In order to combine them we need two do two things. First, we need to declare the options as real single-character options. We can do this by providing alternate, single-character names in the definition of the options:

  1. GetOptions (
  2. 'from|f=s' => \$source_address ,
  3. 'debug|d' => \$debug ,
  4. ) or die "Usage: $0 --debug --from NAME\n" ;

The second thing is that we need to enable the gnu_getopt configuration option of Getopt::Long by calling Getopt::Long::Configure qw(gnu_getopt);

  1. use Getopt :: Long qw ( GetOptions );
  2. Getopt :: Long :: Configure qw ( gnu_getopt );

After doing that we can now run

$ perl cli.pl -df Foo
debug
Foo

The full version of the script with the above changes looks like this:

  1. use strict ;
  2. use warnings ;
  3. use 5.010 ;
  4. use Getopt :: Long qw ( GetOptions );
  5. Getopt :: Long :: Configure qw ( gnu_getopt );
  6. use Data :: Dumper ;
  7. my $debug ;
  8. my $source_address = 'Maven' ;
  9. GetOptions (
  10. 'from|f=s' => \$source_address ,
  11. 'debug|d' => \$debug ,
  12. ) or die "Usage: $0 --debug --from NAME\n" ;
  13. say $debug ? 'debug' : 'no debug' ;
  14. if ( $source_address ) {
  15. say $source_address ;
  16. }
Non-affiliated values

The GetOptions function only handles the parameters that start with a dash and their corresponding values, when they are relevant. Once it processed the options it will remove them from @ARGV . (Both the option name and the option value will be removed.) Any other, non-affiliated values on the command line will stay in @ARGV . Hence if we add Data::Dumper to our script and use that to print the content of @ARGV at the end ( print Dumper \@ARGV ) as in this script:

  1. use strict ;
  2. use warnings ;
  3. use 5.010 ;
  4. use Getopt :: Long qw ( GetOptions );
  5. use Data :: Dumper ;
  6. my $debug ;
  7. my $source_address = 'Maven' ;
  8. GetOptions (
  9. 'from=s' => \$source_address ,
  10. 'debug' => \$debug ,
  11. ) or die "Usage: $0 --debug --from NAME\n" ;
  12. say $debug ? 'debug' : 'no debug' ;
  13. if ( $source_address ) {
  14. say $source_address ;
  15. }
  16. print Dumper \@ARGV ;

We get the following results:

$ perl files/cli.pl  -f Foo -d file1.txt file2.txt
debug
Foo
$VAR1 = [
          'file1.txt',
          'file2.txt'
        ];

After processing the options, file1.txt and file2.txt were left in @ARGV . We can now do whatever we want with them, for example we can iterate over the @ARGV array using foreach .

Advanced

Getopt::Long has tons of other options. You might want to check out the documentation.

There are also other solutions, for example if you are using Moo for light-weight object oriented programming, you could take a look at MooX::Options explained in a number of advanced articles: for example Switching to Moo - adding command line parameters and Writing Command line scripts and accepting command line parameters using Moo .

[Nov 22, 2017] Options parsing configured by using GetOpt::Long

Nov 22, 2017 | stackoverflow.com

down vote favorite

Speeddymon, 2 days ago

I've been reading up on dispatch tables and I get the general idea of how they work, but I'm having some trouble taking what I see online and applying the concept to some code I originally wrote as an ugly mess of if-elsif-else statements.

I have options parsing configured by using GetOpt::Long , and in turn, those options set a value in the %OPTIONS hash, depending on the option used.

Taking the below code as an example... ( UPDATED WITH MORE DETAIL

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

I'd like to turn the above code into a dispatch table, but can't figure out how to do it.

Any help is appreciated.

Jim Garrison ,2 days ago

Are the sets of conflicting options always pairs? Can you have situations where options a , b , and c cannot occur together but any two are OK? Before you can pick a representation you need to be sure your model can handle the logic you need in a general way. This is not an easy problem. – Jim Garrison 2 days ago

simbabque ,yesterday

Don't use English, it's horribly slow and makes your code harder to read. – simbabque yesterday

Speeddymon ,yesterday

Removed English module and changed $ARG / @ARG to $_ / @_ Added $EMPTY as I forgot I had it defined globally. – Speeddymon yesterday

Speeddymon ,yesterday

@JimGarrison -- you are correct. The if-elsif-else does not explicitly account for 3 options that conflict (though it does account for that implicitly) As an example, using -h is required with all of the other options. But, using -h , -r , v , all together is not allowed, while -h , -r , and -d is allowed. – Speeddymon yesterday

ikegami ,yesterday

Since the host must be provided, it should be an argument, not an option. – ikegami yesterday

zdim ,2 days ago

I am not sure how a dispatch table would help since you need to go through pair-wise combinations of specific possibilities, and thus cannot trigger a suitable action by one lookup.

Here is another way to organize it

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

This checks all options listed in %opt_excl against each other for conflict, removing the segments of elsif involving the (five) options that are mutually exclusive. It uses List::MoreUtils::firstval . The few other specific invocations are best checked one by one.

There is no use of returning $OPTIONS since it is passed as reference so any changes apply to the original structure (while it's not meant to be changed either). Perhaps you can keep track of whether there were errors and return that if it can be used in the caller, or just return 1 .

This addresses the long elsif chain as asked, and doesn't go into the rest of code. Here is one comment though: There is no need for {%OPTIONS} , which copies the hash in order to create an anonymous one; just use return \%OPTIONS;


Comment on possible multiple conflicting options

This answer as it stands does not print all conflicting options that have been used if there are more than two, as raised by ikegami in comments; it does catch any conflicts so that the run is aborted.

The code is readily adjusted for this. Instead of the code in the if block either

However, one is expected to know of allowed invocations and any listing of conflicts is a courtesy to the forgetful user (or a debugging aid); a usage message is printed as well anyway.

Given the high number of conflicting options the usage message should contain a prominent note on this. Also consider that so many conflicting options may indicate a design flaw.

Finally, this code fully relies on the fact that this processing goes once per run and operates with a handful of options; thus it is not concerned with efficiency and freely uses ancillary data structures.

Speeddymon ,yesterday

Updated the question to clarify. – Speeddymon yesterday

zdim ,yesterday

@Speeddymon Thank you, updated. This brings together checks of those five options which can't go one with another. The remaining few I leave to be checked one by one; "encoding" one or two possibilities in some all-encompassing system would just increase complexity (and may end up less readable). – zdim yesterday

zdim ,yesterday

@Speeddymon Added the missing include, use List::MoreUtils 'firstval' . Edited a little in the meanwhile, as well. – zdim yesterday

Speeddymon ,yesterday

Thank you for the easy to follow example. I went with yours as it was the clearest and contained the least duplicate code. – Speeddymon yesterday

ikegami ,23 hours ago

@Speeddymon, Apparently, it's not clear as you think since you didn't realize if doesn't work. It doesn't mention the error of using -r and -c together if -b is also provided. And why is a hash being used at all? Wasteful and needlessly complex. – ikegami 23 hours ago

simbabque ,yesterday

You can use a dispatch table if there are a lot of options. I would build that table programmatically. It might not be the best option here, but it works and the configuration is more readable than your elsif construct.
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

The output of the p function is the dispatch 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"
}

As you can see, we've sorted all the options ascii-betically to use them as keys. That way, you could in theory build all kinds of combinations like exclusive options.

Let's take a look at the configuration itself.

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

We use a list of array references. Each entry is on one line and contains two pieces of information. Using the fat comma => makes it easy to read. The first part, which is much like a key in a hash, is the combination. It's a list of fields that should not occur together. The second element in the array ref is the error message. I've removed all the recurring elements, like the newline, to make it easier to change how and where the error can be displayed.

The map around this list of combination configuration runs the options through our create_key function, which translates it to a simple bitmap-style string. We assign all of it to a hash of that map and the error message.

Inside create_key , we check if it was called with an array reference as its argument. If that's the case, the call was for building the table, and we convert it to a hash reference so we have a proper map to look stuff up in. We know that the %OPTIONS always contains all the keys that exist, and that those are pre-filled with values that all evaluate to false . We can harness that convert the truthiness of those values to 1 or 0 , which then builds our key.

We will see in a moment why that is useful.

Now how do we use this?

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

All we need to do now is get the $OPTIONS hash reference from Getopt::Long, and pass it through our create_key function to turn it into the map string. Then we can simply see if that key exists in our %forbidden_combinations dispatch table and show the corresponding error message.


Advantages of this approach

If you want to add more parameters, all you need to do is include them in @opts . In a full implementation that would probably be auto-generated from the config for the Getopt call. The keys will change under the hood, but since that is abstracted away you don't have to care.

Furthermore, this is easy to read. The create_key aside, the actual dispatch table syntax is quite concise and even has documentary character.

Disadvantages of this approach

There is a lot of programmatic generation going on for just a single call. It's certainly not the most efficient way to do it.


To take this further, you can write functions that auto-generate entries for certain scenarios.

I suggest you take a look at the second chapter in Mark Jason Dominus' excellent book Higher-Order Perl , which is available for free as a PDF.

Speeddymon ,yesterday

Thank you for the detailed answer. I've updated the question to help clarify how the $OPTIONS hash is setup. Can your example work within the bounds of what I have already, or should I rewrite the whole thing from scratch? – Speeddymon yesterday

simbabque ,yesterday

@Speeddymon yeah, that should work. I see you've got %OPTIONS , and it is always pre-set with values. That's going to be interesting. Let me try. – simbabque yesterday

Speeddymon ,yesterday

Speaking of the HOP book... That was actually what I was using to try to learn and where I was having trouble in applying the concept to my code. :-) I couldn't find a PDF version before, so thank you for the link! – Speeddymon yesterday

simbabque ,yesterday

@Speeddymon I've updated the answer and changed it to match your updated code. I suggest you read the diff first. What I don't like about it yet is that the possible keys are there twice, but that can be solved with some more trickery. I think that would blow up the answer even more, so I didn't do that. – simbabque yesterday

ikegami ,23 hours ago

Doesn't detect the case when -r , -v and -b are provided as an error. – ikegami 23 hours ago

ikegami ,2 days ago

You shouldn't be using elsif here because multiple condition could be true. And since multiple conditions could be true, a dispatch table can't be used. Your code can still be simplified greatly.
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;

Note that the above fixes numerous errors in your code.


Help vs Usage Error

Calling HelpMessage indifferently in both situations is therefore incorrect.

Create the following sub named usage to use (without arguments) when GetOptions returns false, and with an error message when some other usage error occurs:

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

Keep using HelpMessage in response to --help , but the defaults for the arguments are not appropriate for --help . You should use the following:

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

Speeddymon ,yesterday

I wondered if it would be impossible because of multiple conditions being true, but based on other answers, it seems that it is possible to still build a table and compare... – Speeddymon yesterday

ikegami ,yesterday

What are you talking about? No answer used a dispatch table. All the answers (including mine) used a ( for or map ) loop that performs as many checks as there are conditions. The points of a dispatch table is to do a single check no matter how many conditions there are. Since all conditions can be true, you need to check all conditions, so a dispatch table is impossible by definition. (And that's without even mentioning that the value of a dispatch table should be a code reference or similar (something to dispatch to).) – ikegami yesterday

ikegami ,yesterday

The difference between mine and the others is that mine avoids using an inefficient unordered hash and uses an efficient ordered list instead. (You could place the list in an array if you prefer.) – ikegami yesterday

ikegami ,yesterday

Updated to match updated question. That fact that none of the other answers can be extended for your updated question proves my pointthat trying to put everything into one loop or table just makes things less flexible, longer and more complex. – ikegami yesterday

Speeddymon ,yesterday

In response to the "help" tip -- HelpMessage is defined by GetOpt::Long and reads from the PODs at the end of the file. – Speeddymon yesterday

Recommended Links

Google matched content

Softpanorama Recommended

Top articles

Sites

Getopt--Std - Process single-character switches with switch clustering - Perldoc Browser

How to process command line arguments in Perl using Getopt--Long

A Perl getopts example - alvinalexander.com

Getopt--Long - Extended processing of command line options - Perldoc Browser

Perl getopts Howto

Reference- GetOptions (Perl in a Nutshell)



Etc

Society

Groupthink : Two Party System as Polyarchy : Corruption of Regulators : Bureaucracies : Understanding Micromanagers and Control Freaks : Toxic Managers :   Harvard Mafia : Diplomatic Communication : Surviving a Bad Performance Review : Insufficient Retirement Funds as Immanent Problem of Neoliberal Regime : PseudoScience : Who Rules America : Neoliberalism  : The Iron Law of Oligarchy : Libertarian Philosophy

Quotes

War and Peace : Skeptical Finance : John Kenneth Galbraith :Talleyrand : Oscar Wilde : Otto Von Bismarck : Keynes : George Carlin : Skeptics : Propaganda  : SE quotes : Language Design and Programming Quotes : Random IT-related quotesSomerset Maugham : Marcus Aurelius : Kurt Vonnegut : Eric Hoffer : Winston Churchill : Napoleon Bonaparte : Ambrose BierceBernard Shaw : Mark Twain Quotes

Bulletin:

Vol 25, No.12 (December, 2013) Rational Fools vs. Efficient Crooks The efficient markets hypothesis : Political Skeptic Bulletin, 2013 : Unemployment Bulletin, 2010 :  Vol 23, No.10 (October, 2011) An observation about corporate security departments : Slightly Skeptical Euromaydan Chronicles, June 2014 : Greenspan legacy bulletin, 2008 : Vol 25, No.10 (October, 2013) Cryptolocker Trojan (Win32/Crilock.A) : Vol 25, No.08 (August, 2013) Cloud providers as intelligence collection hubs : Financial Humor Bulletin, 2010 : Inequality Bulletin, 2009 : Financial Humor Bulletin, 2008 : Copyleft Problems Bulletin, 2004 : Financial Humor Bulletin, 2011 : Energy Bulletin, 2010 : Malware Protection Bulletin, 2010 : Vol 26, No.1 (January, 2013) Object-Oriented Cult : Political Skeptic Bulletin, 2011 : Vol 23, No.11 (November, 2011) Softpanorama classification of sysadmin horror stories : Vol 25, No.05 (May, 2013) Corporate bullshit as a communication method  : Vol 25, No.06 (June, 2013) A Note on the Relationship of Brooks Law and Conway Law

History:

Fifty glorious years (1950-2000): the triumph of the US computer engineering : Donald Knuth : TAoCP and its Influence of Computer Science : Richard Stallman : Linus Torvalds  : Larry Wall  : John K. Ousterhout : CTSS : Multix OS Unix History : Unix shell history : VI editor : History of pipes concept : Solaris : MS DOSProgramming Languages History : PL/1 : Simula 67 : C : History of GCC developmentScripting Languages : Perl history   : OS History : Mail : DNS : SSH : CPU Instruction Sets : SPARC systems 1987-2006 : Norton Commander : Norton Utilities : Norton Ghost : Frontpage history : Malware Defense History : GNU Screen : OSS early history

Classic books:

The Peter Principle : Parkinson Law : 1984 : The Mythical Man-MonthHow to Solve It by George Polya : The Art of Computer Programming : The Elements of Programming Style : The Unix Hater’s Handbook : The Jargon file : The True Believer : Programming Pearls : The Good Soldier Svejk : The Power Elite

Most popular humor pages:

Manifest of the Softpanorama IT Slacker Society : Ten Commandments of the IT Slackers Society : Computer Humor Collection : BSD Logo Story : The Cuckoo's Egg : IT Slang : C++ Humor : ARE YOU A BBS ADDICT? : The Perl Purity Test : Object oriented programmers of all nations : Financial Humor : Financial Humor Bulletin, 2008 : Financial Humor Bulletin, 2010 : The Most Comprehensive Collection of Editor-related Humor : Programming Language Humor : Goldman Sachs related humor : Greenspan humor : C Humor : Scripting Humor : Real Programmers Humor : Web Humor : GPL-related Humor : OFM Humor : Politically Incorrect Humor : IDS Humor : "Linux Sucks" Humor : Russian Musical Humor : Best Russian Programmer Humor : Microsoft plans to buy Catholic Church : Richard Stallman Related Humor : Admin Humor : Perl-related Humor : Linus Torvalds Related humor : PseudoScience Related Humor : Networking Humor : Shell Humor : Financial Humor Bulletin, 2011 : Financial Humor Bulletin, 2012 : Financial Humor Bulletin, 2013 : Java Humor : Software Engineering Humor : Sun Solaris Related Humor : Education Humor : IBM Humor : Assembler-related Humor : VIM Humor : Computer Viruses Humor : Bright tomorrow is rescheduled to a day after tomorrow : Classic Computer Humor

The Last but not Least Technology is dominated by two types of people: those who understand what they do not manage and those who manage what they do not understand ~Archibald Putt. Ph.D


Copyright © 1996-2021 by Softpanorama Society. www.softpanorama.org was initially created as a service to the (now defunct) UN Sustainable Development Networking Programme (SDNP) without any remuneration. This document is an industrial compilation designed and created exclusively for educational use and is distributed under the Softpanorama Content License. Original materials copyright belong to respective owners. Quotes are made for educational purposes only in compliance with the fair use doctrine.

FAIR USE NOTICE This site contains copyrighted material the use of which has not always been specifically authorized by the copyright owner. We are making such material available to advance understanding of computer science, IT technology, economic, scientific, and social issues. We believe this constitutes a 'fair use' of any such copyrighted material as provided by section 107 of the US Copyright Law according to which such material can be distributed without profit exclusively for research and educational purposes.

This is a Spartan WHYFF (We Help You For Free) site written by people for whom English is not a native language. Grammar and spelling errors should be expected. The site contain some broken links as it develops like a living tree...

You can use PayPal to to buy a cup of coffee for authors of this site

Disclaimer:

The statements, views and opinions presented on this web page are those of the author (or referenced source) and are not endorsed by, nor do they necessarily reflect, the opinions of the Softpanorama society. We do not warrant the correctness of the information provided or its fitness for any purpose. The site uses AdSense so you need to be aware of Google privacy policy. You you do not want to be tracked by Google please disable Javascript for this site. This site is perfectly usable without Javascript.

Last modified: September 30, 2020