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.
41. Example Script
$ sink --list
who | time | event
------+-------+----------------------------
rjbs | 30min | server mx-pa-1 crashed!
42. Example Script
GetOptions(%opt, ...);
if ($opt{list}) {
die if @ARGV;
@events = Events->get_all;
} else {
my ($duration, $desc) = @ARGV;
Event->new($duration, $desc);
}
43. Example Script
$ sink --list --user jcap
who | time | event
------+-------+----------------------------
jcap | 2hr | redeploy exigency subsystem
44. 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);
}
45. 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);
}
46. 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
49. Insult to Injury
• ...well, that’s going to take a lot of
testing.
• How can we test it?
50. Insult to Injury
• ...well, that’s going to take a lot of
testing.
• How can we test it?
• my $output = `sink @args`;
51. 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)
63. “do” command
sub run {
my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago});
64. “do” command
sub run {
my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago});
my $length = parse_duration($opt->{for});
65. “do” command
sub run {
my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago});
my $length = parse_duration($opt->{for});
my $desc = $args->[0];
66. “do” command
sub run {
my ($self, $opt, $args) = @_;
my $start = parse_ago($opt->{ago});
my $length = parse_duration($opt->{for});
my $desc = $args->[0];
67. “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(
68. “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,
69. “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,
70. “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;
71. “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;
);
72. “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;
);
73. “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!”;
74. “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!”;
}
86. “do” command
sub validate_args {
my ($self, $opt, $args) = @_;
if (@$args != 1) {
$self->usage_error(“provide one argument”);
87. “do” command
sub validate_args {
my ($self, $opt, $args) = @_;
if (@$args != 1) {
$self->usage_error(“provide one argument”);
}
88. “do” command
sub validate_args {
my ($self, $opt, $args) = @_;
if (@$args != 1) {
$self->usage_error(“provide one argument”);
}
}
89. 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;
90. 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;
103. Testing App::Cmd
use Test::More tests => 3;
use Test::Output;
my $error;
my $stdout = do {
local @ARGV = qw(do --for 8hr ‘sleeping’);
104. 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 {
105. 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 = $@;
106. 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 = $@;
});
107. 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 = $@;
});
}
108. 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 = $@;
});
}
109. 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!$/;
110. 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;
111. 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;
112. 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;
113. 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/;
118. 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;
120. 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;
121. More Commands!
$ sink do --for 1hr --ago 1d ‘rebuild raid’
$ sink list --open
$ sink start ‘porting PHP to ASP.NET’
122. 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)
125. 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