File Coverage

blib/lib/Commandable/Finder.pm
Criterion Covered Total %
statement 79 82 96.3
branch 22 34 64.7
condition 6 9 66.6
subroutine 12 13 92.3
pod 3 6 50.0
total 122 144 84.7


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.10;
7              
8 9     9   116 use v5.14;
  9         30  
9 9     9   58 use warnings;
  9         19  
  9         272  
10              
11 9     9   49 use Carp;
  9         16  
  9         549  
12 9     9   55 use List::Util 'max';
  9         18  
  9         11314  
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 4 my $self = shift;
43 1         5 my %conf = @_;
44              
45             exists $conf{$_} and $self->{config}{$_} = delete $conf{$_}
46 1   33     10 for qw( allow_multiple_commands );
47              
48 1 50       4 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 16 my $self = shift;
86 7         14 my ( $cinv ) = @_;
87              
88 7         20 my $multiple = $self->{config}{allow_multiple_commands};
89              
90 7         11 my $result;
91             {
92 7 50       12 defined( my $cmdname = $cinv->pull_token ) or
  8         24  
93             die "Expected a command name\n";
94              
95 8 50       26 my $cmd = $self->find_command( $cmdname ) or
96             die "Unrecognised command '$cmdname'";
97              
98 8         26 my @args = $cmd->parse_invocation( $cinv );
99              
100 8 50 66     26 !$multiple and length $cinv->peek_remaining and
101             die "Unrecognised extra input: " . $cinv->peek_remaining . "\n";
102              
103 8         26 $result = $cmd->code->( @args );
104              
105             # TODO configurable separator - ';' or '|' or whatever
106             # currently blank
107              
108 8 100 100     88 redo if $multiple and length $cinv->peek_remaining;
109             }
110              
111 7         16 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 26 my $self = shift;
144 6         18 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   20 @_ ? return $self->builtin_command_helpcmd( @_ )
159             : return $self->builtin_command_helpsummary;
160             },
161 6         55 );
162             }
163              
164             # TODO: some pretty output formatting maybe using S:T:Terminal?
165             sub _print_table2
166             {
167 2     2   6 my ( $sep, @rows ) = @_;
168              
169 2         5 my $max_len = max map { length $_->[0] } @rows;
  4         13  
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   30 my $sep = shift;
180 2         4 my $ret = shift;
181 2         8 $ret .= "$sep$_" for @_;
182 2         8 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         5 my @commands = sort { $a->name cmp $b->name } $self->find_commands;
  3         9  
204              
205             _print_table2 ": ", map {
206 1         4 [ Commandable::Output->format_note( $_->name ), $_->description ]
  3         13  
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       5 my $cmd = $self->find_command( $cmdname ) or
216             die "Unrecognised command '$cmdname' - see 'help' for a list of commands\n";
217              
218 1         5 my @argspecs = $cmd->arguments;
219 1         3 my %optspecs = $cmd->options;
220              
221 1         5 Commandable::Output->printf( "%s - %s\n",
222             Commandable::Output->format_note( $cmd->name ),
223             $cmd->description
224             );
225 1         11 Commandable::Output->printf( "\n" );
226              
227 1         9 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       31 my $argspec = $_;
  1 50       2  
235 1         5 my $str = "\$" . uc $argspec->name;
236 1 50       4 $str .= "..." if $argspec->slurpy;
237 1 50       13 $str = "($str)" if $argspec->optional;
238 1         10 $str;
239             } @argspecs
240             ) : ()
241             );
242              
243 1 50       10 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         16 my %primary_names = map { $_->name => 1 } values %optspecs;
  4         10  
249 1         6 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       14 Commandable::Output->printf( "\n" ) unless $first; undef $first;
  2         7  
254              
255 2         6 my $default = $optspec->default;
256 2 100       6 my $value = $optspec->mode eq "value" ? " " : "";
257              
258             Commandable::Output->printf( " %s\n",
259             _join( ", ", map {
260 2 100       8 Commandable::Output->format_note( length $_ > 1 ? "--$_$value" : "-$_$value", 1 )
  4         33  
261             } $optspec->names )
262             );
263 2 50       17 Commandable::Output->printf( " %s%s\n",
264             $optspec->description,
265             ( defined $default ? " (default: $default)" : "" ),
266             );
267             }
268             }
269              
270 1 50       13 if( @argspecs ) {
271 1         3 Commandable::Output->printf( "\n" );
272 1         8 Commandable::Output->print_heading( "ARGUMENTS:" );
273              
274             _print_table2 " ", map {
275 1         12 [ " " . Commandable::Output->format_note( '$' . uc $_->name, 1 ),
  1         3  
276             $_->description ]
277             } @argspecs;
278             }
279             }
280              
281             =head1 AUTHOR
282              
283             Paul Evans
284              
285             =cut
286              
287             0x55AA;