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