File Coverage

lib/Path/Resolve.pm
Criterion Covered Total %
statement 163 310 52.5
branch 43 110 39.0
condition 24 99 24.2
subroutine 33 45 73.3
pod 3 4 75.0
total 266 568 46.8


line stmt bran cond sub pod time code
1             package Path::Resolve;
2 4     4   8094 use warnings;
  4         10  
  4         232  
3 4     4   27 use strict;
  4         7  
  4         137  
4 4     4   19 use Carp;
  4         11  
  4         375  
5 4     4   3403 use Data::Dumper;
  4         42774  
  4         1876  
6            
7             our $VERSION = '0.0.2';
8            
9             my $isWindows = $^O eq 'MSWin32';
10            
11             if ($isWindows) {
12             eval "use base 'Path::Resolve::Win'";
13             *splitPath = *Path::Resolve::Win::splitPath;
14             } else {
15 4     4   35 eval "use base 'Path::Resolve::POSIX'";
  4         7  
  4         1873  
16             *splitPath = *Path::Resolve::POSIX::splitPath;
17             }
18            
19             sub new {
20 3     3 0 26491 bless {}, __PACKAGE__;
21             }
22            
23             sub dirname {
24 12     12 1 29 my ($self,$path) = @_;
25 12         33 my @result = splitPath($path);
26 12         26 my $root = $result[0],
27             my $dir = $result[1];
28 12 100 66     42 if (!$root && !$dir) {
29             #No dirname whatsoever
30 1         5 return '.';
31             }
32 11 100       24 if ($dir) {
33             #It has a dirname, strip trailing slash
34 8         24 $dir = substr $dir,0, length($dir) - 1;
35             }
36 11         62 return $root . $dir;
37             }
38            
39             sub extname {
40 50     50 1 96 my ($self,$path) = @_;
41 50         109 return (splitPath($path))[3];
42             }
43            
44             sub basename {
45 21     21 1 73 my ($self, $path, $ext) = @_;
46 21         73 my $f = (splitPath($path))[2];
47             #TODO: make this comparison case-insensitive on windows?
48 21 50 33     86 if ($ext && (substr $f, -1 * length $ext) eq $ext) {
49 0         0 $f = substr $f, 0, length($f) - length($ext);
50             }
51 21         109 return $f;
52             }
53            
54             package
55             Path::Resolve::Win; {
56 4     4   41 use Carp;
  4         9  
  4         320  
57 4     4   28 use strict;
  4         7  
  4         227  
58 4     4   25 use warnings;
  4         15  
  4         227  
59 4     4   25 use Cwd();
  4         5  
  4         85  
60 4     4   24 use Data::Dumper;
  4         12  
  4         9248  
61            
62             my $splitDeviceRe = qr/^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/;
63             my $splitTailRe = qr/^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/;
64             my $CWD = Cwd::cwd();
65            
66 0     0   0 sub sep {'\\'};
67 0     0   0 sub delimiter {';'};
68            
69             sub splitPath {
70 0     0   0 my ($filename) = @_;
71 0         0 my @result = Path::Resolve::Utils::exec($splitDeviceRe,$filename);
72 0   0     0 my $device = ($result[1] || '') . ($result[2] || '');
      0        
73 0   0     0 my $tail = $result[3] || '';
74             #Split the tail into dir, basename and extension
75 0         0 my @result2 = Path::Resolve::Utils::exec($splitTailRe,$tail);
76 0         0 my $dir = $result2[1];
77 0         0 my $basename = $result2[2];
78 0         0 my $ext = $result2[3];
79 0         0 return ($device, $dir, $basename, $ext);
80             }
81            
82             sub parse {
83 0     0   0 my ($self, $path) = @_;
84 0 0 0     0 if (!$path || ref $path){
85 0         0 croak("Parameter 'pathString' must be a string");
86             }
87            
88 0         0 my @allParts = splitPath($path);
89            
90 0 0 0     0 if (!@allParts || scalar @allParts != 4) {
91 0         0 croak("Invalid path '" . $path . "'");
92             }
93            
94             return {
95 0         0 root => $allParts[0],
96             dir => $allParts[0] . substr ($allParts[1], 0, -1),
97             base => $allParts[2],
98             ext => $allParts[3],
99             name => substr ($allParts[2], 0, length($allParts[2]) - length($allParts[3]))
100             };
101             }
102            
103             sub format {
104 0     0   0 my ($self, $pathObject) = @_;
105 0 0       0 if ( ref $pathObject ne 'HASH' ) {
106 0         0 croak "Parameter 'pathObject' must be an Hash Ref";
107             }
108            
109 0   0     0 my $root = $pathObject->{root} || '';
110 0 0 0     0 if (!defined $root || ref $root) {
111 0         0 croak 'pathObject->{root} must be a string or undefined';
112             }
113            
114 0         0 my $dir = $pathObject->{dir};
115 0   0     0 my $base = $pathObject->{base} || '';
116 0 0       0 if (!$dir) {
117 0         0 return $base;
118             }
119            
120 0         0 my $t = substr $dir, length($dir) - 1, 2;
121 0 0       0 if ($t eq $self->sep) {
122 0         0 return $dir . $base;
123             }
124            
125 0         0 return $dir . $self->sep . $base;
126             }
127            
128             sub isAbsolute {
129 0     0   0 my ($self, $path) = @_;
130 0         0 my @result = Path::Resolve::Utils::exec($splitDeviceRe,$path);
131 0   0     0 my $device = $result[1] || '';
132 0   0     0 my $isUnc = $device && $device !~ /^.:/;
133             ##UNC paths are always absolute
134 0   0     0 return !!$result[2] || $isUnc;
135             }
136            
137             sub normalizeUNCRoot {
138 0     0   0 my $device = shift;
139 0         0 $device =~ s/^[\\\/]+//;
140 0         0 $device =~ s/[\\\/]+/\\/g;
141 0         0 return '\\\\' . $device;
142             }
143            
144             sub resolve {
145 0     0   0 my $self = shift;
146 0         0 my $resolvedDevice = '';
147 0         0 my $resolvedTail = '';
148 0         0 my $resolvedAbsolute = 0;
149 0         0 my $isUnc;
150 0         0 my @args = @_;
151 0         0 for (my $i = (scalar @args) - 1; $i >= -1; $i--) {
152 0         0 my $path;
153 0 0       0 if ($i >= 0) {
    0          
154 0         0 $path = $_[$i];
155             } elsif (!$resolvedDevice) {
156 0         0 $path = $CWD;
157             } else {
158             ##TODO
159             }
160            
161             ## skip empty paths
162 0 0       0 if (!$path) {
163 0         0 next;
164             }
165            
166 0         0 my @result = Path::Resolve::Utils::exec($splitDeviceRe,$path);
167 0   0     0 my $device = $result[1] || '';
168 0   0     0 $isUnc = $device && $device !~ /^.:/;
169 0         0 my $isAbsolute = $self->isAbsolute($path);
170 0         0 my $tail = $result[3];
171 0 0 0     0 if ($device &&
      0        
172             $resolvedDevice &&
173             ( lc $device ne lc $resolvedDevice ) ) {
174             #This path points to another device so it is not applicable
175 0         0 next;
176             }
177            
178 0 0       0 if (!$resolvedDevice) {
179 0         0 $resolvedDevice = $device;
180             }
181 0 0       0 if (!$resolvedAbsolute) {
182 0         0 $resolvedTail = $tail . '\\' . $resolvedTail;
183 0         0 $resolvedAbsolute = $isAbsolute;
184             }
185 0 0 0     0 if ($resolvedDevice && $resolvedAbsolute) {
186 0         0 last;
187             }
188             }
189            
190             #Convert slashes to backslashes when `resolvedDevice` points to an UNC
191             #root. Also squash multiple slashes into a single one where appropriate.
192 0 0       0 if ($isUnc) {
193 0         0 $resolvedDevice = normalizeUNCRoot($resolvedDevice);
194             }
195            
196 0 0       0 my @resolvedTail = grep {$_ if $_} split /[\\\/]+/, $resolvedTail;
  0         0  
197 0         0 @resolvedTail = Path::Resolve::Utils::normalizeArray(\@resolvedTail,!$resolvedAbsolute);
198 0         0 $resolvedTail = join '\\',@resolvedTail;
199 0   0     0 my $ret = ($resolvedDevice . ($resolvedAbsolute ? '\\' : '') . $resolvedTail) || '.';
200 0         0 return $ret;
201             }
202            
203             sub normalize {
204 0     0   0 my ($self, $path) = @_;
205 0         0 my @result = Path::Resolve::Utils::exec($splitDeviceRe,$path);
206 0   0     0 my $device = $result[1] || '';
207 0         0 my @device = split '',$device;
208 0   0     0 my $isUnc = @device && $device[1] ne ':';
209 0   0     0 my $isAbsolute = !!$result[2] || $isUnc;
210 0         0 my $tail = $result[3];
211 0         0 my $trailingSlash = $tail =~ /[\\\/]$/;
212             #If device is a drive letter, we'll normalize to lower case.
213 0 0 0     0 if (@device && $device[1] eq ':') {
214 0         0 $device = lc $device;
215             }
216            
217 0 0       0 my @tail = grep {$_ if $_} split /[\\\/]+/, $tail;
  0         0  
218 0         0 @tail = Path::Resolve::Utils::normalizeArray(\@tail,!$isAbsolute);
219 0         0 $tail = join '\\', @tail;
220 0 0 0     0 if (!$tail && !$isAbsolute) {
221 0         0 $tail = '.';
222             }
223 0 0 0     0 if ($tail && $trailingSlash) {
224 0         0 $tail .= '\\';
225             }
226             #Convert slashes to backslashes when `device` points to an UNC root.
227             #Also squash multiple slashes into a single one where appropriate.
228 0 0       0 if ($isUnc) {
229 0         0 $device = normalizeUNCRoot($device);
230             }
231            
232 0 0       0 return $device . ($isAbsolute ? '\\' : '') . $tail;
233             }
234            
235             sub join {
236 0     0   0 my $self = shift;
237 0 0       0 my @paths = grep {$_ if $_} @_;
  0         0  
238 0         0 my $joined = join '\\', @paths;
239 0 0 0     0 if ( @paths && $paths[0] !~ /^[\\\/]{2}[^\\\/]/ ) {
240 0         0 $joined =~ s/^[\\\/]{2,}/\\/;
241             }
242 0         0 return $self->normalize($joined);
243             }
244            
245             sub _trim {
246 0     0   0 my @arr = @_;
247 0         0 my $start = 0;
248 0         0 foreach my $a (@arr) {
249 0 0       0 last if ($a ne '');
250 0         0 $start++;
251             }
252 0         0 my $end = scalar @arr - 1;
253 0         0 while ($end >= 0) {
254 0 0       0 last if ($arr[$end] ne '');
255 0         0 $end--;
256             }
257 0 0       0 return () if ($start > $end);
258 0         0 return splice @arr,$start, $end - $start + 1;
259             }
260            
261             sub relative {
262 0     0   0 my ($self, $from, $to) = @_;
263 0         0 $from = $self->resolve($from);
264 0         0 $to = $self->resolve($to);
265             #windows is not case sensitive
266 0         0 my $lowerFrom = lc $from;
267 0         0 my $lowerTo = lc $to;
268 0         0 my @toParts = _trim(split(/\\/, $to));
269 0         0 my @lowerFromParts = _trim(split(/\\/,$lowerFrom));
270 0         0 my @lowerToParts = _trim(split(/\\/,$lowerTo));
271 0         0 my $length = do {
272 0         0 my $len = scalar @lowerFromParts;
273 0         0 my $len2 = scalar @lowerToParts;
274 0 0       0 $len < $len2 ? $len : $len2;
275             };
276            
277 0         0 my $samePartsLength = $length;
278 0         0 for (my $i = 0; $i < $length; $i++) {
279 0 0       0 if ($lowerFromParts[$i] ne $lowerToParts[$i]) {
280 0         0 $samePartsLength = $i;
281 0         0 last;
282             }
283             }
284 0 0       0 if ($samePartsLength == 0) {
285 0         0 return $to;
286             }
287            
288 0         0 my @outputParts = ();
289 0         0 for (my $i = $samePartsLength; $i < scalar @lowerFromParts; $i++) {
290 0         0 push @outputParts, ('..');
291             }
292            
293 0         0 push @outputParts, ( splice(@toParts,$samePartsLength) );
294 0         0 return CORE::join '\\',@outputParts;
295             }
296             } #end Path::Resolve::Win
297            
298            
299             package
300             Path::Resolve::POSIX; {
301 4     4   72 use strict;
  4         9  
  4         160  
302 4     4   20 use warnings;
  4         5  
  4         154  
303 4     4   20 use Carp;
  4         5  
  4         312  
304 4     4   22 use Cwd();
  4         5  
  4         5491  
305             my $CWD = Cwd::cwd();
306            
307 8     8   21 sub sep {'/'};
308 1     1   298 sub delimiter {':'};
309            
310             #'root' is just a slash, or nothing.
311             my $splitPathRe = qr/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
312             sub splitPath {
313 92     92   113 my ($filename) = @_;
314 92         187 my @res = Path::Resolve::Utils::exec($splitPathRe,$filename);
315 92         413 return splice @res, 1;
316             }
317            
318             sub parse {
319 9     9   3165 my ($self, $path) = @_;
320 9 50 33     64 if (!$path || ref $path){
321 0         0 croak("Parameter 'pathString' must be a string");
322             }
323            
324 9         22 my @allParts = splitPath($path);
325            
326 9 50 33     52 if (!@allParts || scalar @allParts != 4) {
327 0         0 croak("Invalid path '" . $path . "'");
328             }
329            
330 9   100     27 $allParts[1] ||= '';
331 9   50     21 $allParts[2] ||= '';
332 9   100     24 $allParts[3] ||= '';
333            
334             return {
335 9         83 root => $allParts[0],
336             dir => $allParts[0] . substr ($allParts[1], 0, -1),
337             base => $allParts[2],
338             ext => $allParts[3],
339             name => substr ($allParts[2], 0, length($allParts[2]) - length($allParts[3]))
340             };
341             }
342            
343             sub format {
344 12     12   1390 my ($self, $pathObject) = @_;
345 12 50       40 if ( ref $pathObject ne 'HASH' ) {
346 0         0 croak "Parameter 'pathObject' must be an Hash Ref";
347             }
348            
349 12   100     56 my $root = $pathObject->{root} || '';
350 12 50 33     56 if (!defined $root || ref $root) {
351 0         0 croak 'pathObject->{root} must be a string or undefined';
352             }
353            
354 12 100       54 my $dir = $pathObject->{dir} ? $pathObject->{dir} . $self->sep : '';
355 12   100     35 my $base = $pathObject->{base} || '';
356 12         55 return $dir . $base;
357             }
358            
359             sub resolve {
360 18     18   1946 my $self = shift;
361 18         27 my $resolvedPath = '';
362 18         17 my $resolvedAbsolute = 0;
363 18         33 my @args = @_;
364 18   100     101 for (my $i = (scalar @args) - 1; $i >= -1 && !$resolvedAbsolute; $i--) {
365 24 100       45 my $path = ($i >= 0) ? $args[$i] : $CWD;
366             #Skip empty and invalid entries
367 24 50       44 if (!$path) {
368 0         0 next;
369             }
370 24         44 $resolvedPath = $path . '/' . $resolvedPath;
371 24         142 $resolvedAbsolute = $path =~ m/^\//;
372             }
373            
374             #At this point the path should be resolved to a full absolute path, but
375             #handle relative paths to be safe (might happen when process.cwd() fails)
376             #Normalize the path
377 18 100       55 my @resolved = grep { $_ if $_ } split '/', $resolvedPath;
  61         171  
378 18         52 $resolvedPath = join '/', Path::Resolve::Utils::normalizeArray(\@resolved, !$resolvedAbsolute);
379 18   50     88 return (($resolvedAbsolute ? '/' : '') . $resolvedPath) || '.';
380             }
381            
382             sub isAbsolute {
383 101     101   111 my ($self, $path) = @_;
384 101         325 return $path =~ /^\//;
385             }
386            
387             sub normalize {
388 97     97   773 my ($self, $path) = @_;
389 97         180 my $isAbsolute = $self->isAbsolute($path);
390 97         198 my $trailingSlash = (substr $path, -1) eq '/';
391             #Normalize the path
392 97 100       264 my @resolved = grep { $_ if $_ } split '/', $path;
  337         886  
393 97         272 $path = join '/', Path::Resolve::Utils::normalizeArray(\@resolved, !$isAbsolute);
394 97 100 66     267 if (!$path && !$isAbsolute) {
395 20         26 $path = '.';
396             }
397 97 100 100     414 if ($path && $trailingSlash) {
398 8         14 $path .= '/';
399             }
400 97 100       402 return ($isAbsolute ? '/' : '') . $path;
401             }
402            
403             sub join {
404 92     92   38054 my $self = shift;
405 92         207 my @paths = @_;
406 92 100       141 my @norm = grep {$_ if $_} @paths;
  220         652  
407 92         299 return $self->normalize( join('/', @norm) );
408             }
409            
410             sub _trim {
411 12     12   14 my @arr = @_;
412 12         14 my $start = 0;
413 12         13 foreach my $a (@arr) {
414 11 50       27 last if ($a ne '');
415 0         0 $start++;
416             }
417 12         14 my $end = scalar @arr - 1;
418 12         17 while ($end >= 0) {
419 11 50       22 last if ($arr[$end] ne '');
420 0         0 $end--;
421             }
422 12 100       19 return () if ($start > $end);
423 11         27 return splice @arr,$start, $end - $start + 1;
424             }
425            
426             sub relative {
427 6     6   2072 my ($self, $from, $to) = @_;
428 6         13 $from = substr resolve($self,$from),1;
429 6         11 $to = substr resolve($self,$to),1;
430 6         18 my @fromParts = _trim( split('/',$from) );
431 6         13 my @toParts = _trim( split('/',$to) );
432 6         7 my $length = do {
433 6         6 my $len = scalar @fromParts;
434 6         5 my $len2 = scalar @toParts;
435 6 100       13 $len < $len2 ? $len : $len2;
436             };
437            
438 6         5 my $samePartsLength = $length;
439 6         14 for (my $i = 0; $i < $length; $i++) {
440 7 100       19 if ($fromParts[$i] ne $toParts[$i]) {
441 2         2 $samePartsLength = $i;
442 2         2 last;
443             }
444             }
445            
446 6         7 my @outputParts = ();
447 6         12 for (my $i = $samePartsLength; $i < scalar @fromParts; $i++) {
448 4         16 push @outputParts, ('..');
449             }
450            
451 6         8 push @outputParts, ( splice(@toParts,$samePartsLength) );
452 6         19 return CORE::join '/',@outputParts;
453             }
454             } #end Path::Resolve::Posix
455            
456             package
457             Path::Resolve::Utils; {
458 4     4   48 use strict;
  4         8  
  4         218  
459 4     4   24 use warnings;
  4         8  
  4         1333  
460            
461             #==========================================================================
462             # javascript like exec function
463             # we can do it the perl way but for fun I want to emulate node path module
464             # the way it is :)
465             #==========================================================================
466             sub exec {
467 92     92   116 my ($expr,$string) = @_;
468 92         1688 my @m = $string =~ $expr;
469 92 50       280 if (@m){
470 92         445 unshift @m, substr $string,$-[0],$+[0];
471             }
472 92         404 return @m;
473             }
474            
475             sub normalizeArray {
476 115     115   150 my ($parts, $allowAboveRoot) = @_;
477             #if the path tries to go above the root, `up` ends up > 0
478 115         105 my @parts = @{$parts};
  115         222  
479 115         117 my $up = 0;
480 115         323 for (my $i = (scalar @parts) - 1; $i >= 0; $i--) {
481 271         835 my $last = $parts->[$i];
482 271 100       845 if ($last eq '.') {
    100          
    100          
483 58         152 splice @parts, $i, 1;
484             } elsif ($last eq '..') {
485 58         87 splice @parts, $i, 1;
486 58         132 $up++;
487             } elsif ($up) {
488 21         25 splice @parts, $i, 1;
489 21         51 $up--;
490             }
491             }
492            
493             #if the path is allowed to go above the root, restore leading ..s
494 115 100       191 if ($allowAboveRoot) {
495 68         150 while ($up--) {
496 24         62 unshift @parts, ('..');
497             }
498             }
499 115         321 return @parts;
500             }
501             } # end Path::Resolve::Utils
502            
503             1;
504            
505             __END__