145
|
1 // Written in the D programming language.
|
|
2
|
|
3 /** This module is used to manipulate _path strings.
|
|
4
|
|
5 All functions, with the exception of $(LREF expandTilde) (and in some
|
|
6 cases $(LREF absolutePath) and $(LREF relativePath)), are pure
|
|
7 string manipulation functions; they don't depend on any state outside
|
|
8 the program, nor do they perform any actual file system actions.
|
|
9 This has the consequence that the module does not make any distinction
|
|
10 between a _path that points to a directory and a _path that points to a
|
|
11 file, and it does not know whether or not the object pointed to by the
|
|
12 _path actually exists in the file system.
|
|
13 To differentiate between these cases, use $(REF isDir, std,file) and
|
|
14 $(REF exists, std,file).
|
|
15
|
|
16 Note that on Windows, both the backslash ($(D `\`)) and the slash ($(D `/`))
|
|
17 are in principle valid directory separators. This module treats them
|
|
18 both on equal footing, but in cases where a $(I new) separator is
|
|
19 added, a backslash will be used. Furthermore, the $(LREF buildNormalizedPath)
|
|
20 function will replace all slashes with backslashes on that platform.
|
|
21
|
|
22 In general, the functions in this module assume that the input paths
|
|
23 are well-formed. (That is, they should not contain invalid characters,
|
|
24 they should follow the file system's _path format, etc.) The result
|
|
25 of calling a function on an ill-formed _path is undefined. When there
|
|
26 is a chance that a _path or a file name is invalid (for instance, when it
|
|
27 has been input by the user), it may sometimes be desirable to use the
|
|
28 $(LREF isValidFilename) and $(LREF isValidPath) functions to check
|
|
29 this.
|
|
30
|
|
31 Most functions do not perform any memory allocations, and if a string is
|
|
32 returned, it is usually a slice of an input string. If a function
|
|
33 allocates, this is explicitly mentioned in the documentation.
|
|
34
|
|
35 $(SCRIPT inhibitQuickIndex = 1;)
|
|
36 $(DIVC quickindex,
|
|
37 $(BOOKTABLE,
|
|
38 $(TR $(TH Category) $(TH Functions))
|
|
39 $(TR $(TD Normalization) $(TD
|
|
40 $(LREF absolutePath)
|
|
41 $(LREF asAbsolutePath)
|
|
42 $(LREF asNormalizedPath)
|
|
43 $(LREF asRelativePath)
|
|
44 $(LREF buildNormalizedPath)
|
|
45 $(LREF buildPath)
|
|
46 $(LREF chainPath)
|
|
47 $(LREF expandTilde)
|
|
48 ))
|
|
49 $(TR $(TD Partitioning) $(TD
|
|
50 $(LREF baseName)
|
|
51 $(LREF dirName)
|
|
52 $(LREF dirSeparator)
|
|
53 $(LREF driveName)
|
|
54 $(LREF pathSeparator)
|
|
55 $(LREF pathSplitter)
|
|
56 $(LREF relativePath)
|
|
57 $(LREF rootName)
|
|
58 $(LREF stripDrive)
|
|
59 ))
|
|
60 $(TR $(TD Validation) $(TD
|
|
61 $(LREF isAbsolute)
|
|
62 $(LREF isDirSeparator)
|
|
63 $(LREF isRooted)
|
|
64 $(LREF isValidFilename)
|
|
65 $(LREF isValidPath)
|
|
66 ))
|
|
67 $(TR $(TD Extension) $(TD
|
|
68 $(LREF defaultExtension)
|
|
69 $(LREF extension)
|
|
70 $(LREF setExtension)
|
|
71 $(LREF stripExtension)
|
|
72 $(LREF withDefaultExtension)
|
|
73 $(LREF withExtension)
|
|
74 ))
|
|
75 $(TR $(TD Other) $(TD
|
|
76 $(LREF filenameCharCmp)
|
|
77 $(LREF filenameCmp)
|
|
78 $(LREF globMatch)
|
|
79 $(LREF CaseSensitive)
|
|
80 ))
|
|
81 ))
|
|
82
|
|
83 Authors:
|
|
84 Lars Tandle Kyllingstad,
|
|
85 $(HTTP digitalmars.com, Walter Bright),
|
|
86 Grzegorz Adam Hankiewicz,
|
|
87 Thomas K$(UUML)hne,
|
|
88 $(HTTP erdani.org, Andrei Alexandrescu)
|
|
89 Copyright:
|
|
90 Copyright (c) 2000-2014, the authors. All rights reserved.
|
|
91 License:
|
|
92 $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0)
|
|
93 Source:
|
|
94 $(PHOBOSSRC std/_path.d)
|
|
95 */
|
|
96 module std.path;
|
|
97
|
|
98
|
|
99 // FIXME
|
|
100 import std.file; //: getcwd;
|
|
101 static import std.meta;
|
|
102 import std.range.primitives;
|
|
103 import std.traits;
|
|
104
|
|
105 version (unittest)
|
|
106 {
|
|
107 private:
|
|
108 struct TestAliasedString
|
|
109 {
|
|
110 string get() @safe @nogc pure nothrow { return _s; }
|
|
111 alias get this;
|
|
112 @disable this(this);
|
|
113 string _s;
|
|
114 }
|
|
115
|
|
116 bool testAliasedString(alias func, Args...)(string s, Args args)
|
|
117 {
|
|
118 return func(TestAliasedString(s), args) == func(s, args);
|
|
119 }
|
|
120 }
|
|
121
|
|
122 /** String used to separate directory names in a path. Under
|
|
123 POSIX this is a slash, under Windows a backslash.
|
|
124 */
|
|
125 version (Posix) enum string dirSeparator = "/";
|
|
126 else version (Windows) enum string dirSeparator = "\\";
|
|
127 else static assert(0, "unsupported platform");
|
|
128
|
|
129
|
|
130
|
|
131
|
|
132 /** Path separator string. A colon under POSIX, a semicolon
|
|
133 under Windows.
|
|
134 */
|
|
135 version (Posix) enum string pathSeparator = ":";
|
|
136 else version (Windows) enum string pathSeparator = ";";
|
|
137 else static assert(0, "unsupported platform");
|
|
138
|
|
139
|
|
140
|
|
141
|
|
142 /** Determines whether the given character is a directory separator.
|
|
143
|
|
144 On Windows, this includes both $(D `\`) and $(D `/`).
|
|
145 On POSIX, it's just $(D `/`).
|
|
146 */
|
|
147 bool isDirSeparator(dchar c) @safe pure nothrow @nogc
|
|
148 {
|
|
149 if (c == '/') return true;
|
|
150 version (Windows) if (c == '\\') return true;
|
|
151 return false;
|
|
152 }
|
|
153
|
|
154
|
|
155 /* Determines whether the given character is a drive separator.
|
|
156
|
|
157 On Windows, this is true if c is the ':' character that separates
|
|
158 the drive letter from the rest of the path. On POSIX, this always
|
|
159 returns false.
|
|
160 */
|
|
161 private bool isDriveSeparator(dchar c) @safe pure nothrow @nogc
|
|
162 {
|
|
163 version (Windows) return c == ':';
|
|
164 else return false;
|
|
165 }
|
|
166
|
|
167
|
|
168 /* Combines the isDirSeparator and isDriveSeparator tests. */
|
|
169 version (Windows) private bool isSeparator(dchar c) @safe pure nothrow @nogc
|
|
170 {
|
|
171 return isDirSeparator(c) || isDriveSeparator(c);
|
|
172 }
|
|
173 version (Posix) private alias isSeparator = isDirSeparator;
|
|
174
|
|
175
|
|
176 /* Helper function that determines the position of the last
|
|
177 drive/directory separator in a string. Returns -1 if none
|
|
178 is found.
|
|
179 */
|
|
180 private ptrdiff_t lastSeparator(R)(R path)
|
|
181 if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
182 isNarrowString!R)
|
|
183 {
|
|
184 auto i = (cast(ptrdiff_t) path.length) - 1;
|
|
185 while (i >= 0 && !isSeparator(path[i])) --i;
|
|
186 return i;
|
|
187 }
|
|
188
|
|
189
|
|
190 version (Windows)
|
|
191 {
|
|
192 private bool isUNC(R)(R path)
|
|
193 if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
194 isNarrowString!R)
|
|
195 {
|
|
196 return path.length >= 3 && isDirSeparator(path[0]) && isDirSeparator(path[1])
|
|
197 && !isDirSeparator(path[2]);
|
|
198 }
|
|
199
|
|
200 private ptrdiff_t uncRootLength(R)(R path)
|
|
201 if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
202 isNarrowString!R)
|
|
203 in { assert(isUNC(path)); }
|
|
204 body
|
|
205 {
|
|
206 ptrdiff_t i = 3;
|
|
207 while (i < path.length && !isDirSeparator(path[i])) ++i;
|
|
208 if (i < path.length)
|
|
209 {
|
|
210 auto j = i;
|
|
211 do { ++j; } while (j < path.length && isDirSeparator(path[j]));
|
|
212 if (j < path.length)
|
|
213 {
|
|
214 do { ++j; } while (j < path.length && !isDirSeparator(path[j]));
|
|
215 i = j;
|
|
216 }
|
|
217 }
|
|
218 return i;
|
|
219 }
|
|
220
|
|
221 private bool hasDrive(R)(R path)
|
|
222 if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
223 isNarrowString!R)
|
|
224 {
|
|
225 return path.length >= 2 && isDriveSeparator(path[1]);
|
|
226 }
|
|
227
|
|
228 private bool isDriveRoot(R)(R path)
|
|
229 if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
230 isNarrowString!R)
|
|
231 {
|
|
232 return path.length >= 3 && isDriveSeparator(path[1])
|
|
233 && isDirSeparator(path[2]);
|
|
234 }
|
|
235 }
|
|
236
|
|
237
|
|
238 /* Helper functions that strip leading/trailing slashes and backslashes
|
|
239 from a path.
|
|
240 */
|
|
241 private auto ltrimDirSeparators(R)(R path)
|
|
242 if (isInputRange!R && !isInfinite!R && isSomeChar!(ElementType!R) ||
|
|
243 isNarrowString!R)
|
|
244 {
|
|
245 static if (isRandomAccessRange!R && hasSlicing!R || isNarrowString!R)
|
|
246 {
|
|
247 int i = 0;
|
|
248 while (i < path.length && isDirSeparator(path[i]))
|
|
249 ++i;
|
|
250 return path[i .. path.length];
|
|
251 }
|
|
252 else
|
|
253 {
|
|
254 while (!path.empty && isDirSeparator(path.front))
|
|
255 path.popFront();
|
|
256 return path;
|
|
257 }
|
|
258 }
|
|
259
|
|
260 @system unittest
|
|
261 {
|
|
262 import std.array;
|
|
263 import std.utf : byDchar;
|
|
264
|
|
265 assert(ltrimDirSeparators("//abc//").array == "abc//");
|
|
266 assert(ltrimDirSeparators("//abc//"d).array == "abc//"d);
|
|
267 assert(ltrimDirSeparators("//abc//".byDchar).array == "abc//"d);
|
|
268 }
|
|
269
|
|
270 private auto rtrimDirSeparators(R)(R path)
|
|
271 if (isBidirectionalRange!R && isSomeChar!(ElementType!R) ||
|
|
272 isNarrowString!R)
|
|
273 {
|
|
274 static if (isRandomAccessRange!R && hasSlicing!R && hasLength!R || isNarrowString!R)
|
|
275 {
|
|
276 auto i = (cast(ptrdiff_t) path.length) - 1;
|
|
277 while (i >= 0 && isDirSeparator(path[i]))
|
|
278 --i;
|
|
279 return path[0 .. i+1];
|
|
280 }
|
|
281 else
|
|
282 {
|
|
283 while (!path.empty && isDirSeparator(path.back))
|
|
284 path.popBack();
|
|
285 return path;
|
|
286 }
|
|
287 }
|
|
288
|
|
289 @system unittest
|
|
290 {
|
|
291 import std.array;
|
|
292 import std.utf : byDchar;
|
|
293
|
|
294 assert(rtrimDirSeparators("//abc//").array == "//abc");
|
|
295 assert(rtrimDirSeparators("//abc//"d).array == "//abc"d);
|
|
296
|
|
297 assert(rtrimDirSeparators(MockBiRange!char("//abc//")).array == "//abc");
|
|
298 }
|
|
299
|
|
300 private auto trimDirSeparators(R)(R path)
|
|
301 if (isBidirectionalRange!R && isSomeChar!(ElementType!R) ||
|
|
302 isNarrowString!R)
|
|
303 {
|
|
304 return ltrimDirSeparators(rtrimDirSeparators(path));
|
|
305 }
|
|
306
|
|
307 @system unittest
|
|
308 {
|
|
309 import std.array;
|
|
310 import std.utf : byDchar;
|
|
311
|
|
312 assert(trimDirSeparators("//abc//").array == "abc");
|
|
313 assert(trimDirSeparators("//abc//"d).array == "abc"d);
|
|
314
|
|
315 assert(trimDirSeparators(MockBiRange!char("//abc//")).array == "abc");
|
|
316 }
|
|
317
|
|
318
|
|
319
|
|
320
|
|
321 /** This $(D enum) is used as a template argument to functions which
|
|
322 compare file names, and determines whether the comparison is
|
|
323 case sensitive or not.
|
|
324 */
|
|
325 enum CaseSensitive : bool
|
|
326 {
|
|
327 /// File names are case insensitive
|
|
328 no = false,
|
|
329
|
|
330 /// File names are case sensitive
|
|
331 yes = true,
|
|
332
|
|
333 /** The default (or most common) setting for the current platform.
|
|
334 That is, $(D no) on Windows and Mac OS X, and $(D yes) on all
|
|
335 POSIX systems except OS X (Linux, *BSD, etc.).
|
|
336 */
|
|
337 osDefault = osDefaultCaseSensitivity
|
|
338 }
|
|
339 version (Windows) private enum osDefaultCaseSensitivity = false;
|
|
340 else version (OSX) private enum osDefaultCaseSensitivity = false;
|
|
341 else version (Posix) private enum osDefaultCaseSensitivity = true;
|
|
342 else static assert(0);
|
|
343
|
|
344
|
|
345
|
|
346
|
|
347 /**
|
|
348 Params:
|
|
349 cs = Whether or not suffix matching is case-sensitive.
|
|
350 path = A path name. It can be a string, or any random-access range of
|
|
351 characters.
|
|
352 suffix = An optional suffix to be removed from the file name.
|
|
353 Returns: The name of the file in the path name, without any leading
|
|
354 directory and with an optional suffix chopped off.
|
|
355
|
|
356 If $(D suffix) is specified, it will be compared to $(D path)
|
|
357 using $(D filenameCmp!cs),
|
|
358 where $(D cs) is an optional template parameter determining whether
|
|
359 the comparison is case sensitive or not. See the
|
|
360 $(LREF filenameCmp) documentation for details.
|
|
361
|
|
362 Example:
|
|
363 ---
|
|
364 assert(baseName("dir/file.ext") == "file.ext");
|
|
365 assert(baseName("dir/file.ext", ".ext") == "file");
|
|
366 assert(baseName("dir/file.ext", ".xyz") == "file.ext");
|
|
367 assert(baseName("dir/filename", "name") == "file");
|
|
368 assert(baseName("dir/subdir/") == "subdir");
|
|
369
|
|
370 version (Windows)
|
|
371 {
|
|
372 assert(baseName(`d:file.ext`) == "file.ext");
|
|
373 assert(baseName(`d:\dir\file.ext`) == "file.ext");
|
|
374 }
|
|
375 ---
|
|
376
|
|
377 Note:
|
|
378 This function $(I only) strips away the specified suffix, which
|
|
379 doesn't necessarily have to represent an extension.
|
|
380 To remove the extension from a path, regardless of what the extension
|
|
381 is, use $(LREF stripExtension).
|
|
382 To obtain the filename without leading directories and without
|
|
383 an extension, combine the functions like this:
|
|
384 ---
|
|
385 assert(baseName(stripExtension("dir/file.ext")) == "file");
|
|
386 ---
|
|
387
|
|
388 Standards:
|
|
389 This function complies with
|
|
390 $(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/utilities/basename.html,
|
|
391 the POSIX requirements for the 'basename' shell utility)
|
|
392 (with suitable adaptations for Windows paths).
|
|
393 */
|
|
394 auto baseName(R)(R path)
|
|
395 if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) && !isSomeString!R)
|
|
396 {
|
|
397 return _baseName(path);
|
|
398 }
|
|
399
|
|
400 /// ditto
|
|
401 auto baseName(C)(C[] path)
|
|
402 if (isSomeChar!C)
|
|
403 {
|
|
404 return _baseName(path);
|
|
405 }
|
|
406
|
|
407 private R _baseName(R)(R path)
|
|
408 if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || isNarrowString!R)
|
|
409 {
|
|
410 auto p1 = stripDrive(path);
|
|
411 if (p1.empty)
|
|
412 {
|
|
413 version (Windows) if (isUNC(path))
|
|
414 return path[0 .. 1];
|
|
415 static if (isSomeString!R)
|
|
416 return null;
|
|
417 else
|
|
418 return p1; // which is empty
|
|
419 }
|
|
420
|
|
421 auto p2 = rtrimDirSeparators(p1);
|
|
422 if (p2.empty) return p1[0 .. 1];
|
|
423
|
|
424 return p2[lastSeparator(p2)+1 .. p2.length];
|
|
425 }
|
|
426
|
|
427 /// ditto
|
|
428 inout(C)[] baseName(CaseSensitive cs = CaseSensitive.osDefault, C, C1)
|
|
429 (inout(C)[] path, in C1[] suffix)
|
|
430 @safe pure //TODO: nothrow (because of filenameCmp())
|
|
431 if (isSomeChar!C && isSomeChar!C1)
|
|
432 {
|
|
433 auto p = baseName(path);
|
|
434 if (p.length > suffix.length
|
|
435 && filenameCmp!cs(cast(const(C)[])p[$-suffix.length .. $], suffix) == 0)
|
|
436 {
|
|
437 return p[0 .. $-suffix.length];
|
|
438 }
|
|
439 else return p;
|
|
440 }
|
|
441
|
|
442 @safe unittest
|
|
443 {
|
|
444 assert(baseName("").empty);
|
|
445 assert(baseName("file.ext"w) == "file.ext");
|
|
446 assert(baseName("file.ext"d, ".ext") == "file");
|
|
447 assert(baseName("file", "file"w.dup) == "file");
|
|
448 assert(baseName("dir/file.ext"d.dup) == "file.ext");
|
|
449 assert(baseName("dir/file.ext", ".ext"d) == "file");
|
|
450 assert(baseName("dir/file"w, "file"d) == "file");
|
|
451 assert(baseName("dir///subdir////") == "subdir");
|
|
452 assert(baseName("dir/subdir.ext/", ".ext") == "subdir");
|
|
453 assert(baseName("dir/subdir/".dup, "subdir") == "subdir");
|
|
454 assert(baseName("/"w.dup) == "/");
|
|
455 assert(baseName("//"d.dup) == "/");
|
|
456 assert(baseName("///") == "/");
|
|
457
|
|
458 assert(baseName!(CaseSensitive.yes)("file.ext", ".EXT") == "file.ext");
|
|
459 assert(baseName!(CaseSensitive.no)("file.ext", ".EXT") == "file");
|
|
460
|
|
461 {
|
|
462 auto r = MockRange!(immutable(char))(`dir/file.ext`);
|
|
463 auto s = r.baseName();
|
|
464 foreach (i, c; `file`)
|
|
465 assert(s[i] == c);
|
|
466 }
|
|
467
|
|
468 version (Windows)
|
|
469 {
|
|
470 assert(baseName(`dir\file.ext`) == `file.ext`);
|
|
471 assert(baseName(`dir\file.ext`, `.ext`) == `file`);
|
|
472 assert(baseName(`dir\file`, `file`) == `file`);
|
|
473 assert(baseName(`d:file.ext`) == `file.ext`);
|
|
474 assert(baseName(`d:file.ext`, `.ext`) == `file`);
|
|
475 assert(baseName(`d:file`, `file`) == `file`);
|
|
476 assert(baseName(`dir\\subdir\\\`) == `subdir`);
|
|
477 assert(baseName(`dir\subdir.ext\`, `.ext`) == `subdir`);
|
|
478 assert(baseName(`dir\subdir\`, `subdir`) == `subdir`);
|
|
479 assert(baseName(`\`) == `\`);
|
|
480 assert(baseName(`\\`) == `\`);
|
|
481 assert(baseName(`\\\`) == `\`);
|
|
482 assert(baseName(`d:\`) == `\`);
|
|
483 assert(baseName(`d:`).empty);
|
|
484 assert(baseName(`\\server\share\file`) == `file`);
|
|
485 assert(baseName(`\\server\share\`) == `\`);
|
|
486 assert(baseName(`\\server\share`) == `\`);
|
|
487
|
|
488 auto r = MockRange!(immutable(char))(`\\server\share`);
|
|
489 auto s = r.baseName();
|
|
490 foreach (i, c; `\`)
|
|
491 assert(s[i] == c);
|
|
492 }
|
|
493
|
|
494 assert(baseName(stripExtension("dir/file.ext")) == "file");
|
|
495
|
|
496 static assert(baseName("dir/file.ext") == "file.ext");
|
|
497 static assert(baseName("dir/file.ext", ".ext") == "file");
|
|
498
|
|
499 static struct DirEntry { string s; alias s this; }
|
|
500 assert(baseName(DirEntry("dir/file.ext")) == "file.ext");
|
|
501 }
|
|
502
|
|
503 @safe unittest
|
|
504 {
|
|
505 assert(testAliasedString!baseName("file"));
|
|
506
|
|
507 enum S : string { a = "file/path/to/test" }
|
|
508 assert(S.a.baseName == "test");
|
|
509
|
|
510 char[S.a.length] sa = S.a[];
|
|
511 assert(sa.baseName == "test");
|
|
512 }
|
|
513
|
|
514 /** Returns the directory part of a path. On Windows, this
|
|
515 includes the drive letter if present.
|
|
516
|
|
517 Params:
|
|
518 path = A path name.
|
|
519
|
|
520 Returns:
|
|
521 A slice of $(D path) or ".".
|
|
522
|
|
523 Standards:
|
|
524 This function complies with
|
|
525 $(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/utilities/dirname.html,
|
|
526 the POSIX requirements for the 'dirname' shell utility)
|
|
527 (with suitable adaptations for Windows paths).
|
|
528 */
|
|
529 auto dirName(R)(R path)
|
|
530 if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) && !isSomeString!R)
|
|
531 {
|
|
532 return _dirName(path);
|
|
533 }
|
|
534
|
|
535 /// ditto
|
|
536 auto dirName(C)(C[] path)
|
|
537 if (isSomeChar!C)
|
|
538 {
|
|
539 return _dirName(path);
|
|
540 }
|
|
541
|
|
542 private auto _dirName(R)(R path)
|
|
543 if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) ||
|
|
544 isNarrowString!R)
|
|
545 {
|
|
546 static auto result(bool dot, typeof(path[0 .. 1]) p)
|
|
547 {
|
|
548 static if (isSomeString!R)
|
|
549 return dot ? "." : p;
|
|
550 else
|
|
551 {
|
|
552 import std.range : choose, only;
|
|
553 return choose(dot, only(cast(ElementEncodingType!R)'.'), p);
|
|
554 }
|
|
555 }
|
|
556
|
|
557 if (path.empty)
|
|
558 return result(true, path[0 .. 0]);
|
|
559
|
|
560 auto p = rtrimDirSeparators(path);
|
|
561 if (p.empty)
|
|
562 return result(false, path[0 .. 1]);
|
|
563
|
|
564 version (Windows)
|
|
565 {
|
|
566 if (isUNC(p) && uncRootLength(p) == p.length)
|
|
567 return result(false, p);
|
|
568
|
|
569 if (p.length == 2 && isDriveSeparator(p[1]) && path.length > 2)
|
|
570 return result(false, path[0 .. 3]);
|
|
571 }
|
|
572
|
|
573 auto i = lastSeparator(p);
|
|
574 if (i == -1)
|
|
575 return result(true, p);
|
|
576 if (i == 0)
|
|
577 return result(false, p[0 .. 1]);
|
|
578
|
|
579 version (Windows)
|
|
580 {
|
|
581 // If the directory part is either d: or d:\
|
|
582 // do not chop off the last symbol.
|
|
583 if (isDriveSeparator(p[i]) || isDriveSeparator(p[i-1]))
|
|
584 return result(false, p[0 .. i+1]);
|
|
585 }
|
|
586 // Remove any remaining trailing (back)slashes.
|
|
587 return result(false, rtrimDirSeparators(p[0 .. i]));
|
|
588 }
|
|
589
|
|
590 ///
|
|
591 @safe unittest
|
|
592 {
|
|
593 assert(dirName("") == ".");
|
|
594 assert(dirName("file"w) == ".");
|
|
595 assert(dirName("dir/"d) == ".");
|
|
596 assert(dirName("dir///") == ".");
|
|
597 assert(dirName("dir/file"w.dup) == "dir");
|
|
598 assert(dirName("dir///file"d.dup) == "dir");
|
|
599 assert(dirName("dir/subdir/") == "dir");
|
|
600 assert(dirName("/dir/file"w) == "/dir");
|
|
601 assert(dirName("/file"d) == "/");
|
|
602 assert(dirName("/") == "/");
|
|
603 assert(dirName("///") == "/");
|
|
604
|
|
605 version (Windows)
|
|
606 {
|
|
607 assert(dirName(`dir\`) == `.`);
|
|
608 assert(dirName(`dir\\\`) == `.`);
|
|
609 assert(dirName(`dir\file`) == `dir`);
|
|
610 assert(dirName(`dir\\\file`) == `dir`);
|
|
611 assert(dirName(`dir\subdir\`) == `dir`);
|
|
612 assert(dirName(`\dir\file`) == `\dir`);
|
|
613 assert(dirName(`\file`) == `\`);
|
|
614 assert(dirName(`\`) == `\`);
|
|
615 assert(dirName(`\\\`) == `\`);
|
|
616 assert(dirName(`d:`) == `d:`);
|
|
617 assert(dirName(`d:file`) == `d:`);
|
|
618 assert(dirName(`d:\`) == `d:\`);
|
|
619 assert(dirName(`d:\file`) == `d:\`);
|
|
620 assert(dirName(`d:\dir\file`) == `d:\dir`);
|
|
621 assert(dirName(`\\server\share\dir\file`) == `\\server\share\dir`);
|
|
622 assert(dirName(`\\server\share\file`) == `\\server\share`);
|
|
623 assert(dirName(`\\server\share\`) == `\\server\share`);
|
|
624 assert(dirName(`\\server\share`) == `\\server\share`);
|
|
625 }
|
|
626 }
|
|
627
|
|
628 @safe unittest
|
|
629 {
|
|
630 assert(testAliasedString!dirName("file"));
|
|
631
|
|
632 enum S : string { a = "file/path/to/test" }
|
|
633 assert(S.a.dirName == "file/path/to");
|
|
634
|
|
635 char[S.a.length] sa = S.a[];
|
|
636 assert(sa.dirName == "file/path/to");
|
|
637 }
|
|
638
|
|
639 @system unittest
|
|
640 {
|
|
641 static assert(dirName("dir/file") == "dir");
|
|
642
|
|
643 import std.array;
|
|
644 import std.utf : byChar, byWchar, byDchar;
|
|
645
|
|
646 assert(dirName("".byChar).array == ".");
|
|
647 assert(dirName("file"w.byWchar).array == "."w);
|
|
648 assert(dirName("dir/"d.byDchar).array == "."d);
|
|
649 assert(dirName("dir///".byChar).array == ".");
|
|
650 assert(dirName("dir/subdir/".byChar).array == "dir");
|
|
651 assert(dirName("/dir/file"w.byWchar).array == "/dir"w);
|
|
652 assert(dirName("/file"d.byDchar).array == "/"d);
|
|
653 assert(dirName("/".byChar).array == "/");
|
|
654 assert(dirName("///".byChar).array == "/");
|
|
655
|
|
656 version (Windows)
|
|
657 {
|
|
658 assert(dirName(`dir\`.byChar).array == `.`);
|
|
659 assert(dirName(`dir\\\`.byChar).array == `.`);
|
|
660 assert(dirName(`dir\file`.byChar).array == `dir`);
|
|
661 assert(dirName(`dir\\\file`.byChar).array == `dir`);
|
|
662 assert(dirName(`dir\subdir\`.byChar).array == `dir`);
|
|
663 assert(dirName(`\dir\file`.byChar).array == `\dir`);
|
|
664 assert(dirName(`\file`.byChar).array == `\`);
|
|
665 assert(dirName(`\`.byChar).array == `\`);
|
|
666 assert(dirName(`\\\`.byChar).array == `\`);
|
|
667 assert(dirName(`d:`.byChar).array == `d:`);
|
|
668 assert(dirName(`d:file`.byChar).array == `d:`);
|
|
669 assert(dirName(`d:\`.byChar).array == `d:\`);
|
|
670 assert(dirName(`d:\file`.byChar).array == `d:\`);
|
|
671 assert(dirName(`d:\dir\file`.byChar).array == `d:\dir`);
|
|
672 assert(dirName(`\\server\share\dir\file`.byChar).array == `\\server\share\dir`);
|
|
673 assert(dirName(`\\server\share\file`) == `\\server\share`);
|
|
674 assert(dirName(`\\server\share\`.byChar).array == `\\server\share`);
|
|
675 assert(dirName(`\\server\share`.byChar).array == `\\server\share`);
|
|
676 }
|
|
677
|
|
678 //static assert(dirName("dir/file".byChar).array == "dir");
|
|
679 }
|
|
680
|
|
681
|
|
682
|
|
683
|
|
684 /** Returns the root directory of the specified path, or $(D null) if the
|
|
685 path is not rooted.
|
|
686
|
|
687 Params:
|
|
688 path = A path name.
|
|
689
|
|
690 Returns:
|
|
691 A slice of $(D path).
|
|
692 */
|
|
693 auto rootName(R)(R path)
|
|
694 if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) ||
|
|
695 isNarrowString!R) &&
|
|
696 !isConvertibleToString!R)
|
|
697 {
|
|
698 if (path.empty)
|
|
699 goto Lnull;
|
|
700
|
|
701 version (Posix)
|
|
702 {
|
|
703 if (isDirSeparator(path[0])) return path[0 .. 1];
|
|
704 }
|
|
705 else version (Windows)
|
|
706 {
|
|
707 if (isDirSeparator(path[0]))
|
|
708 {
|
|
709 if (isUNC(path)) return path[0 .. uncRootLength(path)];
|
|
710 else return path[0 .. 1];
|
|
711 }
|
|
712 else if (path.length >= 3 && isDriveSeparator(path[1]) &&
|
|
713 isDirSeparator(path[2]))
|
|
714 {
|
|
715 return path[0 .. 3];
|
|
716 }
|
|
717 }
|
|
718 else static assert(0, "unsupported platform");
|
|
719
|
|
720 assert(!isRooted(path));
|
|
721 Lnull:
|
|
722 static if (is(StringTypeOf!R))
|
|
723 return null; // legacy code may rely on null return rather than slice
|
|
724 else
|
|
725 return path[0 .. 0];
|
|
726 }
|
|
727
|
|
728 ///
|
|
729 @safe unittest
|
|
730 {
|
|
731 assert(rootName("") is null);
|
|
732 assert(rootName("foo") is null);
|
|
733 assert(rootName("/") == "/");
|
|
734 assert(rootName("/foo/bar") == "/");
|
|
735
|
|
736 version (Windows)
|
|
737 {
|
|
738 assert(rootName("d:foo") is null);
|
|
739 assert(rootName(`d:\foo`) == `d:\`);
|
|
740 assert(rootName(`\\server\share\foo`) == `\\server\share`);
|
|
741 assert(rootName(`\\server\share`) == `\\server\share`);
|
|
742 }
|
|
743 }
|
|
744
|
|
745 @safe unittest
|
|
746 {
|
|
747 assert(testAliasedString!rootName("/foo/bar"));
|
|
748 }
|
|
749
|
|
750 @safe unittest
|
|
751 {
|
|
752 import std.array;
|
|
753 import std.utf : byChar;
|
|
754
|
|
755 assert(rootName("".byChar).array == "");
|
|
756 assert(rootName("foo".byChar).array == "");
|
|
757 assert(rootName("/".byChar).array == "/");
|
|
758 assert(rootName("/foo/bar".byChar).array == "/");
|
|
759
|
|
760 version (Windows)
|
|
761 {
|
|
762 assert(rootName("d:foo".byChar).array == "");
|
|
763 assert(rootName(`d:\foo`.byChar).array == `d:\`);
|
|
764 assert(rootName(`\\server\share\foo`.byChar).array == `\\server\share`);
|
|
765 assert(rootName(`\\server\share`.byChar).array == `\\server\share`);
|
|
766 }
|
|
767 }
|
|
768
|
|
769 auto rootName(R)(R path)
|
|
770 if (isConvertibleToString!R)
|
|
771 {
|
|
772 return rootName!(StringTypeOf!R)(path);
|
|
773 }
|
|
774
|
|
775
|
|
776 /**
|
|
777 Get the drive portion of a path.
|
|
778
|
|
779 Params:
|
|
780 path = string or range of characters
|
|
781
|
|
782 Returns:
|
|
783 A slice of $(D _path) that is the drive, or an empty range if the drive
|
|
784 is not specified. In the case of UNC paths, the network share
|
|
785 is returned.
|
|
786
|
|
787 Always returns an empty range on POSIX.
|
|
788 */
|
|
789 auto driveName(R)(R path)
|
|
790 if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) ||
|
|
791 isNarrowString!R) &&
|
|
792 !isConvertibleToString!R)
|
|
793 {
|
|
794 version (Windows)
|
|
795 {
|
|
796 if (hasDrive(path))
|
|
797 return path[0 .. 2];
|
|
798 else if (isUNC(path))
|
|
799 return path[0 .. uncRootLength(path)];
|
|
800 }
|
|
801 static if (isSomeString!R)
|
|
802 return cast(ElementEncodingType!R[]) null; // legacy code may rely on null return rather than slice
|
|
803 else
|
|
804 return path[0 .. 0];
|
|
805 }
|
|
806
|
|
807 ///
|
|
808 @safe unittest
|
|
809 {
|
|
810 import std.range : empty;
|
|
811 version (Posix) assert(driveName("c:/foo").empty);
|
|
812 version (Windows)
|
|
813 {
|
|
814 assert(driveName(`dir\file`).empty);
|
|
815 assert(driveName(`d:file`) == "d:");
|
|
816 assert(driveName(`d:\file`) == "d:");
|
|
817 assert(driveName("d:") == "d:");
|
|
818 assert(driveName(`\\server\share\file`) == `\\server\share`);
|
|
819 assert(driveName(`\\server\share\`) == `\\server\share`);
|
|
820 assert(driveName(`\\server\share`) == `\\server\share`);
|
|
821
|
|
822 static assert(driveName(`d:\file`) == "d:");
|
|
823 }
|
|
824 }
|
|
825
|
|
826 auto driveName(R)(auto ref R path)
|
|
827 if (isConvertibleToString!R)
|
|
828 {
|
|
829 return driveName!(StringTypeOf!R)(path);
|
|
830 }
|
|
831
|
|
832 @safe unittest
|
|
833 {
|
|
834 assert(testAliasedString!driveName(`d:\file`));
|
|
835 }
|
|
836
|
|
837 @safe unittest
|
|
838 {
|
|
839 import std.array;
|
|
840 import std.utf : byChar;
|
|
841
|
|
842 version (Posix) assert(driveName("c:/foo".byChar).empty);
|
|
843 version (Windows)
|
|
844 {
|
|
845 assert(driveName(`dir\file`.byChar).empty);
|
|
846 assert(driveName(`d:file`.byChar).array == "d:");
|
|
847 assert(driveName(`d:\file`.byChar).array == "d:");
|
|
848 assert(driveName("d:".byChar).array == "d:");
|
|
849 assert(driveName(`\\server\share\file`.byChar).array == `\\server\share`);
|
|
850 assert(driveName(`\\server\share\`.byChar).array == `\\server\share`);
|
|
851 assert(driveName(`\\server\share`.byChar).array == `\\server\share`);
|
|
852
|
|
853 static assert(driveName(`d:\file`).array == "d:");
|
|
854 }
|
|
855 }
|
|
856
|
|
857
|
|
858 /** Strips the drive from a Windows path. On POSIX, the path is returned
|
|
859 unaltered.
|
|
860
|
|
861 Params:
|
|
862 path = A pathname
|
|
863
|
|
864 Returns: A slice of path without the drive component.
|
|
865 */
|
|
866 auto stripDrive(R)(R path)
|
|
867 if ((isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) ||
|
|
868 isNarrowString!R) &&
|
|
869 !isConvertibleToString!R)
|
|
870 {
|
|
871 version (Windows)
|
|
872 {
|
|
873 if (hasDrive!(BaseOf!R)(path)) return path[2 .. path.length];
|
|
874 else if (isUNC!(BaseOf!R)(path)) return path[uncRootLength!(BaseOf!R)(path) .. path.length];
|
|
875 }
|
|
876 return path;
|
|
877 }
|
|
878
|
|
879 ///
|
|
880 @safe unittest
|
|
881 {
|
|
882 version (Windows)
|
|
883 {
|
|
884 assert(stripDrive(`d:\dir\file`) == `\dir\file`);
|
|
885 assert(stripDrive(`\\server\share\dir\file`) == `\dir\file`);
|
|
886 }
|
|
887 }
|
|
888
|
|
889 auto stripDrive(R)(auto ref R path)
|
|
890 if (isConvertibleToString!R)
|
|
891 {
|
|
892 return stripDrive!(StringTypeOf!R)(path);
|
|
893 }
|
|
894
|
|
895 @safe unittest
|
|
896 {
|
|
897 assert(testAliasedString!stripDrive(`d:\dir\file`));
|
|
898 }
|
|
899
|
|
900 @safe unittest
|
|
901 {
|
|
902 version (Windows)
|
|
903 {
|
|
904 assert(stripDrive(`d:\dir\file`) == `\dir\file`);
|
|
905 assert(stripDrive(`\\server\share\dir\file`) == `\dir\file`);
|
|
906 static assert(stripDrive(`d:\dir\file`) == `\dir\file`);
|
|
907
|
|
908 auto r = MockRange!(immutable(char))(`d:\dir\file`);
|
|
909 auto s = r.stripDrive();
|
|
910 foreach (i, c; `\dir\file`)
|
|
911 assert(s[i] == c);
|
|
912 }
|
|
913 version (Posix)
|
|
914 {
|
|
915 assert(stripDrive(`d:\dir\file`) == `d:\dir\file`);
|
|
916
|
|
917 auto r = MockRange!(immutable(char))(`d:\dir\file`);
|
|
918 auto s = r.stripDrive();
|
|
919 foreach (i, c; `d:\dir\file`)
|
|
920 assert(s[i] == c);
|
|
921 }
|
|
922 }
|
|
923
|
|
924
|
|
925 /* Helper function that returns the position of the filename/extension
|
|
926 separator dot in path.
|
|
927
|
|
928 Params:
|
|
929 path = file spec as string or indexable range
|
|
930 Returns:
|
|
931 index of extension separator (the dot), or -1 if not found
|
|
932 */
|
|
933 private ptrdiff_t extSeparatorPos(R)(const R path)
|
|
934 if (isRandomAccessRange!R && hasLength!R && isSomeChar!(ElementType!R) ||
|
|
935 isNarrowString!R)
|
|
936 {
|
|
937 for (auto i = path.length; i-- > 0 && !isSeparator(path[i]); )
|
|
938 {
|
|
939 if (path[i] == '.' && i > 0 && !isSeparator(path[i-1]))
|
|
940 return i;
|
|
941 }
|
|
942 return -1;
|
|
943 }
|
|
944
|
|
945 @safe unittest
|
|
946 {
|
|
947 assert(extSeparatorPos("file") == -1);
|
|
948 assert(extSeparatorPos("file.ext"w) == 4);
|
|
949 assert(extSeparatorPos("file.ext1.ext2"d) == 9);
|
|
950 assert(extSeparatorPos(".foo".dup) == -1);
|
|
951 assert(extSeparatorPos(".foo.ext"w.dup) == 4);
|
|
952 }
|
|
953
|
|
954 @safe unittest
|
|
955 {
|
|
956 assert(extSeparatorPos("dir/file"d.dup) == -1);
|
|
957 assert(extSeparatorPos("dir/file.ext") == 8);
|
|
958 assert(extSeparatorPos("dir/file.ext1.ext2"w) == 13);
|
|
959 assert(extSeparatorPos("dir/.foo"d) == -1);
|
|
960 assert(extSeparatorPos("dir/.foo.ext".dup) == 8);
|
|
961
|
|
962 version (Windows)
|
|
963 {
|
|
964 assert(extSeparatorPos("dir\\file") == -1);
|
|
965 assert(extSeparatorPos("dir\\file.ext") == 8);
|
|
966 assert(extSeparatorPos("dir\\file.ext1.ext2") == 13);
|
|
967 assert(extSeparatorPos("dir\\.foo") == -1);
|
|
968 assert(extSeparatorPos("dir\\.foo.ext") == 8);
|
|
969
|
|
970 assert(extSeparatorPos("d:file") == -1);
|
|
971 assert(extSeparatorPos("d:file.ext") == 6);
|
|
972 assert(extSeparatorPos("d:file.ext1.ext2") == 11);
|
|
973 assert(extSeparatorPos("d:.foo") == -1);
|
|
974 assert(extSeparatorPos("d:.foo.ext") == 6);
|
|
975 }
|
|
976
|
|
977 static assert(extSeparatorPos("file") == -1);
|
|
978 static assert(extSeparatorPos("file.ext"w) == 4);
|
|
979 }
|
|
980
|
|
981
|
|
982 /**
|
|
983 Params: path = A path name.
|
|
984 Returns: The _extension part of a file name, including the dot.
|
|
985
|
|
986 If there is no _extension, $(D null) is returned.
|
|
987 */
|
|
988 auto extension(R)(R path)
|
|
989 if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) ||
|
|
990 is(StringTypeOf!R))
|
|
991 {
|
|
992 auto i = extSeparatorPos!(BaseOf!R)(path);
|
|
993 if (i == -1)
|
|
994 {
|
|
995 static if (is(StringTypeOf!R))
|
|
996 return StringTypeOf!R.init[]; // which is null
|
|
997 else
|
|
998 return path[0 .. 0];
|
|
999 }
|
|
1000 else return path[i .. path.length];
|
|
1001 }
|
|
1002
|
|
1003 ///
|
|
1004 @safe unittest
|
|
1005 {
|
|
1006 import std.range : empty;
|
|
1007 assert(extension("file").empty);
|
|
1008 assert(extension("file.") == ".");
|
|
1009 assert(extension("file.ext"w) == ".ext");
|
|
1010 assert(extension("file.ext1.ext2"d) == ".ext2");
|
|
1011 assert(extension(".foo".dup).empty);
|
|
1012 assert(extension(".foo.ext"w.dup) == ".ext");
|
|
1013
|
|
1014 static assert(extension("file").empty);
|
|
1015 static assert(extension("file.ext") == ".ext");
|
|
1016 }
|
|
1017
|
|
1018 @safe unittest
|
|
1019 {
|
|
1020 {
|
|
1021 auto r = MockRange!(immutable(char))(`file.ext1.ext2`);
|
|
1022 auto s = r.extension();
|
|
1023 foreach (i, c; `.ext2`)
|
|
1024 assert(s[i] == c);
|
|
1025 }
|
|
1026
|
|
1027 static struct DirEntry { string s; alias s this; }
|
|
1028 assert(extension(DirEntry("file")).empty);
|
|
1029 }
|
|
1030
|
|
1031
|
|
1032 /** Remove extension from path.
|
|
1033
|
|
1034 Params:
|
|
1035 path = string or range to be sliced
|
|
1036
|
|
1037 Returns:
|
|
1038 slice of path with the extension (if any) stripped off
|
|
1039 */
|
|
1040 auto stripExtension(R)(R path)
|
|
1041 if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) ||
|
|
1042 isNarrowString!R) &&
|
|
1043 !isConvertibleToString!R)
|
|
1044 {
|
|
1045 auto i = extSeparatorPos(path);
|
|
1046 return (i == -1) ? path : path[0 .. i];
|
|
1047 }
|
|
1048
|
|
1049 ///
|
|
1050 @safe unittest
|
|
1051 {
|
|
1052 assert(stripExtension("file") == "file");
|
|
1053 assert(stripExtension("file.ext") == "file");
|
|
1054 assert(stripExtension("file.ext1.ext2") == "file.ext1");
|
|
1055 assert(stripExtension("file.") == "file");
|
|
1056 assert(stripExtension(".file") == ".file");
|
|
1057 assert(stripExtension(".file.ext") == ".file");
|
|
1058 assert(stripExtension("dir/file.ext") == "dir/file");
|
|
1059 }
|
|
1060
|
|
1061 auto stripExtension(R)(auto ref R path)
|
|
1062 if (isConvertibleToString!R)
|
|
1063 {
|
|
1064 return stripExtension!(StringTypeOf!R)(path);
|
|
1065 }
|
|
1066
|
|
1067 @safe unittest
|
|
1068 {
|
|
1069 assert(testAliasedString!stripExtension("file"));
|
|
1070 }
|
|
1071
|
|
1072 @safe unittest
|
|
1073 {
|
|
1074 assert(stripExtension("file.ext"w) == "file");
|
|
1075 assert(stripExtension("file.ext1.ext2"d) == "file.ext1");
|
|
1076
|
|
1077 import std.array;
|
|
1078 import std.utf : byChar, byWchar, byDchar;
|
|
1079
|
|
1080 assert(stripExtension("file".byChar).array == "file");
|
|
1081 assert(stripExtension("file.ext"w.byWchar).array == "file");
|
|
1082 assert(stripExtension("file.ext1.ext2"d.byDchar).array == "file.ext1");
|
|
1083 }
|
|
1084
|
|
1085
|
|
1086 /** Sets or replaces an extension.
|
|
1087
|
|
1088 If the filename already has an extension, it is replaced. If not, the
|
|
1089 extension is simply appended to the filename. Including a leading dot
|
|
1090 in $(D ext) is optional.
|
|
1091
|
|
1092 If the extension is empty, this function is equivalent to
|
|
1093 $(LREF stripExtension).
|
|
1094
|
|
1095 This function normally allocates a new string (the possible exception
|
|
1096 being the case when path is immutable and doesn't already have an
|
|
1097 extension).
|
|
1098
|
|
1099 Params:
|
|
1100 path = A path name
|
|
1101 ext = The new extension
|
|
1102
|
|
1103 Returns: A string containing the _path given by $(D path), but where
|
|
1104 the extension has been set to $(D ext).
|
|
1105
|
|
1106 See_Also:
|
|
1107 $(LREF withExtension) which does not allocate and returns a lazy range.
|
|
1108 */
|
|
1109 immutable(Unqual!C1)[] setExtension(C1, C2)(in C1[] path, in C2[] ext)
|
|
1110 if (isSomeChar!C1 && !is(C1 == immutable) && is(Unqual!C1 == Unqual!C2))
|
|
1111 {
|
|
1112 try
|
|
1113 {
|
|
1114 import std.conv : to;
|
|
1115 return withExtension(path, ext).to!(typeof(return));
|
|
1116 }
|
|
1117 catch (Exception e)
|
|
1118 {
|
|
1119 assert(0);
|
|
1120 }
|
|
1121 }
|
|
1122
|
|
1123 ///ditto
|
|
1124 immutable(C1)[] setExtension(C1, C2)(immutable(C1)[] path, const(C2)[] ext)
|
|
1125 if (isSomeChar!C1 && is(Unqual!C1 == Unqual!C2))
|
|
1126 {
|
|
1127 if (ext.length == 0)
|
|
1128 return stripExtension(path);
|
|
1129
|
|
1130 try
|
|
1131 {
|
|
1132 import std.conv : to;
|
|
1133 return withExtension(path, ext).to!(typeof(return));
|
|
1134 }
|
|
1135 catch (Exception e)
|
|
1136 {
|
|
1137 assert(0);
|
|
1138 }
|
|
1139 }
|
|
1140
|
|
1141 ///
|
|
1142 @safe unittest
|
|
1143 {
|
|
1144 assert(setExtension("file", "ext") == "file.ext");
|
|
1145 assert(setExtension("file"w, ".ext"w) == "file.ext");
|
|
1146 assert(setExtension("file."d, "ext"d) == "file.ext");
|
|
1147 assert(setExtension("file.", ".ext") == "file.ext");
|
|
1148 assert(setExtension("file.old"w, "new"w) == "file.new");
|
|
1149 assert(setExtension("file.old"d, ".new"d) == "file.new");
|
|
1150 }
|
|
1151
|
|
1152 @safe unittest
|
|
1153 {
|
|
1154 assert(setExtension("file"w.dup, "ext"w) == "file.ext");
|
|
1155 assert(setExtension("file"w.dup, ".ext"w) == "file.ext");
|
|
1156 assert(setExtension("file."w, "ext"w.dup) == "file.ext");
|
|
1157 assert(setExtension("file."w, ".ext"w.dup) == "file.ext");
|
|
1158 assert(setExtension("file.old"d.dup, "new"d) == "file.new");
|
|
1159 assert(setExtension("file.old"d.dup, ".new"d) == "file.new");
|
|
1160
|
|
1161 static assert(setExtension("file", "ext") == "file.ext");
|
|
1162 static assert(setExtension("file.old", "new") == "file.new");
|
|
1163
|
|
1164 static assert(setExtension("file"w.dup, "ext"w) == "file.ext");
|
|
1165 static assert(setExtension("file.old"d.dup, "new"d) == "file.new");
|
|
1166
|
|
1167 // Issue 10601
|
|
1168 assert(setExtension("file", "") == "file");
|
|
1169 assert(setExtension("file.ext", "") == "file");
|
|
1170 }
|
|
1171
|
|
1172 /************
|
|
1173 * Replace existing extension on filespec with new one.
|
|
1174 *
|
|
1175 * Params:
|
|
1176 * path = string or random access range representing a filespec
|
|
1177 * ext = the new extension
|
|
1178 * Returns:
|
|
1179 * Range with $(D path)'s extension (if any) replaced with $(D ext).
|
|
1180 * The element encoding type of the returned range will be the same as $(D path)'s.
|
|
1181 * See_Also:
|
|
1182 * $(LREF setExtension)
|
|
1183 */
|
|
1184 auto withExtension(R, C)(R path, C[] ext)
|
|
1185 if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) ||
|
|
1186 isNarrowString!R) &&
|
|
1187 !isConvertibleToString!R &&
|
|
1188 isSomeChar!C)
|
|
1189 {
|
|
1190 import std.range : only, chain;
|
|
1191 import std.utf : byUTF;
|
|
1192
|
|
1193 alias CR = Unqual!(ElementEncodingType!R);
|
|
1194 auto dot = only(CR('.'));
|
|
1195 if (ext.length == 0 || ext[0] == '.')
|
|
1196 dot.popFront(); // so dot is an empty range, too
|
|
1197 return chain(stripExtension(path).byUTF!CR, dot, ext.byUTF!CR);
|
|
1198 }
|
|
1199
|
|
1200 ///
|
|
1201 @safe unittest
|
|
1202 {
|
|
1203 import std.array;
|
|
1204 assert(withExtension("file", "ext").array == "file.ext");
|
|
1205 assert(withExtension("file"w, ".ext"w).array == "file.ext");
|
|
1206 assert(withExtension("file.ext"w, ".").array == "file.");
|
|
1207
|
|
1208 import std.utf : byChar, byWchar;
|
|
1209 assert(withExtension("file".byChar, "ext").array == "file.ext");
|
|
1210 assert(withExtension("file"w.byWchar, ".ext"w).array == "file.ext"w);
|
|
1211 assert(withExtension("file.ext"w.byWchar, ".").array == "file."w);
|
|
1212 }
|
|
1213
|
|
1214 auto withExtension(R, C)(auto ref R path, C[] ext)
|
|
1215 if (isConvertibleToString!R)
|
|
1216 {
|
|
1217 return withExtension!(StringTypeOf!R)(path, ext);
|
|
1218 }
|
|
1219
|
|
1220 @safe unittest
|
|
1221 {
|
|
1222 assert(testAliasedString!withExtension("file", "ext"));
|
|
1223 }
|
|
1224
|
|
1225 /** Params:
|
|
1226 path = A path name.
|
|
1227 ext = The default extension to use.
|
|
1228
|
|
1229 Returns: The _path given by $(D path), with the extension given by $(D ext)
|
|
1230 appended if the path doesn't already have one.
|
|
1231
|
|
1232 Including the dot in the extension is optional.
|
|
1233
|
|
1234 This function always allocates a new string, except in the case when
|
|
1235 path is immutable and already has an extension.
|
|
1236 */
|
|
1237 immutable(Unqual!C1)[] defaultExtension(C1, C2)(in C1[] path, in C2[] ext)
|
|
1238 if (isSomeChar!C1 && is(Unqual!C1 == Unqual!C2))
|
|
1239 {
|
|
1240 import std.conv : to;
|
|
1241 return withDefaultExtension(path, ext).to!(typeof(return));
|
|
1242 }
|
|
1243
|
|
1244 ///
|
|
1245 @safe unittest
|
|
1246 {
|
|
1247 assert(defaultExtension("file", "ext") == "file.ext");
|
|
1248 assert(defaultExtension("file", ".ext") == "file.ext");
|
|
1249 assert(defaultExtension("file.", "ext") == "file.");
|
|
1250 assert(defaultExtension("file.old", "new") == "file.old");
|
|
1251 assert(defaultExtension("file.old", ".new") == "file.old");
|
|
1252 }
|
|
1253
|
|
1254 @safe unittest
|
|
1255 {
|
|
1256 assert(defaultExtension("file"w.dup, "ext"w) == "file.ext");
|
|
1257 assert(defaultExtension("file.old"d.dup, "new"d) == "file.old");
|
|
1258
|
|
1259 static assert(defaultExtension("file", "ext") == "file.ext");
|
|
1260 static assert(defaultExtension("file.old", "new") == "file.old");
|
|
1261
|
|
1262 static assert(defaultExtension("file"w.dup, "ext"w) == "file.ext");
|
|
1263 static assert(defaultExtension("file.old"d.dup, "new"d) == "file.old");
|
|
1264 }
|
|
1265
|
|
1266
|
|
1267 /********************************
|
|
1268 * Set the extension of $(D path) to $(D ext) if $(D path) doesn't have one.
|
|
1269 *
|
|
1270 * Params:
|
|
1271 * path = filespec as string or range
|
|
1272 * ext = extension, may have leading '.'
|
|
1273 * Returns:
|
|
1274 * range with the result
|
|
1275 */
|
|
1276 auto withDefaultExtension(R, C)(R path, C[] ext)
|
|
1277 if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) ||
|
|
1278 isNarrowString!R) &&
|
|
1279 !isConvertibleToString!R &&
|
|
1280 isSomeChar!C)
|
|
1281 {
|
|
1282 import std.range : only, chain;
|
|
1283 import std.utf : byUTF;
|
|
1284
|
|
1285 alias CR = Unqual!(ElementEncodingType!R);
|
|
1286 auto dot = only(CR('.'));
|
|
1287 auto i = extSeparatorPos(path);
|
|
1288 if (i == -1)
|
|
1289 {
|
|
1290 if (ext.length > 0 && ext[0] == '.')
|
|
1291 ext = ext[1 .. $]; // remove any leading . from ext[]
|
|
1292 }
|
|
1293 else
|
|
1294 {
|
|
1295 // path already has an extension, so make these empty
|
|
1296 ext = ext[0 .. 0];
|
|
1297 dot.popFront();
|
|
1298 }
|
|
1299 return chain(path.byUTF!CR, dot, ext.byUTF!CR);
|
|
1300 }
|
|
1301
|
|
1302 ///
|
|
1303 @safe unittest
|
|
1304 {
|
|
1305 import std.array;
|
|
1306 assert(withDefaultExtension("file", "ext").array == "file.ext");
|
|
1307 assert(withDefaultExtension("file"w, ".ext").array == "file.ext"w);
|
|
1308 assert(withDefaultExtension("file.", "ext").array == "file.");
|
|
1309 assert(withDefaultExtension("file", "").array == "file.");
|
|
1310
|
|
1311 import std.utf : byChar, byWchar;
|
|
1312 assert(withDefaultExtension("file".byChar, "ext").array == "file.ext");
|
|
1313 assert(withDefaultExtension("file"w.byWchar, ".ext").array == "file.ext"w);
|
|
1314 assert(withDefaultExtension("file.".byChar, "ext"d).array == "file.");
|
|
1315 assert(withDefaultExtension("file".byChar, "").array == "file.");
|
|
1316 }
|
|
1317
|
|
1318 auto withDefaultExtension(R, C)(auto ref R path, C[] ext)
|
|
1319 if (isConvertibleToString!R)
|
|
1320 {
|
|
1321 return withDefaultExtension!(StringTypeOf!R, C)(path, ext);
|
|
1322 }
|
|
1323
|
|
1324 @safe unittest
|
|
1325 {
|
|
1326 assert(testAliasedString!withDefaultExtension("file", "ext"));
|
|
1327 }
|
|
1328
|
|
1329 /** Combines one or more path segments.
|
|
1330
|
|
1331 This function takes a set of path segments, given as an input
|
|
1332 range of string elements or as a set of string arguments,
|
|
1333 and concatenates them with each other. Directory separators
|
|
1334 are inserted between segments if necessary. If any of the
|
|
1335 path segments are absolute (as defined by $(LREF isAbsolute)), the
|
|
1336 preceding segments will be dropped.
|
|
1337
|
|
1338 On Windows, if one of the path segments are rooted, but not absolute
|
|
1339 (e.g. $(D `\foo`)), all preceding path segments down to the previous
|
|
1340 root will be dropped. (See below for an example.)
|
|
1341
|
|
1342 This function always allocates memory to hold the resulting path.
|
|
1343 The variadic overload is guaranteed to only perform a single
|
|
1344 allocation, as is the range version if $(D paths) is a forward
|
|
1345 range.
|
|
1346
|
|
1347 Params:
|
|
1348 segments = An input range of segments to assemble the path from.
|
|
1349 Returns: The assembled path.
|
|
1350 */
|
|
1351 immutable(ElementEncodingType!(ElementType!Range))[]
|
|
1352 buildPath(Range)(Range segments)
|
|
1353 if (isInputRange!Range && !isInfinite!Range && isSomeString!(ElementType!Range))
|
|
1354 {
|
|
1355 if (segments.empty) return null;
|
|
1356
|
|
1357 // If this is a forward range, we can pre-calculate a maximum length.
|
|
1358 static if (isForwardRange!Range)
|
|
1359 {
|
|
1360 auto segments2 = segments.save;
|
|
1361 size_t precalc = 0;
|
|
1362 foreach (segment; segments2) precalc += segment.length + 1;
|
|
1363 }
|
|
1364 // Otherwise, just venture a guess and resize later if necessary.
|
|
1365 else size_t precalc = 255;
|
|
1366
|
|
1367 auto buf = new Unqual!(ElementEncodingType!(ElementType!Range))[](precalc);
|
|
1368 size_t pos = 0;
|
|
1369 foreach (segment; segments)
|
|
1370 {
|
|
1371 if (segment.empty) continue;
|
|
1372 static if (!isForwardRange!Range)
|
|
1373 {
|
|
1374 immutable neededLength = pos + segment.length + 1;
|
|
1375 if (buf.length < neededLength)
|
|
1376 buf.length = reserve(buf, neededLength + buf.length/2);
|
|
1377 }
|
|
1378 auto r = chainPath(buf[0 .. pos], segment);
|
|
1379 size_t i;
|
|
1380 foreach (c; r)
|
|
1381 {
|
|
1382 buf[i] = c;
|
|
1383 ++i;
|
|
1384 }
|
|
1385 pos = i;
|
|
1386 }
|
|
1387 static U trustedCast(U, V)(V v) @trusted pure nothrow { return cast(U) v; }
|
|
1388 return trustedCast!(typeof(return))(buf[0 .. pos]);
|
|
1389 }
|
|
1390
|
|
1391 /// ditto
|
|
1392 immutable(C)[] buildPath(C)(const(C)[][] paths...)
|
|
1393 @safe pure nothrow
|
|
1394 if (isSomeChar!C)
|
|
1395 {
|
|
1396 return buildPath!(typeof(paths))(paths);
|
|
1397 }
|
|
1398
|
|
1399 ///
|
|
1400 @safe unittest
|
|
1401 {
|
|
1402 version (Posix)
|
|
1403 {
|
|
1404 assert(buildPath("foo", "bar", "baz") == "foo/bar/baz");
|
|
1405 assert(buildPath("/foo/", "bar/baz") == "/foo/bar/baz");
|
|
1406 assert(buildPath("/foo", "/bar") == "/bar");
|
|
1407 }
|
|
1408
|
|
1409 version (Windows)
|
|
1410 {
|
|
1411 assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`);
|
|
1412 assert(buildPath(`c:\foo`, `bar\baz`) == `c:\foo\bar\baz`);
|
|
1413 assert(buildPath("foo", `d:\bar`) == `d:\bar`);
|
|
1414 assert(buildPath("foo", `\bar`) == `\bar`);
|
|
1415 assert(buildPath(`c:\foo`, `\bar`) == `c:\bar`);
|
|
1416 }
|
|
1417 }
|
|
1418
|
|
1419 @system unittest // non-documented
|
|
1420 {
|
|
1421 import std.range;
|
|
1422 // ir() wraps an array in a plain (i.e. non-forward) input range, so that
|
|
1423 // we can test both code paths
|
|
1424 InputRange!(C[]) ir(C)(C[][] p...) { return inputRangeObject(p); }
|
|
1425 version (Posix)
|
|
1426 {
|
|
1427 assert(buildPath("foo") == "foo");
|
|
1428 assert(buildPath("/foo/") == "/foo/");
|
|
1429 assert(buildPath("foo", "bar") == "foo/bar");
|
|
1430 assert(buildPath("foo", "bar", "baz") == "foo/bar/baz");
|
|
1431 assert(buildPath("foo/".dup, "bar") == "foo/bar");
|
|
1432 assert(buildPath("foo///", "bar".dup) == "foo///bar");
|
|
1433 assert(buildPath("/foo"w, "bar"w) == "/foo/bar");
|
|
1434 assert(buildPath("foo"w.dup, "/bar"w) == "/bar");
|
|
1435 assert(buildPath("foo"w, "bar/"w.dup) == "foo/bar/");
|
|
1436 assert(buildPath("/"d, "foo"d) == "/foo");
|
|
1437 assert(buildPath(""d.dup, "foo"d) == "foo");
|
|
1438 assert(buildPath("foo"d, ""d.dup) == "foo");
|
|
1439 assert(buildPath("foo", "bar".dup, "baz") == "foo/bar/baz");
|
|
1440 assert(buildPath("foo"w, "/bar"w, "baz"w.dup) == "/bar/baz");
|
|
1441
|
|
1442 static assert(buildPath("foo", "bar", "baz") == "foo/bar/baz");
|
|
1443 static assert(buildPath("foo", "/bar", "baz") == "/bar/baz");
|
|
1444
|
|
1445 // The following are mostly duplicates of the above, except that the
|
|
1446 // range version does not accept mixed constness.
|
|
1447 assert(buildPath(ir("foo")) == "foo");
|
|
1448 assert(buildPath(ir("/foo/")) == "/foo/");
|
|
1449 assert(buildPath(ir("foo", "bar")) == "foo/bar");
|
|
1450 assert(buildPath(ir("foo", "bar", "baz")) == "foo/bar/baz");
|
|
1451 assert(buildPath(ir("foo/".dup, "bar".dup)) == "foo/bar");
|
|
1452 assert(buildPath(ir("foo///".dup, "bar".dup)) == "foo///bar");
|
|
1453 assert(buildPath(ir("/foo"w, "bar"w)) == "/foo/bar");
|
|
1454 assert(buildPath(ir("foo"w.dup, "/bar"w.dup)) == "/bar");
|
|
1455 assert(buildPath(ir("foo"w.dup, "bar/"w.dup)) == "foo/bar/");
|
|
1456 assert(buildPath(ir("/"d, "foo"d)) == "/foo");
|
|
1457 assert(buildPath(ir(""d.dup, "foo"d.dup)) == "foo");
|
|
1458 assert(buildPath(ir("foo"d, ""d)) == "foo");
|
|
1459 assert(buildPath(ir("foo", "bar", "baz")) == "foo/bar/baz");
|
|
1460 assert(buildPath(ir("foo"w.dup, "/bar"w.dup, "baz"w.dup)) == "/bar/baz");
|
|
1461 }
|
|
1462 version (Windows)
|
|
1463 {
|
|
1464 assert(buildPath("foo") == "foo");
|
|
1465 assert(buildPath(`\foo/`) == `\foo/`);
|
|
1466 assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`);
|
|
1467 assert(buildPath("foo", `\bar`) == `\bar`);
|
|
1468 assert(buildPath(`c:\foo`, "bar") == `c:\foo\bar`);
|
|
1469 assert(buildPath("foo"w, `d:\bar`w.dup) == `d:\bar`);
|
|
1470 assert(buildPath(`c:\foo\bar`, `\baz`) == `c:\baz`);
|
|
1471 assert(buildPath(`\\foo\bar\baz`d, `foo`d, `\bar`d) == `\\foo\bar\bar`d);
|
|
1472
|
|
1473 static assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`);
|
|
1474 static assert(buildPath("foo", `c:\bar`, "baz") == `c:\bar\baz`);
|
|
1475
|
|
1476 assert(buildPath(ir("foo")) == "foo");
|
|
1477 assert(buildPath(ir(`\foo/`)) == `\foo/`);
|
|
1478 assert(buildPath(ir("foo", "bar", "baz")) == `foo\bar\baz`);
|
|
1479 assert(buildPath(ir("foo", `\bar`)) == `\bar`);
|
|
1480 assert(buildPath(ir(`c:\foo`, "bar")) == `c:\foo\bar`);
|
|
1481 assert(buildPath(ir("foo"w.dup, `d:\bar`w.dup)) == `d:\bar`);
|
|
1482 assert(buildPath(ir(`c:\foo\bar`, `\baz`)) == `c:\baz`);
|
|
1483 assert(buildPath(ir(`\\foo\bar\baz`d, `foo`d, `\bar`d)) == `\\foo\bar\bar`d);
|
|
1484 }
|
|
1485
|
|
1486 // Test that allocation works as it should.
|
|
1487 auto manyShort = "aaa".repeat(1000).array();
|
|
1488 auto manyShortCombined = join(manyShort, dirSeparator);
|
|
1489 assert(buildPath(manyShort) == manyShortCombined);
|
|
1490 assert(buildPath(ir(manyShort)) == manyShortCombined);
|
|
1491
|
|
1492 auto fewLong = 'b'.repeat(500).array().repeat(10).array();
|
|
1493 auto fewLongCombined = join(fewLong, dirSeparator);
|
|
1494 assert(buildPath(fewLong) == fewLongCombined);
|
|
1495 assert(buildPath(ir(fewLong)) == fewLongCombined);
|
|
1496 }
|
|
1497
|
|
1498 @safe unittest
|
|
1499 {
|
|
1500 // Test for issue 7397
|
|
1501 string[] ary = ["a", "b"];
|
|
1502 version (Posix)
|
|
1503 {
|
|
1504 assert(buildPath(ary) == "a/b");
|
|
1505 }
|
|
1506 else version (Windows)
|
|
1507 {
|
|
1508 assert(buildPath(ary) == `a\b`);
|
|
1509 }
|
|
1510 }
|
|
1511
|
|
1512
|
|
1513 /**
|
|
1514 * Concatenate path segments together to form one path.
|
|
1515 *
|
|
1516 * Params:
|
|
1517 * r1 = first segment
|
|
1518 * r2 = second segment
|
|
1519 * ranges = 0 or more segments
|
|
1520 * Returns:
|
|
1521 * Lazy range which is the concatenation of r1, r2 and ranges with path separators.
|
|
1522 * The resulting element type is that of r1.
|
|
1523 * See_Also:
|
|
1524 * $(LREF buildPath)
|
|
1525 */
|
|
1526 auto chainPath(R1, R2, Ranges...)(R1 r1, R2 r2, Ranges ranges)
|
|
1527 if ((isRandomAccessRange!R1 && hasSlicing!R1 && hasLength!R1 && isSomeChar!(ElementType!R1) ||
|
|
1528 isNarrowString!R1 &&
|
|
1529 !isConvertibleToString!R1) &&
|
|
1530 (isRandomAccessRange!R2 && hasSlicing!R2 && hasLength!R2 && isSomeChar!(ElementType!R2) ||
|
|
1531 isNarrowString!R2 &&
|
|
1532 !isConvertibleToString!R2) &&
|
|
1533 (Ranges.length == 0 || is(typeof(chainPath(r2, ranges))))
|
|
1534 )
|
|
1535 {
|
|
1536 static if (Ranges.length)
|
|
1537 {
|
|
1538 return chainPath(chainPath(r1, r2), ranges);
|
|
1539 }
|
|
1540 else
|
|
1541 {
|
|
1542 import std.range : only, chain;
|
|
1543 import std.utf : byUTF;
|
|
1544
|
|
1545 alias CR = Unqual!(ElementEncodingType!R1);
|
|
1546 auto sep = only(CR(dirSeparator[0]));
|
|
1547 bool usesep = false;
|
|
1548
|
|
1549 auto pos = r1.length;
|
|
1550
|
|
1551 if (pos)
|
|
1552 {
|
|
1553 if (isRooted(r2))
|
|
1554 {
|
|
1555 version (Posix)
|
|
1556 {
|
|
1557 pos = 0;
|
|
1558 }
|
|
1559 else version (Windows)
|
|
1560 {
|
|
1561 if (isAbsolute(r2))
|
|
1562 pos = 0;
|
|
1563 else
|
|
1564 {
|
|
1565 pos = rootName(r1).length;
|
|
1566 if (pos > 0 && isDirSeparator(r1[pos - 1]))
|
|
1567 --pos;
|
|
1568 }
|
|
1569 }
|
|
1570 else
|
|
1571 static assert(0);
|
|
1572 }
|
|
1573 else if (!isDirSeparator(r1[pos - 1]))
|
|
1574 usesep = true;
|
|
1575 }
|
|
1576 if (!usesep)
|
|
1577 sep.popFront();
|
|
1578 // Return r1 ~ '/' ~ r2
|
|
1579 return chain(r1[0 .. pos].byUTF!CR, sep, r2.byUTF!CR);
|
|
1580 }
|
|
1581 }
|
|
1582
|
|
1583 ///
|
|
1584 @safe unittest
|
|
1585 {
|
|
1586 import std.array;
|
|
1587 version (Posix)
|
|
1588 {
|
|
1589 assert(chainPath("foo", "bar", "baz").array == "foo/bar/baz");
|
|
1590 assert(chainPath("/foo/", "bar/baz").array == "/foo/bar/baz");
|
|
1591 assert(chainPath("/foo", "/bar").array == "/bar");
|
|
1592 }
|
|
1593
|
|
1594 version (Windows)
|
|
1595 {
|
|
1596 assert(chainPath("foo", "bar", "baz").array == `foo\bar\baz`);
|
|
1597 assert(chainPath(`c:\foo`, `bar\baz`).array == `c:\foo\bar\baz`);
|
|
1598 assert(chainPath("foo", `d:\bar`).array == `d:\bar`);
|
|
1599 assert(chainPath("foo", `\bar`).array == `\bar`);
|
|
1600 assert(chainPath(`c:\foo`, `\bar`).array == `c:\bar`);
|
|
1601 }
|
|
1602
|
|
1603 import std.utf : byChar;
|
|
1604 version (Posix)
|
|
1605 {
|
|
1606 assert(chainPath("foo", "bar", "baz").array == "foo/bar/baz");
|
|
1607 assert(chainPath("/foo/".byChar, "bar/baz").array == "/foo/bar/baz");
|
|
1608 assert(chainPath("/foo", "/bar".byChar).array == "/bar");
|
|
1609 }
|
|
1610
|
|
1611 version (Windows)
|
|
1612 {
|
|
1613 assert(chainPath("foo", "bar", "baz").array == `foo\bar\baz`);
|
|
1614 assert(chainPath(`c:\foo`.byChar, `bar\baz`).array == `c:\foo\bar\baz`);
|
|
1615 assert(chainPath("foo", `d:\bar`).array == `d:\bar`);
|
|
1616 assert(chainPath("foo", `\bar`.byChar).array == `\bar`);
|
|
1617 assert(chainPath(`c:\foo`, `\bar`w).array == `c:\bar`);
|
|
1618 }
|
|
1619 }
|
|
1620
|
|
1621 auto chainPath(Ranges...)(auto ref Ranges ranges)
|
|
1622 if (Ranges.length >= 2 &&
|
|
1623 std.meta.anySatisfy!(isConvertibleToString, Ranges))
|
|
1624 {
|
|
1625 import std.meta : staticMap;
|
|
1626 alias Types = staticMap!(convertToString, Ranges);
|
|
1627 return chainPath!Types(ranges);
|
|
1628 }
|
|
1629
|
|
1630 @safe unittest
|
|
1631 {
|
|
1632 assert(chainPath(TestAliasedString(null), TestAliasedString(null), TestAliasedString(null)).empty);
|
|
1633 assert(chainPath(TestAliasedString(null), TestAliasedString(null), "").empty);
|
|
1634 assert(chainPath(TestAliasedString(null), "", TestAliasedString(null)).empty);
|
|
1635 static struct S { string s; }
|
|
1636 static assert(!__traits(compiles, chainPath(TestAliasedString(null), S(""), TestAliasedString(null))));
|
|
1637 }
|
|
1638
|
|
1639 /** Performs the same task as $(LREF buildPath),
|
|
1640 while at the same time resolving current/parent directory
|
|
1641 symbols ($(D ".") and $(D "..")) and removing superfluous
|
|
1642 directory separators.
|
|
1643 It will return "." if the path leads to the starting directory.
|
|
1644 On Windows, slashes are replaced with backslashes.
|
|
1645
|
|
1646 Using buildNormalizedPath on null paths will always return null.
|
|
1647
|
|
1648 Note that this function does not resolve symbolic links.
|
|
1649
|
|
1650 This function always allocates memory to hold the resulting path.
|
|
1651 Use $(LREF asNormalizedPath) to not allocate memory.
|
|
1652
|
|
1653 Params:
|
|
1654 paths = An array of paths to assemble.
|
|
1655
|
|
1656 Returns: The assembled path.
|
|
1657 */
|
|
1658 immutable(C)[] buildNormalizedPath(C)(const(C[])[] paths...)
|
|
1659 @trusted pure nothrow
|
|
1660 if (isSomeChar!C)
|
|
1661 {
|
|
1662 import std.array : array;
|
|
1663
|
|
1664 const(C)[] result;
|
|
1665 foreach (path; paths)
|
|
1666 {
|
|
1667 if (result)
|
|
1668 result = chainPath(result, path).array;
|
|
1669 else
|
|
1670 result = path;
|
|
1671 }
|
|
1672 result = asNormalizedPath(result).array;
|
|
1673 return cast(typeof(return)) result;
|
|
1674 }
|
|
1675
|
|
1676 ///
|
|
1677 @safe unittest
|
|
1678 {
|
|
1679 assert(buildNormalizedPath("foo", "..") == ".");
|
|
1680
|
|
1681 version (Posix)
|
|
1682 {
|
|
1683 assert(buildNormalizedPath("/foo/./bar/..//baz/") == "/foo/baz");
|
|
1684 assert(buildNormalizedPath("../foo/.") == "../foo");
|
|
1685 assert(buildNormalizedPath("/foo", "bar/baz/") == "/foo/bar/baz");
|
|
1686 assert(buildNormalizedPath("/foo", "/bar/..", "baz") == "/baz");
|
|
1687 assert(buildNormalizedPath("foo/./bar", "../../", "../baz") == "../baz");
|
|
1688 assert(buildNormalizedPath("/foo/./bar", "../../baz") == "/baz");
|
|
1689 }
|
|
1690
|
|
1691 version (Windows)
|
|
1692 {
|
|
1693 assert(buildNormalizedPath(`c:\foo\.\bar/..\\baz\`) == `c:\foo\baz`);
|
|
1694 assert(buildNormalizedPath(`..\foo\.`) == `..\foo`);
|
|
1695 assert(buildNormalizedPath(`c:\foo`, `bar\baz\`) == `c:\foo\bar\baz`);
|
|
1696 assert(buildNormalizedPath(`c:\foo`, `bar/..`) == `c:\foo`);
|
|
1697 assert(buildNormalizedPath(`\\server\share\foo`, `..\bar`) ==
|
|
1698 `\\server\share\bar`);
|
|
1699 }
|
|
1700 }
|
|
1701
|
|
1702 @safe unittest
|
|
1703 {
|
|
1704 assert(buildNormalizedPath(".", ".") == ".");
|
|
1705 assert(buildNormalizedPath("foo", "..") == ".");
|
|
1706 assert(buildNormalizedPath("", "") is null);
|
|
1707 assert(buildNormalizedPath("", ".") == ".");
|
|
1708 assert(buildNormalizedPath(".", "") == ".");
|
|
1709 assert(buildNormalizedPath(null, "foo") == "foo");
|
|
1710 assert(buildNormalizedPath("", "foo") == "foo");
|
|
1711 assert(buildNormalizedPath("", "") == "");
|
|
1712 assert(buildNormalizedPath("", null) == "");
|
|
1713 assert(buildNormalizedPath(null, "") == "");
|
|
1714 assert(buildNormalizedPath!(char)(null, null) == "");
|
|
1715
|
|
1716 version (Posix)
|
|
1717 {
|
|
1718 assert(buildNormalizedPath("/", "foo", "bar") == "/foo/bar");
|
|
1719 assert(buildNormalizedPath("foo", "bar", "baz") == "foo/bar/baz");
|
|
1720 assert(buildNormalizedPath("foo", "bar/baz") == "foo/bar/baz");
|
|
1721 assert(buildNormalizedPath("foo", "bar//baz///") == "foo/bar/baz");
|
|
1722 assert(buildNormalizedPath("/foo", "bar/baz") == "/foo/bar/baz");
|
|
1723 assert(buildNormalizedPath("/foo", "/bar/baz") == "/bar/baz");
|
|
1724 assert(buildNormalizedPath("/foo/..", "/bar/./baz") == "/bar/baz");
|
|
1725 assert(buildNormalizedPath("/foo/..", "bar/baz") == "/bar/baz");
|
|
1726 assert(buildNormalizedPath("/foo/../../", "bar/baz") == "/bar/baz");
|
|
1727 assert(buildNormalizedPath("/foo/bar", "../baz") == "/foo/baz");
|
|
1728 assert(buildNormalizedPath("/foo/bar", "../../baz") == "/baz");
|
|
1729 assert(buildNormalizedPath("/foo/bar", ".././/baz/..", "wee/") == "/foo/wee");
|
|
1730 assert(buildNormalizedPath("//foo/bar", "baz///wee") == "/foo/bar/baz/wee");
|
|
1731 static assert(buildNormalizedPath("/foo/..", "/bar/./baz") == "/bar/baz");
|
|
1732 }
|
|
1733 else version (Windows)
|
|
1734 {
|
|
1735 assert(buildNormalizedPath(`\`, `foo`, `bar`) == `\foo\bar`);
|
|
1736 assert(buildNormalizedPath(`foo`, `bar`, `baz`) == `foo\bar\baz`);
|
|
1737 assert(buildNormalizedPath(`foo`, `bar\baz`) == `foo\bar\baz`);
|
|
1738 assert(buildNormalizedPath(`foo`, `bar\\baz\\\`) == `foo\bar\baz`);
|
|
1739 assert(buildNormalizedPath(`\foo`, `bar\baz`) == `\foo\bar\baz`);
|
|
1740 assert(buildNormalizedPath(`\foo`, `\bar\baz`) == `\bar\baz`);
|
|
1741 assert(buildNormalizedPath(`\foo\..`, `\bar\.\baz`) == `\bar\baz`);
|
|
1742 assert(buildNormalizedPath(`\foo\..`, `bar\baz`) == `\bar\baz`);
|
|
1743 assert(buildNormalizedPath(`\foo\..\..\`, `bar\baz`) == `\bar\baz`);
|
|
1744 assert(buildNormalizedPath(`\foo\bar`, `..\baz`) == `\foo\baz`);
|
|
1745 assert(buildNormalizedPath(`\foo\bar`, `../../baz`) == `\baz`);
|
|
1746 assert(buildNormalizedPath(`\foo\bar`, `..\.\/baz\..`, `wee\`) == `\foo\wee`);
|
|
1747
|
|
1748 assert(buildNormalizedPath(`c:\`, `foo`, `bar`) == `c:\foo\bar`);
|
|
1749 assert(buildNormalizedPath(`c:foo`, `bar`, `baz`) == `c:foo\bar\baz`);
|
|
1750 assert(buildNormalizedPath(`c:foo`, `bar\baz`) == `c:foo\bar\baz`);
|
|
1751 assert(buildNormalizedPath(`c:foo`, `bar\\baz\\\`) == `c:foo\bar\baz`);
|
|
1752 assert(buildNormalizedPath(`c:\foo`, `bar\baz`) == `c:\foo\bar\baz`);
|
|
1753 assert(buildNormalizedPath(`c:\foo`, `\bar\baz`) == `c:\bar\baz`);
|
|
1754 assert(buildNormalizedPath(`c:\foo\..`, `\bar\.\baz`) == `c:\bar\baz`);
|
|
1755 assert(buildNormalizedPath(`c:\foo\..`, `bar\baz`) == `c:\bar\baz`);
|
|
1756 assert(buildNormalizedPath(`c:\foo\..\..\`, `bar\baz`) == `c:\bar\baz`);
|
|
1757 assert(buildNormalizedPath(`c:\foo\bar`, `..\baz`) == `c:\foo\baz`);
|
|
1758 assert(buildNormalizedPath(`c:\foo\bar`, `..\..\baz`) == `c:\baz`);
|
|
1759 assert(buildNormalizedPath(`c:\foo\bar`, `..\.\\baz\..`, `wee\`) == `c:\foo\wee`);
|
|
1760
|
|
1761 assert(buildNormalizedPath(`\\server\share`, `foo`, `bar`) == `\\server\share\foo\bar`);
|
|
1762 assert(buildNormalizedPath(`\\server\share\`, `foo`, `bar`) == `\\server\share\foo\bar`);
|
|
1763 assert(buildNormalizedPath(`\\server\share\foo`, `bar\baz`) == `\\server\share\foo\bar\baz`);
|
|
1764 assert(buildNormalizedPath(`\\server\share\foo`, `\bar\baz`) == `\\server\share\bar\baz`);
|
|
1765 assert(buildNormalizedPath(`\\server\share\foo\..`, `\bar\.\baz`) == `\\server\share\bar\baz`);
|
|
1766 assert(buildNormalizedPath(`\\server\share\foo\..`, `bar\baz`) == `\\server\share\bar\baz`);
|
|
1767 assert(buildNormalizedPath(`\\server\share\foo\..\..\`, `bar\baz`) == `\\server\share\bar\baz`);
|
|
1768 assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\baz`) == `\\server\share\foo\baz`);
|
|
1769 assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\..\baz`) == `\\server\share\baz`);
|
|
1770 assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\.\\baz\..`, `wee\`) == `\\server\share\foo\wee`);
|
|
1771
|
|
1772 static assert(buildNormalizedPath(`\foo\..\..\`, `bar\baz`) == `\bar\baz`);
|
|
1773 }
|
|
1774 else static assert(0);
|
|
1775 }
|
|
1776
|
|
1777 @safe unittest
|
|
1778 {
|
|
1779 // Test for issue 7397
|
|
1780 string[] ary = ["a", "b"];
|
|
1781 version (Posix)
|
|
1782 {
|
|
1783 assert(buildNormalizedPath(ary) == "a/b");
|
|
1784 }
|
|
1785 else version (Windows)
|
|
1786 {
|
|
1787 assert(buildNormalizedPath(ary) == `a\b`);
|
|
1788 }
|
|
1789 }
|
|
1790
|
|
1791
|
|
1792 /** Normalize a path by resolving current/parent directory
|
|
1793 symbols ($(D ".") and $(D "..")) and removing superfluous
|
|
1794 directory separators.
|
|
1795 It will return "." if the path leads to the starting directory.
|
|
1796 On Windows, slashes are replaced with backslashes.
|
|
1797
|
|
1798 Using asNormalizedPath on empty paths will always return an empty path.
|
|
1799
|
|
1800 Does not resolve symbolic links.
|
|
1801
|
|
1802 This function always allocates memory to hold the resulting path.
|
|
1803 Use $(LREF buildNormalizedPath) to allocate memory and return a string.
|
|
1804
|
|
1805 Params:
|
|
1806 path = string or random access range representing the _path to normalize
|
|
1807
|
|
1808 Returns:
|
|
1809 normalized path as a forward range
|
|
1810 */
|
|
1811
|
|
1812 auto asNormalizedPath(R)(R path)
|
|
1813 if (isSomeChar!(ElementEncodingType!R) &&
|
|
1814 (isRandomAccessRange!R && hasSlicing!R && hasLength!R || isNarrowString!R) &&
|
|
1815 !isConvertibleToString!R)
|
|
1816 {
|
|
1817 alias C = Unqual!(ElementEncodingType!R);
|
|
1818 alias S = typeof(path[0 .. 0]);
|
|
1819
|
|
1820 static struct Result
|
|
1821 {
|
|
1822 @property bool empty()
|
|
1823 {
|
|
1824 return c == c.init;
|
|
1825 }
|
|
1826
|
|
1827 @property C front()
|
|
1828 {
|
|
1829 return c;
|
|
1830 }
|
|
1831
|
|
1832 void popFront()
|
|
1833 {
|
|
1834 C lastc = c;
|
|
1835 c = c.init;
|
|
1836 if (!element.empty)
|
|
1837 {
|
|
1838 c = getElement0();
|
|
1839 return;
|
|
1840 }
|
|
1841 L1:
|
|
1842 while (1)
|
|
1843 {
|
|
1844 if (elements.empty)
|
|
1845 {
|
|
1846 element = element[0 .. 0];
|
|
1847 return;
|
|
1848 }
|
|
1849 element = elements.front;
|
|
1850 elements.popFront();
|
|
1851 if (isDot(element) || (rooted && isDotDot(element)))
|
|
1852 continue;
|
|
1853
|
|
1854 if (rooted || !isDotDot(element))
|
|
1855 {
|
|
1856 int n = 1;
|
|
1857 auto elements2 = elements.save;
|
|
1858 while (!elements2.empty)
|
|
1859 {
|
|
1860 auto e = elements2.front;
|
|
1861 elements2.popFront();
|
|
1862 if (isDot(e))
|
|
1863 continue;
|
|
1864 if (isDotDot(e))
|
|
1865 {
|
|
1866 --n;
|
|
1867 if (n == 0)
|
|
1868 {
|
|
1869 elements = elements2;
|
|
1870 element = element[0 .. 0];
|
|
1871 continue L1;
|
|
1872 }
|
|
1873 }
|
|
1874 else
|
|
1875 ++n;
|
|
1876 }
|
|
1877 }
|
|
1878 break;
|
|
1879 }
|
|
1880
|
|
1881 static assert(dirSeparator.length == 1);
|
|
1882 if (lastc == dirSeparator[0] || lastc == lastc.init)
|
|
1883 c = getElement0();
|
|
1884 else
|
|
1885 c = dirSeparator[0];
|
|
1886 }
|
|
1887
|
|
1888 static if (isForwardRange!R)
|
|
1889 {
|
|
1890 @property auto save()
|
|
1891 {
|
|
1892 auto result = this;
|
|
1893 result.element = element.save;
|
|
1894 result.elements = elements.save;
|
|
1895 return result;
|
|
1896 }
|
|
1897 }
|
|
1898
|
|
1899 private:
|
|
1900 this(R path)
|
|
1901 {
|
|
1902 element = rootName(path);
|
|
1903 auto i = element.length;
|
|
1904 while (i < path.length && isDirSeparator(path[i]))
|
|
1905 ++i;
|
|
1906 rooted = i > 0;
|
|
1907 elements = pathSplitter(path[i .. $]);
|
|
1908 popFront();
|
|
1909 if (c == c.init && path.length)
|
|
1910 c = C('.');
|
|
1911 }
|
|
1912
|
|
1913 C getElement0()
|
|
1914 {
|
|
1915 static if (isNarrowString!S) // avoid autodecode
|
|
1916 {
|
|
1917 C c = element[0];
|
|
1918 element = element[1 .. $];
|
|
1919 }
|
|
1920 else
|
|
1921 {
|
|
1922 C c = element.front;
|
|
1923 element.popFront();
|
|
1924 }
|
|
1925 version (Windows)
|
|
1926 {
|
|
1927 if (c == '/') // can appear in root element
|
|
1928 c = '\\'; // use native Windows directory separator
|
|
1929 }
|
|
1930 return c;
|
|
1931 }
|
|
1932
|
|
1933 // See if elem is "."
|
|
1934 static bool isDot(S elem)
|
|
1935 {
|
|
1936 return elem.length == 1 && elem[0] == '.';
|
|
1937 }
|
|
1938
|
|
1939 // See if elem is ".."
|
|
1940 static bool isDotDot(S elem)
|
|
1941 {
|
|
1942 return elem.length == 2 && elem[0] == '.' && elem[1] == '.';
|
|
1943 }
|
|
1944
|
|
1945 bool rooted; // the path starts with a root directory
|
|
1946 C c;
|
|
1947 S element;
|
|
1948 typeof(pathSplitter(path[0 .. 0])) elements;
|
|
1949 }
|
|
1950
|
|
1951 return Result(path);
|
|
1952 }
|
|
1953
|
|
1954 ///
|
|
1955 @safe unittest
|
|
1956 {
|
|
1957 import std.array;
|
|
1958 assert(asNormalizedPath("foo/..").array == ".");
|
|
1959
|
|
1960 version (Posix)
|
|
1961 {
|
|
1962 assert(asNormalizedPath("/foo/./bar/..//baz/").array == "/foo/baz");
|
|
1963 assert(asNormalizedPath("../foo/.").array == "../foo");
|
|
1964 assert(asNormalizedPath("/foo/bar/baz/").array == "/foo/bar/baz");
|
|
1965 assert(asNormalizedPath("/foo/./bar/../../baz").array == "/baz");
|
|
1966 }
|
|
1967
|
|
1968 version (Windows)
|
|
1969 {
|
|
1970 assert(asNormalizedPath(`c:\foo\.\bar/..\\baz\`).array == `c:\foo\baz`);
|
|
1971 assert(asNormalizedPath(`..\foo\.`).array == `..\foo`);
|
|
1972 assert(asNormalizedPath(`c:\foo\bar\baz\`).array == `c:\foo\bar\baz`);
|
|
1973 assert(asNormalizedPath(`c:\foo\bar/..`).array == `c:\foo`);
|
|
1974 assert(asNormalizedPath(`\\server\share\foo\..\bar`).array ==
|
|
1975 `\\server\share\bar`);
|
|
1976 }
|
|
1977 }
|
|
1978
|
|
1979 auto asNormalizedPath(R)(auto ref R path)
|
|
1980 if (isConvertibleToString!R)
|
|
1981 {
|
|
1982 return asNormalizedPath!(StringTypeOf!R)(path);
|
|
1983 }
|
|
1984
|
|
1985 @safe unittest
|
|
1986 {
|
|
1987 assert(testAliasedString!asNormalizedPath(null));
|
|
1988 }
|
|
1989
|
|
1990 @safe unittest
|
|
1991 {
|
|
1992 import std.array;
|
|
1993 import std.utf : byChar;
|
|
1994
|
|
1995 assert(asNormalizedPath("").array is null);
|
|
1996 assert(asNormalizedPath("foo").array == "foo");
|
|
1997 assert(asNormalizedPath(".").array == ".");
|
|
1998 assert(asNormalizedPath("./.").array == ".");
|
|
1999 assert(asNormalizedPath("foo/..").array == ".");
|
|
2000
|
|
2001 auto save = asNormalizedPath("fob").save;
|
|
2002 save.popFront();
|
|
2003 assert(save.front == 'o');
|
|
2004
|
|
2005 version (Posix)
|
|
2006 {
|
|
2007 assert(asNormalizedPath("/foo/bar").array == "/foo/bar");
|
|
2008 assert(asNormalizedPath("foo/bar/baz").array == "foo/bar/baz");
|
|
2009 assert(asNormalizedPath("foo/bar/baz").array == "foo/bar/baz");
|
|
2010 assert(asNormalizedPath("foo/bar//baz///").array == "foo/bar/baz");
|
|
2011 assert(asNormalizedPath("/foo/bar/baz").array == "/foo/bar/baz");
|
|
2012 assert(asNormalizedPath("/foo/../bar/baz").array == "/bar/baz");
|
|
2013 assert(asNormalizedPath("/foo/../..//bar/baz").array == "/bar/baz");
|
|
2014 assert(asNormalizedPath("/foo/bar/../baz").array == "/foo/baz");
|
|
2015 assert(asNormalizedPath("/foo/bar/../../baz").array == "/baz");
|
|
2016 assert(asNormalizedPath("/foo/bar/.././/baz/../wee/").array == "/foo/wee");
|
|
2017 assert(asNormalizedPath("//foo/bar/baz///wee").array == "/foo/bar/baz/wee");
|
|
2018
|
|
2019 assert(asNormalizedPath("foo//bar").array == "foo/bar");
|
|
2020 assert(asNormalizedPath("foo/bar").array == "foo/bar");
|
|
2021
|
|
2022 //Curent dir path
|
|
2023 assert(asNormalizedPath("./").array == ".");
|
|
2024 assert(asNormalizedPath("././").array == ".");
|
|
2025 assert(asNormalizedPath("./foo/..").array == ".");
|
|
2026 assert(asNormalizedPath("foo/..").array == ".");
|
|
2027 }
|
|
2028 else version (Windows)
|
|
2029 {
|
|
2030 assert(asNormalizedPath(`\foo\bar`).array == `\foo\bar`);
|
|
2031 assert(asNormalizedPath(`foo\bar\baz`).array == `foo\bar\baz`);
|
|
2032 assert(asNormalizedPath(`foo\bar\baz`).array == `foo\bar\baz`);
|
|
2033 assert(asNormalizedPath(`foo\bar\\baz\\\`).array == `foo\bar\baz`);
|
|
2034 assert(asNormalizedPath(`\foo\bar\baz`).array == `\foo\bar\baz`);
|
|
2035 assert(asNormalizedPath(`\foo\..\\bar\.\baz`).array == `\bar\baz`);
|
|
2036 assert(asNormalizedPath(`\foo\..\bar\baz`).array == `\bar\baz`);
|
|
2037 assert(asNormalizedPath(`\foo\..\..\\bar\baz`).array == `\bar\baz`);
|
|
2038
|
|
2039 assert(asNormalizedPath(`\foo\bar\..\baz`).array == `\foo\baz`);
|
|
2040 assert(asNormalizedPath(`\foo\bar\../../baz`).array == `\baz`);
|
|
2041 assert(asNormalizedPath(`\foo\bar\..\.\/baz\..\wee\`).array == `\foo\wee`);
|
|
2042
|
|
2043 assert(asNormalizedPath(`c:\foo\bar`).array == `c:\foo\bar`);
|
|
2044 assert(asNormalizedPath(`c:foo\bar\baz`).array == `c:foo\bar\baz`);
|
|
2045 assert(asNormalizedPath(`c:foo\bar\baz`).array == `c:foo\bar\baz`);
|
|
2046 assert(asNormalizedPath(`c:foo\bar\\baz\\\`).array == `c:foo\bar\baz`);
|
|
2047 assert(asNormalizedPath(`c:\foo\bar\baz`).array == `c:\foo\bar\baz`);
|
|
2048
|
|
2049 assert(asNormalizedPath(`c:\foo\..\\bar\.\baz`).array == `c:\bar\baz`);
|
|
2050 assert(asNormalizedPath(`c:\foo\..\bar\baz`).array == `c:\bar\baz`);
|
|
2051 assert(asNormalizedPath(`c:\foo\..\..\\bar\baz`).array == `c:\bar\baz`);
|
|
2052 assert(asNormalizedPath(`c:\foo\bar\..\baz`).array == `c:\foo\baz`);
|
|
2053 assert(asNormalizedPath(`c:\foo\bar\..\..\baz`).array == `c:\baz`);
|
|
2054 assert(asNormalizedPath(`c:\foo\bar\..\.\\baz\..\wee\`).array == `c:\foo\wee`);
|
|
2055 assert(asNormalizedPath(`\\server\share\foo\bar`).array == `\\server\share\foo\bar`);
|
|
2056 assert(asNormalizedPath(`\\server\share\\foo\bar`).array == `\\server\share\foo\bar`);
|
|
2057 assert(asNormalizedPath(`\\server\share\foo\bar\baz`).array == `\\server\share\foo\bar\baz`);
|
|
2058 assert(asNormalizedPath(`\\server\share\foo\..\\bar\.\baz`).array == `\\server\share\bar\baz`);
|
|
2059 assert(asNormalizedPath(`\\server\share\foo\..\bar\baz`).array == `\\server\share\bar\baz`);
|
|
2060 assert(asNormalizedPath(`\\server\share\foo\..\..\\bar\baz`).array == `\\server\share\bar\baz`);
|
|
2061 assert(asNormalizedPath(`\\server\share\foo\bar\..\baz`).array == `\\server\share\foo\baz`);
|
|
2062 assert(asNormalizedPath(`\\server\share\foo\bar\..\..\baz`).array == `\\server\share\baz`);
|
|
2063 assert(asNormalizedPath(`\\server\share\foo\bar\..\.\\baz\..\wee\`).array == `\\server\share\foo\wee`);
|
|
2064
|
|
2065 static assert(asNormalizedPath(`\foo\..\..\\bar\baz`).array == `\bar\baz`);
|
|
2066
|
|
2067 assert(asNormalizedPath("foo//bar").array == `foo\bar`);
|
|
2068
|
|
2069 //Curent dir path
|
|
2070 assert(asNormalizedPath(`.\`).array == ".");
|
|
2071 assert(asNormalizedPath(`.\.\`).array == ".");
|
|
2072 assert(asNormalizedPath(`.\foo\..`).array == ".");
|
|
2073 assert(asNormalizedPath(`foo\..`).array == ".");
|
|
2074 }
|
|
2075 else static assert(0);
|
|
2076 }
|
|
2077
|
|
2078 @safe unittest
|
|
2079 {
|
|
2080 import std.array;
|
|
2081
|
|
2082 version (Posix)
|
|
2083 {
|
|
2084 // Trivial
|
|
2085 assert(asNormalizedPath("").empty);
|
|
2086 assert(asNormalizedPath("foo/bar").array == "foo/bar");
|
|
2087
|
|
2088 // Correct handling of leading slashes
|
|
2089 assert(asNormalizedPath("/").array == "/");
|
|
2090 assert(asNormalizedPath("///").array == "/");
|
|
2091 assert(asNormalizedPath("////").array == "/");
|
|
2092 assert(asNormalizedPath("/foo/bar").array == "/foo/bar");
|
|
2093 assert(asNormalizedPath("//foo/bar").array == "/foo/bar");
|
|
2094 assert(asNormalizedPath("///foo/bar").array == "/foo/bar");
|
|
2095 assert(asNormalizedPath("////foo/bar").array == "/foo/bar");
|
|
2096
|
|
2097 // Correct handling of single-dot symbol (current directory)
|
|
2098 assert(asNormalizedPath("/./foo").array == "/foo");
|
|
2099 assert(asNormalizedPath("/foo/./bar").array == "/foo/bar");
|
|
2100
|
|
2101 assert(asNormalizedPath("./foo").array == "foo");
|
|
2102 assert(asNormalizedPath("././foo").array == "foo");
|
|
2103 assert(asNormalizedPath("foo/././bar").array == "foo/bar");
|
|
2104
|
|
2105 // Correct handling of double-dot symbol (previous directory)
|
|
2106 assert(asNormalizedPath("/foo/../bar").array == "/bar");
|
|
2107 assert(asNormalizedPath("/foo/../../bar").array == "/bar");
|
|
2108 assert(asNormalizedPath("/../foo").array == "/foo");
|
|
2109 assert(asNormalizedPath("/../../foo").array == "/foo");
|
|
2110 assert(asNormalizedPath("/foo/..").array == "/");
|
|
2111 assert(asNormalizedPath("/foo/../..").array == "/");
|
|
2112
|
|
2113 assert(asNormalizedPath("foo/../bar").array == "bar");
|
|
2114 assert(asNormalizedPath("foo/../../bar").array == "../bar");
|
|
2115 assert(asNormalizedPath("../foo").array == "../foo");
|
|
2116 assert(asNormalizedPath("../../foo").array == "../../foo");
|
|
2117 assert(asNormalizedPath("../foo/../bar").array == "../bar");
|
|
2118 assert(asNormalizedPath(".././../foo").array == "../../foo");
|
|
2119 assert(asNormalizedPath("foo/bar/..").array == "foo");
|
|
2120 assert(asNormalizedPath("/foo/../..").array == "/");
|
|
2121
|
|
2122 // The ultimate path
|
|
2123 assert(asNormalizedPath("/foo/../bar//./../...///baz//").array == "/.../baz");
|
|
2124 static assert(asNormalizedPath("/foo/../bar//./../...///baz//").array == "/.../baz");
|
|
2125 }
|
|
2126 else version (Windows)
|
|
2127 {
|
|
2128 // Trivial
|
|
2129 assert(asNormalizedPath("").empty);
|
|
2130 assert(asNormalizedPath(`foo\bar`).array == `foo\bar`);
|
|
2131 assert(asNormalizedPath("foo/bar").array == `foo\bar`);
|
|
2132
|
|
2133 // Correct handling of absolute paths
|
|
2134 assert(asNormalizedPath("/").array == `\`);
|
|
2135 assert(asNormalizedPath(`\`).array == `\`);
|
|
2136 assert(asNormalizedPath(`\\\`).array == `\`);
|
|
2137 assert(asNormalizedPath(`\\\\`).array == `\`);
|
|
2138 assert(asNormalizedPath(`\foo\bar`).array == `\foo\bar`);
|
|
2139 assert(asNormalizedPath(`\\foo`).array == `\\foo`);
|
|
2140 assert(asNormalizedPath(`\\foo\\`).array == `\\foo`);
|
|
2141 assert(asNormalizedPath(`\\foo/bar`).array == `\\foo\bar`);
|
|
2142 assert(asNormalizedPath(`\\\foo\bar`).array == `\foo\bar`);
|
|
2143 assert(asNormalizedPath(`\\\\foo\bar`).array == `\foo\bar`);
|
|
2144 assert(asNormalizedPath(`c:\`).array == `c:\`);
|
|
2145 assert(asNormalizedPath(`c:\foo\bar`).array == `c:\foo\bar`);
|
|
2146 assert(asNormalizedPath(`c:\\foo\bar`).array == `c:\foo\bar`);
|
|
2147
|
|
2148 // Correct handling of single-dot symbol (current directory)
|
|
2149 assert(asNormalizedPath(`\./foo`).array == `\foo`);
|
|
2150 assert(asNormalizedPath(`\foo/.\bar`).array == `\foo\bar`);
|
|
2151
|
|
2152 assert(asNormalizedPath(`.\foo`).array == `foo`);
|
|
2153 assert(asNormalizedPath(`./.\foo`).array == `foo`);
|
|
2154 assert(asNormalizedPath(`foo\.\./bar`).array == `foo\bar`);
|
|
2155
|
|
2156 // Correct handling of double-dot symbol (previous directory)
|
|
2157 assert(asNormalizedPath(`\foo\..\bar`).array == `\bar`);
|
|
2158 assert(asNormalizedPath(`\foo\../..\bar`).array == `\bar`);
|
|
2159 assert(asNormalizedPath(`\..\foo`).array == `\foo`);
|
|
2160 assert(asNormalizedPath(`\..\..\foo`).array == `\foo`);
|
|
2161 assert(asNormalizedPath(`\foo\..`).array == `\`);
|
|
2162 assert(asNormalizedPath(`\foo\../..`).array == `\`);
|
|
2163
|
|
2164 assert(asNormalizedPath(`foo\..\bar`).array == `bar`);
|
|
2165 assert(asNormalizedPath(`foo\..\../bar`).array == `..\bar`);
|
|
2166
|
|
2167 assert(asNormalizedPath(`..\foo`).array == `..\foo`);
|
|
2168 assert(asNormalizedPath(`..\..\foo`).array == `..\..\foo`);
|
|
2169 assert(asNormalizedPath(`..\foo\..\bar`).array == `..\bar`);
|
|
2170 assert(asNormalizedPath(`..\.\..\foo`).array == `..\..\foo`);
|
|
2171 assert(asNormalizedPath(`foo\bar\..`).array == `foo`);
|
|
2172 assert(asNormalizedPath(`\foo\..\..`).array == `\`);
|
|
2173 assert(asNormalizedPath(`c:\foo\..\..`).array == `c:\`);
|
|
2174
|
|
2175 // Correct handling of non-root path with drive specifier
|
|
2176 assert(asNormalizedPath(`c:foo`).array == `c:foo`);
|
|
2177 assert(asNormalizedPath(`c:..\foo\.\..\bar`).array == `c:..\bar`);
|
|
2178
|
|
2179 // The ultimate path
|
|
2180 assert(asNormalizedPath(`c:\foo\..\bar\\.\..\...\\\baz\\`).array == `c:\...\baz`);
|
|
2181 static assert(asNormalizedPath(`c:\foo\..\bar\\.\..\...\\\baz\\`).array == `c:\...\baz`);
|
|
2182 }
|
|
2183 else static assert(false);
|
|
2184 }
|
|
2185
|
|
2186 /** Slice up a path into its elements.
|
|
2187
|
|
2188 Params:
|
|
2189 path = string or slicable random access range
|
|
2190
|
|
2191 Returns:
|
|
2192 bidirectional range of slices of `path`
|
|
2193 */
|
|
2194 auto pathSplitter(R)(R path)
|
|
2195 if ((isRandomAccessRange!R && hasSlicing!R ||
|
|
2196 isNarrowString!R) &&
|
|
2197 !isConvertibleToString!R)
|
|
2198 {
|
|
2199 static struct PathSplitter
|
|
2200 {
|
|
2201 @property bool empty() const { return pe == 0; }
|
|
2202
|
|
2203 @property R front()
|
|
2204 {
|
|
2205 assert(!empty);
|
|
2206 return _path[fs .. fe];
|
|
2207 }
|
|
2208
|
|
2209 void popFront()
|
|
2210 {
|
|
2211 assert(!empty);
|
|
2212 if (ps == pe)
|
|
2213 {
|
|
2214 if (fs == bs && fe == be)
|
|
2215 {
|
|
2216 pe = 0;
|
|
2217 }
|
|
2218 else
|
|
2219 {
|
|
2220 fs = bs;
|
|
2221 fe = be;
|
|
2222 }
|
|
2223 }
|
|
2224 else
|
|
2225 {
|
|
2226 fs = ps;
|
|
2227 fe = fs;
|
|
2228 while (fe < pe && !isDirSeparator(_path[fe]))
|
|
2229 ++fe;
|
|
2230 ps = ltrim(fe, pe);
|
|
2231 }
|
|
2232 }
|
|
2233
|
|
2234 @property R back()
|
|
2235 {
|
|
2236 assert(!empty);
|
|
2237 return _path[bs .. be];
|
|
2238 }
|
|
2239
|
|
2240 void popBack()
|
|
2241 {
|
|
2242 assert(!empty);
|
|
2243 if (ps == pe)
|
|
2244 {
|
|
2245 if (fs == bs && fe == be)
|
|
2246 {
|
|
2247 pe = 0;
|
|
2248 }
|
|
2249 else
|
|
2250 {
|
|
2251 bs = fs;
|
|
2252 be = fe;
|
|
2253 }
|
|
2254 }
|
|
2255 else
|
|
2256 {
|
|
2257 bs = pe;
|
|
2258 be = bs;
|
|
2259 while (bs > ps && !isDirSeparator(_path[bs - 1]))
|
|
2260 --bs;
|
|
2261 pe = rtrim(ps, bs);
|
|
2262 }
|
|
2263 }
|
|
2264 @property auto save() { return this; }
|
|
2265
|
|
2266
|
|
2267 private:
|
|
2268 R _path;
|
|
2269 size_t ps, pe;
|
|
2270 size_t fs, fe;
|
|
2271 size_t bs, be;
|
|
2272
|
|
2273 this(R p)
|
|
2274 {
|
|
2275 if (p.empty)
|
|
2276 {
|
|
2277 pe = 0;
|
|
2278 return;
|
|
2279 }
|
|
2280 _path = p;
|
|
2281
|
|
2282 ps = 0;
|
|
2283 pe = _path.length;
|
|
2284
|
|
2285 // If path is rooted, first element is special
|
|
2286 version (Windows)
|
|
2287 {
|
|
2288 if (isUNC(_path))
|
|
2289 {
|
|
2290 auto i = uncRootLength(_path);
|
|
2291 fs = 0;
|
|
2292 fe = i;
|
|
2293 ps = ltrim(fe, pe);
|
|
2294 }
|
|
2295 else if (isDriveRoot(_path))
|
|
2296 {
|
|
2297 fs = 0;
|
|
2298 fe = 3;
|
|
2299 ps = ltrim(fe, pe);
|
|
2300 }
|
|
2301 else if (_path.length >= 1 && isDirSeparator(_path[0]))
|
|
2302 {
|
|
2303 fs = 0;
|
|
2304 fe = 1;
|
|
2305 ps = ltrim(fe, pe);
|
|
2306 }
|
|
2307 else
|
|
2308 {
|
|
2309 assert(!isRooted(_path));
|
|
2310 popFront();
|
|
2311 }
|
|
2312 }
|
|
2313 else version (Posix)
|
|
2314 {
|
|
2315 if (_path.length >= 1 && isDirSeparator(_path[0]))
|
|
2316 {
|
|
2317 fs = 0;
|
|
2318 fe = 1;
|
|
2319 ps = ltrim(fe, pe);
|
|
2320 }
|
|
2321 else
|
|
2322 {
|
|
2323 popFront();
|
|
2324 }
|
|
2325 }
|
|
2326 else static assert(0);
|
|
2327
|
|
2328 if (ps == pe)
|
|
2329 {
|
|
2330 bs = fs;
|
|
2331 be = fe;
|
|
2332 }
|
|
2333 else
|
|
2334 {
|
|
2335 pe = rtrim(ps, pe);
|
|
2336 popBack();
|
|
2337 }
|
|
2338 }
|
|
2339
|
|
2340 size_t ltrim(size_t s, size_t e)
|
|
2341 {
|
|
2342 while (s < e && isDirSeparator(_path[s]))
|
|
2343 ++s;
|
|
2344 return s;
|
|
2345 }
|
|
2346
|
|
2347 size_t rtrim(size_t s, size_t e)
|
|
2348 {
|
|
2349 while (s < e && isDirSeparator(_path[e - 1]))
|
|
2350 --e;
|
|
2351 return e;
|
|
2352 }
|
|
2353 }
|
|
2354
|
|
2355 return PathSplitter(path);
|
|
2356 }
|
|
2357
|
|
2358 ///
|
|
2359 @safe unittest
|
|
2360 {
|
|
2361 import std.algorithm.comparison : equal;
|
|
2362 import std.conv : to;
|
|
2363
|
|
2364 assert(equal(pathSplitter("/"), ["/"]));
|
|
2365 assert(equal(pathSplitter("/foo/bar"), ["/", "foo", "bar"]));
|
|
2366 assert(equal(pathSplitter("foo/../bar//./"), ["foo", "..", "bar", "."]));
|
|
2367
|
|
2368 version (Posix)
|
|
2369 {
|
|
2370 assert(equal(pathSplitter("//foo/bar"), ["/", "foo", "bar"]));
|
|
2371 }
|
|
2372
|
|
2373 version (Windows)
|
|
2374 {
|
|
2375 assert(equal(pathSplitter(`foo\..\bar\/.\`), ["foo", "..", "bar", "."]));
|
|
2376 assert(equal(pathSplitter("c:"), ["c:"]));
|
|
2377 assert(equal(pathSplitter(`c:\foo\bar`), [`c:\`, "foo", "bar"]));
|
|
2378 assert(equal(pathSplitter(`c:foo\bar`), ["c:foo", "bar"]));
|
|
2379 }
|
|
2380 }
|
|
2381
|
|
2382 auto pathSplitter(R)(auto ref R path)
|
|
2383 if (isConvertibleToString!R)
|
|
2384 {
|
|
2385 return pathSplitter!(StringTypeOf!R)(path);
|
|
2386 }
|
|
2387
|
|
2388 @safe unittest
|
|
2389 {
|
|
2390 import std.algorithm.comparison : equal;
|
|
2391 assert(testAliasedString!pathSplitter("/"));
|
|
2392 }
|
|
2393
|
|
2394 @safe unittest
|
|
2395 {
|
|
2396 // equal2 verifies that the range is the same both ways, i.e.
|
|
2397 // through front/popFront and back/popBack.
|
|
2398 import std.algorithm;
|
|
2399 import std.range;
|
|
2400 bool equal2(R1, R2)(R1 r1, R2 r2)
|
|
2401 {
|
|
2402 static assert(isBidirectionalRange!R1);
|
|
2403 return equal(r1, r2) && equal(retro(r1), retro(r2));
|
|
2404 }
|
|
2405
|
|
2406 assert(pathSplitter("").empty);
|
|
2407
|
|
2408 // Root directories
|
|
2409 assert(equal2(pathSplitter("/"), ["/"]));
|
|
2410 assert(equal2(pathSplitter("//"), ["/"]));
|
|
2411 assert(equal2(pathSplitter("///"w), ["/"w]));
|
|
2412
|
|
2413 // Absolute paths
|
|
2414 assert(equal2(pathSplitter("/foo/bar".dup), ["/", "foo", "bar"]));
|
|
2415
|
|
2416 // General
|
|
2417 assert(equal2(pathSplitter("foo/bar"d.dup), ["foo"d, "bar"d]));
|
|
2418 assert(equal2(pathSplitter("foo//bar"), ["foo", "bar"]));
|
|
2419 assert(equal2(pathSplitter("foo/bar//"w), ["foo"w, "bar"w]));
|
|
2420 assert(equal2(pathSplitter("foo/../bar//./"d), ["foo"d, ".."d, "bar"d, "."d]));
|
|
2421
|
|
2422 // save()
|
|
2423 auto ps1 = pathSplitter("foo/bar/baz");
|
|
2424 auto ps2 = ps1.save;
|
|
2425 ps1.popFront();
|
|
2426 assert(equal2(ps1, ["bar", "baz"]));
|
|
2427 assert(equal2(ps2, ["foo", "bar", "baz"]));
|
|
2428
|
|
2429 // Platform specific
|
|
2430 version (Posix)
|
|
2431 {
|
|
2432 assert(equal2(pathSplitter("//foo/bar"w.dup), ["/"w, "foo"w, "bar"w]));
|
|
2433 }
|
|
2434 version (Windows)
|
|
2435 {
|
|
2436 assert(equal2(pathSplitter(`\`), [`\`]));
|
|
2437 assert(equal2(pathSplitter(`foo\..\bar\/.\`), ["foo", "..", "bar", "."]));
|
|
2438 assert(equal2(pathSplitter("c:"), ["c:"]));
|
|
2439 assert(equal2(pathSplitter(`c:\foo\bar`), [`c:\`, "foo", "bar"]));
|
|
2440 assert(equal2(pathSplitter(`c:foo\bar`), ["c:foo", "bar"]));
|
|
2441 assert(equal2(pathSplitter(`\\foo\bar`), [`\\foo\bar`]));
|
|
2442 assert(equal2(pathSplitter(`\\foo\bar\\`), [`\\foo\bar`]));
|
|
2443 assert(equal2(pathSplitter(`\\foo\bar\baz`), [`\\foo\bar`, "baz"]));
|
|
2444 }
|
|
2445
|
|
2446 import std.exception;
|
|
2447 assertCTFEable!(
|
|
2448 {
|
|
2449 assert(equal(pathSplitter("/foo/bar".dup), ["/", "foo", "bar"]));
|
|
2450 });
|
|
2451
|
|
2452 static assert(is(typeof(pathSplitter!(const(char)[])(null).front) == const(char)[]));
|
|
2453
|
|
2454 import std.utf : byDchar;
|
|
2455 assert(equal2(pathSplitter("foo/bar"d.byDchar), ["foo"d, "bar"d]));
|
|
2456 }
|
|
2457
|
|
2458
|
|
2459
|
|
2460
|
|
2461 /** Determines whether a path starts at a root directory.
|
|
2462
|
|
2463 Params: path = A path name.
|
|
2464 Returns: Whether a path starts at a root directory.
|
|
2465
|
|
2466 On POSIX, this function returns true if and only if the path starts
|
|
2467 with a slash (/).
|
|
2468 ---
|
|
2469 version (Posix)
|
|
2470 {
|
|
2471 assert(isRooted("/"));
|
|
2472 assert(isRooted("/foo"));
|
|
2473 assert(!isRooted("foo"));
|
|
2474 assert(!isRooted("../foo"));
|
|
2475 }
|
|
2476 ---
|
|
2477
|
|
2478 On Windows, this function returns true if the path starts at
|
|
2479 the root directory of the current drive, of some other drive,
|
|
2480 or of a network drive.
|
|
2481 ---
|
|
2482 version (Windows)
|
|
2483 {
|
|
2484 assert(isRooted(`\`));
|
|
2485 assert(isRooted(`\foo`));
|
|
2486 assert(isRooted(`d:\foo`));
|
|
2487 assert(isRooted(`\\foo\bar`));
|
|
2488 assert(!isRooted("foo"));
|
|
2489 assert(!isRooted("d:foo"));
|
|
2490 }
|
|
2491 ---
|
|
2492 */
|
|
2493 bool isRooted(R)(R path)
|
|
2494 if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
2495 is(StringTypeOf!R))
|
|
2496 {
|
|
2497 if (path.length >= 1 && isDirSeparator(path[0])) return true;
|
|
2498 version (Posix) return false;
|
|
2499 else version (Windows) return isAbsolute!(BaseOf!R)(path);
|
|
2500 }
|
|
2501
|
|
2502
|
|
2503 @safe unittest
|
|
2504 {
|
|
2505 assert(isRooted("/"));
|
|
2506 assert(isRooted("/foo"));
|
|
2507 assert(!isRooted("foo"));
|
|
2508 assert(!isRooted("../foo"));
|
|
2509
|
|
2510 version (Windows)
|
|
2511 {
|
|
2512 assert(isRooted(`\`));
|
|
2513 assert(isRooted(`\foo`));
|
|
2514 assert(isRooted(`d:\foo`));
|
|
2515 assert(isRooted(`\\foo\bar`));
|
|
2516 assert(!isRooted("foo"));
|
|
2517 assert(!isRooted("d:foo"));
|
|
2518 }
|
|
2519
|
|
2520 static assert(isRooted("/foo"));
|
|
2521 static assert(!isRooted("foo"));
|
|
2522
|
|
2523 static struct DirEntry { string s; alias s this; }
|
|
2524 assert(!isRooted(DirEntry("foo")));
|
|
2525 }
|
|
2526
|
|
2527
|
|
2528
|
|
2529
|
|
2530 /** Determines whether a path is absolute or not.
|
|
2531
|
|
2532 Params: path = A path name.
|
|
2533
|
|
2534 Returns: Whether a path is absolute or not.
|
|
2535
|
|
2536 Example:
|
|
2537 On POSIX, an absolute path starts at the root directory.
|
|
2538 (In fact, $(D _isAbsolute) is just an alias for $(LREF isRooted).)
|
|
2539 ---
|
|
2540 version (Posix)
|
|
2541 {
|
|
2542 assert(isAbsolute("/"));
|
|
2543 assert(isAbsolute("/foo"));
|
|
2544 assert(!isAbsolute("foo"));
|
|
2545 assert(!isAbsolute("../foo"));
|
|
2546 }
|
|
2547 ---
|
|
2548
|
|
2549 On Windows, an absolute path starts at the root directory of
|
|
2550 a specific drive. Hence, it must start with $(D `d:\`) or $(D `d:/`),
|
|
2551 where $(D d) is the drive letter. Alternatively, it may be a
|
|
2552 network path, i.e. a path starting with a double (back)slash.
|
|
2553 ---
|
|
2554 version (Windows)
|
|
2555 {
|
|
2556 assert(isAbsolute(`d:\`));
|
|
2557 assert(isAbsolute(`d:\foo`));
|
|
2558 assert(isAbsolute(`\\foo\bar`));
|
|
2559 assert(!isAbsolute(`\`));
|
|
2560 assert(!isAbsolute(`\foo`));
|
|
2561 assert(!isAbsolute("d:foo"));
|
|
2562 }
|
|
2563 ---
|
|
2564 */
|
|
2565 version (StdDdoc)
|
|
2566 {
|
|
2567 bool isAbsolute(R)(R path) pure nothrow @safe
|
|
2568 if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
2569 is(StringTypeOf!R));
|
|
2570 }
|
|
2571 else version (Windows)
|
|
2572 {
|
|
2573 bool isAbsolute(R)(R path)
|
|
2574 if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
2575 is(StringTypeOf!R))
|
|
2576 {
|
|
2577 return isDriveRoot!(BaseOf!R)(path) || isUNC!(BaseOf!R)(path);
|
|
2578 }
|
|
2579 }
|
|
2580 else version (Posix)
|
|
2581 {
|
|
2582 alias isAbsolute = isRooted;
|
|
2583 }
|
|
2584
|
|
2585
|
|
2586 @safe unittest
|
|
2587 {
|
|
2588 assert(!isAbsolute("foo"));
|
|
2589 assert(!isAbsolute("../foo"w));
|
|
2590 static assert(!isAbsolute("foo"));
|
|
2591
|
|
2592 version (Posix)
|
|
2593 {
|
|
2594 assert(isAbsolute("/"d));
|
|
2595 assert(isAbsolute("/foo".dup));
|
|
2596 static assert(isAbsolute("/foo"));
|
|
2597 }
|
|
2598
|
|
2599 version (Windows)
|
|
2600 {
|
|
2601 assert(isAbsolute("d:\\"w));
|
|
2602 assert(isAbsolute("d:\\foo"d));
|
|
2603 assert(isAbsolute("\\\\foo\\bar"));
|
|
2604 assert(!isAbsolute("\\"w.dup));
|
|
2605 assert(!isAbsolute("\\foo"d.dup));
|
|
2606 assert(!isAbsolute("d:"));
|
|
2607 assert(!isAbsolute("d:foo"));
|
|
2608 static assert(isAbsolute(`d:\foo`));
|
|
2609 }
|
|
2610
|
|
2611 {
|
|
2612 auto r = MockRange!(immutable(char))(`../foo`);
|
|
2613 assert(!r.isAbsolute());
|
|
2614 }
|
|
2615
|
|
2616 static struct DirEntry { string s; alias s this; }
|
|
2617 assert(!isAbsolute(DirEntry("foo")));
|
|
2618 }
|
|
2619
|
|
2620
|
|
2621
|
|
2622
|
|
2623 /** Transforms $(D path) into an absolute _path.
|
|
2624
|
|
2625 The following algorithm is used:
|
|
2626 $(OL
|
|
2627 $(LI If $(D path) is empty, return $(D null).)
|
|
2628 $(LI If $(D path) is already absolute, return it.)
|
|
2629 $(LI Otherwise, append $(D path) to $(D base) and return
|
|
2630 the result. If $(D base) is not specified, the current
|
|
2631 working directory is used.)
|
|
2632 )
|
|
2633 The function allocates memory if and only if it gets to the third stage
|
|
2634 of this algorithm.
|
|
2635
|
|
2636 Params:
|
|
2637 path = the relative path to transform
|
|
2638 base = the base directory of the relative path
|
|
2639
|
|
2640 Returns:
|
|
2641 string of transformed path
|
|
2642
|
|
2643 Throws:
|
|
2644 $(D Exception) if the specified _base directory is not absolute.
|
|
2645
|
|
2646 See_Also:
|
|
2647 $(LREF asAbsolutePath) which does not allocate
|
|
2648 */
|
|
2649 string absolutePath(string path, lazy string base = getcwd())
|
|
2650 @safe pure
|
|
2651 {
|
|
2652 import std.array : array;
|
|
2653 if (path.empty) return null;
|
|
2654 if (isAbsolute(path)) return path;
|
|
2655 auto baseVar = base;
|
|
2656 if (!isAbsolute(baseVar)) throw new Exception("Base directory must be absolute");
|
|
2657 return chainPath(baseVar, path).array;
|
|
2658 }
|
|
2659
|
|
2660 ///
|
|
2661 @safe unittest
|
|
2662 {
|
|
2663 version (Posix)
|
|
2664 {
|
|
2665 assert(absolutePath("some/file", "/foo/bar") == "/foo/bar/some/file");
|
|
2666 assert(absolutePath("../file", "/foo/bar") == "/foo/bar/../file");
|
|
2667 assert(absolutePath("/some/file", "/foo/bar") == "/some/file");
|
|
2668 }
|
|
2669
|
|
2670 version (Windows)
|
|
2671 {
|
|
2672 assert(absolutePath(`some\file`, `c:\foo\bar`) == `c:\foo\bar\some\file`);
|
|
2673 assert(absolutePath(`..\file`, `c:\foo\bar`) == `c:\foo\bar\..\file`);
|
|
2674 assert(absolutePath(`c:\some\file`, `c:\foo\bar`) == `c:\some\file`);
|
|
2675 assert(absolutePath(`\`, `c:\`) == `c:\`);
|
|
2676 assert(absolutePath(`\some\file`, `c:\foo\bar`) == `c:\some\file`);
|
|
2677 }
|
|
2678 }
|
|
2679
|
|
2680 @safe unittest
|
|
2681 {
|
|
2682 version (Posix)
|
|
2683 {
|
|
2684 static assert(absolutePath("some/file", "/foo/bar") == "/foo/bar/some/file");
|
|
2685 }
|
|
2686
|
|
2687 version (Windows)
|
|
2688 {
|
|
2689 static assert(absolutePath(`some\file`, `c:\foo\bar`) == `c:\foo\bar\some\file`);
|
|
2690 }
|
|
2691
|
|
2692 import std.exception;
|
|
2693 assertThrown(absolutePath("bar", "foo"));
|
|
2694 }
|
|
2695
|
|
2696 /** Transforms $(D path) into an absolute _path.
|
|
2697
|
|
2698 The following algorithm is used:
|
|
2699 $(OL
|
|
2700 $(LI If $(D path) is empty, return $(D null).)
|
|
2701 $(LI If $(D path) is already absolute, return it.)
|
|
2702 $(LI Otherwise, append $(D path) to the current working directory,
|
|
2703 which allocates memory.)
|
|
2704 )
|
|
2705
|
|
2706 Params:
|
|
2707 path = the relative path to transform
|
|
2708
|
|
2709 Returns:
|
|
2710 the transformed path as a lazy range
|
|
2711
|
|
2712 See_Also:
|
|
2713 $(LREF absolutePath) which returns an allocated string
|
|
2714 */
|
|
2715 auto asAbsolutePath(R)(R path)
|
|
2716 if ((isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
2717 isNarrowString!R) &&
|
|
2718 !isConvertibleToString!R)
|
|
2719 {
|
|
2720 import std.file : getcwd;
|
|
2721 string base = null;
|
|
2722 if (!path.empty && !isAbsolute(path))
|
|
2723 base = getcwd();
|
|
2724 return chainPath(base, path);
|
|
2725 }
|
|
2726
|
|
2727 ///
|
|
2728 @system unittest
|
|
2729 {
|
|
2730 import std.array;
|
|
2731 assert(asAbsolutePath(cast(string) null).array == "");
|
|
2732 version (Posix)
|
|
2733 {
|
|
2734 assert(asAbsolutePath("/foo").array == "/foo");
|
|
2735 }
|
|
2736 version (Windows)
|
|
2737 {
|
|
2738 assert(asAbsolutePath("c:/foo").array == "c:/foo");
|
|
2739 }
|
|
2740 asAbsolutePath("foo");
|
|
2741 }
|
|
2742
|
|
2743 auto asAbsolutePath(R)(auto ref R path)
|
|
2744 if (isConvertibleToString!R)
|
|
2745 {
|
|
2746 return asAbsolutePath!(StringTypeOf!R)(path);
|
|
2747 }
|
|
2748
|
|
2749 @system unittest
|
|
2750 {
|
|
2751 assert(testAliasedString!asAbsolutePath(null));
|
|
2752 }
|
|
2753
|
|
2754 /** Translates $(D path) into a relative _path.
|
|
2755
|
|
2756 The returned _path is relative to $(D base), which is by default
|
|
2757 taken to be the current working directory. If specified,
|
|
2758 $(D base) must be an absolute _path, and it is always assumed
|
|
2759 to refer to a directory. If $(D path) and $(D base) refer to
|
|
2760 the same directory, the function returns $(D `.`).
|
|
2761
|
|
2762 The following algorithm is used:
|
|
2763 $(OL
|
|
2764 $(LI If $(D path) is a relative directory, return it unaltered.)
|
|
2765 $(LI Find a common root between $(D path) and $(D base).
|
|
2766 If there is no common root, return $(D path) unaltered.)
|
|
2767 $(LI Prepare a string with as many $(D `../`) or $(D `..\`) as
|
|
2768 necessary to reach the common root from base path.)
|
|
2769 $(LI Append the remaining segments of $(D path) to the string
|
|
2770 and return.)
|
|
2771 )
|
|
2772
|
|
2773 In the second step, path components are compared using $(D filenameCmp!cs),
|
|
2774 where $(D cs) is an optional template parameter determining whether
|
|
2775 the comparison is case sensitive or not. See the
|
|
2776 $(LREF filenameCmp) documentation for details.
|
|
2777
|
|
2778 This function allocates memory.
|
|
2779
|
|
2780 Params:
|
|
2781 cs = Whether matching path name components against the base path should
|
|
2782 be case-sensitive or not.
|
|
2783 path = A path name.
|
|
2784 base = The base path to construct the relative path from.
|
|
2785
|
|
2786 Returns: The relative path.
|
|
2787
|
|
2788 See_Also:
|
|
2789 $(LREF asRelativePath) which does not allocate memory
|
|
2790
|
|
2791 Throws:
|
|
2792 $(D Exception) if the specified _base directory is not absolute.
|
|
2793 */
|
|
2794 string relativePath(CaseSensitive cs = CaseSensitive.osDefault)
|
|
2795 (string path, lazy string base = getcwd())
|
|
2796 {
|
|
2797 if (!isAbsolute(path))
|
|
2798 return path;
|
|
2799 auto baseVar = base;
|
|
2800 if (!isAbsolute(baseVar))
|
|
2801 throw new Exception("Base directory must be absolute");
|
|
2802
|
|
2803 import std.conv : to;
|
|
2804 return asRelativePath!cs(path, baseVar).to!string;
|
|
2805 }
|
|
2806
|
|
2807 ///
|
|
2808 @system unittest
|
|
2809 {
|
|
2810 assert(relativePath("foo") == "foo");
|
|
2811
|
|
2812 version (Posix)
|
|
2813 {
|
|
2814 assert(relativePath("foo", "/bar") == "foo");
|
|
2815 assert(relativePath("/foo/bar", "/foo/bar") == ".");
|
|
2816 assert(relativePath("/foo/bar", "/foo/baz") == "../bar");
|
|
2817 assert(relativePath("/foo/bar/baz", "/foo/woo/wee") == "../../bar/baz");
|
|
2818 assert(relativePath("/foo/bar/baz", "/foo/bar") == "baz");
|
|
2819 }
|
|
2820 version (Windows)
|
|
2821 {
|
|
2822 assert(relativePath("foo", `c:\bar`) == "foo");
|
|
2823 assert(relativePath(`c:\foo\bar`, `c:\foo\bar`) == ".");
|
|
2824 assert(relativePath(`c:\foo\bar`, `c:\foo\baz`) == `..\bar`);
|
|
2825 assert(relativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`) == `..\..\bar\baz`);
|
|
2826 assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz");
|
|
2827 assert(relativePath(`c:\foo\bar`, `d:\foo`) == `c:\foo\bar`);
|
|
2828 }
|
|
2829 }
|
|
2830
|
|
2831 @system unittest
|
|
2832 {
|
|
2833 import std.exception;
|
|
2834 assert(relativePath("foo") == "foo");
|
|
2835 version (Posix)
|
|
2836 {
|
|
2837 relativePath("/foo");
|
|
2838 assert(relativePath("/foo/bar", "/foo/baz") == "../bar");
|
|
2839 assertThrown(relativePath("/foo", "bar"));
|
|
2840 }
|
|
2841 else version (Windows)
|
|
2842 {
|
|
2843 relativePath(`\foo`);
|
|
2844 assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz");
|
|
2845 assertThrown(relativePath(`c:\foo`, "bar"));
|
|
2846 }
|
|
2847 else static assert(0);
|
|
2848 }
|
|
2849
|
|
2850 /** Transforms `path` into a _path relative to `base`.
|
|
2851
|
|
2852 The returned _path is relative to `base`, which is usually
|
|
2853 the current working directory.
|
|
2854 `base` must be an absolute _path, and it is always assumed
|
|
2855 to refer to a directory. If `path` and `base` refer to
|
|
2856 the same directory, the function returns `'.'`.
|
|
2857
|
|
2858 The following algorithm is used:
|
|
2859 $(OL
|
|
2860 $(LI If `path` is a relative directory, return it unaltered.)
|
|
2861 $(LI Find a common root between `path` and `base`.
|
|
2862 If there is no common root, return `path` unaltered.)
|
|
2863 $(LI Prepare a string with as many `../` or `..\` as
|
|
2864 necessary to reach the common root from base path.)
|
|
2865 $(LI Append the remaining segments of `path` to the string
|
|
2866 and return.)
|
|
2867 )
|
|
2868
|
|
2869 In the second step, path components are compared using `filenameCmp!cs`,
|
|
2870 where `cs` is an optional template parameter determining whether
|
|
2871 the comparison is case sensitive or not. See the
|
|
2872 $(LREF filenameCmp) documentation for details.
|
|
2873
|
|
2874 Params:
|
|
2875 path = _path to transform
|
|
2876 base = absolute path
|
|
2877 cs = whether filespec comparisons are sensitive or not; defaults to
|
|
2878 `CaseSensitive.osDefault`
|
|
2879
|
|
2880 Returns:
|
|
2881 a random access range of the transformed _path
|
|
2882
|
|
2883 See_Also:
|
|
2884 $(LREF relativePath)
|
|
2885 */
|
|
2886 auto asRelativePath(CaseSensitive cs = CaseSensitive.osDefault, R1, R2)
|
|
2887 (R1 path, R2 base)
|
|
2888 if ((isNarrowString!R1 ||
|
|
2889 (isRandomAccessRange!R1 && hasSlicing!R1 && isSomeChar!(ElementType!R1)) &&
|
|
2890 !isConvertibleToString!R1) &&
|
|
2891 (isNarrowString!R2 ||
|
|
2892 (isRandomAccessRange!R2 && hasSlicing!R2 && isSomeChar!(ElementType!R2)) &&
|
|
2893 !isConvertibleToString!R2))
|
|
2894 {
|
|
2895 bool choosePath = !isAbsolute(path);
|
|
2896
|
|
2897 // Find common root with current working directory
|
|
2898
|
|
2899 auto basePS = pathSplitter(base);
|
|
2900 auto pathPS = pathSplitter(path);
|
|
2901 choosePath |= filenameCmp!cs(basePS.front, pathPS.front) != 0;
|
|
2902
|
|
2903 basePS.popFront();
|
|
2904 pathPS.popFront();
|
|
2905
|
|
2906 import std.algorithm.comparison : mismatch;
|
|
2907 import std.algorithm.iteration : joiner;
|
|
2908 import std.array : array;
|
|
2909 import std.range.primitives : walkLength;
|
|
2910 import std.range : repeat, chain, choose;
|
|
2911 import std.utf : byCodeUnit, byChar;
|
|
2912
|
|
2913 // Remove matching prefix from basePS and pathPS
|
|
2914 auto tup = mismatch!((a, b) => filenameCmp!cs(a, b) == 0)(basePS, pathPS);
|
|
2915 basePS = tup[0];
|
|
2916 pathPS = tup[1];
|
|
2917
|
|
2918 string sep;
|
|
2919 if (basePS.empty && pathPS.empty)
|
|
2920 sep = "."; // if base == path, this is the return
|
|
2921 else if (!basePS.empty && !pathPS.empty)
|
|
2922 sep = dirSeparator;
|
|
2923
|
|
2924 // Append as many "../" as necessary to reach common base from path
|
|
2925 auto r1 = ".."
|
|
2926 .byChar
|
|
2927 .repeat(basePS.walkLength())
|
|
2928 .joiner(dirSeparator.byChar);
|
|
2929
|
|
2930 auto r2 = pathPS
|
|
2931 .joiner(dirSeparator.byChar)
|
|
2932 .byChar;
|
|
2933
|
|
2934 // Return (r1 ~ sep ~ r2)
|
|
2935 return choose(choosePath, path.byCodeUnit, chain(r1, sep.byChar, r2));
|
|
2936 }
|
|
2937
|
|
2938 ///
|
|
2939 @system unittest
|
|
2940 {
|
|
2941 import std.array;
|
|
2942 version (Posix)
|
|
2943 {
|
|
2944 assert(asRelativePath("foo", "/bar").array == "foo");
|
|
2945 assert(asRelativePath("/foo/bar", "/foo/bar").array == ".");
|
|
2946 assert(asRelativePath("/foo/bar", "/foo/baz").array == "../bar");
|
|
2947 assert(asRelativePath("/foo/bar/baz", "/foo/woo/wee").array == "../../bar/baz");
|
|
2948 assert(asRelativePath("/foo/bar/baz", "/foo/bar").array == "baz");
|
|
2949 }
|
|
2950 else version (Windows)
|
|
2951 {
|
|
2952 assert(asRelativePath("foo", `c:\bar`).array == "foo");
|
|
2953 assert(asRelativePath(`c:\foo\bar`, `c:\foo\bar`).array == ".");
|
|
2954 assert(asRelativePath(`c:\foo\bar`, `c:\foo\baz`).array == `..\bar`);
|
|
2955 assert(asRelativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`).array == `..\..\bar\baz`);
|
|
2956 assert(asRelativePath(`c:/foo/bar/baz`, `c:\foo\woo\wee`).array == `..\..\bar\baz`);
|
|
2957 assert(asRelativePath(`c:\foo\bar\baz`, `c:\foo\bar`).array == "baz");
|
|
2958 assert(asRelativePath(`c:\foo\bar`, `d:\foo`).array == `c:\foo\bar`);
|
|
2959 assert(asRelativePath(`\\foo\bar`, `c:\foo`).array == `\\foo\bar`);
|
|
2960 }
|
|
2961 else
|
|
2962 static assert(0);
|
|
2963 }
|
|
2964
|
|
2965 auto asRelativePath(CaseSensitive cs = CaseSensitive.osDefault, R1, R2)
|
|
2966 (auto ref R1 path, auto ref R2 base)
|
|
2967 if (isConvertibleToString!R1 || isConvertibleToString!R2)
|
|
2968 {
|
|
2969 import std.meta : staticMap;
|
|
2970 alias Types = staticMap!(convertToString, R1, R2);
|
|
2971 return asRelativePath!(cs, Types)(path, base);
|
|
2972 }
|
|
2973
|
|
2974 @system unittest
|
|
2975 {
|
|
2976 import std.array;
|
|
2977 version (Posix)
|
|
2978 assert(asRelativePath(TestAliasedString("foo"), TestAliasedString("/bar")).array == "foo");
|
|
2979 else version (Windows)
|
|
2980 assert(asRelativePath(TestAliasedString("foo"), TestAliasedString(`c:\bar`)).array == "foo");
|
|
2981 assert(asRelativePath(TestAliasedString("foo"), "bar").array == "foo");
|
|
2982 assert(asRelativePath("foo", TestAliasedString("bar")).array == "foo");
|
|
2983 assert(asRelativePath(TestAliasedString("foo"), TestAliasedString("bar")).array == "foo");
|
|
2984 import std.utf : byDchar;
|
|
2985 assert(asRelativePath("foo"d.byDchar, TestAliasedString("bar")).array == "foo");
|
|
2986 }
|
|
2987
|
|
2988 @system unittest
|
|
2989 {
|
|
2990 import std.array, std.utf : bCU=byCodeUnit;
|
|
2991 version (Posix)
|
|
2992 {
|
|
2993 assert(asRelativePath("/foo/bar/baz".bCU, "/foo/bar".bCU).array == "baz");
|
|
2994 assert(asRelativePath("/foo/bar/baz"w.bCU, "/foo/bar"w.bCU).array == "baz"w);
|
|
2995 assert(asRelativePath("/foo/bar/baz"d.bCU, "/foo/bar"d.bCU).array == "baz"d);
|
|
2996 }
|
|
2997 else version (Windows)
|
|
2998 {
|
|
2999 assert(asRelativePath(`\\foo\bar`.bCU, `c:\foo`.bCU).array == `\\foo\bar`);
|
|
3000 assert(asRelativePath(`\\foo\bar`w.bCU, `c:\foo`w.bCU).array == `\\foo\bar`w);
|
|
3001 assert(asRelativePath(`\\foo\bar`d.bCU, `c:\foo`d.bCU).array == `\\foo\bar`d);
|
|
3002 }
|
|
3003 }
|
|
3004
|
|
3005 /** Compares filename characters.
|
|
3006
|
|
3007 This function can perform a case-sensitive or a case-insensitive
|
|
3008 comparison. This is controlled through the $(D cs) template parameter
|
|
3009 which, if not specified, is given by $(LREF CaseSensitive)$(D .osDefault).
|
|
3010
|
|
3011 On Windows, the backslash and slash characters ($(D `\`) and $(D `/`))
|
|
3012 are considered equal.
|
|
3013
|
|
3014 Params:
|
|
3015 cs = Case-sensitivity of the comparison.
|
|
3016 a = A filename character.
|
|
3017 b = A filename character.
|
|
3018
|
|
3019 Returns:
|
|
3020 $(D < 0) if $(D a < b),
|
|
3021 $(D 0) if $(D a == b), and
|
|
3022 $(D > 0) if $(D a > b).
|
|
3023 */
|
|
3024 int filenameCharCmp(CaseSensitive cs = CaseSensitive.osDefault)(dchar a, dchar b)
|
|
3025 @safe pure nothrow
|
|
3026 {
|
|
3027 if (isDirSeparator(a) && isDirSeparator(b)) return 0;
|
|
3028 static if (!cs)
|
|
3029 {
|
|
3030 import std.uni : toLower;
|
|
3031 a = toLower(a);
|
|
3032 b = toLower(b);
|
|
3033 }
|
|
3034 return cast(int)(a - b);
|
|
3035 }
|
|
3036
|
|
3037 ///
|
|
3038 @safe unittest
|
|
3039 {
|
|
3040 assert(filenameCharCmp('a', 'a') == 0);
|
|
3041 assert(filenameCharCmp('a', 'b') < 0);
|
|
3042 assert(filenameCharCmp('b', 'a') > 0);
|
|
3043
|
|
3044 version (linux)
|
|
3045 {
|
|
3046 // Same as calling filenameCharCmp!(CaseSensitive.yes)(a, b)
|
|
3047 assert(filenameCharCmp('A', 'a') < 0);
|
|
3048 assert(filenameCharCmp('a', 'A') > 0);
|
|
3049 }
|
|
3050 version (Windows)
|
|
3051 {
|
|
3052 // Same as calling filenameCharCmp!(CaseSensitive.no)(a, b)
|
|
3053 assert(filenameCharCmp('a', 'A') == 0);
|
|
3054 assert(filenameCharCmp('a', 'B') < 0);
|
|
3055 assert(filenameCharCmp('A', 'b') < 0);
|
|
3056 }
|
|
3057 }
|
|
3058
|
|
3059 @safe unittest
|
|
3060 {
|
|
3061 assert(filenameCharCmp!(CaseSensitive.yes)('A', 'a') < 0);
|
|
3062 assert(filenameCharCmp!(CaseSensitive.yes)('a', 'A') > 0);
|
|
3063
|
|
3064 assert(filenameCharCmp!(CaseSensitive.no)('a', 'a') == 0);
|
|
3065 assert(filenameCharCmp!(CaseSensitive.no)('a', 'b') < 0);
|
|
3066 assert(filenameCharCmp!(CaseSensitive.no)('b', 'a') > 0);
|
|
3067 assert(filenameCharCmp!(CaseSensitive.no)('A', 'a') == 0);
|
|
3068 assert(filenameCharCmp!(CaseSensitive.no)('a', 'A') == 0);
|
|
3069 assert(filenameCharCmp!(CaseSensitive.no)('a', 'B') < 0);
|
|
3070 assert(filenameCharCmp!(CaseSensitive.no)('B', 'a') > 0);
|
|
3071 assert(filenameCharCmp!(CaseSensitive.no)('A', 'b') < 0);
|
|
3072 assert(filenameCharCmp!(CaseSensitive.no)('b', 'A') > 0);
|
|
3073
|
|
3074 version (Posix) assert(filenameCharCmp('\\', '/') != 0);
|
|
3075 version (Windows) assert(filenameCharCmp('\\', '/') == 0);
|
|
3076 }
|
|
3077
|
|
3078
|
|
3079 /** Compares file names and returns
|
|
3080
|
|
3081 Individual characters are compared using $(D filenameCharCmp!cs),
|
|
3082 where $(D cs) is an optional template parameter determining whether
|
|
3083 the comparison is case sensitive or not.
|
|
3084
|
|
3085 Treatment of invalid UTF encodings is implementation defined.
|
|
3086
|
|
3087 Params:
|
|
3088 cs = case sensitivity
|
|
3089 filename1 = range for first file name
|
|
3090 filename2 = range for second file name
|
|
3091
|
|
3092 Returns:
|
|
3093 $(D < 0) if $(D filename1 < filename2),
|
|
3094 $(D 0) if $(D filename1 == filename2) and
|
|
3095 $(D > 0) if $(D filename1 > filename2).
|
|
3096
|
|
3097 See_Also:
|
|
3098 $(LREF filenameCharCmp)
|
|
3099 */
|
|
3100 int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault, Range1, Range2)
|
|
3101 (Range1 filename1, Range2 filename2)
|
|
3102 if (isInputRange!Range1 && !isInfinite!Range1 &&
|
|
3103 isSomeChar!(ElementEncodingType!Range1) &&
|
|
3104 !isConvertibleToString!Range1 &&
|
|
3105 isInputRange!Range2 && !isInfinite!Range2 &&
|
|
3106 isSomeChar!(ElementEncodingType!Range2) &&
|
|
3107 !isConvertibleToString!Range2)
|
|
3108 {
|
|
3109 alias C1 = Unqual!(ElementEncodingType!Range1);
|
|
3110 alias C2 = Unqual!(ElementEncodingType!Range2);
|
|
3111
|
|
3112 static if (!cs && (C1.sizeof < 4 || C2.sizeof < 4) ||
|
|
3113 C1.sizeof != C2.sizeof)
|
|
3114 {
|
|
3115 // Case insensitive - decode so case is checkable
|
|
3116 // Different char sizes - decode to bring to common type
|
|
3117 import std.utf : byDchar;
|
|
3118 return filenameCmp!cs(filename1.byDchar, filename2.byDchar);
|
|
3119 }
|
|
3120 else static if (isSomeString!Range1 && C1.sizeof < 4 ||
|
|
3121 isSomeString!Range2 && C2.sizeof < 4)
|
|
3122 {
|
|
3123 // Avoid autodecoding
|
|
3124 import std.utf : byCodeUnit;
|
|
3125 return filenameCmp!cs(filename1.byCodeUnit, filename2.byCodeUnit);
|
|
3126 }
|
|
3127 else
|
|
3128 {
|
|
3129 for (;;)
|
|
3130 {
|
|
3131 if (filename1.empty) return -(cast(int) !filename2.empty);
|
|
3132 if (filename2.empty) return 1;
|
|
3133 const c = filenameCharCmp!cs(filename1.front, filename2.front);
|
|
3134 if (c != 0) return c;
|
|
3135 filename1.popFront();
|
|
3136 filename2.popFront();
|
|
3137 }
|
|
3138 }
|
|
3139 }
|
|
3140
|
|
3141 ///
|
|
3142 @safe unittest
|
|
3143 {
|
|
3144 assert(filenameCmp("abc", "abc") == 0);
|
|
3145 assert(filenameCmp("abc", "abd") < 0);
|
|
3146 assert(filenameCmp("abc", "abb") > 0);
|
|
3147 assert(filenameCmp("abc", "abcd") < 0);
|
|
3148 assert(filenameCmp("abcd", "abc") > 0);
|
|
3149
|
|
3150 version (linux)
|
|
3151 {
|
|
3152 // Same as calling filenameCmp!(CaseSensitive.yes)(filename1, filename2)
|
|
3153 assert(filenameCmp("Abc", "abc") < 0);
|
|
3154 assert(filenameCmp("abc", "Abc") > 0);
|
|
3155 }
|
|
3156 version (Windows)
|
|
3157 {
|
|
3158 // Same as calling filenameCmp!(CaseSensitive.no)(filename1, filename2)
|
|
3159 assert(filenameCmp("Abc", "abc") == 0);
|
|
3160 assert(filenameCmp("abc", "Abc") == 0);
|
|
3161 assert(filenameCmp("Abc", "abD") < 0);
|
|
3162 assert(filenameCmp("abc", "AbB") > 0);
|
|
3163 }
|
|
3164 }
|
|
3165
|
|
3166 int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault, Range1, Range2)
|
|
3167 (auto ref Range1 filename1, auto ref Range2 filename2)
|
|
3168 if (isConvertibleToString!Range1 || isConvertibleToString!Range2)
|
|
3169 {
|
|
3170 import std.meta : staticMap;
|
|
3171 alias Types = staticMap!(convertToString, Range1, Range2);
|
|
3172 return filenameCmp!(cs, Types)(filename1, filename2);
|
|
3173 }
|
|
3174
|
|
3175 @safe unittest
|
|
3176 {
|
|
3177 assert(filenameCmp!(CaseSensitive.yes)(TestAliasedString("Abc"), "abc") < 0);
|
|
3178 assert(filenameCmp!(CaseSensitive.yes)("Abc", TestAliasedString("abc")) < 0);
|
|
3179 assert(filenameCmp!(CaseSensitive.yes)(TestAliasedString("Abc"), TestAliasedString("abc")) < 0);
|
|
3180 }
|
|
3181
|
|
3182 @safe unittest
|
|
3183 {
|
|
3184 assert(filenameCmp!(CaseSensitive.yes)("Abc", "abc") < 0);
|
|
3185 assert(filenameCmp!(CaseSensitive.yes)("abc", "Abc") > 0);
|
|
3186
|
|
3187 assert(filenameCmp!(CaseSensitive.no)("abc", "abc") == 0);
|
|
3188 assert(filenameCmp!(CaseSensitive.no)("abc", "abd") < 0);
|
|
3189 assert(filenameCmp!(CaseSensitive.no)("abc", "abb") > 0);
|
|
3190 assert(filenameCmp!(CaseSensitive.no)("abc", "abcd") < 0);
|
|
3191 assert(filenameCmp!(CaseSensitive.no)("abcd", "abc") > 0);
|
|
3192 assert(filenameCmp!(CaseSensitive.no)("Abc", "abc") == 0);
|
|
3193 assert(filenameCmp!(CaseSensitive.no)("abc", "Abc") == 0);
|
|
3194 assert(filenameCmp!(CaseSensitive.no)("Abc", "abD") < 0);
|
|
3195 assert(filenameCmp!(CaseSensitive.no)("abc", "AbB") > 0);
|
|
3196
|
|
3197 version (Posix) assert(filenameCmp(`abc\def`, `abc/def`) != 0);
|
|
3198 version (Windows) assert(filenameCmp(`abc\def`, `abc/def`) == 0);
|
|
3199 }
|
|
3200
|
|
3201 /** Matches a pattern against a path.
|
|
3202
|
|
3203 Some characters of pattern have a special meaning (they are
|
|
3204 $(I meta-characters)) and can't be escaped. These are:
|
|
3205
|
|
3206 $(BOOKTABLE,
|
|
3207 $(TR $(TD $(D *))
|
|
3208 $(TD Matches 0 or more instances of any character.))
|
|
3209 $(TR $(TD $(D ?))
|
|
3210 $(TD Matches exactly one instance of any character.))
|
|
3211 $(TR $(TD $(D [)$(I chars)$(D ]))
|
|
3212 $(TD Matches one instance of any character that appears
|
|
3213 between the brackets.))
|
|
3214 $(TR $(TD $(D [!)$(I chars)$(D ]))
|
|
3215 $(TD Matches one instance of any character that does not
|
|
3216 appear between the brackets after the exclamation mark.))
|
|
3217 $(TR $(TD $(D {)$(I string1)$(D ,)$(I string2)$(D ,)…$(D }))
|
|
3218 $(TD Matches either of the specified strings.))
|
|
3219 )
|
|
3220
|
|
3221 Individual characters are compared using $(D filenameCharCmp!cs),
|
|
3222 where $(D cs) is an optional template parameter determining whether
|
|
3223 the comparison is case sensitive or not. See the
|
|
3224 $(LREF filenameCharCmp) documentation for details.
|
|
3225
|
|
3226 Note that directory
|
|
3227 separators and dots don't stop a meta-character from matching
|
|
3228 further portions of the path.
|
|
3229
|
|
3230 Params:
|
|
3231 cs = Whether the matching should be case-sensitive
|
|
3232 path = The path to be matched against
|
|
3233 pattern = The glob pattern
|
|
3234
|
|
3235 Returns:
|
|
3236 $(D true) if pattern matches path, $(D false) otherwise.
|
|
3237
|
|
3238 See_also:
|
|
3239 $(LINK2 http://en.wikipedia.org/wiki/Glob_%28programming%29,Wikipedia: _glob (programming))
|
|
3240 */
|
|
3241 bool globMatch(CaseSensitive cs = CaseSensitive.osDefault, C, Range)
|
|
3242 (Range path, const(C)[] pattern)
|
|
3243 @safe pure nothrow
|
|
3244 if (isForwardRange!Range && !isInfinite!Range &&
|
|
3245 isSomeChar!(ElementEncodingType!Range) && !isConvertibleToString!Range &&
|
|
3246 isSomeChar!C && is(Unqual!C == Unqual!(ElementEncodingType!Range)))
|
|
3247 in
|
|
3248 {
|
|
3249 // Verify that pattern[] is valid
|
|
3250 import std.algorithm.searching : balancedParens;
|
|
3251 assert(balancedParens(pattern, '[', ']', 0));
|
|
3252 assert(balancedParens(pattern, '{', '}', 0));
|
|
3253 }
|
|
3254 body
|
|
3255 {
|
|
3256 alias RC = Unqual!(ElementEncodingType!Range);
|
|
3257
|
|
3258 static if (RC.sizeof == 1 && isSomeString!Range)
|
|
3259 {
|
|
3260 import std.utf : byChar;
|
|
3261 return globMatch!cs(path.byChar, pattern);
|
|
3262 }
|
|
3263 else static if (RC.sizeof == 2 && isSomeString!Range)
|
|
3264 {
|
|
3265 import std.utf : byWchar;
|
|
3266 return globMatch!cs(path.byWchar, pattern);
|
|
3267 }
|
|
3268 else
|
|
3269 {
|
|
3270 C[] pattmp;
|
|
3271 foreach (ref pi; 0 .. pattern.length)
|
|
3272 {
|
|
3273 const pc = pattern[pi];
|
|
3274 switch (pc)
|
|
3275 {
|
|
3276 case '*':
|
|
3277 if (pi + 1 == pattern.length)
|
|
3278 return true;
|
|
3279 for (; !path.empty; path.popFront())
|
|
3280 {
|
|
3281 auto p = path.save;
|
|
3282 if (globMatch!(cs, C)(p,
|
|
3283 pattern[pi + 1 .. pattern.length]))
|
|
3284 return true;
|
|
3285 }
|
|
3286 return false;
|
|
3287
|
|
3288 case '?':
|
|
3289 if (path.empty)
|
|
3290 return false;
|
|
3291 path.popFront();
|
|
3292 break;
|
|
3293
|
|
3294 case '[':
|
|
3295 if (path.empty)
|
|
3296 return false;
|
|
3297 auto nc = path.front;
|
|
3298 path.popFront();
|
|
3299 auto not = false;
|
|
3300 ++pi;
|
|
3301 if (pattern[pi] == '!')
|
|
3302 {
|
|
3303 not = true;
|
|
3304 ++pi;
|
|
3305 }
|
|
3306 auto anymatch = false;
|
|
3307 while (1)
|
|
3308 {
|
|
3309 const pc2 = pattern[pi];
|
|
3310 if (pc2 == ']')
|
|
3311 break;
|
|
3312 if (!anymatch && (filenameCharCmp!cs(nc, pc2) == 0))
|
|
3313 anymatch = true;
|
|
3314 ++pi;
|
|
3315 }
|
|
3316 if (anymatch == not)
|
|
3317 return false;
|
|
3318 break;
|
|
3319
|
|
3320 case '{':
|
|
3321 // find end of {} section
|
|
3322 auto piRemain = pi;
|
|
3323 for (; piRemain < pattern.length
|
|
3324 && pattern[piRemain] != '}'; ++piRemain)
|
|
3325 { }
|
|
3326
|
|
3327 if (piRemain < pattern.length)
|
|
3328 ++piRemain;
|
|
3329 ++pi;
|
|
3330
|
|
3331 while (pi < pattern.length)
|
|
3332 {
|
|
3333 const pi0 = pi;
|
|
3334 C pc3 = pattern[pi];
|
|
3335 // find end of current alternative
|
|
3336 for (; pi < pattern.length && pc3 != '}' && pc3 != ','; ++pi)
|
|
3337 {
|
|
3338 pc3 = pattern[pi];
|
|
3339 }
|
|
3340
|
|
3341 auto p = path.save;
|
|
3342 if (pi0 == pi)
|
|
3343 {
|
|
3344 if (globMatch!(cs, C)(p, pattern[piRemain..$]))
|
|
3345 {
|
|
3346 return true;
|
|
3347 }
|
|
3348 ++pi;
|
|
3349 }
|
|
3350 else
|
|
3351 {
|
|
3352 /* Match for:
|
|
3353 * pattern[pi0 .. pi-1] ~ pattern[piRemain..$]
|
|
3354 */
|
|
3355 if (pattmp is null)
|
|
3356 // Allocate this only once per function invocation.
|
|
3357 // Should do it with malloc/free, but that would make it impure.
|
|
3358 pattmp = new C[pattern.length];
|
|
3359
|
|
3360 const len1 = pi - 1 - pi0;
|
|
3361 pattmp[0 .. len1] = pattern[pi0 .. pi - 1];
|
|
3362
|
|
3363 const len2 = pattern.length - piRemain;
|
|
3364 pattmp[len1 .. len1 + len2] = pattern[piRemain .. $];
|
|
3365
|
|
3366 if (globMatch!(cs, C)(p, pattmp[0 .. len1 + len2]))
|
|
3367 {
|
|
3368 return true;
|
|
3369 }
|
|
3370 }
|
|
3371 if (pc3 == '}')
|
|
3372 {
|
|
3373 break;
|
|
3374 }
|
|
3375 }
|
|
3376 return false;
|
|
3377
|
|
3378 default:
|
|
3379 if (path.empty)
|
|
3380 return false;
|
|
3381 if (filenameCharCmp!cs(pc, path.front) != 0)
|
|
3382 return false;
|
|
3383 path.popFront();
|
|
3384 break;
|
|
3385 }
|
|
3386 }
|
|
3387 return path.empty;
|
|
3388 }
|
|
3389 }
|
|
3390
|
|
3391 ///
|
|
3392 @safe unittest
|
|
3393 {
|
|
3394 assert(globMatch("foo.bar", "*"));
|
|
3395 assert(globMatch("foo.bar", "*.*"));
|
|
3396 assert(globMatch(`foo/foo\bar`, "f*b*r"));
|
|
3397 assert(globMatch("foo.bar", "f???bar"));
|
|
3398 assert(globMatch("foo.bar", "[fg]???bar"));
|
|
3399 assert(globMatch("foo.bar", "[!gh]*bar"));
|
|
3400 assert(globMatch("bar.fooz", "bar.{foo,bif}z"));
|
|
3401 assert(globMatch("bar.bifz", "bar.{foo,bif}z"));
|
|
3402
|
|
3403 version (Windows)
|
|
3404 {
|
|
3405 // Same as calling globMatch!(CaseSensitive.no)(path, pattern)
|
|
3406 assert(globMatch("foo", "Foo"));
|
|
3407 assert(globMatch("Goo.bar", "[fg]???bar"));
|
|
3408 }
|
|
3409 version (linux)
|
|
3410 {
|
|
3411 // Same as calling globMatch!(CaseSensitive.yes)(path, pattern)
|
|
3412 assert(!globMatch("foo", "Foo"));
|
|
3413 assert(!globMatch("Goo.bar", "[fg]???bar"));
|
|
3414 }
|
|
3415 }
|
|
3416
|
|
3417 bool globMatch(CaseSensitive cs = CaseSensitive.osDefault, C, Range)
|
|
3418 (auto ref Range path, const(C)[] pattern)
|
|
3419 @safe pure nothrow
|
|
3420 if (isConvertibleToString!Range)
|
|
3421 {
|
|
3422 return globMatch!(cs, C, StringTypeOf!Range)(path, pattern);
|
|
3423 }
|
|
3424
|
|
3425 @safe unittest
|
|
3426 {
|
|
3427 assert(testAliasedString!globMatch("foo.bar", "*"));
|
|
3428 }
|
|
3429
|
|
3430 @safe unittest
|
|
3431 {
|
|
3432 assert(globMatch!(CaseSensitive.no)("foo", "Foo"));
|
|
3433 assert(!globMatch!(CaseSensitive.yes)("foo", "Foo"));
|
|
3434
|
|
3435 assert(globMatch("foo", "*"));
|
|
3436 assert(globMatch("foo.bar"w, "*"w));
|
|
3437 assert(globMatch("foo.bar"d, "*.*"d));
|
|
3438 assert(globMatch("foo.bar", "foo*"));
|
|
3439 assert(globMatch("foo.bar"w, "f*bar"w));
|
|
3440 assert(globMatch("foo.bar"d, "f*b*r"d));
|
|
3441 assert(globMatch("foo.bar", "f???bar"));
|
|
3442 assert(globMatch("foo.bar"w, "[fg]???bar"w));
|
|
3443 assert(globMatch("foo.bar"d, "[!gh]*bar"d));
|
|
3444
|
|
3445 assert(!globMatch("foo", "bar"));
|
|
3446 assert(!globMatch("foo"w, "*.*"w));
|
|
3447 assert(!globMatch("foo.bar"d, "f*baz"d));
|
|
3448 assert(!globMatch("foo.bar", "f*b*x"));
|
|
3449 assert(!globMatch("foo.bar", "[gh]???bar"));
|
|
3450 assert(!globMatch("foo.bar"w, "[!fg]*bar"w));
|
|
3451 assert(!globMatch("foo.bar"d, "[fg]???baz"d));
|
|
3452 assert(!globMatch("foo.di", "*.d")); // test issue 6634: triggered bad assertion
|
|
3453
|
|
3454 assert(globMatch("foo.bar", "{foo,bif}.bar"));
|
|
3455 assert(globMatch("bif.bar"w, "{foo,bif}.bar"w));
|
|
3456
|
|
3457 assert(globMatch("bar.foo"d, "bar.{foo,bif}"d));
|
|
3458 assert(globMatch("bar.bif", "bar.{foo,bif}"));
|
|
3459
|
|
3460 assert(globMatch("bar.fooz"w, "bar.{foo,bif}z"w));
|
|
3461 assert(globMatch("bar.bifz"d, "bar.{foo,bif}z"d));
|
|
3462
|
|
3463 assert(globMatch("bar.foo", "bar.{biz,,baz}foo"));
|
|
3464 assert(globMatch("bar.foo"w, "bar.{biz,}foo"w));
|
|
3465 assert(globMatch("bar.foo"d, "bar.{,biz}foo"d));
|
|
3466 assert(globMatch("bar.foo", "bar.{}foo"));
|
|
3467
|
|
3468 assert(globMatch("bar.foo"w, "bar.{ar,,fo}o"w));
|
|
3469 assert(globMatch("bar.foo"d, "bar.{,ar,fo}o"d));
|
|
3470 assert(globMatch("bar.o", "bar.{,ar,fo}o"));
|
|
3471
|
|
3472 assert(!globMatch("foo", "foo?"));
|
|
3473 assert(!globMatch("foo", "foo[]"));
|
|
3474 assert(!globMatch("foo", "foob"));
|
|
3475 assert(!globMatch("foo", "foo{b}"));
|
|
3476
|
|
3477
|
|
3478 static assert(globMatch("foo.bar", "[!gh]*bar"));
|
|
3479 }
|
|
3480
|
|
3481
|
|
3482
|
|
3483
|
|
3484 /** Checks that the given file or directory name is valid.
|
|
3485
|
|
3486 The maximum length of $(D filename) is given by the constant
|
|
3487 $(D core.stdc.stdio.FILENAME_MAX). (On Windows, this number is
|
|
3488 defined as the maximum number of UTF-16 code points, and the
|
|
3489 test will therefore only yield strictly correct results when
|
|
3490 $(D filename) is a string of $(D wchar)s.)
|
|
3491
|
|
3492 On Windows, the following criteria must be satisfied
|
|
3493 ($(LINK2 http://msdn.microsoft.com/en-us/library/aa365247(v=vs.85).aspx,source)):
|
|
3494 $(UL
|
|
3495 $(LI $(D filename) must not contain any characters whose integer
|
|
3496 representation is in the range 0-31.)
|
|
3497 $(LI $(D filename) must not contain any of the following $(I reserved
|
|
3498 characters): <>:"/\|?*)
|
|
3499 $(LI $(D filename) may not end with a space ($(D ' ')) or a period
|
|
3500 ($(D '.')).)
|
|
3501 )
|
|
3502
|
|
3503 On POSIX, $(D filename) may not contain a forward slash ($(D '/')) or
|
|
3504 the null character ($(D '\0')).
|
|
3505
|
|
3506 Params:
|
|
3507 filename = string to check
|
|
3508
|
|
3509 Returns:
|
|
3510 $(D true) if and only if $(D filename) is not
|
|
3511 empty, not too long, and does not contain invalid characters.
|
|
3512
|
|
3513 */
|
|
3514 bool isValidFilename(Range)(Range filename)
|
|
3515 if ((isRandomAccessRange!Range && hasLength!Range && hasSlicing!Range && isSomeChar!(ElementEncodingType!Range) ||
|
|
3516 isNarrowString!Range) &&
|
|
3517 !isConvertibleToString!Range)
|
|
3518 {
|
|
3519 import core.stdc.stdio : FILENAME_MAX;
|
|
3520 if (filename.length == 0 || filename.length >= FILENAME_MAX) return false;
|
|
3521 foreach (c; filename)
|
|
3522 {
|
|
3523 version (Windows)
|
|
3524 {
|
|
3525 switch (c)
|
|
3526 {
|
|
3527 case 0:
|
|
3528 ..
|
|
3529 case 31:
|
|
3530 case '<':
|
|
3531 case '>':
|
|
3532 case ':':
|
|
3533 case '"':
|
|
3534 case '/':
|
|
3535 case '\\':
|
|
3536 case '|':
|
|
3537 case '?':
|
|
3538 case '*':
|
|
3539 return false;
|
|
3540
|
|
3541 default:
|
|
3542 break;
|
|
3543 }
|
|
3544 }
|
|
3545 else version (Posix)
|
|
3546 {
|
|
3547 if (c == 0 || c == '/') return false;
|
|
3548 }
|
|
3549 else static assert(0);
|
|
3550 }
|
|
3551 version (Windows)
|
|
3552 {
|
|
3553 auto last = filename[filename.length - 1];
|
|
3554 if (last == '.' || last == ' ') return false;
|
|
3555 }
|
|
3556
|
|
3557 // All criteria passed
|
|
3558 return true;
|
|
3559 }
|
|
3560
|
|
3561 ///
|
|
3562 @safe pure @nogc nothrow
|
|
3563 unittest
|
|
3564 {
|
|
3565 import std.utf : byCodeUnit;
|
|
3566
|
|
3567 assert(isValidFilename("hello.exe".byCodeUnit));
|
|
3568 }
|
|
3569
|
|
3570 bool isValidFilename(Range)(auto ref Range filename)
|
|
3571 if (isConvertibleToString!Range)
|
|
3572 {
|
|
3573 return isValidFilename!(StringTypeOf!Range)(filename);
|
|
3574 }
|
|
3575
|
|
3576 @safe unittest
|
|
3577 {
|
|
3578 assert(testAliasedString!isValidFilename("hello.exe"));
|
|
3579 }
|
|
3580
|
|
3581 @safe pure
|
|
3582 unittest
|
|
3583 {
|
|
3584 import std.conv;
|
|
3585 auto valid = ["foo"];
|
|
3586 auto invalid = ["", "foo\0bar", "foo/bar"];
|
|
3587 auto pfdep = [`foo\bar`, "*.txt"];
|
|
3588 version (Windows) invalid ~= pfdep;
|
|
3589 else version (Posix) valid ~= pfdep;
|
|
3590 else static assert(0);
|
|
3591
|
|
3592 import std.meta : AliasSeq;
|
|
3593 foreach (T; AliasSeq!(char[], const(char)[], string, wchar[],
|
|
3594 const(wchar)[], wstring, dchar[], const(dchar)[], dstring))
|
|
3595 {
|
|
3596 foreach (fn; valid)
|
|
3597 assert(isValidFilename(to!T(fn)));
|
|
3598 foreach (fn; invalid)
|
|
3599 assert(!isValidFilename(to!T(fn)));
|
|
3600 }
|
|
3601
|
|
3602 {
|
|
3603 auto r = MockRange!(immutable(char))(`dir/file.d`);
|
|
3604 assert(!isValidFilename(r));
|
|
3605 }
|
|
3606
|
|
3607 static struct DirEntry { string s; alias s this; }
|
|
3608 assert(isValidFilename(DirEntry("file.ext")));
|
|
3609
|
|
3610 version (Windows)
|
|
3611 {
|
|
3612 immutable string cases = "<>:\"/\\|?*";
|
|
3613 foreach (i; 0 .. 31 + cases.length)
|
|
3614 {
|
|
3615 char[3] buf;
|
|
3616 buf[0] = 'a';
|
|
3617 buf[1] = i <= 31 ? cast(char) i : cases[i - 32];
|
|
3618 buf[2] = 'b';
|
|
3619 assert(!isValidFilename(buf[]));
|
|
3620 }
|
|
3621 }
|
|
3622 }
|
|
3623
|
|
3624
|
|
3625
|
|
3626 /** Checks whether $(D path) is a valid _path.
|
|
3627
|
|
3628 Generally, this function checks that $(D path) is not empty, and that
|
|
3629 each component of the path either satisfies $(LREF isValidFilename)
|
|
3630 or is equal to $(D ".") or $(D "..").
|
|
3631
|
|
3632 $(B It does $(I not) check whether the _path points to an existing file
|
|
3633 or directory; use $(REF exists, std,file) for this purpose.)
|
|
3634
|
|
3635 On Windows, some special rules apply:
|
|
3636 $(UL
|
|
3637 $(LI If the second character of $(D path) is a colon ($(D ':')),
|
|
3638 the first character is interpreted as a drive letter, and
|
|
3639 must be in the range A-Z (case insensitive).)
|
|
3640 $(LI If $(D path) is on the form $(D `\\$(I server)\$(I share)\...`)
|
|
3641 (UNC path), $(LREF isValidFilename) is applied to $(I server)
|
|
3642 and $(I share) as well.)
|
|
3643 $(LI If $(D path) starts with $(D `\\?\`) (long UNC path), the
|
|
3644 only requirement for the rest of the string is that it does
|
|
3645 not contain the null character.)
|
|
3646 $(LI If $(D path) starts with $(D `\\.\`) (Win32 device namespace)
|
|
3647 this function returns $(D false); such paths are beyond the scope
|
|
3648 of this module.)
|
|
3649 )
|
|
3650
|
|
3651 Params:
|
|
3652 path = string or Range of characters to check
|
|
3653
|
|
3654 Returns:
|
|
3655 true if $(D path) is a valid _path.
|
|
3656 */
|
|
3657 bool isValidPath(Range)(Range path)
|
|
3658 if ((isRandomAccessRange!Range && hasLength!Range && hasSlicing!Range && isSomeChar!(ElementEncodingType!Range) ||
|
|
3659 isNarrowString!Range) &&
|
|
3660 !isConvertibleToString!Range)
|
|
3661 {
|
|
3662 alias C = Unqual!(ElementEncodingType!Range);
|
|
3663
|
|
3664 if (path.empty) return false;
|
|
3665
|
|
3666 // Check whether component is "." or "..", or whether it satisfies
|
|
3667 // isValidFilename.
|
|
3668 bool isValidComponent(Range component)
|
|
3669 {
|
|
3670 assert(component.length > 0);
|
|
3671 if (component[0] == '.')
|
|
3672 {
|
|
3673 if (component.length == 1) return true;
|
|
3674 else if (component.length == 2 && component[1] == '.') return true;
|
|
3675 }
|
|
3676 return isValidFilename(component);
|
|
3677 }
|
|
3678
|
|
3679 if (path.length == 1)
|
|
3680 return isDirSeparator(path[0]) || isValidComponent(path);
|
|
3681
|
|
3682 Range remainder;
|
|
3683 version (Windows)
|
|
3684 {
|
|
3685 if (isDirSeparator(path[0]) && isDirSeparator(path[1]))
|
|
3686 {
|
|
3687 // Some kind of UNC path
|
|
3688 if (path.length < 5)
|
|
3689 {
|
|
3690 // All valid UNC paths must have at least 5 characters
|
|
3691 return false;
|
|
3692 }
|
|
3693 else if (path[2] == '?')
|
|
3694 {
|
|
3695 // Long UNC path
|
|
3696 if (!isDirSeparator(path[3])) return false;
|
|
3697 foreach (c; path[4 .. $])
|
|
3698 {
|
|
3699 if (c == '\0') return false;
|
|
3700 }
|
|
3701 return true;
|
|
3702 }
|
|
3703 else if (path[2] == '.')
|
|
3704 {
|
|
3705 // Win32 device namespace not supported
|
|
3706 return false;
|
|
3707 }
|
|
3708 else
|
|
3709 {
|
|
3710 // Normal UNC path, i.e. \\server\share\...
|
|
3711 size_t i = 2;
|
|
3712 while (i < path.length && !isDirSeparator(path[i])) ++i;
|
|
3713 if (i == path.length || !isValidFilename(path[2 .. i]))
|
|
3714 return false;
|
|
3715 ++i; // Skip a single dir separator
|
|
3716 size_t j = i;
|
|
3717 while (j < path.length && !isDirSeparator(path[j])) ++j;
|
|
3718 if (!isValidFilename(path[i .. j])) return false;
|
|
3719 remainder = path[j .. $];
|
|
3720 }
|
|
3721 }
|
|
3722 else if (isDriveSeparator(path[1]))
|
|
3723 {
|
|
3724 import std.ascii : isAlpha;
|
|
3725 if (!isAlpha(path[0])) return false;
|
|
3726 remainder = path[2 .. $];
|
|
3727 }
|
|
3728 else
|
|
3729 {
|
|
3730 remainder = path;
|
|
3731 }
|
|
3732 }
|
|
3733 else version (Posix)
|
|
3734 {
|
|
3735 remainder = path;
|
|
3736 }
|
|
3737 else static assert(0);
|
|
3738 remainder = ltrimDirSeparators(remainder);
|
|
3739
|
|
3740 // Check that each component satisfies isValidComponent.
|
|
3741 while (!remainder.empty)
|
|
3742 {
|
|
3743 size_t i = 0;
|
|
3744 while (i < remainder.length && !isDirSeparator(remainder[i])) ++i;
|
|
3745 assert(i > 0);
|
|
3746 if (!isValidComponent(remainder[0 .. i])) return false;
|
|
3747 remainder = ltrimDirSeparators(remainder[i .. $]);
|
|
3748 }
|
|
3749
|
|
3750 // All criteria passed
|
|
3751 return true;
|
|
3752 }
|
|
3753
|
|
3754 ///
|
|
3755 @safe pure @nogc nothrow
|
|
3756 unittest
|
|
3757 {
|
|
3758 assert(isValidPath("/foo/bar"));
|
|
3759 assert(!isValidPath("/foo\0/bar"));
|
|
3760 assert(isValidPath("/"));
|
|
3761 assert(isValidPath("a"));
|
|
3762
|
|
3763 version (Windows)
|
|
3764 {
|
|
3765 assert(isValidPath(`c:\`));
|
|
3766 assert(isValidPath(`c:\foo`));
|
|
3767 assert(isValidPath(`c:\foo\.\bar\\\..\`));
|
|
3768 assert(!isValidPath(`!:\foo`));
|
|
3769 assert(!isValidPath(`c::\foo`));
|
|
3770 assert(!isValidPath(`c:\foo?`));
|
|
3771 assert(!isValidPath(`c:\foo.`));
|
|
3772
|
|
3773 assert(isValidPath(`\\server\share`));
|
|
3774 assert(isValidPath(`\\server\share\foo`));
|
|
3775 assert(isValidPath(`\\server\share\\foo`));
|
|
3776 assert(!isValidPath(`\\\server\share\foo`));
|
|
3777 assert(!isValidPath(`\\server\\share\foo`));
|
|
3778 assert(!isValidPath(`\\ser*er\share\foo`));
|
|
3779 assert(!isValidPath(`\\server\sha?e\foo`));
|
|
3780 assert(!isValidPath(`\\server\share\|oo`));
|
|
3781
|
|
3782 assert(isValidPath(`\\?\<>:"?*|/\..\.`));
|
|
3783 assert(!isValidPath("\\\\?\\foo\0bar"));
|
|
3784
|
|
3785 assert(!isValidPath(`\\.\PhysicalDisk1`));
|
|
3786 assert(!isValidPath(`\\`));
|
|
3787 }
|
|
3788
|
|
3789 import std.utf : byCodeUnit;
|
|
3790 assert(isValidPath("/foo/bar".byCodeUnit));
|
|
3791 }
|
|
3792
|
|
3793 bool isValidPath(Range)(auto ref Range path)
|
|
3794 if (isConvertibleToString!Range)
|
|
3795 {
|
|
3796 return isValidPath!(StringTypeOf!Range)(path);
|
|
3797 }
|
|
3798
|
|
3799 @safe unittest
|
|
3800 {
|
|
3801 assert(testAliasedString!isValidPath("/foo/bar"));
|
|
3802 }
|
|
3803
|
|
3804 /** Performs tilde expansion in paths on POSIX systems.
|
|
3805 On Windows, this function does nothing.
|
|
3806
|
|
3807 There are two ways of using tilde expansion in a path. One
|
|
3808 involves using the tilde alone or followed by a path separator. In
|
|
3809 this case, the tilde will be expanded with the value of the
|
|
3810 environment variable $(D HOME). The second way is putting
|
|
3811 a username after the tilde (i.e. $(D ~john/Mail)). Here,
|
|
3812 the username will be searched for in the user database
|
|
3813 (i.e. $(D /etc/passwd) on Unix systems) and will expand to
|
|
3814 whatever path is stored there. The username is considered the
|
|
3815 string after the tilde ending at the first instance of a path
|
|
3816 separator.
|
|
3817
|
|
3818 Note that using the $(D ~user) syntax may give different
|
|
3819 values from just $(D ~) if the environment variable doesn't
|
|
3820 match the value stored in the user database.
|
|
3821
|
|
3822 When the environment variable version is used, the path won't
|
|
3823 be modified if the environment variable doesn't exist or it
|
|
3824 is empty. When the database version is used, the path won't be
|
|
3825 modified if the user doesn't exist in the database or there is
|
|
3826 not enough memory to perform the query.
|
|
3827
|
|
3828 This function performs several memory allocations.
|
|
3829
|
|
3830 Params:
|
|
3831 inputPath = The path name to expand.
|
|
3832
|
|
3833 Returns:
|
|
3834 $(D inputPath) with the tilde expanded, or just $(D inputPath)
|
|
3835 if it could not be expanded.
|
|
3836 For Windows, $(D expandTilde) merely returns its argument $(D inputPath).
|
|
3837
|
|
3838 Example:
|
|
3839 -----
|
|
3840 void processFile(string path)
|
|
3841 {
|
|
3842 // Allow calling this function with paths such as ~/foo
|
|
3843 auto fullPath = expandTilde(path);
|
|
3844 ...
|
|
3845 }
|
|
3846 -----
|
|
3847 */
|
|
3848 string expandTilde(string inputPath) nothrow
|
|
3849 {
|
|
3850 version (Posix)
|
|
3851 {
|
|
3852 import core.exception : onOutOfMemoryError;
|
|
3853 import core.stdc.errno : errno, ERANGE;
|
|
3854 import core.stdc.stdlib : malloc, free, realloc;
|
|
3855
|
|
3856 /* Joins a path from a C string to the remainder of path.
|
|
3857
|
|
3858 The last path separator from c_path is discarded. The result
|
|
3859 is joined to path[char_pos .. length] if char_pos is smaller
|
|
3860 than length, otherwise path is not appended to c_path.
|
|
3861 */
|
|
3862 static string combineCPathWithDPath(char* c_path, string path, size_t char_pos) nothrow
|
|
3863 {
|
|
3864 import core.stdc.string : strlen;
|
|
3865
|
|
3866 assert(c_path != null);
|
|
3867 assert(path.length > 0);
|
|
3868 assert(char_pos >= 0);
|
|
3869
|
|
3870 // Search end of C string
|
|
3871 size_t end = strlen(c_path);
|
|
3872
|
|
3873 // Remove trailing path separator, if any
|
|
3874 if (end && isDirSeparator(c_path[end - 1]))
|
|
3875 end--;
|
|
3876
|
|
3877 // (this is the only GC allocation done in expandTilde())
|
|
3878 string cp;
|
|
3879 if (char_pos < path.length)
|
|
3880 // Append something from path
|
|
3881 cp = cast(string)(c_path[0 .. end] ~ path[char_pos .. $]);
|
|
3882 else
|
|
3883 // Create our own copy, as lifetime of c_path is undocumented
|
|
3884 cp = c_path[0 .. end].idup;
|
|
3885
|
|
3886 return cp;
|
|
3887 }
|
|
3888
|
|
3889 // Replaces the tilde from path with the environment variable HOME.
|
|
3890 static string expandFromEnvironment(string path) nothrow
|
|
3891 {
|
|
3892 import core.stdc.stdlib : getenv;
|
|
3893
|
|
3894 assert(path.length >= 1);
|
|
3895 assert(path[0] == '~');
|
|
3896
|
|
3897 // Get HOME and use that to replace the tilde.
|
|
3898 auto home = getenv("HOME");
|
|
3899 if (home == null)
|
|
3900 return path;
|
|
3901
|
|
3902 return combineCPathWithDPath(home, path, 1);
|
|
3903 }
|
|
3904
|
|
3905 // Replaces the tilde from path with the path from the user database.
|
|
3906 static string expandFromDatabase(string path) nothrow
|
|
3907 {
|
|
3908 // bionic doesn't really support this, as getpwnam_r
|
|
3909 // isn't provided and getpwnam is basically just a stub
|
|
3910 version (CRuntime_Bionic)
|
|
3911 {
|
|
3912 return path;
|
|
3913 }
|
|
3914 else
|
|
3915 {
|
|
3916 import core.sys.posix.pwd : passwd, getpwnam_r;
|
|
3917 import std.string : indexOf;
|
|
3918
|
|
3919 assert(path.length > 2 || (path.length == 2 && !isDirSeparator(path[1])));
|
|
3920 assert(path[0] == '~');
|
|
3921
|
|
3922 // Extract username, searching for path separator.
|
|
3923 auto last_char = indexOf(path, dirSeparator[0]);
|
|
3924
|
|
3925 size_t username_len = (last_char == -1) ? path.length : last_char;
|
|
3926 char* username = cast(char*) malloc(username_len * char.sizeof);
|
|
3927 if (!username)
|
|
3928 onOutOfMemoryError();
|
|
3929 scope(exit) free(username);
|
|
3930
|
|
3931 if (last_char == -1)
|
|
3932 {
|
|
3933 username[0 .. username_len - 1] = path[1 .. $];
|
|
3934 last_char = path.length + 1;
|
|
3935 }
|
|
3936 else
|
|
3937 {
|
|
3938 username[0 .. username_len - 1] = path[1 .. last_char];
|
|
3939 }
|
|
3940 username[username_len - 1] = 0;
|
|
3941
|
|
3942 assert(last_char > 1);
|
|
3943
|
|
3944 // Reserve C memory for the getpwnam_r() function.
|
|
3945 version (unittest)
|
|
3946 uint extra_memory_size = 2;
|
|
3947 else
|
|
3948 uint extra_memory_size = 5 * 1024;
|
|
3949 char* extra_memory;
|
|
3950 scope(exit) free(extra_memory);
|
|
3951
|
|
3952 passwd result;
|
|
3953 while (1)
|
|
3954 {
|
|
3955 extra_memory = cast(char*) realloc(extra_memory, extra_memory_size * char.sizeof);
|
|
3956 if (extra_memory == null)
|
|
3957 onOutOfMemoryError();
|
|
3958
|
|
3959 // Obtain info from database.
|
|
3960 passwd *verify;
|
|
3961 errno = 0;
|
|
3962 if (getpwnam_r(username, &result, extra_memory, extra_memory_size,
|
|
3963 &verify) == 0)
|
|
3964 {
|
|
3965 // Succeeded if verify points at result
|
|
3966 if (verify == &result)
|
|
3967 // username is found
|
|
3968 path = combineCPathWithDPath(result.pw_dir, path, last_char);
|
|
3969 break;
|
|
3970 }
|
|
3971
|
|
3972 if (errno != ERANGE &&
|
|
3973 // On BSD and OSX, errno can be left at 0 instead of set to ERANGE
|
|
3974 errno != 0)
|
|
3975 onOutOfMemoryError();
|
|
3976
|
|
3977 // extra_memory isn't large enough
|
|
3978 import core.checkedint : mulu;
|
|
3979 bool overflow;
|
|
3980 extra_memory_size = mulu(extra_memory_size, 2, overflow);
|
|
3981 if (overflow) assert(0);
|
|
3982 }
|
|
3983 return path;
|
|
3984 }
|
|
3985 }
|
|
3986
|
|
3987 // Return early if there is no tilde in path.
|
|
3988 if (inputPath.length < 1 || inputPath[0] != '~')
|
|
3989 return inputPath;
|
|
3990
|
|
3991 if (inputPath.length == 1 || isDirSeparator(inputPath[1]))
|
|
3992 return expandFromEnvironment(inputPath);
|
|
3993 else
|
|
3994 return expandFromDatabase(inputPath);
|
|
3995 }
|
|
3996 else version (Windows)
|
|
3997 {
|
|
3998 // Put here real windows implementation.
|
|
3999 return inputPath;
|
|
4000 }
|
|
4001 else
|
|
4002 {
|
|
4003 static assert(0); // Guard. Implement on other platforms.
|
|
4004 }
|
|
4005 }
|
|
4006
|
|
4007
|
|
4008 version (unittest) import std.process : environment;
|
|
4009 @system unittest
|
|
4010 {
|
|
4011 version (Posix)
|
|
4012 {
|
|
4013 // Retrieve the current home variable.
|
|
4014 auto oldHome = environment.get("HOME");
|
|
4015
|
|
4016 // Testing when there is no environment variable.
|
|
4017 environment.remove("HOME");
|
|
4018 assert(expandTilde("~/") == "~/");
|
|
4019 assert(expandTilde("~") == "~");
|
|
4020
|
|
4021 // Testing when an environment variable is set.
|
|
4022 environment["HOME"] = "dmd/test";
|
|
4023 assert(expandTilde("~/") == "dmd/test/");
|
|
4024 assert(expandTilde("~") == "dmd/test");
|
|
4025
|
|
4026 // The same, but with a variable ending in a slash.
|
|
4027 environment["HOME"] = "dmd/test/";
|
|
4028 assert(expandTilde("~/") == "dmd/test/");
|
|
4029 assert(expandTilde("~") == "dmd/test");
|
|
4030
|
|
4031 // Recover original HOME variable before continuing.
|
|
4032 if (oldHome !is null) environment["HOME"] = oldHome;
|
|
4033 else environment.remove("HOME");
|
|
4034
|
|
4035 // Test user expansion for root, no /root on Android
|
|
4036 version (OSX)
|
|
4037 {
|
|
4038 assert(expandTilde("~root") == "/var/root", expandTilde("~root"));
|
|
4039 assert(expandTilde("~root/") == "/var/root/", expandTilde("~root/"));
|
|
4040 }
|
|
4041 else version (Android)
|
|
4042 {
|
|
4043 }
|
|
4044 else
|
|
4045 {
|
|
4046 assert(expandTilde("~root") == "/root", expandTilde("~root"));
|
|
4047 assert(expandTilde("~root/") == "/root/", expandTilde("~root/"));
|
|
4048 }
|
|
4049 assert(expandTilde("~Idontexist/hey") == "~Idontexist/hey");
|
|
4050 }
|
|
4051 }
|
|
4052
|
|
4053 version (unittest)
|
|
4054 {
|
|
4055 /* Define a mock RandomAccessRange to use for unittesting.
|
|
4056 */
|
|
4057
|
|
4058 struct MockRange(C)
|
|
4059 {
|
|
4060 this(C[] array) { this.array = array; }
|
|
4061 const
|
|
4062 {
|
|
4063 @property size_t length() { return array.length; }
|
|
4064 @property bool empty() { return array.length == 0; }
|
|
4065 @property C front() { return array[0]; }
|
|
4066 @property C back() { return array[$ - 1]; }
|
|
4067 @property size_t opDollar() { return length; }
|
|
4068 C opIndex(size_t i) { return array[i]; }
|
|
4069 }
|
|
4070 void popFront() { array = array[1 .. $]; }
|
|
4071 void popBack() { array = array[0 .. $-1]; }
|
|
4072 MockRange!C opSlice( size_t lwr, size_t upr) const
|
|
4073 {
|
|
4074 return MockRange!C(array[lwr .. upr]);
|
|
4075 }
|
|
4076 @property MockRange save() { return this; }
|
|
4077 private:
|
|
4078 C[] array;
|
|
4079 }
|
|
4080
|
|
4081 static assert( isRandomAccessRange!(MockRange!(const(char))) );
|
|
4082 }
|
|
4083
|
|
4084 version (unittest)
|
|
4085 {
|
|
4086 /* Define a mock BidirectionalRange to use for unittesting.
|
|
4087 */
|
|
4088
|
|
4089 struct MockBiRange(C)
|
|
4090 {
|
|
4091 this(const(C)[] array) { this.array = array; }
|
|
4092 const
|
|
4093 {
|
|
4094 @property bool empty() { return array.length == 0; }
|
|
4095 @property C front() { return array[0]; }
|
|
4096 @property C back() { return array[$ - 1]; }
|
|
4097 @property size_t opDollar() { return array.length; }
|
|
4098 }
|
|
4099 void popFront() { array = array[1 .. $]; }
|
|
4100 void popBack() { array = array[0 .. $-1]; }
|
|
4101 @property MockBiRange save() { return this; }
|
|
4102 private:
|
|
4103 const(C)[] array;
|
|
4104 }
|
|
4105
|
|
4106 static assert( isBidirectionalRange!(MockBiRange!(const(char))) );
|
|
4107 }
|
|
4108
|
|
4109 private template BaseOf(R)
|
|
4110 {
|
|
4111 static if (isRandomAccessRange!R && isSomeChar!(ElementType!R))
|
|
4112 alias BaseOf = R;
|
|
4113 else
|
|
4114 alias BaseOf = StringTypeOf!R;
|
|
4115 }
|