File Coverage

blib/lib/PPIx/Grep.pm
Criterion Covered Total %
statement 76 261 29.1
branch 5 88 5.6
condition 0 3 0.0
subroutine 24 52 46.1
pod 5 5 100.0
total 110 409 26.8


line stmt bran cond sub pod time code
1             package PPIx::Grep;
2              
3 3     3   120774 use 5.008001;
  3         15  
  3         154  
4 3     3   18 use utf8;
  3         6  
  3         23  
5              
6 3     3   80 use strict;
  3         6  
  3         93  
7 3     3   17 use warnings;
  3         6  
  3         94  
8              
9 3     3   16 use version; our $VERSION = qv('v0.0.6');
  3         13  
  3         24  
10              
11 3     3   3239 use English qw<-no_match_vars>;
  3         14864  
  3         19  
12 3     3   1463 use Carp qw< confess >;
  3         6  
  3         167  
13 3     3   1930 use Readonly;
  3         10727  
  3         206  
14              
15 3     3   24 use Exporter qw< import >;
  3         4  
  3         834  
16              
17             our @EXPORT_OK =
18             qw<
19             run
20             set_print_format
21             >;
22             our %EXPORT_TAGS = (
23             all => [@EXPORT_OK],
24             );
25              
26 3     3   3173 use File::Next ();
  3         11702  
  3         78  
27 3     3   19211 use Getopt::Long ();
  3         85716  
  3         110  
28 3     3   11979 use List::MoreUtils qw< any none >;
  3         3246  
  3         360  
29 3     3   3159 use PPI::Document ();
  3         478804  
  3         101  
30 3     3   3497 use PPIx::Shorthand qw< get_ppi_class >;
  3         54919  
  3         323  
31 3     3   3099 use String::Format qw< stringf >;
  3         2438  
  3         19740  
