File Coverage

blib/lib/File/Globstar.pm
Criterion Covered Total %
statement 172 175 98.2
branch 89 94 94.6
condition 13 15 86.6
subroutine 23 23 100.0
pod 5 6 83.3
total 302 313 96.4


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