OSDN Git Service

Version 5.91
[vbslib/main.git] / GPL_bin_fullset / NaturalDocs / Modules / NaturalDocs / File.pm
1 ###############################################################################
2 #
3 #   Package: NaturalDocs::File
4 #
5 ###############################################################################
6 #
7 #   A package to manage file access across platforms.  Incorporates functions from various standard File:: packages, but more
8 #   importantly, works around the glorious suckage present in File::Spec, at least in version 0.82 and earlier.  Read the "Why oh
9 #   why?" sections for why this package was necessary.
10 #
11 #   Usage and Dependencies:
12 #
13 #       - The package doesn't depend on any other Natural Docs packages and is ready to use immediately.
14 #
15 #       - All functions except <CanonizePath()> assume that all parameters are canonized.
16 #
17 ###############################################################################
18
19 # This file is part of Natural Docs, which is Copyright © 2003-2010 Greg Valure
20 # Natural Docs is licensed under version 3 of the GNU Affero General Public License (AGPL)
21 # Refer to License.txt for the complete details
22
23 use File::Spec ();
24 use File::Path ();
25 use File::Copy ();
26
27 use strict;
28 use integer;
29
30 package NaturalDocs::File;
31
32
33 #
34 #   Function: CheckCompatibility
35 #
36 #   Checks if the standard packages required by this one are up to snuff and dies if they aren't.  This is done because I can't
37 #   tell which versions of File::Spec have splitpath just by the version numbers.
38 #
39 sub CheckCompatibility
40     {
41     my ($self) = @_;
42
43     eval {
44         File::Spec->splitpath('');
45     };
46
47     if ($@)
48         {
49         NaturalDocs::Error->SoftDeath("Natural Docs requires a newer version of File::Spec than you have.  "
50                                                     . "You must either upgrade it or upgrade Perl.");
51         };
52     };
53
54
55 ###############################################################################
56 # Group: Path String Functions
57
58
59 #
60 #   Function: CanonizePath
61 #
62 #   Takes a path and returns a logically simplified version of it.
63 #
64 #   Why oh why?:
65 #
66 #       Because File::Spec->canonpath doesn't strip quotes on Windows.  So if you pass in "a b\c" or "a b"\c, they still end up as
67 #       different strings even though they're logically the same.
68 #
69 #       It also doesn't remove things like "..", so "a/b/../c" doesn't simplify to "a/c" like it should.
70 #
71 sub CanonizePath #(path)
72     {
73     my ($self, $path) = @_;
74
75     if ($::OSNAME eq 'MSWin32')
76         {
77         # We don't have to use a smarter algorithm for dropping quotes because they're invalid characters for actual file and
78         # directory names.
79         $path =~ s/\"//g;
80         };
81
82     $path = File::Spec->canonpath($path);
83
84     # Condense a/b/../c into a/c.
85
86     my $upDir = File::Spec->updir();
87     if (index($path, $upDir) != -1)
88         {
89         my ($volume, $directoryString, $file) = $self->SplitPath($path);
90         my @directories = $self->SplitDirectories($directoryString);
91
92         my $i = 1;
93         while ($i < scalar @directories)
94             {
95             if ($i > 0 && $directories[$i] eq $upDir && $directories[$i - 1] ne $upDir)
96                 {
97                 splice(@directories, $i - 1, 2);
98                 $i--;
99                 }
100             else
101                 {  $i++;  };
102             };
103
104         $directoryString = $self->JoinDirectories(@directories);
105         $path = $self->JoinPath($volume, $directoryString, $file);
106         };
107
108     return $path;
109     };
110
111
112 #
113 #   Function: PathIsAbsolute
114 #
115 #   Returns whether the passed path is absolute.
116 #
117 sub PathIsAbsolute #(path)
118     {
119     my ($self, $path) = @_;
120     return File::Spec->file_name_is_absolute($path);
121     };
122
123
124 #
125 #   Function: JoinPath
126 #
127 #   Creates a path from its elements.
128 #
129 #   Parameters:
130 #
131 #       volume - The volume, such as the drive letter on Windows.  Undef if none.
132 #       dirString - The directory string.  Create with <JoinDirectories()> if necessary.
133 #       file - The file name, or undef if none.
134 #
135 #   Returns:
136 #
137 #       The joined path.
138 #
139 sub JoinPath #(volume, dirString, $file)
140     {
141     my ($self, $volume, $dirString, $file) = @_;
142     return File::Spec->catpath($volume, $dirString, $file);
143     };
144
145
146 #
147 #   Function: JoinPaths
148 #
149 #   Joins two paths.
150 #
151 #   Parameters:
152 #
153 #       basePath       - May be a relative path, an absolute path, or undef.
154 #       extraPath      - May be a relative path, a file, a relative path and file together, or undef.
155 #       noFileInExtra - Set this to true if extraPath is a relative path only, and doesn't have a file.
156 #
157 #   Returns:
158 #
159 #       The joined path.
160 #
161 #   Why oh why?:
162 #
163 #       Because nothing in File::Spec will simply slap two paths together.  They have to be split up for catpath/file, and rel2abs
164 #       requires the base to be absolute.
165 #
166 sub JoinPaths #(basePath, extraPath, noFileInExtra)
167     {
168     my ($self, $basePath, $extraPath, $noFileInExtra) = @_;
169
170     # If both are undef, it will return undef, which is what we want.
171     if (!defined $basePath)
172         {  return $extraPath;  }
173     elsif (!defined $extraPath)
174         {  return $basePath;  };
175
176     my ($baseVolume, $baseDirString, $baseFile) = File::Spec->splitpath($basePath, 1);
177     my ($extraVolume, $extraDirString, $extraFile) = File::Spec->splitpath($extraPath, $noFileInExtra);
178
179     my @baseDirectories = $self->SplitDirectories($baseDirString);
180     my @extraDirectories = $self->SplitDirectories($extraDirString);
181
182     my $fullDirString = $self->JoinDirectories(@baseDirectories, @extraDirectories);
183
184     my $fullPath = File::Spec->catpath($baseVolume, $fullDirString, $extraFile);
185
186     return $self->CanonizePath($fullPath);
187     };
188
189
190 #
191 #   Function: SplitPath
192 #
193 #   Takes a path and returns its elements.
194 #
195 #   Parameters:
196 #
197 #       path - The path to split.
198 #       noFile - Set to true if the path doesn't have a file at the end.
199 #
200 #   Returns:
201 #
202 #       The array ( volume, directoryString, file ).  If any don't apply, they will be undef.  Use <SplitDirectories()> to split the
203 #       directory string if desired.
204 #
205 #   Why oh Why?:
206 #
207 #       Because File::Spec->splitpath may leave a trailing slash/backslash/whatever on the directory string, which makes
208 #       it a bit hard to match it with results from File::Spec->catdir.
209 #
210 sub SplitPath #(path, noFile)
211     {
212     my ($self, $path, $noFile) = @_;
213
214     my @segments = File::Spec->splitpath($path, $noFile);
215
216     if (!length $segments[0])
217         {  $segments[0] = undef;  };
218     if (!length $segments[2])
219         {  $segments[2] = undef;  };
220
221     $segments[1] = File::Spec->catdir( File::Spec->splitdir($segments[1]) );
222
223     return @segments;
224     };
225
226
227 #
228 #   Function: JoinDirectories
229 #
230 #   Creates a directory string from an array of directory names.
231 #
232 #   Parameters:
233 #
234 #       directory - A directory name.  There may be as many of these as desired.
235 #
236 sub JoinDirectories #(directory, directory, ...)
237     {
238     my ($self, @directories) = @_;
239     return File::Spec->catdir(@directories);
240     };
241
242
243 #
244 #   Function: SplitDirectories
245 #
246 #   Takes a string of directories and returns an array of its elements.
247 #
248 #   Why oh why?:
249 #
250 #       Because File::Spec->splitdir might leave an empty element at the end of the array, which screws up both joining in
251 #       <ConvertToURL> and navigation in <MakeRelativePath>.
252 #
253 sub SplitDirectories #(directoryString)
254     {
255     my ($self, $directoryString) = @_;
256
257     my @directories = File::Spec->splitdir($directoryString);
258
259     if (!length $directories[-1])
260         {  pop @directories;  };
261
262     return @directories;
263     };
264
265
266 #
267 #   Function: MakeRelativePath
268 #
269 #   Takes two paths and returns a relative path between them.
270 #
271 #   Parameters:
272 #
273 #       basePath    - The starting path.  May be relative or absolute, so long as the target path is as well.
274 #       targetPath  - The target path.  May be relative or absolute, so long as the base path is as well.
275 #
276 #       If both paths are relative, they are assumed to be relative to the same base.
277 #
278 #   Returns:
279 #
280 #       The target path relative to base.
281 #
282 #   Why oh why?:
283 #
284 #       First, there's nothing that gives a relative path between two relative paths.
285 #
286 #       Second, if target and base are absolute but on different volumes, File::Spec->abs2rel creates a totally non-functional
287 #       relative path.  It should return the target as is, since there is no relative path.
288 #
289 #       Third, File::Spec->abs2rel between absolute paths on the same volume, at least on Windows, leaves the drive letter
290 #       on.  So abs2rel('a:\b\c\d', 'a:\b') returns 'a:c\d' instead of the expected 'c\d'.  That makes no sense whatsoever.  It's
291 #       not like it was designed to handle only directory names, either; the documentation says 'path' and the code seems to
292 #       explicitly handle it.  There's just an 'unless' in there that tacks on the volume, defeating the purpose of a *relative* path
293 #       and making the function worthless.
294 #
295 sub MakeRelativePath #(basePath, targetPath)
296     {
297     my ($self, $basePath, $targetPath) = @_;
298
299     my ($baseVolume, $baseDirString, $baseFile) = $self->SplitPath($basePath, 1);
300     my ($targetVolume, $targetDirString, $targetFile) = $self->SplitPath($targetPath);
301
302     # If the volumes are different, there is no possible relative path.
303     if ($targetVolume ne $baseVolume)
304         {  return $targetPath;  };
305
306     my @baseDirectories = $self->SplitDirectories($baseDirString);
307     my @targetDirectories = $self->SplitDirectories($targetDirString);
308
309     # Skip the parts of the path that are the same.
310     while (scalar @baseDirectories && @targetDirectories && $baseDirectories[0] eq $targetDirectories[0])
311         {
312         shift @baseDirectories;
313         shift @targetDirectories;
314         };
315
316     # Back out of the base path until it reaches where they were similar.
317     for (my $i = 0; $i < scalar @baseDirectories; $i++)
318         {
319         unshift @targetDirectories, File::Spec->updir();
320         };
321
322     $targetDirString = $self->JoinDirectories(@targetDirectories);
323
324     return File::Spec->catpath(undef, $targetDirString, $targetFile);
325     };
326
327
328 #
329 #   Function: IsSubPathOf
330 #
331 #   Returns whether the path is a descendant of another path.
332 #
333 #   Parameters:
334 #
335 #       base - The base path to test against.
336 #       path - The possible subpath to test.
337 #
338 #   Returns:
339 #
340 #       Whether path is a descendant of base.
341 #
342 sub IsSubPathOf #(base, path)
343     {
344     my ($self, $base, $path) = @_;
345
346     # This is a quick test that should find a false quickly.
347     if ($base eq substr($path, 0, length($base)))
348         {
349         # This doesn't guarantee true, because it could be "C:\A B" and "C:\A B C\File".  So we test for it by seeing if the last
350         # directory in base is the same as the equivalent directory in path.
351
352         my ($baseVolume, $baseDirString, $baseFile) = NaturalDocs::File->SplitPath($base, 1);
353         my @baseDirectories = NaturalDocs::File->SplitDirectories($baseDirString);
354
355         my ($pathVolume, $pathDirString, $pathFile) = NaturalDocs::File->SplitPath($path);
356         my @pathDirectories = NaturalDocs::File->SplitDirectories($pathDirString);
357
358         return ( $baseDirectories[-1] eq $pathDirectories[ scalar @baseDirectories - 1 ] );
359         }
360     else
361         {  return undef;  };
362     };
363
364
365 #
366 #   Function: ConvertToURL
367 #
368 #   Takes a relative path and converts it from the native format to a relative URL.  Note that it _doesn't_ convert special characters
369 #   to amp chars.
370 #
371 sub ConvertToURL #(path)
372     {
373     my ($self, $path) = @_;
374
375     my ($pathVolume, $pathDirString, $pathFile) = $self->SplitPath($path);
376     my @pathDirectories = $self->SplitDirectories($pathDirString);
377
378     my $i = 0;
379     while ($i < scalar @pathDirectories && $pathDirectories[$i] eq File::Spec->updir())
380         {
381         $pathDirectories[$i] = '..';
382         $i++;
383         };
384
385     return join('/', @pathDirectories, $pathFile);
386     };
387
388
389 #
390 #   Function: NoUpwards
391 #
392 #   Takes an array of directory entries and returns one without all the entries that refer to the parent directory, such as '.' and '..'.
393 #
394 sub NoUpwards #(array)
395     {
396     my ($self, @array) = @_;
397     return File::Spec->no_upwards(@array);
398     };
399
400
401 #
402 #   Function: NoFileName
403 #
404 #   Takes a path and returns a version without the file name.  Useful for sending paths to <CreatePath()>.
405 #
406 sub NoFileName #(path)
407     {
408     my ($self, $path) = @_;
409
410     my ($pathVolume, $pathDirString, $pathFile) = File::Spec->splitpath($path);
411
412     return File::Spec->catpath($pathVolume, $pathDirString, undef);
413     };
414
415
416 #
417 #   Function: NoExtension
418 #
419 #   Returns the path without an extension.
420 #
421 sub NoExtension #(path)
422     {
423     my ($self, $path) = @_;
424
425     my $extension = $self->ExtensionOf($path);
426
427     if ($extension)
428         {  $path = substr($path, 0, length($path) - length($extension) - 1);  };
429
430     return $path;
431     };
432
433
434 #
435 #   Function: ExtensionOf
436 #
437 #   Returns the extension of the passed path, or undef if none.
438 #
439 sub ExtensionOf #(path)
440     {
441     my ($self, $path) = @_;
442
443     my ($pathVolume, $pathDirString, $pathFile) = File::Spec->splitpath($path);
444
445     # We need the leading dot in the regex so files that start with a dot but don't have an extension count as extensionless files.
446     if ($pathFile =~ /.\.([^\.]+)$/)
447         {  return $1;  }
448     else
449         {  return undef;  };
450     };
451
452
453 #
454 #   Function: IsCaseSensitive
455 #
456 #   Returns whether the current platform has case-sensitive paths.
457 #
458 sub IsCaseSensitive
459     {
460     return !(File::Spec->case_tolerant());
461     };
462
463
464
465 ###############################################################################
466 # Group: Disk Functions
467
468
469 #
470 #   Function: CreatePath
471 #
472 #   Creates a directory tree corresponding to the passed path, regardless of how many directories do or do not already exist.
473 #   Do _not_ include a file name in the path.  Use <NoFileName()> first if you need to.
474 #
475 sub CreatePath #(path)
476     {
477     my ($self, $path) = @_;
478     File::Path::mkpath($path);
479     };
480
481
482 #
483 #   Function: RemoveEmptyTree
484 #
485 #   Removes an empty directory tree.  The passed directory will be removed if it's empty, and it will keep removing its parents
486 #   until it reaches one that's not empty or a set limit.
487 #
488 #   Parameters:
489 #
490 #       path - The path to start from.  It will try to remove this directory and work it's way down.
491 #       limit - The path to stop at if it doesn't find any non-empty directories first.  This path will *not* be removed.
492 #
493 sub RemoveEmptyTree #(path, limit)
494     {
495     my ($self, $path, $limit) = @_;
496
497     my ($volume, $directoryString) = $self->SplitPath($path, 1);
498     my @directories = $self->SplitDirectories($directoryString);
499
500     my $directory = $path;
501
502     while (-d $directory && $directory ne $limit)
503         {
504         opendir FH_ND_FILE, $directory;
505         my @entries = readdir FH_ND_FILE;
506         closedir FH_ND_FILE;
507
508         @entries = $self->NoUpwards(@entries);
509
510         if (scalar @entries || !rmdir($directory))
511             {  last;  };
512
513         pop @directories;
514         $directoryString = $self->JoinDirectories(@directories);
515         $directory = $self->JoinPath($volume, $directoryString);
516         };
517     };
518
519
520 #
521 #   Function: Copy
522 #
523 #   Copies a file from one path to another.  If the destination file exists, it is overwritten.
524 #
525 #   Parameters:
526 #
527 #       source       - The file to copy.
528 #       destination - The destination to copy to.
529 #
530 #   Returns:
531 #
532 #       Whether it succeeded
533 #
534 sub Copy #(source, destination) => bool
535     {
536     my ($self, $source, $destination) = @_;
537     return File::Copy::copy($source, $destination);
538     };
539
540
541 1;