File Coverage

blib/lib/Path/Iterator/Rule.pm
Criterion Covered Total %
statement 260 276 94.2
branch 117 130 90.0
condition 39 43 90.7
subroutine 49 49 100.0
pod 11 12 91.6
total 476 510 93.3


line stmt bran cond sub pod time code
1 22     22   1976232 use 5.008001;
  22         257  
2 22     22   126 use strict;
  22         47  
  22         476  
3 22     22   110 use warnings;
  22         40  
  22         1190  
4              
5             package Path::Iterator::Rule;
6             # ABSTRACT: Iterative, recursive file finder
7             our $VERSION = '1.014';
8              
9             # Register warnings category
10 22     22   142 use warnings::register;
  22         40  
  22         3029  
11              
12 22     22   144 use if $] ge '5.010000', 're', 'regexp_pattern';
  22         45  
  22         132  
13              
14             # Dependencies
15 22     22   4148 use Carp ();
  22         58  
  22         402  
16 22     22   116 use File::Basename ();
  22         40  
  22         412  
17 22     22   119 use File::Spec ();
  22         48  
  22         406  
18 22     22   109 use List::Util ();
  22         56  
  22         586  
19 22     22   8454 use Number::Compare 0.02;
  22         9417  
  22         516  
20 22     22   130 use Scalar::Util ();
  22         47  
  22         374  
21 22     22   8214 use Text::Glob ();
  22         16654  
  22         470  
22 22     22   9299 use Try::Tiny;
  22         40742  
  22         5337  
