MooX::Commander

How to build command line apps with sub commands and option parsing

Eric Johnson / @kablamo_

What we want to build


$ bin/pie-factory --help
$ bin/pie-factory -h
$ bin/pie-factory help
          

usage: pie-factory [options] [commands]

You have inherited a pie factory.  Use your powers wisely.

COMMANDS
pie-factory bake <pie>              Bake a pie
pie-factory eat  <pie>              Eat a pie
pie-factory recipe list             List pie recipes
pie-factory recipe show   <recipe>  Display a recipe
pie-factory recipe add    <recipe>  Add a recipe
pie-factory recipe delete <recipe>  Delete a recipe
pie-factory throw <pie> <target>    Throw a pie at something

pie-factory help <cmd>              Get help with a command

OPTIONS
  -v, --version  Show version
  -h, --help     Show this message
          

Help for commands


$ bin/pie-factory throw --help
$ bin/pie-factory throw -h
$ bin/pie-factory help throw
          

usage: pie-factory throw <pie> <target> [options]

Throw <pie> at <target>.  Valid values for <pie> are apple, 
rhubarb, or mud.

OPTIONS
    -a, --angrily  Curse the target after throwing the pie
    -s, --speed    Throw the pie this many mph
    -h, --help     Show this message
          

List subcommands


$ bin/pie-factory recipe --help
$ bin/pie-factory recipe -h
$ bin/pie-factory recipe
$ bin/pie-factory help recipe
          

Subcommands for: piefactory recipe

pie-factory recipe list             List pie recipes
pie-factory recipe add    <recipe>  Display a recipe
pie-factory recipe delete <recipe>  Add a recipe
pie-factory recipe show   <recipe>  Delete a recipe

          

A note on usage or help

  • Everyone has their own opinion on what it should look like
  • Not every cmd line app is the same
  • There isn't a right or wrong way to do it
  • Its a hard problem
  • Its not that hard to maintain usage by hand
  • So I didn't even try to solve it
  • MooX::Commander doesn't generate help or usage
  • Advantage of this is simplicity and flexibility

Project layout


~/code/pie-factory $ tree
.
├── bin
│   └── pie-factory
└── lib
    ├── PieFactory
    │   └── Cmd
    │       ├── Bake.pm
    │       ├── Eat.pm
    │       ├── Help.pm
    │       ├── Recipe
    │       │   ├── Add.pm
    │       │   ├── Delete.pm
    │       │   ├── List.pm
    │       │   └── Show.pm
    │       └── Throw.pm
    └── PieFactory.pm

5 directories, 10 files
          

Dispatch to command classes


$ pie-factory throw rhubarb-pie Batman
          

# inside bin/pie-factory:
use MooX::Commander;

my $commander = MooX::Commander->new(
    base_class   => 'PieFactory',
    class_prefix => 'Cmd',  # optional, default value is 'Cmd'
);

$commander->dispatch(argv => \@ARGV);
          

Command classes


package PieFactory::Cmd::Throw;
use Moo;


sub go {
    my ($self, $pie, $target) = @_;

    # print usage and then exit unsuccessfully
    $self->usage() unless $pie && $target;

    # print "Not a valid value", 
    # print usage, and exit unsuccessfully
    $self->usage("Not a valid value for <pie>") 
        unless $pie eq 'rhubarb-pie';


    $self->throw($pie => $target);
}

# Print usage and exit unsuccessfully.
sub usage { 
    my ($self, $msg) = @_;
    print $shift, "\n" if $msg;
    print "usage: pie-factory throw <pie> <target> [options]...";
    exit 1; 
}



          

Command classes


package PieFactory::Cmd::Throw;
use Moo;
with 'MooX::Commander::HasOptions';

sub go {
    my ($self, $pie, $target) = @_;

    # print usage and then exit unsuccessfully
    $self->usage() unless $pie && $target;

    # print "Not a valid value", 
    # print usage, and exit unsuccessfully
    $self->usage("Not a valid value for <pie>") 
        unless $pie eq 'rhubarb-pie';


    $self->throw($pie => $target);
}

# Print usage and exit unsuccessfully.
sub usage { "usage: pie-factory throw <pie> <target> [options]..."; }








          

Command classes


package PieFactory::Cmd::Throw;
use Moo;
with 'MooX::Commander::HasOptions';

sub go {
    my ($self, $pie, $target) = @_;

    # print usage and then exit unsuccessfully
    $self->usage() unless $pie && $target;

    # print "Not a valid value", 
    # print usage, and exit unsuccessfully
    $self->usage("Not a valid value for <pie>")
        unless $pie eq 'rhubarb-pie';

    $self->curse_loudly() if $self->options->{angrily};
    $self->throw($pie => $target, $self->options->{speed});
}

# Print usage and exit unsuccessfully.
sub usage { "usage: pie-factory throw <pie> <target> [options]..." }

# This array is used to configure Getopt::Long
sub _build_options {(
    'angrily|a',
    'speed|s=i',
)}
          

Dispatch to subcommands


package PieFactory::Cmd::Recipes;
use Moo;
with 'MooX::Commander::HasSubcommands';


          

Dispatch to subcommands


package PieFactory::Cmd::Recipes;
use Moo;
with 'MooX::Commander::HasSubcommands';

usage { "Subcommands for: piefactory recipes..." }
          

Subcommand classes

Create subcommand classes the same way you would build any command class.

Help for commands and subcommands


package PieFactory::Cmd::Help;
use Moo;
with 'MooX::Commander::IsaHelpCommand';


          

Help for commands and subcommands


package PieFactory::Cmd::Help;
use Moo;
with 'MooX::Commander::IsaHelpCommand';

sub usage { "usage: pie-factory [options] [commands]..." }
          

Recap

  1. Setup dispatch to cmd classes
    MooX::Commander->new(...)->dispatch(...)
     
  2. Parse options in cmd classes
    with 'MooX::Commander::HasOptions'
     
  3. Setup dispatch for subcommands
    with 'MooX::Commander::HasSubcommands';
     
  4. Create a help command
    with 'MooX::Commander::IsaHelpCommand';
     

THE END

Slides are online at

github.com/kablamo/slides-moox-commander