1 /++
2   Authors:
3     Manuzor
4 
5   ToDo:
6     - Path().open(...)
7 +/
8 module pathlib;
9 
10 public import std.file : SpanMode;
11 
12 static import std.path;
13 static import std.file;
14 static import std.uni;
15 import std.algorithm;
16 import std.array      : array, replace, replaceLast;
17 import std.format     : format;
18 import std..string     : split, indexOf, empty, squeeze, removechars, lastIndexOf;
19 import std.conv       : to;
20 import std.typecons   : Flag;
21 import std.traits     : isSomeString, isSomeChar;
22 import std.range      : iota, take, isInputRange;
23 import std.typetuple;
24 
25 debug
26 {
27   void logDebug(T...)(T args) {
28     import std.stdio;
29 
30     writefln(args);
31   }
32 }
33 
34 void assertEqual(alias predicate = "a == b", A, B, StringType = string)
35                 (A a, B b, StringType fmt = "`%s` must be equal to `%s`")
36 {
37   static if(isInputRange!A && isInputRange!B) {
38     assert(std.algorithm.equal!predicate(a, b), format(fmt, a, b));
39   }
40   else {
41     import std.functional : binaryFun;
42     assert(binaryFun!predicate(a, b), format(fmt, a, b));
43   }
44 }
45 
46 void assertNotEqual(alias predicate = "a != b", A, B, StringType = string)
47                     (A a, B b, StringType fmt = "`%s` must not be equal to `%s`")
48 {
49   assertEqual!predicate(a, b, fmt);
50 }
51 
52 void assertEmpty(A, StringType = string)(A a, StringType fmt = "String `%s` should be empty.") {
53   assert(a.empty, format(fmt, a));
54 }
55 
56 void assertNotEmpty(A, StringType = string)(A a, StringType fmt = "String `%s` should be empty.") {
57   assert(!a.empty, format(fmt, a));
58 }
59 
60 template isSomePath(PathType)
61 {
62   enum isSomePath = is(PathType == WindowsPath) ||
63                     is(PathType == PosixPath);
64 }
65 
66 ///
67 unittest
68 {
69   static assert(isSomePath!WindowsPath);
70   static assert(isSomePath!PosixPath);
71   static assert(!isSomePath!string);
72 }
73 
74 
75 /// Exception that will be thrown when any path operations fail.
76 class PathException : Exception
77 {
78   @safe pure nothrow
79   this(string msg)
80   {
81     super(msg);
82   }
83 }
84 
85 
86 mixin template PathCommon(TheStringType, alias theSeparator, alias theCaseSensitivity)
87   if(isSomeString!TheStringType && isSomeString!(typeof(theSeparator)))
88 {
89   alias PathType = typeof(this);
90   alias StringType = TheStringType;
91 
92   StringType data = ".";
93 
94   ///
95   unittest
96   {
97     assert(PathType().data != "");
98     assert(PathType("123").data == "123");
99     assert(PathType("C:///toomany//slashes!\\").data == "C:///toomany//slashes!\\");
100   }
101 
102 
103   /// Used to separate each path segment.
104   alias separator = theSeparator;
105 
106 
107   /// A value of std.path.CaseSensitive whether this type of path is case sensetive or not.
108   alias caseSensitivity = theCaseSensitivity;
109 
110 
111   /// Concatenate a path and a string, which will be treated as a path.
112   auto opBinary(string op : "~", InStringType)(auto ref in InStringType str) const
113     if(isSomeString!InStringType)
114   {
115     return this ~ PathType(str);
116   }
117 
118   /// Concatenate two paths.
119   auto opBinary(string op : "~")(auto ref in PathType other) const
120     if(isSomePath!PathType)
121   {
122     auto p = PathType(this.data);
123     p ~= other;
124     return p;
125   }
126 
127   /// Concatenate the path-string $(D str) to this path.
128   void opOpAssign(string op : "~", InStringType)(auto ref in InStringType str)
129     if(isSomeString!InStringType)
130   {
131     this ~= PathType(str);
132   }
133 
134   /// Concatenate the path $(D other) to this path.
135   void opOpAssign(string op : "~")(auto ref in PathType other)
136     if(isSomePath!PathType)
137   {
138     auto l = PathType(this.normalizedData);
139     auto r = PathType(other.normalizedData);
140 
141     //logDebug("~= %s: %s => %s | %s => %s", PathType.stringof, this.data, l, other.data, r);
142 
143     if(l.isDot || r.isAbsolute) {
144       this.data = r.data;
145       return;
146     }
147 
148     if(r.isDot) {
149       this.data = l.data;
150       return;
151     }
152 
153     auto sep = "";
154     if(!l.data.endsWith('/', '\\') && !r.data.startsWith('/', '\\')) {
155       sep = this.separator;
156     }
157 
158     this.data = l.data ~ sep ~ r.data;
159   }
160 
161   ///
162   unittest
163   {
164     assertEqual(PathType() ~ "hello", PathType("hello"));
165     assertEqual(PathType("") ~ "hello", PathType("hello"));
166     assertEqual(PathType(".") ~ "hello", PathType("hello"));
167 
168     assertEqual(WindowsPath("/") ~ "hello", WindowsPath(`hello`));
169     assertEqual(WindowsPath(`C:\`) ~ "hello" ~ "world", WindowsPath(`C:\hello\world`));
170     assertEqual(WindowsPath("hello") ~ "..", WindowsPath(`hello\..`));
171     assertEqual(WindowsPath("..") ~ "hello", WindowsPath(`..\hello`));
172 
173     assertEqual(PosixPath("/") ~ "hello", PosixPath("/hello"));
174     assertEqual(PosixPath("/") ~ "hello" ~ "world", PosixPath("/hello/world"));
175     assertEqual(PosixPath("hello") ~ "..", PosixPath("hello/.."));
176     assertEqual(PosixPath("..") ~ "hello", PosixPath("../hello"));
177   }
178 
179   /// Equality overload.
180   bool opEquals()(auto ref in PathType other) const
181   {
182     static if(theCaseSensitivity == std.path.CaseSensitive.no) {
183       return std.uni.sicmp(this.normalizedData, other.normalizedData) == 0;
184     }
185     else {
186       return std.algorithm.cmp(this.normalizedData, other.normalizedData) == 0;
187     }
188   }
189 
190   int opCmp(ref const PathType other) const
191   {
192     static if(theCaseSensitivity == std.path.CaseSensitive.no) {
193       return std.uni.sicmp(this.normalizedData, other.normalizedData);
194     }
195     else {
196       return std.algorithm.cmp(this.normalizedData, other.normalizedData);
197     }
198   }
199 
200   ///
201   unittest
202   {
203     assertEqual(PathType(""), PathType(""));
204     assertEqual(PathType("."), PathType(""));
205     assertEqual(PathType(""), PathType("."));
206     auto p1 = PathType("/hello/world");
207     auto p2 = PathType("/hello/world");
208     assertEqual(p1, p2);
209     static if(is(PathType == WindowsPath))
210     {
211       auto p3 = PathType("/hello/world");
212     }
213     else static if(is(PathType == PosixPath))
214     {
215       auto p3 = PathType("/hello\\world");
216     }
217     auto p4 = PathType("/hello\\world");
218     assertEqual(p3, p4);
219   }
220 
221 
222   /// Cast the path to a string.
223   auto toString(OtherStringType = StringType)() const {
224     return this.data.to!OtherStringType;
225   }
226 
227   ///
228   unittest
229   {
230     assertEqual(PathType("C:/hello/world.exe.xml").to!StringType, "C:/hello/world.exe.xml");
231   }
232 }
233 
234 
235 struct WindowsPath
236 {
237   mixin PathCommon!(string, `\`, std.path.CaseSensitive.no);
238 
239   version(Windows)
240   {
241     /// Overload conversion `to` for Path => std.file.DirEntry.
242     auto opCast(To : std.file.DirEntry)() const
243     {
244       return To(this.normalizedData);
245     }
246 
247     ///
248     unittest
249     {
250       auto d = cwd().to!(std.file.DirEntry);
251     }
252   }
253 }
254 
255 
256 struct PosixPath
257 {
258   mixin PathCommon!(string, "/", std.path.CaseSensitive.yes);
259 
260   version(Posix)
261   {
262     /// Overload conversion `to` for Path => std.file.DirEntry.
263     auto opCast(To : std.file.DirEntry)() const
264     {
265       return To(this.normalizedData);
266     }
267 
268     ///
269     unittest
270     {
271       auto d = cwd().to!(std.file.DirEntry);
272     }
273   }
274 }
275 
276 /// Set the default path depending on the current platform.
277 version(Windows)
278 {
279   alias Path = WindowsPath;
280 }
281 else // Assume posix on non-windows platforms.
282 {
283   alias Path = PosixPath;
284 }
285 
286 
287 /// Helper struct to change the directory for the current scope.
288 struct ScopedChdir
289 {
290   Path prevDir;
291 
292   this(in Path newDir)
293   {
294     this.prevDir = cwd();
295     if(newDir.isAbsolute) {
296       chdir(newDir);
297     }
298     else {
299       chdir(cwd() ~ newDir);
300     }
301   }
302 
303   /// Convenience overload that takes a string.
304   this(string newDir) {
305     this(Path(newDir));
306   }
307 
308   ~this() {
309     chdir(this.prevDir);
310   }
311 }
312 
313 ///
314 unittest
315 {
316   auto orig = cwd();
317   with(ScopedChdir("..")) {
318     assertNotEqual(orig, cwd());
319   }
320   assertEqual(orig, cwd());
321 }
322 
323 
324 /// Whether $(D str) can be represented by ".".
325 bool isDot(StringType)(auto ref in StringType str)
326   if(isSomeString!StringType)
327 {
328   if(str.length > 0 && str[0] != '.') {
329     return false;
330   }
331   if(str.startsWith("..")) {
332     return false;
333   }
334   auto data = str.removechars("./\\");
335   return data.empty;
336 }
337 
338 ///
339 unittest
340 {
341   assert("".isDot);
342   assert(".".isDot);
343   assert("./".isDot);
344   assert("./.".isDot);
345   assert("././".isDot);
346   assert(!"/".isDot);
347   assert(!"hello".isDot);
348   assert(!".git".isDot);
349 }
350 
351 
352 /// Whether the path is either "" or ".".
353 auto isDot(PathType)(auto ref in PathType p)
354   if(isSomePath!PathType)
355 {
356   return p.data.isDot;
357 }
358 
359 ///
360 unittest
361 {
362   assert(WindowsPath().isDot);
363   assert(WindowsPath("").isDot);
364   assert(WindowsPath(".").isDot);
365   assert(!WindowsPath("/").isDot);
366   assert(!WindowsPath("hello").isDot);
367   assert(PosixPath().isDot);
368   assert(PosixPath("").isDot);
369   assert(PosixPath(".").isDot);
370   assert(!PosixPath("/").isDot);
371   assert(!PosixPath("hello").isDot);
372 }
373 
374 
375 /// Returns: The root of the path $(D p).
376 auto root(PathType)(auto ref in PathType p)
377   if(isSomePath!PathType)
378 {
379   static if(is(PathType == WindowsPath))
380   {
381     return p.drive;
382   }
383   else // Assume PosixPath
384   {
385     auto data = p.data;
386     if(data.length > 0 && data[0] == '/') {
387       return data[0..1];
388     }
389 
390     return "";
391   }
392 }
393 
394 ///
395 unittest
396 {
397   assertEqual(WindowsPath("").root, "");
398   assertEqual(PosixPath("").root, "");
399   assertEqual(WindowsPath("C:/Hello/World").root, "C:");
400   assertEqual(WindowsPath("/Hello/World").root, "");
401   assertEqual(PosixPath("/hello/world").root, "/");
402   assertEqual(PosixPath("C:/hello/world").root, "");
403 }
404 
405 
406 /// The drive of the path $(D p).
407 /// Note: Non-Windows platforms have no concept of "drives".
408 auto drive(PathType)(auto ref in PathType p)
409   if(isSomePath!PathType)
410 {
411   static if(is(PathType == WindowsPath))
412   {
413     auto data = p.data;
414     if(data.length > 1 && data[1] == ':') {
415       return data[0..2];
416     }
417   }
418 
419   return "";
420 }
421 
422 ///
423 unittest
424 {
425   assertEqual(WindowsPath("").drive, "");
426   assertEqual(WindowsPath("/Hello/World").drive, "");
427   assertEqual(WindowsPath("C:/Hello/World").drive, "C:");
428   assertEqual(PosixPath("").drive, "");
429   assertEqual(PosixPath("/Hello/World").drive, "");
430   assertEqual(PosixPath("C:/Hello/World").drive, "");
431 }
432 
433 
434 /// Returns: The path data using forward slashes, regardless of the current platform.
435 auto posixData(PathType)(auto ref in PathType p)
436   if(isSomePath!PathType)
437 {
438   return normalizedDataImpl!"posix"(p);
439 }
440 
441 ///
442 unittest
443 {
444   assertEqual(WindowsPath().posixData, ".");
445   assertEqual(WindowsPath(``).posixData, ".");
446   assertEqual(WindowsPath(`.`).posixData, ".");
447   assertEqual(WindowsPath(`..`).posixData, "..");
448   assertEqual(WindowsPath(`/foo/bar`).posixData, `foo/bar`);
449   assertEqual(WindowsPath(`/foo/bar/`).posixData, `foo/bar`);
450   assertEqual(WindowsPath(`C:\foo/bar.exe`).posixData, `C:/foo/bar.exe`);
451   assertEqual(WindowsPath(`./foo\/../\/bar/.//\/baz.exe`).posixData, `foo/../bar/baz.exe`);
452 
453   assertEqual(PosixPath().posixData, `.`);
454   assertEqual(PosixPath(``).posixData, `.`);
455   assertEqual(PosixPath(`.`).posixData, `.`);
456   assertEqual(PosixPath(`..`).posixData, `..`);
457   assertEqual(PosixPath(`/foo/bar`).posixData, `/foo/bar`);
458   assertEqual(PosixPath(`/foo/bar/`).posixData, `/foo/bar`);
459   assertEqual(PosixPath(`.//foo\ bar/.//./baz.txt`).posixData, `foo\ bar/baz.txt`);
460 }
461 
462 
463 /// Returns: The path data using backward slashes, regardless of the current platform.
464 auto windowsData(PathType)(auto ref in PathType p)
465   if(isSomePath!PathType)
466 {
467   return normalizedDataImpl!"windows"(p);
468 }
469 
470 ///
471 unittest
472 {
473   assertEqual(WindowsPath().windowsData, `.`);
474   assertEqual(WindowsPath(``).windowsData, `.`);
475   assertEqual(WindowsPath(`.`).windowsData, `.`);
476   assertEqual(WindowsPath(`..`).windowsData, `..`);
477   assertEqual(WindowsPath(`C:\foo\bar.exe`).windowsData, `C:\foo\bar.exe`);
478   assertEqual(WindowsPath(`C:\foo\bar\`).windowsData, `C:\foo\bar`);
479   assertEqual(WindowsPath(`C:\./foo\.\\.\\\bar\`).windowsData, `C:\foo\bar`);
480   assertEqual(WindowsPath(`C:\./foo\..\\.\\\bar\//baz.exe`).windowsData, `C:\foo\..\bar\baz.exe`);
481 
482   assertEqual(PosixPath().windowsData, `.`);
483   assertEqual(PosixPath(``).windowsData, `.`);
484   assertEqual(PosixPath(`.`).windowsData, `.`);
485   assertEqual(PosixPath(`..`).windowsData, `..`);
486   assertEqual(PosixPath(`/foo/bar.txt`).windowsData, `foo\bar.txt`);
487   assertEqual(PosixPath(`/foo/bar\`).windowsData, `foo\bar`);
488   assertEqual(PosixPath(`/foo/bar\`).windowsData, `foo\bar`);
489   assertEqual(PosixPath(`/./\\foo/\/.\..\bar/./baz.txt`).windowsData, `foo\..\bar\baz.txt`);
490 }
491 
492 
493 /// Remove duplicate directory separators and stand-alone dots, on a textual basis.
494 /// Does not resolve ".."s in paths, since this would be wrong when dealing with symlinks.
495 /// Given the symlink "/foo" pointing to "/what/ever", the path "/foo/../baz.exe" would 'physically' be "/what/baz.exe".
496 /// If "/foo" was no symlink, the actual path would be "/baz.exe".
497 ///
498 /// If you need to resolve ".."s and symlinks, see resolved().
499 auto normalizedData(PathType)(auto ref in PathType p)
500   if(allSatisfy!(isSomePath, PathType))
501 {
502   static if(is(PathType == WindowsPath))
503   {
504     return p.windowsData;
505   }
506   else static if(is(PathType == PosixPath))
507   {
508     return p.posixData;
509   }
510 }
511 
512 private auto normalizedDataImpl(alias target, PathType)(in PathType p)
513 {
514   static assert(target == "posix" || target == "windows");
515 
516   if(p.isDot) {
517     return ".";
518   }
519 
520   static if(target == "windows")
521   {
522     auto sep = WindowsPath.separator;
523   }
524   else static if(target == "posix")
525   {
526     auto sep = PosixPath.separator;
527   }
528 
529   // Note: We cannot make use of std.path.buildNormalizedPath because it textually resolves ".."s in paths.
530   static if(is(PathType == WindowsPath))
531   {
532     //logDebug("WindowsPath 1: %s", p.data);
533     //logDebug("WindowsPath 2: %s", p.data.replace(WindowsPath.separator, PosixPath.separator));
534     //logDebug("WindowsPath 3: %s", p.data.replace(WindowsPath.separator, PosixPath.separator)
535     //                                    .split(PosixPath.separator));
536     //logDebug("WindowsPath 4: %s", p.data.replace(WindowsPath.separator, PosixPath.separator)
537     //                                    .split(PosixPath.separator)
538     //                                    .filter!(a => !a.empty && !a.isDot));
539     //logDebug("WindowsPath 5: %s", p.data.replace(WindowsPath.separator, PosixPath.separator)
540     //                                    .split(PosixPath.separator)
541     //                                    .filter!(a => !a.empty && !a.isDot)
542     //                                    .joiner(sep));
543 
544     // For WindowsPaths, replace "\" with "/".
545     return p.data.replace(WindowsPath.separator, PosixPath.separator)
546                  .split(PosixPath.separator)
547                  .filter!(a => !a.empty && !a.isDot)
548                  .joiner(sep)
549                  .to!string();
550   }
551   else static if(is(PathType == PosixPath))
552   {
553     // For PosixPaths, do not replace any "\"s and make sure to append the root as prefix,
554     // since it would get `split` away.
555 
556     static if(target == "windows")
557     {
558       return WindowsPath(p.data).windowsData;
559     }
560     else static if(target == "posix")
561     {
562       auto root = p.root;
563 
564       //logDebug("PosixPath root: %s", root);
565       //logDebug("PosixPath 1: %s", root ~ p.data);
566       //logDebug("PosixPath 2: %s", root ~ p.data.split(PosixPath.separator));
567       //logDebug("PosixPath 3: %s", root ~ p.data.split(PosixPath.separator)
568       //                                           .filter!(a => !a.empty && !a.isDot)
569       //                                           .to!string);
570       //logDebug("PosixPath 4: %s", root ~ p.data.split(PosixPath.separator)
571       //                                           .filter!(a => !a.empty && !a.isDot)
572       //                                           .joiner(sep)
573       //                                           .to!string);
574 
575       return root ~ p.data.split(PosixPath.separator)
576                           .filter!(a => !a.empty && !a.isDot)
577                           .joiner(sep)
578                           .to!string();
579     }
580   }
581 }
582 
583 
584 auto asPosixPath(PathType)(auto ref in PathType p)
585   if(isSomePath!PathType)
586 {
587   return PathType(p.posixData);
588 }
589 
590 auto asWindowsPath(PathType)(auto ref in PathType p)
591   if(isSomePath!PathType)
592 {
593   return PathType(p.windowsData);
594 }
595 
596 auto asNormalizedPath(DestType = SrcType, SrcType)(auto ref in SrcType p)
597   if(isSomePath!DestType && isSomePath!SrcType)
598 {
599   return DestType(p.normalizedData);
600 }
601 
602 
603 /// Whether the path is absolute.
604 auto isAbsolute(PathType)(auto ref in PathType p)
605   if(isSomePath!PathType)
606 {
607   // If the path has a root, it is absolute.
608   return !p.root.empty;
609 }
610 
611 ///
612 unittest
613 {
614   assert(!WindowsPath("").isAbsolute);
615   assert(!WindowsPath("/Hello/World").isAbsolute);
616   assert(WindowsPath("C:/Hello/World").isAbsolute);
617   assert(!WindowsPath("foo/bar.exe").isAbsolute);
618   assert(!PosixPath("").isAbsolute);
619   assert(PosixPath("/Hello/World").isAbsolute);
620   assert(!PosixPath("C:/Hello/World").isAbsolute);
621   assert(!PosixPath("foo/bar.exe").isAbsolute);
622 }
623 
624 
625 auto absolute(PathType)(auto ref in PathType p, lazy PathType parent = cwd().asNormalizedPath!PathType())
626 {
627   if(p.isAbsolute) {
628     return p;
629   }
630 
631   return parent ~ p;
632 }
633 
634 /// TODO
635 unittest
636 {
637   assertEqual(WindowsPath("bar/baz.exe").absolute(WindowsPath("C:/foo")), WindowsPath("C:/foo/bar/baz.exe"));
638   assertEqual(WindowsPath("C:/foo/bar.exe").absolute(WindowsPath("C:/baz")), WindowsPath("C:/foo/bar.exe"));
639 }
640 
641 
642 /// Returns: The parts of the path as an array.
643 auto parts(PathType)(auto ref in PathType p)
644   if(isSomePath!PathType)
645 {
646   static if(is(PathType == WindowsPath))
647   {
648     auto prefix = "";
649   }
650   else static if(is(PathType == PosixPath))
651   {
652     auto prefix = p.root;
653   }
654   auto theSplit = p.normalizedData.split(PathType.separator);
655   return (prefix ~ theSplit).filter!(a => !a.empty);
656 }
657 
658 ///
659 unittest
660 {
661   assertEqual(Path().parts, ["."]);
662   assertEqual(Path("./.").parts, ["."]);
663 
664   assertEqual(WindowsPath("C:/hello/world").parts, ["C:", "hello", "world"]);
665   assertEqual(WindowsPath(`hello/.\world.exe.xml`).parts, ["hello", "world.exe.xml"]);
666   assertEqual(WindowsPath("C:/hello/world.exe.xml").parts, ["C:", "hello", "world.exe.xml"]);
667 
668   assertEqual(PosixPath("hello/world.exe.xml").parts, ["hello", "world.exe.xml"]);
669   assertEqual(PosixPath("/hello/.//world.exe.xml").parts, ["/", "hello", "world.exe.xml"]);
670   assertEqual(PosixPath("/hello\\ world.exe.xml").parts, ["/", "hello\\ world.exe.xml"]);
671 }
672 
673 
674 auto parent(PathType)(auto ref in PathType p)
675   if(isSomePath!PathType)
676 {
677   auto theParts = p.parts.map!(a => PathType(a)).array;
678   if(theParts.length > 1) {
679     return theParts[0 .. $ - 1].reduce!((a, b){ return a ~ b;});
680   }
681   return PathType();
682 }
683 
684 ///
685 unittest
686 {
687   assertEqual(Path().parent, Path());
688   assertEqual(Path("IHaveNoParents").parent, Path());
689   assertEqual(WindowsPath("C:/hello/world").parent, WindowsPath(`C:\hello`));
690   assertEqual(WindowsPath("C:/hello/world/").parent, WindowsPath(`C:\hello`));
691   assertEqual(WindowsPath("C:/hello/world.exe.foo").parent, WindowsPath(`C:\hello`));
692   assertEqual(PosixPath("/hello/world").parent, PosixPath("/hello"));
693   assertEqual(PosixPath("/hello/\\ world/").parent, PosixPath("/hello"));
694   assertEqual(PosixPath("/hello.foo.bar/world/").parent, PosixPath("/hello.foo.bar"));
695 }
696 
697 
698 /// /foo/bar/baz/hurr.durr => { Path("/foo/bar/baz"), Path("/foo/bar"), Path("/foo"), Path("/") }
699 auto parents(PathType)(auto ref in PathType p)
700   if(isSomePath!PathType)
701 {
702   auto theParts = p.parts.map!(x => PathType(x)).array;
703   return iota(theParts.length - 1, 0, -1).map!(x => theParts.take(x)
704                                                             .reduce!((a, b){ return a ~ b; }));
705 }
706 
707 ///
708 unittest
709 {
710   assertEmpty(WindowsPath().parents);
711   assertEmpty(WindowsPath(".").parents);
712   assertEmpty(WindowsPath("foo.txt").parents);
713   assertEqual(WindowsPath("C:/hello/world").parents, [ WindowsPath(`C:\hello`), WindowsPath(`C:\`) ]);
714   assertEqual(WindowsPath("C:/hello/world/").parents, [ WindowsPath(`C:\hello`), WindowsPath(`C:\`) ]);
715 
716   assertEmpty(PosixPath().parents);
717   assertEmpty(PosixPath(".").parents);
718   assertEmpty(PosixPath("foo.txt").parents);
719   assertEqual(PosixPath("/hello/world").parents, [ PosixPath("/hello"), PosixPath("/") ]);
720   assertEqual(PosixPath("/hello/world/").parents, [ PosixPath("/hello"), PosixPath("/") ]);
721 }
722 
723 
724 /// The name of the path without any of its parents.
725 auto name(PathType)(auto ref in PathType p)
726   if(isSomePath!PathType)
727 {
728   import std.algorithm : min;
729 
730   auto data = p.posixData;
731   auto i = min(data.lastIndexOf('/') + 1, data.length);
732   return data[i .. $];
733 }
734 
735 ///
736 unittest
737 {
738   assertEqual(WindowsPath().name, ".");
739   assertEqual(WindowsPath("").name, ".");
740   assertEmpty(WindowsPath("/").name);
741   assertEqual(WindowsPath("/hello").name, "hello");
742   assertEqual(WindowsPath("C:\\hello").name, "hello");
743   assertEqual(WindowsPath("C:/hello/world.exe").name, "world.exe");
744   assertEqual(WindowsPath("hello/world.foo.bar.exe").name, "world.foo.bar.exe");
745 
746   assertEqual(PosixPath().name, ".");
747   assertEqual(PosixPath("").name, ".");
748   assertEmpty(PosixPath("/").name, "/");
749   assertEqual(PosixPath("/hello").name, "hello");
750   assertEqual(PosixPath("C:\\hello").name, "C:\\hello");
751   assertEqual(PosixPath("/foo/bar\\ baz.txt").name, "bar\\ baz.txt");
752   assertEqual(PosixPath("C:/hello/world.exe").name, "world.exe");
753   assertEqual(PosixPath("hello/world.foo.bar.exe").name, "world.foo.bar.exe");
754 }
755 
756 
757 /// The extension of the path including the leading dot.
758 ///
759 /// Examples: The extension of "hello.foo.bar.exe" is ".exe".
760 auto extension(PathType)(auto ref in PathType p)
761   if(isSomePath!PathType)
762 {
763   auto data = p.name;
764   auto i = data.lastIndexOf('.');
765   if(i < 0) {
766     return "";
767   }
768   if(i + 1 == data.length) {
769     // This prevents preserving the dot in empty extensions such as `hello.foo.`.
770     ++i;
771   }
772   return data[i .. $];
773 }
774 
775 ///
776 unittest
777 {
778   assertEmpty(Path().extension);
779   assertEmpty(Path("").extension);
780   assertEmpty(Path("/").extension);
781   assertEmpty(Path("/hello").extension);
782   assertEmpty(Path("C:/hello/world").extension);
783   assertEqual(Path("C:/hello/world.exe").extension, ".exe");
784   assertEqual(Path("hello/world.foo.bar.exe").extension, ".exe");
785 }
786 
787 
788 /// All extensions of the path.
789 auto extensions(PathType)(auto ref in PathType p)
790   if(isSomePath!PathType)
791 {
792   import std.algorithm : splitter, filter;
793   import std.range : dropOne;
794 
795   auto result = p.name.splitter('.').filter!(a => !a.empty);
796   if(!result.empty) {
797     result = result.dropOne;
798   }
799   return result.map!(a => '.' ~ a).array;
800 }
801 
802 ///
803 unittest
804 {
805   assertEmpty(Path().extensions);
806   assertEmpty(Path("").extensions);
807   assertEmpty(Path("/").extensions);
808   assertEmpty(Path("/hello").extensions);
809   assertEmpty(Path("C:/hello/world").extensions);
810   assertEqual(Path("C:/hello/world.exe").extensions, [".exe"]);
811   assertEqual(Path("hello/world.foo.bar.exe").extensions, [".foo", ".bar", ".exe"]);
812 }
813 
814 
815 /// The full extension of the path.
816 ///
817 /// Examples: The full extension of "hello.foo.bar.exe" would be ".foo.bar.exe".
818 auto fullExtension(PathType)(auto ref in PathType p)
819   if(isSomePath!PathType)
820 {
821   auto data = p.name;
822   auto i = data.indexOf('.');
823   if(i < 0) {
824     return "";
825   }
826   if(i + 1 == data.length) {
827     // This prevents preserving the dot in empty extensions such as `hello.foo.`.
828     ++i;
829   }
830   return data[i .. $];
831 }
832 
833 ///
834 unittest
835 {
836   assertEmpty(Path().fullExtension);
837   assertEmpty(Path("").fullExtension);
838   assertEmpty(Path("/").fullExtension);
839   assertEmpty(Path("/hello").fullExtension);
840   assertEmpty(Path("C:/hello/world").fullExtension);
841   assertEqual(Path("C:/hello/world.exe").fullExtension, ".exe");
842   assertEqual(Path("hello/world.foo.bar.exe").fullExtension, ".foo.bar.exe");
843 }
844 
845 
846 /// The name of the path without its extension.
847 auto stem(PathType)(auto ref in PathType p)
848   if(isSomePath!PathType)
849 {
850   auto data = p.name;
851   auto i = data.indexOf('.');
852   if(i < 0) {
853     return data;
854   }
855   if(i + 1 == data.length) {
856     // This prevents preserving the dot in empty extensions such as `hello.foo.`.
857     ++i;
858   }
859   return data[0 .. i];
860 }
861 
862 ///
863 unittest
864 {
865   assertEqual(Path().stem, ".");
866   assertEqual(Path("").stem, ".");
867   assertEqual(Path("/").stem, "");
868   assertEqual(Path("/hello").stem, "hello");
869   assertEqual(Path("C:/hello/world").stem, "world");
870   assertEqual(Path("C:/hello/world.exe").stem, "world");
871   assertEqual(Path("hello/world.foo.bar.exe").stem, "world");
872 }
873 
874 
875 /// Create a path from $(D p) that is relative to $(D parent).
876 auto relativeTo(PathType)(auto ref in PathType p, in auto ref PathType parent)
877   if(isSomePath!PathType)
878 {
879   auto ldata = p.normalizedData;
880   auto rdata = parent.normalizedData;
881   if(!ldata.startsWith(rdata)) {
882     throw new PathException(format("'%s' is not a subpath of '%s'.", ldata, rdata));
883   }
884   auto sliceStart = rdata.length;
885   if(rdata.length != ldata.length) {
886     // Remove trailing path separator.
887     ++sliceStart;
888   }
889   auto result = ldata[sliceStart .. $];
890   return result.isDot ? PathType(".") : PathType(result);
891 }
892 
893 ///
894 unittest
895 {
896   import std.exception : assertThrown;
897 
898   assertEqual(Path("C:/hello/world.exe").relativeTo(Path("C:/hello")), Path("world.exe"));
899   assertEqual(Path("C:/hello").relativeTo(Path("C:/hello")), Path());
900   assertEqual(Path("C:/hello/").relativeTo(Path("C:/hello")), Path());
901   assertEqual(Path("C:/hello").relativeTo(Path("C:/hello/")), Path());
902   assertEqual(WindowsPath("C:/foo/bar/baz").relativeTo(WindowsPath("C:/foo")), WindowsPath(`bar\baz`));
903   assertEqual(PosixPath("C:/foo/bar/baz").relativeTo(PosixPath("C:/foo")), PosixPath("bar/baz"));
904   assertThrown!PathException(Path("a").relativeTo(Path("b")));
905 }
906 
907 
908 /// Whether the given path matches the given glob-style pattern
909 auto match(PathType, Pattern)(auto ref in PathType p, Pattern pattern)
910   if(isSomePath!PathType)
911 {
912   import std.path : globMatch;
913 
914   return p.normalizedData.globMatch!(PathType.caseSensitivity)(pattern);
915 }
916 
917 ///
918 unittest
919 {
920   assert(Path().match("*"));
921   assert(Path("").match("*"));
922   assert(Path(".").match("*"));
923   assert(Path("/").match("*"));
924   assert(Path("/hello").match("*"));
925   assert(Path("/hello/world.exe").match("*"));
926   assert(Path("/hello/world.exe").match("*.exe"));
927   assert(!Path("/hello/world.exe").match("*.zip"));
928   assert(WindowsPath("/hello/world.EXE").match("*.exe"));
929   assert(!PosixPath("/hello/world.EXE").match("*.exe"));
930 }
931 
932 
933 /// Whether the path exists or not. It does not matter whether it is a file or not.
934 bool exists(in Path p) {
935   return std.file.exists(p.data);
936 }
937 
938 ///
939 unittest
940 {
941 }
942 
943 
944 /// Whether the path is an existing directory.
945 bool isDir(in Path p) {
946   return std.file.isDir(p.data);
947 }
948 
949 ///
950 unittest
951 {
952 }
953 
954 
955 /// Whether the path is an existing file.
956 bool isFile(in Path p) {
957   return std.file.isFile(p.data);
958 }
959 
960 ///
961 unittest
962 {
963 }
964 
965 
966 /// Whether the given path $(D p) points to a symbolic link (or junction point in Windows).
967 bool isSymlink(in Path p) {
968   return std.file.isSymlink(p.normalizedData);
969 }
970 
971 ///
972 unittest
973 {
974 }
975 
976 
977 // Resolve all ".", "..", and symlinks.
978 Path resolved(in Path p) {
979   return Path(Path(std.path.absolutePath(p.data)).normalizedData);
980 }
981 
982 ///
983 unittest
984 {
985   assertNotEqual(Path(), Path().resolved());
986 }
987 
988 
989 /// The absolute path to the current working directory with symlinks and friends resolved.
990 Path cwd() {
991   return Path(std.file.getcwd());
992 }
993 
994 ///
995 unittest
996 {
997   assertNotEmpty(cwd().data);
998 }
999 
1000 
1001 /// The path to the current executable.
1002 Path currentExePath() {
1003   return Path(std.file.thisExePath()).resolved();
1004 }
1005 
1006 ///
1007 unittest
1008 {
1009   assertNotEmpty(currentExePath().data);
1010 }
1011 
1012 
1013 void chdir(in Path p) {
1014   std.file.chdir(p.normalizedData);
1015 }
1016 
1017 ///
1018 unittest
1019 {
1020 }
1021 
1022 
1023 /// Generate an input range of Paths that match the given pattern.
1024 auto glob(PatternType)(auto ref in Path p, PatternType pattern, SpanMode spanMode = SpanMode.shallow) {
1025   return std.file.dirEntries(p.normalizedData, pattern, spanMode).map!(a => Path(a.name));
1026 }
1027 
1028 ///
1029 unittest
1030 {
1031   assertNotEmpty(currentExePath().parent.glob("*"));
1032   assertNotEmpty(currentExePath().parent.glob("*", SpanMode.shallow));
1033   assertNotEmpty(currentExePath().parent.glob("*", SpanMode.breadth));
1034   assertNotEmpty(currentExePath().parent.glob("*", SpanMode.depth));
1035 }
1036 
1037 
1038 auto open(in Path p, in char[] openMode = "rb") {
1039   static import std.stdio;
1040 
1041   return std.stdio.File(p.normalizedData, openMode);
1042 }
1043 
1044 ///
1045 unittest
1046 {
1047 }
1048 
1049 
1050 /// Copy a file to some destination.
1051 /// If the destination exists and is a file, it is overwritten. If it is an existing directory, the actual destination will be ($D dest ~ src.name).
1052 /// Behaves like std.file.copy except that $(D dest) does not have to be a file.
1053 /// See_Also: copyTo(in Path, in Path)
1054 void copyFileTo(in Path src, in Path dest) {
1055   if(dest.exists && dest.isDir) {
1056     std.file.copy(src.normalizedData, (dest ~ src.name).normalizedData);
1057   }
1058   else {
1059     std.file.copy(src.normalizedData, dest.normalizedData);
1060   }
1061 }
1062 
1063 ///
1064 unittest
1065 {
1066 }
1067 
1068 
1069 /// Copy a file or directory to a target file or directory.
1070 ///
1071 /// This function essentially behaves like the unix shell command `cp -r` with just asingle source input.
1072 ///
1073 /// Params:
1074 ///   src = The path to a file or a directory.
1075 ///   dest = The path to a file or a directory. If $(D src) is a directory, $(D dest) must be an existing directory.
1076 ///
1077 /// Throws: PathException
1078 void copyTo(alias copyCondition = (a, b){ return true; })(in Path src, in Path dest) {
1079   if(!src.exists) {
1080     throw new PathException(format("The source path does not exist: %s", src));
1081   }
1082 
1083   if(src.isFile) {
1084     if(copyCondition(src, dest)) src.copyFileTo(dest);
1085     return;
1086   }
1087 
1088   if(!dest.exists) {
1089     dest.mkdir(false);
1090   }
1091   else if(dest.isFile) {
1092     // At this point we know that src must be a dir.
1093     throw new PathException(format("Since the source path is a directory, the destination must be a directory as well. Source: %s | Destination: %s", src, dest));
1094   }
1095 
1096   foreach(srcFile; src.glob("*", SpanMode.breadth).filter!(a => !a.isDir)) {
1097     auto destFile = dest ~ srcFile.relativeTo(src);
1098     if(!copyCondition(srcFile, destFile)) {
1099       continue;
1100     }
1101     if(!destFile.exists) {
1102       destFile.parent.mkdir(true);
1103     }
1104     srcFile.copyFileTo(destFile);
1105   }
1106 }
1107 
1108 ///
1109 unittest
1110 {
1111 }
1112 
1113 
1114 alias copyToIfNewer = copyTo!((src, dest){
1115   import std.datetime : SysTime;
1116   SysTime _, src_modTime, dest_modTime;
1117   std.file.getTimes(src.normalizedData, _, src_modTime);
1118   std.file.getTimes(dest.normalizedData, _, dest_modTime);
1119   return src_modTime < dest_modTime;
1120 });
1121 
1122 
1123 /// Remove path from filesystem. Similar to unix `rm`. If the path is a dir, will reecursively remove all subdirs by default.
1124 void remove(in Path p, bool recursive = true) {
1125   if(p.isFile) {
1126     std.file.remove(p.normalizedData);
1127   }
1128   else if(recursive) {
1129     std.file.rmdirRecurse(p.normalizedData);
1130   }
1131   else {
1132     std.file.rmdir(p.normalizedData);
1133   }
1134 }
1135 
1136 ///
1137 unittest
1138 {
1139 }
1140 
1141 
1142 ///
1143 void mkdir(in Path p, bool parents = true) {
1144   if(parents) {
1145     std.file.mkdirRecurse(p.normalizedData);
1146   }
1147   else {
1148     std.file.mkdir(p.normalizedData);
1149   }
1150 }
1151 
1152 ///
1153 unittest
1154 {
1155 }
1156 
1157 
1158 auto readFile(in Path p) {
1159   return std.file.read(p.normalizedData);
1160 }
1161 
1162 ///
1163 unittest
1164 {
1165 }
1166 
1167 
1168 ///
1169 auto readFile(S)(in Path p)
1170   if(isSomeString!S)
1171 {
1172   return cast(S)std.file.readText!S(p.normalizedData);
1173 }
1174 
1175 ///
1176 unittest
1177 {
1178 }
1179 
1180 
1181 ///
1182 void writeFile(in Path p, const void[] buffer) {
1183   std.file.write(p.normalizedData, buffer);
1184 }
1185 
1186 ///
1187 unittest
1188 {
1189 }
1190 
1191 
1192 ///
1193 void appendFile(in Path p, in void[] buffer) {
1194   std.file.append(p.normalizedData, buffer);
1195 }
1196 
1197 ///
1198 unittest
1199 {
1200 }