1 /++
2   Authors:
3     Manuzor
4 
5   ToDo:
6     - Path().open(...)
7 +/
8 module pathlib;
9 
10 static import std.path;
11 static import std.file;
12 static import std.uni;
13 import std.array      : array, replace, replaceLast;
14 import std.format     : format;
15 import std.string     : split, indexOf, empty, squeeze, removechars, lastIndexOf;
16 import std.conv       : to;
17 import std.typecons   : Flag;
18 import std.traits     : isSomeString, isSomeChar;
19 import std.algorithm  : equal, map, reduce, startsWith, endsWith, strip, stripRight, stripLeft, remove;
20 import std.range      : iota, take, isInputRange;
21 
22 
23 debug
24 {
25   void logDebug(T...)(T args) {
26     import std.stdio;
27 
28     writefln(args);
29   }
30 }
31 
32 void assertEqual(alias predicate = "a == b", A, B, StringType = string)
33                 (A a, B b, StringType fmt = "`%s` must be equal to `%s`")
34 {
35   static if (isInputRange!A && isInputRange!B) {
36     assert(std.algorithm.equal!predicate(a, b), format(fmt, a, b));
37   }
38   else {
39     import std.functional : binaryFun;
40     assert(binaryFun!predicate(a, b), format(fmt, a, b));
41   }
42 }
43 
44 void assertNotEqual(alias predicate = "a != b", A, B, StringType = string)
45                     (A a, B b, StringType fmt = "`%s` must not be equal to `%s`")
46 {
47   assertEqual!predicate(a, b, fmt);
48 }
49 
50 void assertEmpty(A, StringType = string)(A a, StringType fmt = "String `%s` should be empty.") {
51   assert(a.empty, format(fmt, a));
52 }
53 
54 void assertNotEmpty(A, StringType = string)(A a, StringType fmt = "String `%s` should be empty.") {
55   assert(!a.empty, format(fmt, a));
56 }
57 
58 
59 /// Whether $(D str) can be represented by ".".
60 bool isDot(StringType)(auto ref in StringType str)
61   if (isSomeString!StringType)
62 {
63   if (str.length && str[0] != '.') {
64     return false;
65   }
66   auto data = str.removechars("./\\");
67   return data.empty;
68 }
69 
70 ///
71 unittest {
72   assert("".isDot);
73   assert(".".isDot);
74   assert("./".isDot);
75   assert("./.".isDot);
76   assert("././".isDot);
77   assert(!"/".isDot);
78   assert(!"hello".isDot);
79   assert(!".git".isDot);
80 }
81 
82 
83 /// Whether the path is either "" or ".".
84 auto isDot(PathType)(auto ref in PathType p)
85   if (!isSomeString!PathType)
86 {
87   return p.data.isDot;
88 }
89 
90 ///
91 unittest {
92   assert(WindowsPath().isDot);
93   assert(WindowsPath("").isDot);
94   assert(WindowsPath(".").isDot);
95   assert(PosixPath().isDot);
96   assert(PosixPath("").isDot);
97   assert(PosixPath(".").isDot);
98 }
99 
100 
101 /// Returns: The root of the path $(D p).
102 auto root(PathType)(auto ref in PathType p) {
103   auto data = p.data;
104 
105   static if(is(PathType == WindowsPath))
106   {
107     if (data.length > 1 && data[1] == ':') {
108       return data[0..2];
109     }
110   }
111   else // Assume PosixPath
112   {
113     if (data.length > 0 && data[0] == '/') {
114       return data[0..1];
115     }
116   }
117 
118   return "";
119 }
120 
121 ///
122 unittest {
123   assertEqual(WindowsPath("").root, "");
124   assertEqual(PosixPath("").root, "");
125   assertEqual(WindowsPath("C:/Hello/World").root, "C:");
126   assertEqual(WindowsPath("/Hello/World").root, "");
127   assertEqual(PosixPath("/hello/world").root, "/");
128   assertEqual(PosixPath("C:/hello/world").root, "");
129 }
130 
131 
132 /// The drive of the path $(D p).
133 /// Note: Non-Windows platforms have no concept of "drives".
134 auto drive(PathType)(auto ref in PathType p) {
135   static if(is(PathType == WindowsPath))
136   {
137     auto data = p.data;
138     if (data.length > 1 && data[1] == ':') {
139       return data[0..2];
140     }
141   }
142 
143   return "";
144 }
145 
146 ///
147 unittest {
148   assertEqual(WindowsPath("").drive, "");
149   assertEqual(WindowsPath("/Hello/World").drive, "");
150   assertEqual(WindowsPath("C:/Hello/World").drive, "C:");
151   assertEqual(PosixPath("").drive, "");
152   assertEqual(PosixPath("/Hello/World").drive, "");
153   assertEqual(PosixPath("C:/Hello/World").drive, "");
154 }
155 
156 
157 /// Returns: The path data using forward slashes, regardless of the current platform.
158 auto posixData(PathType)(auto ref in PathType p) {
159   if (p.isDot) {
160     return ".";
161   }
162   auto root = p.root;
163   auto result = p.data[root.length..$].replace("\\", "/").squeeze("/");
164   if (result.length > 1 && result[$ - 1] == '/') {
165     result = result[0..$ - 1];
166   }
167   while (result.endsWith("/.")) {
168     result = result[0 .. $ - 2].squeeze("/");
169   }
170   return root ~ result;
171 }
172 
173 ///
174 unittest {
175   assertEqual(Path().posixData, ".");
176   assertEqual(Path("").posixData, ".");
177   assertEqual(Path("/hello/world").posixData, "/hello/world");
178   assertEqual(Path("/\\hello/\\/////world//").posixData, "/hello/world");
179   assertEqual(Path(`C:\`).posixData, "C:/");
180   assertEqual(Path(`C:\hello\`).posixData, "C:/hello");
181   assertEqual(Path(`C:\/\hello\`).posixData, "C:/hello");
182   assertEqual(Path(`C:\some windows\/path.exe.doodee`).posixData, "C:/some windows/path.exe.doodee");
183   assertEqual(Path(`C:\some windows\/path.exe.doodee\\\`).posixData, "C:/some windows/path.exe.doodee");
184   assertEqual(Path(`C:\some windows\/path.exe.doodee\\\`).posixData, Path(Path(`C:\some windows\/path.exe.doodee\\\`).posixData).data);
185   assertEqual((Path(".") ~ "hello" ~ "./" ~ "world").posixData, "hello/world");
186   assertEqual(Path("hello/.").posixData, "hello");
187   assertEqual(Path("hello/././.").posixData, "hello");
188   assertEqual(Path("hello/././/.//").posixData, "hello");
189 }
190 
191 
192 /// Returns: The path data using backward slashes, regardless of the current platform.
193 auto windowsData(PathType)(auto ref in PathType p) {
194   return p.posixData.replace("/", `\`);
195 }
196 
197 ///
198 unittest {
199   assertEqual(Path().windowsData, ".");
200   assertEqual(Path("").windowsData, Path("").windowsData);
201   assertEqual(Path("/hello/world").windowsData, `\hello\world`);
202   assertEqual(Path("/\\hello/\\/////world//").windowsData, `\hello\world`);
203   assertEqual(Path(`C:\`).windowsData, `C:\`);
204   assertEqual(Path(`C:/`).windowsData, `C:\`);
205   assertEqual(Path(`C:\hello\`).windowsData, `C:\hello`);
206   assertEqual(Path(`C:\/\hello\`).windowsData, `C:\hello`);
207   assertEqual(Path(`C:\some windows\/path.exe.doodee`).windowsData, `C:\some windows\path.exe.doodee`);
208   assertEqual(Path(`C:\some windows\/path.exe.doodee\\\`).windowsData, `C:\some windows\path.exe.doodee`);
209   assertEqual(Path(`C:/some windows\/path.exe.doodee\\\`).windowsData, Path(Path(`C:\some windows\/path.exe.doodee\\\`).windowsData).data);
210 }
211 
212 
213 /// Will call either posixData or windowsData, according to PathType.
214 auto normalizedData(PathType)(auto ref in PathType p)
215   if(is(PathType == PosixPath) || is(PathType == WindowsPath))
216 {
217   static if (is(PathType == PosixPath)) {
218     return p.posixData;
219   }
220   else static if (is(PathType == WindowsPath)) {
221     return p.windowsData;
222   }
223 }
224 
225 
226 auto asPosixPath(PathType)(auto ref in PathType p) {
227   return PathType(p.posixData);
228 }
229 
230 auto asWindowsPath(PathType)(auto ref in PathType p) {
231   return PathType(p.windowsData);
232 }
233 
234 auto asNormalizedPath(PathType)(auto ref in PathType p) {
235   return PathType(p.normalizedData);
236 }
237 
238 
239 /// Whether the path is absolute.
240 auto isAbsolute(PathType)(auto ref in PathType p) {
241   // If the path has a root or a drive, it is absolute.
242   return !p.root.empty || !p.drive.empty;
243 }
244 
245 
246 /// Returns: The parts of the path as an array.
247 auto parts(PathType)(auto ref in PathType p) {
248   string[] theParts;
249   auto root = p.root;
250   if (!root.empty) {
251     static if(is(PathType == WindowsPath)) {
252       root ~= p.separator;
253     }
254     theParts ~= root;
255   }
256   theParts ~= p.posixData[root.length .. $].strip('/').split('/')[];
257   return theParts;
258 }
259 
260 ///
261 unittest {
262   assertEqual(Path().parts, ["."]);
263   assertEqual(Path("./.").parts, ["."]);
264   assertEqual(WindowsPath("hello/world.exe.xml").parts, ["hello", "world.exe.xml"]);
265   assertEqual(WindowsPath("C:/hello/world.exe.xml").parts, [`C:\`, "hello", "world.exe.xml"]);
266   assertEqual(PosixPath("hello/world.exe.xml").parts, ["hello", "world.exe.xml"]);
267   assertEqual(PosixPath("/hello/world.exe.xml").parts, ["/", "hello", "world.exe.xml"]);
268 }
269 
270 
271 auto parent(PathType)(auto ref in PathType p) {
272   auto theParts = p.parts.map!(a => PathType(a));
273   if (theParts.length > 1) {
274     return theParts[0 .. $ - 1].reduce!((a, b){ return a ~ b;});
275   }
276   return PathType();
277 }
278 
279 ///
280 unittest {
281   assertEqual(Path().parent, Path());
282   assertEqual(Path("IHaveNoParents").parent, Path());
283   assertEqual(WindowsPath("C:/hello/world").parent, WindowsPath(`C:\hello`));
284   assertEqual(WindowsPath("C:/hello/world/").parent, WindowsPath(`C:\hello`));
285   assertEqual(WindowsPath("C:/hello/world.exe.foo").parent, WindowsPath(`C:\hello`));
286   assertEqual(PosixPath("/hello/world").parent, PosixPath("/hello"));
287   assertEqual(PosixPath("/hello/\\/world/").parent, PosixPath("/hello"));
288   assertEqual(PosixPath("/hello.foo.bar/world/").parent, PosixPath("/hello.foo.bar"));
289 }
290 
291 
292 /// Returns: The parts of the path as an array, without the last component.
293 auto parents(PathType)(auto ref in PathType p) {
294   auto theParts = p.parts.map!(x => PathType(x));
295   return iota(theParts.length - 1, 0, -1).map!(x => theParts.take(x).reduce!((a, b){ return a ~ b; })).array;
296 }
297 
298 ///
299 unittest {
300   assertEmpty(Path().parents);
301   assertEqual(WindowsPath("C:/hello/world").parents, [WindowsPath(`C:\hello`), WindowsPath(`C:\`)]);
302   assertEqual(WindowsPath("C:/hello/world/").parents, [WindowsPath(`C:\hello`), WindowsPath(`C:\`)]);
303   assertEqual(PosixPath("/hello/world").parents, [PosixPath("/hello"), PosixPath("/")]);
304   assertEqual(PosixPath("/hello/world/").parents, [PosixPath("/hello"), PosixPath("/")]);
305 }
306 
307 
308 /// The name of the path without any of its parents.
309 auto name(PathType)(auto ref in PathType p) {
310   import std.algorithm : min;
311 
312   auto data = p.posixData;
313   auto i = min(data.lastIndexOf('/') + 1, data.length);
314   return data[i .. $];
315 }
316 
317 ///
318 unittest {
319   assertEqual(Path().name, ".");
320   assertEqual(Path("").name, ".");
321   assertEmpty(Path("/").name);
322   assertEqual(Path("/hello").name, "hello");
323   assertEqual(Path("C:\\hello").name, "hello");
324   assertEqual(Path("C:/hello/world.exe").name, "world.exe");
325   assertEqual(Path("hello/world.foo.bar.exe").name, "world.foo.bar.exe");
326 }
327 
328 
329 /// The extension of the path including the leading dot.
330 ///
331 /// Examples: The extension of "hello.foo.bar.exe" is "exe".
332 auto extension(PathType)(auto ref in PathType p) {
333   auto data = p.name;
334   auto i = data.lastIndexOf('.');
335   if (i < 0) {
336     return "";
337   }
338   if (i + 1 == data.length) {
339     // This prevents preserving the dot in empty extensions such as `hello.foo.`.
340     ++i;
341   }
342   return data[i .. $];
343 }
344 
345 ///
346 unittest {
347   assertEmpty(Path().extension);
348   assertEmpty(Path("").extension);
349   assertEmpty(Path("/").extension);
350   assertEmpty(Path("/hello").extension);
351   assertEmpty(Path("C:/hello/world").extension);
352   assertEqual(Path("C:/hello/world.exe").extension, ".exe");
353   assertEqual(Path("hello/world.foo.bar.exe").extension, ".exe");
354 }
355 
356 
357 /// All extensions of the path.
358 auto extensions(PathType)(auto ref in PathType p) {
359   import std.algorithm : splitter, filter;
360   import std.range : dropOne;
361 
362   auto result = p.name.splitter('.').filter!(a => !a.empty);
363   if (!result.empty) {
364     result = result.dropOne;
365   }
366   return result.map!(a => '.' ~ a).array;
367 }
368 
369 ///
370 unittest {
371   assertEmpty(Path().extensions);
372   assertEmpty(Path("").extensions);
373   assertEmpty(Path("/").extensions);
374   assertEmpty(Path("/hello").extensions);
375   assertEmpty(Path("C:/hello/world").extensions);
376   assertEqual(Path("C:/hello/world.exe").extensions, [".exe"]);
377   assertEqual(Path("hello/world.foo.bar.exe").extensions, [".foo", ".bar", ".exe"]);
378 }
379 
380 
381 /// The full extension of the path.
382 ///
383 /// Examples: The full extension of "hello.foo.bar.exe" would be "foo.bar.exe".
384 auto fullExtension(PathType)(auto ref in PathType p) {
385   auto data = p.name;
386   auto i = data.indexOf('.');
387   if (i < 0) {
388     return "";
389   }
390   if (i + 1 == data.length) {
391     // This prevents preserving the dot in empty extensions such as `hello.foo.`.
392     ++i;
393   }
394   return data[i .. $];
395 }
396 
397 ///
398 unittest {
399   assertEmpty(Path().fullExtension);
400   assertEmpty(Path("").fullExtension);
401   assertEmpty(Path("/").fullExtension);
402   assertEmpty(Path("/hello").fullExtension);
403   assertEmpty(Path("C:/hello/world").fullExtension);
404   assertEqual(Path("C:/hello/world.exe").fullExtension, ".exe");
405   assertEqual(Path("hello/world.foo.bar.exe").fullExtension, ".foo.bar.exe");
406 }
407 
408 
409 /// The name of the path without its extension.
410 auto stem(PathType)(auto ref in PathType p) {
411   auto data = p.name;
412   auto i = data.indexOf('.');
413   if (i < 0) {
414     return data;
415   }
416   if (i + 1 == data.length) {
417     // This prevents preserving the dot in empty extensions such as `hello.foo.`.
418     ++i;
419   }
420   return data[0 .. i];
421 }
422 
423 ///
424 unittest {
425   assertEqual(Path().stem, ".");
426   assertEqual(Path("").stem, ".");
427   assertEqual(Path("/").stem, "");
428   assertEqual(Path("/hello").stem, "hello");
429   assertEqual(Path("C:/hello/world").stem, "world");
430   assertEqual(Path("C:/hello/world.exe").stem, "world");
431   assertEqual(Path("hello/world.foo.bar.exe").stem, "world");
432 }
433 
434 
435 /// Whether the given path matches the given glob-style pattern
436 auto match(PathType, Pattern)(auto ref in PathType p, Pattern pattern) {
437   import std.path : globMatch;
438 
439   return p.normalizedData.globMatch!(PathType.caseSensitivity)(pattern);
440 }
441 
442 ///
443 unittest {
444   assert(Path().match("*"));
445   assert(Path("").match("*"));
446   assert(Path(".").match("*"));
447   assert(Path("/").match("*"));
448   assert(Path("/hello").match("*"));
449   assert(Path("/hello/world.exe").match("*"));
450   assert(Path("/hello/world.exe").match("*.exe"));
451   assert(!Path("/hello/world.exe").match("*.zip"));
452   assert(WindowsPath("/hello/world.EXE").match("*.exe"));
453   assert(!PosixPath("/hello/world.EXE").match("*.exe"));
454 }
455 
456 
457 /// Whether the path exists or not. It does not matter whether it is a file or not.
458 bool exists(in Path p) {
459   return std.file.exists(p.data);
460 }
461 
462 ///
463 unittest {
464 }
465 
466 
467 /// Whether the path is an existing directory.
468 bool isDir(in Path p) {
469   return p.exists && std.file.isDir(p.data);
470 }
471 
472 ///
473 unittest {
474 }
475 
476 
477 /// Whether the path is an existing file.
478 bool isFile(in Path p) {
479   return p.exists && std.file.isFile(p.data);
480 }
481 
482 ///
483 unittest {
484 }
485 
486 
487 /// Whether the given path $(D p) points to a symbolic link (or junction point in Windows).
488 bool isSymlink(in Path p) {
489   return std.file.isSymlink(p.normalizedData);
490 }
491 
492 ///
493 unittest {
494 }
495 
496 
497 // Resolve all ".", "..", and symlinks.
498 Path resolve(in Path p) {
499   return Path(Path(std.path.absolutePath(p.data)).normalizedData);
500 }
501 
502 ///
503 unittest {
504   assertNotEqual(Path(), Path().resolve());
505 }
506 
507 
508 /// The absolute path to the current working directory with symlinks and friends resolved.
509 Path cwd() {
510   return Path().resolve();
511 }
512 
513 ///
514 unittest {
515   assertNotEmpty(cwd().data);
516 }
517 
518 
519 /// The path to the current executable.
520 Path currentExePath() {
521   return Path(std.file.thisExePath()).resolve();
522 }
523 
524 ///
525 unittest {
526   assertNotEmpty(currentExePath().data);
527 }
528 
529 
530 void mkdir(in Path p) {
531   std.file.mkdirRecurse(p.normalizedData);
532 }
533 
534 
535 void chdir(in Path p) {
536   std.file.chdir(p.normalizedData);
537 }
538 
539 
540 /// Generate an array of Paths that match the given pattern in and beneath the given path.
541 auto glob(PatternType)(auto ref in Path p, PatternType pattern) {
542   import std.algorithm : filter;
543   import std.file : SpanMode;
544 
545   return std.file.dirEntries(p.normalizedData, pattern, SpanMode.shallow)
546          .map!(a => Path(a.name));
547 }
548 
549 ///
550 unittest {
551   assertNotEmpty(currentExePath().parent.glob("*"));
552 }
553 
554 
555 /// Generate an array of Paths that match the given pattern in and beneath the given path.
556 auto rglob(PatternType)(auto ref in Path p, PatternType pattern) {
557   import std.algorithm : filter;
558   import std.file : SpanMode;
559 
560   return std.file.dirEntries(p.normalizedData, pattern, SpanMode.breadth)
561          .map!(a => Path(a.name));
562 }
563 
564 ///
565 unittest {
566   assertNotEmpty(currentExePath().parent.rglob("*"));
567 }
568 
569 
570 auto open(in Path p, in char[] openMode = "rb") {
571   static import std.stdio;
572 
573   return std.stdio.File(p.normalizedData, openMode);
574 }
575 
576 ///
577 unittest {
578 }
579 
580 
581 mixin template PathCommon(PathType, StringType, alias theSeparator, alias theCaseSensitivity)
582   if (isSomeString!StringType && isSomeChar!(typeof(theSeparator)))
583 {
584   StringType data = ".";
585 
586   ///
587   unittest {
588     assert(PathType().data != "");
589     assert(PathType("123").data == "123");
590     assert(PathType("C:///toomany//slashes!\\").data == "C:///toomany//slashes!\\");
591   }
592 
593 
594   /// Used to separate each path segment.
595   alias separator = theSeparator;
596 
597   /// A value of std.path.CaseSensetive whether this type of path is case sensetive or not.
598   alias caseSensitivity = theCaseSensitivity;
599 
600 
601   /// Concatenate a path and a string, which will be treated as a path.
602   auto opBinary(string op, InStringType)(auto ref in InStringType str) const
603     if (op == "~" && isSomeString!InStringType)
604   {
605     return this ~ PathType(str);
606   }
607 
608   /// Concatenate two paths.
609   auto opBinary(string op)(auto ref in PathType other) const
610     if (op == "~")
611   {
612     auto p = PathType(data);
613     p ~= other;
614     return p;
615   }
616 
617   /// Concatenate the path-string $(D str) to this path.
618   void opOpAssign(string op, InStringType)(auto ref in InStringType str)
619     if (op == "~" && isSomeString!InStringType)
620   {
621     this ~= PathType(str);
622   }
623 
624   /// Concatenate the path $(D other) to this path.
625   void opOpAssign(string op)(auto ref in PathType other)
626     if (op == "~")
627   {
628     auto l = PathType(this.normalizedData);
629     auto r = PathType(other.normalizedData);
630 
631     if (l.isDot || r.isAbsolute) {
632       this.data = r.data;
633       return;
634     }
635 
636     if (r.isDot) {
637       this.data = l.data;
638       return;
639     }
640 
641     auto sep = "";
642     if (!l.data.endsWith('/', '\\') && !r.data.startsWith('/', '\\')) {
643       sep = [separator];
644     }
645 
646     this.data = format("%s%s%s", l.data, sep, r.data);
647   }
648 
649   ///
650   unittest {
651     assertEqual(PathType() ~ "hello", PathType("hello"));
652     assertEqual(PathType("") ~ "hello", PathType("hello"));
653     assertEqual(PathType(".") ~ "hello", PathType("hello"));
654     assertEqual(PosixPath("/") ~ "hello", PosixPath("/hello"));
655     assertEqual(WindowsPath("/") ~ "hello", WindowsPath(`\hello`));
656     assertEqual(PosixPath("/") ~ "hello" ~ "world", PosixPath("/hello/world"));
657     assertEqual(WindowsPath(`C:\`) ~ "hello" ~ "world", WindowsPath(`C:\hello\world`));
658   }
659 
660 
661   /// Equality overload.
662   bool opBinary(string op)(auto ref in PathType other) const
663     if (op == "==")
664   {
665     auto l = this.data.empty ? "." : this.data;
666     auto r = other.data.empty ? "." : other.data;
667     static if (theCaseSensitivity == std.path.CaseSensetive.no) {
668       return std.uni.sicmp(l, r);
669     }
670     else {
671       return std.algorithm.cmp(l, r);
672     }
673   }
674 
675   ///
676   unittest {
677     auto p1 = PathType("/hello/world");
678     auto p2 = PathType("/hello/world");
679     assertEqual(p1, p2);
680   }
681 
682 
683   /// Cast the path to a string.
684   auto toString(OtherStringType = StringType)() const {
685     return this.data.to!OtherStringType;
686   }
687 
688   ///
689   unittest {
690     assertEqual(PathType("C:/hello/world.exe.xml").to!StringType, "C:/hello/world.exe.xml");
691   }
692 }
693 
694 
695 struct WindowsPath
696 {
697   mixin PathCommon!(WindowsPath, string, '\\', std.path.CaseSensitive.no);
698 }
699 
700 
701 struct PosixPath
702 {
703   mixin PathCommon!(PosixPath, string, '/', std.path.CaseSensitive.yes);
704 }
705 
706 /// Set the default path depending on the current platform.
707 version(Windows)
708 {
709   alias Path = WindowsPath;
710 }
711 else // Assume posix on non-windows platforms.
712 {
713   alias Path = PosixPath;
714 }