nowox
nowox

Reputation: 29096

Getting -h with Pod::Usage and GetOpt::Long violates of the DRY principle

I would like to give a better documentation to my Perl Programs. For this I don't have many solutions. Actuatlly I found only one: the POD.

For the NAME section we usually have:

=head1 NAME

program_name - Short description

=cut

It takes 5 lines where the only relevant information is the short description. The program_name should be automatically filled with basename($0).

Then it comes the options. From the GetOptions arguments, I can automatically extract:

We can easily add options such as category, mandatory, description and in_conflict_with. So why should I repeat myself in the POD?

To illustrate my example I wrote this:

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use Getopt::Long;
use Pod::Usage 'pod2usage';
use File::Basename;

=head1 AUTHOR

John Doe <[email protected]>

=head1 DESCRIPTION

B<This program> does nothing important. But at least it is properly documented.

=head1 COPYRIGHT

    Copyright(c) Nobody

=head1 VERSION

    v0.1

=cut


my %cfg;
CliInfo (
    args => [
    {arg => "l|length=i", dest => \$cfg{length}, desc => "Length of the string", type => 'mandatory'},
    {arg => "f|file=s"  , dest => \$cfg{data},   desc => "Input filename", type => 'mandatory'},
    {arg => "v"         , dest => sub {\$cfg{verbose}++}, desc => "Verbose"},
    ]
);

sub CliInfo { 
    my %info = @_;
    my $programName = basename $0;
    GetOptions(map( {$_->{arg} => $_->{dest}}  @{$info{args}}), 
        'h|help' => sub {
            say "Usage: $programName [options]...";
            say pod2scalar(-verbose => 99, -sections => "DESCRIPTION");
            say "\nOptions:";
            say sprintf('%-20s', (sub {
                my @opt = split /\|/, shift =~ s/=.*//r; 
                return "  -$opt[0], --$opt[1]" if @opt > 1;
                return "      --$opt[0]" if length $opt[0] > 1;
                return "  -$opt[0]";
            })->($_->{arg})), $_->{desc} for @{$info{args}};
            say "\n";
        }, 
        'version' => sub {
            my $ver = pod2scalar(-verbose => 99, -sections => "VERSION");
            say STDERR $programName, " - $ver";
            say pod2scalar(-verbose => 99, -sections => "COPYRIGHT");
            say "";
            exit;
        })
        or say "Invalid option, try --help" and exit 1; 

    sub pod2scalar {
        open my $fh, '>', \my $text;
        pod2usage(@_, -output => $fh, -exitval => 'NOEXIT');
        $text =~ s/^(?:.*\n)//;
        $text =~ s/(?:\s*\n)\z//r;
    }
}

__END__

Which gives this output (Also pretty compatible with the GNU standards):

$ ./help.pl  --help
Usage: help.pl [options]...
This program does nothing important. But at least it is properly documented.

Options:
  -l, --length      Length of the string
  -f, --file        Input filename
  -v                Verbose

So the question:

Is there any standard and similar solution to what I am showing here with respect of the DRY principle?

Upvotes: 0

Views: 78

Answers (1)

bessarabov
bessarabov

Reputation: 11871

Maybe Perl library Getopt::Long::Descriptive is what you need.

This is your script rewritten with Getopt::Long::Descriptive:

#!/usr/bin/env perl

use strict;
use warnings;

use Pod::Usage 'pod2usage';
use Capture::Tiny ':all';
use Getopt::Long::Descriptive;

=head1 AUTHOR

John Doe <[email protected]>

=head1 DESCRIPTION

B<This program> does nothing important. But at least it is properly documented.

=head1 COPYRIGHT

    Copyright(c) Nobody

=head1 VERSION

    v0.1

=cut

sub pod2scalar {
    my $stdout = capture_merged {
        pod2usage(-verbose => 99, -sections => "DESCRIPTION", -exitval => "noexit");
    };

    return $stdout;
}

my ($opt, $usage) = describe_options(
    pod2scalar() . "%c %o <some-arg>",
    [ 'l|length=i', 'Length of the string', { required => 1, default => 4  } ],
    [ 'f|file=s', 'Input filename', { required => 1  } ],
    [ 'v', 'Verbose' ],
    [ 'help', "print usage message and exit" ],
    {
        show_defaults => 1,
    },
);

if ($opt->help) {
    print($usage->text);
    exit;
}

This is the output of your original script:

$ ./before.pl --help
Usage: before.pl [options]...
    This program does nothing important. But at least it is properly
    documented.

Options:
  -l, --length      Length of the string
  -f, --file        Input filename
  -v                Verbose

And here is the output of the new script:

$ ./after.pl --help
Mandatory parameter 'f' missing in call to (eval)

Description:
 This program does nothing important. But at least it is properly
 documented.

after.pl [-flv] [long options...] <some-arg>
        --length INT -l INT   Length of the string
                              (default value: 4)
        --file STR -f STR     Input filename
        -v                    Verbose
        --help                print usage message and exit

Upvotes: 1

Related Questions