23              
24             #--------------------------------------------------------------------------#
25             # constructors and meta methods
26             #--------------------------------------------------------------------------#
27              
28             sub new {
29 107     107 1 137839 my $class = shift;
30 107 100       299 $class = ref $class if ref $class;
31 107         572 return bless { rules => [] }, $class;
32             }
33              
34             sub clone {
35 1     1 1 5 my $self = shift;
36 1         4 return bless _my_clone( {%$self} ), ref $self;
37             }
38              
39             # avoid XS/buggy dependencies for a simple recursive clone; we clone
40             # fully instead of just 'rules' in case we get subclassed and they
41             # add attributes
42             sub _my_clone {
43 3     3   4 my $d = shift;
44 3 100       8 if ( ref $d eq 'HASH' ) {
    100          
45             return {
46 1 50       3 map { ; my $v = $d->{$_}; $_ => ( ref($v) ? _my_clone($v) : $v ) }
  1         2  
  1         6  
47             keys %$d
48             };
49             }
50             elsif ( ref $d eq 'ARRAY' ) {
51 1 50       2 return [ map { ref($_) ? _my_clone($_) : $_ } @$d ];
  1         4  
52             }
53             else {
54 1         7 return $d;
55             }
56             }
57              
58             sub add_helper {
59 1432     1432 0 2860 my ( $class, $name, $coderef, $skip_negation ) = @_;
60 1432 100       2677 $class = ref $class if ref $class;
61 1432 100       8337 if ( !$class->can($name) ) {
62 22     22   183 no strict 'refs'; ## no critic
  22         52  
  22         102082  
63             *$name = sub {
64 99     99   1623 my $self = shift;
65 99         249 my $rule = $coderef->(@_);
66 99         301 $self->and($rule);
67 1431         6608 };
68 1431 100       3587 if ( !$skip_negation ) {
69 1211         7362 *{"not_$name"} = sub {
70 7     7   15 my $self = shift;
71 7         23 my $rule = $coderef->(@_);
72 7         36 $self->not($rule);
73 1211         3513 };
74             }
75             }
76             else {
77 1         128 Carp::croak("Can't add rule '$name' because it conflicts with an existing method");
78             }
79             }
80              
81             #--------------------------------------------------------------------------#
82             # Implementation-specific method; these may be overridden by subclasses
83             # to test/return results of file wrappers like Path::Class or IO::All
84             # or to provide custom error handler, visitors or other features
85             #--------------------------------------------------------------------------#
86              
87             sub _objectify {
88 95     95   201 my ( $self, $path ) = @_;
89 95         303 return "$path";
90             }
91              
92             ## We inline this below, but a subclass equivalent would be this:
93             ##sub _children {
94             ## my $self = shift;
95             ## my $path = "" . shift; # stringify objects
96             ## opendir( my $dh, $path );
97             ## return map { [ $_, "$path/$_" ] } grep { $_ ne "." && $_ ne ".." } readdir $dh;
98             ##}
99              
100             # The _stringify option controls whether the string form of an object is cached
101             # for iteration control. This is generally a good idea to avoid extra overhead,
102             # but subclasses can override this if necessary
103              
104             sub _defaults {
105             return (
106             _stringify => 1,
107             follow_symlinks => 1,
108             depthfirst => 0,
109             sorted => 1,
110             loop_safe => ( $^O eq 'MSWin32' ? 0 : 1 ), # No inode #'s on Windows
111 2     2   21 error_handler => sub { die sprintf( "%s: %s", @_ ) },
112 78 50   78   834 visitor => undef,
113             );
114             }
115              
116             sub _fast_defaults {
117             return (
118 2     2   22 _stringify => 1,
119             follow_symlinks => 1,
120             depthfirst => -1,
121             sorted => 0,
122             loop_safe => 0,
123             error_handler => undef,
124             visitor => undef,
125             );
126             }
127              
128             #--------------------------------------------------------------------------#
129             # iteration methods
130             #--------------------------------------------------------------------------#
131              
132             sub iter {
133 78     78 1 149 my $self = shift;
134 78         212 $self->_iter( { $self->_defaults }, @_ );
135             }
136              
137             sub iter_fast {
138 2     2 1 2 my $self = shift;
139 2         7 $self->_iter( { $self->_fast_defaults }, @_ );
140             }
141              
142             sub _iter {
143 80     80   157 my $self = shift;
144 80         127 my $defaults = shift;
145 80 100 100     731 my $args =
    100 100        
146             ref( $_[0] ) && !Scalar::Util::blessed( $_[0] ) ? shift
147             : ref( $_[-1] ) && !Scalar::Util::blessed( $_[-1] ) ? pop
148             : {};
149 80         482 my %opts = ( %$defaults, %$args );
150              
151             # unroll these for efficiency
152 80         194 my $opt_stringify = $opts{_stringify};
153 80         140 my $opt_depthfirst = $opts{depthfirst};
154 80         128 my $opt_follow_symlinks = $opts{follow_symlinks};
155 80         173 my $opt_sorted = $opts{sorted};
156 80         151 my $opt_loop_safe = $opts{loop_safe};
157 80         138 my $opt_error_handler = $opts{error_handler};
158 80         156 my $opt_relative = $opts{relative};
159 80         127 my $opt_visitor = $opts{visitor};
160 80         111 my $has_rules = @{ $self->{rules} };
  80         169  
161 80         144 my $stash = {};
162              
163             my $opt_report_symlinks =
164             defined( $opts{report_symlinks} )
165             ? $opts{report_symlinks}
166 80 100       211 : $opts{follow_symlinks};
167              
168             # if not subclassed, we want to inline
169 80         334 my $can_children = $self->can("_children");
170              
171             # queue structure: flat list of (unnested) tuples of 4 (object,
172             # basename, depth, origin). If object is a coderef, it's a deferred
173             # directory list. If the object is an arrayref, then that's a special
174             # case signal that it was already of interest and can finally be
175             # returned for postorder searches
176             my @queue =
177             map {
178 80 100       248 my $i = $self->_objectify($_);
  80         217  
179 80         893 ( $i, File::Basename::basename("$_"), 0, $i )
180             } @_ ? @_ : '.';
181              
182             return sub {
183             LOOP: {
184 373     373   548 my ( $item, $base, $depth, $origin ) = splice( @queue, 0, 4 );
  804         2060  
185 804 100       1772 return unless $item;
186 726 100       1603 if ( ref $item eq 'CODE' ) {
187             # replace placeholder with children
188 200         392 unshift @queue, $item->();
189 200         1441 redo LOOP;
190             }
191 526 100       1203 return $item->[0] if ref $item eq 'ARRAY'; # deferred for postorder
192 517 100       1172 my $string_item = $opt_stringify ? "$item" : $item;
193              
194             # by default, we're interested in everything and prune nothing
195 517         924 my ( $interest, $prune ) = ( 1, 0 );
196              
197 517 100       5735 if ( -l $string_item ) {
198 15 100       42 $prune = 1 if !$opt_follow_symlinks;
199 15 100       30 redo LOOP if !$opt_report_symlinks;
200             }
201              
202 515 100       1518 if ($has_rules) {
203 390         756 local $_ = $item;
204 390         720 $stash->{_depth} = $depth;
205 390 100       755 if ($opt_error_handler) {
206 383         15417 $interest = try { $self->test( $item, $base, $stash ) }
207 383         2481 catch { $opt_error_handler->( $item, $_ ) };
  5         148  
208             }
209             else {
210 7         14 $interest = $self->test( $item, $base, $stash );
211             }
212             # New way to signal prune is returning a reference to a scalar.
213             # Value of the scalar indicates if it should be returned by the
214             # iterator or not
215 388 100       5170 if ( ref $interest eq 'SCALAR' ) {
216 26         42 $prune = 1;
217 26         41 $interest = $$interest;
218             }
219             }
220              
221             # if we have a visitor, we call it like a custom rule
222 513 100 100     1145 if ( $opt_visitor && $interest ) {
223 34         56 local $_ = $item;
224 34         65 $stash->{_depth} = $depth;
225 34         84 $opt_visitor->( $item, $base, $stash );
226             }
227              
228             # if it's a directory, maybe add children to the queue
229 513 100 100     14040 if ( ( -d $string_item )
      100        
      100        
230             && ( !$prune )
231             && ( !$opt_loop_safe || $self->_is_unique( $string_item, $stash ) ) )
232             {
233 200 50       2585 if ( !-r $string_item ) {
234 0         0 warnings::warnif("Directory '$string_item' is not readable. Skipping it");
235             }
236             else {
237 200         585 my $depth_p1 = $depth + 1;
238 200         277 my $next;
239 200 100       414 if ($can_children) {
240             $next = sub {
241 15         39 my @paths = $can_children->( $self, $item );
242 15 100       3146 if ($opt_sorted) {
243 12         34 @paths = sort { "$a->[0]" cmp "$b->[0]" } @paths;
  20         40  
244             }
245 15         27 map { ( $_->[1], $_->[0], $depth_p1, $origin ) } @paths;
  35         98  
246 15         69 };
247             }
248             else {
249             $next = sub {
250 185         4483 opendir( my $dh, $string_item );
251 185 100       648 if ($opt_sorted) {
252 390         3449 map { ( "$string_item/$_", $_, $depth_p1, $origin ) }
253 183 100       3378 sort { $a cmp $b } grep { $_ ne "." && $_ ne ".." } readdir $dh;
  314         557  
  756         3011  
254             }
255             else {
256 12         76 map { ( "$string_item/$_", $_, $depth_p1, $origin ) }
257 2 100       42 grep { $_ ne "." && $_ ne ".." } readdir $dh;
  16         54  
258             }
259 185         1036 };
260             }
261              
262 200 100       507 if ($opt_depthfirst) {
263             # either preorder (parents before kids) or
264             # postorder (parents after kids); for postorder,
265             # requeue current item as a reference to signal it
266             # can be returned without being retested
267 31 100 100     217 unshift @queue,
    100          
268             [
269             (
270             $opt_relative
271             ? $self->_objectify( File::Spec->abs2rel( $string_item, $origin ) )
272             : $item
273             )
274             ],
275             undef, undef, undef
276             if $interest && $opt_depthfirst > 0;
277 31         58 unshift @queue, $next, undef, undef, undef;
278 31 100       74 redo LOOP if $opt_depthfirst > 0;
279             }
280             else {
281             # breadth-first: add placeholder for children at the end
282 169         459 push @queue, $next, undef, undef, undef;
283             }
284             }
285             } # end of "is directory maybe with children"
286             return (
287 501 100       2598 $opt_relative
    100          
288             ? $self->_objectify( File::Spec->abs2rel( $string_item, $origin ) )
289             : $item
290             ) if $interest;
291 217         465 redo LOOP;
292             }
293 80         3403 };
294             }
295              
296             sub all {
297 76     76 1 77553 my $self = shift;
298 76         231 return $self->_all( $self->iter(@_) );
299             }
300              
301             sub all_fast {
302 2     2 1 17067 my $self = shift;
303 2         10 return $self->_all( $self->iter_fast(@_) );
304             }
305              
306             sub _all {
307 78     78   207 my $self = shift;
308 78         159 my $iter = shift;
309 78 100       204 if (wantarray) {
    100          
310 69         110 my @results;
311 69         143 while ( defined( my $item = $iter->() ) ) {
312 249         650 push @results, $item;
313             }
314 69         429 return @results;
315             }
316             elsif ( defined wantarray ) {
317 1         3 my $count = 0;
318 1         3 $count++ while defined $iter->();
319 1         6 return $count;
320             }
321             else {
322 8         20 1 while defined $iter->();
323             }
324             }
325              
326             #--------------------------------------------------------------------------#
327             # logic methods
328             #--------------------------------------------------------------------------#
329              
330             sub and {
331 135     135 1 223 my $self = shift;
332 135         179 push @{ $self->{rules} }, $self->_rulify(@_);
  135         424  
333 134         471 return $self;
334             }
335              
336             sub or {
337 9     9 1 18 my $self = shift;
338 9         26 my @rules = $self->_rulify(@_);
339             my $coderef = sub {
340 58     58   77 my ( $result, $prune );
341 58         92 for my $rule (@rules) {
342 113         223 $result = $rule->(@_);
343             # once any rule says to prune, we remember that
344 113   100     528 $prune ||= ref($result) eq 'SCALAR';
345             # extract whether constraint was met
346 113 100       220 $result = $$result if ref($result) eq 'SCALAR';
347             # shortcut if met, propagating prune state
348 113 100       270 return ( $prune ? \1 : 1 ) if $result;
    100          
349             }
350 35 100       131 return ( $prune ? \$result : $result )
351             ; # may or may not be met, but propagate prune state
352 9         34 };
353 9         27 return $self->and($coderef);
354             }
355              
356             sub not {
357 8     8 1 15 my $self = shift;
358 8         20 my $obj = $self->new->and(@_);
359             my $coderef = sub {
360 38     38   88 my $result = $obj->test(@_);
361 38 100       117 return ref($result) ? \!$$result : !$result; # invert, but preserve prune
362 8         32 };
363 8         27 return $self->and($coderef);
364             }
365              
366             sub skip {
367 4     4 1 20 my $self = shift;
368 4         9 my @rules = @_;
369 4         8 my $obj = $self->new->or(@rules);
370             my $coderef = sub {
371 26     26   50 my $result = $obj->test(@_);
372 26         39 my ( $prune, $interest );
373 26 100       45 if ( ref($result) eq 'SCALAR' ) {
374             # test told us to prune, so make that sticky
375             # and also skip it
376 3         4 $prune = 1;
377 3         5 $interest = 0;
378             }
379             else {
380             # prune if test was true
381 23         26 $prune = $result;
382             # negate test result
383 23         34 $interest = !$result;
384             }
385 26 100       58 return $prune ? \$interest : $interest;
386 4         12 };
387 4         8 return $self->and($coderef);
388             }
389              
390             sub test {
391 665     665 1 1348 my ( $self, $item, $base, $stash ) = @_;
392 665         865 my ( $result, $prune );
393 665         816 for my $rule ( @{ $self->{rules} } ) {
  665         1269  
394 816   100     8800 $result = $rule->( $item, $base, $stash ) || 0;
395 812 100 100     3056 if ( !ref($result) && $result eq '0 but true' ) {
396 1         134 Carp::croak("0 but true no longer supported by custom rules");
397             }
398             # once any rule says to prune, we remember that
399 811   100     2666 $prune ||= ref($result) eq 'SCALAR';
400             # extract whether constraint was met
401 811 100       1405 $result = $$result if ref($result) eq 'SCALAR';
402             # shortcut if not met, propagating prune state
403 811 100       2399 return ( $prune ? \0 : 0 ) if !$result;
    100          
404             }
405 248 100       780 return ( $prune ? \1 : 1 ); # all constraints met, but propagate prune state
406             }
407              
408             #--------------------------------------------------------------------------#
409             # private methods
410             #--------------------------------------------------------------------------#
411              
412             sub _rulify {
413 144     144   296 my ( $self, @args ) = @_;
414 144         200 my @rules;
415 144         281 for my $arg (@args) {
416 154         192 my $rule;
417 154 100 100     720 if ( Scalar::Util::blessed($arg) && $arg->isa("Path::Iterator::Rule") ) {
    100          
418 25     181   82 $rule = sub { $arg->test(@_) };
  181         401  
419             }
420             elsif ( ref($arg) eq 'CODE' ) {
421 128         205 $rule = $arg;
422             }
423             else {
424 1         152 Carp::croak("Rules must be coderef or Path::Iterator::Rule");
425             }
426 153         323 push @rules, $rule;
427             }
428 143         302 return @rules;
429             }
430              
431             sub _is_unique {
432 194     194   632 my ( $self, $string_item, $stash ) = @_;
433 194         273 my $unique_id;
434 194         280 my @st = eval { stat $string_item };
  194         2080  
435 194 50       635 @st = eval { lstat $string_item } unless @st;
  0         0  
436 194 50       433 if (@st) {
437 194         636 $unique_id = join( ",", $st[0], $st[1] );
438             }
439             else {
440 0 0       0 my $type = -d $string_item ? 'directory' : 'file';
441 0         0 warnings::warnif("Could not stat $type '$string_item'");
442 0         0 $unique_id = $string_item;
443             }
444 194         1344 return !$stash->{_seen}{$unique_id}++;
445             }
446              
447             #--------------------------------------------------------------------------#
448             # built-in helpers
449             #--------------------------------------------------------------------------#
450              
451             sub _regexify {
452 42     42   91 my ( $re, $add ) = @_;
453 42   100     170 $add ||= '';
454 42 100       138 my $new = ref($re) eq 'Regexp' ? $re : Text::Glob::glob_to_regex($re);
455 42 100       2935 return $new unless $add;
456 3         6 my ( $pattern, $flags ) = _split_re($new);
457 3 50       8 my $new_flags = $add ? _reflag( $flags, $add ) : "";
458 3         40 return qr/$new_flags$pattern/;
459             }
460              
461             sub _split_re {
462 3     3   4 my $value = shift;
463 3 50       15 if ( $] ge 5.010 ) {
464 3         11 return re::regexp_pattern($value);
465             }
466             else {
467 0         0 $value =~ s/^\(\?\^?//;
468 0         0 $value =~ s/\)$//;
469 0         0 my ( $opt, $re ) = split( /:/, $value, 2 );
470 0         0 $opt =~ s/\-\w+$//;
471 0         0 return ( $re, $opt );
472             }
473             }
474              
475             sub _reflag {
476 3     3   6 my ( $orig, $add ) = @_;
477 3   100     11 $orig ||= "";
478              
479 3 50       4 if ( $] >= 5.014 ) {
480 3         8 return "(?^$orig$add)";
481             }
482             else {
483 0         0 my ( $pos, $neg ) = split /-/, $orig;
484 0   0     0 $pos ||= "";
485 0   0     0 $neg ||= "";
486 0         0 $neg =~ s/i//;
487 0 0       0 $neg = "-$neg" if length $neg;
488 0         0 return "(?$add$pos$neg)";
489             }
490             }
491              
492             # "simple" helpers take no arguments
493             my %simple_helpers = (
494             directory => sub { -d $_ }, # see also -d => dir below
495             dangling => sub { -l $_ && !stat $_ },
496             );
497              
498             while ( my ( $k, $v ) = each %simple_helpers ) {
499             __PACKAGE__->add_helper( $k, sub { return $v } );
500             }
501              
502             sub _generate_name_matcher {
503 33     33   88 my (@patterns) = @_;
504 33 100       82 if ( @patterns > 1 ) {
505             return sub {
506 28     28   53 my $name = "$_[1]";
507 28 100       120 return ( List::Util::first { $name =~ $_ } @patterns ) ? 1 : 0;
  83         297  
508             }
509 4         22 }
510             else {
511 29         47 my $pattern = $patterns[0];
512             return sub {
513 133     133   216 my $name = "$_[1]";
514 133 100       793 return $name =~ $pattern ? 1 : 0;
515             }
516 29         133 }
517             }
518              
519             # "complex" helpers take arguments
520             my %complex_helpers = (
521             name => sub {
522             Carp::croak("No patterns provided to 'name'") unless @_;
523             _generate_name_matcher( map { _regexify($_) } @_ );
524             },
525             iname => sub {
526             Carp::croak("No patterns provided to 'iname'") unless @_;
527             _generate_name_matcher( map { _regexify( $_, "i" ) } @_ );
528             },
529             min_depth => sub {
530             Carp::croak("No depth argument given to 'min_depth'") unless @_;
531             my $min_depth = 0+ shift; # if this warns, do here and not on every file
532             return sub {
533             my ( $f, $b, $stash ) = @_;
534             return $stash->{_depth} >= $min_depth;
535             }
536             },
537             max_depth => sub {
538             Carp::croak("No depth argument given to 'max_depth'") unless @_;
539             my $max_depth = 0+ shift; # if this warns, do here and not on every file
540             return sub {
541             my ( $f, $b, $stash ) = @_;
542             return 1 if $stash->{_depth} < $max_depth;
543             return \1 if $stash->{_depth} == $max_depth;
544             return \0;
545             }
546             },
547             shebang => sub {
548             Carp::croak("No patterns provided to 'shebang'") unless @_;
549             my @patterns = map { _regexify($_) } @_;
550             return sub {
551             my $f = shift;
552             return unless !-d $f;
553             open my $fh, "<", $f;
554             my $shebang = <$fh>;
555             return unless defined $shebang;
556             return ( List::Util::first { $shebang =~ $_ } @patterns ) ? 1 : 0;
557             };
558             },
559             contents_match => sub {
560             my @regexp = @_;
561             my $filter = ':encoding(UTF-8)';
562             $filter = shift @regexp unless ref $regexp[0];
563             return sub {
564             my $f = shift;
565             return unless !-d $f;
566             my $contents = do {
567             local $/ = undef;
568             open my $fh, "<$filter", $f;
569             <$fh>;
570             };
571             for my $re (@regexp) {
572             return 1 if $contents =~ $re;
573             }
574             return 0;
575             };
576             },
577             line_match => sub {
578             my @regexp = @_;
579             my $filter = ':encoding(UTF-8)';
580             $filter = shift @regexp unless ref $regexp[0];
581             return sub {
582             my $f = shift;
583             return unless !-d $f;
584 1     1   7 open my $fh, "<$filter", $f;
  1         1  
  1         9  
585             while ( my $line = <$fh> ) {
586             for my $re (@regexp) {
587             return 1 if $line =~ $re;
588             }
589             }
590             return 0;
591             };
592             },
593             );
594              
595             while ( my ( $k, $v ) = each %complex_helpers ) {
596             __PACKAGE__->add_helper( $k, $v );
597             }
598              
599             # skip_dirs
600             __PACKAGE__->add_helper(
601             skip_dirs => sub {
602             Carp::croak("No patterns provided to 'skip_dirs'") unless @_;
603             my $name_check = Path::Iterator::Rule->new->name(@_);
604             return sub {
605             return \0 if -d $_[0] && $name_check->test(@_);
606             return 1; # otherwise, like a null rule
607             }
608             } => 1 # don't create not_skip_dirs
609             );
610              
611             __PACKAGE__->add_helper(
612             skip_subdirs => sub {
613             Carp::croak("No patterns provided to 'skip_subdirs'") unless @_;
614             my $name_check = Path::Iterator::Rule->new->name(@_);
615             return sub {
616             my ( $f, $b, $stash ) = @_;
617             return \0 if -d $f && $stash->{_depth} && $name_check->test(@_);
618             return 1; # otherwise, like a null rule
619             }
620             } => 1 # don't create not_skip_dirs
621             );
622              
623             # X_tests adapted from File::Find::Rule
624             #<<< do not perltidy this
625             my %X_tests = (
626             -r => readable => -R => r_readable =>
627             -w => writeable => -W => r_writeable =>
628             -w => writable => -W => r_writable =>
629             -x => executable => -X => r_executable =>
630             -o => owned => -O => r_owned =>
631              
632             -e => exists => -f => file =>
633             -z => empty => -d => dir =>
634             -s => nonempty => -l => symlink =>
635             => -p => fifo =>
636             -u => setuid => -S => socket =>
637             -g => setgid => -b => block =>
638             -k => sticky => -c => character =>
639             => -t => tty =>
640             -T => ascii =>
641             -B => binary =>
642             );
643             #>>>
644              
645             while ( my ( $op, $name ) = each %X_tests ) {
646             my $coderef = eval "sub { $op \$_ }"; ## no critic
647             __PACKAGE__->add_helper( $name, sub { return $coderef } );
648             }
649              
650             my %time_tests = ( -A => accessed => -M => modified => -C => changed => );
651              
652             while ( my ( $op, $name ) = each %time_tests ) {
653             my $filetest = eval "sub { $op \$_ }"; ## no critic
654             my $coderef = sub {
655             Carp::croak("The '$name' test requires a single argument") unless @_ == 1;
656             my $comparator = Number::Compare->new(shift);
657             return sub { return $comparator->( $filetest->() ) };
658             };
659             __PACKAGE__->add_helper( $name, $coderef );
660             }
661              
662             # stat tests adapted from File::Find::Rule
663             my @stat_tests = qw(
664             dev ino mode nlink uid gid rdev size atime mtime ctime blksize blocks
665             );
666              
667             for my $i ( 0 .. $#stat_tests ) {
668             my $name = $stat_tests[$i];
669             my $coderef = sub {
670             Carp::croak("The '$name' test requires a single argument") unless @_ == 1;
671             my $comparator = Number::Compare->new(shift);
672             return sub { return $comparator->( ( stat($_) )[$i] ) };
673             };
674             __PACKAGE__->add_helper( $name, $coderef );
675             }
676              
677             # VCS rules adapted from File::Find::Rule::VCS
678             my %vcs_rules = (
679             skip_cvs => sub {
680             return Path::Iterator::Rule->new->skip_dirs('CVS')->not_name(qr/\.\#$/);
681             },
682             skip_rcs => sub {
683             return Path::Iterator::Rule->new->skip_dirs('RCS')->not_name(qr/,v$/);
684             },
685             skip_git => sub {
686             return Path::Iterator::Rule->new->skip_dirs('.git');
687             },
688             skip_svn => sub {
689             return Path::Iterator::Rule->new->skip_dirs(
690             ( $^O eq 'MSWin32' ) ? ( '.svn', '_svn' ) : ('.svn') );
691             },
692             skip_bzr => sub {
693             return Path::Iterator::Rule->new->skip_dirs('.bzr');
694             },
695             skip_hg => sub {
696             return Path::Iterator::Rule->new->skip_dirs('.hg');
697             },
698             skip_darcs => sub {
699             return Path::Iterator::Rule->new->skip_dirs('_darcs');
700             },
701             skip_vcs => sub {
702             return Path::Iterator::Rule->new->skip_dirs(qw/.git .bzr .hg _darcs CVS RCS/)
703             ->skip_svn->not_name( qr/\.\#$/, qr/,v$/ );
704             },
705             );
706              
707             while ( my ( $name, $coderef ) = each %vcs_rules ) {
708             __PACKAGE__->add_helper( $name, $coderef, 1 ); # don't create not_*
709             }
710              
711             # perl rules adapted from File::Find::Rule::Perl
712             my %perl_rules = (
713             perl_module => sub { return Path::Iterator::Rule->new->file->name('*.pm') },
714             perl_pod => sub { return Path::Iterator::Rule->new->file->name('*.pod') },
715             perl_test => sub { return Path::Iterator::Rule->new->file->name('*.t') },
716             perl_installer => sub {
717             return Path::Iterator::Rule->new->file->name( 'Makefile.PL', 'Build.PL' );
718             },
719             perl_script => sub {
720             return Path::Iterator::Rule->new->file->or(
721             Path::Iterator::Rule->new->name('*.pl'),
722             Path::Iterator::Rule->new->shebang(qr/#!.*\bperl\b/),
723             );
724             },
725             perl_file => sub {
726             return Path::Iterator::Rule->new->or(
727             Path::Iterator::Rule->new->perl_module, Path::Iterator::Rule->new->perl_pod,
728             Path::Iterator::Rule->new->perl_test, Path::Iterator::Rule->new->perl_installer,
729             Path::Iterator::Rule->new->perl_script,
730             );
731             },
732             );
733              
734             while ( my ( $name, $coderef ) = each %perl_rules ) {
735             __PACKAGE__->add_helper( $name, $coderef );
736             }
737              
738             1;
739              
740              
741             # vim: ts=4 sts=4 sw=4 et:
742              
743             __END__