File Coverage

blib/lib/Path/Iterator/Rule.pm
Criterion Covered Total %
statement 261 279 93.5
branch 118 132 89.3
condition 39 43 90.7
subroutine 49 49 100.0
pod 11 12 91.6
total 478 515 92.8


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