File Coverage

blib/lib/File/Find/Closures.pm
Criterion Covered Total %
statement 102 125 81.6
branch 43 66 65.1
condition n/a
subroutine 47 58 81.0
pod 17 17 100.0
total 209 266 78.5


line stmt bran cond sub pod time code
1 14     14   4197445 use 5.008;
  14         51  
2              
3             package File::Find::Closures;
4 14     14   82 use strict;
  14         33  
  14         366  
5              
6 14     14   66 use warnings;
  14         30  
  14         887  
7 14     14   90 no warnings;
  14         38  
  14         752  
8              
9 14     14   78 use Carp qw(carp croak);
  14         23  
  14         1238  
10 14     14   73 use Exporter qw(import);
  14         24  
  14         561  
11 14     14   87 use File::Basename qw(dirname);
  14         22  
  14         1078  
12 14     14   1445 use File::Spec::Functions qw(canonpath no_upwards);
  14         2336  
  14         831  
13 14     14   6707 use UNIVERSAL;
  14         228  
  14         25823  
14              
15             our $VERSION = '1.118';
16              
17             our @EXPORT_OK = qw(
18             find_by_created_after
19             find_by_created_before
20             find_by_directory_contains
21             find_by_executable
22             find_by_extension
23             find_by_group
24             find_by_max_size
25             find_by_min_size
26             find_by_modified_after
27             find_by_modified_before
28             find_by_name
29             find_by_owner
30             find_by_regex
31             find_by_umask
32             find_by_writeable
33             find_by_zero_size
34             find_regular_files
35             );
36              
37             our %EXPORT_TAGS = (
38             all => \@EXPORT_OK
39             );
40              
41 1     1   1277 sub _unimplemented { croak "Unimplemented function!" }
42              
43             =encoding utf8
44              
45             =head1 NAME
46              
47             File::Find::Closures - functions you can use with File::Find
48              
49             =head1 SYNOPSIS
50              
51             use File::Find;
52             use File::Find::Closures qw(:all);
53              
54             my( $wanted, $list_reporter ) = find_by_name( qw(README) );
55              
56             File::Find::find( $wanted, @directories );
57             File::Find::find( { wanted => $wanted, ... }, @directories );
58              
59             my @readmes = $list_reporter->();
60              
61             =head1 DESCRIPTION
62              
63             I wrote this module as an example of both using closures and using
64             L. Students are always asking me what closures are good
65             for, and here's some examples. The functions mostly stand alone (i.e.
66             they don't need the rest of the module), so rather than creating a
67             dependency in your code, just lift the parts you want).
68              
69             When I use L, I have two headaches—coming up with the
70             C<\&wanted function> to pass to C, and acculumating the files.
71              
72             This module provides the C<\&wanted> functions as a closures that I can
73             pass directly to C. Actually, for each pre-made closure, I
74             provide a closure to access the list of files too, so I don't have to
75             create a new array to hold the results.
76              
77             The filenames are the full path to the file as reported by L.
78              
79             Unless otherwise noted, the reporter closure returns a list of the
80             filenames in list context and an anonymous array that is a copy (not a
81             reference) of the original list. The filenames have been normalized
82             by C unless otherwise noted. The list of files
83             has been processed by C so that "." and ".." (or
84             their equivalents) do not show up in the list.
85              
86             =head2 The closure factories
87              
88             Each factory returns two closures. The first one is for C,
89             and the second one is the reporter.
90              
91             =over 4
92              
93             =item find_by_created_after( EPOCH_TIME );
94              
95             Find files created after EPOCH_TIME, which is in seconds since
96             the local epoch (I may need to adjust this for some operating
97             systems).
98              
99             =cut
100              
101             sub find_by_created_after {
102 1     1 1 1436 return _find_by_stat_part_greaterthan( $_[0], 10 );
103             }
104              
105             =item find_by_created_before( EPOCH_TIME );
106              
107             Find files created before EPOCH_TIME, which is in seconds since
108             the local epoch (I may need to adjust this for some operating
109             systems).
110              
111             =cut
112              
113             sub find_by_created_before {
114 1     1 1 1384 return _find_by_stat_part_lessthan( $_[0], 10 );
115             }
116              
117             =item find_by_directory_contains( @names );
118              
119             Find directories which contain files with the same name
120             as any of the values in C<@names>.
121              
122             =cut
123              
124             sub find_by_directory_contains {
125 2     2 1 2670 my @contains = @_;
126 2         6 my %contains = map { $_, 1 } @contains;
  2         11  
127              
128 2         4 my %files = ();
129              
130             sub {
131 158 100   158   7089 return unless exists $contains{$_};
132 2         96 my $dir = dirname( canonpath( $File::Find::name ) );
133              
134 2         35 $files{ $dir }++;
135             },
136              
137              
138 4 100   4   35 sub { wantarray ? ( keys %files ) : [ keys %files ] }
139 2         16 }
140              
141             =item find_by_executable();
142              
143             Find files that are executable. This may not work on some operating
144             systems (like Windows) unless someone can provide me with an
145             alternate version.
146              
147             =cut
148              
149             sub find_by_executable {
150 0     0 1 0 my @files = ();
151 0 0   0   0 sub { push @files, canonpath( $File::Find::name ) if -x },
152 0 0   0   0 sub { wantarray ? @files : [ @files ] }
153 0         0 }
154              
155             =item find_by_extension( EXTENSIONS )
156              
157             This function removes any leading C<.> from each value in EXTENSIONS,
158             so these are the same:
159              
160             my( $finder, $reporter ) = find_by_extension( 't' );
161             my( $finder, $reporter ) = find_by_extension( '.t' );
162              
163             Internal dots are left alone:
164              
165             my( $finder, $reporter ) = find_by_extension( 'tar.gz' );
166              
167             =cut
168              
169             sub find_by_extension {
170 5     5 1 17214 my @files = ();
171 5         9 my $pattern = join '|', map { my $s = $_; $s =~ s/\A\.//; quotemeta($s) } @_;
  6         6  
  6         11  
  6         17  
172             sub {
173 143 100   143   7428 push @files, canonpath( $File::Find::name ) if m/\.(?:$pattern)\z/;
174             },
175 10 100   10   51 sub { wantarray ? @files : [ @files ] }
176 5         22 }
177              
178             =item find_by_group( GROUP_NAME | GROUP_GID );
179              
180             Find files that are owned by the owner with the name GROUP_NAME.
181             You can also use the group's GID.
182              
183             =cut
184              
185             sub find_by_group {
186 0     0 1 0 my $id = getgrnam( $_[0] );
187 0 0       0 $id = $_ unless defined( $id );
188              
189 0 0       0 unless( $id =~ /\d+/ ) {
190 0         0 carp "Gid must be numeric or a valid system user name";
191             }
192              
193 0         0 return _find_by_stat_part_equal( $id, 5 );
194             }
195              
196             =item find_by_max_size( SIZE );
197              
198             Find files whose size is equal to or less than SIZE bytes.
199              
200             =cut
201              
202             sub find_by_max_size {
203 1     1 1 833 my $min = shift;
204              
205 1         2 my @files = ();
206              
207 92 100   92   5944 sub { push @files, canonpath( $File::Find::name ) if -s $_ <= $min },
208 2 100   2   17 sub { @files = no_upwards( @files ); wantarray ? @files : [ @files ] }
  2         56  
209 1         6 }
210              
211             =item find_by_min_size( SIZE );
212              
213             Find files whose size is equal to or greater than SIZE bytes.
214              
215             =cut
216              
217             sub find_by_min_size {
218 1     1 1 1040 my $min = shift;
219              
220 1         2 my @files = ();
221              
222 96 100   96   12139 sub { push @files, canonpath( $File::Find::name ) if -s $_ >= $min },
223 2 100   2   16 sub { @files = no_upwards( @files ); wantarray ? @files : [ @files ] }
  2         59  
224 1         9 }
225              
226             =item find_by_modified_after( EPOCH_TIME );
227              
228             Find files modified after EPOCH_TIME, which is in seconds since
229             the local epoch (I may need to adjust this for some operating
230             systems).
231              
232             =cut
233              
234             sub find_by_modified_after {
235 1     1 1 1464 return _find_by_stat_part_greaterthan( $_[0], 9 );
236             }
237              
238             =item find_by_modified_before( EPOCH_TIME );
239              
240             Find files modified before EPOCH_TIME, which is in seconds since
241             the local epoch (I may need to adjust this for some operating
242             systems).
243              
244             =cut
245              
246             sub find_by_modified_before {
247 1     1 1 1327 return _find_by_stat_part_lessthan( $_[0], 9 );
248             }
249              
250             =item find_by_name( @names );
251              
252             Find files with the names in C<@names>. The result is the name returned
253             by C<$File::Find::name normalized> by C.
254              
255             In list context, it returns the list of files. In scalar context,
256             it returns an anonymous array.
257              
258             This function does not use C, so if you ask for "." or "..",
259             that's what you get.
260              
261             =cut
262              
263             sub find_by_name {
264 2     2 1 9461 my %hash = map { $_, 1 } @_;
  2         8  
265 2         3 my @files = ();
266              
267 200 100   200   8850 sub { push @files, canonpath( $File::Find::name ) if exists $hash{$_} },
268 4 100   4   28 sub { wantarray ? @files : [ @files ] }
269 2         13 }
270              
271             =item find_by_owner( OWNER_NAME | OWNER_UID );
272              
273             Find files that are owned by the owner with the name OWNER_NAME.
274             You can also use the owner's UID.
275              
276             =cut
277              
278             sub find_by_owner {
279 0     0 1 0 my $id = getpwnam($_[0]);
280 0 0       0 $id = $_ unless defined($id);
281              
282 0 0       0 unless( $id =~ /\d+/ ) {
283 0         0 carp "Uid must be numeric of a valid system user name";
284             }
285              
286 0         0 return _find_by_stat_part_equal( $id, 4 );
287             }
288              
289             =item find_by_regex( REGEX );
290              
291             Find files whose name match REGEX.
292              
293             This function does not use no_updirs, so if you ask for "." or "..",
294             that's what you get.
295              
296             =cut
297              
298             sub find_by_regex {
299 2     2 1 11550 require File::Spec::Functions;
300 2         9 require Carp;
301 2         7 require UNIVERSAL;
302              
303 2         5 my $regex = shift;
304              
305 2 100       21 unless( UNIVERSAL::isa( $regex, ref qr// ) ) {
306 1         199 croak "Argument must be a regular expression";
307             }
308              
309 1         4 my @files = ();
310              
311 108 100   108   6369 sub { push @files,
312             File::Spec::Functions::canonpath( $File::Find::name ) if m/$regex/ },
313 2 100   2   19 sub { wantarray ? @files : [ @files ] }
314 1         14 }
315              
316             =item find_regular_files();
317              
318             Find all regular files.
319              
320             =cut
321              
322             sub find_regular_files {
323 2     2 1 9058 my @files = ();
324              
325 21 100   21   2912 sub { push @files, canonpath( $File::Find::name ) if -f $_ },
326 4 100   4   1139 sub { @files = no_upwards( @files ); wantarray ? @files : [ @files ] }
  4         50  
327 2         12 }
328              
329             =item find_by_umask( UMASK );
330              
331             Find files that fit the umask UMASK. The files will not have those
332             permissions.
333              
334             =cut
335              
336             sub find_by_umask {
337 0     0 1 0 my ($mask) = @_;
338              
339 0         0 my @files;
340              
341 0 0   0   0 sub { push @files, canonpath( $File::Find::name )
342             if ((stat($_))[2] & $mask) == 0},
343 0 0   0   0 sub { wantarray ? @files : [ @files ] }
344 0         0 }
345              
346             =item find_by_zero_size();
347              
348             Find files whose size is equal to 0 bytes.
349              
350             =cut
351              
352             sub find_by_zero_size {
353 1     1 1 1119 my $min = shift;
354              
355 1         2 my @files = ();
356              
357 116 100   116   7152 sub { push @files, canonpath( $File::Find::name ) if -s $_ == 0 },
358 2 100   2   16 sub { @files = no_upwards( @files ); wantarray ? @files : [ @files ] }
  2         51  
359 1         9 }
360              
361             =item find_by_writeable();
362              
363             Find files that are writable. This may not work on some operating
364             systems (like Windows) unless someone can provide me with an
365             alternate version.
366              
367             =cut
368              
369             sub find_by_writeable {
370 0     0 1 0 my @files = ();
371 0 0   0   0 sub { push @files, canonpath( $File::Find::name )
372             if -w },
373 0 0   0   0 sub { wantarray ? @files : [ @files ] }
374 0         0 }
375              
376             sub _find_by_stat_part_equal {
377 1     1   1565 my ($value, $stat_part) = @_;
378              
379 1         22 my @files;
380              
381 112 100   112   4842 sub { push @files, canonpath( $File::Find::name )
382             if (stat($_))[$stat_part] == $value },
383 2 50   2   2235 sub { wantarray ? @files : [ @files ] }
384 1         10 }
385              
386             sub _find_by_stat_part_lessthan {
387 3     3   1466 my ($value, $stat_part) = @_;
388              
389 3         8 my @files;
390              
391 336 100   336   18312 sub { push @files, canonpath( $File::Find::name )
392             if (stat($_))[$stat_part] < $value },
393 6 50   6   5928 sub { wantarray ? @files : [ @files ] }
394 3         25 }
395              
396             sub _find_by_stat_part_greaterthan {
397 3     3   1428 my ($value, $stat_part) = @_;
398              
399 3         7 my @files;
400              
401 336 100   336   18272 sub { push @files, canonpath( $File::Find::name )
402             if (stat($_))[$stat_part] > $value },
403 6 50   6   5701 sub { wantarray ? @files : [ @files ] }
404 3         26 }
405              
406              
407             =back
408              
409             =head1 ADD A CLOSURE
410              
411             I want to add as many of these little functions as I can, so please
412             send me ones that you create!
413              
414             You can follow the examples in the source code, but here is how you
415             should write your closures.
416              
417             You need to provide both closures. Start of with the basic subroutine
418             stub to do this. Create a lexical array in the scope of the subroutine.
419             The two closures will share this variable. Create two closures: one
420             of give to C and one to access the lexical array.
421              
422             sub find_by_foo {
423             my @args = @_;
424              
425             my @found = ();
426              
427             my $finder = sub { push @found, $File::Find::name if ... };
428             my $reporter = sub { @found };
429              
430             return( $finder, $reporter );
431             }
432              
433             The filename should be the full path to the file that you get
434             from C<$File::Find::name>, unless you are doing something wierd,
435             like C.
436              
437             Once you have something, send it to me at C<< >>. You
438             must release your code under the Perl Artistic License.
439              
440             =head1 TO DO
441              
442             * more functions!
443              
444             * need input on how things like mod times work on other operating
445             systems
446              
447             =head1 SEE ALSO
448              
449             L
450              
451             Randal Schwartz's L, which does the same task but
452             differently.
453              
454             =head1 SOURCE AVAILABILITY
455              
456             This module is in Github:
457              
458             https://github.com/briandfoy/file-find-closures.git
459              
460             =head1 AUTHOR
461              
462             brian d foy, C<< >>
463              
464             Some functions implemented by Nathan Wagner, C<< >>
465              
466             =head1 COPYRIGHT AND LICENSE
467              
468             Copyright © 2004-2025, brian d foy . All rights reserved.
469              
470             You may redistribute this under the same terms as the Artistic License
471             2.0.
472              
473             =cut
474              
475             "Kanga and Baby Roo Come to the Forest";