File Coverage

lib/CLI/Framework/Command.pm
Criterion Covered Total %
statement 107 136 78.6
branch 25 42 59.5
condition 1 9 11.1
subroutine 26 31 83.8
pod 19 19 100.0
total 178 237 75.1


line stmt bran cond sub pod time code
1             package CLI::Framework::Command;
2              
3 5     5   19 use strict;
  5         4  
  5         127  
4 5     5   16 use warnings;
  5         10  
  5         173  
5             #use warnings::register;
6              
7             our $VERSION = 0.04;
8              
9 5     5   18 use Carp;
  5         5  
  5         326  
10 5     5   19 use Getopt::Long::Descriptive;
  5         5  
  5         43  
11 5     5   1451 use Exception::Class::TryCatch;
  5         6  
  5         217  
12 5     5   2279 use Class::Inspector;
  5         14218  
  5         154  
13              
14 5     5   28 use CLI::Framework::Exceptions qw( :all );
  5         5  
  5         2421  
15              
16             #FIXME-TODO-CLASS_GENERATION:
17             #our %CLASSES; # remember which classes have been auto-generated
18             #
19             #sub import {
20             # my ($class, %import_args) = @_;
21             #
22             # # If caller has supplied import args, CLIF's "inline form" is being used;
23             # # we need to generate command classes dynamically...
24             # while( my ($cmd_pkg, $cmd_def) = each %import_args ) {
25             #
26             ##FIXME-TODO-CLASS_GENERATION: Create the new classes named in $cmd_pkg, injecting the subs indicated
27             ## (whether explicitly or implicitly) by the contents of $cmd_def...
28             #
29             ## $cmd_obj = __PACKAGE__->new();
30             # }
31             #}
32              
33             ###############################
34             #
35             # OBJECT CONSTRUCTION
36             #
37             ###############################
38              
39             sub manufacture {
40 7     7 1 9 my ($class, $target_pkg) = @_;
41              
42             # Manufacture base command...
43 7         389 eval "require $target_pkg"; # (may or may not be pre-loaded)
44 7 50       899 my $object = $target_pkg->new()
45             or croak "Failed to instantiate command package '$target_pkg' via new(): $!";
46              
47             # Recognize subcommands that were defined in their own package files...
48 7         34 $object->_manufacture_subcommands_in_dir_tree();
49              
50             # Recognize subcommands that have been loaded via an inline definition...
51 7         39 $object->_register_preloaded_subcommands();
52              
53 7         30 return $object;
54             }
55              
56             sub _manufacture_subcommands_in_dir_tree {
57 10     10   15 my ($parent_command_object) = @_;
58              
59             # Check for a subdirectory by the name of the current command containing .pm
60             # files representing subcommands, then manufacture() any that are found...
61              
62             # Look for subdirectory with name of current command...
63 10         60 my $subcommand_dir = Class::Inspector->resolved_filename( ref $parent_command_object );
64 10         662 substr( $subcommand_dir, -3, 3 ) = ''; # trim trailing '.pm'
65              
66 10 100       87 if( -d $subcommand_dir ) {
67             # Directory with name of current command exists; look inside for .pm
68             # files representing subcommands...
69              
70 2         2 my $dh;
71 2 50       56 opendir( $dh, $subcommand_dir ) or die "cannot opendir '$dh': $!";
72 2         21 while( my $subcommand = readdir $dh ) {
73             # Ignore non-module files...
74 8 100       62 next unless substr( $subcommand, -3 ) =~ s/\.pm//; # trim trailing '.pm'
75              
76 3         6 my $subcommand_pkg = (ref $parent_command_object).'::'.$subcommand;
77 3         140 eval "require $subcommand_pkg";
78 3 50       712 if( $subcommand_pkg->isa(ref $parent_command_object) ) {
79 3 50       13 my $subcommand_obj = $subcommand_pkg->new()
80             or croak 'Failed to instantiate subcommand',
81             "'$subcommand_pkg' via method new(): $!";
82              
83 3         8 $parent_command_object->register_subcommand( $subcommand_obj );
84              
85 3         17 $subcommand_obj->_manufacture_subcommands_in_dir_tree();
86 3         13 $subcommand_obj->_register_preloaded_subcommands();
87             }
88             # else {
89             # warnings::warn "Found a non-subclass Perl package file in search path: '$subcommand_pkg' -- ignoring..."
90             # if warnings::enabled;
91             # }
92             }
93             }
94 10         14 return 1;
95             }
96              
97             sub _register_preloaded_subcommands {
98 13     13   16 my ($parent_cmd_obj) = @_;
99              
100             # Find direct subclasses and register them beneath the given parent...
101              
102             # Class::Inspector::subclasses actually finds all *descendants*
103             # (not just direct subclasses)...
104 13     13   44 my $descendants = sub { Class::Inspector->subclasses(@_) };
  13         43  
105 13         28 my $descendant_names = $descendants->( ref $parent_cmd_obj );
106 13 100       134212 return unless ref $descendant_names eq 'ARRAY';
107              
108 4         8 for my $descendant_cmd ( @$descendant_names ) {
109             # (skip if already registered)
110 8 100       33 next if $parent_cmd_obj->package_is_registered( $descendant_cmd );
111              
112             # Find the direct parent class(es) of the descendant...
113 5         5 my @direct_parents;
114 5     5   25 { no strict 'refs';
  5         6  
  5         4215  
  5         3  
115 5         4 @direct_parents = @{ $descendant_cmd.'::ISA' };
  5         24  
116             }
117 5         6 for my $direct_parent_of_descendant (@direct_parents) {
118             # If the descendant is a *direct* subclass of the given parent...
119 5 100       14 if( $direct_parent_of_descendant eq ref $parent_cmd_obj ) {
120             # ...register child command as subcommand of parent...
121 3         12 my $child_cmd = $descendant_cmd->new();
122 3         8 $parent_cmd_obj->register_subcommand( $child_cmd );
123 3         15 $child_cmd->_register_preloaded_subcommands();
124             }
125             }
126             }
127 4         18 return 1;
128             }
129              
130 10     10 1 54 sub new { bless { _cache => undef }, $_[0] }
131              
132 5     5 1 10 sub set_cache { $_[0]->{_cache} = $_[1] }
133 3     3 1 32 sub cache { $_[0]->{_cache} }
134              
135             ###############################
136             #
137             # COMMAND DISPATCHING
138             #
139             ###############################
140              
141 0     0 1 0 sub get_default_usage { $_[0]->{_default_usage} }
142 5     5 1 134 sub set_default_usage { $_[0]->{_default_usage} = $_[1] }
143              
144             sub usage {
145 0     0 1 0 my ($cmd, $subcommand_name, @subcommand_args) = @_;
146              
147             # Allow subcommand aliases in place of subcommand name...
148 0         0 $cmd->_canonicalize($subcommand_name);
149              
150 0         0 my $usage_text;
151 0 0       0 if(my $subcommand = $cmd->registered_subcommand_object($subcommand_name)) {
152             # Get usage from subcommand object...
153 0         0 $usage_text = $subcommand->usage(@subcommand_args);
154             }
155             else {
156             # Get usage from Command object...
157 0         0 $usage_text = $cmd->usage_text();
158             }
159             # Finally, fall back to default command usage message...
160 0   0     0 $usage_text ||= $cmd->get_default_usage();
161 0         0 return $usage_text;
162             }
163              
164             sub _canonicalize {
165 5     5   9 my ($cmd, $input) = @_;
166              
167             # Translate shorthand aliases for subcommands to full names...
168              
169 5 100       16 return unless $input;
170              
171 1         4 my %aliases = $cmd->subcommand_alias();
172 1 50       2 return unless %aliases;
173              
174 0   0     0 my $command_name = $aliases{$input} || $input;
175 0         0 $_[1] = $command_name;
176             }
177              
178             #
179             # ARGV_Format
180             #
181             # $ app [app-opts] [cmd-opts]
182             #
183             # params contain: $cmd = , $cmd_opts = [cmd-opts], @args =
184             #
185             # could, in turn, indicate nested subcommands:
186             # { [subcmd-opts] {...} } [subcmd-args]
187             #
188              
189             sub dispatch {
190 5     5 1 13 my ($cmd, $cmd_opts, @args) = @_;
191              
192             # --- VALIDATE COMMAND OPTIONS AND ARGS ---
193 5         5 eval { $cmd->validate($cmd_opts, @args) };
  5         47  
194 5 50       15 if( catch my $e ) { # (command failed validation)
195 0         0 throw_cmd_validation_exception( error => $e );
196             }
197             # Check if a subcommand is being requested...
198 5         51 my $first_arg = shift @args; # consume potential subcommand name from input
199 5         31 $cmd->_canonicalize( $first_arg );
200 5         5 my ($subcmd_opts, $subcmd_usage);
201 5 50       24 if( my $subcommand = $cmd->registered_subcommand_object($first_arg) ) {
202             # A subcommand is being requested; parse its options...
203 0         0 @ARGV = @args;
204 0         0 my $format = $cmd->name().' '.$subcommand->name().'%o ...';
205 0         0 eval { ($subcmd_opts, $subcmd_usage) =
  0         0  
206             describe_options( $format, $subcommand->option_spec() )
207             };
208 0 0       0 if( catch my $e ) { # (subcommand failed options parsing)
209 0 0       0 $e->isa( 'CLI::Framework::Exception' ) && do{ $e->rethrow };
  0         0  
210 0         0 throw_cmd_opts_parse_exception( error => $e );
211             }
212 0         0 $subcommand->set_default_usage( $subcmd_usage->text() );
213              
214             # Reset arg list to reflect only arguments ( options may have been
215             # consumed by describe_options() )...
216 0         0 @args = @ARGV;
217              
218             # Pass session data to subcommand...
219 0         0 $subcommand->set_cache( $cmd->cache() );
220              
221             # --- NOTIFY MASTER COMMAND OF SUBCOMMAND DISPATCH ---
222 0         0 $cmd->notify_of_subcommand_dispatch( $subcommand, $cmd_opts, @args );
223              
224             # Dispatch subcommand with its options and the remaining args...
225 0         0 $subcommand->dispatch( $subcmd_opts, @args );
226             }
227             else {
228             # If first arg is not a subcommand then put it back in input...
229 5 100       15 unshift @args, $first_arg if defined $first_arg;
230              
231             # ...and run the command itself...
232 5         5 my $output;
233 5         7 eval { $output = $cmd->run( $cmd_opts, @args ) };
  5         18  
234 5 50       17 if( catch my $e ) { # (error during command execution)
235 0 0       0 $e->isa( 'CLI::Framework::Exception' ) && do{ $e->rethrow };
  0         0  
236 0         0 throw_cmd_run_exception( error => $e );
237             }
238 5         57 return $output;
239             }
240             }
241              
242             ###############################
243             #
244             # COMMAND REGISTRATION
245             #
246             ###############################
247              
248 9     9 1 8 sub registered_subcommand_names { keys %{$_[0]->{_subcommands}} }
  9         23  
