File Coverage

lib/Command/Tree.pm
Criterion Covered Total %
statement 151 294 51.3
branch 30 106 28.3
condition 6 31 19.3
subroutine 20 27 74.0
pod 0 16 0.0
total 207 474 43.6


line stmt bran cond sub pod time code
1             package Command::Tree;
2              
3 4     4   971 use strict;
  4         6  
  4         97  
4 4     4   12 use warnings;
  4         4  
  4         81  
5 4     4   13 use UR;
  4         4  
  4         23  
6 4     4   15 use File::Basename qw/basename/;
  4         4  
  4         2404  
7              
8             our $VERSION = "0.46"; # UR $VERSION;
9              
10             class Command::Tree {
11             is => 'Command::V2',
12             is_abstract => 1,
13             doc => 'base class for commands which delegate to sub-commands',
14             };
15              
16             sub resolve_class_and_params_for_argv {
17             # This is used by execute_with_shell_params_and_exit, but might be used within an application.
18 1     1 0 1 my $self = shift;
19 1         2 my @argv = @_;
20              
21 1 50 33     9 if ( $argv[0] and $argv[0] !~ /^\-/
    50 33        
      33        
22             and my $class_for_sub_command = $self->class_for_sub_command($argv[0]) ) {
23             # delegate
24 0         0 shift @argv;
25 0         0 return $class_for_sub_command->resolve_class_and_params_for_argv(@argv);
26             }
27             elsif ( @argv == 1 and $argv[0] =~ /^(\-)?\-h(elp)?$/ ) { # HELP ME!
28 0         0 return ($self, { help => 1 });
29             }
30             else {
31             # error
32 1         3 return ($self,undef);
33             }
34             }
35              
36             sub resolve_option_completion_spec {
37 1     1 0 3 my $class = shift;
38 1         2 my @completion_spec;
39              
40 1         1 my @sub = eval { $class->sub_command_names };
  1         7  
41 1 50       5 if ($@) {
42 0         0 $class->warning_message("Couldn't load class $class: $@\nSkipping $class...");
43 0         0 return;
44             }
45 1         3 for my $sub (@sub) {
46 3         12 my $sub_class = $class->class_for_sub_command($sub);
47 3 50       32 my $sub_tree = $sub_class->resolve_option_completion_spec() if defined($sub_class);
48              
49             # Hack to fix several broken commands, this should be removed once commands are fixed.
50             # If the commands were not broken then $sub_tree will always exist.
51             # Basically if $sub_tree is undef then we need to remove '>' to not break the OPTS_SPEC
52 3 50       7 if ($sub_tree) {
53 3         8 push @completion_spec, '>' . $sub => $sub_tree;
54             }
55             else {
56 0 0       0 if (defined $sub_class) {
57 0         0 print "WARNING: $sub has sub_class $sub_class of ($class) but could not resolve option completion spec for it.\n".
58             "Setting $sub to non-delegating command, investigate to correct tab completion.\n";
59             } else {
60 0         0 print "WARNING: $sub has no sub_class so could not resolve option completion spec for it.\n".
61             "Setting $sub to non-delegating command, investigate to correct tab completion.\n";
62             }
63 0         0 push @completion_spec, $sub => undef;
64             }
65             }
66 1         3 push @completion_spec, "help!" => undef;
67              
68             return \@completion_spec
69 1         3 }
70              
71             sub help_brief {
72 0     0 0 0 my $self = shift;
73 0 0       0 if (my $doc = $self->__meta__->doc) {
74 0         0 return $doc;
75             }
76             else {
77 0         0 my @parents = $self->__meta__->ancestry_class_metas;
78 0         0 for my $parent (@parents) {
79 0 0       0 if (my $doc = $parent->doc) {
80 0         0 return $doc;
81             }
82             }
83 0         0 return "";
84             }
85             }
86              
87             sub doc_help {
88 1     1 0 8 my $self = shift;
89              
90 1         3 my $command_name = $self->command_name;
91 1         2 my $text;
92              
93             # show the list of sub-commands
94 1         7 $text = sprintf(
95             "Sub-commands for %s:\n%s",
96             Term::ANSIColor::colored($command_name, 'bold'),
97             $self->help_sub_commands,
98             );
99              
100 1         10 return $text;
101             }
102              
103              
104             sub doc_manual {
105 0     0 0 0 my $self = shift;
106 0         0 my $pod = $self->_doc_name_version;
107              
108 0         0 my $manual = $self->_doc_manual_body;
109 0         0 my $help = $self->help_detail;
110 0 0 0     0 if ($manual or $help) {
111 0         0 $pod .= "=head1 DESCRIPTION:\n\n";
112              
113 0   0     0 my $txt = $manual || $help;
114 0 0       0 if ($txt =~ /^\=/) {
115             # pure POD
116 0         0 $pod .= $manual;
117             }
118             else {
119 0         0 $txt =~ s/\n/\n\n/g;
120 0         0 $pod .= $txt;
121             #$pod .= join('', map { " $_\n" } split ("\n",$txt)) . "\n";
122             }
123             }
124              
125              
126 0         0 my $sub_commands = $self->help_sub_commands(brief => 1);
127 0         0 $pod .= "=head1 SUB-COMMANDS\n\n" . $sub_commands . "\n\n";
128              
129 0         0 $pod .= $self->_doc_footer();
130 0         0 $pod .= "\n\n=cut\n\n";
131 0         0 return "\n$pod";
132             }
133              
134             sub sorted_sub_command_classes {
135 4     4   20 no warnings;
  4         5  
  4         1670  
136 1     1 0 5 my @c = map { [ $_->sub_command_sort_position, $_ ] } shift->sub_command_classes;
  2         15  
137 2         4 return map { $_->[1] }
138             sort {
139 1 0       4 ($a->[0] <=> $b->[0])
  1         9  
140             ||
141             ($a->[0] cmp $b->[0])
142             }
143             @c;
144             }
145              
146             sub sorted_sub_command_names {
147 0     0 0 0 my $class = shift;
148 0         0 my @sub_command_classes = $class->sorted_sub_command_classes;
149 0         0 my @sub_command_names = map { $_->command_name_brief } @sub_command_classes;
  0         0  
150 0         0 return @sub_command_names;
151             }
152              
153             sub sub_commands_table {
154 0     0 0 0 my $class = shift;
155 0         0 my @sub_command_names = $class->sorted_sub_command_names;
156              
157 0         0 my $max_length = 0;
158 0         0 for (@sub_command_names) {
159 0 0       0 $max_length = length($_) if ($max_length < length($_));
160             }
161 0   0     0 $max_length ||= 79;
162 0         0 my $col_spacer = '_'x$max_length;
163              
164 0         0 my $n_cols = floor(80/$max_length);
165 0         0 my $n_rows = ceil(@sub_command_names/$n_cols);
166 0         0 my @tb_rows;
167 0         0 for (my $i = 0; $i < @sub_command_names; $i += $n_cols) {
168 0         0 my $end = $i + $n_cols - 1;
169 0 0       0 $end = $#sub_command_names if ($end > $#sub_command_names);
170 0         0 push @tb_rows, [@sub_command_names[$i..$end]];
171             }
172 0         0 my @col_alignment;
173 0         0 for (my $i = 0; $i < $n_cols; $i++) {
174 0         0 push @col_alignment, { sample => "&$col_spacer" };
175             }
176 0         0 my $tb = Text::Table->new(@col_alignment);
177 0         0 $tb->load(@tb_rows);
178 0         0 return $tb;
179             }
180              
181             sub _categorize_sub_commands {
182 1     1   1 my $class = shift;
183              
184 1         14 my @sub_command_classes = $class->sorted_sub_command_classes;
185 1         2 my %categories;
186             my @order;
187 1         3 for my $sub_command_class (@sub_command_classes) {
188 2 50       8 next if $sub_command_class->_is_hidden_in_docs();
189 2   50     8 my $category = $sub_command_class->sub_command_category || '';
190 2 100       5 unless (exists $categories{$category}) {
191 1 50       4 if ($category) {
192 0         0 push(@order, $category)
193             } else {
194 1         2 unshift(@order, '');
195             }
196 1         2 $categories{$category} = [];
197             }
198 2         2 push(@{$categories{$category}}, $sub_command_class);
  2         4  
199             }
200              
201 1         4 return (\@order, \%categories);
202             }
203              
204             sub help_sub_commands {
205 1     1 0 39 my ($self, %params) = @_;
206 1         6 my ($order, $categories) = $self->_categorize_sub_commands(@_);
207 1         2 my $command_name_method = 'command_name_brief';
208              
209 4     4   17 no warnings;
  4         5  
  4         2611  
210 1         2 local $Text::Wrap::columns = 60;
211              
212 1         1 my @full_data;
213 1         2 for my $category (@$order) {
214 1         2 my $sub_commands_within_this_category = $categories->{$category};
215             my @data = map {
216 1         2 my @rows = split("\n",Text::Wrap::wrap('', ' ', $_->help_brief));
  2         11  
217 2         301 chomp @rows;
218             (
219             [
220             $_->$command_name_method,
221             ($_->isa('Command::Tree') ? '...' : ''), #$_->_shell_args_usage_string_abbreviated,
222             $rows[0],
223             ],
224             map {
225 2 50       11 [
226 0         0 '',
227             ' ',
228             $rows[$_],
229             ]
230             } (1..$#rows)
231             );
232             }
233             @$sub_commands_within_this_category;
234              
235 1 50       3 if ($category) {
236             # add a space between categories
237 0 0       0 push @full_data, ['','',''] if @full_data;
238              
239 0 0       0 if ($category =~ /\D/) {
240             # non-numeric categories show their category as a header
241 0 0       0 $category .= ':' if $category =~ /\S/;
242 0         0 push @full_data,
243             [
244             Term::ANSIColor::colored(uc($category), 'blue'),
245             '',
246             ''
247             ];
248              
249             }
250             else {
251             # numeric categories just sort
252             }
253             }
254              
255 1         2 push @full_data, @data;
256             }
257              
258 1         3 my @max_width_found = (0,0,0);
259 1         2 for (@full_data) {
260 2         3 for my $c (0..2) {
261 6 100       12 $max_width_found[$c] = length($_->[$c]) if $max_width_found[$c] < length($_->[$c]);
262             }
263             }
264              
265 1         2 my @colors = (qw/ red bold /);
266 1         2 my $text = '';
267 1         2 for my $row (@full_data) {
268 2         4 for my $c (0..2) {
269 6         3 $text .= ' ';
270 6 100       16 $text .= $colors[$c] ? Term::ANSIColor::colored($row->[$c], $colors[$c]) : $row->[$c];
271 6         76 $text .= ' ';
272 6         12 $text .= ' ' x ($max_width_found[$c]-length($row->[$c]));
273             }
274 2         2 $text .= "\n";
275             }
276 1         8 return $text;
277             }
278              
279             sub doc_sub_commands {
280 0     0 0 0 my $self = shift;
281 0         0 my ($order, $categories) = $self->_categorize_sub_commands(@_);
282 0         0 my $text = "";
283 0         0 my $indent_lvl = 4;
284 0         0 for my $category (@$order) {
285 0 0       0 my $category_name = ($category ? uc $category : "GENERAL");
286 0         0 $text .= "=head2 $category_name\n\n";
287 0         0 for my $cmd (@{$categories->{$category}}) {
  0         0  
288 0         0 $text .= "=over $indent_lvl\n\n";
289 0         0 my $name = $cmd->command_name_brief;
290 0         0 my $link = $cmd->command_name;
291 0         0 $link =~ s/ /-/g;
292 0         0 my $description = $cmd->help_brief;
293 0         0 $text .= "=item B>\n\n=over 2\n\n=item $description\n\n=back\n\n";
294 0         0 $text .= "=back\n\nE<10>\n\n";
295             }
296             }
297              
298 0         0 return $text;
299             }
300              
301             #
302             # The following methods build allow a command to determine its
303             # sub-commands, if there are any.
304             #
305              
306             # This is for cases in which the Foo::Bar command delegates to
307             # Foo::Bar::Baz, Foo::Bar::Buz or Foo::Bar::Doh, depending on its paramters.
308              
309             sub sub_command_dirs {
310 0     0 0 0 my $class = shift;
311 0   0     0 my $subdir = ref($class) || $class;
312 0         0 $subdir =~ s|::|\/|g;
313 0         0 my @dirs = grep { -d $_ } map { $_ . '/' . $subdir } @INC;
  0         0  
  0         0  
314 0         0 return @dirs;
315             }
316              
317             sub sub_command_classes {
318 3     3 0 387 my $class = shift;
319 3         17 my $mapping = $class->_build_sub_command_mapping;
320 3         19 return values %$mapping;
321             }
322              
323             # For compatability with Command::V1-based callers
324             sub is_sub_command_delegator {
325 1     1 0 6 return scalar(shift->sub_command_classes);
326             }
327              
328             sub command_tree_source_classes {
329             # override in subclass if you want different sources
330 3     3 0 5 my $class = shift;
331 3         9 return $class;
332             }
333              
334             sub _build_sub_command_mapping {
335 3     3   5 my $class = shift;
336 3   33     15 $class = ref($class) || $class;
337              
338 3         16 my @source_classes = $class->command_tree_source_classes;
339              
340 3         7 my $mapping;
341 3         4 do {
342 4     4   19 no strict 'refs';
  4         5  
  4         3304  
343 3         3 $mapping = ${ $class . '::SUB_COMMAND_MAPPING'};
  3         14  
344 3 100       11 if (ref($mapping) eq 'HASH') {
345 1         2 return $mapping;
346             }
347             };
348              
349 2         3 for my $source_class (@source_classes) {
350             # check if this class is valid
351 2         4 eval{ $source_class->class; };
  2         11  
352 2 50       5 if ( $@ ) {
353 0         0 warn $@;
354             }
355              
356             # for My::Foo::Command::* commands and sub-trees
357 2         3 my $subdir = $source_class;
358 2         9 $subdir =~ s|::|\/|g;
359              
360             # for My::Foo::*::Command sub-trees
361 2         4 my $source_class_above = $source_class;
362 2         5 $source_class_above =~ s/::Command//;
363 2         3 my $subdir2 = $source_class_above;
364 2         5 $subdir2 =~ s|::|/|g;
365              
366             # check everywhere
367 2         3 for my $lib (@INC) {
368 24         37 my $subdir_full_path = $lib . '/' . $subdir;
369              
370             # find My::Foo::Command::*
371 24 100       310 if (-d $subdir_full_path) {
372 8         716 my @files = glob($subdir_full_path . '/*');
373 8         18 for my $file (@files) {
374 24         643 my $basename = basename($file);
375 24 50       85 $basename =~ s/.pm$// or next;
376 24         35 my $sub_command_class_name = $source_class . '::' . $basename;
377 24         62 my $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name);
378 24 50       45 unless ($sub_command_class_meta) {
379 0         0 local $SIG{__DIE__};
380 0         0 local $SIG{__WARN__};
381             # until _use_safe is refactored to be permissive, use directly...
382 0         0 print ">> $sub_command_class_name\n";
383 0         0 eval "use $sub_command_class_name";
384             }
385 24         52 $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name);
386 24 50       93 next unless $sub_command_class_name->isa("Command");
387 24 50       97 next if $sub_command_class_meta->is_abstract;
388 24 50       40 next if $sub_command_class_name eq $class;
389 24         84 my $name = $source_class->_command_name_for_class_word($basename);
390 24         54 $mapping->{$name} = $sub_command_class_name;
391             }
392             }
393              
394             # find My::Foo::*::Command
395 24         30 $subdir_full_path = $lib . '/' . $subdir2;
396 24         23 my $pattern = $subdir_full_path . '/*/Command.pm';
397 24         374 my @paths = glob($pattern);
398 24         39 for my $file (@paths) {
399 0 0       0 next unless defined $file;
400 0 0       0 next unless length $file;
401 0 0       0 next unless -f $file;
402 0         0 my $last_word = File::Basename::basename($file);
403 0 0       0 $last_word =~ s/.pm$// or next;
404 0         0 my $dir = File::Basename::dirname($file);
405 0         0 my $second_to_last_word = File::Basename::basename($dir);
406 0         0 my $sub_command_class_name = $source_class_above . '::' . $second_to_last_word . '::' . $last_word;
407 0 0       0 next unless $sub_command_class_name->isa('Command');
408 0 0       0 next if $sub_command_class_name->__meta__->is_abstract;
409 0 0       0 next if $sub_command_class_name eq $class;
410 0         0 my $basename = $second_to_last_word;
411 0         0 $basename =~ s/.pm$//;
412 0         0 my $name = $source_class->_command_name_for_class_word($basename);
413 0         0 $mapping->{$name} = $sub_command_class_name;
414             }
415             }
416             }
417 2         4 return $mapping;
418             }
419              
420             sub sub_command_names {
421 1     1 0 1 my $class = shift;
422 1         4 my $mapping = $class->_build_sub_command_mapping;
423 1         6 return keys %$mapping;
424             }
425              
426              
427              
428             sub _try_command_class_named {
429 3     3   3 my $self = shift;
430              
431 3         5 my $sub_class = join('::', @_);
432              
433 3         10 my $meta = UR::Object::Type->get($sub_class); # allow in memory classes
434 3 50       20 unless ( $meta ) {
    50          
435 0         0 eval "use $sub_class;";
436 0 0       0 if ($@) {
437 0 0       0 if ($@ =~ /^Can't locate .*\.pm in \@INC/) {
438             #die "Failed to find $sub_class! $class_for_sub_command.pm!\n$@";
439 0         0 return;
440             }
441             else {
442 0         0 my @msg = split("\n",$@);
443 0         0 pop @msg;
444 0         0 pop @msg;
445 0         0 $self->error_message("$sub_class failed to compile!:\n@msg\n\n");
446 0         0 return;
447             }
448             }
449             }
450 0         0 elsif (my $isa = $sub_class->isa("Command")) {
451 3 50       7 if (ref($isa)) {
452             # dumb modules (Test::Class) mess with the standard isa() API
453 0 0       0 if ($sub_class->SUPER::isa("Command")) {
454 0         0 return $sub_class;
455             }
456             else {
457 0         0 return;
458             }
459             }
460 3         6 return $sub_class;
461             }
462             else {
463 0         0 return;
464             }
465             }
466              
467              
468             sub class_for_sub_command {
469 3     3 0 5 my $self = shift;
470 3   33     10 my $class = ref($self) || $self;
471 3         4 my $sub_command = shift;
472              
473 3 50       6 return if $sub_command =~ /^\-/; # If it starts with a "-", then it's a command-line option
474              
475             # First attempt is to convert $sub_command into a camel-case module name
476             # and just try loading it
477              
478 3         8 my $name_for_sub_command = join("", map { ucfirst($_) } split(/-/, $sub_command));
  3         9  
479 3         9 my @class_name_parts = (split(/::/,$class), $name_for_sub_command);
480 3         10 my $sub_command_class = $self->_try_command_class_named(@class_name_parts);
481 3 50       11 return $sub_command_class if $sub_command_class;
482              
483             # Remove "Command" if it's embedded in the middle and try inserting it in other places, starting at the end
484 0 0         @class_name_parts = ( ( map { $_ eq 'Command' ? () : $_ } @class_name_parts) , 'Command');
  0            
485 0           for(my $i = $#class_name_parts; $i > 0; $i--) {
486 0           $sub_command_class = $self->_try_command_class_named(@class_name_parts);
487 0 0         return $sub_command_class if $sub_command_class;
488 0           $class_name_parts[$i] = $class_name_parts[$i-1];
489 0           $class_name_parts[$i-1] = 'Command';
490             }
491              
492             # Didn't find it yet. Try exhaustively loading all the command modules under $class
493 0           my $mapping = $class->_build_sub_command_mapping;
494 0 0         if (my $sub_command_class = $mapping->{$sub_command}) {
495 0           return $sub_command_class;
496             } else {
497 0           return;
498             }
499             }
500              
501             my $depth = 0;
502             sub __extend_namespace__ {
503 0     0     my ($self,$ext) = @_;
504              
505 0           my $meta = $self->SUPER::__extend_namespace__($ext);
506 0 0         return $meta if $meta;
507              
508 0           $depth++;
509 0 0         if ($depth>1) {
510 0           $depth--;
511 0           return;
512             }
513              
514 0   0       my $class = Command::Tree::class_for_sub_command((ref $self || $self), $self->_command_name_for_class_word($ext));
515 0 0         return $class->__meta__ if $class;
516 0           return;
517             }
518              
519             1;
520              
521             __END__