writing modular command-line apps with app::cmd

Post on 11-Jun-2015

14.683 Views

Category:

Technology

4 Downloads

Preview:

Click to see full reader

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!

top related