File Coverage

lib/CLI/Dispatch/Help.pm
Criterion Covered Total %
statement 277 277 100.0
branch 14 48 29.1
condition 2 17 11.7
subroutine 88 88 100.0
pod 9 9 100.0
total 390 439 88.8


line stmt bran cond sub pod time code
1             package CLI::Dispatch::Help;
2              
3 16     16   24199 use strict;
  16     13   21  
  16     13   393  
  8     7   2056  
  8     7   22  
  8     1   155  
  8     1   27  
  8         21  
  8         164  
  7         23  
  7         23  
  7         135  
  7         28  
  7         22  
  7         155  
4 16     16   57 use warnings;
  16     13   20  
  16     13   513  
  8     7   20  
  8     7   84  
  8     1   139  
  8     1   28  
  8         105  
  8         253  
  7         22  
  7         89  
  7         199  
  7         22  
  7         89  
  7         203  
5 16     16   56 use base qw( CLI::Dispatch::Command );
  16     13   23  
  16     13   1507  
  8     7   20  
  8     7   23  
  8     1   399  
  8     1   27  
  8         24  
  8         553  
  7         23  
  7         25  
  7         502  
  7         22  
  7         24  
  7         484  
6 16     16   1185 use Class::Unload;
  16     13   3816  
  16     13   260  
  8     7   2569  
  8     7   1745  
  8     1   146  
  8     1   29  
  8         42  
  8         97  
  7         32  
  7         41  
  7         96  
  7         29  
  7         45  
  7         81  
7 16     16   64 use Class::Inspector;
  16     13   22  
  16     13   288  
  8     7   71  
  8     7   31  
  8     1   91  
  8     1   25  
  8         33  
  8         96  
  7         24  
  7         31  
  7         80  
  7         22  
  7         33  
  7         79  
8 16     16   549 use Encode;
  16     13   9069  
  16     13   1239  
  8     7   22  
  8     7   19  
  8     1   495  
  8     1   24  
  8         19  
  8         510  
  7         22  
  7         23  
  7         458  
  7         19  
  7         19  
  7         462  
9 16     16   1300 use Pod::Simple::Text;
  16     13   76684  
  16     8   335  
  8     7   2581  
  8     7   176289  
  8     1   235  
  8         26  
  8         1360  
  8         144  
  7         26  
  7         1212  
  7         106  
  7         25  
  7         1329  
  6         104  
10 16     16   2117 use Path::Tiny;
  16     13   24199  
  16     8   663  
  8     7   4908  
  8     7   56515  
  8     1   483  
  8         24  
  8         31  
  8         215  
  7         20  
  7         31  
  7         184  
  6         18  
  6         7  
  6         189  
11 16     16   601 use String::CamelCase;
  16     13   497  
  16     8   408  
  8     7   55  
  8     7   59  
  8     1   214  
  8         29  
  8         41  
  8         168  
  7         23  
  7         42  
  7         140  
  6         22  
  6         8  
  6         143  
12 16     16   420 use Term::Encoding ();
  16     13   418  
  16     8   234  
  8     7   30  
  8     7   100  
  8     1   98  
  8         22  
  8         108  
  8         83  
  7         21  
  7         97  
  7         66  
  6         19  
  6         9  
  6         64  
13 16     16   455 use Try::Tiny;
  16     13   1060  
  16     8   18322  
  8     7   26  
  8     7   24  
  8     1   7819  
  8         20  
  8         37  
  8         8195  
  7         21  
  7         22  
  7         6854  
  6         40  
  6         9  
  6         7121  
