File Coverage

blib/lib/File/Globstar.pm
Criterion Covered Total %
statement 173 176 98.3
branch 90 96 93.7
condition 13 15 86.6
subroutine 23 23 100.0
pod 5 6 83.3
total 304 316 96.2


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