First-Class CLI Applications
Scripts are Our Friends
In my experience, it's pretty common to find a large piece of functionality built into a command-line program (or "script" (and I am totally not going to get into the "script" vs. "program" debate here)) and then to find that the program isn't tested at all. When you say, "For the love of God, why are you not testing this vital program?" the answer is, "Well, scripts are really hard to test!"
It's true. Maybe they're not as hard to test as people think, but it's still a pain. They're also full of too many standards for getopt or error messages. People just do whatever gets work done, and then later have to pay the price for making crazy decisions.
A Simple App::Cmd Program
App::Cmd is a simple framework for writing command-line applications that are easy to test, that have powerful and easy to use standard tools, and that can be extended easily.
For example, here's a simple command we might write, in two parts. First, the script that we put in our path, then the library that implements it.
./bin/christmas:
1: | use Christmas::App; |
./lib/Christmas/App.pm:
1: | package Christmas::App; |
So far, we've only added a little structure to our code, but it's already a big help. The opt_spec
routine uses Getopt::Long::Descriptive to not only process command line switches (with quite a lot of power), but also to generate helpful usage messages like:
Usage: christmas
christmas [-an] [long options...]
-n --nice list only nice people
-a --all list even people for whom shopping is done
We also get a phase before execution but after argument processing to decide whether the arguments we were given make any sense -- here we just ensure that we didn't get any!
Putting it to the Test
One of the big reasons to use App::Cmd was supposed to be its testability, so let's see how that works.
1: | use Test::More; |
This should be fairly straightforward: test_app
runs the application, using the passed arrayref as the value for @ARGS
. It doesn't run in a subprocess, so there's no weird issues with interprocess communication. Also, because it runs in process, you can replace hunks of the app with mocks if you want, and you'll have them available for inspection after testing.
Organizing Complex Interfaces
I didn't write App::Cmd for simple programs, though, I wrote it for complex ones. I wanted to write programs that behave like svn
or git
, where the first thing you tell the command-line program is which of its subcommands you want to run. So, maybe the program we wrote above is meant to be run as christmas list
. We also want to have christmas music
to control our MP3 player and christmas cards
to assemble and send off some mkit Christmas cards.
This is easy, we do it like this:
rename Christmas::App to Christmas::App::Command::list
replace its
use base 'App::Cmd::Simple'
withuse Christmas::App -command
create a new Christmas::App (shown below)
create Christmas::App::Command::music and ::cards
Christmas::App is easy to write; here it is in its entirety:
1: | package Christmas::App; |
Extra commands just need those three original methods, for example:
1: | package Christmas::App::Command::music; |
That's it. Now the new Christmas::App will be run, it will find all the command classes we've written, and it will decide which one to execute based on the first argument to the christmas
command.
Other Cool Stuff
When you write a "full" App::Cmd program -- that is, one that uses App::Cmd and not App::Cmd::Simple -- you get a bunch more features for free. For one thing, you get commands for commands
and help
that can list and describe the other available commands. (commands
is what happens by default if you were to run christmas
with no arguments, but you can change that by writing a default_command
method.)
You get access to the plugins system, which is woefully underdocumented, but allows you to set up easy to use routines in all your commands, so that you could say, in Christmas::App:
1: | package Christmas::App; |
...and then all your commands could use routines like prompt_yn
or prompt_any_key
without you having to waste keystrokes on ugly method calls to some object delegate.
See Also
App::CLI - an alternative to App::Cmd (with almost no documentation)