Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl: the Markov chain saw
 
PerlMonks  

Getopt - Validate arguments before processing

by g_speran (Scribe)
on Jan 30, 2022 at 11:06 UTC ( [id://11140961]=perlquestion: print w/replies, xml ) Need Help??

g_speran has asked for the wisdom of the Perl Monks concerning the following question:

Hello Perlmonks, I have the following code and I am wondering if there is a way to actually validate the options passed BEFORE processing? As you can see based upon the execution of the code, Getopt is processing the arguments as they are received. The value of --year was passed which is not a valid argument, but then it processed --first successfully. What I would like to accomplish is if any invalid argument is passed, immediatly display the help and exit the script without processing any arguments

#!/usr/bin/env perl use warnings; use strict; use Getopt::Long; GetOptions( 'first' => \&first, 'second' => \&second, 'help' => \&help, ) or die "Invalid options passed to $0\n"; sub first () { print "Processing sub first\n"; } sub second () { print "Processing sub second\n"; } sub help () { print "Available options are: \n"; print "\t--first\n"; print "\t--second\n"; print "\t--third\n"; print "\t--fourth\n"; print "\t--help\n"; }
***** Output ******* C:\temp>getopts.pl --year --first Unknown option: year Processing sub first Invalid options passed to C:\temp\getopts.pl

Replies are listed 'Best First'.
Re: Getopt - Validate arguments before processing (updated)
by haukex (Archbishop) on Jan 30, 2022 at 11:23 UTC
    As you can see based upon the execution of the code, Getopt is processing the arguments as they are received. ... What I would like to accomplish is if any invalid argument is passed, immediatly display the help and exit the script without processing any arguments

    In this example, what are sub first and second actually doing? Because if they have any side effects other than simply saving the fact that the option value was passed to the program, then I think you're not using the module as intended.

    Update: Here's another variation, and my suspicion is that this may come closest to your original intent:

    use warnings; use strict; use Getopt::Long; my @actions; GetOptions( first => sub { push @actions, \&first }, second => sub { push @actions, \&second }, ) or die "Invalid options passed to $0\n"; sub first { print "Processing sub first\n"; } sub second { print "Processing sub second\n"; } $_->() for @actions; __END__ $ perl 11140961c.pl --first --second --first --second --second --first Processing sub first Processing sub second Processing sub first Processing sub second Processing sub second Processing sub first $ perl 11140961c.pl --first --second --year --first --second --second +--first Unknown option: year Invalid options passed to 11140961c.pl

    /Update

Re: Getopt - Validate arguments before processing
by davies (Prior) on Jan 30, 2022 at 16:10 UTC
Re: Getopt - Validate arguments before processing
by clueless newbie (Curate) on Jan 30, 2022 at 16:58 UTC
Re: Getopt - Validate arguments before processing
by bliako (Monsignor) on Feb 01, 2022 at 13:25 UTC

    I do not subscribe to your logic. It's best to first parse all arguments and have a flag that all checks have passed, and then act on each argument. Just like haukex in Re: Getopt - Validate arguments before processing (updated) suggests. It's cumbersome but from what I understand you are asking for the impossible: parse the arguments as they are entered by the user, do some destructive actions (e.g. --erase-all-files) in your first() et al. and then realise that the last argument does not validate and you need to restore original state. How can you rollback that? Only if you create the actions and you only commit them when all validates, eventually.

    Perhaps you can compromise with having Getopt::Long processing your options in a specific and strict order, independently on how the user types them on the command line. In this way, you can achieve having only one "destructive" option at the end. In my code, the getopt_in_order() will take an @ARGV, the Getopt spec and the order you want the options to be processed and return you back a new @ARGV which will be in that order. You then simply call Getopt::Long::GetOptionsFromArray(\@newARGV, %getopt_spec).

    I am sure this will be of little help to your problem but hey, I always wanted such feature from Getopt::Long.

    use Getopt::Long qw(GetOptionsFromArray); use Test::More; my %getopt_spec = ( 'first=s{2}' => 1, 'second=i{3}' => 2, 'third' => 3, ); my @testARGV = ( ['--second', '1', '2', '3', '--first', 'ahah', 'and spaces', '--th +ird'], ['--first', 'ahah', 'and spaces', '--second', '1', '2', '3'], ['--first', 'ahah', 'and spaces', '--third'], ['--third', '--first', 'ahah', 'and spaces', '--second', '1', '2', + '3'], ['--third'], ['--first', 'ahah', 'and spaces'], ['--second', '1', '2', '3'], [] ); my @expectedARGV = ( ['--first', 'ahah', 'and spaces', '--second', '1', '2', '3', '--th +ird'], ['--first', 'ahah', 'and spaces', '--second', '1', '2', '3'], ['--first', 'ahah', 'and spaces', '--third'], ['--first', 'ahah', 'and spaces', '--second', '1', '2', '3', '--th +ird'], ['--third'], ['--first', 'ahah', 'and spaces'], ['--second', '1', '2', '3'], [] ); for my $i (0..@testARGV-1){ my $testar = $testARGV[$i]; my $expear = $expectedARGV[$i]; # make a copy of it for printing diags because getopt_in_order() w +ill destroy it my $copy_testar = [ @$testar ]; my $newargv = getopt_in_order(\%getopt_spec, $testar); ok(defined $newargv, "getopt_in_order() called for '@$copy_testar' +."); is_deeply($newargv, $expear, "got (@$newargv) and expected (@$expe +ar) for '@$copy_testar'"); } done_testing; # returns the ordered @$an_argv as per the @$options_order # or undef on failure # WARNING: $an_argv will be destroyed on return sub getopt_in_order { # by bliako for https://perlmonks.org/?node_id=11140961 # 01/Feb/2022 my ( # a hashref keyed on getopt specs $getopt_spec, # a hash of option names with the index you want them processe +d, e.g. 'first' => 1 #$options_order, # arrayref to @ARGV or its copy, this will be destroyed by Get +opt $an_argv ) = @_; my %options_order = map { (split('=', $_))[0] => $getopt_spec->{$_ +}-1 } keys %$getopt_spec; my %getopt_spec; my @tmpARGV; for my $aspec (keys %$getopt_spec){ $getopt_spec{$aspec} = sub { my $k = shift; my $expects_args = @_ && ($aspec =~ /^.+?=.+?$/); my $idx = $options_order{$k}; if( exists($tmpARGV[$idx]) && defined($tmpARGV[$idx]) ){ push @{$tmpARGV[$idx]}, @_ if $expects_args } else { if( $expects_args ){ $tmpARGV[$idx] = [ '--'.$k, @_ ] +} else { $tmpARGV[$idx] = [ '--'.$k ] } } } } if( ! GetOptionsFromArray($an_argv, %getopt_spec) ){ print STDERR "getopt_in_order() : error parsing command line a +rguments.\n"; return undef } # remove undef entries e.g. because -second was not present # see https://stackoverflow.com/a/11123138 @tmpARGV = grep defined, @tmpARGV; my @newARGV; foreach my $opt (@tmpARGV){ # in correct order now and no holes push @newARGV, @$opt } return \@newARGV; }

    bw, bliako

Re: Getopt - Validate arguments before processing -- BEGIN
by Discipulus (Canon) on Jan 31, 2022 at 08:57 UTC
    Hello g_speran,

    interesting question: I was sure Getopt::Long was consuming the @ARGV array so you had the possibility to check if something unkonow is still there.. but is too late for this when it arrives the moment and "Processing sub first" is already fired.

    Then I read about die("!FINISH") special behaviour, but is not useful.. again it fires too late.. so you must move quick to intercept the unkonown option.

    A BEGIN block does the trick (but I moved the help sub on top, so it is parsed before the BEGIN block, so it can be called: you must move other subs to give them a chance to be called):

    use warnings; use strict; use Getopt::Long; sub help { print "Available options are: \n"; print "\t--first\n"; print "\t--help\n"; } BEGIN{ # no warnings; #strangely this is not able to suppress the warning + "Undefined subroutine &main::first called at.." GetOptions( 'first' => \&first, 'help' => \&help, ) or &help and die "Invalid options passed to $0\n" } sub first { print "Processing sub first\n"; } __END__ perl getoptcheck.pl --year --first Unknown option: year Undefined subroutine &main::first called at ..perl5.26.64bit/perl/lib/ +Getopt/Long.pm line 606. Available options are: --first --second --help Invalid options passed to getoptcheck.pl BEGIN failed--compilation aborted at getoptcheck.pl line 20.

    L*

    There are no rules, there are no thumbs..
    Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.

      The code in the BEGIN block runs before Perl has even seen your declaration of the subroutine sub first { .... To work around that, you need to move the BEGIN block below the subroutine declaration(s).

        yes thanks Corion, I know that.. but probably I have expressed it poorly :)

        > (but I moved the help sub on top, so it is parsed before the BEGIN block, so it can be called: you must move other subs to give them a chance to be called)

        I just wanted to suppress the warning but I was not able to find its category, and neither no warnings suppresses it

        L*

        There are no rules, there are no thumbs..
        Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
      as long as sub first is defined below the BEGIN block,
      even if the only argument is '--first' (i.e. no invalid options),
      you'll have
      Undefined subroutine &main::first called at .../Getopt/Long.pm line 60 +7. Available options are: --first --help Invalid options passed to ....pl BEGIN failed--compilation aborted at ....pl line 17.
      which means, as Corion already mentioned, you have to move the BEGIN block below the subroutines, which defeats the purpose.

      besides, from an esthetic viewpoint, it isn't nice, if a program reacts with a "compilation error" to a wrong invocation.

      I think I like haukex's variant "push @actions" best, and it would even allow for an optional --ignore-unknown-switches or --please-dont-die switch to suppress dying on invalid options ;-)

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11140961]
Approved by haukex
Front-paged by Corion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others having a coffee break in the Monastery: (3)
As of 2024-04-19 19:44 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found