File Coverage

blib/lib/Commandable/Command.pm
Criterion Covered Total %
statement 86 86 100.0
branch 35 40 87.5
condition 14 19 73.6
subroutine 22 22 100.0
pod 4 8 50.0
total 161 175 92.0


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.10;
7              
8 9     9   509 use v5.14;
  9         33  
9 9     9   46 use warnings;
  9         18  
  9         11227  
10              
11             =head1 NAME
12              
13             C - represent metadata for an invokable command
14              
15             =cut
16              
17             sub new
18             {
19 20     20 0 36 my $class = shift;
20 20         105 my %args = @_;
21 20   100     76 $args{arguments} //= [];
22 20   100     128 $args{options} //= {};
23 20         141 bless [ @args{qw( name description arguments options package code )} ], $class;
24             }
25              
26             =head1 ACCESSORS
27              
28             The following simple methods return metadata fields about the command
29              
30             =cut
31              
32             =head2 name
33              
34             =head2 description
35              
36             $name = $command->name
37             $desc = $command->description
38              
39             Strings giving the short name (to be used on a commandline), and descriptive
40             text for the command.
41              
42             =head2 arguments
43              
44             @args = $command->arguments
45              
46             A (possibly-empty) list of argument metadata structures.
47              
48             =cut
49              
50 19     19 1 101 sub name { shift->[0] }
51 6     6 1 41 sub description { shift->[1] }
52 26     26 1 52 sub arguments { @{ shift->[2] } }
  26         66  
53 25     25 0 43 sub options { %{ shift->[3] } }
  25         144  
54 3     3 0 26 sub package { shift->[4] }
55 9     9 0 29 sub code { shift->[5] }
56              
57             =head1 METHODS
58              
59             =cut
60              
61             =head2 parse_invocation
62              
63             @vals = $command->parse_invocation( $cinv )
64              
65             Parses values out of a L instance according to the
66             specification for the command's arguments. Returns a list of perl values
67             suitable to pass into the function implementing the command.
68              
69             This method will throw an exception if mandatory arguments are missing.
70              
71             =cut
72              
73             sub parse_invocation
74             {
75 23     23 1 78 my $self = shift;
76 23         50 my ( $cinv ) = @_;
77              
78 23         36 my @args;
79              
80 23 100       55 if( my %optspec = $self->options ) {
81 15         34 push @args, my $opts = {};
82 15         23 my @remaining;
83              
84 15         39 while( defined( my $token = $cinv->pull_token ) ) {
85 17 50       41 last if $token eq "--";
86              
87 17         27 my $spec;
88             my $value_in_token;
89              
90 17         24 my $value = 1;
91 17 100       79 if( $token =~ s/^--([^=]+)(=|$)// ) {
    100          
92 9         30 my ( $opt, $equal ) = ($1, $2);
93 9 100 66     53 if( !$optspec{$opt} and $opt =~ /no-(.+)/ ) {
94 1 50 33     9 $spec = $optspec{$1} and $spec->negatable
95             or die "Unrecognised option name --$opt\n";
96 1         3 $value = undef;
97             }
98             else {
99 8 50       21 $spec = $optspec{$opt} or die "Unrecognised option name --$opt\n";
100 8         15 $value_in_token = length $equal;
101             }
102             }
103             elsif( $token =~ s/^-(.)// ) {
104 4 50       13 $spec = $optspec{$1} or die "Unrecognised option name -$1\n";
105 4         5 $value_in_token = length $token;
106             }
107             else {
108 4         8 push @remaining, $token;
109 4         12 next;
110             }
111              
112 13         33 my $name = $spec->name;
113              
114 13 100       30 if( $spec->mode =~ /value$/ ) {
115 5 100 50     17 $value = $value_in_token ? $token
116             : ( $cinv->pull_token // die "Expected value for option --".$spec->name."\n" );
117             }
118              
119 13 100       24 if( $spec->mode eq "multi_value" ) {
    100          
120 2         6 push @{ $opts->{$name} }, $value;
  2         8  
121             }
122             elsif( $spec->mode eq "inc" ) {
123 5         14 $opts->{$name}++;
124             }
125             else {
126 6         25 $opts->{$name} = $value;
127             }
128             }
129              
130 15         47 $cinv->putback_tokens( @remaining );
131              
132 15         44 foreach my $spec ( values %optspec ) {
133 58         90 my $name = $spec->name;
134 58 100 100     87 $opts->{$name} = $spec->default if defined $spec->default and !exists $opts->{$name};
135             }
136             }
137              
138 23         62 foreach my $argspec ( $self->arguments ) {
139 13         37 my $val = $cinv->pull_token;
140 13 100       46 if( defined $val ) {
    100          
141 7 100       19 if( $argspec->slurpy ) {
142 1         3 my @vals = ( $val );
143 1         4 while( defined( $val = $cinv->pull_token ) ) {
144 2         6 push @vals, $val;
145             }
146 1         3 $val = \@vals;
147             }
148 7         24 push @args, $val;
149             }
150             elsif( !$argspec->optional ) {
151 1         7 die "Expected a value for '".$argspec->name."' argument\n";
152             }
153             else {
154             # optional argument was missing; this is the end of the args
155 5         13 last;
156             }
157             }
158              
159 22         132 return @args;
160             }
161              
162             package # hide
163             Commandable::Command::_Argument;
164              
165             sub new
166             {
167 13     13   31 my $class = shift;
168 13         53 my %args = @_;
169 13         107 bless [ @args{qw( name description optional slurpy )} ], $class;
170             }
171              
172 4     4   37 sub name { shift->[0] }
173 2     2   23 sub description { shift->[1] }
174 7     7   66 sub optional { shift->[2] }
175 8     8   29 sub slurpy { shift->[3] }
176              
177             package # hide
178             Commandable::Command::_Option;
179              
180             sub new
181             {
182 12     12   23 my $class = shift;
183 12         48 my %args = @_;
184             warn "Use of $args{name} in a Commandable command option name; should be " . $args{name} =~ s/:$/=/r
185 12 50       46 if $args{name} =~ m/:$/;
186 12 100       56 $args{mode} = "value" if $args{name} =~ s/[=:]$//;
187 12 100       37 $args{mode} = "multi_value" if $args{multi};
188 12         43 my @names = split m/\|/, delete $args{name};
189 12   100     45 $args{mode} //= "set";
190 12 100 50     58 $args{negatable} //= 1 if $args{mode} eq "bool";
191 12         62 bless [ \@names, @args{qw( description mode default negatable )} ], $class;
192             }
193              
194 75     75   172 sub name { shift->[0]->[0] }
195 14     14   26 sub names { @{ shift->[0] } }
  14         62  
196 2     2   8 sub description { shift->[1] }
197 42     42   151 sub mode { shift->[2] }
198 62     62   156 sub default { shift->[3] }
199 4     4   27 sub negatable { shift->[4] }
200              
201             =head1 AUTHOR
202              
203             Paul Evans
204              
205             =cut
206              
207             0x55AA;