249              
250             sub registered_subcommand_object {
251 17     17 1 29 my ($cmd, $subcommand_name) = @_;
252              
253 17 100       38 return unless $subcommand_name;
254              
255 13         31 return $cmd->{_subcommands}->{$subcommand_name};
256             }
257              
258             sub register_subcommand {
259 6     6 1 9 my ($cmd, $subcommand_obj) = @_;
260              
261 6 50 33     40 return unless $subcommand_obj &&
262             $subcommand_obj->isa("CLI::Framework::Command");
263              
264 6         17 my $subcommand_name = $subcommand_obj->name();
265 6         10 $cmd->{_subcommands}->{$subcommand_name} = $subcommand_obj;
266              
267 6         5 return $subcommand_obj;
268             }
269              
270             sub package_is_registered {
271 8     8 1 12 my ($cmd, $pkg) = @_;
272 8         5 my @registered_pkgs = map { ref $_ } values %{ $cmd->{_subcommands} };
  10         15  
  8         19  
273 8         12 return grep { $pkg eq $_ } @registered_pkgs;
  10         54  
274             }
275              
276             ###############################
277             #
278             # COMMAND SUBCLASS HOOKS
279             #
280             ###############################
281              
282             sub name {
283 14     14 1 52 my ($cmd) = @_;
284              
285             # Use base name of package as command name...
286 14         20 my $pkg = ref $cmd;
287 14         50 my @pkg_parts = split /::/, $pkg;
288 14         60 return lc $pkg_parts[-1];
289             }
290              
291 5     5 1 25 sub option_spec { ( ) }
292              
293 1     1 1 2 sub subcommand_alias { ( ) }
294              
295       5 1   sub validate { }
296              
297       0 1   sub notify_of_subcommand_dispatch { }
298              
299       0 1   sub usage_text { }
300              
301 0     0 1   sub run { $_[0]->usage() }
302              
303             #-------
304             1;
305              
306             __END__