Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -2201,14 +2206,16 @@ 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"
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
Expand Down Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions os/src-2/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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)
}
}
}
56 changes: 54 additions & 2 deletions os/src-3/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,34 @@ 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] = {
import quotes.reflect.*

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 _ =>
'{
Expand All @@ -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)
}
}
}
6 changes: 3 additions & 3 deletions os/src/Path.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion os/test/src-jvm/ExampleTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -197,14 +199,15 @@ 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"
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

// `up`s are not allowed in sub paths
intercept[Exception](os.pwd subRelativeTo target)
Expand Down
34 changes: 0 additions & 34 deletions os/test/src-jvm/ExpectyIntegration.scala

This file was deleted.

33 changes: 33 additions & 0 deletions os/test/src/PathTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down