File Coverage

blib/lib/Module/Finder.pm
Criterion Covered Total %
statement 143 160 89.3
branch 42 56 75.0
condition 4 9 44.4
subroutine 22 25 88.0
pod 7 7 100.0
total 218 257 84.8


line stmt bran cond sub pod time code
1             package Module::Finder;
2             $VERSION = v0.1.5;
3              
4 3     3   98317 use warnings;
  3         10  
  3         117  
5 3     3   18 use strict;
  3         6  
  3         193  
6 3     3   19 use Carp;
  3         11  
  3         397  
7              
8 3     3   26 use File::Find ();
  3         6  
  3         67  
9 3     3   16 use File::Spec ();
  3         6  
  3         79  
10 3     3   15 use constant {fs => 'File::Spec'};
  3         5  
  3         355  
11              
12             BEGIN {
13             package Module::Finder::Info;
14 3     3   16 use File::Spec ();
  3         6  
  3         76  
15 3     3   13 use constant {fs => 'File::Spec'};
  3         6  
  3         133  
16              
17 3     3   4073 use Class::Accessor::Classy;
  3         19201  
  3         26  
18 3     3   1501 with 'new';
19             #with 'clone'; # todo for C::A::C ?
20 3         326 ro 'filename'; # absolute path
21 3         1005 ro 'module_path'; # relative to search directory (part 2 of filename)
22 3         1069 ro 'inc_path'; # other part of filename
23 3         818 ro 'module_name'; # My::Module::Name
24 3     3   1549 no Class::Accessor::Classy;
  3         8  
  3         13  
25              
26             sub new {
27 88     88   119 my $class = shift;
28 88         323 my $self = $class->SUPER::new(@_);
29 88         2226 $self->{inc_path} = fs->rel2abs($self->{inc_path});
30 88         860 $self->{filename} = fs->catfile(
31             $self->{inc_path},
32             $self->{module_path}
33             );
34 88         250 return($self);
35             } # end sub new
36             } # end Module::Finder::Info package
37              
38             =head1 NAME
39              
40             Module::Finder - find and query modules in @INC and/or elsewhere
41              
42             =head1 SYNOPSIS
43              
44             use Module::Finder;
45             my $finder = Module::Finder->new(
46             dirs => ['/usr/local/junk/', '/junk/', @INC],
47             paths => {
48             'Module::Name::Prefix' => '-', # no recursion - just *.pm
49             'This::Path' => '-/-', # only This/Path/*/*.pm
50             'My' => '*', # everything below My/
51             },
52             );
53              
54             # dirs searches @INC only if it is blank
55              
56             # the first request will cache search results
57             my @modnames = $finder->modules;
58              
59             my @modinfos = $finder->module_infos;
60              
61             my $info = $finder->module_info('My::Found');
62              
63             # if you're creating/installing code, you might want to rescan
64             $finder->reset;
65              
66             =cut
67              
68 3     3   1006 use Class::Accessor::Classy;
  3         5  
  3         14  
69             with 'new';
70             ro 'paths';
71             ro 'name';
72 3     3   678 no Class::Accessor::Classy;
  3         15  
  3         12  
