Getopt::Long, Optional Arguments, and Greediness

89 views Asked by At

I'm trying to write a program that has an option that takes an optional argument, in such a way that it accepts options in the same fashion with the exact same behavior as perl -i or git --color. Unfortunately Getopt::Long doesn't exactly work that way and I can't get it to. Allow me to explain.

Example #1: If you run perl -i .bak, it does not use .bak as a backup file extension for editing in-place, as perl -i.bak would.

Example #2: If you run git diff --color always, it does not specify always for the --color option; for that you must use --color=always.

I call that "non-greedy".

I cannot figure out how to configure Getopt::Long to handle this scenario similarly. Programs using it behave "greedy" in this regard.

Consider the following program:

#!/usr/bin/env perl
use warnings;
use strict;
use Getopt::Long;

our $color;
our $backup;
Getopt::Long::Configure(qw(no_bundling));
Getopt::Long::GetOptions(
    "color:s" => \$color,
    "i:s" => \$backup,
) or die(":-(\n");

printf("\$color = '%s'\n", $color // '(undef)');
printf("\$backup = '%s'\n", $backup // '(undef)');
printf("\@ARGV = '%s'\n", join(', ', @ARGV));

and the following invocation, for example:

./foo.pl -i .bak --color always meow moo

Desired behavior:

$ ./foo.pl -i .bak --color always meow moo
$color = ''
$backup = ''
@ARGV = .bak, always, meow, moo

Actual behavior:

$ ./foo.pl -i .bak --color always meow moo
$color = 'always'
$backup = '.bak'
@ARGV = meow, moo
2

There are 2 answers

0
ikegami On

As a workaround, you can use the option terminator --.

./foo.pl -i .bak --color -- always meow moo
0
Håkon Hægland On

I cannot figure out how to configure Getopt::Long to handle this scenario similarly.

It seems it is not possible as noted by @Casper in the comments. But you can of course parse the command line manually. For example:

use v5.38;

our $color;
our $backup;

my @new_argv;
while (defined(my $arg = shift @ARGV)) {
    if ($arg eq '--color' && @ARGV && $ARGV[0] !~ /^-/) {
        # --color followed by a non-option argument
        $color = '';
        push @new_argv, shift @ARGV;
    } elsif ($arg =~ /^--color=(.*)/) {
        # --color=blue
        $color = $1;
    } elsif ($arg =~ /^--color$/) {
        # Just --color
        $color = '';
    } elsif ($arg eq '-i' && @ARGV && $ARGV[0] !~ /^-/) {
        # -i followed by a non-option argument
        $backup = '';
        push @new_argv, shift @ARGV;
    } elsif ($arg =~ /^-i=(.*)/) {
        # -i=bak
        $backup = $1;
    } elsif ($arg =~ /^-i$/) {
        # Just -i
        $backup = '';
    } else {
        push @new_argv, $arg;
    }
}

# @new_argv now contains the arguments that were not processed
printf("\$color = '%s'\n", $color // '(undef)');
printf("\$backup = '%s'\n", $backup // '(undef)');
printf("\@ARGV = '%s'\n", join(', ', @new_argv));