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