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