File Coverage

blib/lib/Commandable/Command.pm
Criterion Covered Total %
statement 105 105 100.0
branch 55 62 88.7
condition 23 30 76.6
subroutine 25 25 100.0
pod 8 9 88.8
total 216 231 93.5


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2021-2023 -- leonerd@leonerd.org.uk
5              
6             package Commandable::Command 0.11;
7              
8 10     10   451 use v5.14;
  10         31  
9 10     10   43 use warnings;
  10         53  
  10         13508  
10              
11             =head1 NAME
12              
13             C<Commandable::Command> - represent metadata for an invokable command
14              
15             =cut
16              
17             sub new
18             {
19 26     26 0 47 my $class = shift;
20 26         96 my %args = @_;
21 26   100     77 $args{arguments} //= [];
22 26   100     98 $args{options} //= {};
23 26   100     97 $args{config} //= {};
24 26         181 bless [ @args{qw( name description arguments options package code config )} ], $class;
25             }
26              
27             =head1 ACCESSORS
28              
29             The following simple methods return metadata fields about the command
30              
31             =cut
32              
33             =head2 name
34              
35             =head2 description
36              
37             $name = $command->name;
38             $desc = $command->description;
39              
40             Strings giving the short name (to be used on a commandline), and descriptive
41             text for the command.
42              
43             =head2 arguments
44              
45             @args = $command->arguments;
46              
47             A (possibly-empty) list of argument metadata structures.
48              
49             =head2 options
50              
51             %opts = $command->options;
52              
53             A (possibly-empty) kvlist of option metadata structures.
54              
55             =head2 package
56              
57             $pkg = $command->packaage;
58              
59             The package name as a plain string.
60              
61             =head2 code
62              
63             $sub = $command->code;
64              
65             A CODE reference to the code actually implementing the command.
66              
67             =head2 config
68              
69             $conf = $command->config;
70              
71             A HASH reference to the configuration of the command.
72              
73             =cut
74              
75 19     19 1 91 sub name { shift->[0] }
76 6     6 1 328 sub description { shift->[1] }
77 31     31 1 59 sub arguments { @{ shift->[2] } }
  31         77  
78 32     32 1 45 sub options { %{ shift->[3] } }
  32         189  
