Even lighter Perl modulinos with Util::H2O::More

A few weeks ago, I wrote about how to use the mod­uli­no pat­tern in Perl to cre­ate unit-​testable com­mand line tools. Fellow Houston Perl Monger Brett Estrade point­ed me to a dif­fer­ent approach on the Perl Applications & Algorithms Discord. This approach trims boil­er­plate while keep­ing scripts testable.

Brett’s Utils::H2O::More mod­ule amends the light­weight class builder Utils::H2O. It adds many extra meth­ods, includ­ing com­mand line argu­ment pro­cess­ing via the Perl-​packaged Getopt::Long mod­ule. It also promis­es to build its acces­sors with less cer­e­mo­ny and code than Moo.

So let’s dive in!

A simple script

A script can use Util::H2O::More’s Getopt2h2o func­tion to process com­mand line options. It returns an object with acces­sors for each parameter.

Here’s a sim­ple exam­ple, mod­eled after my ear­li­er mod­uli­no exer­cise:

#!/usr/bin/env perl

use v5.38;
use Util::H2O::More v0.4.2 qw(Getopt2h2o);

# name is a string, water is an array of strings
my $o = Getopt2h2o \@ARGV, {}, qw(
    name=s
    water=s@
);
die "Missing --name\n" unless $o->name;

printf "Good %s, %s!\n", time_of_day(), $o->name;

if ( defined $o->water and $o->water->@* ) {
    say 'What kind of water would you like?';
    say "- $_" for $o->water->@*;
}

sub time_of_day {
    my %hours = (
         5 => 'morning',
        12 => 'afternoon',
        17 => 'evening',
        21 => 'night',
    );

    for ( sort { $b <=> $a } keys %hours ) {
        return $hours{$_} if (localtime)[2] >= $_;
    }
    return 'night';
}

This is short and read­able. I like how Getopt::Long’s quirky para­me­ter pars­ing syn­tax is repur­posed to cre­ate acces­sors, even for multi-​valued options.

You can call this script like so:

./h2options.pl --name Aquarius --water hot --water cold

We had to check that --name was set, as Util::H2O::More does­n’t sup­port using an = mod­i­fi­er to spec­i­fy required options. This is some­thing Getopt::Long allows.

And as Util::H2O’s doc­u­men­ta­tion sug­gests: ​“You should prob­a­bly switch to some­thing like Moo instead [for advanced features].”

But enough about limitations–what if you want­ed to use this as a Perl mod­ule for testing?

Testing the waters

One of the strengths of a mod­uli­no is the abil­i­ty to unit test its log­ic with­out invok­ing it from the shell. A typ­i­cal test script looks like this:

#!/usr/bin/env perl

use v5.38;
use Test2::V0;
use modulinh2o;

plan(6);

can_ok(
    'modulinh2o',
    [ 'time_of_day', 'name', 'water' ],
    'class has methods',
);
my $water = modulinh2o->new( name => 'Aquarius' );
isa_ok( $water, ['modulinh2o'],
        'object is expected class' );
can_ok(
    $water,
    [ 'time_of_day', 'name', 'water' ],
    'object has methods',
);

is( $water->name,        'Aquarius', 'name set' );
is( $water->name('Bob'), 'Bob',      'name change' );
is( $water->time_of_day,
    in_set( qw(
        morning
        afternoon
        evening
        night
    ) ),
    'time of day function',
);

It isn’t dif­fi­cult to adapt a mod­uli­no from our ear­li­er sim­ple script:

#!/usr/bin/env perl

use v5.38;

package modulinh2o;

use Getopt::Long qw();
use Util::H2O::More v0.4.2 qw(h2o opt2h2o);

my @opt_spec = qw(
    name=s
    water=s@
);
my $o = h2o -classify => __PACKAGE__, {},
        opt2h2o(@opt_spec);

sub time_of_day {
    my %hours = (
         5 => 'morning',
        12 => 'afternoon',
        17 => 'evening',
        21 => 'night',
    );

    for ( sort { $b <=> $a } keys %hours ) {
        return $hours{$_} if (localtime)[2] >= $_;
    }
    return 'night';
}

