1 // Scriptlike: Utility to aid in script-like programs. 2 // Written in the D programming language. 3 4 /// Copyright: Copyright (C) 2014-2015 Nick Sabalausky 5 /// License: zlib/libpng 6 /// Authors: Nick Sabalausky 7 8 module scriptlike.path; 9 10 import std.algorithm; 11 import std.conv; 12 import std.datetime; 13 import std.file; 14 import std.process; 15 import std.range; 16 import std.stdio; 17 import std.string; 18 import std.traits; 19 import std.typecons; 20 import std.typetuple; 21 22 static import std.path; 23 public import std.path : dirSeparator, pathSeparator, isDirSeparator, 24 CaseSensitive, osDefaultCaseSensitivity, buildPath, buildNormalizedPath; 25 26 /// Represents a file extension. 27 struct Ext 28 { 29 private string str; 30 31 /// Main constructor. 32 this(string extension = null) pure @safe nothrow 33 { 34 this.str = extension; 35 } 36 37 /// Convert to string. 38 string toString() pure @safe nothrow 39 { 40 return str; 41 } 42 43 /// No longer needed. Use Ext.toString() instead. 44 string toRawString() 45 { 46 return str; 47 } 48 49 /// Compare using OS-specific case-sensitivity rules. If you want to force 50 /// case-sensitive or case-insensistive, then call filenameCmp instead. 51 int opCmp(ref const Ext other) const 52 { 53 return std.path.filenameCmp(this.str, other.str); 54 } 55 56 ///ditto 57 int opCmp(Ext other) const 58 { 59 return std.path.filenameCmp(this.str, other.str); 60 } 61 62 ///ditto 63 int opCmp(string other) const 64 { 65 return std.path.filenameCmp(this.str, other); 66 } 67 68 /// Compare using OS-specific case-sensitivity rules. If you want to force 69 /// case-sensitive or case-insensistive, then call filenameCmp instead. 70 int opEquals(ref const Ext other) const 71 { 72 return opCmp(other) == 0; 73 } 74 75 ///ditto 76 int opEquals(Ext other) const 77 { 78 return opCmp(other) == 0; 79 } 80 81 ///ditto 82 int opEquals(string other) const 83 { 84 return opCmp(other) == 0; 85 } 86 87 /// Convert to bool 88 T opCast(T)() if(is(T==bool)) 89 { 90 return !!str; 91 } 92 } 93 94 /// Represents a filesystem path. The path is always kept normalized 95 /// automatically (as performed by buildNormalizedPathFixed). 96 struct Path 97 { 98 private string str = "."; 99 100 /// Main constructor. 101 this(string path = ".") @safe pure nothrow 102 { 103 this.str = buildNormalizedPathFixed(path); 104 } 105 106 @trusted pure nothrow invariant() 107 { 108 assert(str == buildNormalizedPathFixed(str)); 109 } 110 111 /// Convert to string, quoting or escaping spaces if necessary. 112 string toString() 113 { 114 return escapeShellArg(str); 115 } 116 117 /// Returns the underlying string. Does NOT do any escaping, even if path contains spaces. 118 string toRawString() const 119 { 120 return str; 121 } 122 123 /// Concatenates two paths, with a directory separator in between. 124 Path opBinary(string op)(Path rhs) if(op=="~") 125 { 126 Path newPath; 127 newPath.str = buildNormalizedPathFixed(this.str, rhs.str); 128 return newPath; 129 } 130 131 ///ditto 132 Path opBinary(string op)(string rhs) if(op=="~") 133 { 134 Path newPath; 135 newPath.str = buildNormalizedPathFixed(this.str, rhs); 136 return newPath; 137 } 138 139 ///ditto 140 Path opBinaryRight(string op)(string lhs) if(op=="~") 141 { 142 Path newPath; 143 newPath.str = buildNormalizedPathFixed(lhs, this.str); 144 return newPath; 145 } 146 147 /// Appends an extension to a path. Naturally, a directory separator 148 /// is NOT inserted in between. 149 Path opBinary(string op)(Ext rhs) if(op=="~") 150 { 151 Path newPath; 152 newPath.str = std.path.setExtension(this.str, rhs.str); 153 return newPath; 154 } 155 156 /// Appends a path to this one, with a directory separator in between. 157 Path opOpAssign(string op)(Path rhs) if(op=="~") 158 { 159 str = buildNormalizedPathFixed(str, rhs.str); 160 return this; 161 } 162 163 ///ditto 164 Path opOpAssign(string op)(string rhs) if(op=="~") 165 { 166 str = buildNormalizedPathFixed(str, rhs); 167 return this; 168 } 169 170 /// Appends an extension to this path. Naturally, a directory separator 171 /// is NOT inserted in between. 172 Path opOpAssign(string op)(Ext rhs) if(op=="~") 173 { 174 str = std.path.setExtension(str, rhs.str); 175 return this; 176 } 177 178 /// Compare using OS-specific case-sensitivity rules. If you want to force 179 /// case-sensitive or case-insensistive, then call filenameCmp instead. 180 int opCmp(ref const Path other) const 181 { 182 return std.path.filenameCmp(this.str, other.str); 183 } 184 185 ///ditto 186 int opCmp(Path other) const 187 { 188 return std.path.filenameCmp(this.str, other.str); 189 } 190 191 ///ditto 192 int opCmp(string other) const 193 { 194 return std.path.filenameCmp(this.str, other); 195 } 196 197 /// Compare using OS-specific case-sensitivity rules. If you want to force 198 /// case-sensitive or case-insensistive, then call filenameCmp instead. 199 int opEquals(ref const Path other) const 200 { 201 return opCmp(other) == 0; 202 } 203 204 ///ditto 205 int opEquals(Path other) const 206 { 207 return opCmp(other) == 0; 208 } 209 210 ///ditto 211 int opEquals(string other) const 212 { 213 return opCmp(other) == 0; 214 } 215 216 /// Convert to bool 217 T opCast(T)() if(is(T==bool)) 218 { 219 return !!str; 220 } 221 222 /// Returns the parent path, according to std.path.dirName. 223 @property Path up() 224 { 225 return this.dirName(); 226 } 227 228 /// Is this path equal to empty string? 229 @property bool empty() 230 { 231 return str == ""; 232 } 233 } 234 235 /// Convenience aliases 236 alias extOf = extension; 237 alias stripExt = stripExtension; ///ditto 238 alias setExt = setExtension; ///ditto 239 alias defaultExt = defaultExtension; ///ditto 240 241 /// Like buildNormalizedPath, but if the result is the current directory, 242 /// this returns "." instead of "". However, if all the inputs are "", or there 243 /// are no inputs, this still returns "" just like buildNormalizedPath. 244 string buildNormalizedPathFixed(string[] paths...) 245 @trusted pure nothrow 246 { 247 if(all!`a is null`(paths)) 248 return null; 249 250 if(all!`a==""`(paths)) 251 return ""; 252 253 auto result = std.path.buildNormalizedPath(paths); 254 return result==""? "." : result; 255 } 256 257 /// Properly escape arguments containing spaces for the command shell, if necessary. 258 string escapeShellArg(string str) 259 { 260 if(str.canFind(' ')) 261 { 262 version(Windows) 263 return escapeWindowsArgument(str); 264 else version(Posix) 265 return escapeShellFileName(str); 266 else 267 static assert(0, "This platform not supported."); 268 } 269 else 270 return str; 271 } 272 273 // -- std.path wrappers to support Path type -------------------- 274 275 /// Just like std.path.baseName, but operates on Path. 276 Path baseName(Path path) 277 @trusted pure 278 { 279 return Path( std.path.baseName(path.str) ); 280 } 281 282 ///ditto 283 Path baseName(CaseSensitive cs = CaseSensitive.osDefault) 284 (Path path, in string suffix) 285 @safe pure 286 { 287 return Path( std.path.baseName!cs(path.str, suffix) ); 288 } 289 /// Just like std.path.dirName, but operates on Path. 290 Path dirName(Path path) 291 { 292 return Path( std.path.dirName(path.str) ); 293 } 294 295 /// Just like std.path.rootName, but operates on Path. 296 Path rootName(Path path) @safe pure nothrow 297 { 298 return Path( std.path.rootName(path.str) ); 299 } 300 301 /// Just like std.path.driveName, but operates on Path. 302 Path driveName(Path path) @safe pure nothrow 303 { 304 return Path( std.path.driveName(path.str) ); 305 } 306 307 /// Just like std.path.stripDrive, but operates on Path. 308 Path stripDrive(Path path) @safe pure nothrow 309 { 310 return Path( std.path.stripDrive(path.str) ); 311 } 312 313 /// Just like std.path.extension, but takes a Path and returns an Ext. 314 Ext extension(in Path path) @safe pure nothrow 315 { 316 return Ext( std.path.extension(path.str) ); 317 } 318 319 /// Just like std.path.stripExtension, but operates on Path. 320 Path stripExtension(Path path) @safe pure nothrow 321 { 322 return Path( std.path.stripExtension(path.str) ); 323 } 324 325 /// Just like std.path.setExtension, but operates on Path. 326 Path setExtension(Path path, string ext) 327 @trusted pure nothrow 328 { 329 return Path( std.path.setExtension(path.str, ext) ); 330 } 331 332 ///ditto 333 Path setExtension(Path path, Ext ext) 334 @trusted pure nothrow 335 { 336 return path.setExtension(ext.toString()); 337 } 338 339 /// Just like std.path.defaultExtension, but operates on Path and optionally Ext. 340 Path defaultExtension(Path path, in string ext) 341 @trusted pure 342 { 343 return Path( std.path.defaultExtension(path.str, ext) ); 344 } 345 346 ///ditto 347 Path defaultExtension(Path path, Ext ext) 348 @trusted pure 349 { 350 return path.defaultExtension(ext.toString()); 351 } 352 353 /// Just like std.path.pathSplitter. Note this returns a range of strings, 354 /// not a range of Path. 355 auto pathSplitter(Path path) @safe pure nothrow 356 { 357 return std.path.pathSplitter(path.str); 358 } 359 360 /// Just like std.path.isRooted, but operates on Path. 361 bool isRooted(in Path path) @safe pure nothrow 362 { 363 return std.path.isRooted(path.str); 364 } 365 366 /// Just like std.path.isAbsolute, but operates on Path. 367 bool isAbsolute(in Path path) @safe pure nothrow 368 { 369 return std.path.isAbsolute(path.str); 370 } 371 372 /// Just like std.path.absolutePath, but operates on Path. 373 Path absolutePath(Path path, lazy string base = getcwd()) 374 @safe pure 375 { 376 return Path( std.path.absolutePath(path.str, base) ); 377 } 378 379 ///ditto 380 Path absolutePath(Path path, Path base) 381 @safe pure 382 { 383 return Path( std.path.absolutePath(path.str, base.str.to!string()) ); 384 } 385 386 /// Just like std.path.relativePath, but operates on Path. 387 Path relativePath(CaseSensitive cs = CaseSensitive.osDefault) 388 (Path path, lazy string base = getcwd()) 389 { 390 return Path( std.path.relativePath!cs(path.str, base) ); 391 } 392 393 ///ditto 394 Path relativePath(CaseSensitive cs = CaseSensitive.osDefault) 395 (Path path, Path base) 396 { 397 return Path( std.path.relativePath!cs(path.str, base.str.to!string()) ); 398 } 399 400 /// Just like std.path.filenameCmp, but operates on Path. 401 int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault) 402 (Path path, Path filename2) 403 @safe pure 404 { 405 return std.path.filenameCmp(path.str, filename2.str); 406 } 407 408 ///ditto 409 int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault) 410 (Path path, string filename2) 411 @safe pure 412 { 413 return std.path.filenameCmp(path.str, filename2); 414 } 415 416 ///ditto 417 int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault) 418 (string path, Path filename2) 419 @safe pure 420 { 421 return std.path.filenameCmp(path, filename2.str); 422 } 423 424 /// Just like std.path.globMatch, but operates on Path. 425 bool globMatch(CaseSensitive cs = CaseSensitive.osDefault) 426 (Path path, string pattern) 427 @safe pure nothrow 428 { 429 return std.path.globMatch!cs(path.str, pattern); 430 } 431 432 /// Just like std.path.isValidFilename, but operates on Path. 433 bool isValidFilename(in Path path) @safe pure nothrow 434 { 435 return std.path.isValidFilename(path.str); 436 } 437 438 /// Just like std.path.isValidPath, but operates on Path. 439 bool isValidPath(in Path path) @safe pure nothrow 440 { 441 return std.path.isValidPath(path.str); 442 } 443 444 /// Just like std.path.expandTilde, but operates on Path. 445 Path expandTilde(Path path) 446 { 447 return Path( std.path.expandTilde(path.str) ); 448 } 449 450 // The unittests in this module mainly check that all the templates compile 451 // correctly and that the appropriate Phobos functions are correctly called. 452 // 453 // A completely thorough testing of the behavior of such functions is 454 // occasionally left to Phobos itself as it is outside the scope of these tests. 455 456 version(unittest_scriptlike_d) 457 unittest 458 { 459 import std.stdio : writeln; 460 writeln("Running Scriptlike unittests: std.path wrappers"); 461 462 alias dirSep = dirSeparator; 463 464 { 465 auto e = Ext(".txt"); 466 assert(e != Ext(".dat")); 467 assert(e == Ext(".txt")); 468 version(Windows) 469 assert(e == Ext(".TXT")); 470 else version(OSX) 471 assert(e == Ext(".TXT")); 472 else version(Posix) 473 assert(e != Ext(".TXT")); 474 else 475 static assert(0, "This platform not supported."); 476 477 // Test the other comparison overloads 478 assert(e != Ext(".dat")); 479 assert(e == Ext(".txt")); 480 assert(Ext(".dat") != e); 481 assert(Ext(".txt") == e); 482 assert(".dat" != e); 483 assert(".txt" == e); 484 485 assert(Ext("foo")); 486 assert(Ext("")); 487 assert(Ext(null).str is null); 488 assert(!Ext(null)); 489 } 490 491 auto p = Path(); 492 assert(p.str == "."); 493 assert(!p.empty); 494 495 assert(Path("").empty); 496 497 assert(Path("foo")); 498 assert(Path("")); 499 assert(Path(null).str is null); 500 assert(!Path(null)); 501 502 version(Windows) 503 auto testStrings = ["/foo/bar", "/foo/bar/", `\foo\bar`, `\foo\bar\`]; 504 else version(Posix) 505 auto testStrings = ["/foo/bar", "/foo/bar/"]; 506 else 507 static assert(0, "This platform not supported."); 508 509 foreach(str; testStrings) 510 { 511 writeln(" testing str: ", str); 512 513 p = Path(str); 514 assert(!p.empty); 515 assert(p.str == dirSep~"foo"~dirSep~"bar"); 516 517 p = Path(str); 518 assert(p.str == dirSep~"foo"~dirSep~"bar"); 519 assert(p.toRawString() == p.str); 520 assert(p.toString() == p.str.to!string()); 521 522 assert(p.up.toString() == dirSep~"foo"); 523 assert(p.up.up.toString() == dirSep); 524 525 assert((p~"sub").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"sub"); 526 assert((p~"sub"~"2").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"sub"~dirSep~"2"); 527 assert((p~Path("sub")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"sub"); 528 529 version(Windows) 530 assert((p~"sub dir").toString() == `"`~dirSep~"foo"~dirSep~"bar"~dirSep~"sub dir"~`"`); 531 else version(Posix) 532 assert((p~"sub dir").toString() == `'`~dirSep~"foo"~dirSep~"bar"~dirSep~`sub dir'`); 533 else 534 static assert(0, "This platform not supported."); 535 536 assert(("dir"~p).toString() == dirSep~"foo"~dirSep~"bar"); 537 assert(("dir"~Path(str[1..$])).toString() == "dir"~dirSep~"foo"~dirSep~"bar"); 538 539 p ~= "blah"; 540 assert(p.toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"blah"); 541 542 p ~= Path("more"); 543 assert(p.toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"blah"~dirSep~"more"); 544 545 p ~= ".."; 546 assert(p.toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"blah"); 547 548 p ~= Path(".."); 549 assert(p.toString() == dirSep~"foo"~dirSep~"bar"); 550 551 p ~= "sub dir"; 552 p ~= ".."; 553 assert(p.toString() == dirSep~"foo"~dirSep~"bar"); 554 555 p ~= "filename"; 556 assert((p~Ext(".txt")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 557 assert((p~Ext("txt")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 558 assert((p~Ext("")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename"); 559 560 p ~= Ext(".ext"); 561 assert(p.toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 562 assert(p.baseName().toString() == "filename.ext"); 563 assert(p.dirName().toString() == dirSep~"foo"~dirSep~"bar"); 564 assert(p.rootName().toString() == dirSep); 565 assert(p.driveName().toString() == ""); 566 assert(p.stripDrive().toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 567 version(Windows) 568 { 569 assert(( Path("C:"~p.toRawString()) ).toString() == "C:"~dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 570 assert(( Path("C:"~p.toRawString()) ).stripDrive().toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 571 } 572 assert(p.extension().toString() == ".ext"); 573 assert(p.stripExtension().toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename"); 574 assert(p.setExtension(".txt").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 575 assert(p.setExtension("txt").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 576 assert(p.setExtension("").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename"); 577 assert(p.setExtension(Ext(".txt")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 578 assert(p.setExtension(Ext("txt")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 579 assert(p.setExtension(Ext("")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename"); 580 581 assert(p.defaultExtension(".dat").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 582 assert(p.stripExtension().defaultExtension(".dat").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.dat"); 583 584 assert(equal(p.pathSplitter(), [dirSep, "foo", "bar", "filename.ext"])); 585 586 assert(p.isRooted()); 587 version(Windows) 588 assert(!p.isAbsolute()); 589 else version(Posix) 590 assert(p.isAbsolute()); 591 else 592 static assert(0, "This platform not supported."); 593 594 assert(!( Path("dir"~p.toRawString()) ).isRooted()); 595 assert(!( Path("dir"~p.toRawString()) ).isAbsolute()); 596 597 version(Windows) 598 { 599 assert(( Path("dir"~p.toRawString()) ).absolutePath("C:/main").toString() == "C:"~dirSep~"main"~dirSep~"dir"~dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 600 assert(( Path("C:"~p.toRawString()) ).relativePath("C:/foo").toString() == "bar"~dirSep~"filename.ext"); 601 assert(( Path("C:"~p.toRawString()) ).relativePath("C:/foo/bar").toString() == "filename.ext"); 602 } 603 else version(Posix) 604 { 605 assert(( Path("dir"~p.toRawString()) ).absolutePath("/main").toString() == dirSep~"main"~dirSep~"dir"~dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 606 assert(p.relativePath("/foo").toString() == "bar"~dirSep~"filename.ext"); 607 assert(p.relativePath("/foo/bar").toString() == "filename.ext"); 608 } 609 else 610 static assert(0, "This platform not supported."); 611 612 assert(p.filenameCmp(dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext") == 0); 613 assert(p.filenameCmp(dirSep~"faa"~dirSep~"bat"~dirSep~"filename.ext") != 0); 614 assert(p.globMatch("*foo*name.ext")); 615 assert(!p.globMatch("*foo*Bname.ext")); 616 617 assert(!p.isValidFilename()); 618 assert(p.baseName().isValidFilename()); 619 assert(p.isValidPath()); 620 621 assert(p.expandTilde().toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 622 623 assert(p != Path("/dir/subdir/filename.ext")); 624 assert(p == Path("/foo/bar/filename.ext")); 625 version(Windows) 626 assert(p == Path("/FOO/BAR/FILENAME.EXT")); 627 else version(OSX) 628 assert(p == Path("/FOO/BAR/FILENAME.EXT")); 629 else version(Posix) 630 assert(p != Path("/FOO/BAR/FILENAME.EXT")); 631 else 632 static assert(0, "This platform not supported."); 633 634 // Test the other comparison overloads 635 assert(p != Path("/dir/subdir/filename.ext")); 636 assert(p == Path("/foo/bar/filename.ext")); 637 assert(Path("/dir/subdir/filename.ext") != p); 638 assert(Path("/foo/bar/filename.ext") == p); 639 assert("/dir/subdir/filename.ext" != p); 640 assert("/foo/bar/filename.ext" == p); 641 } 642 }