79 3     3 1 25 sub package { shift->[4] }
80 12     12 1 51 sub code { shift->[5] }
81 17     17 1 55 sub config { shift->[6] }
82              
83             =head1 METHODS
84              
85             =cut
86              
87             =head2 parse_invocation
88              
89             @vals = $command->parse_invocation( $cinv );
90              
91             Parses values out of a L<Commandable::Invocation> instance according to the
92             specification for the command's arguments. Returns a list of perl values
93             suitable to pass into the function implementing the command.
94              
95             This method will throw an exception if mandatory arguments are missing.
96              
97             =cut
98              
99             sub parse_invocation
100             {
101 30     30 1 150 my $self = shift;
102 30         45 my ( $cinv ) = @_;
103              
104 30         41 my @args;
105              
106 30 100       70 if( my %optspec = $self->options ) {
107 22         45 push @args, my $opts = {};
108 22         26 my @remaining;
109              
110 22         72 while( defined( my $token = $cinv->pull_token ) ) {
111 31 50       62 last if $token eq "--";
112              
113 31         60 my $spec;
114             my $value_in_token;
115 31         0 my $token_again;
116              
117 31         37 my $value = 1;
118 31 100       121 if( $token =~ s/^--([^=]+)(=|$)// ) {
    100          
119 12         35 my ( $opt, $equal ) = ($1, $2);
120 12 100 66     42 if( !$optspec{$opt} and $opt =~ /no-(.+)/ ) {
121 1 50 33     8 $spec = $optspec{$1} and $spec->negatable
122             or die "Unrecognised option name --$opt\n";
123 1         2 $value = undef;
124             }
125             else {
126 11 50       26 $spec = $optspec{$opt} or die "Unrecognised option name --$opt\n";
127 11         17 $value_in_token = length $equal;
128             }
129             }
130             elsif( $token =~ s/^-(.)// ) {
131 11 50       30 $spec = $optspec{$1} or die "Unrecognised option name -$1\n";
132 11 100 100     28 if( $spec->mode_expects_value ) {
    100 66        
133 2         4 $value_in_token = length $token;
134             }
135             elsif( $self->config->{bundling} and length $token and length($1) == 1 ) {
136 2         4 $token_again = "-$token";
137 2         3 undef $token;
138             }
139             }
140             else {
141 8         24 push @remaining, $token;
142 8 100       16 if( $self->config->{require_order} ) {
143 1         3 last;
144             }
145             else {
146 7         17 next;
147             }
148             }
149              
150 23         58 my $name = $spec->name;
151              
152 23 100       40 if( $spec->mode_expects_value ) {
153 10 100 50     31 $value = $value_in_token ? $token
154             : ( $cinv->pull_token // die "Expected value for option --$name\n" );
155             }
156             else {
157 13 100 66     59 die "Unexpected value for parameter $name\n" if $value_in_token or length $token;
158             }
159              
160 22 100       57 if( defined( my $typespec = $spec->typespec ) ) {
161 2 50       6 if( $typespec eq "i" ) {
162 2 100       14 $value =~ m/^-?\d+$/ or
163             die "Value for parameter $name must be an integer\n";
164             }
165             }
166              
167 21         38 $name =~ s/-/_/g;
168              
169 21 100       30 if( $spec->mode eq "multi_value" ) {
    100          
170 5         10 push @{ $opts->{$name} }, $value;
  5         19  
171             }
172             elsif( $spec->mode eq "inc" ) {
173 8         14 $opts->{$name}++;
174             }
175             else {
176 8         14 $opts->{$name} = $value;
177             }
178              
179 21 100       85 $token = $token_again, redo if defined $token_again;
180             }
181              
182 20         58 $cinv->putback_tokens( @remaining );
183              
184 20         50 foreach my $spec ( values %optspec ) {
185 113         148 my $name = $spec->name;
186 113 100 100     158 $opts->{$name} = $spec->default if defined $spec->default and !exists $opts->{$name};
187             }
188             }
189              
190 28         65 foreach my $argspec ( $self->arguments ) {
191 16         43 my $val = $cinv->pull_token;
192 16 100       41 if( defined $val ) {
    100          
193 10 100       32 if( $argspec->slurpy ) {
194 4         22 my @vals = ( $val );
195 4         11 while( defined( $val = $cinv->pull_token ) ) {
196 6         14 push @vals, $val;
197             }
198 4         9 $val = \@vals;
199             }
200 10         24 push @args, $val;
201             }
202             elsif( !$argspec->optional ) {
203 1         3 die "Expected a value for '".$argspec->name."' argument\n";
204             }
205             else {
206             # optional argument was missing; this is the end of the args
207 5         17 last;
208             }
209             }
210              
211 27         136 return @args;
212             }
213              
214             package # hide
215             Commandable::Command::_Argument;
216              
217             =head1 ARGUMENT SPECIFICATIONS
218              
219             Each argument specification is given by an object having the following structure:
220              
221             =head2 name
222              
223             =head2 description
224              
225             $name = $argspec->name;
226              
227             $desc = $argspec->description;
228              
229             Text strings for the user, used to generate the help text.
230              
231             =head2 optional
232              
233             $bool = $argspec->optional;
234              
235             If false, the option is mandatory and an error is raised if no value is
236             provided for it. If true, it is optional and if absent an C<undef> will passed
237             instead.
238              
239             =head2 slurpy
240              
241             $bool = $argspec->slurpy;
242              
243             If true, the argument will be passed as an ARRAY reference containing the
244             entire remaining list of tokens provided by the user.
245              
246             =cut
247              
248             sub new
249             {
250 19     19   33 my $class = shift;
251 19         115 my %args = @_;
252 19         141 bless [ @args{qw( name description optional slurpy )} ], $class;
253             }
254              
255 4     4   26 sub name { shift->[0] }
256 2     2   21 sub description { shift->[1] }
257 7     7   51 sub optional { shift->[2] }
258 11     11   53 sub slurpy { shift->[3] }
259              
260             package # hide
261             Commandable::Command::_Option;
262              
263             =head1 OPTION SPECIFICATIONS
264              
265             Each option specification is given by an object having the following
266             structure:
267              
268             =head2 name
269              
270             $name = $optspec->name;
271              
272             A string giving the name of the option. This is the name it will be given in
273             the options hash provided to the command subroutine.
274              
275             =head2 names
276              
277             @names = $optspec->names;
278              
279             A list containing the name plus all the aliases this option is known by.
280              
281             =head2 description
282              
283             $desc = $optspec->description;
284              
285             A text string containing information for the user, used to generate the help
286             text.
287              
288             =head2 mode
289              
290             $mode = $optspec->mode;
291              
292             A string that describes the behaviour of the option.
293              
294             C<set> options do not expect a value to be suppled by the user, and will store a
295             true value in the options hash if present.
296              
297             C<value> options take a value from the rest of the token, or the next token.
298              
299             --opt=value
300             --opt value
301              
302             C<multi_value> options can be supplied more than once; values are pushed into
303             an ARRAY reference which is passed in the options hash.
304              
305             C<inc> options may be supplied more than once; each occurance will increment
306             the stored value by one.
307              
308             =head2 default
309              
310             $val = $optspec->default;
311              
312             A value to provide in the options hash if the user did not specify a different
313             one.
314              
315             =head2 negatable
316              
317             $bool = $optspec->negatable;
318              
319             If true, also accept a C<--no-OPT> option to reset the value of the option to
320             C<undef>.
321              
322             =head2 typespec
323              
324             $type = $optspec->typespec;
325              
326             If defined, gives a type specification that any user-supplied value must
327             conform to.
328              
329             The C<i> type must be a string giving a (possibly-negative) decimal integer.
330              
331             =cut
332              
333             sub new
334             {
335 21     21   73 my $class = shift;
336 21         60 my %args = @_;
337             warn "Use of $args{name} in a Commandable command option name; should be " . $args{name} =~ s/:$/=/r
338 21 50       60 if $args{name} =~ m/:$/;
339 21 100       61 $args{typespec} = $2 if $args{name} =~ s/([=:])(.+?)$/$1/;
340 21 100       47 if( defined( my $typespec = $args{typespec} ) ) {
341 1 50       4 $typespec eq "i" or
342             die "Unrecognised typespec $typespec";
343             }
344 21 100       75 $args{mode} = "value" if $args{name} =~ s/[=:]$//;
345 21 100       46 $args{mode} = "multi_value" if $args{multi};
346 21         52 my @names = split m/\|/, delete $args{name};
347 21   100     71 $args{mode} //= "set";
348 21 100 50     51 $args{negatable} //= 1 if $args{mode} eq "bool";
349 21         85 bless [ \@names, @args{qw( description mode default negatable typespec )} ], $class;
350             }
351              
352 141     141   213 sub name { shift->[0]->[0] }
353 24     24   29 sub names { @{ shift->[0] } }
  24         91  
354 3     3   11 sub description { shift->[1] }
355 77     77   254 sub mode { shift->[2] }
356 118     118   226 sub default { shift->[3] }
357 7     7   59 sub negatable { shift->[4] }
358 22     22   60 sub typespec { shift->[5] }
359              
360 34     34   65 sub mode_expects_value { shift->mode =~ m/value$/ }
361              
362             =head1 AUTHOR
363              
364             Paul Evans <leonerd@leonerd.org.uk>
365              
366             =cut
367              
368             0x55AA;