6
\$\begingroup\$

I use this idiom to parse command line arguments in Perl:

my @args;
my $encode = 1;

while (@ARGV) {
    for (shift(@ARGV)) {
        ($_ eq '-h' || $_ eq '--help') && do { &usage(); };
        # ... more options here
        ($_ eq '--') && do { push(@args, @ARGV); undef @ARGV; last; };
        ($_ =~ m/^-.+/) && do { print "Unknown option: $_\n"; &usage(); };
        push(@args, $_);
    }
}

This let's me do things like:

  • Supports command line arguments in arbitrary order
  • Supports using -- to use any following arguments "as is", without parsing
  • Stop parsing and print usage message on -h, or --help
  • Raise error if a -something flag is detected that is not matched by any of the parsing rules

Here's a short example script using this idiom:

#!/usr/bin/env perl

use strict;
use warnings;

&usage() unless @ARGV;

my @args;
my $encode = 1;

while (@ARGV) {
    for (shift(@ARGV)) {
        ($_ eq '-h' || $_ eq '--help') && do { &usage(); };
        ($_ eq '-D' || $_ eq '--decode') && do { $encode = ! $encode; last; };
        ($_ eq '--') && do { push(@args, @ARGV); undef @ARGV; last; };
        ($_ =~ m/^-.+/) && do { print "Unknown option: $_\n"; &usage(); };
        push(@args, $_);
    }
}

sub usage {
    $0 =~ m|[^/]+$|;
    print "Usage: $& [-h|--help] [-D|--decode]\n";
    print "\n";
    print "Encode (= default) or decode Base64\n";
    exit 1;
}

use MIME::Base64;
if ($encode) {
    print map(&MIME::Base64::encode_base64($_), @args);
} else {
    print map(&MIME::Base64::decode_base64($_), @args);
}

Is this a good technique for parsing command line arguments in Perl? Is there a better way?

I'm also interested in improvement ideas about the short script itself too.

\$\endgroup\$
1

2 Answers 2

2
\$\begingroup\$

Using the & sigil to call a subroutine has gone out of style since Perl 5 was released in 1994.

Nesting a for-loop inside a while-loop is a weird way to express the flow of control, in my opinion. You want to iterate through @ARGV at most once, so it should look like a single loop. The for-loop isn't really a loop at all; it's just a way for last to goto the point after push(@args, $_). (The use of for would be acceptable in conjunction with the when keyword, as it would then be recognizable as a Perl switch statement.) I think that the following way would be a clearer way to write the loop:

while (@ARGV) {
    local $_ = shift @ARGV;
    ($_ eq '-h' || $_ eq '--help') && do { usage(); };
    ($_ eq '-D' || $_ eq '--decode') && do { $encode = ! $encode; next; };
    ($_ eq '--') && do { push(@args, @ARGV); undef @ARGV; next; };
    ($_ =~ /^-./) && do { print STDERR "Unknown option: $_\n"; usage(); };
    push(@args, $_);
}

If a solitary - is passed on the command line, it is treated as a valid argument to be appended literally to @args. I'm not sure if that is intentional.


Explicitly asking for usage help differs from displaying help in the case of a usage error. In the former case, the exit status should be 0, and the display should go to STDOUT. In the latter case, the exit status should be non-zero, and the display should go to STDERR. In addition, I don't recommend burying the exit() call inside usage(), because it makes your option-parsing loop harder to follow.

sub usage {
    my ($status) = @_;
    my $old_fh = select STDERR if $status;

    print …;

    select $old_fh if $old_fh;
    return $status;
}

exit usage(1) unless @ARGV;
while (@ARGV) {
    local $_ = shift @ARGV;
    ($_ eq '-h' || $_ eq '--help') && do { exit usage(0); };
    ($_ eq '-D' || $_ eq '--decode') && do { $encode = ! $encode; next; };
    ($_ eq '--') && do { push(@args, @ARGV); undef @ARGV; next; };
    ($_ =~ /^-./) && do { print STDERR "Unknown option: $_\n"; exit usage(1); };
    push(@args, $_);
}

The use of $encode is problematic in three ways. First, you should use variables that reflect the UI. Also, prefer to use variables that default to 0 or a false value. Finally, toggling $encode whenever a --decode flag is encountered is inappropriate, because it is surprising that --decode --decode actually causes it to encode instead of decode. (Imagine what would happen if one defined a shell alias rm as rm -i, such that running rm -i expands to rm -i -i, only to find that file deletion is no longer interactive!)


In my opinion, the code formatting hinders readability. Aligning the do blocks would help. However, you might as well be less stingy and place statements on separate lines.

my $decode;
while (@ARGV) {
    local $_ = shift @ARGV;
    if ($_ eq '-h' || $_ eq '--help') {
        exit usage(0);
    } elsif ($_ eq '-D' || $_ eq '--decode') {
        $decode = 1;
        next;
    } elsif ($_ eq '--') {
        push(@args, @ARGV);
        undef @ARGV;
        next;
    } elsif (/^-./) {
        print STDERR "Unknown option: $_\n";
        exit usage(1);
    } else {
        push(@args, $_);
    }
}

The usage of next and elsif are redundant. I think that the redundancy helps make the code easier to follow, but it's a matter of taste.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Thanks for the great review! I've been using this technique since 2005, too bad (as you say) it was already obsolete then :) \$\endgroup\$
    – janos
    Oct 5, 2014 at 7:07
  • 1
    \$\begingroup\$ Excellent review. However, it's perfectly idiomatic to use for not as a loop, but as a topicalizer (similar in function to Pascal's with statement). Doing while ($_ = shift @ARGV) { … } is not necessarily correct: It does not localize $_, and has problems with false values. Doing while (@ARGV) { my $flag = shift @ARGV; … } would be optimal. \$\endgroup\$
    – amon
    Oct 5, 2014 at 8:06
  • \$\begingroup\$ why are you still localizing $_? There is no benifit from it in that case. I would understand, if, at some point, many uses of a function that defaults to $_ is used. but that way it just obscures the function of the variable. $argument or for brevity's sake, $arg would help, in my opinion. \$\endgroup\$ Oct 5, 2014 at 16:50
9
\$\begingroup\$

There are several modules on CPAN that are made for this task. They all reside in the Getopt namespace. Getopt::Long even comes with the standard distribution. So coming to your question: Yes, there are better ways.

If you are interested in something more fancy, you could have a look at Getopt::Declare where you can write the usage page and fill your variables in the same place. It has a downside, though since it introduces another DSL.

What I definitely would recommend is that you take a look at <<HERE docs for printing longer texts. Some people dislike them,because of their strange syntactic properties, but they are the usual way to express multiline strings in perl.

\$\endgroup\$
3
  • \$\begingroup\$ For maximum portability, I prefer standard packages that I can safely assume to exist everywhere. I was not aware of Getopt::Long, it seems to be a standard package now, I will try to rewrite my script to use it, thanks! \$\endgroup\$
    – janos
    Oct 5, 2014 at 5:21
  • 1
    \$\begingroup\$ the build system for perl has greatly improved. and with metacpan's crosslinks to cpantesters you can check for so many system where some modules works. \$\endgroup\$ Oct 5, 2014 at 17:59
  • 3
    \$\begingroup\$ @janos According to Module::CoreList Getopt::Long has come with Perl since Perl 5, which was released in 1994. \$\endgroup\$ Jan 27, 2015 at 3:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.