Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing Maintainable Commands with App::Cmd

Writing Maintainable Commands with App::Cmd

It's easy to write command-line programs in Perl. There are a million option parsers to choose from, and Perl makes it easy to deal with input, output, and all that stuff.

Once your program has gotten beyond just taking a few switches, though, it can be difficult to maintain a clear interface and well-tested code. App::Cmd is a lightweight framework for writing easy to manage CLI programs.

This talk provides an introduction to writing programs with App::Cmd.

Ricardo Signes

September 15, 2007
Tweet

More Decks by Ricardo Signes

Other Decks in Programming

Transcript

  1. Web

  2. TMTOWTDI •All the big problem sets have a few solutions!

    •So, when I needed to write a CLI app, I checked CPAN...
  3. Second-Class Citizens •That’s how we view them. •They’re •hard to

    test •not reusable components •hard to add more behavior later
  4. Example Script $ sink --list who | time | event

    ------+-------+---------------------------- rjbs | 30min | server mx-pa-1 crashed!
  5. Example Script GetOptions(\%opt, ...); if ($opt{list}) { die if @ARGV;

    @events = Events->get_all; print_events(@events);
  6. Example Script GetOptions(\%opt, ...); if ($opt{list}) { die if @ARGV;

    @events = Events->get_all; print_events(@events); } else {
  7. Example Script GetOptions(\%opt, ...); if ($opt{list}) { die if @ARGV;

    @events = Events->get_all; print_events(@events); } else { my ($duration, $desc) = @ARGV;
  8. Example Script GetOptions(\%opt, ...); if ($opt{list}) { die if @ARGV;

    @events = Events->get_all; print_events(@events); } else { my ($duration, $desc) = @ARGV; Event->new($duration, $desc);
  9. Example Script GetOptions(\%opt, ...); if ($opt{list}) { die if @ARGV;

    @events = Events->get_all; print_events(@events); } else { my ($duration, $desc) = @ARGV; Event->new($duration, $desc); }
  10. Example Script $ sink --list --user jcap who | time

    | event ------+-------+---------------------------- jcap | 2hr | redeploy exigency subsystem
  11. Example Script GetOptions(\%opt, ...); if ($opt{list}) { die if @ARGV;

    @events = $opt{user} ? Events->get(user => $opt{user}) : Events->get_all; print_events(@events); } else { my ($duration, $desc) = @ARGV; Event->new($duration, $desc); }
  12. Example Script GetOptions(\%opt, ...); if ($opt{list}) { die if @ARGV;

    @events = $opt{user} ? Events->get(user => $opt{user}) : Events->get_all; print_events(@events); } else { my ($duration, $desc) = @ARGV; die if $opt{user}; Event->new($duration, $desc); }
  13. Example Script $ sink --start ‘putting out oil fire‘ Event

    begun! use --finish to finish event $ sink --list --open 18. putting out oil fire $ sink --finish 18 Event finished! Total time taken: 23 min
  14. Insult to Injury •...well, that’s going to take a lot

    of testing. •How can we test it? •my $output = `sink @args`;
  15. Insult to Injury •...well, that’s going to take a lot

    of testing. •How can we test it? •my $output = `sink @args`; •IPC::Run3 (or one of those)
  16. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago});
  17. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for});
  18. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0];
  19. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0];
  20. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create(
  21. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start,
  22. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start, finish => $start + $length,
  23. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start, finish => $start + $length, desc => $desc;
  24. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start, finish => $start + $length, desc => $desc; );
  25. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start, finish => $start + $length, desc => $desc; );
  26. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start, finish => $start + $length, desc => $desc; ); print “event created!”;
  27. “do” command sub run { my ($self, $opt, $args) =

    @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start, finish => $start + $length, desc => $desc; ); print “event created!”; }
  28. “do” command sub opt_desc { [ “start=s”, “when you started

    doing this” ], [ “for=s”, “how long you did this for”,
  29. “do” command sub opt_desc { [ “start=s”, “when you started

    doing this” ], [ “for=s”, “how long you did this for”, { required => 1} ],
  30. “do” command sub opt_desc { [ “start=s”, “when you started

    doing this” ], [ “for=s”, “how long you did this for”, { required => 1} ], }
  31. “do” command sub validate_args { my ($self, $opt, $args) =

    @_; if (@$args != 1) { $self->usage_error(“provide one argument”);
  32. “do” command sub validate_args { my ($self, $opt, $args) =

    @_; if (@$args != 1) { $self->usage_error(“provide one argument”); }
  33. “do” command sub validate_args { my ($self, $opt, $args) =

    @_; if (@$args != 1) { $self->usage_error(“provide one argument”); } }
  34. package Sink::Command::Do; use base ‘App::Cmd::Command’; sub opt_desc { [ “start=s”,

    “when you started doing this” ], [ “for=s”, “how long you did this for”, { required => 1} ], } sub validate_args { my ($self, $opt, $args) = @_; if (@$args != 1) { $self->usage_error(“provide one argument”); } } sub run { my ($self, $opt, $args) = @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start, finish => $start + $length, desc => $desc; ); print “event created!”; } 1;
  35. package Sink::Command::Do; use base ‘App::Cmd::Command’; sub opt_desc { [ “start=s”,

    “when you started doing this” ], [ “for=s”, “how long you did this for”, { required => 1} ], } sub validate_args { my ($self, $opt, $args) = @_; if (@$args != 1) { $self->usage_error(“provide one argument”); } } sub run { my ($self, $opt, $args) = @_; my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0]; Sink::Event->create( start => $start, finish => $start + $length, desc => $desc; ); print “event created!”; } 1;
  36. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’);
  37. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’); stdout_from(sub {
  38. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’); stdout_from(sub { eval { Sink->run; 1 } or $error = $@;
  39. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’); stdout_from(sub { eval { Sink->run; 1 } or $error = $@; });
  40. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’); stdout_from(sub { eval { Sink->run; 1 } or $error = $@; }); }
  41. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’); stdout_from(sub { eval { Sink->run; 1 } or $error = $@; }); }
  42. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’); stdout_from(sub { eval { Sink->run; 1 } or $error = $@; }); } like $stdout, qr/^event created!$/;
  43. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’); stdout_from(sub { eval { Sink->run; 1 } or $error = $@; }); } like $stdout, qr/^event created!$/; is Sink::Event->get_count, 1;
  44. Testing App::Cmd use Test::More tests => 3; use Test::Output; my

    $error; my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’); stdout_from(sub { eval { Sink->run; 1 } or $error = $@; }); } like $stdout, qr/^event created!$/; is Sink::Event->get_count, 1; ok ! $error;
  45. Testing App::Cmd use Test::More tests => 3; use Test::App::Cmd; use

    Sink; my ($stdout, $error) = test_app( Sink => qw(do --for 8hr ‘sleeping’) ); like $stdout, qr/^event created!$/; is Sink::Event->get_count, 1; ok ! $error;
  46. Testing App::Cmd use Test::More tests => π; use Sink::Command::Do; eval

    { Sink::Command::Do->validate_args( { for => ‘1hr’ }, [ 1, 2, 3 ], ); }; like $@, qr/one arg/;
  47. package Sink::Command::List; use base ‘App::Cmd::Command’; sub opt_desc { [ “open”,

    “only unfinished events” ], [ “user|u=s”, “only events for this user” ], } sub validate_args { shift->usage_error(’no args allowed’) if @{ $_[1] } } sub run { ... } 1;
  48. package Sink::Command::Start; use base ‘App::Cmd::Command’; sub opt_desc { return }

    sub validate_args { shift->usage_error(’one args required’) if @{ $_[1] } != 1 } sub run { ... } 1;
  49. More Commands! $ sink do --for 1hr --ago 1d ‘rebuild

    raid’ $ sink list --open $ sink start ‘porting PHP to ASP.NET’
  50. More Commands! $ sink sink help <command> Available commands: commands:

    list the application’s commands help: display a command’s help screen do: (unknown) list: (unknown) start: (unknown)
  51. Command Listing $ sink commands Available commands: commands: list the

    application’s commands help: display a command’s help screen do: record that you did something list: list existing events start: start a new task
  52. Command Listing $ sink help list sink list [long options...]

    -u --user only events for this user --open only unfinished events
  53. App::Cmd::Simple •You write a command... •...but you use it like

    an App::Cmd. •Later, you can just demote it.