Introducing App::Dispatcher

I have an issue somewhat related to Steven Haryanto's concern about bloated apps. I can live with a command taking a second longer to run when performing work, but I absolutely detest waiting that long for the usage message to be generated. I am constantly running commands without arguments (eg Git) to get an overview of sub-commands and/or their options. A slow Perl command line app (eg dzil) is a pain in comparison. It is sometimes quicker to load the manpage.

But it doesn't necessarily have to be that way. App::Dispatcher is a tool for constructing command line applications written in Perl. It is specifically designed to handle applications with multiple sub-commands and will generate code to display usage text, validate options and arguments, and dispatch commands. Its main strength is that the usage and validation does not load any command classes with their possibly heavy startup dependencies.

An application built with App::Dispatcher is composed of three parts:

  • Command Classes

    These classes are written by the application author to do the actual work. They have special methods which App::Dispatcher uses to build the Dispatcher class.

  • Dispatcher Class

    The Dispatcher class is generated when the application author runs the app-dispatcher(1) command. The dispatcher class is called by the Application Script at runtime. After performing option and argument processing the dispatcher class calls the appropriate Command Class.

    The only runtime dependency that the Dispatcher Class needs to run is Getopt::Long::Descriptive(3p), which the application author should add to their Makefile.PL and/or Build.PL scripts.

    Note that App::Dispatcher is a code generation tool. This means that option and argument processing will not change when command classes are modified, until app-dispatcher is re-run.

  • Application Script

    The application script is what the user runs, and does nothing more than call the Dispatcher Class:

    #!/usr/bin/perl
    use Your::Command::Dispatcher;
    Your::Command::Dispatcher->run;
    

    All the examples below assume the existence of this application script.

The rest of this tutorial is all about how to write Command Classes. Let's start with a simple command that has two options, one optional argument, and no sub-commands:

package Your::Command;

sub opt_spec {(
    [ "dry-run|n",     "print out SQL instead of running it" ],
    [ "drop-tables|D", "DROP TABLEs before deploying" ],
)};

sub arg_spec {(
    [ "database=s",   "which database to deploy to",
        { default => 'development' } 
    ],
)};

sub run {
    my ($class,$opt) = @_;

    if ( $opt->dry_run ) {
        print "Not ";
    }
    print "Deploying to ". $opt->database ."\n";
}

1;

With the above code in 'lib/Your/Command.pm' we can run app-dispatcher and then our script:

ex1$ app-dispatcher --add-help Your::Command
Writing lib/Your/Command/Dispatcher.pm

ex1$ ./ex --help
usage: ex [options] [<database>]
    --help             print usage message and exit
    -n --dry-run       print out SQL instead of running it
    -D --drop-tables   DROP TABLEs before deploying

ex1$ ./ex
Deploying to development

ex1$ ./ex -n production
Not Deploying to production

There are several things to note here. The first is the definition of the opt_spec() and arg_spec() methods. The values returned from these methods are passed more or less untouched to Getopt::Long::Descriptive's describe_options() routine.

Obvious should be the fact that the '-add-help' option to app-dispatcher has added a '--help' option to our command.

What is more interesting is that arguments specification has the same format as the options specification. An argument could actually be considered the same as an un-named option with a fixed position. So App::Dispatcher actually folds arguments into the option object passed to the command class. The added benefit is that the parameter validation of Getopt::Long is now also run against arguments.

Let's make the argument mandatory by adding a "required" key to the arg_spec definition:

{ default => 'development', required => 1 }

ex2$ ./ex
usage: ex [options] <database>
    --help             print usage message and exit
    -n --dry-run       print out SQL instead of running it
    -D --drop-tables   DROP TABLEs before deploying

You can see that the that the argument is now mandatory, and the usage message no longer has the '[]' brackets. If the automatically generated usage message is not to your liking, you can write a usage_desc() method to specify your own, but that is not really recommended as you'll have to keep it up to date manually when your code changes:

sub usage_desc {
    return '%c %o <DATABASE>'
}

Now imagine that the application should have more functions, for example 'test', and 'undeploy', and that they should be run as sub-commands. We will rewrite 'Your::Command' as follows:

package Your::Command;


sub opt_spec {(
    [ "dry-run|n",     "print out SQL instead of running it" ],
)};


sub arg_spec {(
    [ "command=s",   "what to do", { required => 1 } ],
)};

What we now have is a global option 'dry-run', that applies to all commands, accessed through the third argument to the run() method (see below). Because the 'Your::Command' arg_spec defines a mandatory argument, our usage message is still displayed when we run our command with no arguments. If we wanted an action to take place when no arguments are given, we would make the Your::Command argument optional, and reinstate the run() method in that package.

Now we write a new command class 'Your::Command::deploy' as follows ( 'test' and 'undeploy' are similarly written):

package Your::Command::deploy;


sub arg_spec {(
    [ "database=s",   "production|development",
        { default => 'development', required => 1 } 
    ],
)};


sub run {
    my ($self,$opt,$gopt) = @_;


    if ( $gopt->dry_run ) {
        print "Not ";
    } 
    print "Deploying to ". $opt->database ."\n";
}

1;
__END__


=head1 NAME


Your::Command::deploy - deploy to a database

Lets build the dispatcher class again:

ex3$ app-dispatcher --add-help Your::Command
Found lib/Your/Command/test.pm
Found lib/Your/Command/undeploy.pm
Found lib/Your/Command/deploy.pm
Writing lib/Your/Command/Dispatcher.pm

Notice that the dispatcher has found our new command classes, and the usage message now shows more information about our subcommands:

ex3$ ./ex
usage: ex [options] <command>
    --help         print usage message and exit
    -n --dry-run   print out SQL instead of running it

Commands:
    test         test a database
    deploy       deploy to a database
    undeploy     undeploy a database

The informational text for sub-commands has been taken from the POD documentation in the class file. You can override this by specifying an 'abstract()' method.

Also displayed contextually correctly are usage messages for our sub commands:

ex3$ ./ex deploy 
usage: ex deploy [options] <database>
    --help    print usage message and exit

You can write sub-sub-commands as far down as you want to go. There is however only ever one global option as specified in the Your::Command class.

ex3$ ./ex -n deploy backup
Not Deploying to backup

By default, commands are listed in a semi-random order. If you want to list them in an order that makes more sense (for example in the order they would typicaly be run) you can add an order() method to each class which returns an integer.

package Your::Command::deploy;

sub order {1};

package Your::Command::test;

sub order {2};

package Your::Command::undeploy;

sub order {3};

And now we have:

ex4$ ./ex 
usage: ex [options] <command>
    --help         print usage message and exit
    -n --dry-run   print out SQL instead of running it

Commands:
    deploy       deploy to a database
    test         test a database
    undeploy     undeploy a database

App::Dispatcher is brand new and has just been uploaded to CPAN. Feedback, bug reports, patches etc are very welcome.

Alternatives

It seems the current state of the art for command line applications is App::Cmd. I wrote App::Dispatcher out of the frustration with the time that it takes App::Cmd to simply generate a usage message, since App::Cmd loads every command class in an app dynamically.

1 Comment

In one of my projects, the main program already has hundreds of subcommands. A 'prog --list' is needed to list the subcommands. We also provide a bash-completion script to help users work more quickly on the command line.

I had to split all the help, 1-line summary, and option list of each subcommand into a pregenerated module to speed things up. Otherwise, 'prog --list' will need to read the summary from each subcommand.pm file.

Leave a comment

About Mark Lawrence

user-pic I blog about Perl.