File Coverage

lib/CLI/Dispatch/Help.pm
Criterion Covered Total %
statement 277 278 99.6
branch 14 48 29.1
condition 2 17 11.7
subroutine 71 71 100.0
pod 9 9 100.0
total 373 423 88.1


line stmt bran cond sub pod time code
1             package CLI::Dispatch::Help;
2              
3 13     13   811 use strict;
  13     10   28  
  13     10   496  
  6     5   6756  
  6     5   12  
  6         242  
  6         34  
  6         10  
  6         215  
  5         30  
  5         10  
  5         187  
  5         29  
  5         10  
  5         195  
4 13     13   72 use warnings;
  13     10   24  
  13     10   478  
  6     5   31  
  6     5   11  
  6         172  
  6         34  
  6         12  
  6         228  
  5         28  
  5         10  
  5         191  
  5         35  
  5         11  
  5         199  
5 13     13   77 use base qw( CLI::Dispatch::Command );
  13     10   24  
  13     10   1393  
  6     5   31  
  6     5   11  
  6         804  
  6         33  
  6         11  
  6         551  
  5         31  
  5         10  
  5         513  
  5         29  
  5         11  
  5         504  
6 13     13   2485 use Class::Unload;
  13     10   5826  
  13     10   286  
  6     5   4775  
  6     5   1851  
  6         155  
  6         29  
  6         11  
  6         108  
  5         31  
  5         11  
  5         100  
  5         28  
  5         10  
  5         95  
7 13     13   80 use Class::Inspector;
  13     10   27  
  13     10   243  
  6     5   33  
  6     5   10  
  6         105  
  6         33  
  6         10  
  6         106  
  5         27  
  5         10  
  5         93  
  5         29  
  5         11  
  5         93  
8 13     13   2857 use Encode;
  13     10   22338  
  13     10   1474  
  6     5   33  
  6     5   8  
  6         614  
  6         29  
  6         9  
  6         599  
  5         27  
  5         10  
  5         572  
  5         25  
  5         12  
  5         4794  
9 13     13   3294 use Pod::Simple::Text;
  13     10   131785  
  13     6   431  
  6     5   5084  
  6     5   239170  
  6         206  
  6         32  
  6         11  
  6         158  
  5         29  
  5         9  
  5         115  
  5         30  
  5         11  
  5         130  
10 13     13   5405 use Path::Tiny;
  13     10   54755  
  13     6   692  
  6     5   6944  
  6     5   97645  
  6         435  
  6         30  
  6         26  
  6         269  
  5         25  
  5         10  
  5         219  
  5         30  
  5         9  
  5         233  
11 13     13   84 use String::CamelCase;
  13     10   30  
  13     6   421  
  6     5   57  
  6     5   13  
  6         189  
  6         33  
  6         10  
  6         183  
  5         30  
  5         11  
  5         157  
  5         30  
  5         10  
  5         167  
12 13     13   1061 use Term::Encoding ();
  13     10   1398  
  13     6   225  
  6     5   32  
  6     5   11  
  6         84  
  6         36  
  6         10  
  6         94  
  5         27  
  5         9  
  5         80  
  5         30  
  5         11  
  5         87  
13 13     13   72 use Try::Tiny;
  13     10   26  
  13     6   27973  
  6     5   33  
  6     5   12  
  6         10182  
  6         28  
  6         12  
  6         9667  
  5         26  
  5         6  
  5         8041  
  5         26  
  5         10  
  5         8617  
