Skip to content

Commit

Permalink
Add root to Path & root constructor (#196)
Browse files Browse the repository at this point in the history
Resolve #170 

This PR adds a `def root(root: String, fileSystem: FileSystem): Path` in
`os` package object. Additionally, it adds `root` and `filesystem`
members to `Path`. It addresses two problems:
 - Specifying custom roots for a path
 - Using custom filesystems

## Filesystem

Filesystem was added as a field of the `os.Path`. It was not added as a
Root field for simplicity and consistency with Java's `nio`. Root is a
part of the path; filesystem is the context of the whole path. One
filesystem can have many roots; one root can have many subdirectories.

## Root as String

A string is the simplest type that can represent the path's root. It
also corresponds to the mental model of the path - usually, developers
perceive it as a String. Therefore - String was chosen as the type of
root.

Pull request: #196

---------

Co-authored-by: Li Haoyi <[email protected]>
  • Loading branch information
szymon-rd and lihaoyi authored Oct 18, 2023
1 parent 6bfcf30 commit af2fd36
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 4 deletions.
32 changes: 32 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2025,6 +2025,38 @@ Python, ...) do. Even in cases where it's uncertain, e.g. you're taking user
input as a String, you have to either handle both possibilities with BasePath or
explicitly choose to convert relative paths to absolute using some base.

==== Roots and filesystems

If you are using a system that supports different roots of paths, e.g. Windows,
you can use the argument of `os.root` to specify which root you want to use.
If not specified, the default root will be used (usually, C on Windows, / on Unix).

