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-2023 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 = 'v1.0.0';
12 9     9   413411 use strict;
  9         71  
  9         306  
13              
14 9     9   3487 use Locale::TextDomain qw(File-Globstar);
  9         114259  
  9         59  
15 9     9   181328 use File::Glob qw(bsd_glob);
  9         27  
  9         998  
16 9     9   62 use Scalar::Util 1.21 qw(reftype);
  9         183  
  9         402  
17 9     9   59 use File::Find;
  9         28  
  9         666  
18              
19 9     9   67 use base 'Exporter';
  9         27  
  9         1062  
20 9     9   58 use vars qw(@EXPORT_OK);
  9         20  
  9         548  
21             @EXPORT_OK = qw(globstar fnmatchstar translatestar quotestar pnmatchstar);
22              
23 9     9   61 use constant RE_NONE => 0x0;
  9         20  
  9         612  
24 9     9   58 use constant RE_NEGATED => 0x1;
  9         39  
  9         455  
25 9     9   57 use constant RE_FULL_MATCH => 0x2;
  9         22  
  9         502  
26 9     9   77 use constant RE_DIRECTORY => 0x4;
  9         21  
  9         19933  
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 25     25 0 42 my ($what) = @_;
41              
42 25 100 100     119 return if defined $what && length $what;
43              
44 8         23 return 1;
45             }
46              
47             sub _find_directories($) {
48 15     15   32 my ($directory) = @_;
49              
50 15         30 my $empty = empty $directory;
51 15 100       38 $directory = '.' if $empty;
52              
53 15         24 my @hits;
54             File::Find::find sub {
55 171 100   171   3472 return if !-d $_;
56 45 100       1127 return if '.' eq substr $_, 0, 1;
57 30         1722 push @hits, $File::Find::name;
58 15         809 }, $directory;
59              
60 15 100       98 if ($empty) {
61 5         17 @hits = map { substr $_, 2 } @hits;
  16         43  
62             }
63              
64 15         51 return @hits;
65             }
66              
67             sub _find_all($) {
68 6     6   15 my ($directory) = @_;
69              
70 6         14 my $empty = empty $directory;
71 6 100       15 $directory = '.' if $empty;
72              
73 6         11 my @hits;
74             File::Find::find sub {
75 66 100   66   688 return if '.' eq substr $_, 0, 1;
76 60         1860 push @hits, $File::Find::name;
77 6         401 }, $directory;
78              
79 6 100       42 if ($empty) {
80 2         7 @hits = map { substr $_, 2 } @hits;
  32         57  
81             }
82              
83 6         37 return @hits;
84             }
85              
86             sub _globstar($$;$) {
87 35     35   82 my ($pattern, $directory, $flags) = @_;
88              
89 35 50       85 $directory = '' if !defined $directory;
90 35 50       81 $pattern = $_ if !@_;
91              
92 35 100       151 if ('**' eq $pattern) {
    100          
    100          
93 2         8 return _find_all $directory;
94             } elsif ('**/' eq $pattern) {
95 2         11 return map { $_ . '/' } _find_directories $directory;
  7         25  
96             } elsif ($pattern =~ s{^\*\*/}{}) {
97 3         8 my %found_files;
98 3         11 foreach my $directory ('', _find_directories $directory) {
99 12         48 foreach my $file (_globstar $pattern, $directory, $flags) {
100 18         75 $found_files{$file} = 1;
101             }
102             }
103 3         39 return keys %found_files;
104             }
105              
106 28         46 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 28         54 $current =~ s{([\x00-\x2d\x3b-\x40\x5b-\x5e\x60\x7b-\x7f])}{\\$1}g;
112 28 100 100     110 if ($directory ne '' && '/' ne substr $directory, -1, 1) {
113 9         16 $current .= '/';
114             }
115 28         197 while ($pattern =~ s/(.)//s) {
116 189 50 100     628 if ($1 eq '\\') {
    100          
    100          
117 0         0 $pattern =~ s/(..?)//s;
118 0         0 $current .= $1;
119             } elsif ('/' eq $1 && $pattern =~ s{^\*\*/}{}) {
120 4         8 $current .= '/';
121              
122             # Expand until here.
123 4         83 my @directories = bsd_glob $current, $flags;
124              
125             # And search in every subdirectory;
126 4         11 my %found_dirs;
127 4         23 foreach my $directory (@directories) {
128 4         13 $found_dirs{$directory} = 1;
129 4         10 foreach my $subdirectory (_find_directories $directory) {
130 8         30 $found_dirs{$subdirectory . '/'} = 1;
131             }
132             }
133              
134 4 100       15 if ('' eq $pattern) {
135 2         4 my %found_subdirs;
136 2         8 foreach my $directory (keys %found_dirs) {
137 6         14 $found_subdirs{$directory} = 1;
138 6         13 foreach my $subdirectory (_find_directories $directory) {
139 6         20 $found_subdirs{$subdirectory . '/'} = 1;
140             }
141             }
142 2         21 return keys %found_subdirs;
143             }
144 2         7 my %found_files;
145 2         13 foreach my $directory (keys %found_dirs) {
146 6         38 foreach my $hit (_globstar $pattern, $directory, $flags) {
147 18         55 $found_files{$hit} = 1;
148             }
149             }
150 2         22 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         20 $found_files{$directory . '/'} = 1;
155 4         11 foreach my $file (_find_all $directory) {
156 28         58 $found_files{$file} = 1;
157             }
158             }
159 4         43 return keys %found_files;
160             } else {
161 181         522 $current .= $1;
162             }
163             }
164              
165             # Pattern without globstar. Just return the normal expansion.
166 20         1257 return bsd_glob $current, $flags;
167             }
168              
169             sub globstar {
170 17     17 1 11922 my ($pattern, $flags) = @_;
171              
172             # The double asterisk can only be used in place of a directory.
173             # It is illegal everywhere else.
174 17         124 my @parts = split /\//, $pattern;
175 17         41 foreach my $part (@parts) {
176 34 50 66     136 $part ne '**' and 0 <= index $part, '**' and return;
177             }
178              
179 17         43 return _globstar $pattern, '', $flags;
180             }
181              
182             sub quotestar {
183 8     8 1 109 my ($string, $listmatch) = @_;
184              
185 8         67 $string =~ s/([\\\[\]*?])/\\$1/g;
186 8 100       29 $string =~ s/^!/\\!/ if $listmatch;
187              
188 8         37 return $string;
189             }
190              
191             sub _transpile_range($) {
192 35     35   78 my ($range) = @_;
193              
194             # Strip-off enclosing brackets.
195 35         63 $range = substr $range, 1, -2 + length $range;
196              
197             # Replace leading exclamation mark with caret.
198 35         65 $range =~ s/^!/^/;
199              
200             # Backslashes escape inside Perl ranges but not in ours. Escape them:
201 35         55 $range =~ s/\\/\\\\/g;
202              
203             # Quote dots and equal sign to prevent Perl from interpreting
204             # equivalence and collating classes.
205 35         58 $range =~ s/\./\\\./g;
206 35         52 $range =~ s/\=/\\\=/g;
207              
208 35         83 return "[$range]";
209             }
210              
211             sub translatestar {
212 153     153 1 479 my ($pattern, %options) = @_;
213              
214 153 100       414 die __x("invalid pattern '{pattern}'\n", pattern => $pattern)
215             if $pattern =~ m{^/+$};
216              
217 151         233 my $blessing = RE_NONE;
218              
219 151 100       301 if ($options{pathMode}) {
220 91 100       238 $blessing |= RE_NEGATED if $pattern =~ s/^!//;
221 91 100       229 $blessing |= RE_DIRECTORY if $pattern =~ s{/$}{};
222 91 100       206 $blessing |= RE_FULL_MATCH if $pattern =~ m{/};
223 91         148 $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         7390 $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         3141 my $translated = quotemeta $1;
263 1823 100       8139 if ('\\' eq substr $2, 0, 1) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
264 8         17 $translated .= quotemeta substr $2, 1, 1;
265             } elsif ('**' eq $2) {
266 2         5 $translated .= '.*';
267             } elsif ('/**' eq $2) {
268 8         15 $translated .= '(?:/.*)?';
269             } elsif ('.' eq $2) {
270 16         29 $translated .= '\\.';
271             } elsif ('*' eq $2) {
272 13         27 $translated .= '[^/]*';
273             } elsif ('?' eq $2) {
274 4         6 $translated .= '[^/]';
275             } elsif ('[' eq substr $2, 0, 1) {
276 35         66 $translated .= _transpile_range $2;
277             } elsif (length $2) {
278 2 50       10 if ($2 =~ /\*\*/) {
279 2         18 die $invalid_msg;
280             }
281 0         0 die "should not happen: $2";
282             }
283 1821         6025 $translated;
284             }gsex;
285              
286 149 100       1858 my $re = $options{ignoreCase} ? qr/^$pattern$/i : qr/^$pattern$/;
287              
288 149         714 bless $re, $blessing;
289             }
290              
291             sub fnmatchstar {
292 31     31 1 9048 my ($pattern, $string, %options) = @_;
293              
294 31         49 my $transpiled = eval { translatestar $pattern, %options };
  31         79  
295 31 100       65 return if $@;
296              
297 30 100       209 $string =~ $transpiled or return;
298              
299 22         84 return 1;
300             }
301              
302             sub pnmatchstar {
303 204     204 1 2270 my ($pattern, $string, %options) = @_;
304              
305 204 100       526 $options{isDirectory} = 1 if $string =~ s{/$}{};
306              
307 204         300 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         446 my $reftype = reftype $pattern;
312 204 100 66     666 unless (defined $reftype && $regex_type eq $reftype) {
313 14         24 $pattern = eval { translatestar $pattern, %options, pathMode => 1 };
  14         35  
314 14 100       158 return if $@;
315             }
316              
317 203         305 my $flags = ref $pattern;
318 203 100       570 $string =~ s{.*/}{} unless $flags & RE_FULL_MATCH;
319              
320 203         831 my $match = $string =~ $pattern;
321 203 100       408 if ($flags & RE_DIRECTORY) {
322 30 100       69 undef $match if !$options{isDirectory};
323             }
324              
325 203         299 my $negated = $flags & RE_NEGATED;
326              
327 203 100       383 if ($match) {
328 115 100       181 if ($negated) {
329 35         123 return;
330             } else {
331 80         353 return 1;
332             }
333             }
334              
335 88 100       301 if ($full_path =~ s{/[^/]*$}{}) {
336 52         174 return pnmatchstar $pattern, $full_path, %options, isDirectory => 1;
337             }
338              
339 36 100       86 return 1 if $negated;
340              
341 26         137 return;
342             }
343              
344             1;