File Coverage

blib/lib/PERLANCAR/Module/List.pm
Criterion Covered Total %
statement 89 115 77.3
branch 52 96 54.1
condition 41 62 66.1
subroutine 5 5 100.0
pod 0 2 0.0
total 187 280 66.7


line stmt bran cond sub pod time code
1             package PERLANCAR::Module::List;
2              
3             our $DATE = '2019-09-12'; # DATE
4             our $VERSION = '0.004005'; # VERSION
5              
6             #IFUNBUILT
7             # # use strict 'subs', 'vars';
8             # # use warnings;
9             #END IFUNBUILT
10              
11             my $has_globstar;
12              
13             # do our own exporting to start faster
14             sub import {
15 1     1   8 my $pkg = shift;
16 1         2 my $caller = caller;
17 1         3 for my $sym (@_) {
18 1 50       3 if ($sym eq 'list_modules') { *{"$caller\::$sym"} = \&{$sym} }
  1         1  
  1         81660  
  1         3  
19 0         0 else { die "$sym is not exported!" }
20             }
21             }
22              
23             sub list_modules($$) {
24 12     12 0 25830 my($prefix, $options) = @_;
25 12         23 my $trivial_syntax = $options->{trivial_syntax};
26 12         26 my($root_leaf_rx, $root_notleaf_rx);
27 12         0 my($notroot_leaf_rx, $notroot_notleaf_rx);
28 12 50       24 if($trivial_syntax) {
29 0         0 $root_leaf_rx = $notroot_leaf_rx = qr#:?(?:[^/:]+:)*[^/:]+:?#;
30 0         0 $root_notleaf_rx = $notroot_notleaf_rx =
31             qr#:?(?:[^/:]+:)*[^/:]+#;
32             } else {
33 12         56 $root_leaf_rx = $root_notleaf_rx = qr/[a-zA-Z_][0-9a-zA-Z_]*/;
34 12         35 $notroot_leaf_rx = $notroot_notleaf_rx = qr/[0-9a-zA-Z_]+/;
35             }
36              
37 12         20 my $recurse = $options->{recurse};
38              
39             # filter by wildcard. we cannot do this sooner because wildcard can be put
40             # at the end or at the beginning (e.g. '*::Path') so we still need
41 12         16 my $re_wildcard;
42 12 100       28 if ($options->{wildcard}) {
43 8         43 require String::Wildcard::Bash;
44 8         11 my $orig_prefix = $prefix;
45 8         24 my @prefix_parts = split /::/, $prefix;
46 8         12 $prefix = "";
47 8         10 my $has_wildcard;
48 8         20 while (defined(my $part = shift @prefix_parts)) {
49 8 50       22 if (String::Wildcard::Bash::contains_wildcard($part)) {
50 8         285 $has_wildcard++;
51             # XXX limit recurse level to scalar(@prefix_parts), or -1 if has_globstar
52 8 100       17 $recurse = 1 if @prefix_parts;
53 8         14 last;
54             } else {
55 0         0 $prefix .= "$part\::";
56             }
57             }
58 8 50       14 if ($has_wildcard) {
59 8         17 $re_wildcard = convert_wildcard_to_re($orig_prefix);
60             }
61 8 100       20 $recurse = 1 if $has_globstar;
62             }
63              
64 12 50 33     120 die "bad module name prefix `$prefix'"
65             unless $prefix =~ /\A(?:${root_notleaf_rx}::
66             (?:${notroot_notleaf_rx}::)*)?\z/x &&
67             $prefix !~ /(?:\A|[^:]::)\.\.?::/;
68              
69 12         22 my $list_modules = $options->{list_modules};
70 12         18 my $list_prefixes = $options->{list_prefixes};
71 12         15 my $list_pod = $options->{list_pod};
72 12         14 my $use_pod_dir = $options->{use_pod_dir};
73 12 50 66     34 return {} unless $list_modules || $list_prefixes || $list_pod;
      33        
74 12         18 my $return_path = $options->{return_path};
75 12         14 my $all = $options->{all};
76 12         23 my @prefixes = ($prefix);
77 12         16 my %seen_prefixes;
78             my %results;
79 12         24 while(@prefixes) {
80 40         77 my $prefix = pop(@prefixes);
81 40         116 my @dir_suffix = split(/::/, $prefix);
82 40 100       78 my $module_rx =
83             $prefix eq "" ? $root_leaf_rx : $notroot_leaf_rx;
84 40         415 my $pm_rx = qr/\A($module_rx)\.pmc?\z/;
85 40         281 my $pod_rx = qr/\A($module_rx)\.pod\z/;
86 40 100       86 my $dir_rx =
87             $prefix eq "" ? $root_notleaf_rx : $notroot_notleaf_rx;
88 40         212 $dir_rx = qr/\A$dir_rx\z/;
89 40         76 foreach my $incdir (@INC) {
90 80         182 my $dir = join("/", $incdir, @dir_suffix);
91 80 100       1542 opendir(my $dh, $dir) or next;
92 52         640 while(defined(my $entry = readdir($dh))) {
93 211 100 100     2274 if(($list_modules && $entry =~ $pm_rx) ||
    100 33        
      66        
      100        
      100        
      100        
      100        
      66        
94             ($list_pod &&
95             $entry =~ $pod_rx)) {
96 48         148 my $key = $prefix.$1;
97 48 100 100     268 next if $re_wildcard && $key !~ $re_wildcard;
98 0 0       0 $results{$key} = $return_path ? ($all ? [@{ $results{$key} || [] }, "$dir/$entry"] : "$dir/$entry") : undef
99 21 0 33     154 if $all && $return_path || !exists($results{$key});
    50 66        
    100          
100             } elsif(($list_prefixes || $recurse) &&
101             ($entry ne '.' && $entry ne '..') &&
102             $entry =~ $dir_rx &&
103             -d join("/", $dir,
104             $entry)) {
105 37         96 my $newmod = $prefix.$entry;
106 37         59 my $newpfx = $newmod."::";
107 37 50       83 next if exists $seen_prefixes{$newpfx};
108 0 0       0 $results{$newpfx} = $return_path ? ($all ? [@{ $results{$newpfx} || [] }, "$dir/$entry/"] : "$dir/$entry/") : undef
109 37 0 33     154 if ($all && $return_path || !exists($results{$newpfx})) && $list_prefixes;
    50 66        
    100          
110 37 100       143 push @prefixes, $newpfx if $recurse;
111             }
112             }
113 52 50 33     620 next unless $list_pod && $use_pod_dir;
114 0         0 $dir = join("/", $dir, "pod");
115 0 0       0 opendir($dh, $dir) or next;
116 0         0 while(defined(my $entry = readdir($dh))) {
117 0 0       0 if($entry =~ $pod_rx) {
118 0         0 my $key = $prefix.$1;
119 0 0 0     0 next if $re_wildcard && $key !~ $re_wildcard;
120 0 0       0 $results{$key} = $return_path ? ($all ? [@{ $results{$key} || [] }, "$dir/$entry"] : "$dir/$entry") : undef;
  0 0       0  
    0          
121             }
122             }
123             }
124             }
125              
126             # we cannot filter prefixes early with wildcard because we need to dig down
127             # first and that would've been prevented if we had a wildcard like *::Foo.
128 12 100 100     43 if ($list_prefixes && $re_wildcard) {
129 2         8 for my $k (keys %results) {
130 6 50       18 next unless $k =~ /::\z/;
131 6         25 (my $k_nocolon = $k) =~ s/::\z//;
132 6 100 100     39 delete $results{$k} unless $k =~ $re_wildcard || $k_nocolon =~ $re_wildcard;
133             }
134             }
135              
136 12         60 return \%results;
137             }
138              
139             sub convert_wildcard_to_re {
140 8     8 0 11 $has_globstar = 0;
141 8         16 my $re = _convert_wildcard_to_re(@_);
142 8         159 $re = qr/\A$re\z/;
143             #print "DEBUG: has_globstar=<$has_globstar>, re=$re\n";
144 8         20 $re;
145             }
146              
147             # modified from String::Wildcard::Bash 0.040's convert_wildcard_to_re
148             sub _convert_wildcard_to_re {
149 8 50   8   18 my $opts = ref $_[0] eq 'HASH' ? shift : {};
150 8         11 my $str = shift;
151              
152 8   50     29 my $opt_brace = $opts->{brace} // 1;
153              
154 8         12 my @res;
155             my $p;
156 8         48 while ($str =~ /$String::Wildcard::Bash::RE_WILDCARD_BASH/g) {
157 1     1   110107 my %m = %+;
  1         310  
  1         249  
  25         135  
158 25 50       101 if (defined($p = $m{bash_brace_content})) {
    100          
    50          
    100          
    50          
    50          
159             push @res, quotemeta($m{slashes_before_bash_brace}) if
160 0 0       0 $m{slashes_before_bash_brace};
161 0 0       0 if ($opt_brace) {
162 0         0 my @elems;
163 0         0 while ($p =~ /($String::Wildcard::Bash::re_bash_brace_element)(,|\z)/g) {
164 0         0 push @elems, $1;
165 0 0       0 last unless $2;
166             }
167             #use DD; dd \@elems;
168             push @res, "(?:", join("|", map {
169 0         0 convert_wildcard_to_re({
  0         0  
170             bash_brace => 0,
171             }, $_)} @elems), ")";
172             } else {
173 0         0 push @res, quotemeta($m{bash_brace});
174             }
175              
176             } elsif (defined($p = $m{bash_joker})) {
177 10 50       24 if ($p eq '?') {
    100          
    50          
178 0         0 push @res, '[^:]';
179             } elsif ($p eq '*') {
180 9         47 push @res, '[^:]*';
181             } elsif ($p eq '**') {
182 1         2 $has_globstar++;
183 1         6 push @res, '.*';
184             }
185              
186             } elsif (defined($p = $m{literal_brace_single_element})) {
187 0         0 push @res, quotemeta($p);
188             } elsif (defined($p = $m{bash_class})) {
189             # XXX no need to escape some characters?
190 4         22 push @res, $p;
191             } elsif (defined($p = $m{sql_joker})) {
192 0         0 push @res, quotemeta($p);
193             } elsif (defined($p = $m{literal})) {
194 11         56 push @res, quotemeta($p);
195             }
196             }
197              
198 8         28 join "", @res;
199             }
200              
201             1;
202             # ABSTRACT: A fork of Module::List
203              
204             __END__
205              
206             =pod
207              
208             =encoding UTF-8
209              
210             =head1 NAME
211              
212             PERLANCAR::Module::List - A fork of Module::List
213              
214             =head1 VERSION
215              
216             This document describes version 0.004005 of PERLANCAR::Module::List (from Perl distribution PERLANCAR-Module-List), released on 2019-09-12.
217              
218             =head1 SYNOPSIS
219              
220             Use like you would L<Module::List>, e.g.:
221              
222             use PERLANCAR::Module::List qw(list_modules);
223              
224             $id_modules = list_modules("Data::ID::", { list_modules => 1});
225             $prefixes = list_modules("", { list_prefixes => 1, recurse => 1 });
226              
227             =head1 DESCRIPTION
228              
229             This module is my personal experimental fork of L<Module::List>; the experiment
230             has also produced other forks like L<Module::List::Tiny>,
231             L<Module::List::Wildcard>. It's like Module::List, except for the following
232             differences:
233              
234             =over
235              
236             =item * lower startup overhead (with some caveats)
237              
238             It avoids using L<Exporter> and implements its own import(). It avoids
239             L<IO::Dir>, L<Carp>, L<File::Spec>, with the goal of saving a few milliseconds
240             (a casual test on my PC results in 11ms vs 39ms).
241              
242             Path separator is hard-coded as C</>.
243              
244             =item * Recognize C<all> option
245              
246             If set to true and C<return_path> is also set to true, will return all found
247             paths for each module instead of just the first found one. The values of result
248             will be an arrayref containing all found paths.
249              
250             =item * Recognize C<wildcard> option
251              
252             This boolean option can be set to true to recognize wildcard pattern in prefix.
253             Wildcard patterns such as jokers (C<?>, C<*>, C<**>), classes (C<[a-z]>), as
254             well as braces (C<{One,Two}>) are supported. C<**> implies recursive listing.
255              
256             Examples:
257              
258             list_modules("Module::P*", {wildcard=>1, list_modules=>1});
259              
260             results in something like:
261              
262             {
263             "Module::Patch" => undef,
264             "Module::Path" => undef,
265             "Module::Pluggable" => undef,
266             }
267              
268             while:
269              
270             list_modules("Module::P**", {wildcard=>1, list_modules=>1});
271              
272             results in something like:
273              
274             {
275             "Module::Patch" => undef,
276             "Module::Path" => undef,
277             "Module::Path::More" => undef,
278             "Module::Pluggable" => undef,
279             "Module::Pluggable::Object" => undef,
280             }
281              
282             while:
283              
284             list_modules("Module::**le", {wildcard=>1, list_modules=>1});
285              
286             results in something like:
287              
288             {
289             "Module::Depakable" => undef,
290             "Module::Install::Admin::Bundle" => undef,
291             "Module::Install::Admin::Makefile" => undef,
292             "Module::Install::Bundle" => undef,
293             "Module::Install::Makefile" => undef,
294             "Module::Pluggable" => undef,
295             }
296              
297             =back
298              
299             =for Pod::Coverage .+
300              
301             =head1 HOMEPAGE
302              
303             Please visit the project's homepage at L<https://metacpan.org/release/PERLANCAR-Module-List>.
304              
305             =head1 SOURCE
306              
307             Source repository is at L<https://github.com/perlancar/perl-PERLANCAR-Module-List>.
308              
309             =head1 BUGS
310              
311             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=PERLANCAR-Module-List>
312              
313             When submitting a bug or request, please include a test-file or a
314             patch to an existing test-file that illustrates the bug or desired
315             feature.
316              
317             =head1 SEE ALSO
318              
319             L<Module::List>
320              
321             L<Module::List::Tiny>
322              
323             L<Module::List::Wildcard>
324              
325             =head1 AUTHOR
326              
327             perlancar <perlancar@cpan.org>
328              
329             =head1 COPYRIGHT AND LICENSE
330              
331             This software is copyright (c) 2019, 2016, 2015 by perlancar@cpan.org.
332              
333             This is free software; you can redistribute it and/or modify it under
334             the same terms as the Perl 5 programming language system itself.
335              
336             =cut