32              
33              
34             Readonly my $NUMBER_OF_PPI_LOCATION_COMPONENTS => 3;
35             Readonly my $PPI_LINE_NUMBER => 0;
36             Readonly my $PPI_CHARACTER_NUMBER => 1;
37             Readonly my $PPI_COLUMN_NUMBER => 2;
38             Readonly my @OPTIONS => qw<
39             format=s
40             match=s
41             help|h|?
42             version|V
43             >;
44             # usage
45             # man
46             # chomp
47             # ignore-case|i
48             # files-with-matches|l
49             # files-without-match|L
50             # no-filename|h
51             # with-filename|H
52             # line-number|n
53             # invert-match|v
54             # tab-length
55             Readonly my $EXIT_CODE_FOUND => 0;
56             Readonly my $EXIT_CODE_NOT_FOUND => 1;
57             Readonly my $EXIT_CODE_ERROR => 2;
58              
59             Readonly my %ignored_directories =>
60             map { $_ => 1 }
61             qw<
62             .bzr
63             .cdv
64             ~.dep
65             ~.dot
66             ~.nib
67             ~.plst
68             .git
69             .hg
70             .pc
71             .svn
72             blib
73             CVS
74             RCS
75             SCCS
76             _darcs
77             _sgbak
78             autom4te.cache
79             cover_db
80             _build
81             >;
82              
83              
84             my $stdout = *STDOUT;
85             my $stderr = *STDERR;
86             my $match = undef;
87             my $print_format = "%f:%l:%c:%s\n";
88              
89              
90             sub run {
91 0     0 1 0 my @argv = @_;
92              
93 0         0 binmode _get_stdout(), ':utf8';
94 0         0 binmode _get_stderr(), ':utf8';
95              
96 0         0 my %options = _initialize_from_command_line(\@argv);
97 0 0       0 if (_handle_info_requests(\%options)) {
98 0         0 return $EXIT_CODE_ERROR;
99             } # end if
100              
101 0 0       0 if (@argv < 2) {
102 0         0 _emit_usage_message();
103 0         0 return $EXIT_CODE_ERROR;
104             } # end if
105              
106 0         0 my ($pattern, @paths) = @argv;
107 0 0       0 my @ppi_classes = _derive_ppi_classes($pattern)
108             or return $EXIT_CODE_ERROR;
109              
110 0         0 my $file_error = 0;
111             my $iterator = File::Next::files(
112             {
113             file_filter => sub {
114 0 0   0   0 not _is_ignored_file($_)
115             and _is_perl_file($File::Next::name, $file_error)
116             },
117 0     0   0 descend_filter => sub { not _is_ignored_directory($_) },
118             },
119 0         0 @paths,
120             );
121              
122 0         0 my $return_code = $EXIT_CODE_NOT_FOUND;
123 0         0 while ( defined ( my $file = $iterator->() ) ) {
124 0         0 my $found_something =
125             _search_and_emit(
126             $file,
127             $file,
128             _build_query(\@ppi_classes),
129             _get_stdout()
130             );
131              
132 0 0 0     0 if (not defined $found_something) {
    0          
133 0         0 $return_code = $EXIT_CODE_ERROR;
134             } elsif ( $EXIT_CODE_ERROR != $return_code and $found_something ) {
135 0         0 $return_code = $EXIT_CODE_FOUND;
136             } # end if
137             } # end foreach
138              
139 0 0       0 if ($file_error) {
140 0         0 $return_code = $EXIT_CODE_ERROR;
141             }
142              
143 0         0 return $return_code;
144             } # end run()
145              
146              
147             sub _initialize_from_command_line {
148 7     7   37278 my ($argv) = @_;
149 7         14 my %values;
150              
151 7         36 Getopt::Long::Configure( qw< bundling permute no_getopt_compat> );
152 7 50       413 if ( Getopt::Long::GetOptionsFromArray($argv, \%values, @OPTIONS) ) {
153 7 50       2627 _set_options(\%values) or return;
154              
155 7         34 return %values;
156             } # end if
157              
158 0         0 return;
159             } # end _initialize_from_command_line()
160              
161              
162             sub _handle_info_requests {
163 0     0   0 my ($options) = @_;
164              
165 0 0       0 if ($options->{help}) {
166 0         0 _emit_usage_message();
167              
168 0         0 return 1;
169             } # end if
170              
171 0 0       0 if ($options->{version}) {
172 0         0 _emit_version();
173              
174 0         0 return 1;
175             } # end if
176              
177 0         0 return;
178             } # end _handle_info_requests()
179              
180              
181             sub _emit_usage_message {
182 0     0   0 print {_get_stderr()} <<'END_USAGE'; ## no critic (RequireCheckedSyscalls)
  0         0  
183             ppigrep [--match regex] [--format format] PPI-class file [...]
184              
185             ppigrep { -h | --help | -V | --version }
186              
187             --format escapes:
188             %f – The name of the file.
189             %l – The starting line number of the element.
190             %c – The starting character within the first line of the element.
191             %C – The starting column within the first line of the element.
192             %L – The class of the element, with the 'PPI::' prefix removed.
193             %s – The source-code/content for the element.
194             %S – The source-code/content for the element, Ced.
195             %W – The source-code/content for the element, whitespace shrunk.
196              
197             (Note: file argument is required-- STDIN is not yet handled.)
198             END_USAGE
199              
200 0         0 return;
201             } # end _emit_usage_message()
202              
203             sub _emit_version {
204 0     0   0 print {_get_stderr()} <<"END_VERSION"; ## no critic (RequireCheckedSyscalls)
  0         0  
205             ppigrep $VERSION, Copyright ©2007-2008, Elliot Shank .
206             END_VERSION
207              
208 0         0 return;
209             } # end _emit_usage_message()
210              
211              
212             sub _derive_ppi_classes {
213 0     0   0 my ($pattern) = @_;
214              
215 0         0 my @ppi_classes;
216 0         0 foreach my $subpattern ( split m/,/xms, $pattern ) {
217 0         0 my $ppi_class = get_ppi_class($subpattern);
218 0 0       0 if (not $ppi_class) {
219 0         0 print
220 0         0 {_get_stderr()}
221             qq;
222 0         0 return;
223             } # end if
224              
225 0         0 push @ppi_classes, $ppi_class;
226             } # end foreach
227              
228 0 0       0 if (not @ppi_classes) {
229 0         0 print
230 0         0 {_get_stderr()}
231             qq;
232              
233 0         0 return;
234             } # end if
235              
236 0         0 return @ppi_classes;
237             } # end _derive_ppi_classes()
238              
239              
240             sub _build_query {
241 0     0   0 my ($ppi_classes) = @_;
242              
243 0         0 my $ppi_class;
244 0 0       0 if ( 1 == @{$ppi_classes} ) {
  0         0  
245 0         0 $ppi_class = $ppi_classes->[0]
246             } # end if
247              
248 0 0       0 if ( my $match = _get_match() ) {
249 0 0       0 if ($ppi_class) {
250             return sub {
251 0     0   0 my (undef, $element) = @_;
252              
253 0 0       0 return 0 if not $element->isa($ppi_class);
254 0 0       0 return 1 if $element->content() =~ $match;
255 0         0 return 0;
256 0         0 };
257             } # end if
258              
259             return sub {
260 0     0   0 my (undef, $element) = @_;
261              
262 0 0       0 return 0 if none { $element->isa($_) } @{$ppi_classes};
  0         0  
  0         0  
263 0 0       0 return 1 if $element->content() =~ $match;
264 0         0 return 0;
265 0         0 };
266             } # end if
267              
268 0 0       0 return $ppi_class if ($ppi_class);
269              
270             return sub {
271 0     0   0 my (undef, $element) = @_;
272              
273 0 0       0 return 1 if any { $element->isa($_) } @{$ppi_classes};
  0         0  
  0         0  
274 0         0 return 0;
275 0         0 };
276             } # end _build_query()
277              
278              
279             sub _search_and_emit {
280 0     0   0 my ($source, $source_description, $query, $destination) = @_;
281              
282 0 0       0 my $document = _create_document($source, $source_description)
283             or return;
284 0         0 $document->index_locations();
285              
286 0         0 my $elements = $document->find($query);
287 0 0       0 if ($elements) {
288 0         0 foreach my $element ( @{$elements} ) {
  0         0  
289 0         0 my $location = $element->location();
290 0         0 my @location_components;
291 0 0       0 if ($location) {
292 0         0 @location_components = @{$location};
  0         0  
293             } else {
294 0         0 @location_components = (q<>) x $NUMBER_OF_PPI_LOCATION_COMPONENTS;
295             } # end if
296              
297 0         0 print
298 0         0 {$destination}
299             _format_element($element, $source, \@location_components);
300             } # end foreach
301              
302 0         0 return 1;
303             } # end if
304              
305 0         0 return 0;
306             } # end _search_and_emit()
307              
308             sub _create_document {
309 0     0   0 my ($source, $source_description) = @_;
310              
311 0 0       0 if ( not -e $source ) {
312 0         0 print {_get_stderr()} qq<"$source_description" does not exist.\n>;
  0         0  
313 0         0 return;
314             } # end if
315              
316 0 0       0 if ( not -r $source ) {
317 0         0 print {_get_stderr()} qq<"$source_description" is not readable.\n>;
  0         0  
318 0         0 return;
319             } # end if
320              
321 0 0       0 if ( -d $source ) {
322 0         0 print {_get_stderr()} qq<"$source_description" is a directory.\n>;
  0         0  
323 0         0 return;
324             } # end if
325              
326 0 0       0 if ( -z $source ) {
327             # PPI barfs on empty documents for some reason.
328 0         0 return PPI::Document->new();
329             }
330              
331 0         0 my $document = PPI::Document->new($source, readonly => 1);
332 0 0       0 if (not $document) {
333 0         0 print {_get_stderr()} qq;
  0         0  
334 0         0 return;
335             } # end if
336              
337 0         0 return $document;
338             } # _create_document()
339              
340              
341             sub _set_options {
342 7     7   11 my ($options) = @_;
343              
344 7         15 my $match = $options->{match};
345 7 50       17 if ($match) {
346 0         0 my $compiled_match;
347              
348 0         0 eval { $compiled_match = qr/$match/; 1; } ## no critic (RegularExpressions)
  0         0  
349 0 0       0 or do {
350 0 0       0 if ($EVAL_ERROR) {
351 0         0 (my $error = $EVAL_ERROR) =~
352             s< \s+ at \s+ \S+ \s+ line \s+ \d+ .* ><>xms;
353 0         0 chomp $error;
354              
355 0         0 print {_get_stderr()} qq;
  0         0  
356              
357 0         0 return;
358             }
359              
360 0         0 print {_get_stderr()} qq;
  0         0  
361 0         0 return;
362             };
363              
364 0         0 set_match( $compiled_match );
365             } # end if
366              
367 7         12 my $format = $options->{format};
368 7 100       16 if ($format) {
369 3         11 set_print_format( "$format\n" );
370             } # end if
371              
372 7         23 return 1;
373             } # end _set_options()
374              
375              
376             sub _get_stdout {
377 0     0   0 return $stdout;
378             } # end _get_stdout()
379              
380             sub set_stdout {
381 0     0 1 0 my ($destination) = @_;
382              
383 0         0 $stdout = $destination;
384              
385 0         0 return;
386             } # end set_stdout()
387              
388              
389             sub _get_stderr {
390 0     0   0 return $stderr;
391             } # end _get_stderr()
392              
393             sub set_stderr {
394 0     0 1 0 my ($destination) = @_;
395              
396 0         0 $stderr = $destination;
397              
398 0         0 return;
399             } # end set_stderr()
400              
401              
402             sub _get_match {
403 0     0   0 return $match;
404             } # end _get_match()
405              
406             sub set_match {
407 0     0 1 0 my ($new_pattern) = @_;
408              
409 0         0 $match = $new_pattern;
410              
411 0         0 return;
412             } # end set_match()
413              
414              
415             sub _get_print_format {
416 9     9   49 return $print_format;
417             } # end _get_print_format()
418              
419             sub set_print_format {
420 11     11 1 5131 my ($new_format) = @_;
421              
422 11         19 $print_format = $new_format;
423              
424 11         20 return;
425             } # end set_print_format()
426              
427             Readonly my $PPI_PREFIX_LENGTH => length 'PPI::';
428              
429             sub _format_element {
430 9     9   816 my ($element, $filename, $location_components) = @_;
431              
432             my %format_specification = (
433             f => $filename,
434             l => $location_components->[$PPI_LINE_NUMBER],
435             c => $location_components->[$PPI_CHARACTER_NUMBER],
436             C => $location_components->[$PPI_COLUMN_NUMBER],
437 0     0   0 L => sub { substr $element->class(), $PPI_COLUMN_NUMBER },
438 1     1   44 s => sub { $element->content() },
439 0     0   0 S => sub { _invoke_method($element, $_[0]) },
440 1     1   40 t => sub { my $source = $element->content(); chomp $source; $source },
  1         7  
  1         3  
441 0     0   0 T => sub { my $source = _invoke_method($element, $_[0]); chomp $source; $source },
  0         0  
  0         0  
442 2     2   62 w => sub { _strip( $element ) },
443 0     0   0 W => sub { _strip( _invoke_method($element, $_[0]) ) },
444 9         82 );
445              
446 9         236 return stringf(_get_print_format(), %format_specification);
447             } # end _format_element()
448              
449             # Invoke an arbitrary method safely on an element.
450             sub _invoke_method {
451 0     0   0 my ($element, $method_name) = @_;
452              
453 0         0 my $value;
454 0         0 local $EVAL_ERROR = undef;
455 0 0       0 eval { $value = $element->$method_name(); 1; } or return '';
  0         0  
  0         0  
456              
457 0 0       0 if (not defined $value) {
458 0         0 return '';
459             } # end if
460              
461 0         0 return $value;
462             } # end _invoke_method()
463              
464             sub _strip {
465 2     2   4 my ($element) = @_;
466              
467 2         7 my $source = "$element"; # no content(): may be a plain string.
468 2         13 $source =~ s< \A \s+ ><>xms;
469 2         16 $source =~ s< \s+ \z ><>xms;
470 2         16 $source =~ s< \s+ >< >xmsg;
471              
472 2         6 return $source;
473             } # end _strip()
474              
475              
476             sub _is_ignored_directory {
477 0     0     my ($directory) = @_;
478              
479 0 0         return 1 if $ignored_directories{$directory};
480 0           return 0;
481             }
482              
483             sub _is_ignored_file {
484 0     0     my ($file) = @_;
485              
486 0 0         return 1 if $file =~ qr< (?: [.] bak | ~ ) \z >xms;
487 0 0         return 1 if $file =~ qr< [#] .+ [#] \z >xms;
488 0 0         return 1 if $file =~ qr< [._] .* \.swp \z >xms;
489 0           return $file =~ qr< core [.] \d+ \z >xms;
490             }
491              
492             sub _is_perl_file {
493 0     0     my ($file, $error) = @_;
494              
495 0 0         return 1 if $file =~ m/ [.] (?: p (?: l x? | m ) | t | PL ) \z /xms; ## no critic (ProhibitSingleCharAlternation)
496 0 0         return 0 if index($file, q<.>) >= 0;
497 0           return _is_perl_program($file, $error);
498             }
499              
500             sub _is_perl_program {
501 0     0     my ($file, $error) = @_;
502              
503 0 0         if (open my $handle, '<', $file) {
504 0           my $first_line = <$handle>;
505              
506 0 0         if (not close $handle) {
507 0           print {*STDERR} qq;
  0            
508 0           ${$error} = 1;
  0            
509 0           return 0;
510             }
511              
512 0           return $first_line =~ m< \A [#]! .* \bperl >xms;
513             }
514              
515 0           print {*STDERR} qq;
  0            
516 0           ${$error} = 1;
  0            
517 0           return 0;
518             }
519              
520              
521             1; # Magic true value required at end of module.
522              
523             __END__