# constructor that parses arguments w/ basic validation
sub new_with_options ($class) {
    Getopt::Long::GetOptionsFromArray(
      \@ARGV, $o, @opt_spec
    ) or die "bad options\n";
    die "Missing --name\n" unless $o->name;
    return $o;
}

sub run ($self) {
    printf "Good %s, %s!\n",
           time_of_day(), $self->name;

    if ( defined $self->water and $self->water->@* ) {
        say 'What kind of water would you like?';
        say "- $_" for $self->water->@*;
    }
    return;
}

package main;

main() unless caller;

sub main { modulinh2o->new_with_options->run() }

And run:

./modulinh2o.pm --name Aquarius \
  --water sparkling --water still

This is much short­er than the Moo-​based mod­uli­no from three weeks ago, but it also does­n’t do as much. There’s no sup­port for using com­ma sep­a­ra­tors to pass mul­ti­ple val­ues to a sin­gle argu­ment. Worse, there’s no auto­mat­ic help text if one pass­es the wrong options.

Both are fix­able, as we’ll see in a moment. Still, you end up hav­ing to write the POD your­self, print­ed out with var­i­ous invo­ca­tions of Pod::Usage​’s pod2usage() function.

An ounce of script is worth a gallon of documentation

Here’s a full exam­ple that adds both --help and --man com­mand line options, as typ­i­cal­ly pro­vid­ed by tra­di­tion­al Getopt::Long-based scripts:

#!/usr/bin/env perl

use v5.38;

package modulinh2o2;

use Getopt::Long qw();
use Util::H2O::More v0.4.2 qw(h2o opt2h2o);
use Pod::Usage;

my @opt_spec = qw(
    name=s
    water=s@

    help
    man
);
my $o = h2o -classify => __PACKAGE__, {},
        opt2h2o(@opt_spec);

sub time_of_day {
    my %hours = (
         5 => 'morning',
        12 => 'afternoon',
        17 => 'evening',
        21 => 'night',
    );

    for ( sort { $b <=> $a } keys %hours ) {
        return $hours{$_} if (localtime)[2] >= $_;
    }
    return 'night';
}

# different parameter mixes for pod2usage()
my %pod2usage_opt = (
    cmdline => {
        -exitval  => 2,
        -verbose  => 99,
        -sections => 'USAGE/Command line',
        -message  => 'Use --help to list options',
    },
    opts => {
        -exitval  => 0,
        -verbose  => 99,
        -sections => ['USAGE/Command line', 'OPTIONS'],
    },
    man => {
        -exitval => 0,
        -verbose => 2,
    },
);

sub new_with_options ($class) {
    Getopt::Long::GetOptionsFromArray(
      \@ARGV, $o, @opt_spec
    ) or pod2usage( %pod2usage_opt{cmdline} );

    pod2usage( $pod2usage_opt{opts} ) if $o->help;
    pod2usage( $pod2usage_opt{man} )  if $o->man;
    pod2usage( %pod2usage_opt{cmdline},
      -message => 'Missing --name',
    ) unless $o->name;

    # default values for the water parameter
    $o->water(
          ( defined $o->water and $o->water->@* )
        ? [ split /,/, join q{,}, $o->water->@* ]
        : [ qw(
            still
            sparkling
            tap
        ) ] );

    return $o;
}

sub run ($self) {
    printf "Good %s, %s!\n",
           time_of_day(), $self->name;

    say 'What kind of water would you like?';
    say "- $_" for $self->water->@*;

    return;
}

package main;

main() unless caller;

sub main { modulinh2o2->new_with_options->run() }

# the rest below is documentation

__END__

=head1 NAME

modulinh2o2 - demo of a modulino using Util::H2O::More

=head1 USAGE

=head2 Command line

    modulinh2o2.pm [options]

=head2 Perl

    use modulinh2o2;
    my $water = modulinh2o2->new(
        name  => 'Aquarius',
        water => [ qw(
            sparkling
            still
            tap
        ) ],
    );
    $water->run;

