Getopt modules 20: Smart::Options

About this mini-article series. Each day for 24 days, I will be reviewing a module that parses command-line options (such module is usually under the Getopt::* namespace). First article is here.

In the next few days, I'll be reviewing Perl ports of some popular option parsing modules from other languages. Today: Smart::Options.

Summary

Smart::Options is written by KAN Fushihara (MIKIHOSHI) and is a Perl port*) of node's optimist package, which in turns uses minimist as the option parsing engine and adds some stuffs, mainly the ability to generate usage/help message. Ironically, optimist is now deprecated in favor of yargs which is roughly the same as optimist but does its own parsing and adds features like bash completion.

So minimist is roughly the equivalent of Getopt::Long, optimist is the equivalent of Getopt::Long::Descriptive, yargs is roughly the equivalent of Getopt::Long::Descriptive + tab completion (like Getopt::Long::Complete or Getopt::Long::More).

You can get an idea of the sheer number of packages in npm, the CPAN equivalent in the node ecosystem, by looking at these numbers: compared to Getopt::Long's 1127 dependents, minimist has 5768 dependents. And it isn't even the most popular option parsing package. The most popular one on npm is currently commander (from the legendary TJ Holowaychuk) which has 12252 dependents! yargs has 4073, optimist has 3546 (remember that optimist has been declared as deprecated), and nomnom (another deprecated option parsing package) still has 510.

Currently there is no CPAN distribution depending on Smart::Options.

commander itself resembles Getopt::Long::Descriptive a bit more in its interface. I didn't find any Perl port of commander on CPAN though.

But I digress. Let's go back Smart::Options and optimist. As I said earlier, optimist is roughly equivalent to Getopt::Long::Descriptive. Except for one main difference: you are not required to specify any specification. Without any specification, the library will simply accept any option and put it in a hash. But remember that without specification, you cannot check for an unknown option or get auto-abbreviation.

Bundling of short one-letter options is supported, but if you don't provide specification the library cannot differentiate which short options require value and which ones don't: the library will simply assume that all short options are just flags which don't take value.

Another difference is the usage of OO and method chaining.

Usage

Here's how one would use Smart::Options in the simplest way (without any specification):

use 5.010;
use Smart::Options;
my $opts = argv(); # you can also say: $opts = Smart::Options->new->parse
say "foo = ", $opts->{foo};
say "b = ", $opts->{b};
say "args = [", join(", ", @{ $opts->{_} }), "]";
say "ARGV = [", join(", ", @ARGV), "]";

Let's try to run it:

% ./script.pl –foo 10 -b — a b c
foo = 10
b = 1
args = [a, b, c]
ARGV = [–foo, 10, -b, –, a, b, c]

As you can see, the command-line arguments will be put in the _ key. And unlike Getopt::Long, it does not modify @ARGV.

One nitpick: the argv() or the parse() function (or method) can accept a list to parse options from array other than @ARGV, but since it accepts a list instead of arrayref, when you pass a zero-length array it will assume that you don't pass any array and so still defaults to @ARGV. This can be remedied, e.g., by accepting an arrayref instead.

Without options specification, it's not possible to declare an option to be required, repeatable, or as a flag. So let's add some specification:

use 5.010;
use Smart::Options;
my $opts = Smart::Options->new
    ->demand('foo')                     ->describe(foo => 'The foo option')
    ->default(bar => 3)->alias(b => bar)->describe(bar => 'The bar option')
    ->default(baz => 5)                 ->describe(baz => "The baz option")
    ->parse;
say "foo = ", $opts->{foo};
say "b = ", $opts->{b};
say "args = [", join(", ", @{ $opts->{_} }), "]";

After this, you can generate help message:

$ ./script.pl –help
Usage: ./script.pl

Options:
-b, –bar The bar option [default: 3]
–baz The baz option [default: 5]
–foo The foo option [required]
-h, –help Show help

Missing required arguments: foo

BTW, some option parsing modules, including Smart::Options, still complain about missing –foo when we instruct it to show help message (–help), like shown above. I think this behavior is a bug and should be fixed.

Other features

*) I said earlier that Smart::Options is a port of optimist. It is actually more accurately a blend between optimist and Kan's older module opts. So beyond optimist, Smart::Options adds some more (quite substantial) features, which do not exist even in yargs or commander.

Validation. Like in Getopt::Long, you can add some validation. You can declare an option to accept Bool, Int, Num, Str, ArrayRef (this is similar to Getopt::Long's @ destination type to make option repeatable), HashRef (if say foo is declared as a hashref, you can specify –foo.key1 or –foo.key2 in the command-line and so on), or Config.

Configuration file. The last type, Config, is actually supposed to let you specify a filename to make the module reads an INI-like configuration file. But perhaps this configuration is misplaced and conflated, as this is not a type/validation configuration, and it is not per-option but global.

Coercion. This can be used to convert an option value which is scalar/string to, say, Path::Tiny instance.

Subcomands. This lets you support (nested) subcommands by adding a nested Smart::Options object inside another, like in Getopt::Long::Subcommand. For example:

my $opts = Smart::Options->new
    ->subcmd(subcmd1 => Smart::Options->new->...)
    ->subcmd(subcmd1 => Smart::Options->new->...)
    ->parse;

DSL. If you don't like the chained methods syntax, there's Smart::Options::Declare which offers an alternative interface to declare an option one by one much like Moose's has. Although it doesn't seem to support declaring subcommands yet.

Performance

The startup overhead of Smart::Options is roughly the same as Getopt::Long::Descriptive, while the memory usage is higher.

% bencher-module-startup-overhead Smart::Options Getopt::Long::Descriptive
+—————————+——————————+——————–+—————-+———–+————————+————+———–+———+
| participant | proc_private_dirty_size (MB) | proc_rss_size (MB) | proc_size (MB) | time (ms) | mod_overhead_time (ms) | vs_slowest | errors | samples |
+—————————+——————————+——————–+—————-+———–+————————+————+———–+———+
| Smart::Options | 4.2 | 8 | 33 | 36 | 33.9 | 1 | 0.00018 | 20 |
| Getopt::Long::Descriptive | 0.82 | 4.5 | 23 | 35 | 32.9 | 1 | 9.9e-05 | 20 |
| perl -e1 (baseline) | 4.9 | 9 | 38 | 2.1 | 0 | 17 | 1.5e-05 | 20 |
+—————————+——————————+——————–+—————-+———–+————————+————+———–+———+

Also to be noted is that Smart::Options does not use Getopt::Long but does its own parsing.

Verdict

I find optimist and yargs themselves don't offer any new feature not already existing in Getopt::Long or Getopt::Long::Descriptive (the completion feature can be done with shcompgen). But Smart::Options does offer some extra features like subcommand support and reading of configuration file. On the other hand, you lose some of Getopt::Long's features like: auto-abbreviation and custom handler (in Getopt::Long, you can assign a coderef to an option which can do anything, like printing a message early and exiting, or setting other variable or multiple variables, or whatever).

My problem with this module is the interface: method chaining has its uses (for example I find it convenient in some JSON module or in jQuery) but here it just distracts and make options specification visually convoluted. On the other hand, the DSL alternative interface is not complete (yet).

I personally would still reach for my Perinci::CmdLine most of the time. But I will prefer Smart::Options over App::Options (which is also covered in this mini-article series).

Advertisement

One thought on “Getopt modules 20: Smart::Options

  1. Pingback: Getopt modules 22: Getopt::Kingpin | perlancar's blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s