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