File Coverage

blib/lib/Commandable/Finder.pm
Criterion Covered Total %
statement 78 81 96.3
branch 20 32 62.5
condition 6 9 66.6
subroutine 12 13 92.3
pod 3 6 50.0
total 119 141 84.4


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