1 /++ 2 Authors: 3 Manuzor 4 5 ToDo: 6 - Path().open(...) 7 +/ 8 module pathlib; 9 10 public import std.file : SpanMode; 11 12 static import std.path; 13 static import std.file; 14 static import std.uni; 15 import std.algorithm; 16 import std.array : array, replace, replaceLast; 17 import std.format : format; 18 import std..string : split, indexOf, empty, squeeze, removechars, lastIndexOf; 19 import std.conv : to; 20 import std.typecons : Flag; 21 import std.traits : isSomeString, isSomeChar; 22 import std.range : iota, take, isInputRange; 23 import std.typetuple; 24 25 debug 26 { 27 void logDebug(T...)(T args) { 28 import std.stdio; 29 30 writefln(args); 31 } 32 } 33 34 void assertEqual(alias predicate = "a == b", A, B, StringType = string) 35 (A a, B b, StringType fmt = "`%s` must be equal to `%s`") 36 { 37 static if(isInputRange!A && isInputRange!B) { 38 assert(std.algorithm.equal!predicate(a, b), format(fmt, a, b)); 39 } 40 else { 41 import std.functional : binaryFun; 42 assert(binaryFun!predicate(a, b), format(fmt, a, b)); 43 } 44 } 45 46 void assertNotEqual(alias predicate = "a != b", A, B, StringType = string) 47 (A a, B b, StringType fmt = "`%s` must not be equal to `%s`") 48 { 49 assertEqual!predicate(a, b, fmt); 50 } 51 52 void assertEmpty(A, StringType = string)(A a, StringType fmt = "String `%s` should be empty.") { 53 assert(a.empty, format(fmt, a)); 54 } 55 56 void assertNotEmpty(A, StringType = string)(A a, StringType fmt = "String `%s` should be empty.") { 57 assert(!a.empty, format(fmt, a)); 58 } 59 60 template isSomePath(PathType) 61 { 62 enum isSomePath = is(PathType == WindowsPath) || 63 is(PathType == PosixPath); 64 } 65 66 /// 67 unittest 68 { 69 static assert(isSomePath!WindowsPath); 70 static assert(isSomePath!PosixPath); 71 static assert(!isSomePath!string); 72 } 73 74 75 /// Exception that will be thrown when any path operations fail. 76 class PathException : Exception 77 { 78 @safe pure nothrow 79 this(string msg) 80 { 81 super(msg); 82 } 83 } 84 85 86 mixin template PathCommon(TheStringType, alias theSeparator, alias theCaseSensitivity) 87 if(isSomeString!TheStringType && isSomeString!(typeof(theSeparator))) 88 { 89 alias PathType = typeof(this); 90 alias StringType = TheStringType; 91 92 StringType data = "."; 93 94 /// 95 unittest 96 { 97 assert(PathType().data != ""); 98 assert(PathType("123").data == "123"); 99 assert(PathType("C:///toomany//slashes!\\").data == "C:///toomany//slashes!\\"); 100 } 101 102 103 /// Used to separate each path segment. 104 alias separator = theSeparator; 105 106 107 /// A value of std.path.CaseSensitive whether this type of path is case sensetive or not. 108 alias caseSensitivity = theCaseSensitivity; 109 110 111 /// Concatenate a path and a string, which will be treated as a path. 112 auto opBinary(string op : "~", InStringType)(auto ref in InStringType str) const 113 if(isSomeString!InStringType) 114 { 115 return this ~ PathType(str); 116 } 117 118 /// Concatenate two paths. 119 auto opBinary(string op : "~")(auto ref in PathType other) const 120 if(isSomePath!PathType) 121 { 122 auto p = PathType(this.data); 123 p ~= other; 124 return p; 125 } 126 127 /// Concatenate the path-string $(D str) to this path. 128 void opOpAssign(string op : "~", InStringType)(auto ref in InStringType str) 129 if(isSomeString!InStringType) 130 { 131 this ~= PathType(str); 132 } 133 134 /// Concatenate the path $(D other) to this path. 135 void opOpAssign(string op : "~")(auto ref in PathType other) 136 if(isSomePath!PathType) 137 { 138 auto l = PathType(this.normalizedData); 139 auto r = PathType(other.normalizedData); 140 141 //logDebug("~= %s: %s => %s | %s => %s", PathType.stringof, this.data, l, other.data, r); 142 143 if(l.isDot || r.isAbsolute) { 144 this.data = r.data; 145 return; 146 } 147 148 if(r.isDot) { 149 this.data = l.data; 150 return; 151 } 152 153 auto sep = ""; 154 if(!l.data.endsWith('/', '\\') && !r.data.startsWith('/', '\\')) { 155 sep = this.separator; 156 } 157 158 this.data = l.data ~ sep ~ r.data; 159 } 160 161 /// 162 unittest 163 { 164 assertEqual(PathType() ~ "hello", PathType("hello")); 165 assertEqual(PathType("") ~ "hello", PathType("hello")); 166 assertEqual(PathType(".") ~ "hello", PathType("hello")); 167 168 assertEqual(WindowsPath("/") ~ "hello", WindowsPath(`hello`)); 169 assertEqual(WindowsPath(`C:\`) ~ "hello" ~ "world", WindowsPath(`C:\hello\world`)); 170 assertEqual(WindowsPath("hello") ~ "..", WindowsPath(`hello\..`)); 171 assertEqual(WindowsPath("..") ~ "hello", WindowsPath(`..\hello`)); 172 173 assertEqual(PosixPath("/") ~ "hello", PosixPath("/hello")); 174 assertEqual(PosixPath("/") ~ "hello" ~ "world", PosixPath("/hello/world")); 175 assertEqual(PosixPath("hello") ~ "..", PosixPath("hello/..")); 176 assertEqual(PosixPath("..") ~ "hello", PosixPath("../hello")); 177 } 178 179 /// Equality overload. 180 bool opEquals()(auto ref in PathType other) const 181 { 182 static if(theCaseSensitivity == std.path.CaseSensitive.no) { 183 return std.uni.sicmp(this.normalizedData, other.normalizedData) == 0; 184 } 185 else { 186 return std.algorithm.cmp(this.normalizedData, other.normalizedData) == 0; 187 } 188 } 189 190 int opCmp(ref const PathType other) const 191 { 192 static if(theCaseSensitivity == std.path.CaseSensitive.no) { 193 return std.uni.sicmp(this.normalizedData, other.normalizedData); 194 } 195 else { 196 return std.algorithm.cmp(this.normalizedData, other.normalizedData); 197 } 198 } 199 200 /// 201 unittest 202 { 203 assertEqual(PathType(""), PathType("")); 204 assertEqual(PathType("."), PathType("")); 205 assertEqual(PathType(""), PathType(".")); 206 auto p1 = PathType("/hello/world"); 207 auto p2 = PathType("/hello/world"); 208 assertEqual(p1, p2); 209 static if(is(PathType == WindowsPath)) 210 { 211 auto p3 = PathType("/hello/world"); 212 } 213 else static if(is(PathType == PosixPath)) 214 { 215 auto p3 = PathType("/hello\\world"); 216 } 217 auto p4 = PathType("/hello\\world"); 218 assertEqual(p3, p4); 219 } 220 221 222 /// Cast the path to a string. 223 auto toString(OtherStringType = StringType)() const { 224 return this.data.to!OtherStringType; 225 } 226 227 /// 228 unittest 229 { 230 assertEqual(PathType("C:/hello/world.exe.xml").to!StringType, "C:/hello/world.exe.xml"); 231 } 232 } 233 234 235 struct WindowsPath 236 { 237 mixin PathCommon!(string, `\`, std.path.CaseSensitive.no); 238 239 version(Windows) 240 { 241 /// Overload conversion `to` for Path => std.file.DirEntry. 242 auto opCast(To : std.file.DirEntry)() const 243 { 244 return To(this.normalizedData); 245 } 246 247 /// 248 unittest 249 { 250 auto d = cwd().to!(std.file.DirEntry); 251 } 252 } 253 } 254 255 256 struct PosixPath 257 { 258 mixin PathCommon!(string, "/", std.path.CaseSensitive.yes); 259 260 version(Posix) 261 { 262 /// Overload conversion `to` for Path => std.file.DirEntry. 263 auto opCast(To : std.file.DirEntry)() const 264 { 265 return To(this.normalizedData); 266 } 267 268 /// 269 unittest 270 { 271 auto d = cwd().to!(std.file.DirEntry); 272 } 273 } 274 } 275 276 /// Set the default path depending on the current platform. 277 version(Windows) 278 { 279 alias Path = WindowsPath; 280 } 281 else // Assume posix on non-windows platforms. 282 { 283 alias Path = PosixPath; 284 } 285 286 287 /// Helper struct to change the directory for the current scope. 288 struct ScopedChdir 289 { 290 Path prevDir; 291 292 this(in Path newDir) 293 { 294 this.prevDir = cwd(); 295 if(newDir.isAbsolute) { 296 chdir(newDir); 297 } 298 else { 299 chdir(cwd() ~ newDir); 300 } 301 } 302 303 /// Convenience overload that takes a string. 304 this(string newDir) { 305 this(Path(newDir)); 306 } 307 308 ~this() { 309 chdir(this.prevDir); 310 } 311 } 312 313 /// 314 unittest 315 { 316 auto orig = cwd(); 317 with(ScopedChdir("..")) { 318 assertNotEqual(orig, cwd()); 319 } 320 assertEqual(orig, cwd()); 321 } 322 323 324 /// Whether $(D str) can be represented by ".". 325 bool isDot(StringType)(auto ref in StringType str) 326 if(isSomeString!StringType) 327 { 328 if(str.length > 0 && str[0] != '.') { 329 return false; 330 } 331 if(str.startsWith("..")) { 332 return false; 333 } 334 auto data = str.removechars("./\\"); 335 return data.empty; 336 } 337 338 /// 339 unittest 340 { 341 assert("".isDot); 342 assert(".".isDot); 343 assert("./".isDot); 344 assert("./.".isDot); 345 assert("././".isDot); 346 assert(!"/".isDot); 347 assert(!"hello".isDot); 348 assert(!".git".isDot); 349 } 350 351 352 /// Whether the path is either "" or ".". 353 auto isDot(PathType)(auto ref in PathType p) 354 if(isSomePath!PathType) 355 { 356 return p.data.isDot; 357 } 358 359 /// 360 unittest 361 { 362 assert(WindowsPath().isDot); 363 assert(WindowsPath("").isDot); 364 assert(WindowsPath(".").isDot); 365 assert(!WindowsPath("/").isDot); 366 assert(!WindowsPath("hello").isDot); 367 assert(PosixPath().isDot); 368 assert(PosixPath("").isDot); 369 assert(PosixPath(".").isDot); 370 assert(!PosixPath("/").isDot); 371 assert(!PosixPath("hello").isDot); 372 } 373 374 375 /// Returns: The root of the path $(D p). 376 auto root(PathType)(auto ref in PathType p) 377 if(isSomePath!PathType) 378 { 379 static if(is(PathType == WindowsPath)) 380 { 381 return p.drive; 382 } 383 else // Assume PosixPath 384 { 385 auto data = p.data; 386 if(data.length > 0 && data[0] == '/') { 387 return data[0..1]; 388 } 389 390 return ""; 391 } 392 } 393 394 /// 395 unittest 396 { 397 assertEqual(WindowsPath("").root, ""); 398 assertEqual(PosixPath("").root, ""); 399 assertEqual(WindowsPath("C:/Hello/World").root, "C:"); 400 assertEqual(WindowsPath("/Hello/World").root, ""); 401 assertEqual(PosixPath("/hello/world").root, "/"); 402 assertEqual(PosixPath("C:/hello/world").root, ""); 403 } 404 405 406 /// The drive of the path $(D p). 407 /// Note: Non-Windows platforms have no concept of "drives". 408 auto drive(PathType)(auto ref in PathType p) 409 if(isSomePath!PathType) 410 { 411 static if(is(PathType == WindowsPath)) 412 { 413 auto data = p.data; 414 if(data.length > 1 && data[1] == ':') { 415 return data[0..2]; 416 } 417 } 418 419 return ""; 420 } 421 422 /// 423 unittest 424 { 425 assertEqual(WindowsPath("").drive, ""); 426 assertEqual(WindowsPath("/Hello/World").drive, ""); 427 assertEqual(WindowsPath("C:/Hello/World").drive, "C:"); 428 assertEqual(PosixPath("").drive, ""); 429 assertEqual(PosixPath("/Hello/World").drive, ""); 430 assertEqual(PosixPath("C:/Hello/World").drive, ""); 431 } 432 433 434 /// Returns: The path data using forward slashes, regardless of the current platform. 435 auto posixData(PathType)(auto ref in PathType p) 436 if(isSomePath!PathType) 437 { 438 return normalizedDataImpl!"posix"(p); 439 } 440 441 /// 442 unittest 443 { 444 assertEqual(WindowsPath().posixData, "."); 445 assertEqual(WindowsPath(``).posixData, "."); 446 assertEqual(WindowsPath(`.`).posixData, "."); 447 assertEqual(WindowsPath(`..`).posixData, ".."); 448 assertEqual(WindowsPath(`/foo/bar`).posixData, `foo/bar`); 449 assertEqual(WindowsPath(`/foo/bar/`).posixData, `foo/bar`); 450 assertEqual(WindowsPath(`C:\foo/bar.exe`).posixData, `C:/foo/bar.exe`); 451 assertEqual(WindowsPath(`./foo\/../\/bar/.//\/baz.exe`).posixData, `foo/../bar/baz.exe`); 452 453 assertEqual(PosixPath().posixData, `.`); 454 assertEqual(PosixPath(``).posixData, `.`); 455 assertEqual(PosixPath(`.`).posixData, `.`); 456 assertEqual(PosixPath(`..`).posixData, `..`); 457 assertEqual(PosixPath(`/foo/bar`).posixData, `/foo/bar`); 458 assertEqual(PosixPath(`/foo/bar/`).posixData, `/foo/bar`); 459 assertEqual(PosixPath(`.//foo\ bar/.//./baz.txt`).posixData, `foo\ bar/baz.txt`); 460 } 461 462 463 /// Returns: The path data using backward slashes, regardless of the current platform. 464 auto windowsData(PathType)(auto ref in PathType p) 465 if(isSomePath!PathType) 466 { 467 return normalizedDataImpl!"windows"(p); 468 } 469 470 /// 471 unittest 472 { 473 assertEqual(WindowsPath().windowsData, `.`); 474 assertEqual(WindowsPath(``).windowsData, `.`); 475 assertEqual(WindowsPath(`.`).windowsData, `.`); 476 assertEqual(WindowsPath(`..`).windowsData, `..`); 477 assertEqual(WindowsPath(`C:\foo\bar.exe`).windowsData, `C:\foo\bar.exe`); 478 assertEqual(WindowsPath(`C:\foo\bar\`).windowsData, `C:\foo\bar`); 479 assertEqual(WindowsPath(`C:\./foo\.\\.\\\bar\`).windowsData, `C:\foo\bar`); 480 assertEqual(WindowsPath(`C:\./foo\..\\.\\\bar\//baz.exe`).windowsData, `C:\foo\..\bar\baz.exe`); 481 482 assertEqual(PosixPath().windowsData, `.`); 483 assertEqual(PosixPath(``).windowsData, `.`); 484 assertEqual(PosixPath(`.`).windowsData, `.`); 485 assertEqual(PosixPath(`..`).windowsData, `..`); 486 assertEqual(PosixPath(`/foo/bar.txt`).windowsData, `foo\bar.txt`); 487 assertEqual(PosixPath(`/foo/bar\`).windowsData, `foo\bar`); 488 assertEqual(PosixPath(`/foo/bar\`).windowsData, `foo\bar`); 489 assertEqual(PosixPath(`/./\\foo/\/.\..\bar/./baz.txt`).windowsData, `foo\..\bar\baz.txt`); 490 } 491 492 493 /// Remove duplicate directory separators and stand-alone dots, on a textual basis. 494 /// Does not resolve ".."s in paths, since this would be wrong when dealing with symlinks. 495 /// Given the symlink "/foo" pointing to "/what/ever", the path "/foo/../baz.exe" would 'physically' be "/what/baz.exe". 496 /// If "/foo" was no symlink, the actual path would be "/baz.exe". 497 /// 498 /// If you need to resolve ".."s and symlinks, see resolved(). 499 auto normalizedData(PathType)(auto ref in PathType p) 500 if(allSatisfy!(isSomePath, PathType)) 501 { 502 static if(is(PathType == WindowsPath)) 503 { 504 return p.windowsData; 505 } 506 else static if(is(PathType == PosixPath)) 507 { 508 return p.posixData; 509 } 510 } 511 512 private auto normalizedDataImpl(alias target, PathType)(in PathType p) 513 { 514 static assert(target == "posix" || target == "windows"); 515 516 if(p.isDot) { 517 return "."; 518 } 519 520 static if(target == "windows") 521 { 522 auto sep = WindowsPath.separator; 523 } 524 else static if(target == "posix") 525 { 526 auto sep = PosixPath.separator; 527 } 528 529 // Note: We cannot make use of std.path.buildNormalizedPath because it textually resolves ".."s in paths. 530 static if(is(PathType == WindowsPath)) 531 { 532 //logDebug("WindowsPath 1: %s", p.data); 533 //logDebug("WindowsPath 2: %s", p.data.replace(WindowsPath.separator, PosixPath.separator)); 534 //logDebug("WindowsPath 3: %s", p.data.replace(WindowsPath.separator, PosixPath.separator) 535 // .split(PosixPath.separator)); 536 //logDebug("WindowsPath 4: %s", p.data.replace(WindowsPath.separator, PosixPath.separator) 537 // .split(PosixPath.separator) 538 // .filter!(a => !a.empty && !a.isDot)); 539 //logDebug("WindowsPath 5: %s", p.data.replace(WindowsPath.separator, PosixPath.separator) 540 // .split(PosixPath.separator) 541 // .filter!(a => !a.empty && !a.isDot) 542 // .joiner(sep)); 543 544 // For WindowsPaths, replace "\" with "/". 545 return p.data.replace(WindowsPath.separator, PosixPath.separator) 546 .split(PosixPath.separator) 547 .filter!(a => !a.empty && !a.isDot) 548 .joiner(sep) 549 .to!string(); 550 } 551 else static if(is(PathType == PosixPath)) 552 { 553 // For PosixPaths, do not replace any "\"s and make sure to append the root as prefix, 554 // since it would get `split` away. 555 556 static if(target == "windows") 557 { 558 return WindowsPath(p.data).windowsData; 559 } 560 else static if(target == "posix") 561 { 562 auto root = p.root; 563 564 //logDebug("PosixPath root: %s", root); 565 //logDebug("PosixPath 1: %s", root ~ p.data); 566 //logDebug("PosixPath 2: %s", root ~ p.data.split(PosixPath.separator)); 567 //logDebug("PosixPath 3: %s", root ~ p.data.split(PosixPath.separator) 568 // .filter!(a => !a.empty && !a.isDot) 569 // .to!string); 570 //logDebug("PosixPath 4: %s", root ~ p.data.split(PosixPath.separator) 571 // .filter!(a => !a.empty && !a.isDot) 572 // .joiner(sep) 573 // .to!string); 574 575 return root ~ p.data.split(PosixPath.separator) 576 .filter!(a => !a.empty && !a.isDot) 577 .joiner(sep) 578 .to!string(); 579 } 580 } 581 } 582 583 584 auto asPosixPath(PathType)(auto ref in PathType p) 585 if(isSomePath!PathType) 586 { 587 return PathType(p.posixData); 588 } 589 590 auto asWindowsPath(PathType)(auto ref in PathType p) 591 if(isSomePath!PathType) 592 { 593 return PathType(p.windowsData); 594 } 595 596 auto asNormalizedPath(DestType = SrcType, SrcType)(auto ref in SrcType p) 597 if(isSomePath!DestType && isSomePath!SrcType) 598 { 599 return DestType(p.normalizedData); 600 } 601 602 603 /// Whether the path is absolute. 604 auto isAbsolute(PathType)(auto ref in PathType p) 605 if(isSomePath!PathType) 606 { 607 // If the path has a root, it is absolute. 608 return !p.root.empty; 609 } 610 611 /// 612 unittest 613 { 614 assert(!WindowsPath("").isAbsolute); 615 assert(!WindowsPath("/Hello/World").isAbsolute); 616 assert(WindowsPath("C:/Hello/World").isAbsolute); 617 assert(!WindowsPath("foo/bar.exe").isAbsolute); 618 assert(!PosixPath("").isAbsolute); 619 assert(PosixPath("/Hello/World").isAbsolute); 620 assert(!PosixPath("C:/Hello/World").isAbsolute); 621 assert(!PosixPath("foo/bar.exe").isAbsolute); 622 } 623 624 625 auto absolute(PathType)(auto ref in PathType p, lazy PathType parent = cwd().asNormalizedPath!PathType()) 626 { 627 if(p.isAbsolute) { 628 return p; 629 } 630 631 return parent ~ p; 632 } 633 634 /// TODO 635 unittest 636 { 637 assertEqual(WindowsPath("bar/baz.exe").absolute(WindowsPath("C:/foo")), WindowsPath("C:/foo/bar/baz.exe")); 638 assertEqual(WindowsPath("C:/foo/bar.exe").absolute(WindowsPath("C:/baz")), WindowsPath("C:/foo/bar.exe")); 639 } 640 641 642 /// Returns: The parts of the path as an array. 643 auto parts(PathType)(auto ref in PathType p) 644 if(isSomePath!PathType) 645 { 646 static if(is(PathType == WindowsPath)) 647 { 648 auto prefix = ""; 649 } 650 else static if(is(PathType == PosixPath)) 651 { 652 auto prefix = p.root; 653 } 654 auto theSplit = p.normalizedData.split(PathType.separator); 655 return (prefix ~ theSplit).filter!(a => !a.empty); 656 } 657 658 /// 659 unittest 660 { 661 assertEqual(Path().parts, ["."]); 662 assertEqual(Path("./.").parts, ["."]); 663 664 assertEqual(WindowsPath("C:/hello/world").parts, ["C:", "hello", "world"]); 665 assertEqual(WindowsPath(`hello/.\world.exe.xml`).parts, ["hello", "world.exe.xml"]); 666 assertEqual(WindowsPath("C:/hello/world.exe.xml").parts, ["C:", "hello", "world.exe.xml"]); 667 668 assertEqual(PosixPath("hello/world.exe.xml").parts, ["hello", "world.exe.xml"]); 669 assertEqual(PosixPath("/hello/.//world.exe.xml").parts, ["/", "hello", "world.exe.xml"]); 670 assertEqual(PosixPath("/hello\\ world.exe.xml").parts, ["/", "hello\\ world.exe.xml"]); 671 } 672 673 674 auto parent(PathType)(auto ref in PathType p) 675 if(isSomePath!PathType) 676 { 677 auto theParts = p.parts.map!(a => PathType(a)).array; 678 if(theParts.length > 1) { 679 return theParts[0 .. $ - 1].reduce!((a, b){ return a ~ b;}); 680 } 681 return PathType(); 682 } 683 684 /// 685 unittest 686 { 687 assertEqual(Path().parent, Path()); 688 assertEqual(Path("IHaveNoParents").parent, Path()); 689 assertEqual(WindowsPath("C:/hello/world").parent, WindowsPath(`C:\hello`)); 690 assertEqual(WindowsPath("C:/hello/world/").parent, WindowsPath(`C:\hello`)); 691 assertEqual(WindowsPath("C:/hello/world.exe.foo").parent, WindowsPath(`C:\hello`)); 692 assertEqual(PosixPath("/hello/world").parent, PosixPath("/hello")); 693 assertEqual(PosixPath("/hello/\\ world/").parent, PosixPath("/hello")); 694 assertEqual(PosixPath("/hello.foo.bar/world/").parent, PosixPath("/hello.foo.bar")); 695 } 696 697 698 /// /foo/bar/baz/hurr.durr => { Path("/foo/bar/baz"), Path("/foo/bar"), Path("/foo"), Path("/") } 699 auto parents(PathType)(auto ref in PathType p) 700 if(isSomePath!PathType) 701 { 702 auto theParts = p.parts.map!(x => PathType(x)).array; 703 return iota(theParts.length - 1, 0, -1).map!(x => theParts.take(x) 704 .reduce!((a, b){ return a ~ b; })); 705 } 706 707 /// 708 unittest 709 { 710 assertEmpty(WindowsPath().parents); 711 assertEmpty(WindowsPath(".").parents); 712 assertEmpty(WindowsPath("foo.txt").parents); 713 assertEqual(WindowsPath("C:/hello/world").parents, [ WindowsPath(`C:\hello`), WindowsPath(`C:\`) ]); 714 assertEqual(WindowsPath("C:/hello/world/").parents, [ WindowsPath(`C:\hello`), WindowsPath(`C:\`) ]); 715 716 assertEmpty(PosixPath().parents); 717 assertEmpty(PosixPath(".").parents); 718 assertEmpty(PosixPath("foo.txt").parents); 719 assertEqual(PosixPath("/hello/world").parents, [ PosixPath("/hello"), PosixPath("/") ]); 720 assertEqual(PosixPath("/hello/world/").parents, [ PosixPath("/hello"), PosixPath("/") ]); 721 } 722 723 724 /// The name of the path without any of its parents. 725 auto name(PathType)(auto ref in PathType p) 726 if(isSomePath!PathType) 727 { 728 import std.algorithm : min; 729 730 auto data = p.posixData; 731 auto i = min(data.lastIndexOf('/') + 1, data.length); 732 return data[i .. $]; 733 } 734 735 /// 736 unittest 737 { 738 assertEqual(WindowsPath().name, "."); 739 assertEqual(WindowsPath("").name, "."); 740 assertEmpty(WindowsPath("/").name); 741 assertEqual(WindowsPath("/hello").name, "hello"); 742 assertEqual(WindowsPath("C:\\hello").name, "hello"); 743 assertEqual(WindowsPath("C:/hello/world.exe").name, "world.exe"); 744 assertEqual(WindowsPath("hello/world.foo.bar.exe").name, "world.foo.bar.exe"); 745 746 assertEqual(PosixPath().name, "."); 747 assertEqual(PosixPath("").name, "."); 748 assertEmpty(PosixPath("/").name, "/"); 749 assertEqual(PosixPath("/hello").name, "hello"); 750 assertEqual(PosixPath("C:\\hello").name, "C:\\hello"); 751 assertEqual(PosixPath("/foo/bar\\ baz.txt").name, "bar\\ baz.txt"); 752 assertEqual(PosixPath("C:/hello/world.exe").name, "world.exe"); 753 assertEqual(PosixPath("hello/world.foo.bar.exe").name, "world.foo.bar.exe"); 754 } 755 756 757 /// The extension of the path including the leading dot. 758 /// 759 /// Examples: The extension of "hello.foo.bar.exe" is ".exe". 760 auto extension(PathType)(auto ref in PathType p) 761 if(isSomePath!PathType) 762 { 763 auto data = p.name; 764 auto i = data.lastIndexOf('.'); 765 if(i < 0) { 766 return ""; 767 } 768 if(i + 1 == data.length) { 769 // This prevents preserving the dot in empty extensions such as `hello.foo.`. 770 ++i; 771 } 772 return data[i .. $]; 773 } 774 775 /// 776 unittest 777 { 778 assertEmpty(Path().extension); 779 assertEmpty(Path("").extension); 780 assertEmpty(Path("/").extension); 781 assertEmpty(Path("/hello").extension); 782 assertEmpty(Path("C:/hello/world").extension); 783 assertEqual(Path("C:/hello/world.exe").extension, ".exe"); 784 assertEqual(Path("hello/world.foo.bar.exe").extension, ".exe"); 785 } 786 787 788 /// All extensions of the path. 789 auto extensions(PathType)(auto ref in PathType p) 790 if(isSomePath!PathType) 791 { 792 import std.algorithm : splitter, filter; 793 import std.range : dropOne; 794 795 auto result = p.name.splitter('.').filter!(a => !a.empty); 796 if(!result.empty) { 797 result = result.dropOne; 798 } 799 return result.map!(a => '.' ~ a).array; 800 } 801 802 /// 803 unittest 804 { 805 assertEmpty(Path().extensions); 806 assertEmpty(Path("").extensions); 807 assertEmpty(Path("/").extensions); 808 assertEmpty(Path("/hello").extensions); 809 assertEmpty(Path("C:/hello/world").extensions); 810 assertEqual(Path("C:/hello/world.exe").extensions, [".exe"]); 811 assertEqual(Path("hello/world.foo.bar.exe").extensions, [".foo", ".bar", ".exe"]); 812 } 813 814 815 /// The full extension of the path. 816 /// 817 /// Examples: The full extension of "hello.foo.bar.exe" would be ".foo.bar.exe". 818 auto fullExtension(PathType)(auto ref in PathType p) 819 if(isSomePath!PathType) 820 { 821 auto data = p.name; 822 auto i = data.indexOf('.'); 823 if(i < 0) { 824 return ""; 825 } 826 if(i + 1 == data.length) { 827 // This prevents preserving the dot in empty extensions such as `hello.foo.`. 828 ++i; 829 } 830 return data[i .. $]; 831 } 832 833 /// 834 unittest 835 { 836 assertEmpty(Path().fullExtension); 837 assertEmpty(Path("").fullExtension); 838 assertEmpty(Path("/").fullExtension); 839 assertEmpty(Path("/hello").fullExtension); 840 assertEmpty(Path("C:/hello/world").fullExtension); 841 assertEqual(Path("C:/hello/world.exe").fullExtension, ".exe"); 842 assertEqual(Path("hello/world.foo.bar.exe").fullExtension, ".foo.bar.exe"); 843 } 844 845 846 /// The name of the path without its extension. 847 auto stem(PathType)(auto ref in PathType p) 848 if(isSomePath!PathType) 849 { 850 auto data = p.name; 851 auto i = data.indexOf('.'); 852 if(i < 0) { 853 return data; 854 } 855 if(i + 1 == data.length) { 856 // This prevents preserving the dot in empty extensions such as `hello.foo.`. 857 ++i; 858 } 859 return data[0 .. i]; 860 } 861 862 /// 863 unittest 864 { 865 assertEqual(Path().stem, "."); 866 assertEqual(Path("").stem, "."); 867 assertEqual(Path("/").stem, ""); 868 assertEqual(Path("/hello").stem, "hello"); 869 assertEqual(Path("C:/hello/world").stem, "world"); 870 assertEqual(Path("C:/hello/world.exe").stem, "world"); 871 assertEqual(Path("hello/world.foo.bar.exe").stem, "world"); 872 } 873 874 875 /// Create a path from $(D p) that is relative to $(D parent). 876 auto relativeTo(PathType)(auto ref in PathType p, in auto ref PathType parent) 877 if(isSomePath!PathType) 878 { 879 auto ldata = p.normalizedData; 880 auto rdata = parent.normalizedData; 881 if(!ldata.startsWith(rdata)) { 882 throw new PathException(format("'%s' is not a subpath of '%s'.", ldata, rdata)); 883 } 884 auto sliceStart = rdata.length; 885 if(rdata.length != ldata.length) { 886 // Remove trailing path separator. 887 ++sliceStart; 888 } 889 auto result = ldata[sliceStart .. $]; 890 return result.isDot ? PathType(".") : PathType(result); 891 } 892 893 /// 894 unittest 895 { 896 import std.exception : assertThrown; 897 898 assertEqual(Path("C:/hello/world.exe").relativeTo(Path("C:/hello")), Path("world.exe")); 899 assertEqual(Path("C:/hello").relativeTo(Path("C:/hello")), Path()); 900 assertEqual(Path("C:/hello/").relativeTo(Path("C:/hello")), Path()); 901 assertEqual(Path("C:/hello").relativeTo(Path("C:/hello/")), Path()); 902 assertEqual(WindowsPath("C:/foo/bar/baz").relativeTo(WindowsPath("C:/foo")), WindowsPath(`bar\baz`)); 903 assertEqual(PosixPath("C:/foo/bar/baz").relativeTo(PosixPath("C:/foo")), PosixPath("bar/baz")); 904 assertThrown!PathException(Path("a").relativeTo(Path("b"))); 905 } 906 907 908 /// Whether the given path matches the given glob-style pattern 909 auto match(PathType, Pattern)(auto ref in PathType p, Pattern pattern) 910 if(isSomePath!PathType) 911 { 912 import std.path : globMatch; 913 914 return p.normalizedData.globMatch!(PathType.caseSensitivity)(pattern); 915 } 916 917 /// 918 unittest 919 { 920 assert(Path().match("*")); 921 assert(Path("").match("*")); 922 assert(Path(".").match("*")); 923 assert(Path("/").match("*")); 924 assert(Path("/hello").match("*")); 925 assert(Path("/hello/world.exe").match("*")); 926 assert(Path("/hello/world.exe").match("*.exe")); 927 assert(!Path("/hello/world.exe").match("*.zip")); 928 assert(WindowsPath("/hello/world.EXE").match("*.exe")); 929 assert(!PosixPath("/hello/world.EXE").match("*.exe")); 930 } 931 932 933 /// Whether the path exists or not. It does not matter whether it is a file or not. 934 bool exists(in Path p) { 935 return std.file.exists(p.data); 936 } 937 938 /// 939 unittest 940 { 941 } 942 943 944 /// Whether the path is an existing directory. 945 bool isDir(in Path p) { 946 return std.file.isDir(p.data); 947 } 948 949 /// 950 unittest 951 { 952 } 953 954 955 /// Whether the path is an existing file. 956 bool isFile(in Path p) { 957 return std.file.isFile(p.data); 958 } 959 960 /// 961 unittest 962 { 963 } 964 965 966 /// Whether the given path $(D p) points to a symbolic link (or junction point in Windows). 967 bool isSymlink(in Path p) { 968 return std.file.isSymlink(p.normalizedData); 969 } 970 971 /// 972 unittest 973 { 974 } 975 976 977 // Resolve all ".", "..", and symlinks. 978 Path resolved(in Path p) { 979 return Path(Path(std.path.absolutePath(p.data)).normalizedData); 980 } 981 982 /// 983 unittest 984 { 985 assertNotEqual(Path(), Path().resolved()); 986 } 987 988 989 /// The absolute path to the current working directory with symlinks and friends resolved. 990 Path cwd() { 991 return Path(std.file.getcwd()); 992 } 993 994 /// 995 unittest 996 { 997 assertNotEmpty(cwd().data); 998 } 999 1000 1001 /// The path to the current executable. 1002 Path currentExePath() { 1003 return Path(std.file.thisExePath()).resolved(); 1004 } 1005 1006 /// 1007 unittest 1008 { 1009 assertNotEmpty(currentExePath().data); 1010 } 1011 1012 1013 void chdir(in Path p) { 1014 std.file.chdir(p.normalizedData); 1015 } 1016 1017 /// 1018 unittest 1019 { 1020 } 1021 1022 1023 /// Generate an input range of Paths that match the given pattern. 1024 auto glob(PatternType)(auto ref in Path p, PatternType pattern, SpanMode spanMode = SpanMode.shallow) { 1025 return std.file.dirEntries(p.normalizedData, pattern, spanMode).map!(a => Path(a.name)); 1026 } 1027 1028 /// 1029 unittest 1030 { 1031 assertNotEmpty(currentExePath().parent.glob("*")); 1032 assertNotEmpty(currentExePath().parent.glob("*", SpanMode.shallow)); 1033 assertNotEmpty(currentExePath().parent.glob("*", SpanMode.breadth)); 1034 assertNotEmpty(currentExePath().parent.glob("*", SpanMode.depth)); 1035 } 1036 1037 1038 auto open(in Path p, in char[] openMode = "rb") { 1039 static import std.stdio; 1040 1041 return std.stdio.File(p.normalizedData, openMode); 1042 } 1043 1044 /// 1045 unittest 1046 { 1047 } 1048 1049 1050 /// Copy a file to some destination. 1051 /// If the destination exists and is a file, it is overwritten. If it is an existing directory, the actual destination will be ($D dest ~ src.name). 1052 /// Behaves like std.file.copy except that $(D dest) does not have to be a file. 1053 /// See_Also: copyTo(in Path, in Path) 1054 void copyFileTo(in Path src, in Path dest) { 1055 if(dest.exists && dest.isDir) { 1056 std.file.copy(src.normalizedData, (dest ~ src.name).normalizedData); 1057 } 1058 else { 1059 std.file.copy(src.normalizedData, dest.normalizedData); 1060 } 1061 } 1062 1063 /// 1064 unittest 1065 { 1066 } 1067 1068 1069 /// Copy a file or directory to a target file or directory. 1070 /// 1071 /// This function essentially behaves like the unix shell command `cp -r` with just asingle source input. 1072 /// 1073 /// Params: 1074 /// src = The path to a file or a directory. 1075 /// dest = The path to a file or a directory. If $(D src) is a directory, $(D dest) must be an existing directory. 1076 /// 1077 /// Throws: PathException 1078 void copyTo(alias copyCondition = (a, b){ return true; })(in Path src, in Path dest) { 1079 if(!src.exists) { 1080 throw new PathException(format("The source path does not exist: %s", src)); 1081 } 1082 1083 if(src.isFile) { 1084 if(copyCondition(src, dest)) src.copyFileTo(dest); 1085 return; 1086 } 1087 1088 if(!dest.exists) { 1089 dest.mkdir(false); 1090 } 1091 else if(dest.isFile) { 1092 // At this point we know that src must be a dir. 1093 throw new PathException(format("Since the source path is a directory, the destination must be a directory as well. Source: %s | Destination: %s", src, dest)); 1094 } 1095 1096 foreach(srcFile; src.glob("*", SpanMode.breadth).filter!(a => !a.isDir)) { 1097 auto destFile = dest ~ srcFile.relativeTo(src); 1098 if(!copyCondition(srcFile, destFile)) { 1099 continue; 1100 } 1101 if(!destFile.exists) { 1102 destFile.parent.mkdir(true); 1103 } 1104 srcFile.copyFileTo(destFile); 1105 } 1106 } 1107 1108 /// 1109 unittest 1110 { 1111 } 1112 1113 1114 alias copyToIfNewer = copyTo!((src, dest){ 1115 import std.datetime : SysTime; 1116 SysTime _, src_modTime, dest_modTime; 1117 std.file.getTimes(src.normalizedData, _, src_modTime); 1118 std.file.getTimes(dest.normalizedData, _, dest_modTime); 1119 return src_modTime < dest_modTime; 1120 }); 1121 1122 1123 /// Remove path from filesystem. Similar to unix `rm`. If the path is a dir, will reecursively remove all subdirs by default. 1124 void remove(in Path p, bool recursive = true) { 1125 if(p.isFile) { 1126 std.file.remove(p.normalizedData); 1127 } 1128 else if(recursive) { 1129 std.file.rmdirRecurse(p.normalizedData); 1130 } 1131 else { 1132 std.file.rmdir(p.normalizedData); 1133 } 1134 } 1135 1136 /// 1137 unittest 1138 { 1139 } 1140 1141 1142 /// 1143 void mkdir(in Path p, bool parents = true) { 1144 if(parents) { 1145 std.file.mkdirRecurse(p.normalizedData); 1146 } 1147 else { 1148 std.file.mkdir(p.normalizedData); 1149 } 1150 } 1151 1152 /// 1153 unittest 1154 { 1155 } 1156 1157 1158 auto readFile(in Path p) { 1159 return std.file.read(p.normalizedData); 1160 } 1161 1162 /// 1163 unittest 1164 { 1165 } 1166 1167 1168 /// 1169 auto readFile(S)(in Path p) 1170 if(isSomeString!S) 1171 { 1172 return cast(S)std.file.readText!S(p.normalizedData); 1173 } 1174 1175 /// 1176 unittest 1177 { 1178 } 1179 1180 1181 /// 1182 void writeFile(in Path p, const void[] buffer) { 1183 std.file.write(p.normalizedData, buffer); 1184 } 1185 1186 /// 1187 unittest 1188 { 1189 } 1190 1191 1192 /// 1193 void appendFile(in Path p, in void[] buffer) { 1194 std.file.append(p.normalizedData, buffer); 1195 } 1196 1197 /// 1198 unittest 1199 { 1200 }