writing modular command-line apps with app::cmd
DESCRIPTION
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.TRANSCRIPT
App::CmdWriting Maintainable Commands
TMTOWTDI
There’s More Than One Way To Do It
Web
Web
•Mason
Web
•Mason
•Catalyst
Web
•Mason
•Catalyst
•CGI::Application
Web
•Mason
•Catalyst
•CGI::Application
•Maypole
Web
•Mason
•Catalyst
•CGI::Application
•Maypole
•Continuity
Web
•Mason
•Catalyst
•CGI::Application
•Maypole
•Continuity
•RayApp
Web
•Mason
•Catalyst
•CGI::Application
•Maypole
•Continuity
•RayApp
•Gantry
Web
•Mason
•Catalyst
•CGI::Application
•Maypole
•Continuity
•RayApp
•Gantry
•Tripletail
Web
•Mason
•Catalyst
•CGI::Application
•Maypole
•Continuity
•RayApp
•Gantry
•Tripletail
•CGI::ExApp
Web
•Mason
•Catalyst
•CGI::Application
•Maypole
•Continuity
•RayApp
•Gantry
•Tripletail
•CGI::ExApp
•OpenInteract
Daemons
Daemons
•POE
Daemons
•POE
•Danga
Daemons
•POE
•Danga
•Net::Server
Daemons
•POE
•Danga
•Net::Server
•Daemon::Generic
Daemons
•POE
•Danga
•Net::Server
•Daemon::Generic
•Proc::Daemon
Daemons
•POE
•Danga
•Net::Server
•Daemon::Generic
•Proc::Daemon
•Net::Daemon
Daemons
•POE
•Danga
•Net::Server
•Daemon::Generic
•Proc::Daemon
•Net::Daemon
•MooseX::Daemonize
Daemons
•POE
•Danga
•Net::Server
•Daemon::Generic
•Proc::Daemon
•Net::Daemon
•MooseX::Daemonize
•Event
TMTOWTDI
TMTOWTDI
•All the big problem sets have a few solutions!
TMTOWTDI
•All the big problem sets have a few solutions!
• So, when I needed to write a CLI app, I checked CPAN...
Command-Line Apps
Command-Line Apps
•App::CLI
Command-Line Apps
Command-Line Apps
:-(
Everybody writes command-line apps!
Why are there no good tools?
Second-Class Citizens
Second-Class Citizens
•That’s how we view them.
Second-Class Citizens
•That’s how we view them.
•They’re
Second-Class Citizens
•That’s how we view them.
•They’re
•hard to test
Second-Class Citizens
•That’s how we view them.
•They’re
•hard to test
•not reusable components
Second-Class Citizens
•That’s how we view them.
•They’re
•hard to test
•not reusable components
•hard to add more behavior later
Here’s an Example
Example Script
$ sink 30min “server mx-pa-1 crashed!”
Example Script
$ sink --list
who | time | event------+-------+----------------------------rjbs | 30min | server mx-pa-1 crashed!
Example Script
GetOptions(\%opt, ...);
if ($opt{list}) { die if @ARGV; @events = Events->get_all;} else { my ($duration, $desc) = @ARGV; Event->new($duration, $desc);}
Example Script
$ sink --list --user jcap
who | time | event------+-------+----------------------------jcap | 2hr | redeploy exigency subsystem
Example Script
GetOptions(\%opt, ...);
if ($opt{list}) { die if @ARGV; @events = $opt{user} ? Events->get(user => $opt{user}) : Events->get_all;} else { my ($duration, $desc) = @ARGV; Event->new($duration, $desc);}
Example Script
GetOptions(\%opt, ...);
if ($opt{list}) { die if @ARGV; @events = $opt{user} ? Events->get(user => $opt{user}) : Events->get_all;} else { my ($duration, $desc) = @ARGV; die if $opt{user}; Event->new($duration, $desc);}
Example Script
$ sink --start ‘putting out oil fire‘Event begun! use --finish to finish event
$ sink --list --open18. putting out oil fire
$ sink --finish 18Event finished! Total time taken: 23 min
Insult to Injury
Insult to Injury
•...well, that’s going to take a lot of testing.
Insult to Injury
•...well, that’s going to take a lot of testing.
•How can we test it?
Insult to Injury
•...well, that’s going to take a lot of testing.
•How can we test it?
•my $output = `sink @args`;
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)
Here’s a Solution
Command Breakdown
$ sink do --for 1hr --ago 1d ‘rebuild raid’
Command Breakdown
$ sink do --for 1hr --ago 1d ‘rebuild raid’
App
Command Breakdown
$ sink do --for 1hr --ago 1d ‘rebuild raid’
Command
Command Breakdown
$ sink do --for 1hr --ago 1d ‘rebuild raid’
Options
Command Breakdown
$ sink do --for 1hr --ago 1d ‘rebuild raid’
Args
Command Breakdown
$ sink do --for 1hr --ago 1d ‘rebuild raid’
“do” command
“do” commandsub run {
“do” commandsub run { my ($self, $opt, $args) = @_;
“do” commandsub run { my ($self, $opt, $args) = @_;
“do” commandsub run { my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago});
“do” commandsub run { my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for});
“do” commandsub run { my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0];
“do” commandsub run { my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0];
“do” commandsub run { my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago}); my $length = parse_duration($opt->{for}); my $desc = $args->[0];
Sink::Event->create(
“do” commandsub 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,
“do” commandsub 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,
“do” commandsub 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;
“do” commandsub 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; );
“do” commandsub 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; );
“do” commandsub 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!”;
“do” commandsub 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!”;}
“do” command
“do” command
sub opt_desc {
“do” command
sub opt_desc { [ “start=s”, “when you started doing this” ],
“do” command
sub opt_desc { [ “start=s”, “when you started doing this” ], [ “for=s”, “how long you did this for”,
“do” command
sub opt_desc { [ “start=s”, “when you started doing this” ], [ “for=s”, “how long you did this for”, { required => 1} ],
“do” command
sub opt_desc { [ “start=s”, “when you started doing this” ], [ “for=s”, “how long you did this for”, { required => 1} ],}
“do” command
“do” command
sub validate_args {
“do” command
sub validate_args { my ($self, $opt, $args) = @_;
“do” command
sub validate_args { my ($self, $opt, $args) = @_;
“do” command
sub validate_args { my ($self, $opt, $args) = @_;
if (@$args != 1) {
“do” command
sub validate_args { my ($self, $opt, $args) = @_;
if (@$args != 1) { $self->usage_error(“provide one argument”);
“do” command
sub validate_args { my ($self, $opt, $args) = @_;
if (@$args != 1) { $self->usage_error(“provide one argument”); }
“do” command
sub validate_args { my ($self, $opt, $args) = @_;
if (@$args != 1) { $self->usage_error(“provide one argument”); } }
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;
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;
Extra Scaffolding
Extra Scaffolding
package Sink;
Extra Scaffolding
package Sink;use base ‘App::Cmd’;
Extra Scaffolding
package Sink;use base ‘App::Cmd’;
1;
Extra Scaffolding
use Sink;Sink->run;
Testing Your App
Testing App::Cmd
Testing App::Cmd
use Test::More tests => 3;
Testing App::Cmd
use Test::More tests => 3;use Test::Output;
Testing App::Cmd
use Test::More tests => 3;use Test::Output;
Testing App::Cmd
use Test::More tests => 3;use Test::Output;
my $error;
Testing App::Cmd
use Test::More tests => 3;use Test::Output;
my $error;my $stdout = do {
Testing App::Cmd
use Test::More tests => 3;use Test::Output;
my $error;my $stdout = do { local @ARGV = qw(do --for 8hr ‘sleeping’);
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 {
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 = $@;
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 = $@; });
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 = $@; });}
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 = $@; });}
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!$/;
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;
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;
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;
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/;
Growing Your App
Command Breakdown
$ sink do --for 1hr --ago 1d ‘rebuild raid’
Command Breakdown
$ sink do --for 1hr --ago 1d ‘rebuild raid’
Command
package Sink::Command::List;use base ‘App::Cmd::Command’;
sub opt_desc { ... }
sub validate_args { ... }
sub run { ... }
1;
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;
package Sink::Command::Start;use base ‘App::Cmd::Command’;
sub opt_desc { ... }
sub validate_args { ... }
sub run { ... }
1;
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;
More Commands!
$ sink do --for 1hr --ago 1d ‘rebuild raid’
$ sink list --open
$ sink start ‘porting PHP to ASP.NET’
More Commands!
$ sinksink help <command>
Available commands: commands: list the application’s commands help: display a command’s help screen
do: (unknown) list: (unknown) start: (unknown)
Command Listing
package Sink::Command::Start;
=head1 NAME
Sink::Command::Start - start a new task
=cut
Command Listing
package Sink::Command::Start;
sub abstract { ‘start a new task’; }
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
Command Listing
Command Listing
$ sink help list
Command Listing
$ sink help list
Command Listing
$ sink help list
sink list [long options...]
Command Listing
$ sink help list
sink list [long options...] -u --user only events for this user
Command Listing
$ sink help list
sink list [long options...] -u --user only events for this user --open only unfinished events
Core Commands
Core Commands
•Where did “help” and “commands” come from?
Core Commands
•Where did “help” and “commands” come from?
•App::Cmd::Command::help
Core Commands
•Where did “help” and “commands” come from?
•App::Cmd::Command::help
•App::Cmd::Command::commands
Default Command
package Sink;use base ‘App::Cmd’;
1;
Default Command
package Sink;use base ‘App::Cmd’;
sub default_command { ‘help’ } # default
1;
Default Command
package Sink;use base ‘App::Cmd’;
sub default_command { ‘list’ }
1;
One-Command Applications
Simple Example
$ ckmail check -a work -a homeNo new mail.
Simple Example
$ ckmail -a work -a homeNo new mail.
The Lousy Way
The Lousy Way
•create Ckmail::Command::Check
The Lousy Way
•create Ckmail::Command::Check
•make empty Ckmail.pm
The Lousy Way
•create Ckmail::Command::Check
•make empty Ckmail.pm
•make “check” the default command
The Simple Way
package Ckmail::Command::Check;use base ‘App::Cmd::Command’;
sub opt_desc { ... }
sub run { ... }
1;
The Simple Way
package Ckmail::Command::Check;use base ‘App::Cmd::Simple’;
sub opt_desc { ... }
sub run { ... }
1;
The Simple Way
use Ckmail;Ckmail->run;
The Simple Way
use Ckmail::Command::Check;Ckmail::Command::Check->run;
App::Cmd::Simple
App::Cmd::Simple
•You write a command...
App::Cmd::Simple
•You write a command...
• ...but you use it like an App::Cmd.
App::Cmd::Simple
•You write a command...
• ...but you use it like an App::Cmd.
•Later, you can just demote it.
Any Questions?
Thank You!