diff --git a/.gitignore b/.gitignore index 5771059b..c0b4b082 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ target/ .project .cache .sbtserver +.scala-build/ +.bsp/ project/.sbtserver tags nohup.out diff --git a/os/src/Path.scala b/os/src/Path.scala index 9de9d2a4..b6696de8 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -237,10 +237,12 @@ sealed trait FilePath extends BasePath { def toNIO: java.nio.file.Path def resolveFrom(base: os.Path): os.Path } + object FilePath { def apply[T: PathConvertible](f0: T) = { - val f = implicitly[PathConvertible[T]].apply(f0) - if (f.isAbsolute) Path(f0) + def f = implicitly[PathConvertible[T]].apply(f0) + // if Windows root-relative path, convert it to an absolute path + if (Path.driveRelative(f0) || f.isAbsolute) Path(f0) else { val r = RelPath(f0) if (r.ups == 0) r.asSubPath @@ -297,7 +299,7 @@ object RelPath { def apply[T: PathConvertible](f0: T): RelPath = { val f = implicitly[PathConvertible[T]].apply(f0) - require(!f.isAbsolute, s"$f is not a relative path") + require(!f.isAbsolute && !Path.driveRelative(f0), s"$f is not a relative path") val segments = BasePath.chunkify(f.normalize()) val (ups, rest) = segments.partition(_ == "..") @@ -398,13 +400,16 @@ object Path { def apply[T: PathConvertible](f: T, base: Path): Path = apply(FilePath(f), base) def apply[T: PathConvertible](f0: T): Path = { - val f = implicitly[PathConvertible[T]].apply(f0) + // drive letter prefix is empty unless running in Windows. + val f = if (driveRelative(f0)) { + Paths.get(s"$driveRoot$f0") + } else { + implicitly[PathConvertible[T]].apply(f0) + } if (f.iterator.asScala.count(_.startsWith("..")) > f.getNameCount / 2) { throw PathError.AbsolutePathOutsideRoot } - - val normalized = f.normalize() - new Path(normalized) + new Path(f.normalize()) } implicit val pathOrdering: Ordering[Path] = new Ordering[Path] { @@ -436,6 +441,32 @@ object Path { } } + /** + * @return true if Windows OS and path begins with slash or backslash. + * Examples: + * driveRelative("/Users") // true in `Windows`, false elsewhere. + * driveRelative("\\Users") // true in `Windows`, false elsewhere. + * driveRelative("C:/Users") // false always + */ + def driveRelative[T: PathConvertible](f0: T): Boolean = { + if (driveRoot.isEmpty) { + false // non-Windows os + } else { + f0.toString.take(1) match { + case "\\" | "/" => true + case _ => false + } + } + } + + /** + * @return current working drive if Windows, empty string elsewhere. + * Paths.get(driveRoot) == current working directory on all platforms. + */ + lazy val driveRoot: String = Paths.get(".").toAbsolutePath.getRoot.toString match { + case "/" => "" // implies a non-Windows platform + case s => s.take(2) // Windows current working drive (e.g., "C:") + } } trait ReadablePath { @@ -452,7 +483,7 @@ class Path private[os] (val wrapped: java.nio.file.Path) def toSource: SeekableSource = new SeekableSource.ChannelSource(java.nio.file.Files.newByteChannel(wrapped)) - require(wrapped.isAbsolute, s"$wrapped is not an absolute path") + require(wrapped.isAbsolute || Path.driveRelative(wrapped), s"$wrapped is not an absolute path") def segments: Iterator[String] = wrapped.iterator().asScala.map(_.toString) def getSegment(i: Int): String = wrapped.getName(i).toString def segmentCount = wrapped.getNameCount diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index 646280d5..4c696671 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -3,6 +3,7 @@ package test.os import java.nio.file.Paths import os._ +import os.Path.{driveRoot} import utest.{assert => _, _} import java.net.URI object PathTests extends TestSuite { @@ -10,41 +11,56 @@ object PathTests extends TestSuite { test("Basic") { val base = rel / "src" / "main" / "scala" val subBase = sub / "src" / "main" / "scala" - test("Transformers") { - if (Unix()) { - // os.Path to java.nio.file.Path - assert((root / "omg").wrapped == Paths.get("/omg")) + test("Transform posix paths") { + // verify posix string format of driveRelative path + assert(posix(root / "omg") == posix(Paths.get("/omg").toAbsolutePath)) - // java.nio.file.Path to os.Path - assert(root / "omg" == Path(Paths.get("/omg"))) - assert(rel / "omg" == RelPath(Paths.get("omg"))) - assert(sub / "omg" == SubPath(Paths.get("omg"))) + // verify driveRelative path + assert(sameFile((root / "omg").wrapped, Paths.get("/omg"))) - // URI to os.Path - assert(root / "omg" == Path(Paths.get("/omg").toUri())) + // driveRelative path is an absolute path + assert(posix(root / "omg") == s"$driveRoot/omg") - // We only support file schemes like above, but nothing else - val httpUri = URI.create( - "https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top" - ) - val ldapUri = URI.create( - "ldap://[2001:db8::7]/c=GB?objectClass?one" - ) - intercept[IllegalArgumentException](Path(httpUri)) - intercept[IllegalArgumentException](Path(ldapUri)) + // Paths.get(driveRoot) same file as pwd + val p1 = posix(Paths.get(driveRoot).toAbsolutePath) match { + case s if s.matches(".:.*/") => + s.stripSuffix("/") // java 8, remove spurious trailing slash + case s => + s + } + val p2 = posix(pwd.toNIO.toAbsolutePath) + System.err.printf("p1[%s]\np2[%s]\n", p1, p2) + assert(p1 == p2) + } + test("Transformers") { + // java.nio.file.Path to os.Path + assert(root / "omg" == Path(Paths.get("/omg"))) + assert(rel / "omg" == RelPath(Paths.get("omg"))) + assert(sub / "omg" == SubPath(Paths.get("omg"))) - // os.Path to String - assert((root / "omg").toString == "/omg") - assert((rel / "omg").toString == "omg") - assert((sub / "omg").toString == "omg") - assert((up / "omg").toString == "../omg") - assert((up / up / "omg").toString == "../../omg") + // URI to os.Path + assert(root / "omg" == Path(Paths.get("/omg").toUri())) - // String to os.Path - assert(root / "omg" == Path("/omg")) - assert(rel / "omg" == RelPath("omg")) - assert(sub / "omg" == SubPath("omg")) - } + // We only support file schemes like above, but nothing else + val httpUri = URI.create( + "https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top" + ) + val ldapUri = URI.create( + "ldap://[2001:db8::7]/c=GB?objectClass?one" + ) + intercept[IllegalArgumentException](Path(httpUri)) + intercept[IllegalArgumentException](Path(ldapUri)) + + // os.Path to String + assert((rel / "omg").toString == "omg") + assert((sub / "omg").toString == "omg") + assert((up / "omg").toString == "../omg") + assert((up / up / "omg").toString == "../../omg") + + // String to os.Path + assert(root / "omg" == Path("/omg")) + assert(rel / "omg" == RelPath("omg")) + assert(sub / "omg" == SubPath("omg")) } test("BasePath") { @@ -84,22 +100,18 @@ object PathTests extends TestSuite { test("RelPath") { test("Constructors") { test("Symbol") { - if (Unix()) { - val rel1 = base / "ammonite" - assert( - rel1.segments == Seq("src", "main", "scala", "ammonite"), - rel1.toString == "src/main/scala/ammonite" - ) - } + val rel1 = base / "ammonite" + assert( + rel1.segments == Seq("src", "main", "scala", "ammonite"), + rel1.toString == "src/main/scala/ammonite" + ) } test("String") { - if (Unix()) { - val rel1 = base / "Path.scala" - assert( - rel1.segments == Seq("src", "main", "scala", "Path.scala"), - rel1.toString == "src/main/scala/Path.scala" - ) - } + val rel1 = base / "Path.scala" + assert( + rel1.segments == Seq("src", "main", "scala", "Path.scala"), + rel1.toString == "src/main/scala/Path.scala" + ) } test("Combos") { def check(rel1: RelPath) = assert( @@ -107,29 +119,23 @@ object PathTests extends TestSuite { rel1.toString == "src/main/scala/sub1/sub2" ) test("ArrayString") { - if (Unix()) { - val arr = Array("sub1", "sub2") - check(base / arr) - } + val arr = Array("sub1", "sub2") + check(base / arr) } test("ArraySymbol") { - if (Unix()) { - val arr = Array("sub1", "sub2") - check(base / arr) - } + val arr = Array("sub1", "sub2") + check(base / arr) } test("SeqString") { - if (Unix()) check(base / Seq("sub1", "sub2")) + check(base / Seq("sub1", "sub2")) } test("SeqSymbol") { - if (Unix()) check(base / Seq("sub1", "sub2")) + check(base / Seq("sub1", "sub2")) } test("SeqSeqSeqSymbol") { - if (Unix()) { - check( - base / Seq(Seq(Seq("sub1"), Seq()), Seq(Seq("sub2")), Seq()) - ) - } + check( + base / Seq(Seq(Seq("sub1"), Seq()), Seq(Seq("sub2")), Seq()) + ) } } } @@ -148,22 +154,18 @@ object PathTests extends TestSuite { test("SubPath") { test("Constructors") { test("Symbol") { - if (Unix()) { - val rel1 = subBase / "ammonite" - assert( - rel1.segments == Seq("src", "main", "scala", "ammonite"), - rel1.toString == "src/main/scala/ammonite" - ) - } + val rel1 = subBase / "ammonite" + assert( + rel1.segments == Seq("src", "main", "scala", "ammonite"), + rel1.toString == "src/main/scala/ammonite" + ) } test("String") { - if (Unix()) { - val rel1 = subBase / "Path.scala" - assert( - rel1.segments == Seq("src", "main", "scala", "Path.scala"), - rel1.toString == "src/main/scala/Path.scala" - ) - } + val rel1 = subBase / "Path.scala" + assert( + rel1.segments == Seq("src", "main", "scala", "Path.scala"), + rel1.toString == "src/main/scala/Path.scala" + ) } test("Combos") { def check(rel1: SubPath) = assert( @@ -171,29 +173,23 @@ object PathTests extends TestSuite { rel1.toString == "src/main/scala/sub1/sub2" ) test("ArrayString") { - if (Unix()) { - val arr = Array("sub1", "sub2") - check(subBase / arr) - } + val arr = Array("sub1", "sub2") + check(subBase / arr) } test("ArraySymbol") { - if (Unix()) { - val arr = Array("sub1", "sub2") - check(subBase / arr) - } + val arr = Array("sub1", "sub2") + check(subBase / arr) } test("SeqString") { - if (Unix()) check(subBase / Seq("sub1", "sub2")) + check(subBase / Seq("sub1", "sub2")) } test("SeqSymbol") { - if (Unix()) check(subBase / Seq("sub1", "sub2")) + check(subBase / Seq("sub1", "sub2")) } test("SeqSeqSeqSymbol") { - if (Unix()) { - check( - subBase / Seq(Seq(Seq("sub1"), Seq()), Seq(Seq("sub2")), Seq()) - ) - } + check( + subBase / Seq(Seq(Seq("sub1"), Seq()), Seq(Seq("sub2")), Seq()) + ) } } } @@ -210,8 +206,8 @@ object PathTests extends TestSuite { val d = pwd val abs = d / base test("Constructor") { - if (Unix()) assert( - abs.toString.drop(d.toString.length) == "/src/main/scala", + assert( + posix(abs).drop(d.toString.length) == "/src/main/scala", abs.toString.length > d.toString.length ) } @@ -307,22 +303,18 @@ object PathTests extends TestSuite { intercept[PathError.InvalidSegment](rel / "src" / "..") } test("CannotRelativizeAbsAndRel") { - if (Unix()) { - val abs = pwd - val rel = os.rel / "omg" / "wtf" - compileError(""" - abs.relativeTo(rel) - """).msg.toLowerCase.contains("required: os.path") ==> true - compileError(""" - rel.relativeTo(abs) - """).msg.toLowerCase.contains("required: os.relpath") ==> true - } + val abs = pwd + val rel = os.rel / "omg" / "wtf" + compileError(""" + abs.relativeTo(rel) + """).msg.toLowerCase.contains("required: os.path") ==> true + compileError(""" + rel.relativeTo(abs) + """).msg.toLowerCase.contains("required: os.relpath") ==> true } test("InvalidCasts") { - if (Unix()) { - intercept[IllegalArgumentException](Path("omg/cow")) - intercept[IllegalArgumentException](RelPath("/omg/cow")) - } + intercept[IllegalArgumentException](Path("omg/cow")) + intercept[IllegalArgumentException](RelPath("/omg/cow")) } test("Pollution") { // Make sure we"re" not polluting too much @@ -366,66 +358,58 @@ object PathTests extends TestSuite { } test("construction") { test("success") { - if (Unix()) { - val relStr = "hello/cow/world/.." - val absStr = "/hello/world" + val relStr = "hello/cow/world/.." + val absStr = "/hello/world" - val lhs = Path(absStr) - val rhs = root / "hello" / "world" - assert( - RelPath(relStr) == rel / "hello" / "cow", - // Path(...) also allows paths starting with ~, - // which is expanded to become your home directory - lhs == rhs - ) + val lhs = Path(absStr) + val rhs = root / "hello" / "world" + assert( + RelPath(relStr) == rel / "hello" / "cow", + // Path(...) also allows paths starting with ~, + // which is expanded to become your home directory + lhs == rhs + ) - // You can also pass in java.io.File and java.nio.file.Path - // objects instead of Strings when constructing paths - val relIoFile = new java.io.File(relStr) - val absNioFile = java.nio.file.Paths.get(absStr) + // You can also pass in java.io.File and java.nio.file.Path + // objects instead of Strings when constructing paths + val relIoFile = new java.io.File(relStr) + val absNioFile = java.nio.file.Paths.get(absStr) - assert(RelPath(relIoFile) == rel / "hello" / "cow") - assert(Path(absNioFile) == root / "hello" / "world") - assert(Path(relIoFile, root / "base") == root / "base" / "hello" / "cow") - } + assert(RelPath(relIoFile) == rel / "hello" / "cow") + assert(Path(absNioFile) == root / "hello" / "world") + assert(Path(relIoFile, root / "base") == root / "base" / "hello" / "cow") } test("basepath") { - if (Unix()) { - val relStr = "hello/cow/world/.." - val absStr = "/hello/world" - assert( - FilePath(relStr) == rel / "hello" / "cow", - FilePath(absStr) == root / "hello" / "world" - ) - } + val relStr = "hello/cow/world/.." + val absStr = "/hello/world" + assert( + FilePath(relStr) == rel / "hello" / "cow", + FilePath(absStr) == root / "hello" / "world" + ) } test("based") { - if (Unix()) { - val relStr = "hello/cow/world/.." - val absStr = "/hello/world" - val basePath: FilePath = FilePath(relStr) - assert(Path(relStr, root / "base") == root / "base" / "hello" / "cow") - assert(Path(absStr, root / "base") == root / "hello" / "world") - assert(Path(basePath, root / "base") == root / "base" / "hello" / "cow") - assert(Path(".", pwd).last != "") - } + val relStr = "hello/cow/world/.." + val absStr = "/hello/world" + val basePath: FilePath = FilePath(relStr) + assert(Path(relStr, root / "base") == root / "base" / "hello" / "cow") + assert(Path(absStr, root / "base") == root / "hello" / "world") + assert(Path(basePath, root / "base") == root / "base" / "hello" / "cow") + assert(Path(".", pwd).last != "") } test("failure") { - if (Unix()) { - val relStr = "hello/.." - intercept[java.lang.IllegalArgumentException] { - Path(relStr) - } + val relStr = "hello/.." + intercept[java.lang.IllegalArgumentException] { + Path(relStr) + } - val absStr = "/hello" - intercept[java.lang.IllegalArgumentException] { - RelPath(absStr) - } + val absStr = "/hello" + intercept[java.lang.IllegalArgumentException] { + RelPath(absStr) + } - val tooManyUpsStr = "/hello/../.." - intercept[PathError.AbsolutePathOutsideRoot.type] { - Path(tooManyUpsStr) - } + val tooManyUpsStr = "/hello/../.." + intercept[PathError.AbsolutePathOutsideRoot.type] { + Path(tooManyUpsStr) } } } @@ -436,5 +420,23 @@ object PathTests extends TestSuite { assert(result1 == expected) assert(result2 == expected) } + test("issue201") { + val p = Path("/omg") // driveRelative path does not throw exception. + System.err.printf("p[%s]\n", posix(p)) + assert(posix(p) contains "/omg") + } + } + // compare absolute paths + def sameFile(a: java.nio.file.Path, b: java.nio.file.Path): Boolean = { + a.toAbsolutePath == b.toAbsolutePath + } + def sameFile(a: os.Path, b: java.nio.file.Path): Boolean = { + sameFile(a.wrapped, b) + } + def sameFile(a: Path, b: Path): Boolean = { + sameFile(a.wrapped, b.wrapped) } + def posix(s: String): String = s.replace('\\', '/') + def posix(p: java.nio.file.Path): String = posix(p.toString) + def posix(p: os.Path): String = posix(p.toNIO) }