File Coverage

blib/lib/Commandable/Finder.pm
Criterion Covered Total %
statement 83 84 98.8
branch 24 36 66.6
condition 7 9 77.7
subroutine 13 14 92.8
pod 4 7 57.1
total 131 150 87.3


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::Finder 0.11;
7              
8 10     10   108 use v5.14;
  10         29  
9 10     10   40 use warnings;
  10         18  
  10         194  
10              
11 10     10   39 use Carp;
  10         16  
  10         507  
12 10     10   52 use List::Util 'max';
  10         15  
  10         11015  
13              
14             require Commandable::Output;
15              
16             =head1 NAME
17              
18             C<Commandable::Finder> - an interface for discovery of L<Commandable::Command>s
19              
20             =head1 SYNOPSIS
21              
22             use Commandable::Finder::...;
23              
24             my $finder = Commandable::Finder::...->new(
25             ...
26             );
27              
28             $finder->find_and_invoke( Commandable::Invocation->new( $text ) );
29              
30             =head1 DESCRIPTION
31              
32             This base class is common to the various finder subclasses:
33              
34             =over 4
35              
36             =item *
37              
38             L<Commandable::Finder::SubAttributes>
39              
40             =item *
41              
42             L<Commandable::Finder::MethodAttributes>
43              
44             =item *
45              
46             L<Commandable::Finder::Packages>
47              
48             =back
49              
50             =head1 METHODS
51              
52             =cut
53              
54             =head2 configure
55              
56             $finder = $finder->configure( %conf )
57              
58             Sets configuration options on the finder instance. Returns the finder instance
59             itself, to permit easy chaining.
60              
61             The following configuration options are recognised:
62              
63             =head3 allow_multiple_commands
64              
65             If enabled, the L</find_and_invoke> method will permit multiple command
66             invocations within a single call.
67              
68             =head3 require_order
69              
70             If enabled, stop processing options when the first non-option argument
71             is seen.
72              
73             =head3 bundling
74              
75             If enabled, short (single-letter) options of simple boolean type can be
76             combined into a single C<-abc...> argument. Incrementable options can be
77             specified multiple times (as common with things like C<-vvv> for
78             C<--verbose 3>).
79              
80             =cut
81              
82             sub configure
83             {
84 4     4 1 9 my $self = shift;
85 4         9 my %conf = @_;
86              
87             exists $conf{$_} and $self->{config}{$_} = delete $conf{$_}
88 4   66     36 for qw( allow_multiple_commands require_order bundling );
89              
90 4 50       11 keys %conf and croak "Unrecognised ->configure params: " . join( ", ", sort keys %conf );
91              
92 4         10 return $self;
93             }
94              
95             =head2 find_commands
96              
97             @commands = $finder->find_commands
98              
99             Returns a list of command instances, in no particular order. Each will be an
100             instance of L<Commandable::Command>.
101              
102             =head2 find_command
103              
104             $command = $finder->find_command( $cmdname )
105              
106             Returns a command instance of the given name as an instance of
107             L<Commandable::Command>, or C<undef> if there is none.
108              
109             =cut
110              
111             =head2 find_and_invoke
112              
113             $result = $finder->find_and_invoke( $cinv )
114              
115             A convenient wrapper around the common steps of finding a command named after
116             the initial token in a L<Commandable::Invocation>, parsing arguments from it,
117             and invoking the underlying implementation function.
118              
119             If the C<allow_multiple_commands> configuration option is set, it will
120             repeatedly attempt to parse a command name followed by arguments and options
121             while the invocation string is non-empty.
122              
123             =cut
124              
125             sub find_and_invoke
126             {
127 10     10 1 20 my $self = shift;
128 10         17 my ( $cinv ) = @_;
129              
130 10         26 my $multiple = $self->{config}{allow_multiple_commands};
131              
132 10         41 my $result;
133             {
134 10 50       17 defined( my $cmdname = $cinv->pull_token ) or
  11         29  
135             die "Expected a command name\n";
136              
137 11 50       37 my $cmd = $self->find_command( $cmdname ) or
138             die "Unrecognised command '$cmdname'";
139              
140 11         38 my @args = $cmd->parse_invocation( $cinv );
141              
142 11 50 66     34 !$multiple and length $cinv->peek_remaining and
143             die "Unrecognised extra input: " . $cinv->peek_remaining . "\n";
144              
145 11         32 $result = $cmd->code->( @args );
146              
147             # TODO configurable separator - ';' or '|' or whatever
148             # currently blank
149              
150 11 100 100     152 redo if $multiple and length $cinv->peek_remaining;
151             }
152              
153 10         25 return $result;
154             }
155              
156             =head2 find_and_invoke_list
157              
158             $result = $finder->find_and_invoke_list( @tokens )
159              
160             A further convenience around creating a L<Commandable::Invocation> from the
161             given list of values and using that to invoke a command.
162              
163             =cut
164              
165             sub find_and_invoke_list
166             {
167 8     8 1 8335 my $self = shift;
168              
169 8         844 require Commandable::Invocation;
170 8         40 return $self->find_and_invoke( Commandable::Invocation->new_from_tokens( @_ ) );
171             }
172              
173             =head2 find_and_invoke_ARGV
174              
175             $result = $finder->find_and_invoke_ARGV()
176              
177             A further convenience around creating a L<Commandable::Invocation> from the
178             C<@ARGV> array and using that to invoke a command. Often this allows an entire
179             wrapper script to be created in a single line of code:
180              
181             exit Commandable::Finder::SOMESUBCLASS->new( ... )
182             ->find_and_invoke_ARGV();
183              
184             =cut
185              
186             sub find_and_invoke_ARGV
187             {
188 0     0 1 0 shift->find_and_invoke_list( @ARGV );
189             }
190              
191             =head1 BUILTIN COMMANDS
192              
193             The following built-in commands are automatically provided.
194              
195             =cut
196              
197             sub add_builtin_commands
198             {
199 9     9 0 20 my $self = shift;
200 9         31 my ( $commands ) = @_;
201              
202             $commands->{help} =
203             Commandable::Command->new(
204             name => "help",
205             description => "Display a list of available commands",
206             arguments => [
207             Commandable::Command::_Argument->new(
208             name => "cmd",
209             description => "command name",
210             optional => 1,
211             )
212             ],
213             code => sub {
214 2 100   2   23 @_ ? return $self->builtin_command_helpcmd( @_ )
215             : return $self->builtin_command_helpsummary;
216             },
217 9         50 );
218             }
219              
220             # TODO: some pretty output formatting maybe using S:T:Terminal?
221             sub _print_table2
222             {
223 2     2   6 my ( $sep, @rows ) = @_;
224              
225 2         5 my $max_len = max map { length $_->[0] } @rows;
  4         19  
226              
227             Commandable::Output->printf( "%-*s%s%s\n",
228             $max_len, $_->[0], $sep, $_->[1]
229 2         16 ) for @rows;
230             }
231              
232             # A join() that respects stringify overloading
233             sub _join
234             {
235 3     3   25 my $sep = shift;
236 3         4 my $ret = shift;
237 3         9 $ret .= "$sep$_" for @_;
238 3         7 return $ret;
239             }
240              
241             =head2 help
242              
243             help
244              
245             help $commandname
246              
247             With no arguments, prints a summary table of known command names and their
248             descriptive text.
249              
250             With a command name argument, prints more descriptive text about that command,
251             additionally detailing the arguments.
252              
253             =cut
254              
255             sub builtin_command_helpsummary
256             {
257 1     1 0 3 my $self = shift;
258              
259 1         4 my @commands = sort { $a->name cmp $b->name } $self->find_commands;
  3         7  
260              
261             _print_table2 ": ", map {
262 1         3 [ Commandable::Output->format_note( $_->name ), $_->description ]
  3         11  
263             } @commands;
264             }
265              
266             sub builtin_command_helpcmd
267             {
268 1     1 0 3 my $self = shift;
269 1         2 my ( $cmdname ) = @_;
270              
271 1 50       4 my $cmd = $self->find_command( $cmdname ) or
272             die "Unrecognised command '$cmdname' - see 'help' for a list of commands\n";
273              
274 1         5 my @argspecs = $cmd->arguments;
275 1         4 my %optspecs = $cmd->options;
276              
277 1         5 Commandable::Output->printf( "%s - %s\n",
278             Commandable::Output->format_note( $cmd->name ),
279             $cmd->description
280             );
281 1         18 Commandable::Output->printf( "\n" );
282              
283 1         12 Commandable::Output->print_heading( "SYNOPSIS:" );
284             Commandable::Output->printf( " %s\n",
285             join " ",
286             $cmd->name,
287             %optspecs ? "[OPTIONS...]" : (),
288             @argspecs ? (
289             map {
290 1 50       16 my $argspec = $_;
  1 50       3  
291 1         5 my $str = "\$" . uc $argspec->name;
292 1 50       3 $str .= "..." if $argspec->slurpy;
293 1 50       18 $str = "($str)" if $argspec->optional;
294 1         7 $str;
295             } @argspecs
296             ) : ()
297             );
298              
299 1 50       9 if( %optspecs ) {
300 1         11 Commandable::Output->printf( "\n" );
301 1         8 Commandable::Output->print_heading( "OPTIONS:" );
302              
303             # %optspecs contains duplicates; filter them
304 1         12 my %primary_names = map { $_->name => 1 } values %optspecs;
  5         11  
305 1         5 my @primary_optspecs = @optspecs{ sort keys %primary_names };
306              
307 1         2 my $first = 1;
308 1         9 foreach my $optspec ( @primary_optspecs ) {
309 3 100       18 Commandable::Output->printf( "\n" ) unless $first; undef $first;
  3         10  
310              
311 3         9 my $default = $optspec->default;
312 3 100       8 my $value = $optspec->mode eq "value" ? " <value>" : "";
313 3 100       7 my $no = $optspec->negatable ? "[no-]" : "";
314              
315             Commandable::Output->printf( " %s\n",
316             _join( ", ", map {
317 3 100       8 Commandable::Output->format_note( length $_ > 1 ? "--$no$_$value" : "-$_$value", 1 )
  5         27  
318             } $optspec->names )
319             );
320 3 50       29 Commandable::Output->printf( " %s%s\n",
321             $optspec->description,
322             ( defined $default ? " (default: $default)" : "" ),
323             );
324             }
325             }
326              
327 1 50       10 if( @argspecs ) {
328 1         3 Commandable::Output->printf( "\n" );
329 1         7 Commandable::Output->print_heading( "ARGUMENTS:" );
330              
331             _print_table2 " ", map {
332 1         10 [ " " . Commandable::Output->format_note( '$' . uc $_->name, 1 ),
  1         4  
333             $_->description ]
334             } @argspecs;
335             }
336             }
337              
338             =head1 AUTHOR
339              
340             Paul Evans <leonerd@leonerd.org.uk>
341              
342             =cut
343              
344             0x55AA;