File Coverage

blib/lib/File/Globstar.pm
Criterion Covered Total %
statement 172 187 91.9
branch 89 102 87.2
condition 13 15 86.6
subroutine 23 25 92.0
pod 5 6 83.3
total 302 335 90.1


line stmt bran cond sub pod time code
1             # Copyright (C) 2016-2017 Guido Flohr ,
2             # all rights reserved.
3              
4             # This file is distributed under the same terms and conditions as
5             # Perl itself.
6              
7             # This next lines is here to make Dist::Zilla happy.
8             # ABSTRACT: Perl Globstar (double asterisk globbing) and utils
9              
10             package File::Globstar;
11             $File::Globstar::VERSION = '0.5';
12 7     7   235199 use strict;
  7         35  
  7         188  
13              
14 7     7   1843 use Locale::TextDomain qw(File-Globstar);
  7         59692  
  7         33  
15 7     7   25061 use File::Glob qw(bsd_glob);
  7         13  
  7         668  
16 7     7   45 use Scalar::Util 1.21 qw(reftype);
  7         126  
  7         263  
17 7     7   34 use File::Find;
  7         10  
  7         366  
18              
19 7     7   37 use base 'Exporter';
  7         10  
  7         821  
20 7     7   38 use vars qw(@EXPORT_OK);
  7         10  
  7         298  
21             @EXPORT_OK = qw(globstar fnmatchstar translatestar quotestar pnmatchstar);
22              
23 7     7   52 use constant RE_NONE => 0x0;
  7         12  
  7         327  
24 7     7   41 use constant RE_NEGATED => 0x1;
  7         12  
  7         303  
25 7     7   38 use constant RE_FULL_MATCH => 0x2;
  7         10  
  7         259  
26 7     7   42 use constant RE_DIRECTORY => 0x4;
  7         18  
  7         12530  
