File Coverage

blib/lib/Commandable/Finder/SubAttributes.pm
Criterion Covered Total %
statement 27 100 27.0
branch 0 20 0.0
condition 0 4 0.0
subroutine 8 15 53.3
pod 5 5 100.0
total 40 144 27.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-2024 -- leonerd@leonerd.org.uk
5              
6             package Commandable::Finder::SubAttributes 0.14;
7              
8 3     3   548 use v5.26;
  3         35  
9 3     3   17 use warnings;
  3         7  
  3         294  
10 3     3   24 use experimental qw( signatures );
  3         6  
  3         20  
11 3     3   526 use base qw( Commandable::Finder );
  3         5  
  3         2206  
12              
13 3     3   40 use Carp;
  3         6  
  3         296  
14              
15 3     3   1085 use Commandable::Command;
  3         11  
  3         325  
16              
17 3         7 use constant HAVE_ATTRIBUTE_STORAGE => eval {
18 3         5691 require Attribute::Storage;
19 0         0 Attribute::Storage->VERSION( '0.12' );
20 3     3   23 };
  3         7  
21              
22             =head1 NAME
23              
24             C - find commands stored as subs with attributes
25              
26             =head1 SYNOPSIS
27              
28             use Commandable::Finder::SubAttributes;
29              
30             my $finder = Commandable::Finder::SubAttributes->new(
31             package => "MyApp::Commands",
32             );
33              
34             my $help_command = $finder->find_command( "help" );
35              
36             foreach my $command ( $finder->find_commands ) {
37             ...
38             }
39              
40             =head1 DESCRIPTION
41              
42             This implementation of L looks for functions that define
43             commands, where each command is provided by an individual sub in a given
44             package.
45              
46             =head1 ATTRIBUTES
47              
48             use Commandable::Finder::SubAttributes ':attrs';
49              
50             sub command_example
51             :Command_description("An example of a command")
52             {
53             ...
54             }
55              
56             Properties about each command are stored as attributes on the named function,
57             using L.
58              
59             The following attributes are available on the calling package when imported
60             with the C<:attrs> symbol:
61              
62             =head2 Command_description
63              
64             :Command_description("description text")
65              
66             Gives a plain string description text for the command.
67              
68             =head2 Command_arg
69              
70             :Command_arg("argname", "description")
71              
72             Gives a named argument for the command and its description.
73              
74             If the name is suffixed by a C, this argument is optional. (The C itself
75             will be removed from the name).
76              
77             If the name is suffixed by C<...>, this argument is slurpy. (The C<...> itself
78             will be removed from the name).
79              
80             =head2 Command_opt
81              
82             :Command_opt("optname", "description")
83              
84             :Command_opt("optname", "description", "default")
85              
86             Gives a named option for the command and its description.
87              
88             If the name contains C<|> characters it provides multiple name aliases for the
89             same option.
90              
91             If the name field ends in a C<=> character, a value is expected for the
92             option. It can either be parsed from the next input token, or after an C<=>
93             sign of the same token:
94              
95             --optname VALUE
96             --optname=VALUE
97              
98             If the name field ends in a C<@> character, a value is expected for the option
99             and can be specified multiple times. All the values will be collected into an
100             ARRAY reference.
101              
102             If the name field ends in a C<+> character, the option can be specified
103             multiple times and the total count will be used as the value.
104              
105             If the name field ends in a C character, the option is negatable. An option
106             name of C<--no-OPTNAME> is recognised and will reset the value to C. By
107             setting a default of some true value (e.g. C<1>) you can detect if this has
108             happened.
109              
110             An optional third argument may be present to specify a default value, if not
111             provided by the invocation.
112              
113             =head1 GLOBAL OPTION ATTRIBUTES
114              
115             I this module also allows attaching attributes to package
116             variables in the package that stores the subroutines (often C
), which
117             will then be handled automatically as global options by the finder.
118              
119             Remember that these have to be I variables (i.e. declared with
120             C); lexical variables (declared with C) will not work.
121              
122             our $VERBOSE
123             :GlobalOption("verbose|v+", "Increase the verbosity of status output");
124              
125             This often serves as a convenient alternative to modules like L,
126             because it integrates with the C command automatically.
127              
128             =head2 GlobalOption
129              
130             :GlobalOption("optname", "description")
131              
132             Gives the name for this global option and its description. These are handled
133             in the same way as for L given above, except that no default is
134             handled here. Instead, if the variable already has a value that will be taken
135             as its default.
136              
137             =cut
138              
139 2         6 sub import ( $pkg, @syms )
140 2     2   32 {
  2         5  
  2         4  
141 2         7 my $caller = caller;
142              
143 2         128 foreach ( @syms ) {
144 0 0         if( $_ eq ":attrs" ) {
145 0 0         HAVE_ATTRIBUTE_STORAGE or
146             croak "Cannot import :attrs as Attribute::Storage is not available";
147              
148 0           require Commandable::Finder::SubAttributes::Attrs;
149 0           Commandable::Finder::SubAttributes::Attrs->import_into( $caller );
150 0           next;
151             }
152              
153 0           croak "Unrecognised import symbol $_";
154             }
155             }
156              
157             =head1 CONSTRUCTOR
158              
159             =cut
160              
161             =head2 new
162              
163             $finder = Commandable::Finder::SubAttributes->new( %args )
164              
165             Constructs a new instance of C.
166              
167             Takes the following named arguments:
168              
169             =over 4
170              
171             =item package => STR
172              
173             The name of the package to look in for command subs.
174              
175             =item name_prefix => STR
176              
177             Optional. Gives the name prefix to use to filter for subs that actually
178             provide a command, and to strip off to find the name of the command. Default
179             C.
180              
181             =item underscore_to_hyphen => BOOL
182              
183             Optional. If true, sub names that contain underscores will be converted into
184             hyphens. This is often useful in CLI systems, allowing commands to be typed
185             with hyphenated names (e.g. "get-thing") while the Perl sub that implements it
186             is named with an underscores (e.g. "command_get_thing"). Defaults true, but
187             can be disabled by passing a defined-but-false value such as C<0> or C<''>.
188              
189             =back
190              
191             Any additional arguments are passed to the C method to be used as
192             configuration options.
193              
194             =cut
195              
196 0           sub new ( $class, %args )
197 0     0 1   {
  0            
  0            
198 0 0         HAVE_ATTRIBUTE_STORAGE or
199             croak "Cannot create a $class as Attribute::Storage is not available";
200              
201 0 0         my $package = ( delete $args{package} ) or croak "Require 'package'";
202              
203 0   0       my $name_prefix = ( delete $args{name_prefix} ) // "command_";
204 0   0       my $conv_under = ( delete $args{underscore_to_hyphen} ) // 1;
205              
206 0           my $self = bless {
207             package => $package,
208             name_prefix => $name_prefix,
209             conv_under => $conv_under,
210             }, $class;
211              
212 0 0         $self->configure( %args ) if %args;
213              
214             # TODO: This package name should probably be separately configurable
215 0 0         if( my %global_opts = Attribute::Storage::find_vars_with_attr( $package, "GlobalOption" ) ) {
216 0           foreach my $varname ( sort keys %global_opts ) {
217 0           my $varref = $global_opts{$varname};
218              
219 0           my $optspec = Attribute::Storage::get_varattr( $varref, "GlobalOption" );
220 0           my ( $name, $description ) = @$optspec;
221              
222 0           my %optspec = (
223             name => $name,
224             description => $description,
225             into => $varref,
226             );
227              
228 0 0         $optspec{default} = $$varref if defined $$varref;
229              
230 0           $self->add_global_options( \%optspec );
231             }
232             }
233              
234 0           return $self;
235             }
236              
237             =head2 new_for_caller
238              
239             =head2 new_for_main
240              
241             $finder = Commandable::Finder::SubAttributes->new_for_caller( %args )
242             $finder = Commandable::Finder::SubAttributes->new_for_main( %args )
243              
244             Convenient wrapper constructors that pass either the caller's package name or
245             C
as the package name. Combined with the C method
246             these are particularly convenient for wrapper scripts:
247              
248             #!/usr/bin/perl
249              
250             use v5.14;
251             use warnings;
252              
253             use Commandable::Finder::SubAttributes ':attrs';
254              
255             exit Commandable::Finder::SubAttributes->new_for_main
256             ->find_and_invoke_ARGV;
257              
258             # command subs go here...
259              
260             =cut
261              
262 0           sub new_for_caller ( $class, @args )
263 0     0 1   {
  0            
  0            
264 0           return $class->new( package => scalar caller, @args );
265             }
266              
267 0           sub new_for_main ( $class, @args )
268 0     0 1   {
  0            
  0            
269 0           return $class->new( package => "main", @args );
270             }
271              
272 0           sub _wrap_code ( $self, $code )
273 0     0     {
  0            
  0            
274 0           return $code;
275             }
276              
277             sub _commands ( $self )
278 0     0     {
  0            
  0            
279 0           my $prefix = qr/$self->{name_prefix}/;
280              
281             my %subs = Attribute::Storage::find_subs_with_attr(
282 0           $self->{package}, "Command_description",
283             matching => qr/^$prefix/,
284             );
285              
286 0           my %commands;
287              
288 0           foreach my $subname ( keys %subs ) {
289 0           my $code = $subs{$subname};
290              
291 0           my $name = $subname =~ s/^$prefix//r;
292 0 0         $name =~ s/_/-/g if $self->{conv_under};
293              
294 0           my $args;
295 0 0         if( $args = Attribute::Storage::get_subattr( $code, "Command_arg" ) ) {
296 0           $args = [ map { Commandable::Command::_Argument->new( %$_ ) } @$args ];
  0            
297             }
298              
299 0           my $opts;
300 0 0         if( $opts = Attribute::Storage::get_subattr( $code, "Command_opt" ) ) {
301 0           $opts = { map { my $o = Commandable::Command::_Option->new( %$_ );
  0            
302 0           map { ( $_ => $o ) } $o->names
  0            
303             } @$opts };
304             }
305              
306             $commands{ $name } = Commandable::Command->new(
307             name => $name,
308             description => Attribute::Storage::get_subattr( $code, "Command_description" ),
309             arguments => $args,
310             options => $opts,
311             package => $self->{package},
312 0           code => $self->_wrap_code( $code ),
313             );
314             }
315              
316 0           $self->add_builtin_commands( \%commands );
317              
318 0           return \%commands;
319             }
320              
321             sub find_commands ( $self )
322 0     0 1   {
  0            
  0            
323 0           return values $self->_commands->%*;
324             }
325              
326 0           sub find_command ( $self, $cmd )
327 0     0 1   {
  0            
  0            
328 0           return $self->_commands->{$cmd};
329             }
330              
331             =head1 AUTHOR
332              
333             Paul Evans
334              
335             =cut
336              
337             0x55AA;