diff --git a/Readme.adoc b/Readme.adoc index 9ee6e8d1..763b33be 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -2073,6 +2073,9 @@ wd / "folder" / "file" // The RHS of `/` can have multiple segments if-and-only-if it is a literal string wd / "folder/file" +// Literal syntax for absolute `os.Path` +val p: os.Path = "/folder/file" + // A path starting from the root os.root / "folder/file" @@ -2086,7 +2089,7 @@ wd / os.up wd / os.up / os.up ---- -When constructing `os.Path`s, the right-hand-side of the `/` operator must be either a non-literal +When constructing ``os.Path``s, the right-hand-side of the `/` operator must be either a non-literal a string expression containing a single path segment or a literal string containing one-or-more path segments. If a non-literal string expression on the RHS contains multiple segments, you need to wrap the RHS in an explicit `os.RelPath(...)` or `os.SubPath(...)` constructor to tell OS-Lib @@ -2135,9 +2138,11 @@ before the relative path is applied. They can be created in the following ways: val rel1 = os.rel / "folder" / "file" // RHS of `/` can have multiple segments if-and-only-if it is a literal string val rel2 = os.rel / "folder/file" +// Literal syntax for `os.RelPath` +val rel3: os.RelPath = "folder/file" // The path "file" -val rel3 = os.rel / "file" +val rel4 = os.rel / "file" // The relative difference between two paths val target = os.pwd / "target/file" @@ -2201,6 +2206,8 @@ They can be created in the following ways: val sub1 = os.sub / "folder" / "file" // RHS of `/` can have multiple segments if-and-only-if it is a literal string val sub2 = os.sub / "folder/file" +// Literal syntax for `os.SubPath` +val sub2: os.Subpath = "folder/file" // The relative difference between two paths val target = os.pwd / "out/scratch/file" @@ -2208,7 +2215,7 @@ assert((target subRelativeTo os.pwd) == os.sub / "out/scratch/file") // Converting os.RelPath to os.SubPath val rel3 = os.rel / "folder/file" -val sub3 = rel3.asSubPath +val sub4 = rel3.asSubPath ---- ``os.SubPath``s are useful for representing paths within a particular @@ -2521,6 +2528,7 @@ string, int or set representations of the `os.PermSet` via: * Add ability to instrument path based operations using hooks https://github.com/com-lihaoyi/os-lib/pull/325[#325] * Add compile-time validation of literal paths containing ".." https://github.com/com-lihaoyi/os-lib/pull/329[#329] +* Add literal string syntax for `os.Path`s, `os.SubPath`s, and `os.RelPath`s https://github.com/com-lihaoyi/os-lib/pull/353[#353] [#0-11-3] === 0.11.3 diff --git a/os/src-2/Macros.scala b/os/src-2/Macros.scala index 71cae483..00ea1b96 100644 --- a/os/src-2/Macros.scala +++ b/os/src-2/Macros.scala @@ -11,6 +11,18 @@ trait PathChunkMacros extends StringPathChunkConversion { implicit def stringPathChunkValidated(s: String): PathChunk = macro Macros.stringPathChunkValidatedImpl } +trait SubPathMacros extends StringPathChunkConversion { + implicit def stringSubPathValidated(s: String): SubPath = + macro Macros.stringSubPathValidatedImpl +} +trait RelPathMacros extends StringPathChunkConversion { + implicit def stringRelPathValidated(s: String): RelPath = + macro Macros.stringRelPathValidatedImpl +} +trait PathMacros extends StringPathChunkConversion { + implicit def stringPathValidated(s: String): Path = + macro Macros.stringPathValidatedImpl +} object Macros { @@ -30,4 +42,42 @@ object Macros { ) } } + def stringSubPathValidatedImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[SubPath] = { + import c.universe.{Try => _, _} + + s match { + case Expr(Literal(Constant(literal: String))) if !literal.startsWith("/") => + val stringSegments = segmentsFromStringLiteralValidation(literal) + + if (stringSegments.startsWith(Seq(".."))) { + c.abort(s.tree.pos, "Invalid subpath literal: " + s.tree) + } + c.Expr(q"""os.sub / _root_.os.RelPath.fromStringSegments($stringSegments)""") + + case _ => c.abort(s.tree.pos, "Invalid subpath literal: " + s.tree) + } + } + def stringRelPathValidatedImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[RelPath] = { + import c.universe.{Try => _, _} + + s match { + case Expr(Literal(Constant(literal: String))) if !literal.startsWith("/") => + val stringSegments = segmentsFromStringLiteralValidation(literal) + c.Expr(q"""os.rel / _root_.os.RelPath.fromStringSegments($stringSegments)""") + + case _ => c.abort(s.tree.pos, "Invalid relative path literal: " + s.tree) + } + } + def stringPathValidatedImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[Path] = { + import c.universe.{Try => _, _} + + s match { + case Expr(Literal(Constant(literal: String))) if literal.startsWith("/") => + val stringSegments = segmentsFromStringLiteralValidation(literal.stripPrefix("/")) + + c.Expr(q"""os.root / _root_.os.RelPath.fromStringSegments($stringSegments)""") + + case _ => c.abort(s.tree.pos, "Invalid absolute path literal: " + s.tree) + } + } } diff --git a/os/src-3/Macros.scala b/os/src-3/Macros.scala index 0dca663c..3467f247 100644 --- a/os/src-3/Macros.scala +++ b/os/src-3/Macros.scala @@ -13,6 +13,24 @@ trait PathChunkMacros extends StringPathChunkConversion { Macros.stringPathChunkValidatedImpl('s) } } +trait SubPathMacros extends StringPathChunkConversion { + inline implicit def stringSubPathValidated(s: String): SubPath = + ${ + Macros.stringSubPathValidatedImpl('s) + } +} +trait RelPathMacros extends StringPathChunkConversion { + inline implicit def stringRelPathValidated(s: String): RelPath = + ${ + Macros.stringRelPathValidatedImpl('s) + } +} +trait PathMacros extends StringPathChunkConversion { + inline implicit def stringPathValidated(s: String): Path = + ${ + Macros.stringPathValidatedImpl('s) + } +} object Macros { def stringPathChunkValidatedImpl(s: Expr[String])(using quotes: Quotes): Expr[PathChunk] = { @@ -20,9 +38,9 @@ object Macros { s.asTerm match { case Inlined(_, _, Literal(StringConstant(literal))) => - segmentsFromStringLiteralValidation(literal) + val segments = segmentsFromStringLiteralValidation(literal) '{ - new RelPathChunk(fromStringSegments(segmentsFromString($s))) + new RelPathChunk(fromStringSegments(${Expr(segments)})) } case _ => '{ @@ -32,4 +50,38 @@ object Macros { } } } + def stringSubPathValidatedImpl(s: Expr[String])(using quotes: Quotes): Expr[SubPath] = { + import quotes.reflect.* + + s.asTerm match { + case Inlined(_, _, Literal(StringConstant(literal))) if !literal.startsWith("/") => + val stringSegments = segmentsFromStringLiteralValidation(literal) + if (stringSegments.startsWith(Seq(".."))) { + report.errorAndAbort("Invalid subpath literal: " + s.show) + } + '{ os.sub / fromStringSegments(${Expr(stringSegments)}) } + case _ => report.errorAndAbort("Invalid subpath literal: " + s.show) + + } + } + def stringRelPathValidatedImpl(s: Expr[String])(using quotes: Quotes): Expr[RelPath] = { + import quotes.reflect.* + + s.asTerm match { + case Inlined(_, _, Literal(StringConstant(literal))) if !literal.startsWith("/") => + val segments = segmentsFromStringLiteralValidation(literal) + '{ fromStringSegments(${Expr(segments)}) } + case _ => report.errorAndAbort("Invalid relative path literal: " + s.show) + } + } + def stringPathValidatedImpl(s: Expr[String])(using quotes: Quotes): Expr[Path] = { + import quotes.reflect.* + + s.asTerm match { + case Inlined(_, _, Literal(StringConstant(literal))) if literal.startsWith("/") => + val segments = segmentsFromStringLiteralValidation(literal.stripPrefix("/")) + '{ os.root / fromStringSegments(${Expr(segments)}) } + case _ => report.errorAndAbort("Invalid absolute path literal: " + s.show) + } + } } diff --git a/os/src/Path.scala b/os/src/Path.scala index e135c4c5..681bc0eb 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -349,7 +349,7 @@ class RelPath private[os] (segments0: Array[String], val ups: Int) def resolveFrom(base: os.Path) = base / this } -object RelPath { +object RelPath extends RelPathMacros { def apply[T: PathConvertible](f0: T): RelPath = { val f = implicitly[PathConvertible[T]].apply(f0) @@ -410,7 +410,7 @@ class SubPath private[os] (val segments0: Array[String]) def resolveFrom(base: os.Path) = base / this } -object SubPath { +object SubPath extends SubPathMacros { private[os] def relativeTo0(segments0: Array[String], segments: IndexedSeq[String]): RelPath = { val commonPrefix = { @@ -437,7 +437,7 @@ object SubPath { val sub: SubPath = new SubPath(Internals.emptyStringArray) } -object Path { +object Path extends PathMacros { def apply(p: FilePath, base: Path) = p match { case p: RelPath => base / p case p: SubPath => base / p diff --git a/os/test/src-jvm/ExampleTests.scala b/os/test/src-jvm/ExampleTests.scala index b4a1b861..cfc38f9d 100644 --- a/os/test/src-jvm/ExampleTests.scala +++ b/os/test/src-jvm/ExampleTests.scala @@ -172,12 +172,14 @@ object ExampleTests extends TestSuite { test("newPath") { val target = os.pwd / "out/scratch" + val target2: os.Path = "/out/scratch" // literal syntax } test("relPaths") { // The path "folder/file" val rel1 = os.rel / "folder/file" val rel2 = os.rel / "folder/file" + val rel3: os.RelPath = "folder/file" // literal syntax // The relative difference between two paths val target = os.pwd / "out/scratch/file" @@ -197,6 +199,7 @@ object ExampleTests extends TestSuite { // The path "folder/file" val sub1 = os.sub / "folder/file" val sub2 = os.sub / "folder/file" + val sub3 = "folder/file" // literal syntax // The relative difference between two paths val target = os.pwd / "out/scratch/file" @@ -204,7 +207,7 @@ object ExampleTests extends TestSuite { // Converting os.RelPath to os.SubPath val rel3 = os.rel / "folder/file" - val sub3 = rel3.asSubPath + val sub4 = rel3.asSubPath // `up`s are not allowed in sub paths intercept[Exception](os.pwd subRelativeTo target) diff --git a/os/test/src-jvm/ExpectyIntegration.scala b/os/test/src-jvm/ExpectyIntegration.scala deleted file mode 100644 index 69e2205a..00000000 --- a/os/test/src-jvm/ExpectyIntegration.scala +++ /dev/null @@ -1,34 +0,0 @@ -package test.os - -import os._ -import utest._ -import com.eed3si9n.expecty.Expecty.expect - -object ExpectyIntegration extends TestSuite { - val tests = Tests { - test("Literals") { - test("Basic") { - expect(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") - expect(root / "core/src/test" == root / "core" / "src" / "test") - expect(root / "core/src/test" == root / "core" / "src/test") - } - test("literals with [..]") { - expect(rel / "src" / ".." == rel / "src" / os.up) - expect(root / "src" / ".." == root / "src" / os.up) - expect(root / "hello" / ".." / "world" == root / "hello" / os.up / "world") - expect(root / "hello" / "../world" == root / "hello" / os.up / "world") - } - test("from issue") { - expect(Seq(os.pwd / "foo") == Seq(os.pwd / "foo")) - val path = os.Path("/") / "tmp" / "foo" - expect(path.startsWith(os.Path("/") / "tmp")) - } - test("multiple args") { - expect( - rel / "src" / ".." == rel / "src" / os.up, - root / "src" / "../foo" == root / "src" / os.up / "foo" - ) - } - } - } -} diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index eaac715c..85a27611 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -15,6 +15,39 @@ object PathTests extends TestSuite { val tests = Tests { test("Literals") { + test("implicitConstructors") { + test("valid") { + val p: os.Path = "/hello/world" + val s: os.SubPath = "hello/world" + val r: os.RelPath = "../hello/world" + assert(p == os.Path("/hello/world")) + assert(s == os.SubPath("hello/world")) + assert(r == os.RelPath("../hello/world")) + } + test("invalidLiteral") { + val err1 = compileError("""val p: os.Path = "hello/world" """) + assert(err1.msg.contains("Invalid absolute path literal: \"hello/world\"")) + + val err2 = compileError("""val s: os.SubPath = "../hello/world" """) + assert(err2.msg.contains("Invalid subpath literal: \"../hello/world\"")) + + val err3 = compileError("""val s: os.SubPath = "/hello/world" """) + assert(err3.msg.contains("Invalid subpath literal: \"/hello/world\"")) + + val err4 = compileError("""val r: os.RelPath = "/hello/world" """) + assert(err4.msg.contains("Invalid relative path literal: \"/hello/world\"")) + } + test("nonLiteral") { + val err1 = compileError("""val str = "hello/world"; val p: os.Path = str """) + assert(err1.msg.contains("Invalid absolute path literal: str")) + + val err2 = compileError("""val str = "/hello/world"; val s: os.SubPath = str """) + assert(err2.msg.contains("Invalid subpath literal: str")) + + val err3 = compileError("""val str = "/hello/world"; val r: os.RelPath = str""") + assert(err3.msg.contains("Invalid relative path literal: str")) + } + } test("Basic") { assert(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") assert(root / "core/src/test" == root / "core" / "src" / "test")