1 /++ 2 Authors: 3 Manuzor 4 5 ToDo: 6 - Path().open(...) 7 +/ 8 module pathlib; 9 10 static import std.path; 11 static import std.file; 12 static import std.uni; 13 import std.array : array, replace, replaceLast; 14 import std.format : format; 15 import std.string : split, indexOf, empty, squeeze, removechars, lastIndexOf; 16 import std.conv : to; 17 import std.typecons : Flag; 18 import std.traits : isSomeString, isSomeChar; 19 import std.algorithm : equal, map, reduce, startsWith, endsWith, strip, stripRight, stripLeft, remove; 20 import std.range : iota, take, isInputRange; 21 22 23 debug 24 { 25 void logDebug(T...)(T args) { 26 import std.stdio; 27 28 writefln(args); 29 } 30 } 31 32 void assertEqual(alias predicate = "a == b", A, B, StringType = string) 33 (A a, B b, StringType fmt = "`%s` must be equal to `%s`") 34 { 35 static if (isInputRange!A && isInputRange!B) { 36 assert(std.algorithm.equal!predicate(a, b), format(fmt, a, b)); 37 } 38 else { 39 import std.functional : binaryFun; 40 assert(binaryFun!predicate(a, b), format(fmt, a, b)); 41 } 42 } 43 44 void assertNotEqual(alias predicate = "a != b", A, B, StringType = string) 45 (A a, B b, StringType fmt = "`%s` must not be equal to `%s`") 46 { 47 assertEqual!predicate(a, b, fmt); 48 } 49 50 void assertEmpty(A, StringType = string)(A a, StringType fmt = "String `%s` should be empty.") { 51 assert(a.empty, format(fmt, a)); 52 } 53 54 void assertNotEmpty(A, StringType = string)(A a, StringType fmt = "String `%s` should be empty.") { 55 assert(!a.empty, format(fmt, a)); 56 } 57 58 59 /// Whether $(D str) can be represented by ".". 60 bool isDot(StringType)(auto ref in StringType str) 61 if (isSomeString!StringType) 62 { 63 if (str.length && str[0] != '.') { 64 return false; 65 } 66 auto data = str.removechars("./\\"); 67 return data.empty; 68 } 69 70 /// 71 unittest { 72 assert("".isDot); 73 assert(".".isDot); 74 assert("./".isDot); 75 assert("./.".isDot); 76 assert("././".isDot); 77 assert(!"/".isDot); 78 assert(!"hello".isDot); 79 assert(!".git".isDot); 80 } 81 82 83 /// Whether the path is either "" or ".". 84 auto isDot(PathType)(auto ref in PathType p) 85 if (!isSomeString!PathType) 86 { 87 return p.data.isDot; 88 } 89 90 /// 91 unittest { 92 assert(WindowsPath().isDot); 93 assert(WindowsPath("").isDot); 94 assert(WindowsPath(".").isDot); 95 assert(PosixPath().isDot); 96 assert(PosixPath("").isDot); 97 assert(PosixPath(".").isDot); 98 } 99 100 101 /// Returns: The root of the path $(D p). 102 auto root(PathType)(auto ref in PathType p) { 103 auto data = p.data; 104 105 static if(is(PathType == WindowsPath)) 106 { 107 if (data.length > 1 && data[1] == ':') { 108 return data[0..2]; 109 } 110 } 111 else // Assume PosixPath 112 { 113 if (data.length > 0 && data[0] == '/') { 114 return data[0..1]; 115 } 116 } 117 118 return ""; 119 } 120 121 /// 122 unittest { 123 assertEqual(WindowsPath("").root, ""); 124 assertEqual(PosixPath("").root, ""); 125 assertEqual(WindowsPath("C:/Hello/World").root, "C:"); 126 assertEqual(WindowsPath("/Hello/World").root, ""); 127 assertEqual(PosixPath("/hello/world").root, "/"); 128 assertEqual(PosixPath("C:/hello/world").root, ""); 129 } 130 131 132 /// The drive of the path $(D p). 133 /// Note: Non-Windows platforms have no concept of "drives". 134 auto drive(PathType)(auto ref in PathType p) { 135 static if(is(PathType == WindowsPath)) 136 { 137 auto data = p.data; 138 if (data.length > 1 && data[1] == ':') { 139 return data[0..2]; 140 } 141 } 142 143 return ""; 144 } 145 146 /// 147 unittest { 148 assertEqual(WindowsPath("").drive, ""); 149 assertEqual(WindowsPath("/Hello/World").drive, ""); 150 assertEqual(WindowsPath("C:/Hello/World").drive, "C:"); 151 assertEqual(PosixPath("").drive, ""); 152 assertEqual(PosixPath("/Hello/World").drive, ""); 153 assertEqual(PosixPath("C:/Hello/World").drive, ""); 154 } 155 156 157 /// Returns: The path data using forward slashes, regardless of the current platform. 158 auto posixData(PathType)(auto ref in PathType p) { 159 if (p.isDot) { 160 return "."; 161 } 162 auto root = p.root; 163 auto result = p.data[root.length..$].replace("\\", "/").squeeze("/"); 164 if (result.length > 1 && result[$ - 1] == '/') { 165 result = result[0..$ - 1]; 166 } 167 while (result.endsWith("/.")) { 168 result = result[0 .. $ - 2].squeeze("/"); 169 } 170 return root ~ result; 171 } 172 173 /// 174 unittest { 175 assertEqual(Path().posixData, "."); 176 assertEqual(Path("").posixData, "."); 177 assertEqual(Path("/hello/world").posixData, "/hello/world"); 178 assertEqual(Path("/\\hello/\\/////world//").posixData, "/hello/world"); 179 assertEqual(Path(`C:\`).posixData, "C:/"); 180 assertEqual(Path(`C:\hello\`).posixData, "C:/hello"); 181 assertEqual(Path(`C:\/\hello\`).posixData, "C:/hello"); 182 assertEqual(Path(`C:\some windows\/path.exe.doodee`).posixData, "C:/some windows/path.exe.doodee"); 183 assertEqual(Path(`C:\some windows\/path.exe.doodee\\\`).posixData, "C:/some windows/path.exe.doodee"); 184 assertEqual(Path(`C:\some windows\/path.exe.doodee\\\`).posixData, Path(Path(`C:\some windows\/path.exe.doodee\\\`).posixData).data); 185 assertEqual((Path(".") ~ "hello" ~ "./" ~ "world").posixData, "hello/world"); 186 assertEqual(Path("hello/.").posixData, "hello"); 187 assertEqual(Path("hello/././.").posixData, "hello"); 188 assertEqual(Path("hello/././/.//").posixData, "hello"); 189 } 190 191 192 /// Returns: The path data using backward slashes, regardless of the current platform. 193 auto windowsData(PathType)(auto ref in PathType p) { 194 return p.posixData.replace("/", `\`); 195 } 196 197 /// 198 unittest { 199 assertEqual(Path().windowsData, "."); 200 assertEqual(Path("").windowsData, Path("").windowsData); 201 assertEqual(Path("/hello/world").windowsData, `\hello\world`); 202 assertEqual(Path("/\\hello/\\/////world//").windowsData, `\hello\world`); 203 assertEqual(Path(`C:\`).windowsData, `C:\`); 204 assertEqual(Path(`C:/`).windowsData, `C:\`); 205 assertEqual(Path(`C:\hello\`).windowsData, `C:\hello`); 206 assertEqual(Path(`C:\/\hello\`).windowsData, `C:\hello`); 207 assertEqual(Path(`C:\some windows\/path.exe.doodee`).windowsData, `C:\some windows\path.exe.doodee`); 208 assertEqual(Path(`C:\some windows\/path.exe.doodee\\\`).windowsData, `C:\some windows\path.exe.doodee`); 209 assertEqual(Path(`C:/some windows\/path.exe.doodee\\\`).windowsData, Path(Path(`C:\some windows\/path.exe.doodee\\\`).windowsData).data); 210 } 211 212 213 /// Will call either posixData or windowsData, according to PathType. 214 auto normalizedData(PathType)(auto ref in PathType p) 215 if(is(PathType == PosixPath) || is(PathType == WindowsPath)) 216 { 217 static if (is(PathType == PosixPath)) { 218 return p.posixData; 219 } 220 else static if (is(PathType == WindowsPath)) { 221 return p.windowsData; 222 } 223 } 224 225 226 auto asPosixPath(PathType)(auto ref in PathType p) { 227 return PathType(p.posixData); 228 } 229 230 auto asWindowsPath(PathType)(auto ref in PathType p) { 231 return PathType(p.windowsData); 232 } 233 234 auto asNormalizedPath(PathType)(auto ref in PathType p) { 235 return PathType(p.normalizedData); 236 } 237 238 239 /// Whether the path is absolute. 240 auto isAbsolute(PathType)(auto ref in PathType p) { 241 // If the path has a root or a drive, it is absolute. 242 return !p.root.empty || !p.drive.empty; 243 } 244 245 246 /// Returns: The parts of the path as an array. 247 auto parts(PathType)(auto ref in PathType p) { 248 string[] theParts; 249 auto root = p.root; 250 if (!root.empty) { 251 static if(is(PathType == WindowsPath)) { 252 root ~= p.separator; 253 } 254 theParts ~= root; 255 } 256 theParts ~= p.posixData[root.length .. $].strip('/').split('/')[]; 257 return theParts; 258 } 259 260 /// 261 unittest { 262 assertEqual(Path().parts, ["."]); 263 assertEqual(Path("./.").parts, ["."]); 264 assertEqual(WindowsPath("hello/world.exe.xml").parts, ["hello", "world.exe.xml"]); 265 assertEqual(WindowsPath("C:/hello/world.exe.xml").parts, [`C:\`, "hello", "world.exe.xml"]); 266 assertEqual(PosixPath("hello/world.exe.xml").parts, ["hello", "world.exe.xml"]); 267 assertEqual(PosixPath("/hello/world.exe.xml").parts, ["/", "hello", "world.exe.xml"]); 268 } 269 270 271 auto parent(PathType)(auto ref in PathType p) { 272 auto theParts = p.parts.map!(a => PathType(a)); 273 if (theParts.length > 1) { 274 return theParts[0 .. $ - 1].reduce!((a, b){ return a ~ b;}); 275 } 276 return PathType(); 277 } 278 279 /// 280 unittest { 281 assertEqual(Path().parent, Path()); 282 assertEqual(Path("IHaveNoParents").parent, Path()); 283 assertEqual(WindowsPath("C:/hello/world").parent, WindowsPath(`C:\hello`)); 284 assertEqual(WindowsPath("C:/hello/world/").parent, WindowsPath(`C:\hello`)); 285 assertEqual(WindowsPath("C:/hello/world.exe.foo").parent, WindowsPath(`C:\hello`)); 286 assertEqual(PosixPath("/hello/world").parent, PosixPath("/hello")); 287 assertEqual(PosixPath("/hello/\\/world/").parent, PosixPath("/hello")); 288 assertEqual(PosixPath("/hello.foo.bar/world/").parent, PosixPath("/hello.foo.bar")); 289 } 290 291 292 /// Returns: The parts of the path as an array, without the last component. 293 auto parents(PathType)(auto ref in PathType p) { 294 auto theParts = p.parts.map!(x => PathType(x)); 295 return iota(theParts.length - 1, 0, -1).map!(x => theParts.take(x).reduce!((a, b){ return a ~ b; })).array; 296 } 297 298 /// 299 unittest { 300 assertEmpty(Path().parents); 301 assertEqual(WindowsPath("C:/hello/world").parents, [WindowsPath(`C:\hello`), WindowsPath(`C:\`)]); 302 assertEqual(WindowsPath("C:/hello/world/").parents, [WindowsPath(`C:\hello`), WindowsPath(`C:\`)]); 303 assertEqual(PosixPath("/hello/world").parents, [PosixPath("/hello"), PosixPath("/")]); 304 assertEqual(PosixPath("/hello/world/").parents, [PosixPath("/hello"), PosixPath("/")]); 305 } 306 307 308 /// The name of the path without any of its parents. 309 auto name(PathType)(auto ref in PathType p) { 310 import std.algorithm : min; 311 312 auto data = p.posixData; 313 auto i = min(data.lastIndexOf('/') + 1, data.length); 314 return data[i .. $]; 315 } 316 317 /// 318 unittest { 319 assertEqual(Path().name, "."); 320 assertEqual(Path("").name, "."); 321 assertEmpty(Path("/").name); 322 assertEqual(Path("/hello").name, "hello"); 323 assertEqual(Path("C:\\hello").name, "hello"); 324 assertEqual(Path("C:/hello/world.exe").name, "world.exe"); 325 assertEqual(Path("hello/world.foo.bar.exe").name, "world.foo.bar.exe"); 326 } 327 328 329 /// The extension of the path including the leading dot. 330 /// 331 /// Examples: The extension of "hello.foo.bar.exe" is "exe". 332 auto extension(PathType)(auto ref in PathType p) { 333 auto data = p.name; 334 auto i = data.lastIndexOf('.'); 335 if (i < 0) { 336 return ""; 337 } 338 if (i + 1 == data.length) { 339 // This prevents preserving the dot in empty extensions such as `hello.foo.`. 340 ++i; 341 } 342 return data[i .. $]; 343 } 344 345 /// 346 unittest { 347 assertEmpty(Path().extension); 348 assertEmpty(Path("").extension); 349 assertEmpty(Path("/").extension); 350 assertEmpty(Path("/hello").extension); 351 assertEmpty(Path("C:/hello/world").extension); 352 assertEqual(Path("C:/hello/world.exe").extension, ".exe"); 353 assertEqual(Path("hello/world.foo.bar.exe").extension, ".exe"); 354 } 355 356 357 /// All extensions of the path. 358 auto extensions(PathType)(auto ref in PathType p) { 359 import std.algorithm : splitter, filter; 360 import std.range : dropOne; 361 362 auto result = p.name.splitter('.').filter!(a => !a.empty); 363 if (!result.empty) { 364 result = result.dropOne; 365 } 366 return result.map!(a => '.' ~ a).array; 367 } 368 369 /// 370 unittest { 371 assertEmpty(Path().extensions); 372 assertEmpty(Path("").extensions); 373 assertEmpty(Path("/").extensions); 374 assertEmpty(Path("/hello").extensions); 375 assertEmpty(Path("C:/hello/world").extensions); 376 assertEqual(Path("C:/hello/world.exe").extensions, [".exe"]); 377 assertEqual(Path("hello/world.foo.bar.exe").extensions, [".foo", ".bar", ".exe"]); 378 } 379 380 381 /// The full extension of the path. 382 /// 383 /// Examples: The full extension of "hello.foo.bar.exe" would be "foo.bar.exe". 384 auto fullExtension(PathType)(auto ref in PathType p) { 385 auto data = p.name; 386 auto i = data.indexOf('.'); 387 if (i < 0) { 388 return ""; 389 } 390 if (i + 1 == data.length) { 391 // This prevents preserving the dot in empty extensions such as `hello.foo.`. 392 ++i; 393 } 394 return data[i .. $]; 395 } 396 397 /// 398 unittest { 399 assertEmpty(Path().fullExtension); 400 assertEmpty(Path("").fullExtension); 401 assertEmpty(Path("/").fullExtension); 402 assertEmpty(Path("/hello").fullExtension); 403 assertEmpty(Path("C:/hello/world").fullExtension); 404 assertEqual(Path("C:/hello/world.exe").fullExtension, ".exe"); 405 assertEqual(Path("hello/world.foo.bar.exe").fullExtension, ".foo.bar.exe"); 406 } 407 408 409 /// The name of the path without its extension. 410 auto stem(PathType)(auto ref in PathType p) { 411 auto data = p.name; 412 auto i = data.indexOf('.'); 413 if (i < 0) { 414 return data; 415 } 416 if (i + 1 == data.length) { 417 // This prevents preserving the dot in empty extensions such as `hello.foo.`. 418 ++i; 419 } 420 return data[0 .. i]; 421 } 422 423 /// 424 unittest { 425 assertEqual(Path().stem, "."); 426 assertEqual(Path("").stem, "."); 427 assertEqual(Path("/").stem, ""); 428 assertEqual(Path("/hello").stem, "hello"); 429 assertEqual(Path("C:/hello/world").stem, "world"); 430 assertEqual(Path("C:/hello/world.exe").stem, "world"); 431 assertEqual(Path("hello/world.foo.bar.exe").stem, "world"); 432 } 433 434 435 /// Whether the given path matches the given glob-style pattern 436 auto match(PathType, Pattern)(auto ref in PathType p, Pattern pattern) { 437 import std.path : globMatch; 438 439 return p.normalizedData.globMatch!(PathType.caseSensitivity)(pattern); 440 } 441 442 /// 443 unittest { 444 assert(Path().match("*")); 445 assert(Path("").match("*")); 446 assert(Path(".").match("*")); 447 assert(Path("/").match("*")); 448 assert(Path("/hello").match("*")); 449 assert(Path("/hello/world.exe").match("*")); 450 assert(Path("/hello/world.exe").match("*.exe")); 451 assert(!Path("/hello/world.exe").match("*.zip")); 452 assert(WindowsPath("/hello/world.EXE").match("*.exe")); 453 assert(!PosixPath("/hello/world.EXE").match("*.exe")); 454 } 455 456 457 /// Whether the path exists or not. It does not matter whether it is a file or not. 458 bool exists(in Path p) { 459 return std.file.exists(p.data); 460 } 461 462 /// 463 unittest { 464 } 465 466 467 /// Whether the path is an existing directory. 468 bool isDir(in Path p) { 469 return p.exists && std.file.isDir(p.data); 470 } 471 472 /// 473 unittest { 474 } 475 476 477 /// Whether the path is an existing file. 478 bool isFile(in Path p) { 479 return p.exists && std.file.isFile(p.data); 480 } 481 482 /// 483 unittest { 484 } 485 486 487 /// Whether the given path $(D p) points to a symbolic link (or junction point in Windows). 488 bool isSymlink(in Path p) { 489 return std.file.isSymlink(p.normalizedData); 490 } 491 492 /// 493 unittest { 494 } 495 496 497 // Resolve all ".", "..", and symlinks. 498 Path resolve(in Path p) { 499 return Path(Path(std.path.absolutePath(p.data)).normalizedData); 500 } 501 502 /// 503 unittest { 504 assertNotEqual(Path(), Path().resolve()); 505 } 506 507 508 /// The absolute path to the current working directory with symlinks and friends resolved. 509 Path cwd() { 510 return Path().resolve(); 511 } 512 513 /// 514 unittest { 515 assertNotEmpty(cwd().data); 516 } 517 518 519 /// The path to the current executable. 520 Path currentExePath() { 521 return Path(std.file.thisExePath()).resolve(); 522 } 523 524 /// 525 unittest { 526 assertNotEmpty(currentExePath().data); 527 } 528 529 530 void mkdir(in Path p) { 531 std.file.mkdirRecurse(p.normalizedData); 532 } 533 534 535 void chdir(in Path p) { 536 std.file.chdir(p.normalizedData); 537 } 538 539 540 /// Generate an array of Paths that match the given pattern in and beneath the given path. 541 auto glob(PatternType)(auto ref in Path p, PatternType pattern) { 542 import std.algorithm : filter; 543 import std.file : SpanMode; 544 545 return std.file.dirEntries(p.normalizedData, pattern, SpanMode.shallow) 546 .map!(a => Path(a.name)); 547 } 548 549 /// 550 unittest { 551 assertNotEmpty(currentExePath().parent.glob("*")); 552 } 553 554 555 /// Generate an array of Paths that match the given pattern in and beneath the given path. 556 auto rglob(PatternType)(auto ref in Path p, PatternType pattern) { 557 import std.algorithm : filter; 558 import std.file : SpanMode; 559 560 return std.file.dirEntries(p.normalizedData, pattern, SpanMode.breadth) 561 .map!(a => Path(a.name)); 562 } 563 564 /// 565 unittest { 566 assertNotEmpty(currentExePath().parent.rglob("*")); 567 } 568 569 570 auto open(in Path p, in char[] openMode = "rb") { 571 static import std.stdio; 572 573 return std.stdio.File(p.normalizedData, openMode); 574 } 575 576 /// 577 unittest { 578 } 579 580 581 mixin template PathCommon(PathType, StringType, alias theSeparator, alias theCaseSensitivity) 582 if (isSomeString!StringType && isSomeChar!(typeof(theSeparator))) 583 { 584 StringType data = "."; 585 586 /// 587 unittest { 588 assert(PathType().data != ""); 589 assert(PathType("123").data == "123"); 590 assert(PathType("C:///toomany//slashes!\\").data == "C:///toomany//slashes!\\"); 591 } 592 593 594 /// Used to separate each path segment. 595 alias separator = theSeparator; 596 597 /// A value of std.path.CaseSensetive whether this type of path is case sensetive or not. 598 alias caseSensitivity = theCaseSensitivity; 599 600 601 /// Concatenate a path and a string, which will be treated as a path. 602 auto opBinary(string op, InStringType)(auto ref in InStringType str) const 603 if (op == "~" && isSomeString!InStringType) 604 { 605 return this ~ PathType(str); 606 } 607 608 /// Concatenate two paths. 609 auto opBinary(string op)(auto ref in PathType other) const 610 if (op == "~") 611 { 612 auto p = PathType(data); 613 p ~= other; 614 return p; 615 } 616 617 /// Concatenate the path-string $(D str) to this path. 618 void opOpAssign(string op, InStringType)(auto ref in InStringType str) 619 if (op == "~" && isSomeString!InStringType) 620 { 621 this ~= PathType(str); 622 } 623 624 /// Concatenate the path $(D other) to this path. 625 void opOpAssign(string op)(auto ref in PathType other) 626 if (op == "~") 627 { 628 auto l = PathType(this.normalizedData); 629 auto r = PathType(other.normalizedData); 630 631 if (l.isDot || r.isAbsolute) { 632 this.data = r.data; 633 return; 634 } 635 636 if (r.isDot) { 637 this.data = l.data; 638 return; 639 } 640 641 auto sep = ""; 642 if (!l.data.endsWith('/', '\\') && !r.data.startsWith('/', '\\')) { 643 sep = [separator]; 644 } 645 646 this.data = format("%s%s%s", l.data, sep, r.data); 647 } 648 649 /// 650 unittest { 651 assertEqual(PathType() ~ "hello", PathType("hello")); 652 assertEqual(PathType("") ~ "hello", PathType("hello")); 653 assertEqual(PathType(".") ~ "hello", PathType("hello")); 654 assertEqual(PosixPath("/") ~ "hello", PosixPath("/hello")); 655 assertEqual(WindowsPath("/") ~ "hello", WindowsPath(`\hello`)); 656 assertEqual(PosixPath("/") ~ "hello" ~ "world", PosixPath("/hello/world")); 657 assertEqual(WindowsPath(`C:\`) ~ "hello" ~ "world", WindowsPath(`C:\hello\world`)); 658 } 659 660 661 /// Equality overload. 662 bool opBinary(string op)(auto ref in PathType other) const 663 if (op == "==") 664 { 665 auto l = this.data.empty ? "." : this.data; 666 auto r = other.data.empty ? "." : other.data; 667 static if (theCaseSensitivity == std.path.CaseSensetive.no) { 668 return std.uni.sicmp(l, r); 669 } 670 else { 671 return std.algorithm.cmp(l, r); 672 } 673 } 674 675 /// 676 unittest { 677 auto p1 = PathType("/hello/world"); 678 auto p2 = PathType("/hello/world"); 679 assertEqual(p1, p2); 680 } 681 682 683 /// Cast the path to a string. 684 auto toString(OtherStringType = StringType)() const { 685 return this.data.to!OtherStringType; 686 } 687 688 /// 689 unittest { 690 assertEqual(PathType("C:/hello/world.exe.xml").to!StringType, "C:/hello/world.exe.xml"); 691 } 692 } 693 694 695 struct WindowsPath 696 { 697 mixin PathCommon!(WindowsPath, string, '\\', std.path.CaseSensitive.no); 698 } 699 700 701 struct PosixPath 702 { 703 mixin PathCommon!(PosixPath, string, '/', std.path.CaseSensitive.yes); 704 } 705 706 /// Set the default path depending on the current platform. 707 version(Windows) 708 { 709 alias Path = WindowsPath; 710 } 711 else // Assume posix on non-windows platforms. 712 { 713 alias Path = PosixPath; 714 }