24 days of Perl code from RJBS! Feed

First-Class CLI Applications

App::Cmd - 2009-12-14

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: 
2: 

 

use Christmas::App;
Christmas::App->run;

 

./lib/Christmas/App.pm:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 

 

package Christmas::App;
use base 'App::Cmd::Simple';
# ABSTRACT: an app for managing our christmas shopping

sub opt_spec {
  return (
    [ 'nice|n' => 'list only nice people' ],
    [ 'all|a' => 'list even people for whom shopping is done' ],
  );
}

sub validate_args {
  my ($self, $opt, $args) = @_;
  $self->usage_error("no args expected") if @$args;
}

sub execute {
  my ($self, $opt, $args) = @_;

  my @presents = Christmas::Presents->get(
# In other words: use switches to build query:
($opt->all ? () : (done => 0),
    ($opt->nice ? (nice => 1) : ()),
  );
  
  print $self->_list_presents(@presents); # implementation left to imagination
}

 

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: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 

 

use Test::More;
use App::Cmd::Tester;

use Christmas::App;

{
  my $result = test_app('Christmas::App' => [ qw(--all) ]);
  is($result->stderr, '', "nothing to stderr");

  like($result->stdout, qr/Ricardo Signes/, "rjbs is in our list");
  like($result->stdout, qr/Violet Beauregarde/, "violet is, too");
}

{
  my $result = test_app('Christmas::App' => [ qw(--all --nice) ]);
  is($result->stderr, '', "nothing to stderr");

  like($result->stdout, qr/Ricardo Signes/, "rjbs was nice");
  unlike($result->stdout, qr/Violet Beauregarde/, "Violet was naughty");
}

{
  my $result = test_app('Christmas::App' => [ qw(--nice) ]);
  is($result->stderr, '', "nothing to stderr");

  unlike($result->stdout, qr/Ricardo Signes/, "no rjbs w/o --all");
  unlike($result->stdout, qr/Violet Beauregarde/, "Violet still naughty");
}

 

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' with use 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: 
2: 
3: 

 

package Christmas::App;
use App::Cmd::Setup -app;
1;

 

Extra commands just need those three original methods, for example:


1: 
2: 
3: 
4: 
5: 
6: 

 

package Christmas::App::Command::music;
use Christmas::App -command;

sub validate_args { ... }
sub opt_spec { ... }
sub execute { ... }

 

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: 
2: 
3: 
4: 

 

package Christmas::App;
use App::Cmd::Setup -app => {
  plugins => [ qw(App::Cmd::Plugin::Prompt) ],
};

 

...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