NAME
App::Dispatcher::Tutorial - build fast command-line applications
VERSION
0.11. Development Release.
INTRODUCTION
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.
COMMAND-LINE COMPONENTS
How exactly does one define a command line application? App::Dispatcher assumes a fairly common (but by no means universal) approach:
- command
-
The program name - i.e. the filename be executed by the shell.
- options
-
Options affect the way a command runs. They are generally not required to be present, but that is configurable.
- arguments
-
Arguments are positional parameters that a command needs know in order to do its work.
A command can also have sub-commands. Each sub command can have its own options and arguments. From the users point of view sub-commands and their options are indisinguishable from options and arguments to the main command, but from an implementation perspective they are separate, stand-alone programs, with possibly their own set of sub-commands.
One additional element that App::Dispatcher knows about are Global Options - options which are defined once and used by every sub-command the same way.
The rest of this tutorial is all about how to write Command Classes.
SIMPLE COMMANDS
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 uses the '[]' brackets around '<database>'. 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>'
}
SUB-COMMANDS
Imagine that the application should now 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 require_order { 1 }
sub gopt_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. 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 <command> argument optional, and reinstate the run() method in that package.
The 'require_order' subroutine is new. By writing this routine to return a true value, we force Getopt::Long to look for options before arguments.
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) = @_;
if ( $opt->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
ADVANCED
When your dispatcher runs it also adds a couple of extra methods to your command classes, which let you do advanced things such as re-dispatching to another command or printing the usage based on some run-time (not dispatch-time) condition:
use Database::Lookup;
sub run {
my ($self,$opt) = @_;
if ( database_lookup($self->opt->id) ) {
return $self->dispatch(
'other','sub_command','--other', '--options');
}
else {
die "Unknown ID\n" . $self->usage();
}
}
The extra methods are:
- opt()
-
Same as the first argument to your run() method.
- usage()
-
The Getopt::Long::Descriptive usage object.
- dispatch()
-
Runs the command dispatcher again but with different arguments.
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 my app dynamically.
AUTHOR
Mark Lawrence <nomad@null.net>.
COPYRIGHT AND LICENSE
Copyright (C) 2011 Mark Lawrence <nomad@null.net>
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.