14              
15             my $term_encoding = eval {
16             find_encoding(Term::Encoding::get_encoding())
17             } || 'utf8';
18              
19              
20 8     8 1 46 sub options {qw( from|decode=s to|encode=s )}
21              
22 7     7 1 14 sub extra_namespaces {}
23              
24             sub run {
25 8     8 1 156 my ($self, @args) = @_;
26              
27 8         30 my $text;
28 8 50       24 if ( @args ) {
29 8         169 $text = $self->extract_pod( @args );
30             }
31             else {
32 4         23 $text = $self->list_commands;
33             }
34              
35 8         22 $self->output( $text );
36             }
37              
38             sub output {
39 14     14 1 435 my ($self, $text, $no_print) = @_;
40              
41 14 100       89 unless ( Encode::is_utf8( $text ) ) {
42 10   50     53 $text = decode( $self->option('from') || 'utf8', $text )
43             }
44 14   33     378 $text = encode( $self->option('to') || $term_encoding, $text );
45              
46 14 50       1040 print $text unless $no_print;
47              
48 14         277 return $text;
49             }
50              
51             sub extract_pod {
52 8     8 1 91 my ($self, $command) = @_;
53              
54 8         41 my $content = $self->_lookup( $command );
55              
56 8 50       703 unless ( $content ) {
57 4 0       462 $self->logger(1) unless $self->logger;
58 4         27 $self->log( warn => "$command is not found" );
59 4         9 return $self->list_commands;
60             }
61              
62 8         126 my $pod = $self->_parse_pod($content);
63              
64 8         44 return $self->extract_pod_body($pod);
65             }
66              
67             sub extract_pod_body {
68 8     8 1 20 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 8         213 $pod =~ s/^\S+\s+(.+?)\n(?=\S)//s;
74              
75 8         39 return $pod;
76             }
77              
78             sub list_commands {
79 4     4 1 10 my $self = shift;
80              
81 4         133 my @paths = map { s{::}{/}g; $_ } $self->_namespaces;
  4         21  
  4         7  
82              
83 4         71 my %found;
84             my %classes;
85 4         22 my $maxlength = 0;
86 4         8 foreach my $inc ( @INC ) {
87 4         7031 foreach my $path ( @paths ) {
88 4         24 my $dir = path( $inc, $path );
89 4 0 0     7 next unless $dir->exists && $dir->is_dir;
90 4         149 my $iter = $dir->iterator({recurse => 1});
91 4         26 while (my $file = $iter->()) {
92 4 0       8 next if $file->is_dir;
93              
94 4         159 my $basename = $file->basename;
95 4         25 $basename =~ s/\.(?:pm|pod)$//;
96              
97 4 0       8 next if defined $found{$basename};
98              
99 4         457 (my $class = $path) =~ s{/}{::}g;
100 4         24 $class .= '::'.$basename;
101 4         9 $classes{$class} = 1;
102              
103             # ignore base class
104 4 0       81 next if $class eq 'CLI::Dispatch::Command';
105              
106 4         26 my $podfile = $file->parent->child($basename . '.pod');
107 4         8 my $pmfile = $file->parent->child($basename . '.pm');
108              
109             # should always parse .pod file if it exists
110 4 0       76 my $pod = $self->_parse_pod($podfile->exists ? $podfile->slurp : $file->slurp);
111              
112 4         23 $basename = $self->convert_command($basename);
113              
114 4   0     9 $found{$basename} ||= $self->extract_brief_description($pod, $class);
115              
116             # check availability
117 4 0       444 if ( $pmfile->exists ) {
118 4         31 my $loaded = Class::Inspector->loaded($class);
119 4 0       17 Class::Unload->unload($class) if $loaded;
120 4         98 my $error;
121 4 0   4   13 try { eval "require $class" or die }
122 4   0 4   25 catch { $error = $_ || 'Obscure error' };
  4         169  
123 4 0       24 if ($error) {
    0          
124 4 0       8 if ($error =~ /^Can't locate /) {
125             # most probably this is a subcommand of some command
126             # (ie. in a wrong namespace)
127 4         121 delete $found{$basename};
128             }
129             else {
130 4         22 $found{$basename} .= " [disabled: compile error]";
131             }
132             }
133             elsif ( $class->can('check') ) {
134 4     4   64 try { $class->check }
135             catch {
136 4   0 4   21 $error = $_ || 'Obscure error';
137 4         18 $error =~ s/\s+at .+? line \d+\.?\s*$//;
138 4         6430 $found{$basename} .= " [disabled: $error]";
139 4         7 };
140             }
141 4 0       22 Class::Unload->unload($class) unless $loaded;
142             }
143              
144 4         10 my $len = length $basename;
145 4 0       142 $maxlength = $len if $maxlength < $len;
146             }
147             }
148             }
149              
150 4         22 my $text = '';
151 4         9 my $format = "%-${maxlength}s - %s\n";
152 4         149 foreach my $key ( sort keys %found ) {
153 4         27 $text .= sprintf($format, $key, $found{$key});
154             }
155 4         11 return $text;
156             }
157              
158             sub convert_command {
159 4     4 1 412 my ($self, $command) = @_;
160 4         25 String::CamelCase::decamelize( $command );
161             }
162              
163             sub extract_brief_description {
164 4     4 1 11 my ($self, $pod, $class) = @_;
165              
166             # "NAME" header may be localized
167 4         86 my ($brief_desc) = $pod =~ /^\S+\s+$class\s+\-\s+(.+?)\n/s;
168              
169 4   0     26 return $brief_desc || '';
170             }
171              
172             sub _parse_pod {
173 8     8   20 my ($self, $file) = @_;
174              
175 8         138 my $parser = Pod::Simple::Text->new;
176 8         509 $parser->output_string( \my $pod );
177 8         6894 $parser->parse_string_document("$file");
178              
179 8         10342 return $pod;
180             }
181              
182             sub _namespaces {
183 7     7   28 my $self = shift;
184              
185 7         15 my %seen;
186 10 50       53 return grep { !$seen{$_}++ } (
  7         26  
187             $self->extra_namespaces,
188 7         107 @{ $self->option('_namespaces') || [] },
189             'CLI::Dispatch'
190             );
191             }
192              
193             sub _lookup {
194 8     8   175 my ($self, $command) = @_;
195              
196 8         30 my @paths;
197 8 100       28 if ($command =~ s/^\+//) {
198 5         130 $command =~ s{::}{/}g;
199 5         24 @paths = $command;
200             }
201             else {
202 7         21 @paths = map { s{::}{/}g; "$_/$command" } $self->_namespaces;
  10         169  
  10         42  
203             }
204              
205 8         19 foreach my $inc ( @INC ) {
206 17         6521 foreach my $path ( @paths ) {
207 13         19 foreach my $ext (qw( pod pm )) {
208 26         535 my $file = path( $inc, "$path.$ext" );
209 26 100       675 return $file->slurp if $file->exists;
210             }
211             }
212             }
213              
214             # probably it's embedded in the caller...
215 1         10 my $ct = 0;
216 1         2 my %seen;
217 1         13 while (my @caller = caller($ct++)) {
218 5 100       49 next if $caller[0] =~ /^CLI::Dispatch(::.+)?$/;
219 1 50       5 next if $seen{$caller[0]}++;
220 1         3 my $content = path($caller[1])->slurp;
221 1         203 for my $path ( @paths ) {
222 1         4 (my $package = $path) =~ s{/}{::}g;
223 1 50       23 if ($content =~ /=head1\s+\S+\s+$package/s) { # hopefully NAME
224 1         5 return $content;
225             }
226             }
227             }
228              
229 0         0 return;
230             }
231              
232             1;
233              
234             __END__