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