[source,scala]
----
val root = os.root('C:\') / "Users" / "me"
assert(root == os.Path("C:\Users\me"))
----

Additionally, custom filesystems can be specified by passing a `FileSystem` to
`os.root`. This allows you to use OS-Lib with non-standard filesystems, such as
jar filesystems or in-memory filesystems.

[source,scala]
----
val uri = new URI("jar", Paths.get("foo.jar").toURI().toString, null);
val env = new HashMap[String, String]();
env.put("create", "true");
val fs = FileSystems.newFileSystem(uri, env);
val path = os.root("/", fs) / "dir"
----

Note that the jar file system operations suchs as writing to a file are supported
only on JVM 11+. Depending on the filesystem, some operations may not be supported -
for example, running an `os.proc` with pwd in a jar file won't work. You may also
meet limitations imposed by the implementations - in jar file system, the files are
created only after the file system is closed. Until that, the ones created in your
program are kept in memory.

==== `os.ResourcePath`

In addition to manipulating paths on the filesystem, you can also manipulate
Expand Down
5 changes: 4 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import $ivy.`com.github.lolgab::mill-mima::0.0.24`
// imports
import mill._, scalalib._, scalanativelib._, publish._
import mill.scalalib.api.ZincWorkerUtil
import com.github.lolgab.mill.mima.Mima
import com.github.lolgab.mill.mima._
import de.tobiasroeser.mill.vcs.version.VcsVersion

val communityBuildDottyVersion = sys.props.get("dottyVersion").toList
Expand Down Expand Up @@ -53,6 +53,9 @@ trait SafeDeps extends ScalaModule {

trait MiMaChecks extends Mima {
def mimaPreviousVersions = Seq("0.9.0", "0.9.1")
override def mimaBinaryIssueFilters: T[Seq[ProblemFilter]] = Seq(
ProblemFilter.exclude[ReversedMissingMethodProblem]("os.PathConvertible.isCustomFs")
)
}

trait OsLibModule
Expand Down
9 changes: 9 additions & 0 deletions os/src-jvm/package.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import scala.language.implicitConversions
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Paths

package object os {
type Generator[+T] = geny.Generator[T]
Expand All @@ -10,6 +13,12 @@ package object os {
*/
val root: Path = Path(java.nio.file.Paths.get(".").toAbsolutePath.getRoot)

def root(root: String, fileSystem: FileSystem = FileSystems.getDefault()): Path = {
val path = Path(fileSystem.getPath(root))
assert(path.root == root, s"$root is not a root path")
path
}

def resource(implicit resRoot: ResourceRoot = Thread.currentThread().getContextClassLoader) = {
os.ResourcePath.resource(resRoot)
}
Expand Down
8 changes: 8 additions & 0 deletions os/src-native/package.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import java.nio.file.FileSystem
import java.nio.file.FileSystems
package object os {
type Generator[+T] = geny.Generator[T]
val Generator = geny.Generator
Expand All @@ -8,6 +10,12 @@ package object os {
*/
val root: Path = Path(java.nio.file.Paths.get(".").toAbsolutePath.getRoot)

def root(root: String, fileSystem: FileSystem = FileSystems.getDefault()): Path = {
val path = Path(fileSystem.getPath(root))
assert(path.root == root, s"$root is not a root path")
path
}

/**
* The user's home directory
*/
Expand Down
18 changes: 15 additions & 3 deletions os/src/Path.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.nio.file.Paths

import collection.JavaConverters._
import scala.language.implicitConversions
import java.nio.file

trait PathChunk {
def segments: Seq[String]
Expand Down Expand Up @@ -400,11 +401,12 @@ object Path {

def apply[T: PathConvertible](f: T, base: Path): Path = apply(FilePath(f), base)
def apply[T: PathConvertible](f0: T): Path = {
val pathConvertible = implicitly[PathConvertible[T]]
// drive letter prefix is empty unless running in Windows.
val f = if (driveRelative(f0)) {
val f = if (!pathConvertible.isCustomFs(f0) && driveRelative(f0)) {
Paths.get(s"$driveRoot$f0")
} else {
implicitly[PathConvertible[T]].apply(f0)
pathConvertible.apply(f0)
}
if (f.iterator.asScala.count(_.startsWith("..")) > f.getNameCount / 2) {
throw PathError.AbsolutePathOutsideRoot
Expand Down Expand Up @@ -484,6 +486,9 @@ class Path private[os] (val wrapped: java.nio.file.Path)
new SeekableSource.ChannelSource(java.nio.file.Files.newByteChannel(wrapped))

require(wrapped.isAbsolute || Path.driveRelative(wrapped), s"$wrapped is not an absolute path")
def root = Option(wrapped.getRoot).map(_.toString).getOrElse("")
def fileSystem = wrapped.getFileSystem()

def segments: Iterator[String] = wrapped.iterator().asScala.map(_.toString)
def getSegment(i: Int): String = wrapped.getName(i).toString
def segmentCount = wrapped.getNameCount
Expand All @@ -509,7 +514,11 @@ class Path private[os] (val wrapped: java.nio.file.Path)
def endsWith(target: RelPath) = wrapped.endsWith(target.toString)

def relativeTo(base: Path): RelPath = {

if (fileSystem != base.fileSystem) {
throw new IllegalArgumentException(
s"Paths $wrapped and $base are on different filesystems"
)
}
val nioRel = base.wrapped.relativize(wrapped)
val segments = nioRel.iterator().asScala.map(_.toString).toArray match {
case Array("") => Internals.emptyStringArray
Expand All @@ -533,6 +542,7 @@ class Path private[os] (val wrapped: java.nio.file.Path)

sealed trait PathConvertible[T] {
def apply(t: T): java.nio.file.Path
def isCustomFs(t: T): Boolean = false
}

object PathConvertible {
Expand All @@ -544,6 +554,8 @@ object PathConvertible {
}
implicit object NioPathConvertible extends PathConvertible[java.nio.file.Path] {
def apply(t: java.nio.file.Path) = t
override def isCustomFs(t: java.nio.file.Path): Boolean =
t.getFileSystem() != java.nio.file.FileSystems.getDefault()
}
implicit object UriPathConvertible extends PathConvertible[URI] {
def apply(uri: URI) = uri.getScheme() match {
Expand Down
Loading

0 comments on commit af2fd36

Please sign in to comment.