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.
38. Second-Class Citizens
⢠Thatâs how we view them.
⢠Theyâre
⢠hard to test
⢠not reusable components
⢠hard to add more behavior later
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)
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!â;
}
78. âdoâ command
sub opt_desc {
[ âstart=sâ, âwhen you started doing thisâ ],
[ âfor=sâ, âhow long you did this forâ,
79. âdoâ command
sub opt_desc {
[ âstart=sâ, âwhen you started doing thisâ ],
[ âfor=sâ, âhow long you did this forâ,
{ required => 1} ],
80. âdoâ command
sub opt_desc {
[ âstart=sâ, âwhen you started doing thisâ ],
[ âfor=sâ, âhow long you did this forâ,
{ required => 1} ],
}
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