File Coverage

blib/lib/File/Find/Repository.pm
Criterion Covered Total %
statement 23 25 92.0
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 32 34 94.1


line stmt bran cond sub pod time code
1              
2             package File::Find::Repository ;
3              
4 1     1   34905 use strict;
  1         3  
  1         250  
5 1     1   5 use warnings ;
  1         3  
  1         133  
6              
7             BEGIN
8             {
9 1     1   7 use vars qw ($VERSION);
  1         5  
  1         79  
10 1     1   17 $VERSION = '0.03';
11             }
12              
13             #-------------------------------------------------------------------------------
14              
15 1     1   4 use Carp qw(carp croak confess) ;
  1         2  
  1         242  
16 1     1   855 use English qw( -no_match_vars ) ;
  1         4363  
  1         4  
17              
18 1     1   1106 use Readonly ;
  1         2820  
  1         66  
19             Readonly my $EMPTY_STRING => q{} ;
20              
21 1     1   6 use File::Spec;
  1         2  
  1         17  
22 1     1   389 use Tie::Hash::Indexed ;
  0            
  0            
23              
24             #-------------------------------------------------------------------------------
25              
26             =head1 NAME
27              
28             File::Find::Repository - Find files in your repositories.
29              
30             =head1 SYNOPSIS
31              
32             use File::Find::Repository ;
33            
34             my $locator = new File::Find::Repository
35             (
36             NAME => 'name you want to see when messages are displayed',
37            
38             REPOSITORIES =>
39             [
40             'path',
41             \&sub,
42             ...
43             ],
44             ) ;
45            
46             # single scalar argument
47             my $located_file = $locator->Find($file_to_locate) ;
48            
49             # multiple arguments
50             my $located_files = $locator->Find
51             (
52             FILES => [...],
53             REPOSITORIES => ['path', \&sub, ...],
54             VERBOSE => 1,
55             ) ;
56              
57             =head1 DESCRIPTION
58              
59             This module will find files in a set of repositories.
60              
61             =head1 DOCUMENTATION
62              
63             This module will try to locate a file in the repositories you define. The repositories are either
64             a string representing a local filesystem path or a sub.
65              
66             When locating a file, multiple file match can occur (each in a different repository). The default behavior is
67             to return the first match.
68              
69             You can customize the behavior of the search with two callbacks.
70              
71             B will be called to allow you to add relevant information to the files that have been located.
72              
73             B will be called to let you decide which found files is returned.
74              
75             =head3 Advanced example
76              
77             This module was extracted from B, a build system, made generic and will be re-integrated in B
78             in next version. Here is how it could be used for a more advanced repository search.
79              
80             Let's imagine we have multiple matches for an object file in our repositories. The goal here is to not rebuild the object
81             file. Selecting the first object file in the list would be too naive so we define a B callback that will select
82             the most appropriate. In this case, it might involve looking in the object file digest and/or check what configuration was
83             used when the object file was build.
84              
85             my $located_file = $locator->Find
86             (
87             FILES => [$file_to_locate],
88             REPOSITORIES => [$build_directory, @repositories],
89             WHICH => FIND_NODE_WITH_DEPENDENCIES($information_needed_to_select_the_found_file)
90            
91             # bote that FIND_NODE_WITH_DEPENDENCIES returns a sub reference
92             ) ;
93            
94             $located_file ||= "$build_directory/$located_file" ;
95            
96              
97             =head1 SUBROUTINES/METHODS
98              
99             =cut
100              
101              
102             #-------------------------------------------------------------------------------
103              
104             sub new
105             {
106              
107             =head2 new
108              
109             Create a File::Find::Repository .
110              
111             my $locator = new File::Find::Repository
112             (
113             # all arguments are optional
114            
115             NAME => 'name you want to see when messages are displayed',
116            
117             REPOSITORIES =>
118             [
119             'path',
120             \&sub,
121             ...
122             ],
123            
124             INTERACTION =>
125             {
126             INFO = \&OnMyTerminal,
127             WARN = \&WithBlinkingRedLetters,
128             DIE = \&QuickAndPainless,
129             }
130            
131             VERBOSE => 1,
132             FULL_INFO => \&File::Find::Repository::TIME_AND_SIZE,
133             WHICH => \&File::Find::Repository::FIRST_FOUND,
134             ) ;
135              
136             =head3 Options
137              
138             =over 2
139              
140             =item * NAME
141              
142             Name you want to see when messages are displayed.
143              
144             =item * REPOSITORIES
145              
146             An array reference. The elements are either scalars representing a local filesystem path or a code
147             reference. The code references are passed a single argument, the file to locate, and should either
148             return the located file or undef.
149              
150             This allows you to, for example, to locate the files on servers.
151              
152             =item * INTERACTION
153              
154             Lets you define subs used to interact with the user.
155              
156             INTERACTION =>
157             {
158             INFO => \&sub,
159             WARN => \&sub,
160             DIE => \&sub,
161             }
162              
163             =over 4
164              
165             =item INFO
166              
167             This sub will be used when displaying L information.
168              
169             =item WARN
170              
171             This sub will be used when a warning is displayed.
172              
173             =item DIE
174              
175             Used when an error occurs.
176              
177             =back
178              
179             The functions default to:
180              
181             =over 2
182              
183             =item * INFO => print
184              
185             =item * WARN => Carp::carp
186              
187             =item * DIE => Carp::confess
188              
189             =back
190              
191             =item * VERBOSE
192              
193             When set, informative messages will be displayed.
194              
195             =item * FULL_INFO
196              
197             This is set to a sub ref which is called for all the found files, this allows you to add information.
198             See L for an example.
199              
200             Passed arguments:
201              
202             =over 4
203              
204             =item * the File::Find::Repository object.
205              
206             This is useful when you want to display a message; use the subroutines defined in $object->{INTERACTION}.
207              
208             =item * The file name
209              
210             =item * a hash reference.
211              
212             The found file.
213              
214             =back
215              
216             =item * WHICH
217              
218             By defaults, B will set I to I which
219             return the first file found in the repositories.
220              
221             Define this callback if you wish to return something else, e.g. the newest file or the largest file.
222              
223             I subroutine will be called with these arguments:
224              
225             =over 4
226              
227             =item * the File::Find::Repository object.
228              
229             This is useful when you want to display a message; use the subroutines defined in $object->{INTERACTION}.
230              
231             =item * a hash reference.
232              
233             Containing all the found files, after processing with L. The hash is ordered.
234              
235             =back
236              
237             The subroutine should return one of the array elements or undef. Note that you could also return an element
238             not present in the hash. In this case, a proper documentation of your algorithm will help maintenance.
239              
240             =back
241              
242             =cut
243              
244             my ($invocant, @setup_data) = @_ ;
245              
246             my $class = ref($invocant) || $invocant ;
247             confess 'Invalid constructor call!' unless defined $class ;
248              
249             my $object = {} ;
250              
251             my ($package, $file_name, $line) = caller() ;
252             bless $object, $class ;
253              
254             $object->Setup($package, $file_name, $line, @setup_data) ;
255              
256             return($object) ;
257             }
258              
259             #-------------------------------------------------------------------------------
260              
261             sub Setup
262             {
263              
264             =head2 Setup
265              
266             Helper sub called by new. This is a private sub.
267              
268             =cut
269              
270             my ($object, $package, $file_name, $line, @setup_data) = @_ ;
271              
272             %{$object} =
273             (
274             NAME => "Anonymous created at $file_name:$line",
275             WHICH => \&FIRST_FOUND,
276            
277             @setup_data,
278            
279             AT_FILE => $file_name,
280             AT_LINE => $line,
281             ) ;
282              
283             my $location = "$object->{AT_FILE}:$object->{AT_LINE}" ;
284              
285             $object->{VALID_OPTIONS} =
286             {
287             map{$_ => 1}
288             qw(
289             FILES
290             FULL_INFO
291             INTERACTION
292             REPOSITORIES
293             VERBOSE
294             WHICH
295            
296             AT_FILE
297             AT_LINE
298             )
299             } ;
300              
301             #~ $object->{INTERACTION}{INFO} ||= \&CORE::print ;
302             $object->{INTERACTION}{INFO} ||= sub{print(@_) or croak "Can't print! $!"};
303             $object->{INTERACTION}{WARN} ||= \&Carp::carp ;
304             $object->{INTERACTION}{DIE} ||= \&Carp::confess ;
305              
306             if(defined $object->{REPOSITORIES})
307             {
308             if('ARRAY' ne ref $object->{REPOSITORIES})
309             {
310             $object->{INTERACTION}{DIE}->("$object->{NAME}: REPOSITORIES must be an array reference at '$location'!") ;
311             }
312            
313             for my $repository (@{$object->{REPOSITORIES}})
314             {
315             if(defined $repository)
316             {
317             my $type = ref $repository ;
318            
319             if($EMPTY_STRING ne $type && 'CODE' ne $type)
320             {
321             $object->{INTERACTION}{DIE}->("$object->{NAME}: invalid repository type '$type' at '$location'!") ;
322             }
323             }
324             else
325             {
326             $object->{INTERACTION}{DIE}->("$object->{NAME}: invalid repository [undef] at '$location'!") ;
327             }
328             }
329             }
330            
331             return(1) ;
332             }
333              
334             #-------------------------------------------------------------------------------
335              
336             sub Find
337             { ## no critic (ProhibitExcessComplexity)
338              
339             =head2 Find
340              
341             # single scalar argument
342             my $located_file = $locator->Find($file_to_locate) ;
343            
344             # multiple arguments
345             my $located_files = $locator->Find
346             (
347             FILES => [...],
348            
349             # optional
350             REPOSITORIES => ['path', \&sub, ...],
351             VERBOSE => 1,
352             INTERACTION => { INFO = \&OnMyTerminal,},
353             FULL_INFO => \&File::Find::Repository::TIME_AND_SIZE,
354             WHICH => \&File::Find::Repository::FIRST_FOUND,
355             ) ;
356              
357             =head3 SCALAR calling context
358              
359             Only SCALAR calling context is allowed.
360              
361             =head3 Arguments
362              
363             If a single string argument is passed to Find, a string or undef is returned.
364              
365             If multiple arguments are passed, they will override the object's values for the call duration.
366              
367             Valid arguments:
368              
369             =over 2
370              
371             =item * FILES
372              
373             An array ref with scalar elements. Each element represents a file to locate. The returned value will be an
374             ordered hash reference.
375              
376             =item * AT_FILE and AT_LINE
377              
378             These will be used in the information message and the history information if set. If not set, the values
379             returned by I will be used. B that report the
380             callers location properly.
381              
382             All arguments passed to L, except B are also valid arguments to L.
383              
384             =back
385              
386             =cut
387              
388             my ($self, @arguments) = @_ ;
389              
390             my $single_file_to_find = $EMPTY_STRING ;
391             my ($number_of_arguments) = scalar(@arguments) ;
392              
393             my $location = "$self->{AT_FILE}:$self->{AT_LINE}" ;
394              
395             if($number_of_arguments <= 0)
396             {
397             $self->{INTERACTION}{DIE}->("$self->{NAME}: No argument at '$location'!") ;
398             }
399             elsif($number_of_arguments == 1)
400             {
401             if($EMPTY_STRING eq ref $arguments[0])
402             {
403             $single_file_to_find = $arguments[0] ;
404             @arguments = (FILES => [@arguments]) ;
405             }
406             else
407             {
408             $self->{INTERACTION}{DIE}->("$self->{NAME}: single argument must be scalar at '$location'!") ;
409             }
410             }
411              
412             my %arguments = @arguments ;
413             $self->CheckOptions(\%arguments) ;
414              
415             ## no critic (ProhibitLocalVars ProhibitConditionalDeclarations)
416              
417             local $self->{FILES} = $arguments{FILES} ;
418              
419             local $self->{FULL_INFO} = $arguments{FULL_INFO} if exists $arguments{FULL_INFO} ;
420              
421             local $self->{INTERACTION}{INFO} = $arguments{INTERACTION}{INFO} if exists $arguments{INTERACTION}{INFO} ;
422             local $self->{INTERACTION}{WARN} = $arguments{INTERACTION}{WARN} if exists $arguments{INTERACTION}{WARN} ;
423             local $self->{INTERACTION}{DIE} = $arguments{INTERACTION}{DIE} if exists $arguments{INTERACTION}{DIE} ;
424              
425             local $self->{REPOSITORIES} = $arguments{REPOSITORIES} if exists $arguments{REPOSITORIES} ;
426             local $self->{VERBOSE} = $arguments{VERBOSE} if exists $arguments{VERBOSE} ;
427             local $self->{WHICH} = $arguments{WHICH} if exists $arguments{WHICH} ;
428             local $self->{AT_FILE } = $arguments{AT_FILE } if exists $arguments{AT_FILE } ;
429             local $self->{AT_LINE } = $arguments{AT_LINE } if exists $arguments{AT_LINE };
430              
431             ## use critic
432              
433             $location = "$self->{AT_FILE}:$self->{AT_LINE}" ;
434              
435             if(! defined wantarray)
436             {
437             $self->{INTERACTION}{DIE}->("$self->{NAME}: not called in scalar context at '$location'!") ;
438             }
439            
440             if(wantarray)
441             {
442             $self->{INTERACTION}{DIE}->("$self->{NAME}: not called in scalar context at '$location'!") ;
443             }
444              
445             my %located_files ;
446              
447             for my $file_to_locate (@{$arguments{FILES}})
448             {
449             my $located_files = $self->FindFiles($file_to_locate) ;
450            
451             if(keys %{$located_files})
452             {
453             if($self->{FULL_INFO})
454             {
455             while (my ($file_name, $file) = each %{$located_files})
456             {
457             $self->{FULL_INFO}->($self, $file_to_locate, $file) ;
458             }
459             }
460            
461             $located_files{$file_to_locate} = $self->{WHICH}->($self, $located_files) ;
462             }
463             else
464             {
465             $located_files{$file_to_locate} = undef ;
466             }
467             }
468            
469             if($number_of_arguments == 1)
470             {
471             return($located_files{$single_file_to_find}{FOUND_AT}) ;
472             }
473             else
474             {
475             return(\%located_files) ;
476             }
477             }
478              
479             #-------------------------------------------------------------------------------
480              
481             sub FindFiles
482             {
483              
484             =head2 FindFiles
485              
486             This is a private sub. Do not use directly.
487              
488             Finds all the files in the repositories.
489              
490             =cut
491              
492             my ($self, $file_to_locate) = @_ ;
493              
494             my $location = "$self->{AT_FILE}:$self->{AT_LINE}" ;
495              
496             tie my %files_found, 'Tie::Hash::Indexed' ; ## no critic
497              
498             if(File::Spec->file_name_is_absolute($file_to_locate))
499             {
500             $self->{INTERACTION}{WARN}->("$self->{NAME}: passed absolute file path '$file_to_locate' at $location.\n") ;
501             }
502             else
503             {
504             $self->{INTERACTION}{INFO}->("Searching for '$file_to_locate':\n") if $self->{VERBOSE} ;
505            
506             for my $source_directory (@{$self->{REPOSITORIES}})
507             {
508             my $searched_file = "$source_directory/$file_to_locate" ;
509             my $file_found ;
510            
511             my $type = ref $source_directory ;
512            
513             if($EMPTY_STRING eq $type)
514             {
515             $file_found = $searched_file if( -e $searched_file) ;
516             }
517             elsif('CODE' eq $type)
518             {
519             $file_found = $source_directory->($file_to_locate);
520             }
521             else
522             {
523             $self->{INTERACTION}{DIE}->("$self->{NAME}: Invalid repository type '$type' at $location.\n") ;
524             }
525            
526             if(defined $file_found)
527             {
528             $files_found{$file_found} = {FOUND_AT => $file_found, EXISTS => (-e $file_found)} ;
529             $self->{INTERACTION}{INFO}->(" Found in '$source_directory'\n.") if $self->{VERBOSE} ;
530             }
531             else
532             {
533             $self->{INTERACTION}{INFO}->(" Not found in '$source_directory'.\n") if $self->{VERBOSE} ;
534             }
535             }
536             }
537            
538             return(\%files_found) ;
539             }
540              
541             #-------------------------------------------------------------------------------
542              
543             sub CheckOptions
544             {
545              
546             =head2 CheckOptions
547              
548             Verifies the options passed to the members of this class. Calls B<{INTERACTION}{DIE}> in case
549             of error. This shall not be used directly.
550              
551             =cut
552              
553             my ($self, $options) = @_ ;
554             my $location = "$self->{AT_FILE}:$self->{AT_LINE}" ;
555              
556             for my $option_name (keys %{$options})
557             {
558             $self->{INTERACTION}{DIE}->("$self->{NAME}: Invalid Option '$option_name' at '$self->{AT_FILE}:$self->{AT_LINE}'!") unless exists $self->{VALID_OPTIONS}{$option_name} ;
559             }
560              
561             if
562             (
563             (defined $options->{AT_FILE} && ! defined $options->{AT_LINE})
564             || (!defined $options->{AT_FILE} && defined $options->{AT_LINE})
565             )
566             {
567             $self->{INTERACTION}{DIE}->("$self->{NAME}: Incomplete option AT_FILE::AT_LINE!") ;
568             }
569              
570             # check we have enough to work with
571             unless(exists $options->{FILES})
572             {
573             $self->{INTERACTION}{DIE}->("$self->{NAME}: No FILES to find at '$location'!") ;
574             }
575            
576             if('ARRAY' ne ref $options->{FILES})
577             {
578             $self->{INTERACTION}{DIE}->("$self->{NAME}: Invalid FILES at '$location'!") ;
579             }
580              
581             if(0 == scalar(@{$options->{FILES}}))
582             {
583             $self->{INTERACTION}{DIE}->("$self->{NAME}: no entries in FILES at '$location'!") ;
584             }
585              
586             return(1) ;
587             }
588              
589             #-------------------------------------------------------------------------------
590              
591             sub FIRST_FOUND
592             {
593              
594             =head2 FIRST_FOUND
595              
596             Returns the first matching file.
597              
598             =cut
599              
600             my ($object, $located_files) = @_ ;
601              
602             my (@keys) = keys %{$located_files} ;
603              
604             return($located_files->{$keys[0]}) ;
605             }
606              
607             #-------------------------------------------------------------------------------
608              
609             sub TIME_AND_SIZE
610             {
611              
612             =head2 TIME_AND_SIZE
613              
614             Adds time and size information to the matched file.
615              
616             =cut
617              
618             my ($object, $file_name, $file) = @_ ;
619              
620             Readonly my $YEAR_1900 => 1900 ;
621             Readonly my $STAT_SIZE => 7 ;
622             Readonly my $STAT_CTIME => 10 ;
623              
624             if($file->{EXISTS})
625             {
626             my ($file_size, undef, undef, $modification_time) = (stat($file->{FOUND_AT}))[$STAT_SIZE..$STAT_CTIME];
627             my ($sec, $min, $hour, $month_day, $month, $year, $week_day, $year_day) = gmtime($modification_time) ;
628             $year += $YEAR_1900 ;
629             $month++ ;
630              
631             $file->{SIZE} = $file_size ;
632             $file->{DATE} =
633             {
634             DAY => $month_day,
635             MONTH => $month,
636             YEAR => $year,
637             HOUR => $hour,
638             MINUTE => $min,
639             SECOND => $sec,
640             };
641             }
642            
643             return(1) ;
644             }
645              
646             #-------------------------------------------------------------------------------
647              
648             1 ;
649              
650             =head1 BUGS AND LIMITATIONS
651              
652             None so far.
653              
654             =head1 AUTHOR
655              
656             Khemir Nadim ibn Hamouda
657             CPAN ID: NKH
658             mailto:nadim@khemir.net
659              
660             =head1 LICENSE AND COPYRIGHT
661              
662             This program is free software; you can redistribute
663             it and/or modify it under the same terms as Perl itself.
664              
665             =head1 SUPPORT
666              
667             You can find documentation for this module with the perldoc command.
668              
669             perldoc File::Find::Repository
670              
671             You can also look for information at:
672              
673             =over 4
674              
675             =item * AnnoCPAN: Annotated CPAN documentation
676              
677             L
678              
679             =item * RT: CPAN's request tracker
680              
681             Please report any bugs or feature requests to L .
682              
683             We will be notified, and then you'll automatically be notified of progress on
684             your bug as we make changes.
685              
686             =item * Search CPAN
687              
688             L
689              
690             =back
691              
692             =head1 SEE ALSO
693              
694             L
695              
696             =cut