Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add root to Path & root constructor #196

Merged
merged 13 commits into from
Oct 18, 2023
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("")
lihaoyi marked this conversation as resolved.
Show resolved Hide resolved
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