Writing Modular Command-line Apps with App::Cmd

  • 13,771 views
Uploaded on

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. …

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.

More in: Technology , Business
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
13,771
On Slideshare
0
From Embeds
0
Number of Embeds
3

Actions

Shares
Downloads
184
Comments
0
Likes
12

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. App::Cmd Writing Maintainable Commands
  • 2. TMTOWTDI
  • 3. There’s More Than One Way To Do It
  • 4. Web
  • 5. Web • Mason
  • 6. Web • Mason • Catalyst
  • 7. Web • Mason • Catalyst • CGI::Application
  • 8. Web • Mason • Catalyst • CGI::Application • Maypole
  • 9. Web • Mason • Catalyst • CGI::Application • Maypole • Continuity
  • 10. Web • Mason • RayApp • Catalyst • CGI::Application • Maypole • Continuity
  • 11. Web • Mason • RayApp • Catalyst • Gantry • CGI::Application • Maypole • Continuity
  • 12. Web • Mason • RayApp • Catalyst • Gantry • CGI::Application • Tripletail • Maypole • Continuity
  • 13. Web • Mason • RayApp • Catalyst • Gantry • CGI::Application • Tripletail • Maypole • CGI::ExApp • Continuity
  • 14. Web • Mason • RayApp • Catalyst • Gantry • CGI::Application • Tripletail • Maypole • CGI::ExApp • Continuity • OpenInteract
  • 15. Daemons
  • 16. Daemons • POE
  • 17. Daemons • POE • Danga
  • 18. Daemons • POE • Danga • Net::Server
  • 19. Daemons • POE • Danga • Net::Server • Daemon::Generic
  • 20. Daemons • POE • Proc::Daemon • Danga • Net::Server • Daemon::Generic
  • 21. Daemons • POE • Proc::Daemon • Danga • Net::Daemon • Net::Server • Daemon::Generic
  • 22. Daemons • POE • Proc::Daemon • Danga • Net::Daemon • Net::Server • MooseX::Daemonize • Daemon::Generic
  • 23. Daemons • POE • Proc::Daemon • Danga • Net::Daemon • Net::Server • MooseX::Daemonize • Daemon::Generic • Event
  • 24. TMTOWTDI
  • 25. TMTOWTDI • All the big problem sets have a few solutions!
  • 26. TMTOWTDI • All the big problem sets have a few solutions! • So, when I needed to write a CLI app, I checked CPAN...
  • 27. Command-Line Apps
  • 28. Command-Line Apps • App::CLI
  • 29. Command-Line Apps
  • 30. Command-Line Apps :-(
  • 31. Everybody writes command-line apps!
  • 32. Why are there no good tools?
  • 33. Second-Class Citizens
  • 34. Second-Class Citizens • That’s how we view them.
  • 35. Second-Class Citizens • That’s how we view them. • They’re
  • 36. Second-Class Citizens • That’s how we view them. • They’re • hard to test
  • 37. Second-Class Citizens • That’s how we view them. • They’re • hard to test • not reusable components
  • 38. Second-Class Citizens • That’s how we view them. • They’re • hard to test • not reusable components • hard to add more behavior later
  • 39. Here’s an Example
  • 40. Example Script $ sink 30min “server mx-pa-1 crashed!”
  • 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
  • 47. Insult to Injury
  • 48. Insult to Injury • ...well, that’s going to take a lot of testing.
  • 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)
  • 52. Here’s a Solution
  • 53. Command Breakdown $ sink do --for 1hr --ago 1d ‘rebuild raid’
  • 54. Command Breakdown $ sink do --for 1hr --ago 1d ‘rebuild raid’ App
  • 55. Command Breakdown $ sink do --for 1hr --ago 1d ‘rebuild raid’ Command
  • 56. Command Breakdown $ sink do --for 1hr --ago 1d ‘rebuild raid’ Options
  • 57. Command Breakdown $ sink do --for 1hr --ago 1d ‘rebuild raid’ Args
  • 58. Command Breakdown $ sink do --for 1hr --ago 1d ‘rebuild raid’
  • 59. “do” command
  • 60. “do” command sub run {
  • 61. “do” command sub run { my ($self, $opt, $args) = @_;
  • 62. “do” command sub run { my ($self, $opt, $args) = @_;
  • 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!”; }
  • 75. “do” command
  • 76. “do” command sub opt_desc {
  • 77. “do” command sub opt_desc { [ “start=s”, “when you started doing this” ],
  • 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} ], }
  • 81. “do” command
  • 82. “do” command sub validate_args {
  • 83. “do” command sub validate_args { my ($self, $opt, $args) = @_;
  • 84. “do” command sub validate_args { my ($self, $opt, $args) = @_;
  • 85. “do” command sub validate_args { my ($self, $opt, $args) = @_; if (@$args != 1) {
  • 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;
  • 91. Extra Scaffolding
  • 92. Extra Scaffolding package Sink;
  • 93. Extra Scaffolding package Sink; use base ‘App::Cmd’;
  • 94. Extra Scaffolding package Sink; use base ‘App::Cmd’; 1;
  • 95. Extra Scaffolding use Sink; Sink->run;
  • 96. Testing Your App
  • 97. Testing App::Cmd
  • 98. Testing App::Cmd use Test::More tests => 3;
  • 99. Testing App::Cmd use Test::More tests => 3; use Test::Output;
  • 100. Testing App::Cmd use Test::More tests => 3; use Test::Output;
  • 101. Testing App::Cmd use Test::More tests => 3; use Test::Output; my $error;
  • 102. Testing App::Cmd use Test::More tests => 3; use Test::Output; my $error; my $stdout = do {
  • 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/;
  • 114. Growing Your App
  • 115. Command Breakdown $ sink do --for 1hr --ago 1d ‘rebuild raid’
  • 116. Command Breakdown $ sink do --for 1hr --ago 1d ‘rebuild raid’ Command
  • 117. package Sink::Command::List; use base ‘App::Cmd::Command’; sub opt_desc { ... } sub validate_args { ... } sub run { ... } 1;
  • 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;
  • 119. package Sink::Command::Start; use base ‘App::Cmd::Command’; sub opt_desc { ... } sub validate_args { ... } 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)
  • 123. Command Listing package Sink::Command::Start; =head1 NAME Sink::Command::Start - start a new task =cut
  • 124. Command Listing package Sink::Command::Start; sub abstract { ‘start a new task’; }
  • 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
  • 126. Command Listing
  • 127. Command Listing $ sink help list
  • 128. Command Listing $ sink help list
  • 129. Command Listing $ sink help list sink list [long options...]
  • 130. Command Listing $ sink help list sink list [long options...] -u --user only events for this user
  • 131. Command Listing $ sink help list sink list [long options...] -u --user only events for this user --open only unfinished events
  • 132. Core Commands
  • 133. Core Commands • Where did “help” and “commands” come from?
  • 134. Core Commands • Where did “help” and “commands” come from? • App::Cmd::Command::help
  • 135. Core Commands • Where did “help” and “commands” come from? • App::Cmd::Command::help • App::Cmd::Command::commands
  • 136. Default Command package Sink; use base ‘App::Cmd’; 1;
  • 137. Default Command package Sink; use base ‘App::Cmd’; sub default_command { ‘help’ } # default 1;
  • 138. Default Command package Sink; use base ‘App::Cmd’; sub default_command { ‘list’ } 1;
  • 139. One-Command Applications
  • 140. Simple Example $ ckmail check -a work -a home No new mail.
  • 141. Simple Example $ ckmail -a work -a home No new mail.
  • 142. The Lousy Way
  • 143. The Lousy Way • create Ckmail::Command::Check
  • 144. The Lousy Way • create Ckmail::Command::Check • make empty Ckmail.pm
  • 145. The Lousy Way • create Ckmail::Command::Check • make empty Ckmail.pm • make “check” the default command
  • 146. The Simple Way package Ckmail::Command::Check; use base ‘App::Cmd::Command’; sub opt_desc { ... } sub run { ... } 1;
  • 147. The Simple Way package Ckmail::Command::Check; use base ‘App::Cmd::Simple’; sub opt_desc { ... } sub run { ... } 1;
  • 148. The Simple Way use Ckmail; Ckmail->run;
  • 149. The Simple Way use Ckmail::Command::Check; Ckmail::Command::Check->run;
  • 150. App::Cmd::Simple
  • 151. App::Cmd::Simple • You write a command...
  • 152. App::Cmd::Simple • You write a command... • ...but you use it like an App::Cmd.
  • 153. App::Cmd::Simple • You write a command... • ...but you use it like an App::Cmd. • Later, you can just demote it.
  • 154. Any Questions?
  • 155. Thank You!