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