1 /// This module defines Path - the main structure that represent's highlevel interface to paths
2 module thepath.path;
3 
4 static public import std.file: SpanMode;
5 static private import std.path;
6 static private import std.file;
7 static private import std.stdio;
8 static private import std.process;
9 static private import std.algorithm;
10 private import std.typecons: Nullable, nullable;
11 private import std.path: expandTilde;
12 private import std.format: format;
13 private import std.exception: enforce;
14 private import thepath.utils: createTempPath, createTempDirectory;
15 private import thepath.exception: PathException;
16 
17 
18 /** Path - struct that represents single path object, and provides convenient
19   * interface to deal with filesystem paths.
20   **/
21 struct Path {
22     private string _path;
23 
24     /** Main constructor to build new Path from string
25       * Params:
26       *    path = string representation of path to point to
27       **/
28     @safe pure nothrow this(in string path) {
29         _path = path;
30     }
31 
32     /** Constructor that allows to build path from segments
33       * Params:
34       *     segments = array of segments to build path from
35       **/
36     @safe pure nothrow this(in string[] segments...) {
37         _path = std.path.buildNormalizedPath(segments);
38     }
39 
40     ///
41     unittest {
42         import dshould;
43 
44         version(Posix) {
45             Path("foo", "moo", "boo").toString.should.equal("foo/moo/boo");
46             Path("/foo/moo", "boo").toString.should.equal("/foo/moo/boo");
47         }
48     }
49 
50     invariant {
51         // TODO: it seems that this invariant is not needed.
52         //       Try to find test case when it is needed
53         assert(_path !is null, "Attempt to use uninitialized path!");
54     }
55 
56     /** Check if path is valid.
57       * Returns: true if this is valid path.
58       **/
59     @safe pure nothrow bool isValid() const {
60         return std.path.isValidPath(_path);
61     }
62 
63     ///
64     unittest {
65         import dshould;
66 
67         Path("").isValid.should.be(false);
68         Path(".").isValid.should.be(true);
69         Path("some-path").isValid.should.be(true);
70     }
71 
72     /// Check if path is absolute
73     @safe pure nothrow bool isAbsolute() const {
74         return std.path.isAbsolute(_path);
75     }
76 
77     ///
78     unittest {
79         import dshould;
80 
81         Path("").isAbsolute.should.be(false);
82         Path(".").isAbsolute.should.be(false);
83         Path("some-path").isAbsolute.should.be(false);
84 
85         version(Posix) {
86             Path("/test/path").isAbsolute.should.be(true);
87         }
88     }
89 
90     /// Check if path starts at root directory (or drive letter)
91     @safe pure nothrow bool isRooted() const {
92         return std.path.isRooted(_path);
93     }
94 
95     /// Check if current path is root (does not have parent)
96     @safe pure bool isRoot() const {
97         import std.path: isDirSeparator;
98 
99         version(Posix) {
100             return _path == "/";
101         } else version (Windows) {
102             if (_path.length == 3 && _path[1] == ':' &&
103                     isDirSeparator(_path[2])) {
104                 return true;
105             } else if (_path.length == 1 && isDirSeparator(_path[0])) {
106                 return true;
107             }
108             return false;
109         }
110         else static assert(0, "unsupported platform");
111     }
112 
113     /// Posix
114     version(Posix) unittest {
115         import dshould;
116         Path("/").isRoot.should.be(true);
117         Path("/some-dir").isRoot.should.be(false);
118         Path("local").isRoot.should.be(false);
119         Path("").isRoot.should.be(false);
120     }
121 
122     /// Windows
123     version(Windows) unittest {
124         import dshould;
125         Path(r"C:\").isRoot.should.be(true);
126         Path(r"D:\").isRoot.should.be(true);
127         Path(r"D:\some-dir").isRoot.should.be(false);
128         Path(r"\").isRoot.should.be(true);
129         Path(r"\local").isRoot.should.be(false);
130         Path("").isRoot.should.be(false);
131     }
132 
133     /// Check if current path is inside other path
134     @safe bool isInside(in Path other) const {
135         // TODO: May be there is better way to check if path
136         //       is inside another path
137         return std.algorithm.startsWith(
138             this.toAbsolute.segments,
139             other.toAbsolute.segments);
140     }
141 
142     ///
143     unittest {
144         import dshould;
145 
146         Path("my", "dir", "42").isInside(Path("my", "dir")).should.be(true);
147         Path("my", "dir", "42").isInside(Path("oth", "dir")).should.be(false);
148     }
149 
150 
151     /** Split path on segments.
152       * Under the hood, this method uses $(REF pathSplitter, std, path)
153       **/
154     @safe pure auto segments() const {
155         return std.path.pathSplitter(_path);
156     }
157 
158     ///
159     unittest {
160         import dshould;
161 
162         Path("t1", "t2", "t3").segments.should.equal(["t1", "t2", "t3"]);
163     }
164 
165     /// Determine if path is file.
166     @safe bool isFile() const {
167         return std.file.isFile(_path.expandTilde);
168     }
169 
170     /// Determine if path is directory.
171     @safe bool isDir() const {
172         return std.file.isDir(_path.expandTilde);
173     }
174 
175     /// Determine if path is symlink
176     @safe bool isSymlink() const {
177         return std.file.isSymlink(_path.expandTilde);
178     }
179 
180     /** Override comparison operators to use OS-specific case-sensitivity
181       * rules. They could be used for sorting of path array for example.
182       **/
183 	@safe pure nothrow int opCmp(in Path other) const
184 	{
185 		return std.path.filenameCmp(this._path, other._path);
186 	}
187 
188 	/// ditto
189 	@safe pure nothrow int opCmp(in ref Path other) const
190 	{
191 		return std.path.filenameCmp(this._path, other._path);
192 	}
193 
194     /// Test comparison operators
195     unittest {
196         import dshould;
197         import std.algorithm: sort;
198         Path[] ap = [
199             Path("a", "d", "c"),
200             Path("a", "c", "e"),
201             Path("g", "a", "d"),
202             Path("ab", "c", "d"),
203         ];
204 
205         ap.sort();
206 
207         // We just compare segments of paths
208         // (to avoid calling code that have to checked by this test in check itself)
209         ap[0].segments.should.equal(Path("a", "c", "e").segments);
210         ap[1].segments.should.equal(Path("a", "d", "c").segments);
211         ap[2].segments.should.equal(Path("ab", "c", "d").segments);
212         ap[3].segments.should.equal(Path("g", "a", "d").segments);
213 
214         ap.sort!("a > b");
215 
216         // We just compare segments of paths
217         // (to avoid calling code that have to checked by this test in check itself)
218         ap[0].segments.should.equal(Path("g", "a", "d").segments);
219         ap[1].segments.should.equal(Path("ab", "c", "d").segments);
220         ap[2].segments.should.equal(Path("a", "d", "c").segments);
221         ap[3].segments.should.equal(Path("a", "c", "e").segments);
222 
223         // Check simple comparisons
224         Path("a", "d", "g").should.be.greater(Path("a", "b", "c"));
225         Path("g", "d", "r").should.be.less(Path("m", "g", "x"));
226     }
227 
228 	/** Override equality comparison operators
229       **/
230     @safe pure nothrow bool opEquals(in Path other) const
231 	{
232 		return opCmp(other) == 0;
233 	}
234 
235 	/// ditto
236 	@safe pure nothrow bool opEquals(in ref Path other) const
237 	{
238 		return opCmp(other) == 0;
239 	}
240 
241     /// Test equality comparisons
242     unittest {
243         import dshould;
244 
245         Path("a", "b").should.equal(Path("a", "b"));
246         Path("a", "b").should.not.equal(Path("a"));
247         Path("a", "b").should.not.equal(Path("a", "b", "c"));
248         Path("a", "b").should.not.equal(Path("a", "c"));
249     }
250 
251     /** Compute hash of the Path to be able to use it as key
252       * in asociative arrays.
253       **/
254     @safe nothrow size_t toHash() const {
255         return typeid(_path).getHash(&_path);
256     }
257 
258     ///
259     unittest {
260         import dshould;
261 
262         string[Path] arr;
263         arr[Path("my", "path")] = "hello";
264         arr[Path("w", "42")] = "world";
265 
266         arr[Path("my", "path")].should.equal("hello");
267         arr[Path("w", "42")].should.equal("world");
268 
269         import core.exception: RangeError;
270         arr[Path("x", "124")].should.throwA!RangeError;
271     }
272 
273     /// Return current path (as absolute path)
274     @safe static Path current() {
275         return Path(".").toAbsolute;
276     }
277 
278     ///
279     unittest {
280         import dshould;
281         Path root = createTempPath();
282         scope(exit) root.remove();
283 
284         // Save current directory
285         auto cdir = std.file.getcwd;
286         scope(exit) std.file.chdir(cdir);
287 
288         // Create directory structure
289         root.join("dir1", "dir2", "dir3").mkdir(true);
290         root.join("dir1", "dir2", "dir3").chdir;
291 
292         // Check that current path is equal to dir1/dir2/dir3 (current dir)
293         version(OSX) {
294             // On OSX we have to resolve symbolic links,
295             // because result of createTempPath contains symmbolic links
296             // for some reason, but current returns path with symlinks resolved
297             Path.current.toString.should.equal(
298                     root.join("dir1", "dir2", "dir3").realPath.toString);
299         } else {
300             Path.current.toString.should.equal(
301                     root.join("dir1", "dir2", "dir3").toString);
302         }
303     }
304 
305     /// Check if path exists
306     @safe nothrow bool exists() const {
307         return std.file.exists(_path.expandTilde);
308     }
309 
310     ///
311     unittest {
312         import dshould;
313 
314         version(Posix) {
315             import std.algorithm: startsWith;
316             // Prepare test dir in user's home directory
317             Path home_tmp = createTempPath("~", "tmp-d-test");
318             scope(exit) home_tmp.remove();
319             Path home_rel = Path("~").join(home_tmp.baseName);
320             home_rel.toString.startsWith("~/tmp-d-test").should.be(true);
321 
322             home_rel.join("test-dir").exists.should.be(false);
323             home_rel.join("test-dir").mkdir;
324             home_rel.join("test-dir").exists.should.be(true);
325 
326             home_rel.join("test-file").exists.should.be(false);
327             home_rel.join("test-file").writeFile("test");
328             home_rel.join("test-file").exists.should.be(true);
329         }
330     }
331 
332     /// Return path as string
333     @safe pure nothrow string toString() const {
334         return _path;
335     }
336 
337 
338     /** Convert path to absolute path.
339       * Returns: new instance of Path that represents current path converted to
340       *          absolute path.
341       *          Also, this method will automatically do tilde expansion and
342       *          normalization of path.
343       * Throws: Exception if the specified base directory is not absolute.
344       **/
345     @safe Path toAbsolute() const {
346         return Path(
347             std.path.buildNormalizedPath(
348                 std.path.absolutePath(_path.expandTilde)));
349     }
350 
351     ///
352     unittest {
353         import dshould;
354 
355         version(Posix) {
356             auto cdir = std.file.getcwd;
357             scope(exit) std.file.chdir(cdir);
358 
359             // Change current working directory to /tmp"
360             std.file.chdir("/tmp");
361 
362             version(OSX) {
363                 // On OSX /tmp is symlink to /private/tmp
364                 Path("/tmp").realPath.should.equal(Path("/private/tmp"));
365                 Path("foo/moo").toAbsolute.toString.should.equal(
366                     "/private/tmp/foo/moo");
367                 Path("../my-path").toAbsolute.toString.should.equal("/private/my-path");
368             } else {
369                 Path("foo/moo").toAbsolute.toString.should.equal("/tmp/foo/moo");
370                 Path("../my-path").toAbsolute.toString.should.equal("/my-path");
371             }
372 
373             Path("/a/path").toAbsolute.toString.should.equal("/a/path");
374 
375             string home_path = "~".expandTilde;
376             home_path[0].should.equal('/');
377 
378             Path("~/my/path").toAbsolute.toString.should.equal("%s/my/path".format(home_path));
379         }
380     }
381 
382     /** Expand tilde (~) in current path.
383       * Returns: New path with tilde expaded
384       **/
385     @safe nothrow Path expandTilde() const {
386         return Path(std.path.expandTilde(_path));
387     }
388 
389     /** Normalize path.
390       * Returns: new normalized Path.
391       **/
392     @safe pure nothrow Path normalize() const {
393         return Path(std.path.buildNormalizedPath(_path));
394     }
395 
396     ///
397     unittest {
398         import dshould;
399 
400         version(Posix) {
401             Path("foo").normalize.toString.should.equal("foo");
402             Path("../foo/../moo").normalize.toString.should.equal("../moo");
403             Path("/foo/./moo/../bar").normalize.toString.should.equal("/foo/bar");
404         }
405     }
406 
407     /** Join multiple path segments and return single path.
408       * Params:
409       *     segments = Array of strings (or Path) to build new path..
410       * Returns:
411       *     New path build from current path and provided segments
412       **/
413     @safe pure nothrow auto join(in string[] segments...) const {
414         auto args=[_path] ~ segments;
415         return Path(std.path.buildPath(args));
416     }
417 
418     /// ditto
419     @safe pure nothrow Path join(in Path[] segments...) const {
420         string[] args=[];
421         foreach(p; segments) args ~= p._path;
422         return this.join(args);
423     }
424 
425     ///
426     unittest {
427         import dshould;
428         string tmp_dir = createTempDirectory();
429         scope(exit) std.file.rmdirRecurse(tmp_dir);
430 
431         auto ps = std.path.dirSeparator;
432 
433         Path("tmp").join("test1", "subdir", "2").toString.should.equal(
434             "tmp" ~ ps ~ "test1" ~ ps ~ "subdir" ~ ps ~ "2");
435 
436         Path root = Path(tmp_dir);
437         root._path.should.equal(tmp_dir);
438         auto test_c_file = root.join("test-create.txt");
439         test_c_file._path.should.equal(tmp_dir ~ ps ~"test-create.txt");
440         test_c_file.isAbsolute.should.be(true);
441 
442         version(Posix) {
443             Path("/").join("test2", "test3").toString.should.equal("/test2/test3");
444         }
445 
446     }
447 
448 
449     /** determine parent path of this path
450       * Returns:
451       *     Absolute Path to parent directory.
452       **/
453     @safe Path parent() const {
454         if (isAbsolute()) {
455             return Path(std.path.dirName(_path));
456         } else {
457             return this.toAbsolute.parent;
458         }
459     }
460 
461     ///
462     unittest {
463         import dshould;
464         version(Posix) {
465             Path("/tmp").parent.toString.should.equal("/");
466             Path("/").parent.toString.should.equal("/");
467             Path("/tmp/parent/child").parent.toString.should.equal("/tmp/parent");
468 
469             Path("parent/child").parent.toString.should.equal(
470                 Path(std.file.getcwd).join("parent").toString);
471 
472             auto cdir = std.file.getcwd;
473             scope(exit) std.file.chdir(cdir);
474 
475             std.file.chdir("/tmp");
476 
477             version(OSX) {
478                 Path("parent/child").parent.toString.should.equal(
479                     "/private/tmp/parent");
480             } else {
481                 Path("parent/child").parent.toString.should.equal(
482                     "/tmp/parent");
483             }
484 
485             Path("~/test-dir").parent.toString.should.equal(
486                 "~".expandTilde);
487         }
488     }
489 
490     /** Return this path as relative to base
491       * Params:
492       *     base = base path to make this path relative to. Must be absolute.
493       * Returns:
494       *     new Path that is relative to base but represent same location
495       *     as this path.
496       * Throws:
497       *     PathException if base path is not valid or not absolute
498       **/
499     @safe pure Path relativeTo(in Path base) const {
500         enforce!PathException(
501             base.isValid && base.isAbsolute,
502             "Base path must be valid and absolute");
503         return Path(std.path.relativePath(_path, base._path));
504     }
505 
506     /// ditto
507     @safe pure Path relativeTo(in string base) const {
508         return relativeTo(Path(base));
509     }
510 
511     ///
512     unittest {
513         import dshould;
514         Path("foo").relativeTo(std.file.getcwd).toString().should.equal("foo");
515 
516         version(Posix) {
517             auto path1 = Path("/foo/root/child/subchild");
518             auto root1 = Path("/foo/root");
519             auto root2 = Path("/moo/root");
520             auto rpath1 = path1.relativeTo(root1);
521 
522             rpath1.toString.should.equal("child/subchild");
523             root2.join(rpath1).toString.should.equal("/moo/root/child/subchild");
524             path1.relativeTo(root2).toString.should.equal("../../foo/root/child/subchild");
525 
526             // Base path must be absolute, so this should throw error
527             Path("~/my/path/1").relativeTo("~/my").should.throwA!PathException;
528         }
529     }
530 
531     /// Returns extension for current path
532     @safe pure nothrow string extension() const {
533         return std.path.extension(_path);
534     }
535 
536     /// Returns base name of current path
537     @safe pure nothrow string baseName() const {
538         return std.path.baseName(_path);
539     }
540 
541     ///
542     unittest {
543         import dshould;
544         Path("foo").baseName.should.equal("foo");
545         Path("foo", "moo").baseName.should.equal("moo");
546         Path("foo", "moo", "test.txt").baseName.should.equal("test.txt");
547     }
548 
549     /// Return size of file specified by path
550     @safe ulong getSize() const {
551         return std.file.getSize(_path.expandTilde);
552     }
553 
554     ///
555     unittest {
556         import dshould;
557         Path root = createTempPath();
558         scope(exit) root.remove();
559 
560         ubyte[4] data = [1, 2, 3, 4];
561         root.join("test-file.txt").writeFile(data);
562         root.join("test-file.txt").getSize.should.equal(4);
563 
564         version(Posix) {
565             // Prepare test dir in user's home directory
566             Path home_tmp = createTempPath("~", "tmp-d-test");
567             scope(exit) home_tmp.remove();
568             string tmp_dir_name = home_tmp.baseName;
569 
570             Path("~/%s/test-file.txt".format(tmp_dir_name)).writeFile(data);
571             Path("~/%s/test-file.txt".format(tmp_dir_name)).getSize.should.equal(4);
572         }
573     }
574 
575     /** Resolve link and return real path.
576       * Available only for posix systems.
577       * If path is not symlink, then return it unchanged
578       **/
579     version(Posix) @safe Path readLink() const {
580         if (isSymlink()) {
581             return Path(std.file.readLink(_path.expandTilde));
582         } else {
583             return this;
584         }
585     }
586 
587     /** Get real path with all symlinks resolved.
588       * If any segment of path is symlink, then this method will automatically
589       * resolve that segment.
590       **/
591     version(Posix) Path realPath() const {
592         import core.sys.posix.stdlib : realpath;
593         import core.stdc.stdlib: free;
594         import std.string: toStringz, fromStringz;
595         import std.exception: errnoEnforce;
596         import std.conv: to;
597 
598         auto conv_path = _path.toStringz;
599         auto result = realpath(conv_path, null);
600         scope (exit) {
601             if (result)
602                 free(result);
603         }
604         // TODO: Better handle errors with different exceptions
605         //       See: https://man7.org/linux/man-pages/man3/realpath.3.html
606         // TODO: Add tests on behavior with broken symlinks
607         errnoEnforce(result, "Path.realPath raise error");
608         return Path(to!(string)(result));
609     }
610 
611 
612     /** Check if path matches specified glob pattern.
613       * See Also:
614       * - https://en.wikipedia.org/wiki/Glob_%28programming%29
615       * - https://dlang.org/phobos/std_path.html#globMatch
616       **/
617     @safe pure nothrow bool matchGlob(in string pattern) {
618         return std.path.globMatch(_path, pattern);
619     }
620 
621     /** Iterate over all files and directories inside path;
622       *
623       * Produces rangs with absolute paths found inside specific directory
624       *
625       * Params:
626       *     mode = The way to traverse directories. See [docs](https://dlang.org/phobos/std_file.html#SpanMode)
627       *     followSymlink = do we need to follow symlinks of not. By default set to True.
628       *
629       * Examples:
630       * ---
631       * // Iterate over paths in current directory
632       * foreach (p; Path.current.walk(SpanMode.breadth)) {
633       *     if (p.isFile)
634       *         writeln(p);
635       * ---
636       **/
637     auto walk(in SpanMode mode=SpanMode.shallow, bool followSymlink=true) const {
638         import std.algorithm.iteration: map;
639         return std.file.dirEntries(
640             this.toAbsolute._path, mode, followSymlink).map!(a => Path(a));
641     }
642 
643     ///
644     unittest {
645         import dshould;
646         Path root = createTempPath();
647         scope(exit) root.remove();
648 
649         // Create sample directory structure
650         root.join("d1", "d2").mkdir(true);
651         root.join("d1", "test1.txt").writeFile("Test 1");
652         root.join("d1", "d2", "test2.txt").writeFile("Test 2");
653 
654         // Walk through the derectory d1
655         Path[] result;
656         foreach(p; root.join("d1").walk(SpanMode.breadth)) {
657             result ~= p;
658         }
659 
660         import std.algorithm: sort;
661         import std.array: array;
662 
663         result.sort.array.should.equal([
664             root.join("d1", "d2"),
665             root.join("d1", "d2", "test2.txt"),
666             root.join("d1", "test1.txt"),
667         ]);
668     }
669 
670     /// Walk inside tilda-expandable path
671     version(Posix) unittest {
672         import dshould;
673         import std.algorithm: startsWith;
674 
675         // Prepare test dir in user's home directory
676         Path root = createTempPath("~", "tmp-d-test");
677         scope(exit) root.remove();
678 
679         Path hroot = Path("~").join(root.relativeTo(std.path.expandTilde("~")));
680         hroot._path.startsWith("~").should.be(true);
681 
682         // Create sample directory structure
683         hroot.join("d1", "d2").mkdir(true);
684         hroot.join("d1", "test1.txt").writeFile("Test 1");
685         hroot.join("d1", "d2", "test2.txt").writeFile("Test 2");
686 
687         // Walk through the derectory d1
688         Path[] result;
689         foreach(p; hroot.join("d1").walk(SpanMode.breadth)) {
690             result ~= p;
691         }
692 
693         import std.algorithm: sort;
694         import std.array: array;
695 
696         result.sort.array.should.equal([
697             root.join("d1", "d2"),
698             root.join("d1", "d2", "test2.txt"),
699             root.join("d1", "test1.txt"),
700         ]);
701     }
702 
703     /// Just an alias for walk(SpanModel.depth)
704     auto walkDepth(bool followSymlink=true) const {
705         return walk(SpanMode.depth, followSymlink);
706     }
707 
708     /// Just an alias for walk(SpanModel.breadth)
709     auto walkBreadth(bool followSymlink=true) const {
710         return walk(SpanMode.breadth, followSymlink);
711     }
712 
713     /** Search files that match provided glob pattern inside current path.
714       *
715       * Params:
716       *     pattern = The glob pattern to apply to paths inside current dir.
717       *     mode = The way to traverse directories. See [docs](https://dlang.org/phobos/std_file.html#SpanMode)
718       *     followSymlink = do we need to follow symlinks of not. By default set to True.
719       * Returns:
720       *     Range of absolute path inside specified directory, that match
721       *     specified glob pattern.
722       **/
723     auto glob(in string pattern,
724             in SpanMode mode=SpanMode.shallow,
725             bool followSymlink=true) {
726         import std.algorithm.iteration: filter;
727         Path base = this.toAbsolute;
728         return base.walk(mode, followSymlink).filter!(
729             f => f.relativeTo(base).matchGlob(pattern));
730     }
731 
732     ///
733     unittest {
734         import dshould;
735         import std.array: array;
736         import std.algorithm: sort;
737         Path root = createTempPath();
738         scope(exit) root.remove();
739 
740         // Create sample directory structure
741         root.join("d1").mkdir(true);
742         root.join("d1", "d2").mkdir(true);
743         root.join("d1", "test1.txt").writeFile("Test 1");
744         root.join("d1", "test2.txt").writeFile("Test 2");
745         root.join("d1", "test3.py").writeFile("print('Test 3')");
746         root.join("d1", "d2", "test4.py").writeFile("print('Test 4')");
747         root.join("d1", "d2", "test5.py").writeFile("print('Test 5')");
748         root.join("d1", "d2", "test6.txt").writeFile("print('Test 6')");
749 
750         // Find py files in directory d1
751         root.join("d1").glob("*.py").array.should.equal([
752             root.join("d1", "test3.py"),
753         ]);
754 
755         // Find py files in directory d1 recursively
756         root.join("d1").glob("*.py", SpanMode.breadth).array.sort.array.should.equal([
757             root.join("d1", "d2", "test4.py"),
758             root.join("d1", "d2", "test5.py"),
759             root.join("d1", "test3.py"),
760         ]);
761 
762         // Find py files in directory d1 recursively
763         root.join("d1").glob("*.txt", SpanMode.breadth).array.sort.array.should.equal([
764             root.join("d1", "d2", "test6.txt"),
765             root.join("d1", "test1.txt"),
766             root.join("d1", "test2.txt"),
767         ]);
768     }
769 
770     /// Change current working directory to this.
771     @safe void chdir() const {
772         std.file.chdir(_path.expandTilde);
773     }
774 
775     /** Change current working directory to path inside currect path
776       *
777       * Params:
778       *     sub_path = relative path inside this, to change directory to
779       **/
780     @safe void chdir(in string[] sub_path...) const
781     in {
782         assert(
783             sub_path.length > 0,
784             "at least one path segment have to be provided");
785         assert(
786             !std.path.isAbsolute(sub_path[0]),
787             "sub_path must not be absolute");
788         version(Posix) assert(
789             !std.algorithm.startsWith(sub_path[0], "~"),
790             "sub_path must not start with '~' to " ~
791             "avoid automatic tilde expansion!");
792     } do {
793         this.join(sub_path).chdir();
794     }
795 
796     /// ditto
797     @safe void chdir(in Path sub_path) const
798     in {
799         assert(
800             !sub_path.isAbsolute,
801             "sub_path must not be absolute");
802         version(Posix) assert(
803             !std.algorithm.startsWith(sub_path._path, "~"),
804             "sub_path must not start with '~' to " ~
805             "avoid automatic tilde expansion!");
806     } do {
807         this.join(sub_path).chdir();
808     }
809 
810     ///
811     unittest {
812         import dshould;
813         auto cdir = std.file.getcwd;
814         Path root = createTempPath();
815         scope(exit) {
816             std.file.chdir(cdir);
817             root.remove();
818         }
819 
820         std.file.getcwd.should.not.equal(root._path);
821         root.chdir;
822         version(OSX) {
823             std.file.getcwd.should.equal(root.realPath._path);
824         } else {
825             std.file.getcwd.should.equal(root._path);
826         }
827 
828         version(Posix) {
829             // Prepare test dir in user's home directory
830             Path home_tmp = createTempPath("~", "tmp-d-test");
831             scope(exit) home_tmp.remove();
832             string tmp_dir_name = home_tmp.baseName;
833             std.file.getcwd.should.not.equal(home_tmp._path);
834 
835             // Change current working directory to tmp-dir-name
836             Path("~", tmp_dir_name).chdir;
837             std.file.getcwd.should.equal(home_tmp._path);
838         }
839     }
840 
841     ///
842     unittest {
843         import dshould;
844         auto cdir = std.file.getcwd;
845         Path root = createTempPath();
846         scope(exit) {
847             std.file.chdir(cdir);
848             root.remove();
849         }
850 
851         // Create some directories
852         root.join("my-dir", "some-dir", "some-sub-dir").mkdir(true);
853         root.join("my-dir", "other-dir").mkdir(true);
854 
855         // Check current path is not equal to root
856         version (OSX) {
857             Path.current.should.not.equal(root.realPath);
858         } else {
859             Path.current.should.not.equal(root);
860         }
861 
862         // Change current working directory to test root, and check that it
863         // was changed
864         root.chdir;
865         version (OSX) {
866             Path.current.should.equal(root.realPath);
867         } else {
868             Path.current.should.equal(root);
869         }
870 
871         // Try to change current working directory to "my-dir" inside our
872         // test root dir
873         root.chdir("my-dir");
874         version (OSX) {
875             Path.current.should.equal(root.join("my-dir").realPath);
876         } else {
877             Path.current.should.equal(root.join("my-dir"));
878         }
879 
880         // Try to change current dir to some-sub-dir, and check if it works
881         root.chdir(Path("my-dir", "some-dir", "some-sub-dir"));
882 
883         version(OSX) {
884             Path.current.should.equal(
885                 root.join("my-dir", "some-dir", "some-sub-dir").realPath);
886         } else {
887             Path.current.should.equal(
888                 root.join("my-dir", "some-dir", "some-sub-dir"));
889         }
890     }
891 
892     /** Copy single file to destination.
893       * If destination does not exists,
894       * then file will be copied exactly to that path.
895       * If destination already exists and it is directory, then method will
896       * try to copy file inside that directory with same name.
897       * If destination already exists and it is file,
898       * then depending on `rewrite` param file will be owerwritten or
899       * PathException will be thrown.
900       * Params:
901       *     dest = destination path to copy file to. Could be new file path,
902       *            or directory where to copy file.
903       *     rewrite = do we need to rewrite file if it already exists?
904       * Throws:
905       *     PathException if source file does not exists or
906       *         if destination already exists and
907       *         it is not a directory and rewrite is set to false.
908       **/
909     @safe void copyFileTo(in Path dest, in bool rewrite=false) const {
910         enforce!PathException(
911             this.exists,
912             "Cannot Copy! Source file %s does not exists!".format(_path));
913         if (dest.exists) {
914             if (dest.isDir) {
915                 this.copyFileTo(dest.join(this.baseName), rewrite);
916             } else if (!rewrite) {
917                 throw new PathException(
918                         "Cannot copy! Destination file %s already exists!".format(dest._path));
919             } else {
920                 std.file.copy(_path, dest._path);
921             }
922         } else {
923             std.file.copy(_path, dest._path);
924         }
925     }
926 
927     ///
928     unittest {
929         import dshould;
930 
931         // Prepare temporary path for test
932         auto cdir = std.file.getcwd;
933         Path root = createTempPath();
934         scope(exit) {
935             std.file.chdir(cdir);
936             root.remove();
937         }
938 
939         // Create test directory structure
940         root.join("test-file.txt").writeFile("test");
941         root.join("test-file-2.txt").writeFile("test-2");
942         root.join("test-dst-dir").mkdir;
943 
944         // Test copy file by path
945         root.join("test-dst-dir", "test1.txt").exists.should.be(false);
946         root.join("test-file.txt").copyFileTo(root.join("test-dst-dir", "test1.txt"));
947         root.join("test-dst-dir", "test1.txt").exists.should.be(true);
948 
949         // Test copy file by path with rewrite
950         root.join("test-dst-dir", "test1.txt").readFile.should.equal("test");
951         root.join("test-file-2.txt").copyFileTo(root.join("test-dst-dir", "test1.txt")).should.throwA!PathException;
952         root.join("test-file-2.txt").copyFileTo(root.join("test-dst-dir", "test1.txt"), true);
953         root.join("test-dst-dir", "test1.txt").readFile.should.equal("test-2");
954 
955         // Test copy file inside dir
956         root.join("test-dst-dir", "test-file.txt").exists.should.be(false);
957         root.join("test-file.txt").copyFileTo(root.join("test-dst-dir"));
958         root.join("test-dst-dir", "test-file.txt").exists.should.be(true);
959 
960         // Test copy file inside dir with rewrite
961         root.join("test-file.txt").writeFile("test-42");
962         root.join("test-dst-dir", "test-file.txt").readFile.should.equal("test");
963         root.join("test-file.txt").copyFileTo(root.join("test-dst-dir")).should.throwA!PathException;
964         root.join("test-file.txt").copyFileTo(root.join("test-dst-dir"), true);
965         root.join("test-dst-dir", "test-file.txt").readFile.should.equal("test-42");
966     }
967 
968     /** Copy file or directory to destination
969       * If source is a file, then copyFileTo will be use to copy it.
970       * If source is a directory, then more complex logic will be applied:
971       *
972       * - if dest already exists and it is not dir,
973       *   then exception will be raised.
974       * - if dest already exists and it is dir,
975       *   then source dir will be copied inside that dir with it's name
976       * - if dest does not exists,
977       *   then current directory will be copied to dest path.
978       *
979       * Note, that work with symlinks have to be improved. Not tested yet.
980       *
981       * Params:
982       *     dest = destination path to copy content of this.
983       * Throws:
984       *     PathException when cannot copy
985       **/
986     void copyTo(in Path dest) const {
987         import std.stdio;
988         if (isDir) {
989             Path dst_root = dest.toAbsolute;
990             if (dst_root.exists) {
991                 enforce!PathException(
992                     dst_root.isDir,
993                     "Cannot copy! Destination %s already exists and it is not directory!".format(dst_root));
994                 dst_root = dst_root.join(this.baseName);
995                 enforce!PathException(
996                     !dst_root.exists,
997                     "Cannot copy! Destination %s already exists!".format(dst_root));
998             }
999             std.file.mkdirRecurse(dst_root._path);
1000             auto src_root = this.toAbsolute();
1001             foreach (Path src; src_root.walk(SpanMode.breadth)) {
1002                 enforce!PathException(
1003                     src.isFile || src.isDir,
1004                     "Cannot copy %s: it is not file nor directory.");
1005                 auto dst = dst_root.join(src.relativeTo(src_root));
1006                 if (src.isFile)
1007                     std.file.copy(src._path, dst._path);
1008                 else
1009                     std.file.mkdirRecurse(dst._path);
1010             }
1011         } else {
1012             copyFileTo(dest);
1013         }
1014     }
1015 
1016     /// ditto
1017     void copyTo(in string dest) const {
1018         copyTo(Path(dest));
1019     }
1020 
1021     ///
1022     unittest {
1023         import dshould;
1024         auto cdir = std.file.getcwd;
1025         Path root = createTempPath();
1026         scope(exit) {
1027             std.file.chdir(cdir);
1028             root.remove();
1029         }
1030 
1031         auto test_c_file = root.join("test-create.txt");
1032 
1033         // Create test file to copy
1034         test_c_file.exists.should.be(false);
1035         test_c_file.writeFile("Hello World");
1036         test_c_file.exists.should.be(true);
1037 
1038         // Test copy file when dest dir does not exists
1039         test_c_file.copyTo(
1040             root.join("test-copy-dst", "test.txt")
1041         ).should.throwA!(std.file.FileException);
1042 
1043         // Test copy file where dest dir exists and dest name specified
1044         root.join("test-copy-dst").exists().should.be(false);
1045         root.join("test-copy-dst").mkdir();
1046         root.join("test-copy-dst").exists().should.be(true);
1047         root.join("test-copy-dst", "test.txt").exists.should.be(false);
1048         test_c_file.copyTo(root.join("test-copy-dst", "test.txt"));
1049         root.join("test-copy-dst", "test.txt").exists.should.be(true);
1050 
1051         // Try to copy file when it is already exists in dest folder
1052         test_c_file.copyTo(
1053             root.join("test-copy-dst", "test.txt")
1054         ).should.throwA!PathException;
1055 
1056         // Try to copy file, when only dirname specified
1057         root.join("test-copy-dst", "test-create.txt").exists.should.be(false);
1058         test_c_file.copyTo(root.join("test-copy-dst"));
1059         root.join("test-copy-dst", "test-create.txt").exists.should.be(true);
1060 
1061         // Try to copy empty directory with its content
1062         root.join("test-copy-dir-empty").mkdir;
1063         root.join("test-copy-dir-empty").exists.should.be(true);
1064         root.join("test-copy-dir-empty-cpy").exists.should.be(false);
1065         root.join("test-copy-dir-empty").copyTo(
1066             root.join("test-copy-dir-empty-cpy"));
1067         root.join("test-copy-dir-empty").exists.should.be(true);
1068         root.join("test-copy-dir-empty-cpy").exists.should.be(true);
1069 
1070         // Create test dir with content to test copying non-empty directory
1071         root.join("test-dir").mkdir();
1072         root.join("test-dir", "f1.txt").writeFile("f1");
1073         root.join("test-dir", "d2").mkdir();
1074         root.join("test-dir", "d2", "f2.txt").writeFile("f2");
1075 
1076         // Test that test-dir content created
1077         root.join("test-dir").exists.should.be(true);
1078         root.join("test-dir").isDir.should.be(true);
1079         root.join("test-dir", "f1.txt").exists.should.be(true);
1080         root.join("test-dir", "f1.txt").isFile.should.be(true);
1081         root.join("test-dir", "d2").exists.should.be(true);
1082         root.join("test-dir", "d2").isDir.should.be(true);
1083         root.join("test-dir", "d2", "f2.txt").exists.should.be(true);
1084         root.join("test-dir", "d2", "f2.txt").isFile.should.be(true);
1085 
1086         // Copy non-empty dir to unexisting location
1087         root.join("test-dir-cpy-1").exists.should.be(false);
1088         root.join("test-dir").copyTo(root.join("test-dir-cpy-1"));
1089 
1090         // Test that dir copied successfully
1091         root.join("test-dir-cpy-1").exists.should.be(true);
1092         root.join("test-dir-cpy-1").isDir.should.be(true);
1093         root.join("test-dir-cpy-1", "f1.txt").exists.should.be(true);
1094         root.join("test-dir-cpy-1", "f1.txt").isFile.should.be(true);
1095         root.join("test-dir-cpy-1", "d2").exists.should.be(true);
1096         root.join("test-dir-cpy-1", "d2").isDir.should.be(true);
1097         root.join("test-dir-cpy-1", "d2", "f2.txt").exists.should.be(true);
1098         root.join("test-dir-cpy-1", "d2", "f2.txt").isFile.should.be(true);
1099 
1100         // Copy non-empty dir to existing location
1101         root.join("test-dir-cpy-2").exists.should.be(false);
1102         root.join("test-dir-cpy-2").mkdir;
1103         root.join("test-dir-cpy-2").exists.should.be(true);
1104 
1105         // Copy directory to already existing dir
1106         root.join("test-dir").copyTo(root.join("test-dir-cpy-2"));
1107 
1108         // Test that dir copied successfully
1109         root.join("test-dir-cpy-2", "test-dir").exists.should.be(true);
1110         root.join("test-dir-cpy-2", "test-dir").isDir.should.be(true);
1111         root.join("test-dir-cpy-2", "test-dir", "f1.txt").exists.should.be(true);
1112         root.join("test-dir-cpy-2", "test-dir", "f1.txt").isFile.should.be(true);
1113         root.join("test-dir-cpy-2", "test-dir", "d2").exists.should.be(true);
1114         root.join("test-dir-cpy-2", "test-dir", "d2").isDir.should.be(true);
1115         root.join("test-dir-cpy-2", "test-dir", "d2", "f2.txt").exists.should.be(true);
1116         root.join("test-dir-cpy-2", "test-dir", "d2", "f2.txt").isFile.should.be(true);
1117 
1118         // Try again to copy non-empty dir to already existing dir
1119         // where dir with same base name already exists
1120         root.join("test-dir").copyTo(root.join("test-dir-cpy-2")).should.throwA!PathException;
1121 
1122 
1123         // Change dir to our temp directory and test copying using
1124         // relative paths
1125         root.chdir;
1126 
1127         // Copy content using relative paths
1128         root.join("test-dir-cpy-3").exists.should.be(false);
1129         Path("test-dir-cpy-3").exists.should.be(false);
1130         Path("test-dir").copyTo("test-dir-cpy-3");
1131 
1132         // Test that content was copied in right way
1133         root.join("test-dir-cpy-3").exists.should.be(true);
1134         root.join("test-dir-cpy-3").isDir.should.be(true);
1135         root.join("test-dir-cpy-3", "f1.txt").exists.should.be(true);
1136         root.join("test-dir-cpy-3", "f1.txt").isFile.should.be(true);
1137         root.join("test-dir-cpy-3", "d2").exists.should.be(true);
1138         root.join("test-dir-cpy-3", "d2").isDir.should.be(true);
1139         root.join("test-dir-cpy-3", "d2", "f2.txt").exists.should.be(true);
1140         root.join("test-dir-cpy-3", "d2", "f2.txt").isFile.should.be(true);
1141 
1142         // Try to copy to already existing file
1143         root.join("test-dir-cpy-4").writeFile("Test");
1144 
1145         // Expect error
1146         root.join("test-dir").copyTo("test-dir-cpy-4").should.throwA!PathException;
1147 
1148         version(Posix) {
1149             // Prepare test dir in user's home directory
1150             Path home_tmp = createTempPath("~", "tmp-d-test");
1151             scope(exit) home_tmp.remove();
1152 
1153             // Test if home_tmp created in right way and ensure that
1154             // dest for copy dir does not exists
1155             home_tmp.parent.toString.should.equal(std.path.expandTilde("~"));
1156             home_tmp.isAbsolute.should.be(true);
1157             home_tmp.join("test-dir").exists.should.be(false);
1158 
1159             // Copy test-dir to home_tmp
1160             import std.algorithm: startsWith;
1161             auto home_tmp_rel = home_tmp.baseName;
1162             string home_tmp_tilde = "~/%s".format(home_tmp_rel);
1163             home_tmp_tilde.startsWith("~/tmp-d-test").should.be(true);
1164             root.join("test-dir").copyTo(home_tmp_tilde);
1165 
1166             // Test that content was copied in right way
1167             home_tmp.join("test-dir").exists.should.be(true);
1168             home_tmp.join("test-dir").isDir.should.be(true);
1169             home_tmp.join("test-dir", "f1.txt").exists.should.be(true);
1170             home_tmp.join("test-dir", "f1.txt").isFile.should.be(true);
1171             home_tmp.join("test-dir", "d2").exists.should.be(true);
1172             home_tmp.join("test-dir", "d2").isDir.should.be(true);
1173             home_tmp.join("test-dir", "d2", "f2.txt").exists.should.be(true);
1174             home_tmp.join("test-dir", "d2", "f2.txt").isFile.should.be(true);
1175         }
1176     }
1177 
1178     /// Test behavior with symlinks
1179     version(Posix) unittest {
1180         import dshould;
1181         auto cdir = std.file.getcwd;
1182         Path root = createTempPath();
1183         scope(exit) {
1184             std.file.chdir(cdir);
1185             root.remove();
1186         }
1187 
1188         // Create test dir with content to test copying non-empty directory
1189         root.join("test-dir").mkdir();
1190         root.join("test-dir", "f1.txt").writeFile("f1");
1191         root.join("test-dir", "d2").mkdir();
1192         root.join("test-dir", "d2", "f2.txt").writeFile("f2");
1193         root.join("test-dir", "d2").symlink(root.join("test-dir", "d3-s"));
1194         root.join("test-dir", "d2", "f2.txt").symlink(
1195             root.join("test-dir", "f3.txt"));
1196 
1197 
1198         // Test that test-dir content created
1199         root.join("test-dir").exists.should.be(true);
1200         root.join("test-dir").isDir.should.be(true);
1201         root.join("test-dir", "f1.txt").exists.should.be(true);
1202         root.join("test-dir", "f1.txt").isFile.should.be(true);
1203         root.join("test-dir", "d2").exists.should.be(true);
1204         root.join("test-dir", "d2").isDir.should.be(true);
1205         root.join("test-dir", "d2").isSymlink.should.be(false);
1206         root.join("test-dir", "d2", "f2.txt").exists.should.be(true);
1207         root.join("test-dir", "d2", "f2.txt").isFile.should.be(true);
1208         root.join("test-dir", "d3-s").exists.should.be(true);
1209         root.join("test-dir", "d3-s").isDir.should.be(true);
1210         root.join("test-dir", "d3-s").isSymlink.should.be(true);
1211         root.join("test-dir", "f3.txt").exists.should.be(true);
1212         root.join("test-dir", "f3.txt").isFile.should.be(true);
1213         root.join("test-dir", "f3.txt").isSymlink.should.be(true);
1214 
1215         // Copy non-empty dir to unexisting location
1216         root.join("test-dir-cpy-1").exists.should.be(false);
1217         root.join("test-dir").copyTo(root.join("test-dir-cpy-1"));
1218 
1219         // Test that dir copied successfully
1220         root.join("test-dir-cpy-1").exists.should.be(true);
1221         root.join("test-dir-cpy-1").isDir.should.be(true);
1222         root.join("test-dir-cpy-1", "f1.txt").exists.should.be(true);
1223         root.join("test-dir-cpy-1", "f1.txt").isFile.should.be(true);
1224         root.join("test-dir-cpy-1", "d2").exists.should.be(true);
1225         root.join("test-dir-cpy-1", "d2").isDir.should.be(true);
1226         root.join("test-dir-cpy-1", "d2", "f2.txt").exists.should.be(true);
1227         root.join("test-dir-cpy-1", "d2", "f2.txt").isFile.should.be(true);
1228         root.join("test-dir-cpy-1", "d3-s").exists.should.be(true);
1229         root.join("test-dir-cpy-1", "d3-s").isDir.should.be(true);
1230         root.join("test-dir-cpy-1", "d3-s").isSymlink.should.be(false);
1231         root.join("test-dir-cpy-1", "f3.txt").exists.should.be(true);
1232         root.join("test-dir-cpy-1", "f3.txt").isFile.should.be(true);
1233         root.join("test-dir-cpy-1", "f3.txt").isSymlink.should.be(false);
1234         root.join("test-dir-cpy-1", "f3.txt").readFileText.should.equal("f2");
1235     }
1236 
1237     /** Remove file or directory referenced by this path.
1238       * This operation is recursive, so if path references to a direcotry,
1239       * then directory itself and all content inside referenced dir will be
1240       * removed
1241       **/
1242     @safe void remove() const {
1243         // TODO: Implement in better way
1244         //       Implemented in this way, because isFile and isDir on broken
1245         //       symlink raises error.
1246         version(Posix) {
1247             // This approach does not work on windows
1248             if (isSymlink || isFile) std.file.remove(_path.expandTilde);
1249             else std.file.rmdirRecurse(_path.expandTilde);
1250         } else {
1251             if (isDir) std.file.rmdirRecurse(_path.expandTilde);
1252             else std.file.remove(_path.expandTilde);
1253         }
1254     }
1255 
1256     ///
1257     unittest {
1258         import dshould;
1259         Path root = createTempPath();
1260         scope(exit) root.remove();
1261 
1262         // Try to remove unexisting file
1263         root.join("unexising-file.txt").remove.should.throwA!(std.file.FileException);
1264 
1265         // Try to remove file
1266         root.join("test-file.txt").exists.should.be(false);
1267         root.join("test-file.txt").writeFile("test");
1268         root.join("test-file.txt").exists.should.be(true);
1269         root.join("test-file.txt").remove();
1270         root.join("test-file.txt").exists.should.be(false);
1271 
1272         // Create test dir with contents
1273         root.join("test-dir").mkdir();
1274         root.join("test-dir", "f1.txt").writeFile("f1");
1275         root.join("test-dir", "d2").mkdir();
1276         root.join("test-dir", "d2", "f2.txt").writeFile("f2");
1277 
1278         // Ensure test dir with contents created
1279         root.join("test-dir").exists.should.be(true);
1280         root.join("test-dir").isDir.should.be(true);
1281         root.join("test-dir", "f1.txt").exists.should.be(true);
1282         root.join("test-dir", "f1.txt").isFile.should.be(true);
1283         root.join("test-dir", "d2").exists.should.be(true);
1284         root.join("test-dir", "d2").isDir.should.be(true);
1285         root.join("test-dir", "d2", "f2.txt").exists.should.be(true);
1286         root.join("test-dir", "d2", "f2.txt").isFile.should.be(true);
1287 
1288         // Remove test directory
1289         root.join("test-dir").remove();
1290 
1291         // Ensure directory was removed
1292         root.join("test-dir").exists.should.be(false);
1293         root.join("test-dir", "f1.txt").exists.should.be(false);
1294         root.join("test-dir", "d2").exists.should.be(false);
1295         root.join("test-dir", "d2", "f2.txt").exists.should.be(false);
1296 
1297 
1298         version(Posix) {
1299             // Prepare test dir in user's home directory
1300             Path home_tmp = createTempPath("~", "tmp-d-test");
1301             scope(exit) home_tmp.remove();
1302 
1303             // Create test dir with contents
1304             home_tmp.join("test-dir").mkdir();
1305             home_tmp.join("test-dir", "f1.txt").writeFile("f1");
1306             home_tmp.join("test-dir", "d2").mkdir();
1307             home_tmp.join("test-dir", "d2", "f2.txt").writeFile("f2");
1308 
1309             // Remove created directory
1310             Path("~").join(home_tmp.baseName).toAbsolute.toString.should.equal(home_tmp.toString);
1311             Path("~").join(home_tmp.baseName, "test-dir").remove();
1312 
1313             // Ensure directory was removed
1314             home_tmp.join("test-dir").exists.should.be(false);
1315             home_tmp.join("test-dir", "f1.txt").exists.should.be(false);
1316             home_tmp.join("test-dir", "d2").exists.should.be(false);
1317             home_tmp.join("test-dir", "d2", "f2.txt").exists.should.be(false);
1318         }
1319     }
1320 
1321     /// Test removing broken symlink
1322     version(Posix) unittest {
1323         import dshould;
1324         Path root = createTempPath();
1325         scope(exit) root.remove();
1326 
1327         // Try to create test file
1328         root.join("test-file.txt").exists.should.be(false);
1329         root.join("test-file.txt").writeFile("test");
1330         root.join("test-file.txt").exists.should.be(true);
1331 
1332         // Create symlink to that file
1333         root.join("test-file.txt").symlink(root.join("test-symlink.txt"));
1334         root.join("test-symlink.txt").exists.should.be(true);
1335 
1336         // Delete original file
1337         root.join("test-file.txt").remove();
1338 
1339         // Check that file was deleted, but symlink still exists
1340         root.join("test-file.txt").exists.should.be(false);
1341         root.join("test-symlink.txt").exists.should.be(true);
1342 
1343         // Delete symlink
1344         root.join("test-symlink.txt").remove();
1345 
1346         // Test that symlink was deleted too
1347         root.join("test-file.txt").exists.should.be(false);
1348         root.join("test-symlink.txt").exists.should.be(false);
1349     }
1350 
1351     /** Rename current path.
1352       *
1353       * Note: case of moving file/dir between filesystesm is not tested.
1354       *
1355       * Throws:
1356       *     PathException when destination already exists
1357       **/
1358     @safe void rename(in Path to) const {
1359         // TODO: Add support to move files between filesystems
1360         enforce!PathException(
1361             !to.exists,
1362             "Destination %s already exists!".format(to));
1363         return std.file.rename(_path.expandTilde, to._path.expandTilde);
1364     }
1365 
1366     /// ditto
1367     @safe void rename(in string to) const {
1368         return rename(Path(to));
1369     }
1370 
1371     ///
1372     unittest {
1373         import dshould;
1374         Path root = createTempPath();
1375         scope(exit) root.remove();
1376 
1377         // Create file
1378         root.join("test-file.txt").exists.should.be(false);
1379         root.join("test-file-new.txt").exists.should.be(false);
1380         root.join("test-file.txt").writeFile("test");
1381         root.join("test-file.txt").exists.should.be(true);
1382         root.join("test-file-new.txt").exists.should.be(false);
1383 
1384         // Rename file
1385         root.join("test-file.txt").exists.should.be(true);
1386         root.join("test-file-new.txt").exists.should.be(false);
1387         root.join("test-file.txt").rename(root.join("test-file-new.txt"));
1388         root.join("test-file.txt").exists.should.be(false);
1389         root.join("test-file-new.txt").exists.should.be(true);
1390 
1391         // Try to move file to existing directory
1392         root.join("my-dir").mkdir;
1393         root.join("test-file-new.txt").rename(root.join("my-dir")).should.throwA!PathException;
1394 
1395         // Try to rename one olready existing dir to another
1396         root.join("other-dir").mkdir;
1397         root.join("my-dir").exists.should.be(true);
1398         root.join("other-dir").exists.should.be(true);
1399         root.join("my-dir").rename(root.join("other-dir")).should.throwA!PathException;
1400 
1401         // Create test dir with contents
1402         root.join("test-dir").mkdir();
1403         root.join("test-dir", "f1.txt").writeFile("f1");
1404         root.join("test-dir", "d2").mkdir();
1405         root.join("test-dir", "d2", "f2.txt").writeFile("f2");
1406 
1407         // Ensure test dir with contents created
1408         root.join("test-dir").exists.should.be(true);
1409         root.join("test-dir").isDir.should.be(true);
1410         root.join("test-dir", "f1.txt").exists.should.be(true);
1411         root.join("test-dir", "f1.txt").isFile.should.be(true);
1412         root.join("test-dir", "d2").exists.should.be(true);
1413         root.join("test-dir", "d2").isDir.should.be(true);
1414         root.join("test-dir", "d2", "f2.txt").exists.should.be(true);
1415         root.join("test-dir", "d2", "f2.txt").isFile.should.be(true);
1416 
1417         // Try to rename directory
1418         root.join("test-dir").rename(root.join("test-dir-new"));
1419 
1420         // Ensure old dir does not exists anymore
1421         root.join("test-dir").exists.should.be(false);
1422         root.join("test-dir", "f1.txt").exists.should.be(false);
1423         root.join("test-dir", "d2").exists.should.be(false);
1424         root.join("test-dir", "d2", "f2.txt").exists.should.be(false);
1425 
1426         // Ensure test dir was renamed successfully
1427         root.join("test-dir-new").exists.should.be(true);
1428         root.join("test-dir-new").isDir.should.be(true);
1429         root.join("test-dir-new", "f1.txt").exists.should.be(true);
1430         root.join("test-dir-new", "f1.txt").isFile.should.be(true);
1431         root.join("test-dir-new", "d2").exists.should.be(true);
1432         root.join("test-dir-new", "d2").isDir.should.be(true);
1433         root.join("test-dir-new", "d2", "f2.txt").exists.should.be(true);
1434         root.join("test-dir-new", "d2", "f2.txt").isFile.should.be(true);
1435 
1436 
1437         version(Posix) {
1438             // Prepare test dir in user's home directory
1439             Path home_tmp = createTempPath("~", "tmp-d-test");
1440             scope(exit) home_tmp.remove();
1441 
1442             // Ensure that there is no test dir in our home/based temp dir;
1443             home_tmp.join("test-dir").exists.should.be(false);
1444             home_tmp.join("test-dir", "f1.txt").exists.should.be(false);
1445             home_tmp.join("test-dir", "d2").exists.should.be(false);
1446             home_tmp.join("test-dir", "d2", "f2.txt").exists.should.be(false);
1447 
1448             root.join("test-dir-new").rename(
1449                     Path("~").join(home_tmp.baseName, "test-dir"));
1450 
1451             // Ensure test dir was renamed successfully
1452             home_tmp.join("test-dir").exists.should.be(true);
1453             home_tmp.join("test-dir").isDir.should.be(true);
1454             home_tmp.join("test-dir", "f1.txt").exists.should.be(true);
1455             home_tmp.join("test-dir", "f1.txt").isFile.should.be(true);
1456             home_tmp.join("test-dir", "d2").exists.should.be(true);
1457             home_tmp.join("test-dir", "d2").isDir.should.be(true);
1458             home_tmp.join("test-dir", "d2", "f2.txt").exists.should.be(true);
1459             home_tmp.join("test-dir", "d2", "f2.txt").isFile.should.be(true);
1460         }
1461     }
1462 
1463     /** Create directory by this path
1464       * Params:
1465       *     recursive = if set to true, then
1466       *         parent directories will be created if not exist
1467       * Throws:
1468       *     FileException if cannot create dir (it already exists)
1469       **/
1470     @safe void mkdir(in bool recursive=false) const {
1471         if (recursive) std.file.mkdirRecurse(std.path.expandTilde(_path));
1472         else std.file.mkdir(std.path.expandTilde(_path));
1473     }
1474 
1475     ///
1476     unittest {
1477         import dshould;
1478         Path root = createTempPath();
1479         scope(exit) root.remove();
1480 
1481         root.join("test-dir").exists.should.be(false);
1482         root.join("test-dir", "subdir").exists.should.be(false);
1483 
1484         version(Posix) {
1485             root.join("test-dir", "subdir").mkdir().should.throwA!(
1486                 std.file.FileException);
1487         } else {
1488             import std.windows.syserror;
1489             root.join("test-dir", "subdir").mkdir().should.throwA!(
1490                 WindowsException);
1491         }
1492 
1493         root.join("test-dir").mkdir();
1494         root.join("test-dir").exists.should.be(true);
1495         root.join("test-dir", "subdir").exists.should.be(false);
1496 
1497         root.join("test-dir", "subdir").mkdir();
1498 
1499         root.join("test-dir").exists.should.be(true);
1500         root.join("test-dir", "subdir").exists.should.be(true);
1501     }
1502 
1503     ///
1504     unittest {
1505         import dshould;
1506         Path root = createTempPath();
1507         scope(exit) root.remove();
1508 
1509         root.join("test-dir").exists.should.be(false);
1510         root.join("test-dir", "subdir").exists.should.be(false);
1511 
1512         root.join("test-dir", "subdir").mkdir(true);
1513 
1514         root.join("test-dir").exists.should.be(true);
1515         root.join("test-dir", "subdir").exists.should.be(true);
1516     }
1517 
1518     /** Create symlink for this file in dest path.
1519       *
1520       * Params:
1521       *     dest = Destination path.
1522       *
1523       * Throws:
1524       *     FileException
1525       **/
1526     version(Posix) @safe void symlink(in Path dest) const {
1527         std.file.symlink(_path, dest._path);
1528     }
1529 
1530     ///
1531     version(Posix) unittest {
1532         import dshould;
1533         Path root = createTempPath();
1534         scope(exit) root.remove();
1535 
1536         // Create a file in some directory
1537         root.join("test-dir", "subdir").mkdir(true);
1538         root.join("test-dir", "subdir", "test-file.txt").writeFile("Hello!");
1539 
1540         // Create a symlink for created file
1541         root.join("test-dir", "subdir", "test-file.txt").symlink(
1542             root.join("test-symlink.txt"));
1543 
1544         // Create a symbolik link to directory
1545         root.join("test-dir", "subdir").symlink(root.join("dirlink"));
1546 
1547         // Test that symlink was created
1548         root.join("test-symlink.txt").exists.should.be(true);
1549         root.join("test-symlink.txt").isSymlink.should.be(true);
1550         root.join("test-symlink.txt").readFile.should.equal("Hello!");
1551 
1552         // Test that readlink and realpath works fine
1553         root.join("test-symlink.txt").readLink.should.equal(
1554             root.join("test-dir", "subdir", "test-file.txt"));
1555         version(OSX) {
1556             root.join("test-symlink.txt").realPath.should.equal(
1557                 root.realPath.join("test-dir", "subdir", "test-file.txt"));
1558         } else {
1559             root.join("test-symlink.txt").realPath.should.equal(
1560                 root.join("test-dir", "subdir", "test-file.txt"));
1561         }
1562         root.join("dirlink", "test-file.txt").readLink.should.equal(
1563             root.join("dirlink", "test-file.txt"));
1564         version(OSX) {
1565             root.join("dirlink", "test-file.txt").realPath.should.equal(
1566                 root.realPath.join("test-dir", "subdir", "test-file.txt"));
1567         } else {
1568             root.join("dirlink", "test-file.txt").realPath.should.equal(
1569                 root.join("test-dir", "subdir", "test-file.txt"));
1570         }
1571 
1572 
1573     }
1574 
1575     /** Open file and return `std.stdio.File` struct with opened file
1576       * Params:
1577       *     openMode = string representing open mode with
1578       *         same semantic as in C standard lib
1579       *         $(HTTP cplusplus.com/reference/clibrary/cstdio/fopen.html, fopen) function.
1580       * Returns:
1581       *     std.stdio.File struct
1582       **/
1583     @safe std.stdio.File openFile(in string openMode = "rb") const {
1584         static import std.stdio;
1585 
1586         return std.stdio.File(_path.expandTilde, openMode);
1587     }
1588 
1589     ///
1590     unittest {
1591         import dshould;
1592         Path root = createTempPath();
1593         scope(exit) root.remove();
1594 
1595         auto test_file = root.join("test-create.txt").openFile("wt");
1596         scope(exit) test_file.close();
1597         test_file.write("Test1");
1598         test_file.flush();
1599         root.join("test-create.txt").readFile().should.equal("Test1");
1600         test_file.write("12");
1601         test_file.flush();
1602         root.join("test-create.txt").readFile().should.equal("Test112");
1603     }
1604 
1605     /** Write data to file as is
1606       * Params:
1607       *     buffer = untypes array to write to file.
1608       * Throws:
1609       *     FileException in case of  error
1610       **/
1611     @safe void writeFile(in void[] buffer) const {
1612         return std.file.write(_path.expandTilde, buffer);
1613     }
1614 
1615     ///
1616     unittest {
1617         import dshould;
1618         Path root = createTempPath();
1619         scope(exit) root.remove();
1620 
1621         root.join("test-write-1.txt").exists.should.be(false);
1622         root.join("test-write-1.txt").writeFile("Hello world");
1623         root.join("test-write-1.txt").exists.should.be(true);
1624         root.join("test-write-1.txt").readFile.should.equal("Hello world");
1625 
1626         ubyte[] data = [1, 7, 13, 5, 9];
1627         root.join("test-write-2.txt").exists.should.be(false);
1628         root.join("test-write-2.txt").writeFile(data);
1629         root.join("test-write-2.txt").exists.should.be(true);
1630         ubyte[] rdata = cast(ubyte[])root.join("test-write-2.txt").readFile;
1631         rdata.length.should.equal(5);
1632         rdata[0].should.equal(1);
1633         rdata[1].should.equal(7);
1634         rdata[2].should.equal(13);
1635         rdata[3].should.equal(5);
1636         rdata[4].should.equal(9);
1637     }
1638 
1639     /** Append data to file as is
1640       * Params:
1641       *     buffer = untypes array to write to file.
1642       * Throws:
1643       *     FileException in case of  error
1644       **/
1645     @safe void appendFile(in void[] buffer) const {
1646         return std.file.append(_path.expandTilde, buffer);
1647     }
1648 
1649     ///
1650     unittest {
1651         import dshould;
1652         Path root = createTempPath();
1653         scope(exit) root.remove();
1654 
1655         ubyte[] data = [1, 7, 13, 5, 9];
1656         ubyte[] data2 = [8, 17];
1657         root.join("test-write-2.txt").exists.should.be(false);
1658         root.join("test-write-2.txt").writeFile(data);
1659         root.join("test-write-2.txt").appendFile(data2);
1660         root.join("test-write-2.txt").exists.should.be(true);
1661         ubyte[] rdata = cast(ubyte[])root.join("test-write-2.txt").readFile;
1662         rdata.length.should.equal(7);
1663         rdata[0].should.equal(1);
1664         rdata[1].should.equal(7);
1665         rdata[2].should.equal(13);
1666         rdata[3].should.equal(5);
1667         rdata[4].should.equal(9);
1668         rdata[5].should.equal(8);
1669         rdata[6].should.equal(17);
1670     }
1671 
1672 
1673     /** Read entire contents of file `name` and returns it as an untyped
1674       * array. If the file size is larger than `upTo`, only `upTo`
1675       * bytes are _read.
1676       * Params:
1677       *     upTo = if present, the maximum number of bytes to _read
1678       * Returns:
1679       *     Untyped array of bytes _read
1680       * Throws:
1681       *     FileException in case of error
1682       **/
1683     @safe auto readFile(size_t upTo=size_t.max) const {
1684         return std.file.read(_path.expandTilde, upTo);
1685     }
1686 
1687     ///
1688     unittest {
1689         import dshould;
1690         Path root = createTempPath();
1691         scope(exit) root.remove();
1692 
1693         root.join("test-create.txt").exists.should.be(false);
1694 
1695         // Test file read/write/apppend
1696         root.join("test-create.txt").writeFile("Hello World");
1697         root.join("test-create.txt").exists.should.be(true);
1698         root.join("test-create.txt").readFile.should.equal("Hello World");
1699         root.join("test-create.txt").appendFile("!");
1700         root.join("test-create.txt").readFile.should.equal("Hello World!");
1701 
1702         // Try to remove file
1703         root.join("test-create.txt").exists.should.be(true);
1704         root.join("test-create.txt").remove();
1705         root.join("test-create.txt").exists.should.be(false);
1706 
1707         // Try to read data as bytes
1708         ubyte[] data = [1, 7, 13, 5, 9];
1709         root.join("test-write-2.txt").exists.should.be(false);
1710         root.join("test-write-2.txt").writeFile(data);
1711         root.join("test-write-2.txt").exists.should.be(true);
1712         ubyte[] rdata = cast(ubyte[])root.join("test-write-2.txt").readFile;
1713         rdata.length.should.equal(5);
1714         rdata[0].should.equal(1);
1715         rdata[1].should.equal(7);
1716         rdata[2].should.equal(13);
1717         rdata[3].should.equal(5);
1718         rdata[4].should.equal(9);
1719     }
1720 
1721     /** Read text content of the file.
1722       * Technicall just a call to $(REF readText, std, file).
1723       *
1724       * Params:
1725       *     S = template parameter that represents type of string to read
1726       * Returns:
1727       *     text read from file.
1728       * Throws:
1729       *     $(LREF FileException) if there is an error reading the file,
1730       *     $(REF UTFException, std, utf) on UTF decoding error.
1731       **/
1732     @safe auto readFileText(S=string)() const {
1733         return std.file.readText!S(_path.expandTilde);
1734     }
1735 
1736 
1737     ///
1738     unittest {
1739         import dshould;
1740         Path root = createTempPath();
1741         scope(exit) root.remove();
1742 
1743         // Write some utf-8 data from the file
1744         root.join("test-utf-8.txt").writeFile("Hello World");
1745 
1746         // Test that we read correct value
1747         root.join("test-utf-8.txt").readFileText.should.equal("Hello World");
1748 
1749         // Write some data in UTF-16 with BOM
1750         root.join("test-utf-16.txt").writeFile("\uFEFFhi humans"w);
1751 
1752         // Read utf-16 content
1753         auto content = root.join("test-utf-16.txt").readFileText!wstring;
1754 
1755         // Strip BOM if present.
1756         import std.algorithm.searching : skipOver;
1757         content.skipOver('\uFEFF');
1758 
1759         // Ensure we read correct value
1760         content.should.equal("hi humans"w);
1761     }
1762 
1763     /** Get attributes of the path
1764       *
1765       *  Returns:
1766       *      uint - represening attributes of the file
1767       **/
1768     @safe auto getAttributes() const {
1769         return std.file.getAttributes(_path.expandTilde);
1770     }
1771 
1772     /// Test if file has permission to run
1773     version(Posix) unittest {
1774         import dshould;
1775         import std.conv: octal;
1776         Path root = createTempPath();
1777         scope(exit) root.remove();
1778 
1779         // Here we have to import bitmasks from system;
1780         import core.sys.posix.sys.stat;
1781 
1782         root.join("test-file.txt").writeFile("Hello World!");
1783         auto attributes = root.join("test-file.txt").getAttributes();
1784 
1785         // Test that file has permissions 644
1786         (attributes & octal!644).should.equal(octal!644);
1787 
1788         // Test that file is readable by user
1789         (attributes & S_IRUSR).should.equal(S_IRUSR);
1790 
1791         // Test that file is not writeable by others
1792         (attributes & S_IWOTH).should.not.equal(S_IWOTH);
1793     }
1794 
1795     /** Check if file has numeric attributes.
1796       * This method check if all bits specified by param 'attributes' are set.
1797       *
1798       * Params:
1799       *     attributes = numeric attributes (bit mask) to check
1800       *
1801       * Returns:
1802       *     true if all attributes present on file.
1803       *     false if at lease one bit specified by attributes is not set.
1804       *
1805       **/
1806     @safe bool hasAttributes(in uint attributes) const {
1807         return (this.getAttributes() & attributes) == attributes;
1808 
1809     }
1810 
1811     /// Example of checking attributes of file.
1812     version(Posix) unittest {
1813         import dshould;
1814         import std.conv: octal;
1815         Path root = createTempPath();
1816         scope(exit) root.remove();
1817 
1818         // Here we have to import bitmasks from system;
1819         import core.sys.posix.sys.stat;
1820 
1821         root.join("test-file.txt").writeFile("Hello World!");
1822 
1823         // Check that file has numeric permissions 644
1824         root.join("test-file.txt").hasAttributes(octal!644).should.be(true);
1825 
1826         // Check that it is not 755
1827         root.join("test-file.txt").hasAttributes(octal!755).should.be(false);
1828 
1829         // Check that every user can read this file.
1830         root.join("test-file.txt").hasAttributes(octal!444).should.be(true);
1831 
1832         // Check that owner can read the file
1833         // (do not check access rights for group and others)
1834         root.join("test-file.txt").hasAttributes(octal!400).should.be(true);
1835 
1836         // Test that file is readable by user
1837         root.join("test-file.txt").hasAttributes(S_IRUSR).should.be(true);
1838 
1839         // Test that file is writable by user
1840         root.join("test-file.txt").hasAttributes(S_IWUSR).should.be(true);
1841 
1842         // Test that file is not writable by others
1843         root.join("test-file.txt").hasAttributes(S_IWOTH).should.be(false);
1844     }
1845 
1846     /** Set attributes of the path
1847       *
1848       *  Params:
1849       *      attributes = value representing attributes to set on path.
1850       **/
1851 
1852     @safe void setAttributes(in uint attributes) const {
1853         std.file.setAttributes(_path, attributes);
1854     }
1855 
1856     /// Example of changing attributes of file.
1857     version(Posix) unittest {
1858         import dshould;
1859         import std.conv: octal;
1860         Path root = createTempPath();
1861         scope(exit) root.remove();
1862 
1863         // Here we have to import bitmasks from system;
1864         import core.sys.posix.sys.stat;
1865 
1866         root.join("test-file.txt").writeFile("Hello World!");
1867 
1868         // Check that file has numeric permissions 644
1869         root.join("test-file.txt").hasAttributes(octal!644).should.be(true);
1870 
1871 
1872         auto attributes = root.join("test-file.txt").getAttributes();
1873 
1874         // Test that file is readable by user
1875         (attributes & S_IRUSR).should.equal(S_IRUSR);
1876 
1877         // Test that file is not writeable by others
1878         (attributes & S_IWOTH).should.not.equal(S_IWOTH);
1879 
1880         // Add right to write file by others
1881         root.join("test-file.txt").setAttributes(attributes | S_IWOTH);
1882 
1883         // Test that file is now writable by others
1884         root.join("test-file.txt").hasAttributes(S_IWOTH).should.be(true);
1885 
1886         // Test that numeric permissions changed
1887         root.join("test-file.txt").hasAttributes(octal!646).should.be(true);
1888 
1889         // Set attributes as numeric value
1890         root.join("test-file.txt").setAttributes(octal!660);
1891 
1892         // Test that no group users can write the file
1893         root.join("test-file.txt").hasAttributes(octal!660).should.be(true);
1894 
1895         // Test that others do not have any access to the file
1896         root.join("test-file.txt").hasAttributes(octal!104).should.be(false);
1897         root.join("test-file.txt").hasAttributes(octal!106).should.be(false);
1898         root.join("test-file.txt").hasAttributes(octal!107).should.be(false);
1899         root.join("test-file.txt").hasAttributes(S_IWOTH).should.be(false);
1900         root.join("test-file.txt").hasAttributes(S_IROTH).should.be(false);
1901         root.join("test-file.txt").hasAttributes(S_IXOTH).should.be(false);
1902     }
1903 
1904     /** Execute the file pointed by path
1905       *
1906       * Params:
1907       *     args = arguments to be passed to program
1908       *     env = associative array that represent environment variables
1909       *        to be passed to program pointed by path
1910       *     workDir = Working directory for new process.
1911       *     config = Parameters for process creation.
1912       *        See See $(REF Config, std, process)
1913       *     maxOutput = Max bytes of output to be captured
1914       * Returns:
1915       *     An $(D std.typecons.Tuple!(int, "status", string, "output")).
1916       **/
1917     @safe auto execute(in string[] args=[],
1918             in string[string] env=null,
1919             in Nullable!Path workDir=Nullable!Path.init,
1920             in std.process.Config config=std.process.Config.none,
1921             in size_t maxOutput=size_t.max) const {
1922         return std.process.execute(
1923             this._path ~ args,
1924             env,
1925             config,
1926             maxOutput,
1927             (workDir.isNull) ? null : workDir.get.toString);
1928     }
1929 
1930     /// ditto
1931     @safe auto execute(in string[] args,
1932             in string[string] env,
1933             in Path workDir,
1934             in std.process.Config config=std.process.Config.none,
1935             in size_t maxOutput=size_t.max) const {
1936         return execute(args, env, Nullable!Path(workDir), config, maxOutput);
1937     }
1938 
1939 
1940     /// Example of running execute to run simple script
1941     version(Posix) unittest {
1942         import dshould;
1943         import std.conv: octal;
1944         Path root = createTempPath();
1945         scope(exit) root.remove();
1946 
1947         // Create simple test script that will print its arguments
1948         root.join("test-script").writeFile(
1949             "#!/usr/bin/env bash\necho \"$@\";");
1950 
1951         // Add permission to run this script
1952         root.join("test-script").setAttributes(octal!755);
1953 
1954         // Run test script without args
1955         auto status1 = root.join("test-script").execute;
1956         status1.status.should.be(0);
1957         status1.output.should.equal("\n");
1958 
1959         auto status2 = root.join("test-script").execute(["hello", "world"]);
1960         status2.status.should.be(0);
1961         status2.output.should.equal("hello world\n");
1962 
1963         auto status3 = root.join("test-script").execute(["hello", "world\nplus"]);
1964         status3.status.should.be(0);
1965         status3.output.should.equal("hello world\nplus\n");
1966 
1967         auto status4 = root.join("test-script").execute(
1968                 ["hello", "world"],
1969                 null,
1970                 root.nullable);
1971         status4.status.should.be(0);
1972         status4.output.should.equal("hello world\n");
1973     }
1974 
1975     /// Example of running execute to run script that will print
1976     /// current working directory
1977     version(Posix) unittest {
1978         import dshould;
1979         import std.conv: octal;
1980 
1981         const Path current_dir = Path.current;
1982         scope(exit) current_dir.chdir;
1983 
1984         Path root = createTempPath();
1985         scope(exit) root.remove();
1986 
1987         // Create simple test script that will print its arguments
1988         root.join("test-script").writeFile(
1989             "#!/usr/bin/env bash\npwd;");
1990 
1991         // Add permission to run this script
1992         root.join("test-script").setAttributes(octal!755);
1993 
1994         // Change current working directory to our root;
1995         root.chdir;
1996 
1997         // Do not pass current working directory
1998         // (script have to print current working directory)
1999         auto status0 = root.join("test-script").execute(["hello", "world"]);
2000         status0.status.should.be(0);
2001         version(OSX) {
2002             status0.output.should.equal(root.realPath.toString ~ "\n");
2003         } else {
2004             status0.output.should.equal(root.toString ~ "\n");
2005         }
2006 
2007         // Create some other directory
2008         auto my_dir = root.join("my-dir");
2009         my_dir.mkdir();
2010 
2011         // Passs my-dir as workding directory for script
2012         auto status1 = root.join("test-script").execute(
2013                 ["hello", "world"],
2014                 null,
2015                 my_dir.nullable);
2016         status1.status.should.be(0);
2017         version(OSX) {
2018             status1.output.should.equal(my_dir.realPath.toString ~ "\n");
2019         } else {
2020             status1.output.should.equal(my_dir.toString ~ "\n");
2021         }
2022 
2023         // Passs null path as workding direcotry for script
2024         auto status2 = root.join("test-script").execute(
2025                 ["hello", "world"],
2026                 null,
2027                 Nullable!Path.init);
2028         status2.status.should.be(0);
2029         version(OSX) {
2030             status2.output.should.equal(root.realPath.toString ~ "\n");
2031         } else {
2032             status2.output.should.equal(root.toString ~ "\n");
2033         }
2034 
2035         // Passs my-dir as workding directory for script (without nullable)
2036         auto status3 = root.join("test-script").execute(
2037                 ["hello", "world"],
2038                 null,
2039                 my_dir);
2040         status3.status.should.be(0);
2041         version(OSX) {
2042             status3.output.should.equal(my_dir.realPath.toString ~ "\n");
2043         } else {
2044             status3.output.should.equal(my_dir.toString ~ "\n");
2045         }
2046 
2047     }
2048 
2049     /** Search file by name in current directory and parent directories.
2050       * Usually, this could be used to find project config,
2051       * when current directory is somewhere inside project.
2052       *
2053       * If no file with specified name found, then return null path.
2054       *
2055       * Params:
2056       *     file_name = Name of file to search
2057       * Returns:
2058       *     Path to searched file, if such file was found.
2059       *     Otherwise return null Path.
2060       **/
2061     @safe Nullable!Path searchFileUp(in string file_name) const {
2062         return searchFileUp(Path(file_name));
2063     }
2064 
2065     /// ditto
2066     @safe Nullable!Path searchFileUp(in Path search_path) const {
2067         Path current_path = toAbsolute;
2068         while (!current_path.isRoot) {
2069             auto dst_path = current_path.join(search_path);
2070             if (dst_path.exists && dst_path.isFile) {
2071                 return dst_path.nullable;
2072             }
2073             current_path = current_path.parent;
2074 
2075             if (current_path._path == current_path.parent._path)
2076                 // It seems that if current path is same as parent path,
2077                 // then it could be infinite loop. So, let's break the loop;
2078                 break;
2079         }
2080         // Return null, that means - no path found
2081         return Nullable!Path.init;
2082     }
2083 
2084 
2085     /** Example of searching configuration file, when you are somewhere inside
2086       * project.
2087       **/
2088     unittest {
2089         import dshould;
2090         Path root = createTempPath();
2091         scope(exit) root.remove();
2092 
2093         // Save current directory
2094         auto cdir = std.file.getcwd;
2095         scope(exit) std.file.chdir(cdir);
2096 
2097         // Create directory structure
2098         root.join("dir1", "dir2", "dir3").mkdir(true);
2099         root.join("dir1", "my-conf.conf").writeFile("hello!");
2100         root.join("dir1", "dir4", "dir8").mkdir(true);
2101         root.join("dir1", "dir4", "my-conf.conf").writeFile("Hi!");
2102         root.join("dir1", "dir5", "dir6", "dir7").mkdir(true);
2103 
2104         // Change current working directory to dir7
2105         root.join("dir1", "dir5", "dir6", "dir7").chdir;
2106 
2107         // Find config file. It sould be dir1/my-conf.conf
2108         auto p1 = Path.current.searchFileUp("my-conf.conf");
2109         p1.isNull.should.be(false);
2110         version(OSX) {
2111             p1.get.toString.should.equal(
2112                 root.join("dir1", "my-conf.conf").realPath.toString);
2113         } else {
2114             p1.get.toString.should.equal(
2115                 root.join("dir1", "my-conf.conf").toAbsolute.toString);
2116         }
2117 
2118         // Try to get config, related to "dir8"
2119         auto p2 = root.join("dir1", "dir4", "dir8").searchFileUp(
2120             "my-conf.conf");
2121         p2.isNull.should.be(false);
2122         p2.get.should.equal(
2123                 root.join("dir1", "dir4", "my-conf.conf"));
2124 
2125         // Test searching for some path (instead of simple file/string)
2126         auto p3 = root.join("dir1", "dir2", "dir3").searchFileUp(
2127             Path("dir4", "my-conf.conf"));
2128         p3.isNull.should.be(false);
2129         p3.get.should.equal(
2130                 root.join("dir1", "dir4", "my-conf.conf"));
2131 
2132         // One more test
2133         auto p4 = root.join("dir1", "dir2", "dir3").searchFileUp(
2134             "my-conf.conf");
2135         p4.isNull.should.be(false);
2136         p4.get.should.equal(root.join("dir1", "my-conf.conf"));
2137 
2138         // Try to find up some unexisting file
2139         auto p5 = root.join("dir1", "dir2", "dir3").searchFileUp(
2140             "i-am-not-exist.conf");
2141         p5.isNull.should.be(true);
2142 
2143         import core.exception: AssertError;
2144         p5.get.should.throwA!AssertError;
2145     }
2146 }
2147