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