=head1 OPTIONS

=over

=item B<--name>

Your name here! (required)

=item B<--water>

Type of water to serve. May be specified multiple times, either by repeating the option or separated by commas.

Takes an arrayref when used as a method or construction parameter.

Default values:

=over

=item still

=item sparkling

=item tap

=back

=item B<--help>

Displays a brief help message.

=item B<--man>

Display full documentation as a manual page.

=back

=head1 DESCRIPTION

A sample L<Util::H2O::More> modulino that prints your name, what part of the day it is, and a menu of water choices.

=head1 METHODS

=head2 new

Constructor that takes the above L</OPTIONS> but without the preceding C<-->.

=head2 new_with_options

Alternate constructor that receives its parameters from C<@ARGV>.

=head2 run

Prints out a greeting along with a selection of refreshing drinks.

=head2 ACCESSOR OPTIONS

All of the L</OPTIONS> above are also available as method accessors but without the preceding C<-->.

=head1 FUNCTIONS

=head2 time_of_day

Returns the period of the day based on local time. Possible values are:

=over

=item morning

=item afternoon

=item evening

=item night

=back

=head1 AUTHOR

Mark Gardner <[email protected]>

=head1 LICENSE AND COPYRIGHT

This software is copyright (c) 2025 by Mark Gardner.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.

Now we can run either:

./modulinh2o2.pm --help

to get a quick help mes­sage, or:

./modulinh2o2.pm --man

to show the full man­u­al page.

Yes, over half of the line count is doc­u­men­ta­tion. Even grant­i­ng this the code amount is now com­pa­ra­ble to the Moo-​based ver­sion. And you don’t get the advan­tage of MooX::Options’ declar­a­tive syntax.

To sum­ma­rize, here’s a table chart­ing the evo­lu­tion of our modulinh2o:

StageProsConsComplexity
Simple script:
Getopt2h2o only
* Minimal code foot­print
* Instant acces­sors from CLI argu­ments
* Multi-​valued options via @ syntax
* No required-​argument enforce­ment
* No auto-​help
* Manual validation
Low:
about 20 lines, one file
Basic mod­uli­no:
h2o + opt2h2o
* Testable as a mod­ule
* Reusable new_with_options con­struc­tor
* Keeps brevi­ty vs. Moo
* Still no comma-​separated multi-​values
* No auto-​help on bad arguments
Medium:
adds pack­age struc­ture, test harness
Full mod­uli­no w/​help & man:
Pod::Usage inte­grat­ed
* Support for --help and --man
* Comma-​separated multi-​values han­dled
* Defaults for miss­ing --water
* Clearer on-​boarding for new users
* More boil­er­plate
* Over half the file is POD
* Still less declar­a­tive than MooX::Options
High:
more mov­ing parts, but user-friendly
Util::H2O::More mod­uli­no evo­lu­tion and fea­ture trade-offs

Seen togeth­er, these stages trace a course from a bare-​bones script to a well-​provisioned mod­uli­no. Each step adds fea­tures at the cost of a lit­tle more complexity.

In the end, Util::H2O::More deliv­ers a lean, testable mod­uli­no with far less boil­er­plate than Moo–but also few­er built‑in niceties. If you val­ue speed from idea to work­ing script and can live with­out declar­a­tive option han­dling, it’s a com­pelling choice. For more struc­tured needs, MooX::Options still has the edge. Either way, the path from script to production-​ready mod­uli­no is short­er than you think.

The best tool is the one that flows from idea to ​“it works” in a sin­gle pour.


Discover more from The Phoenix Trap

Subscribe to get the latest posts sent to your email.

Mark Gardner Avatar

Hi, I’m Mark.

Hi, I’m Mark Gard­ner, and this is my personal blog. I show software developers how to level up by building production-ready things that work. Clear code, real projects, lessons learned.

Your post must include a link to this post.

How does this work?
Write a response on your own website and include a link to this post. Then submit your response’s URL here. Your response may appear here after moderation. To update or remove it, edit or delete your original post and submit the same URL again. Learn more about Webmentions.