73              
74             =head2 new
75              
76             my $finder = Module::Finder->new(%args);
77              
78             =over
79              
80             =item globs
81              
82             This isn't the same as shell glob syntax. These globs say how deep (or
83             not) you want to look in a given path and whether you want to pickup
84             modules that appear along the way. A list of shell glob equivalents
85             follows each one.
86              
87             / just recurse (*, */*, */*/*, ...)
88             + just this directory (*)
89             -/+ only one level down (*/*)
90             -/-/+ two levels down (*/*/*)
91             -/+/+ one and two levels down (*/*, */*/*)
92             +/+/+ zero thru two levels down (*, */*, */*/*)
93              
94             If the glob spec is more that just "+", the trailing plus (which is
95             required to make sense) may be omitted (e.g. '-/+' and '-/' are
96             equivalent.)
97              
98             =back
99              
100             =cut
101              
102             sub new {
103 23     23 1 78308 my $class = shift;
104 23 50       95 (@_ % 2) and croak('odd number of elements in argument list');
105 23         77 my %args = @_;
106              
107 23         81 my $self = {%args};
108 23         66 bless($self, $class);
109 23         70 $self->reset;
110 23         76 return($self);
111             } # end subroutine new definition
112             ########################################################################
113              
114             =head2 _find
115              
116             $finder->_find;
117              
118             =cut
119              
120             sub _find {
121 25     25   33 my $self = shift;
122              
123 25 100       68 exists($self->{_module_infos}) and return(%{$self->{_module_infos}});
  2         23  
124 23         257 my $infos = $self->{_module_infos} = {};
125 23         72 my @lookdirs = $self->_which_dirs;
126 23         48 foreach my $look (@lookdirs) {
127 36         73 my ($dir, $part, $nglob) = @$look;
128 36 50       750 (-d $dir) or next;
129 36         222 my $search_in = fs->catdir($dir, $part);
130 36 100       114 if(my @globs = $self->_glob_parse($nglob)) {
131 25         659 my $ret_dir = fs->rel2abs(fs->curdir);
132             #warn "return to $ret_dir";
133 25 50       346 chdir($dir) or die "cannot be in $dir";
134             # things should be fairly sane once we're in the libdir
135             # (famous last words?)
136 25         44 foreach my $glob (map({$_, $_ . 'c'} @globs)) {
  28         97  
137 56         260 my $look = join('/', fs->splitdir($part), $glob);
138 56         2639 foreach my $modpath (glob($look)) {
139 48 100       1025 (-e $modpath) or next;
140 35         199 my $modname = join('::', fs->splitdir($modpath));
141 35         186 $modname =~ s/\.pmc?$//;
142             # TODO should I?
143             # if(($modpath =~ m/\.pmc$/) and
144             # $infos->{$modname} and
145             # ($infos->{$modname}->inc_path eq $dir)) {
146             # next;
147             # }
148 35         108 my $obj = Module::Finder::Info->new(
149             module_path => $modpath,
150             inc_path => '.', #$dir, # will get absolute in new()
151             module_name => $modname,
152             );
153 35   50     189 $infos->{$modname} ||= [];
154 35         48 push(@{$infos->{$modname}}, $obj);
  35         140  
155             }
156             }
157 25 50       487 chdir($ret_dir) or die "ack";
158             }
159             else {
160             my $wanted = sub {
161 83     83   6123 my $modpath = fs->abs2rel($_, $dir);
162 83 100       3634 return if($modpath eq $part);
163             #warn "look $modpath";
164 77 100       2586 m/\.pmc?$/ or return;
165 53         232 my $modname = join('::', fs->splitdir($modpath));
166 53         200 $modname =~ s/\.pmc?$//;
167 53         173 my $obj = Module::Finder::Info->new(
168             module_path => $modpath,
169             inc_path => $dir,
170             module_name => $modname,
171             );
172 53   50     281 $infos->{$modname} ||= [];
173 53         65 push(@{$infos->{$modname}}, $obj);
  53         1109  
174 11         67 };
175             # do the find
176 11 50       222 (-e $search_in) or next;
177 11         868 File::Find::find({wanted => $wanted, no_chdir => 1}, $search_in);
178             }
179             }
180              
181 23         168 return(%$infos);
182             } # end subroutine _find definition
183             ########################################################################
184              
185             =head2 _which_dirs
186              
187             my @dirs = $self->_which_dirs;
188              
189             =cut
190              
191             sub _which_dirs {
192 23     23   26 my $self = shift;
193              
194 23         28 my @dirs = @{$self->{dirs}};
  23         66  
195 23         824 my $paths = $self->paths;
196 23         696 my $name = $self->name;
197 23 100       122 if(defined($name)) { # make the glob be that filename
198 4         11 $name =~ s#::#/#g;
199 4         8 $name .= '.pm';
200             }
201              
202 23 100       60 unless($paths) {
203 5 100       10 return(map({[$_, '', ($name ? $name : '/')]} @dirs));
  5         34  
204             }
205              
206             # TODO maybe bail if we get here and have $name
207             # e.g. $path + $name is just $path::$name
208             # somewhat sensible for multiple paths, but wouldn't that be a 'names'
209             # thing instead?
210              
211 18         29 my @look_dirs;
212 18         37 foreach my $dir (@dirs) {
213 52         124 foreach my $path (keys(%$paths)) {
214             #warn "check for $dir/$path";
215 55         429 my $pathdir = fs->catdir(split(/::/, $path));
216 55 100       1696 if(-d fs->catdir($dir, $pathdir)) {
217 31 100       200 push(@look_dirs,
218             [
219             $dir,
220             $pathdir,
221             ($name ? $name : $paths->{$path})
222             ]
223             );
224             }
225             #else {warn `pwd` ."-- no $dir/$path"};
226             }
227             }
228 18         70 return(@look_dirs);
229             } # end subroutine _which_dirs definition
230             ########################################################################
231              
232             =head2 _glob_parse
233              
234             my $glob = $self->_glob_parse($glob);
235              
236             =cut
237              
238             sub _glob_parse {
239 46     46   10000 my $self = shift;
240 46         64 my ($glob) = @_;
241 46 50 33     240 (defined($glob) and length($glob)) or croak('glob must be defined');
242 46 100       127 ($glob eq '/') and return; # recurse
243 34 100       114 ($glob =~ m/\.pm$/) and return($glob); # explicit
244 25         77 my @parts = split(/\//, $glob, -1);
245 25 100       63 if($parts[-1] ne '') {
246 18 50       44 ($parts[-1] eq '+') or
247             croak("explicit trailing glob part must be + not '$parts[-1]'");
248             }
249             else {
250 7         11 $parts[-1] = '+';
251             }
252 25 100       96 (1 == @parts) and return('*.pm');
253 11         15 my @globs;
254 11         13 my $base = '';
255 11         20 foreach my $part (@parts) {
256 28         64 $base .= '/*';
257 28 100       52 if($part eq '+') {
    50          
258 20         44 push(@globs, $base);
259             }
260             elsif($part eq '-') {
261             # pass
262             }
263             else {
264 0         0 croak "'$part' is not a valid glob segment";
265             }
266             }
267 11         19 foreach my $glob (@globs) {
268 20         58 $glob =~ s#^/##;
269 20         38 $glob .= '.pm';
270             }
271 11         44 return(@globs);
272             } # end subroutine _glob_parse definition
273             ########################################################################
274              
275             =head2 reset
276              
277             $finder->reset;
278              
279             =cut
280              
281             sub reset {
282 23     23 1 35 my $self = shift;
283              
284 23 100       88 if(my $dirs = $self->{dirs}) {
285 21 50 50     93 ((ref($dirs) || '') eq 'ARRAY') or
286             croak("'dirs' argument must be an array ref");
287 21         34 my %seen;
288 21 50       44 @$dirs = grep({exists($seen{$_}) ? 0 : ($seen{$_} = 1)} @$dirs);
  35         185  
289             }
290             else {
291 2         15 $self->{dirs} = [@INC];
292             }
293              
294 23         73 delete($self->{_module_infos});
295             } # end subroutine reset definition
296             ########################################################################
297              
298             =head2 modules
299              
300             my @modnames = $finder->modules;
301              
302             =cut
303              
304             sub modules {
305 22     22 1 1989 my $self = shift;
306              
307 22         93 my %infos = $self->_find;
308 22         160 return(keys(%infos));
309             } # end subroutine modules definition
310             ########################################################################
311              
312             =head2 module_infos
313              
314             Returns the info for the first hit of every found module.
315              
316             my %modinfos = $finder->module_infos;
317              
318             =cut
319              
320             sub module_infos {
321 3     3 1 17 my $self = shift;
322 3         9 my %infos = $self->_find;
323 3         9 return(map({$_ => $infos{$_}[0]} keys(%infos)));
  6         22  
324             } # end subroutine module_infos definition
325             ########################################################################
326              
327             =head2 all_module_infos
328              
329             Returns the info for all hits of every found module. Each element of
330             the returned hash will be an array ref with one or more info objects.
331              
332             my %modinfos = $finder->all_module_infos;
333              
334             =cut
335              
336             sub all_module_infos {
337 0     0 1   my $self = shift;
338 0           my ($module) = @_;
339 0           my %infos = $self->_find;
340 0           return(%infos);
341             } # end subroutine all_module_infos definition
342             ########################################################################
343              
344             =head2 module_info
345              
346             my $info = $finder->module_info('My::Found');
347              
348             =cut
349              
350             sub module_info {
351 0     0 1   my $self = shift;
352 0           my ($module) = @_;
353 0           my %infos = $self->_find;
354 0 0         exists($infos{$module}) or return;
355 0           my $inf = $infos{$module};
356 0           return($inf->[0]);
357             } # end subroutine module_info definition
358             ########################################################################
359              
360             =head2 all_module_info
361              
362             my @info = $finder->all_module_info('My::Found');
363              
364             =cut
365              
366             sub all_module_info {
367 0     0 1   my $self = shift;
368 0           my ($module) = @_;
369 0           my %infos = $self->_find;
370 0 0         exists($infos{$module}) or return;
371 0           my $inf = $infos{$module};
372 0           return(@$inf);
373             } # end subroutine all_module_info definition
374             ########################################################################
375              
376              
377             =head1 AUTHOR
378              
379             Eric Wilhelm
380              
381             http://scratchcomputing.com/
382              
383             =head1 BUGS
384              
385             If you found this module on CPAN, please report any bugs or feature
386             requests through the web interface at L. I will be
387             notified, and then you'll automatically be notified of progress on your
388             bug as I make changes.
389              
390             If you pulled this development version from my /svn/, please contact me
391             directly.
392              
393             =head1 COPYRIGHT
394              
395             Copyright (C) 2006 Eric L. Wilhelm, All Rights Reserved.
396              
397             =head1 NO WARRANTY
398              
399             Absolutely, positively NO WARRANTY, neither express or implied, is
400             offered with this software. You use this software at your own risk. In
401             case of loss, no person or entity owes you anything whatseover. You
402             have been warned.
403              
404             =head1 LICENSE
405              
406             This program is free software; you can redistribute it and/or modify it
407             under the same terms as Perl itself.
408              
409             =head1 SEE ALSO
410              
411             This module is inspired and/or informed by the following. Maybe they do
412             what you want.
413              
414             File::Find
415             File::Finder
416             Module::Find
417             Module::Require
418             Module::Locate
419             Module::Pluggable::Object
420             Module::List
421              
422             =cut
423              
424             # vi:ts=2:sw=2:et:sta
425             1;