File Coverage

blib/lib/FastGlob.pm
Criterion Covered Total %
statement 100 107 93.4
branch 35 48 72.9
condition 5 6 83.3
subroutine 8 8 100.0
pod 0 2 0.0
total 148 171 86.5


line stmt bran cond sub pod time code
1             #!/usr/local/bin/perl
2             package FastGlob;
3              
4             require 5.008;
5              
6             # ABSTRACT: A faster glob() implementation
7              
8             BEGIN {
9 14     14   2206818 our $VERSION = '1.6'; # VERSION: generated by DZP::OurPkgVersion
10             }
11              
12              
13 14     14   295 use 5.008;
  14         44  
14 14     14   68 use strict;
  14         52  
  14         446  
15 14     14   95 use warnings;
  14         23  
  14         910  
16              
17 14     14   75 use Exporter ();
  14         46  
  14         380  
18 14     14   55 use Carp qw(carp);
  14         34  
  14         23103  
19              
20             our @ISA = qw(Exporter);
21             our @EXPORT = qw(glob);
22             our @EXPORT_OK = qw(dirsep rootpat curdir parentdir hidedotfiles);
23              
24             # platform specifics — auto-detect Windows defaults
25              
26             my $IS_WINDOWS = ( $^O eq 'MSWin32' );
27              
28             our $dirsep = $IS_WINDOWS ? '\\' : '/';
29             our $rootpat = $IS_WINDOWS ? '[A-Za-z]:' : '\A\Z';
30             our $curdir = '.';
31             our $parentdir = '..';
32             our $hidedotfiles = 1;
33             our $verbose = 0;
34              
35             #
36             # recursively wildcard expand a list of strings
37             #
38              
39             sub glob {
40              
41 103     103 0 3917800 my @res;
42             my $part;
43 103         0 my $found1;
44 103         0 my $out;
45 103         406 my $bracepat = qr(\{([^\{\}]*)\});
46              
47             # deal with {xxx,yyy,zzz}
48 103         188 @res = ();
49 103         174 $found1 = 1;
50 103         318 while ($found1) {
51 114         151 $found1 = 0;
52 114         216 for (@_) {
53 128 100       735 if ( m{$bracepat} ) {
54 12         51 foreach $part (split(',',$1)) {
55 25         39 $out = $_;
56 25         135 $out =~ s/$bracepat/$part/;
57 25         60 push(@res, $out);
58             }
59 12         27 $found1 = 1;
60             } else {
61 116         282 push(@res, $_);
62             }
63             }
64 114         263 @_ = @res;
65 114         258 @res = ();
66             }
67              
68             # skip empty patterns — CORE::glob returns nothing for them
69 103 50       204 @_ = grep { defined $_ && $_ ne '' } @_;
  116         657  
70              
71 103         187 for (@_) {
72             # check for and do tilde expansion
73 115 100       931 if ( /^\~([^\Q${dirsep}\E]*)/ ) {
74 4         12 my $usr = $1;
75 4         7 my $usrdir;
76 4 100       12 if ( $usr eq "" ) {
77             # ~ alone: try getpwuid, fall back to $HOME / $USERPROFILE
78 2         4 $usrdir = eval { (getpwuid($<))[7] };
  2         149  
79 2 50       13 if ( !defined $usrdir ) {
80 0 0       0 $usrdir = defined $ENV{HOME} ? $ENV{HOME} : $ENV{USERPROFILE};
81             }
82             } else {
83             # ~user: try getpwnam (not available on Windows)
84 2         7 $usrdir = eval { (getpwnam($usr))[7] };
  2         303  
85             }
86 4 100 66     26 if ( defined $usrdir && $usrdir ne "" ) {
87 3         68 s/^\~\Q$usr\E/$usrdir/;
88             }
89             # Always keep the entry — if expansion fails, preserve the
90             # original pattern unchanged (consistent with CORE::glob)
91 4         15 push(@res, $_);
92             } else {
93 111         223 push(@res, $_);
94             }
95             }
96 103         227 @_ = @res;
97 103         203 @res = ();
98              
99 103         167 for (@_) {
100             # if there's no wildcards, just return it
101 115 100       525 unless (/(?
102 28         72 push (@res, $_);
103 28         51 next;
104             }
105              
106             # Split into directory components FIRST, before regex transformation.
107             # This prevents regex escape sequences (e.g. \.) from being confused
108             # with the directory separator on Windows where $dirsep is \.
109             # On Windows, accept both / and \ as path separators in patterns.
110 87         135 my @comps;
111 87 50       194 if ( $IS_WINDOWS ) {
112 0         0 @comps = split(m{[/\\]});
113             } else {
114 87         555 @comps = split(/\Q$dirsep\E/);
115             }
116              
117             # Check for root pattern before transforming components
118 87         445 my $is_rooted = ($comps[0] =~ /($rootpat)/);
119 87 100       214 my $root_prefix = $is_rooted ? $1 : undef;
120              
121             # Transform each component into a regex
122 87         155 for my $comp (@comps) {
123 180 100       407 if ( $comp =~ /(?
124             # Wildcard component: convert glob pattern to regex
125              
126             # escape regex metacharacters that are not glob syntax
127 98         488 $comp =~ s/([+.|(){}\$])/\\$1/g;
128              
129             # convert POSIX [!...] negation to regex [^...]
130             # Only convert when there are chars between ! and ] (avoid [!] -> [^] which is invalid)
131 98         193 $comp =~ s/\[!(?=[^\]]+\])/[^/g;
132              
133             # handle * and ?
134 98         326 $comp =~ s/(?
135 98         245 $comp =~ s/(?
136              
137             } else {
138             # Literal component: escape regex metacharacters
139 82         176 $comp = quotemeta($comp);
140             }
141             }
142              
143             # debugging
144 87 50       208 print "regexp components: @comps\n" if ($verbose);
145              
146 87 100       153 if ( $is_rooted ) {
147 3         6 shift(@comps);
148 3         14 push(@res, &recurseglob( "$root_prefix$dirsep", "$root_prefix$dirsep" , @comps ));
149             }
150             else {
151 84         201 push(@res, &recurseglob( $curdir, '' , @comps ));
152             }
153             }
154 103         762 return sort(@res);
155             }
156              
157             sub recurseglob {
158 186     186 0 516 my($dir, $dirname, @comps) = @_;
159 186         245 my(@res) = ();
160 186         249 my($re, @names);
161              
162              
163 186 50       463 if ( @comps == 0 ) {
    100          
164             # bottom of recursion, just return the path
165 0         0 chop($dirname); # always has gratiutous trailing slash
166 0         0 @res = ($dirname);
167             } elsif ($comps[0] eq '') {
168 2         7 shift(@comps);
169 2         16 push(@res, &recurseglob( "$dir$dirsep",
170             "$dirname$dirsep",
171             @comps ));
172             } else {
173 184         296 $re = '\A' . shift(@comps) . '\Z';
174              
175             # slurp in the directory
176 184         244 my $dh;
177 184 50       6230 if (!opendir($dh, $dir)) {
178 0 0       0 carp "FastGlob: opendir '$dir' failed: $!" if $verbose;
179 0         0 return @res;
180             }
181 184         7179 @names = readdir($dh);
182 184         1546 closedir($dh);
183              
184             # Hide dotfiles at the readdir level (like CORE::glob does)
185             # unless the pattern component explicitly starts with a literal dot.
186             # After glob-to-regex conversion, an explicit dot becomes \. in the
187             # regex; a wildcard like * becomes .* (no backslash before the dot).
188 184 100 100     987 if ( $hidedotfiles && $re !~ /\A\\A\\\./ ) {
189 164         326 @names = grep { !/\A\./ } @names;
  3330         6256  
190             }
191              
192             # look for matches, and if you find one, glob the rest of the
193             # components. We eval the loop so the regexp gets compiled in,
194             # making searches on large directories faster.
195 184 50       365 print "component re is qr($re)\n" if ($verbose);
196 184         275 my $regex = eval { qr($re) };
  184         3371  
197 184 50       468 if (!defined $regex) {
198 0         0 return @res;
199             }
200 184         326 foreach (@names) {
201 3166 50       4357 print "considering |$_|\n" if ($verbose);
202 3166 100       8326 if ( m{$regex} ) {
203 1014 100       1442 if ( $#comps > -1 ) {
204             # Only recurse into actual directories — avoids
205             # futile opendir() calls on plain files.
206 121         205 my $subdir = "$dir$dirsep$_";
207 121 100       1712 if ( -d $subdir ) {
208 97         506 push(@res, &recurseglob( $subdir,
209             "$dirname$_$dirsep",
210             @comps ));
211             }
212             } else {
213 893         1710 push(@res, "$dirname$_" );
214             }
215             }
216             }
217             }
218 186         1291 return @res;
219             }
220              
221             1;
222              
223             __END__