27              
28             # Remember what Scalar::Util::reftype() returns for a compiled regular
29             # expression. It should normally be 'REGEXP' but with Perl 5.10 (or
30             # maybe older) this seems to be an empty string. In this case, the
31             # check in pnmatchstar() whether it received a compiled regex will be
32             # rather weak ...
33             my $test_re = qr/./;
34             my $regex_type = reftype $test_re;
35              
36             sub _globstar;
37             sub pnmatchstar;
38              
39             sub empty($) {
40 14     14 0 22 my ($what) = @_;
41              
42 14 100 100     56 return if defined $what && length $what;
43              
44 4         7 return 1;
45             }
46              
47             sub _find_files($) {
48 0     0   0 my ($directory) = @_;
49              
50 0         0 my $empty = empty $directory;
51 0 0       0 $directory = '.' if $empty;
52              
53 0         0 my @hits;
54             File::Find::find sub {
55 0 0   0   0 return if -d $_;
56 0 0       0 return if '.' eq substr $_, 0, 1;
57 0         0 push @hits, $File::Find::name;
58 0         0 }, $directory;
59              
60 0 0       0 if ($empty) {
61 0         0 @hits = map { substr $_, 2 } @hits;
  0         0  
62             }
63              
64 0         0 return @hits;
65             }
66              
67             sub _find_directories($) {
68 7     7   12 my ($directory) = @_;
69              
70 7         9 my $empty = empty $directory;
71 7 100       11 $directory = '.' if $empty;
72              
73 7         8 my @hits;
74             File::Find::find sub {
75 80 100   80   1271 return if !-d $_;
76 20 100       397 return if '.' eq substr $_, 0, 1;
77 13         649 push @hits, $File::Find::name;
78 7         306 }, $directory;
79              
80 7 100       34 if ($empty) {
81 2         4 @hits = map { substr $_, 2 } @hits;
  6         14  
82             }
83              
84 7         19 return @hits;
85             }
86              
87             sub _find_all($) {
88 3     3   7 my ($directory) = @_;
89              
90 3         3 my $empty = empty $directory;
91 3 100       6 $directory = '.' if $empty;
92              
93 3         5 my @hits;
94             File::Find::find sub {
95 32 100   32   290 return if '.' eq substr $_, 0, 1;
96 29         527 push @hits, $File::Find::name;
97 3         160 }, $directory;
98              
99 3 100       16 if ($empty) {
100 1         3 @hits = map { substr $_, 2 } @hits;
  15         22  
101             }
102              
103 3         12 return @hits;
104             }
105              
106             sub _globstar($$;$) {
107 15     15   31 my ($pattern, $directory, $flags) = @_;
108              
109 15 50       26 $directory = '' if !defined $directory;
110 15 50       23 $pattern = $_ if !@_;
111              
112 15 100       49 if ('**' eq $pattern) {
    100          
    100          
113 1         3 return _find_all $directory;
114             } elsif ('**/' eq $pattern) {
115 1         4 return map { $_ . '/' } _find_directories $directory;
  3         9  
116             } elsif ($pattern =~ s{^\*\*/}{}) {
117 1         2 my %found_files;
118 1         3 foreach my $directory ('', _find_directories $directory) {
119 4         8 foreach my $file (_globstar $pattern, $directory, $flags) {
120 8         23 $found_files{$file} = 1;
121             }
122             }
123 1         8 return keys %found_files;
124             }
125              
126 12         13 my $current = $directory;
127            
128             # This is a quotemeta() that does not escape the slash and the
129             # colon. Escaped slashes confuse bsd_glob() and escaping colons
130             # may make a full port to Windows harder.
131 12         18 $current =~ s{([\x00-\x2e\x3b-\x40\x5b-\x60\x7b-\x7f])}{\\$1}g;
132 12 100 100     33 if ($directory ne '' && '/' ne substr $directory, -1, 1) {
133 3         5 $current .= '/';
134             }
135 12         42 while ($pattern =~ s/(.)//s) {
136 81 50 100     209 if ($1 eq '\\') {
    100          
    100          
137 0         0 $pattern =~ s/(..?)//s;
138 0         0 $current .= $1;
139             } elsif ('/' eq $1 && $pattern =~ s{^\*\*/}{}) {
140 2         3 $current .= '/';
141              
142             # Expand until here.
143 2         47 my @directories = bsd_glob $current, $flags;
144              
145             # And search in every subdirectory;
146 2         7 my %found_dirs;
147 2         5 foreach my $directory (@directories) {
148 2         3 $found_dirs{$directory} = 1;
149 2         5 foreach my $subdirectory (_find_directories $directory) {
150 4         11 $found_dirs{$subdirectory . '/'} = 1;
151             }
152             }
153              
154 2 100       6 if ('' eq $pattern) {
155 1         1 my %found_subdirs;
156 1         3 foreach my $directory (keys %found_dirs) {
157 3         5 $found_subdirs{$directory} = 1;
158 3         5 foreach my $subdirectory (_find_directories $directory) {
159 3         8 $found_subdirs{$subdirectory . '/'} = 1;
160             }
161             }
162 1         8 return keys %found_subdirs;
163             }
164 1         2 my %found_files;
165 1         3 foreach my $directory (keys %found_dirs) {
166 3         9 foreach my $hit (_globstar $pattern, $directory, $flags) {
167 9         21 $found_files{$hit} = 1;
168             }
169             }
170 1         9 return keys %found_files;
171             } elsif ('**' eq $pattern) {
172 2         3 my %found_files;
173 2         34 foreach my $directory (bsd_glob $current, $flags) {
174 2         7 $found_files{$directory . '/'} = 1;
175 2         5 foreach my $file (_find_all $directory) {
176 14         22 $found_files{$file} = 1;
177             }
178             }
179 2         13 return keys %found_files;
180             } else {
181 77         186 $current .= $1;
182             }
183             }
184              
185             # Pattern without globstar. Just return the normal expansion.
186 8         480 return bsd_glob $current, $flags;
187             }
188              
189             sub globstar {
190 8     8 1 4807 my ($pattern, $flags) = @_;
191              
192             # The double asterisk can only be used in place of a directory.
193             # It is illegal everywhere else.
194 8         21 my @parts = split /\//, $pattern;
195 8         13 foreach my $part (@parts) {
196 16 50 66     50 $part ne '**' and 0 <= index $part, '**' and return;
197             }
198              
199 8         14 return _globstar $pattern, '', $flags;
200             }
201              
202             sub quotestar {
203 8     8 1 79 my ($string, $listmatch) = @_;
204              
205 8         52 $string =~ s/([\\\[\]*?])/\\$1/g;
206 8 100       23 $string =~ s/^!/\\!/ if $listmatch;
207            
208 8         30 return $string;
209             }
210              
211             sub _transpile_range($) {
212 35     35   361 my ($range) = @_;
213              
214             # Strip-off enclosing brackets.
215 35         70 $range = substr $range, 1, -2 + length $range;
216              
217             # Replace leading exclamation mark with caret.
218 35         58 $range =~ s/^!/^/;
219              
220             # Backslashes escape inside Perl ranges but not in ours. Escape them:
221 35         63 $range =~ s/\\/\\\\/g;
222              
223             # Quote dots and equal sign to prevent Perl from interpreting
224             # equivalence and collating classes.
225 35         58 $range =~ s/\./\\\./g;
226 35         62 $range =~ s/\=/\\\=/g;
227            
228 35         90 return "[$range]";
229             }
230              
231             sub translatestar {
232 153     153 1 414 my ($pattern, %options) = @_;
233              
234 153 100       343 die __x("invalid pattern '{pattern}'\n", pattern => $pattern)
235             if $pattern =~ m{^/+$};
236              
237 151         197 my $blessing = RE_NONE;
238              
239 151 100       266 if ($options{pathMode}) {
240 91 100       182 $blessing |= RE_NEGATED if $pattern =~ s/^!//;
241 91 100       182 $blessing |= RE_DIRECTORY if $pattern =~ s{/$}{};
242 91 100       170 $blessing |= RE_FULL_MATCH if $pattern =~ m{/};
243 91         118 $pattern =~ s{^/}{};
244             }
245              
246             # xgettext doesn't parse Perl code in regexes.
247 151         327 my $invalid_msg = __"invalid use of double asterisk";
248              
249 151         6533 $pattern =~ s
250             {
251             (.*?) # Anything, followed by ...
252             (
253             \\. # escaped character
254             | # or
255             \A\*\*(?=/) # leading **/
256             | # or
257             /\*\*(?=/|\z) # /**/ or /** at end of string
258             | # or
259             \*\*. # invalid
260             | # or
261             .\*\* # invalid
262             | # or
263             \. # a dot
264             | # or
265             \* # an asterisk
266             |
267             \? # a question mark
268             |
269             \[ # opening bracket
270             !?
271             \]? # possible (literal) closing bracket
272             (?:
273             \\. # escaped character
274             |
275             \[:[a-z]+:\] # character class
276             |
277             [^\\\]]+ # non-backslash or closing bracket
278             )+
279             \]
280             )?
281             }{
282 1823         2839 my $translated = quotemeta $1;
283 1823 100       7316 if ('\\' eq substr $2, 0, 1) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
284 8         14 $translated .= quotemeta substr $2, 1, 1;
285             } elsif ('**' eq $2) {
286 2         4 $translated .= '.*';
287             } elsif ('/**' eq $2) {
288 8         12 $translated .= '(?:/.*)?';
289             } elsif ('.' eq $2) {
290 16         24 $translated .= '\\.';
291             } elsif ('*' eq $2) {
292 13         20 $translated .= '[^/]*';
293             } elsif ('?' eq $2) {
294 4         6 $translated .= '[^/]';
295             } elsif ('[' eq substr $2, 0, 1) {
296 35         79 $translated .= _transpile_range $2;
297             } elsif (length $2) {
298 2 50       7 if ($2 =~ /\*\*/) {
299 2         13 die $invalid_msg;
300             }
301 0         0 die "should not happen: $2";
302             }
303 1821         5602 $translated;
304             }gsex;
305              
306 149 100       1520 my $re = $options{ignoreCase} ? qr/^$pattern$/i : qr/^$pattern$/;
307              
308 149         620 bless $re, $blessing;
309             }
310              
311             sub fnmatchstar {
312 31     31 1 8297 my ($pattern, $string, %options) = @_;
313              
314 31         52 my $transpiled = eval { translatestar $pattern, %options };
  31         78  
315 31 100       71 return if $@;
316              
317 30 100       194 $string =~ $transpiled or return;
318              
319 22         77 return 1;
320             }
321              
322             sub pnmatchstar {
323 204     204 1 1829 my ($pattern, $string, %options) = @_;
324              
325 204 100       400 $options{isDirectory} = 1 if $string =~ s{/$}{};
326              
327 204         247 my $full_path = $string;
328              
329             # Check whether the regular expression is compiled.
330             # (ref $pattern) may be false here because it can be 0.
331 204         342 my $reftype = reftype $pattern;
332 204 100 66     582 unless (defined $reftype && $regex_type eq $reftype) {
333 14         15 $pattern = eval { translatestar $pattern, %options, pathMode => 1 };
  14         31  
334 14 100       139 return if $@;
335             }
336              
337 203         253 my $flags = ref $pattern;
338 203 100       445 $string =~ s{.*/}{} unless $flags & RE_FULL_MATCH;
339              
340 203         659 my $match = $string =~ $pattern;
341 203 100       341 if ($flags & RE_DIRECTORY) {
342 30 100       53 undef $match if !$options{isDirectory};
343             }
344              
345 203         225 my $negated = $flags & RE_NEGATED;
346              
347 203 100       263 if ($match) {
348 115 100       153 if ($negated) {
349 35         101 return;
350             } else {
351 80         267 return 1;
352             }
353             }
354              
355 88 100       240 if ($full_path =~ s{/[^/]*$}{}) {
356 52         138 return pnmatchstar $pattern, $full_path, %options, isDirectory => 1;
357             }
358              
359 36 100       63 return 1 if $negated;
360            
361 26         109 return;
362             }
363              
364             1;
365