File Coverage

blib/lib/Commandable/Finder/SubAttributes.pm
Criterion Covered Total %
statement 21 70 30.0
branch 0 16 0.0
condition 0 4 0.0
subroutine 7 14 50.0
pod 5 5 100.0
total 33 109 30.2


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::SubAttributes 0.09;
7              
8 3     3   81087 use v5.14;
  3         26  
9 3     3   22 use warnings;
  3         8  
  3         104  
10 3     3   17 use base qw( Commandable::Finder );
  3         6  
  3         1745  
11              
12 3     3   31 use Carp;
  3         6  
  3         184  
13              
14 3     3   848 use Commandable::Command;
  3         8  
  3         122  
15              
16 3     3   18 use constant HAVE_ATTRIBUTE_STORAGE => eval { require Attribute::Storage };
  3         6  
  3         5  
  3         2944  
17              
18             =head1 NAME
19              
20             C - find commands stored as subs with attributes
21              
22             =head1 SYNOPSIS
23              
24             use Commandable::Finder::SubAttributes;
25              
26             my $finder = Commandable::Finder::SubAttributes->new(
27             package => "MyApp::Commands",
28             );
29              
30             my $help_command = $finder->find_command( "help" );
31              
32             foreach my $command ( $finder->find_commands ) {
33             ...
34             }
35              
36             =head1 DESCRIPTION
37              
38             This implementation of L looks for functions that define
39             commands, where each command is provided by an individual sub in a given
40             package.
41              
42             =head1 ATTRIBUTES
43              
44             use Commandable::Finder::SubAttributes ':attrs';
45              
46             sub command_example
47             :Command_description("An example of a command")
48             {
49             ...
50             }
51              
52             Properties about each command are stored as attributes on the named function,
53             using L.
54              
55             The following attributes are available on the calling package when imported
56             with the C<:attrs> symbol:
57              
58             =head2 Command_description
59              
60             :Command_description("description text")
61              
62             Gives a plain string description text for the command.
63              
64             =head2 Command_arg
65              
66             :Command_arg("argname", "description")
67              
68             Gives a named argument for the command and its description.
69              
70             If the name is suffixed by a C, this argument is optional. (The C itself
71             will be removed from the name).
72              
73             If the name is suffixed by C<...>, this argument is slurpy. (The C<...> itself
74             will be removed from the name).
75              
76             =head2 Command_opt
77              
78             :Command_opt("optname", "description")
79              
80             Gives a named option for the command and its description.
81              
82             If the name contains C<|> characters it provides multiple name aliases for the
83             same option.
84              
85             If the name field ends in a C<:> character, a value is expected for the
86             option. It can either be parsed from the next input token, or after an C<=>
87             sign of the same token:
88              
89             --optname VALUE
90             --optname=VALUE
91              
92             An optional third argument may be present to specify a default value, if not
93             provided by the invocation:
94              
95             :Command_opt("optname", "description", "default")
96              
97             =cut
98              
99             sub import
100             {
101 4     4   624 my $pkg = shift;
102 4         11 my $caller = caller;
103              
104 4         111 foreach ( @_ ) {
105 0 0         if( $_ eq ":attrs" ) {
106 0 0         HAVE_ATTRIBUTE_STORAGE or
107             croak "Cannot import :attrs as Attribute::Storage is not available";
108              
109 0           require Commandable::Finder::SubAttributes::Attrs;
110 0           Commandable::Finder::SubAttributes::Attrs->import_into( $caller );
111 0           next;
112             }
113              
114 0           croak "Unrecognised import symbol $_";
115             }
116             }
117              
118             =head1 CONSTRUCTOR
119              
120             =cut
121              
122             =head2 new
123              
124             $finder = Commandable::Finder::SubAttributes->new( %args )
125              
126             Constructs a new instance of C.
127              
128             Takes the following named arguments:
129              
130             =over 4
131              
132             =item package => STR
133              
134             The name of the package to look in for command subs.
135              
136             =item name_prefix => STR
137              
138             Optional. Gives the name prefix to use to filter for subs that actually
139             provide a command, and to strip off to find the name of the command. Default
140             C.
141              
142             =item underscore_to_hyphen => BOOL
143              
144             Optional. If true, sub names that contain underscores will be converted into
145             hyphens. This is often useful in CLI systems, allowing commands to be typed
146             with hyphenated names (e.g. "get-thing") while the Perl sub that implements it
147             is named with an underscores (e.g. "command_get_thing"). Defaults true, but
148             can be disabled by passing a defined-but-false value such as C<0> or C<''>.
149              
150             =back
151              
152             Any additional arguments are passed to the C method to be used as
153             configuration options.
154              
155             =cut
156              
157             sub new
158             {
159 0     0 1   my $class = shift;
160 0           my %args = @_;
161              
162 0 0         HAVE_ATTRIBUTE_STORAGE or
163             croak "Cannot create a $class as Attribute::Storage is not available";
164              
165 0 0         my $package = ( delete $args{package} ) or croak "Require 'package'";
166              
167 0   0       my $name_prefix = ( delete $args{name_prefix} ) // "command_";
168 0   0       my $conv_under = ( delete $args{underscore_to_hyphen} ) // 1;
169              
170 0           my $self = bless {
171             package => $package,
172             name_prefix => $name_prefix,
173             conv_under => $conv_under,
174             }, $class;
175              
176 0 0         $self->configure( %args ) if %args;
177              
178 0           return $self;
179             }
180              
181             =head2 new_for_caller
182              
183             =head2 new_for_main
184              
185             $finder = Commandable::Finder::SubAttributes->new_for_caller( %args )
186             $finder = Commandable::Finder::SubAttributes->new_for_main( %args )
187              
188             Convenient wrapper constructors that pass either the caller's package name or
189             C
as the package name. Combined with the C method
190             these are particularly convenient for wrapper scripts:
191              
192             #!/usr/bin/perl
193              
194             use v5.14;
195             use warnings;
196              
197             use Commandable::Finder::SubAttributes ':attrs';
198              
199             exit Commandable::Finder::SubAttributes->new_for_main
200             ->find_and_invoke_ARGV;
201              
202             # command subs go here...
203              
204             =cut
205              
206             sub new_for_caller
207             {
208 0     0 1   my $class = shift;
209 0           return $class->new( package => scalar caller, @_ );
210             }
211              
212             sub new_for_main
213             {
214 0     0 1   my $class = shift;
215 0           return $class->new( package => "main", @_ );
216             }
217              
218             sub _wrap_code
219             {
220 0     0     my $self = shift;
221 0           my ( $code ) = @_;
222              
223 0           return $code;
224             }
225              
226             sub _commands
227             {
228 0     0     my $self = shift;
229              
230 0           my $prefix = qr/$self->{name_prefix}/;
231              
232             my %subs = Attribute::Storage::find_subs_with_attr(
233 0           $self->{package}, "Command_description",
234             matching => qr/^$prefix/,
235             );
236              
237 0           my %commands;
238              
239 0           foreach my $subname ( keys %subs ) {
240 0           my $code = $subs{$subname};
241              
242 0           my $name = $subname =~ s/^$prefix//r;
243 0 0         $name =~ s/_/-/g if $self->{conv_under};
244              
245 0           my $args;
246 0 0         if( $args = Attribute::Storage::get_subattr( $code, "Command_arg" ) ) {
247 0           $args = [ map { Commandable::Command::_Argument->new( %$_ ) } @$args ];
  0            
248             }
249              
250 0           my $opts;
251 0 0         if( $opts = Attribute::Storage::get_subattr( $code, "Command_opt" ) ) {
252 0           $opts = { map { my $o = Commandable::Command::_Option->new( %$_ );
  0            
253 0           map { ( $_ => $o ) } $o->names
  0            
254             } @$opts };
255             }
256              
257             $commands{ $name } = Commandable::Command->new(
258             name => $name,
259             description => Attribute::Storage::get_subattr( $code, "Command_description" ),
260             arguments => $args,
261             options => $opts,
262             package => $self->{package},
263 0           code => $self->_wrap_code( $code ),
264             );
265             }
266              
267 0           $self->add_builtin_commands( \%commands );
268              
269 0           return \%commands;
270             }
271              
272             sub find_commands
273             {
274 0     0 1   my $self = shift;
275              
276 0           return values %{ $self->_commands };
  0            
277             }
278              
279             sub find_command
280             {
281 0     0 1   my $self = shift;
282 0           my ( $cmd ) = @_;
283              
284 0           return $self->_commands->{$cmd};
285             }
286              
287             =head1 AUTHOR
288              
289             Paul Evans
290              
291             =cut
292              
293             0x55AA;