14              
15             my $term_encoding = eval {
16             find_encoding(Term::Encoding::get_encoding())
17             } || 'utf8';
18              
19              
20 10     10 1 40 sub options {qw( from|decode=s to|encode=s )}
21              
22       9 1   sub extra_namespaces {}
23              
24             sub run {
25 10     10 1 19 my ($self, @args) = @_;
26              
27 10         139 my $text;
28 10 50       31 if ( @args ) {
29 10         16 $text = $self->extract_pod( @args );
30             }
31             else {
32 6         203 $text = $self->list_commands;
33             }
34              
35 10         29 $self->output( $text );
36             }
37              
38             sub output {
39 17     17 1 26 my ($self, $text, $no_print) = @_;
40              
41 17 100       1318 unless ( Encode::is_utf8( $text ) ) {
42 12   50     57 $text = decode( $self->option('from') || 'utf8', $text )
43             }
44 17   33     283 $text = encode( $self->option('to') || $term_encoding, $text );
45              
46 17 50       541 print $text unless $no_print;
47              
48 17         210 return $text;
49             }
50              
51             sub extract_pod {
52 10     10 1 11 my ($self, $command) = @_;
53              
54 10         86 my $content = $self->_lookup( $command );
55              
56 10 50       437 unless ( $content ) {
57 6 0       8 $self->logger(1) unless $self->logger;
58 6         487 $self->log( warn => "$command is not found" );
59 6         23 return $self->list_commands;
60             }
61              
62 10         17 my $pod = $self->_parse_pod($content);
63              
64 10         126 return $self->extract_pod_body($pod);
65             }
66              
67             sub extract_pod_body {
68 10     10 1 26 my ($self, $pod) = @_;
69              
70             # remove the first ("NAME") section as the command does not
71             # always belong to the same namespace as the dispatcher/script.
72             # (default CLI::Dispatch namespace may be confusing for end users)
73 10         32 $pod =~ s/^\S+\s+(.+?)\n(?=\S)//s;
74              
75 10         219 return $pod;
76             }
77              
78             sub list_commands {
79 6     6 1 23 my $self = shift;
80              
81 6         7 my @paths = map { s{::}{/}g; $_ } $self->_namespaces;
  6         147  
  6         21  
82              
83 6         7 my %found;
84             my %classes;
85 6         67 my $maxlength = 0;
86 6         18 foreach my $inc ( @INC ) {
87 6         6 foreach my $path ( @paths ) {
88 6         7723 my $dir = path( $inc, $path );
89 6 0 0     23 next unless $dir->exists && $dir->is_dir;
90 6         8 my $iter = $dir->iterator({recurse => 1});
91 6         260 while (my $file = $iter->()) {
92 6 0       22 next if $file->is_dir;
93              
94 6         7 my $basename = $file->basename;
95 6         225 $basename =~ s/\.(?:pm|pod)$//;
96              
97 6 0       21 next if defined $found{$basename};
98              
99 6         8 (my $class = $path) =~ s{/}{::}g;
100 6         513 $class .= '::'.$basename;
101 6         24 $classes{$class} = 1;
102              
103             # ignore base class
104 6 0       8 next if $class eq 'CLI::Dispatch::Command';
105              
106 6         80 my $podfile = $file->parent->child($basename . '.pod');
107 6         21 my $pmfile = $file->parent->child($basename . '.pm');
108              
109             # should always parse .pod file if it exists
110 6 0       7 my $pod = $self->_parse_pod($podfile->exists ? $podfile->slurp : $file->slurp);
111              
112 6         74 $basename = $self->convert_command($basename);
113              
114 6   0     18 $found{$basename} ||= $self->extract_brief_description($pod, $class);
115              
116             # check availability
117 6 0       7 if ( $pmfile->exists ) {
118 6         433 my $loaded = Class::Inspector->loaded($class);
119 6 0       22 Class::Unload->unload($class) if $loaded;
120 6         7 my $error;
121 6 0   6   21 try { eval "require $class" or die }
122 6   0 6   138 catch { $error = $_ || 'Obscure error' };
  6         6  
123 6 0       189 if ($error) {
    0          
124 6 0       22 if ($error =~ /^Can't locate /) {
125             # most probably this is a subcommand of some command
126             # (ie. in a wrong namespace)
127 6         6 delete $found{$basename};
128             }
129             else {
130 6         141 $found{$basename} .= " [disabled: compile error]";
131             }
132             }
133             elsif ( $class->can('check') ) {
134 6     6   7 try { $class->check }
135             catch {
136 6   0 6   64 $error = $_ || 'Obscure error';
137 6         19 $error =~ s/\s+at .+? line \d+\.?\s*$//;
138 6         6 $found{$basename} .= " [disabled: $error]";
139 6         21 };
140             }
141 6 0       6909 Class::Unload->unload($class) unless $loaded;
142             }
143              
144 6         29 my $len = length $basename;
145 6 0       10 $maxlength = $len if $maxlength < $len;
146             }
147             }
148             }
149              
150 6         149 my $text = '';
151 6         21 my $format = "%-${maxlength}s - %s\n";
152 6         6 foreach my $key ( sort keys %found ) {
153 6         205 $text .= sprintf($format, $key, $found{$key});
154             }
155 6         20 return $text;
156             }
157              
158             sub convert_command {
159 6     6 1 8 my ($self, $command) = @_;
160 6         504 String::CamelCase::decamelize( $command );
161             }
162              
163             sub extract_brief_description {
164 6     6 1 26 my ($self, $pod, $class) = @_;
165              
166             # "NAME" header may be localized
167 6         7 my ($brief_desc) = $pod =~ /^\S+\s+$class\s+\-\s+(.+?)\n/s;
168              
169 6   0     80 return $brief_desc || '';
170             }
171              
172             sub _parse_pod {
173 10     10   28 my ($self, $file) = @_;
174              
175 10         31 my $parser = Pod::Simple::Text->new;
176 10         371 $parser->output_string( \my $pod );
177 10         3817 $parser->parse_string_document("$file");
178              
179 10         6797 return $pod;
180             }
181              
182             sub _namespaces {
183 9     9   454 my $self = shift;
184              
185 9         26 my %seen;
186 12         128 return grep { !$seen{$_}++ } (
187             $self->extra_namespaces,
188 9 50       14 @{ $self->option('_namespaces') || [] },
  9         32  
189             'CLI::Dispatch'
190             );
191             }
192              
193             sub _lookup {
194 10     10   12 my ($self, $command) = @_;
195              
196 10         237 my @paths;
197 10 100       30 if ($command =~ s/^\+//) {
198 7         10 $command =~ s{::}{/}g;
199 7         154 @paths = $command;
200             }
201             else {
202 9         25 @paths = map { s{::}{/}g; "$_/$command" } $self->_namespaces;
  12         19  
  12         81  
203             }
204              
205 10         23 foreach my $inc ( @INC ) {
206 19         92 foreach my $path ( @paths ) {
207 19         7034 foreach my $ext (qw( pod pm )) {
208 27         318 my $file = path( $inc, "$path.$ext" );
209 27 100       416 return $file->slurp if $file->exists;
210             }
211             }
212             }
213              
214             # probably it's embedded in the caller...
215 2         28 my $ct = 0;
216 2         5 my %seen;
217 2         12 while (my @caller = caller($ct++)) {
218 6 100       68 next if $caller[0] =~ /^CLI::Dispatch(::.+)?$/;
219 2 50       7 next if $seen{$caller[0]}++;
220 2         5 my $content = path($caller[1])->slurp;
221 2         237 for my $path ( @paths ) {
222 2         9 (my $package = $path) =~ s{/}{::}g;
223 2 50       21 if ($content =~ /=head1\s+\S+\s+$package/s) { # hopefully NAME
224 2         17 return $content;
225             }
226             }
227             }
228              
229 1         3 return;
230             }
231              
232             1;
233              
234             __END__