File Coverage

blib/lib/App/CLI/Toolkit.pm
Criterion Covered Total %
statement 15 138 10.8
branch 0 78 0.0
condition 0 37 0.0
subroutine 5 11 45.4
pod 2 3 66.6
total 22 267 8.2


line stmt bran cond sub pod time code
1             package App::CLI::Toolkit;
2              
3 1     1   28809 use warnings;
  1         3  
  1         35  
4 1     1   6 use strict;
  1         2  
  1         38  
5              
6 1     1   6 use Carp;
  1         7  
  1         87  
7 1     1   7 use File::Basename;
  1         2  
  1         89  
8 1     1   1507 use Getopt::Long;
  1         15200  
  1         7  
9              
10             our $VERSION = '0.03';
11             our $AUTOLOAD;
12              
13             sub new {
14 0     0 0   my $class = shift;
15 0           my %config = @_;
16              
17 0           my $self = \%config;
18 0           bless $self, $class;
19              
20 0           $self->{_opts} = {};
21              
22 0 0 0       if (exists $self->{description} && ref($self->{description})) {
23 0           croak "description should be a plain string";
24             }
25              
26 0 0         if (exists $self->{options}) {
27 0 0         if (!UNIVERSAL::isa($self->{options}, 'HASH')) {
28 0           croak "params argument should be a hash ref";
29             }
30            
31 0 0 0       if (!(grep { /\bh(elp)?\b/ } keys %{$self->{options}}) && !$self->{noautohelp}) {
  0            
  0            
32 0           $self->{options}{'help|h'} = "Show this help documentation";
33             }
34 0 0         GetOptions($self->{_opts}, keys %{$self->{options}}) or $self->_exit_with_usage(1);
  0            
35             }
36              
37 0 0         if (exists $self->{params}) {
38 0 0         if (!UNIVERSAL::isa($self->{params}, 'ARRAY')) {
39 0           croak "params argument should be an array ref";
40             }
41 0           my @params = @{$self->{params}};
  0            
42            
43 0           my $found_multi_value_param = 0;
44 0           my $found_optional_param = 0;
45            
46 0           foreach my $param (@params) {
47 0 0         if ($param !~ /^\w+[\*\+\?]?$/) {
48 0           croak "Invalid param name $param: must match \\w+ with optional trailing [*+?]";
49             }
50            
51 0 0         if ($param =~ /[\*\+]$/) {
    0          
52 0 0         if ($found_optional_param) {
53 0           croak "Can't have multiple-value parameter after an optional parameter";
54             }
55 0 0         if ($found_multi_value_param) {
56 0           croak "Can't have more than one multiple-value parameter"
57             }
58 0           $found_multi_value_param = 1;
59 0 0         $found_optional_param = 1 if $param =~ /\*$/;
60             } elsif ($param =~ /\?$/) {
61 0 0         if ($found_multi_value_param) {
62 0           croak "Can't have optional parameter after a multiple-value parameter";
63             }
64 0           $found_optional_param = 1;
65             }
66            
67 0 0 0       if ($param !~ /[\?\*]/ && $found_optional_param) {
68 0           croak "Can't have a non-optional parameter after an optional parameter";
69             }
70             }
71              
72             # Check number of elements in ARGV is at least as many as the number of
73             # non-optional params
74 0 0         if ((grep { /[\w\+]$/ } @params) > @ARGV) {
  0            
75 0           $self->_exit_with_usage(1, "Missing command-line parameters");
76             }
77              
78 0           my $shifting = 1;
79 0   0       while (@params && @ARGV) {
80 0           my ($key, $value);
81 0 0 0       if (@params > 1 && $params[0] =~ /[\+\*]$/ && $shifting) {
      0        
82             # we've found the multi-value params, so start popping from
83             # the end of @params instead of shifting from the front
84 0           $shifting = 0;
85             }
86            
87 0 0         $key = $shifting ? shift @params : pop @params;
88 0 0         if ($key =~ /[\+\*]$/) {
89 0           $key =~ s/[\+\*]$//;
90 0           $value = [ @ARGV ];
91 0           @ARGV = ();
92             } else {
93 0 0         $value = $shifting ? shift @ARGV : pop @ARGV;
94 0           $key =~ s/\?$//;
95             }
96 0 0         if (exists $self->{_opts}{$key}) {
97 0           croak "Can't have a param and an option with the same name ($key)";
98             }
99 0           $self->{_opts}{$key} = $value;
100             }
101             }
102            
103 0 0 0       if (exists $self->{_opts}{help} && $self->{_opts}{help}) {
104 0           $self->_exit_with_usage(0);
105             }
106              
107 0           return $self;
108             }
109              
110             sub AUTOLOAD {
111 0     0     my $self = shift;
112 0           my $key = $AUTOLOAD;
113 0           $key =~ s/.*:://; # trim off package qualifier
114              
115             # If the key contains an underscore it might represent a key with
116             # a hyphen - let's check
117 0 0         if ($key =~ /_/) {
118 0           (my $alt_key = $key) =~ s/_/-/g;
119 0 0 0       if (!exists $self->{_opts}{$key} && exists $self->{_opts}{$alt_key}) {
120 0           $key = $alt_key;
121             }
122             }
123              
124 0           return $self->get($key);
125             }
126              
127             sub get {
128 0     0 1   my $self = shift;
129 0           my $key = shift;
130 0           my $retval = $self->{_opts}{$key};
131 0 0 0       if (UNIVERSAL::isa($retval, 'ARRAY') && wantarray) {
132 0           return @$retval;
133             } else {
134 0           return $retval;
135             }
136             }
137              
138             # Explicit DESTROY, else it gets handled by AUTOLOAD
139 0     0     sub DESTROY {}
140              
141             sub usage {
142 0     0 1   my $self = shift;
143 0           my $result = '';
144 0           my $script_name = basename($0);
145 0           my $description = $self->{description};
146              
147 0           my %ARG_TYPES = (
148             s => 'STR',
149             i => 'INT',
150             f => 'FLOAT',
151             );
152            
153 0           $result .= "Usage: $script_name";
154 0 0         $result .= " [OPTIONS]" if $self->{options};
155 0           foreach my $param (@{$self->{params}}) {
  0            
156 0 0         if ($param =~ /^(.*)\+$/) {
    0          
    0          
157 0           my $p = uc $1;
158 0           $result .= " $p [$p...]";
159             } elsif ($param =~ /^(.*)\*$/) {
160 0           my $p = uc $1;
161 0           $result .= " [$p $p...]";
162             } elsif ($param =~ /^(.*)\?$/) {
163 0           my $p = uc $1;
164 0           $result .= " [$p]";
165             } else {
166 0           $result .= " " . uc($param);
167             }
168             }
169 0           $result .= "\n";
170 0 0         $result .= $self->{description} . "\n" if $self->{description};
171              
172 0 0         if ($self->{options}) {
173 0           $result .= "\nArguments shown for an option apply to all variants of that option\n";
174 0           foreach my $opt (sort keys %{$self->{options}}) {
  0            
175 0           my ($arg_type, @variants);
176 0           my $option = '';
177            
178 0 0         if ($opt =~ /^(.*)=([sif])([\%\@])?$/) {
    0          
179 0           @variants = split(/\|/, $1);
180 0           $arg_type = $ARG_TYPES{$2};
181 0   0       $option = $3 || '';
182             } elsif ($opt =~ /^(.*)([\+])$/) {
183 0           @variants = split(/\|/, $1);
184 0           $option = $2;
185             } else {
186 0           @variants = split(/\|/, $opt);
187             }
188 0 0         my $variants_str = join(
189             ", ",
190 0           map { length > 1 ? "--$_" : "-$_" }
191 0           sort { length($a) <=> length($b) }
192             @variants
193             );
194 0 0 0       if ($arg_type && $option eq '%') {
    0          
195 0           $variants_str .= " KEY=$arg_type";
196             } elsif ($arg_type) {
197 0           $variants_str .= " $arg_type";
198             }
199 0           $result .= " " x 2 . $variants_str . "\n";
200 0           $result .= " " x 4 . $self->{options}{$opt} . "\n";
201 0 0         $result .= " " x 4 . "(Use more than once for enhanced effect)" . "\n" if $option eq '+';
202 0 0 0       $result .= " " x 4 . "(Use more than once to specify multiple values)" . "\n" if $option eq '@' || $option eq '%';
203             }
204             }
205 0           return $result;
206             }
207              
208             sub _exit_with_usage {
209 0     0     my $self = shift;
210 0   0       my $exit_code = shift || 0;
211 0           my $msg = shift;
212            
213 0 0         print "$msg\n" if $msg;
214 0           print $self->usage;
215 0           exit($exit_code);
216             }
217              
218             1;
219              
220             =head1 NAME
221              
222             App::CLI::Toolkit - a helper module for generating command-line utilities
223              
224             =head1 DESCRIPTION
225              
226             App::CLI::Toolkit is designed to take the hassle out of writing command-line apps
227             in Perl. It handles the parsing of both parameters and options (see below for
228             the distinction) and generates usage information from the details you give it.
229              
230             =head1 SYNOPSIS
231              
232             use App::CLI::Toolkit;
233              
234             my $app = App::CLI::Toolkit->new(
235             description = 'A replacement for cp',
236             params = [ qw(source dest) ],
237             options = {
238             'recursive|r' => 'If source is a directory, copies'
239             . 'the directory contents recursively',
240             'force|f' => 'If target already exists, overwrite it'
241             'verbose|v' => 'Produce verbose output'
242             }
243             );
244            
245             print "Copying " . $app->source . " to " . $app->dest . "\n" if $app->verbose;
246            
247             if ($app->recursive) {
248             # Do recursive gumbo
249             }
250            
251             if ($app->force) {
252             # Don't take no for an answer
253             }
254             ...
255              
256             =head1 CONSTRUCTOR
257              
258             App::CLI::Toolkit->new(%config)
259              
260             Constructs a new App::CLI::Toolkit object
261              
262             =head2 Constructor arguments
263              
264             =head3 description
265              
266             A description of what the app does. Used in the usage string that
267             App::CLI::Toolkit generates.
268              
269             Example:
270              
271             $app = new App::CLI::Toolkit(description => 'A cool new replacement for grep!')
272              
273             =head3 noautohelp
274              
275             App::CLI::Toolkit automatically gives your app help options (-h and --help).
276             Supply a noautohelp value that equates to true (e.g. 1) to suppress this.
277              
278             =head3 options
279              
280             A reference to a hash mapping option names to a description of what the option
281             does. The hash keys follow the conventions of L.
282              
283             =head3 params
284              
285             A reference to an array of parameter names. When the app is invoked, parameters
286             follow the app name on the command line.
287              
288             Example:
289              
290             $app = new App::CLI::Toolkit(params => ['name'])
291             print uc $app->name
292              
293             Yields this result:
294              
295             $ my-app fred
296             FRED
297              
298             =over
299              
300             =item Optional parameters
301              
302             Parameters can be optional, in which case your application will provide a default
303             if the user doesn't provide a parameter. For example, the target argument to C
304             is optional and defaults to the filename of the source in the current working directory.
305              
306             Specify an optional argument in C by adding C to the end of the
307             parameter name.
308              
309             Example:
310              
311             $app = new App::CLI::Toolkit(params => ['target?']);
312             print $app->target || $ENV{PWD} . "\n"
313              
314             Yields this result:
315              
316             $ my-app /var/tmp
317             /var/tmp
318            
319             $ my-app
320             /home/simon
321              
322             =item Multiple-Value Parameters
323              
324             Applications can take one or more instances of a particular parameter. For example,
325             C takes one or more file arguments followed by a single target parameter.
326              
327             Specify a multiple-value argument in C by adding C<+> to the end of the
328             parameter name.
329              
330             The accessor for multiple-value parameters returns a list, even if the user only supplied
331             one value.
332              
333             Example:
334              
335             $app = new App::CLI::Toolkit(params => ['files+']);
336             print join(', ', $app->files) . "\n"
337              
338             Yields this result:
339              
340             $ my-app foo bar wibble
341             foo, bar, wibble
342              
343             =item Optional, Multiple-Value Parameters
344              
345             Applications can take zero or more instances of a particular parameter. For example,
346             C takes either no parameters (in which case it lists the contents of the current
347             working directory) or a list of parameters (in which case it lists the contents of
348             each parameter).
349              
350             Specify an optional, multiple-value argument in C by adding C<*>
351             to the end of the parameter name.
352              
353             The accessor for optional, multiple-value parameters returns a list, even if the
354             user only supplied one value.
355              
356             Example:
357              
358             $app = new App::CLI::Toolkit(params => ['dirs*']);
359             if ($app->dirs) {
360             print join(', ', $app->dirs) . "\n";
361             } else {
362             print "No dirs given, using $ENV{PWD}\n";
363             }
364              
365             Yields this result:
366              
367             $ my-app foo bar wibble
368             foo, bar, wibble
369            
370             $ my-app foo
371             foo
372            
373             $ my-app
374             No dirs given, using /home/simon
375              
376             =item Some notes about optional and multiple-value parameters
377              
378             =over
379              
380             =item *
381              
382             You can only have one multiple-value parameter type per application.
383              
384             =item *
385              
386             You can't follow an optional parameter type with a non-optional parameter type.
387              
388             =back
389              
390             =back
391              
392             =head1 METHODS
393              
394             =head2 App-specific accessors
395              
396             Your App::CLI::Toolkit object has methods named after each of the params and
397             options specified in the constructor.
398              
399             Example:
400              
401             $app = App::CLI::Toolkit(
402             params => [ qw(foo bar?) ],
403             options => {
404             'verbose|v' => 'Give more verbose output',
405             }
406             )
407             print $app->foo;
408             print $app->bar if $app->bar;
409            
410             print "Some verbose rubbish\n" if $app->verbose;
411            
412             Where an option has multiple labels (eg. C and C in the above example),
413             the accessor has the name of the first label in the list.
414              
415             =head2 get(key)
416              
417             Gets the value stored against key, where key is an option name or param label.
418             This is an alternative to the convenience accessors named after the option name
419             or param label.
420              
421             Example:
422              
423             $app = App::CLI::Toolkit(params => ['foo'])
424            
425             print $app->foo; # prints the value of the 'foo' param
426             print $app->get('foo') # same
427              
428             =head2 usage()
429              
430             Gets the usage string for your application
431              
432             =head1 AUTHOR
433              
434             Simon Whitaker, C<< >>
435              
436             =head1 BUGS
437              
438             Please report any bugs or feature requests to C, or through
439             the web interface at L.
440             I will be notified, and then you'll automatically be notified of progress on your bug as I make
441             changes.
442              
443             =head1 SUPPORT
444              
445             You can find documentation for this module with the perldoc command.
446              
447             perldoc App::CLI::Toolkit
448              
449             You can also look for information at:
450              
451             =over 4
452              
453             =item * RT: CPAN's request tracker
454              
455             L
456              
457             =item * AnnoCPAN: Annotated CPAN documentation
458              
459             L
460              
461             =item * CPAN Ratings
462              
463             L
464              
465             =item * Search CPAN
466              
467             L
468              
469             =back
470              
471             =head1 ACKNOWLEDGEMENTS
472              
473             Thanks to Chris Lokotsch for the code reviews.
474              
475             =head1 COPYRIGHT & LICENSE
476              
477             Copyright 2008 Simon Whitaker, all rights reserved.
478              
479             This program is free software; you can redistribute it and/or modify it
480             under the same terms as Perl itself.
481              
482             =cut
483              
484