Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Writing Modular Command-line Apps with App::Cmd

16,432 views

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

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.

Published in: Technology, Business

Writing Modular Command-line Apps with App::Cmd

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

×