Skip to content

Commit

Permalink
- Re-write ammonite.ops.Path internals to use java.nio more heavily
Browse files Browse the repository at this point in the history
- Re-write the `Path`, `RelPath` and `BasePath` constructors to be more regular
- Rename `.nio` into `.toNIO`, add a `.toIO` method that converts to a `java.io.File`
- Should fix #308 by applying a newly standard, documented, tested semantics to conversions from other data-types
- Might fix #120 (not tested yet though) as now Ammonite's paths fall back to NIO for most logic (including serialization to strings) and support multiple-roots or filesystems
  • Loading branch information
Li Haoyi committed Feb 17, 2016
1 parent 33bc7ab commit b96229d
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 93 deletions.
12 changes: 6 additions & 6 deletions ops/src/main/scala/ammonite/ops/FileOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ object mkdir extends Function1[Path, Unit]{
*/
object mv extends Function2[Path, Path, Unit] with Internals.Mover{
def apply(from: Path, to: Path) =
java.nio.file.Files.move(from.nio, to.nio)
java.nio.file.Files.move(from.toNIO, to.toNIO)

def check = false

Expand Down Expand Up @@ -218,7 +218,7 @@ object ls extends StreamableOp1[Path, Path, LsSeq] with ImplicitOp[LsSeq]{
object iter extends (Path => Iterator[Path]){
def apply(arg: Path) = {
import scala.collection.JavaConverters._
val dirStream = Files.newDirectoryStream(arg.nio)
val dirStream = Files.newDirectoryStream(arg.toNIO)
new SelfClosingIterator(
dirStream.iterator().asScala.map(x => Path(x)),
() => dirStream.close()
Expand Down Expand Up @@ -280,7 +280,7 @@ object ls extends StreamableOp1[Path, Path, LsSeq] with ImplicitOp[LsSeq]{
object write extends Function2[Path, Internals.Writable, Unit]{
def apply(target: Path, data: Internals.Writable) = {
mkdir(target/RelPath.up)
Files.write(target.nio, data.writeableData, StandardOpenOption.CREATE_NEW)
Files.write(target.toNIO, data.writeableData, StandardOpenOption.CREATE_NEW)
}

/**
Expand All @@ -291,7 +291,7 @@ object write extends Function2[Path, Internals.Writable, Unit]{
def apply(target: Path, data: Internals.Writable) = {
mkdir(target/RelPath.up)
Files.write(
target.nio,
target.toNIO,
data.writeableData,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
Expand All @@ -305,7 +305,7 @@ object write extends Function2[Path, Internals.Writable, Unit]{
object over extends Function2[Path, Internals.Writable, Unit]{
def apply(target: Path, data: Internals.Writable) = {
mkdir(target/RelPath.up)
Files.write(target.nio, data.writeableData)
Files.write(target.toNIO, data.writeableData)
}
}
}
Expand All @@ -317,7 +317,7 @@ object write extends Function2[Path, Internals.Writable, Unit]{
*/
object read extends Internals.Reader with Function1[Path, String]{
def readIn(p: Path) = {
java.nio.file.Files.newInputStream(p.nio)
java.nio.file.Files.newInputStream(p.toNIO)
}

/**
Expand Down
155 changes: 92 additions & 63 deletions ops/src/main/scala/ammonite/ops/Path.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,38 @@ package ammonite.ops

import acyclic.file

/**
* Enforces a standard interface for constructing [[BasePath]]-like things
* from java types of various sorts
*/
trait PathFactory[PathType <: BasePath] extends (String => PathType){
def apply(f: java.io.File): PathType = apply(f.getPath)
def apply(s: String): PathType = apply(java.nio.file.Paths.get(s))
def apply(f: java.nio.file.Path): PathType
}

object BasePath{
object BasePath extends PathFactory[BasePath]{
def apply(f: java.nio.file.Path) = {
if (f.isAbsolute) Path(f)
else RelPath(f)
}
def invalidChars = Set('/')
def checkSegment(s: String) = {
if (s.exists(BasePath.invalidChars)){
throw PathError.InvalidSegment(s)
}
if (s.exists(BasePath.invalidChars)) throw PathError.InvalidSegment(s)
if (s == "" || s == "." || s == "..") throw new PathError.InvalidSegment(s)
}

def chunkify(s: java.nio.file.Path) = {
import collection.JavaConversions._
s.iterator().map(_.toString).filter(_ != ".").toVector
}
}

/**
* A path which is either an absolute [[Path]] or a relative [[RelPath]],
* with shared APIs and implementations.
*/
trait BasePath[ThisType <: BasePath[ThisType]]{
trait BasePath{
type ThisType <: BasePath
/**
* The individual path segments of this path.
*/
Expand Down Expand Up @@ -53,9 +68,24 @@ trait BasePath[ThisType <: BasePath[ThisType]]{
* represents the name of the file/folder in filesystem paths
*/
def last: String

/**
* Gives you the file extension of this path, or the empty
* string if there is no extension
*/
def ext: String

/**
* Convert this into an java.nio.file.Path for easy interop
*/
def toNIO: java.nio.file.Path
/**
* Convert this into an java.io.File for easy interop
*/
def toIO: java.io.File
}

trait BasePathImpl[ThisType <: BasePath[ThisType]] extends BasePath[ThisType]{
trait BasePathImpl extends BasePath{
def segments: Seq[String]

protected[this] def make(p: Seq[String], ups: Int): ThisType
Expand All @@ -65,22 +95,24 @@ trait BasePathImpl[ThisType <: BasePath[ThisType]] extends BasePath[ThisType]{
math.max(subpath.ups - segments.length, 0)
)

/**
* Gives you the file extension of this path, or the empty
* string if there is no extension
*/
def ext = {
if (!segments.last.contains('.')) ""
else segments.last.split('.').lastOption.getOrElse("")
}

def toIO = toNIO.toFile

def last = segments.last
}

object PathError{
type IAE = IllegalArgumentException
case class InvalidSegment(segment: String)
extends IAE(s"Path segment [$segment] not allowed")
private[this] def errorMsg(s: String) =
s"[$s] is not a valid path segment. If you want to parse an absolute " +
"or relative path that may have multiple segments, consider using the " +
"Path(...) or RelPath(...) constructor calls"

case class InvalidSegment(segment: String) extends IAE(errorMsg(segment))

case object AbsolutePathOutsideRoot
extends IAE("The path created has enough ..s that it would start outside the root directory")
Expand All @@ -95,9 +127,9 @@ object PathError{
* segments can only occur at the left-end of the path, and
* are collapsed into a single number [[ups]].
*/
case class RelPath(segments: Vector[String], ups: Int) extends BasePathImpl[RelPath]{
case class RelPath private[ops] (segments: Vector[String], ups: Int) extends BasePathImpl{
type ThisType = RelPath
require(ups >= 0)
segments.foreach(BasePath.checkSegment)
protected[this] def make(p: Seq[String], ups: Int) = new RelPath(p.toVector, ups + this.ups)
def relativeTo(base: RelPath): RelPath = {
if (base.ups < ups) {
Expand All @@ -119,37 +151,26 @@ case class RelPath(segments: Vector[String], ups: Int) extends BasePathImpl[RelP
this.segments.startsWith(target.segments) && this.ups == target.ups
}

def toNIO = toNIO(java.nio.file.FileSystems.getDefault)
def toNIO(fs: java.nio.file.FileSystem) = fs.getPath(segments.head, segments.tail:_*)

override def toString = (Seq.fill(ups)("..") ++ segments).mkString("/")
override def hashCode = segments.hashCode() + ups.hashCode()
override def equals(o: Any): Boolean = o match {
case p: RelPath => segments == p.segments && p.ups == ups
case _ => false
}

}
trait RelPathStuff{
val up = new RelPath(Vector(), 1)
val empty = new RelPath(Vector(), 0)
implicit class RelPathStart(p1: String){
def /(subpath: RelPath) = empty/p1/subpath
}
implicit class RelPathStart2(p1: Symbol){
def /(subpath: RelPath) = empty/p1/subpath
}
}
object RelPath extends RelPathStuff with (String => RelPath){
def apply(s: String) = {
apply(java.nio.file.Paths.get(s))
}
def apply(s: java.nio.file.Path) = {
object RelPath extends RelPathStuff with PathFactory[RelPath]{
def apply(f: java.nio.file.Path): RelPath = {

import collection.JavaConversions._
require(
s.toAbsolutePath.iterator().size != s.iterator().size,
s + " is not an relative path"
)
empty/s.iterator.toArray.map(_.toString)
}
implicit class Transformable1(p: RelPath){
def nio = java.nio.file.Paths.get(p.toString)
require(!f.isAbsolute, f + " is not an relative path")

val segments = BasePath.chunkify(f.normalize())
val (ups, rest) = segments.partition(_ == "..")
new RelPath(rest, ups.length)
}

implicit def SymPath(s: Symbol): RelPath = StringPath(s.name)
Expand All @@ -169,37 +190,41 @@ object RelPath extends RelPathStuff with (String => RelPath){
implicit val relPathOrdering: Ordering[RelPath] =
Ordering.by((rp: RelPath) => (rp.ups, rp.segments.length, rp.segments.toIterable))
}
object Path extends (String => Path){
def apply(s: String): Path = {
apply(java.nio.file.Paths.get(s))
trait RelPathStuff{
val up: RelPath = new RelPath(Vector.empty, 1)
val empty: RelPath = new RelPath(Vector.empty, 0)
implicit class RelPathStart(p1: String){
def /(subpath: RelPath) = empty/p1/subpath
}
implicit class RelPathStart2(p1: Symbol){
def /(subpath: RelPath) = empty/p1/subpath
}
}

def apply(f: java.io.File): Path = apply(f.getCanonicalPath)

object Path extends PathFactory[Path]{
def apply(p: BasePath, base: Path) = p match{
case p: RelPath => base/p
case p: Path => p
}
def apply(f: java.io.File, base: Path): Path = apply(BasePath(f), base)
def apply(s: String, base: Path): Path = apply(BasePath(s), base)
def apply(f: java.nio.file.Path, base: Path): Path = apply(BasePath(f), base)
def apply(f: java.nio.file.Path): Path = {
import collection.JavaConversions._
require(
f.toAbsolutePath.iterator().size == f.iterator().size,
f + " is not an absolute path"
)
val chunks = collection.mutable.Buffer.empty[String]
f.toAbsolutePath.iterator.map(_.toString).foreach{
case "." => //do nothing
case ".." => chunks.remove(chunks.length-1)
case s => chunks.append(s)
}
val chunks = BasePath.chunkify(f)
if (chunks.count(_ == "..") > chunks.size / 2) throw PathError.AbsolutePathOutsideRoot

root/RelPath.SeqPath(chunks)
require(f.isAbsolute, f + " is not an absolute path")
Path(f.getRoot, BasePath.chunkify(f.normalize()))
}

val root = new Path(Vector())
val home = new Path(System.getProperty("user.home").split("/").drop(1).toVector)
val root = Path(java.nio.file.Paths.get("/"))
val home = Path(System.getProperty("user.home"))

def makeTmp = java.nio.file.Files.createTempDirectory(
java.nio.file.Paths.get(System.getProperty("java.io.tmpdir")), "ammonite"
)
implicit class Transformable(p: Path){
def nio = java.nio.file.Paths.get(p.toString)
}


implicit val pathOrdering: Ordering[Path] =
Ordering.by((rp: Path) => (rp.segments.length, rp.segments.toIterable))
Expand All @@ -209,14 +234,18 @@ object Path extends (String => Path){
* An absolute path on the filesystem. Note that the path is
* normalized and cannot contain any empty `""`, `"."` or `".."` segments
*/
case class Path(segments: Vector[String]) extends BasePathImpl[Path]{
segments.foreach(BasePath.checkSegment)
case class Path private[ops] (root: java.nio.file.Path, segments: Vector[String])
extends BasePathImpl{

type ThisType = Path

def toNIO = root.resolve(root.getFileSystem.getPath(segments.head, segments.tail:_*))

protected[this] def make(p: Seq[String], ups: Int) = {
if (ups > 0){
throw PathError.AbsolutePathOutsideRoot
}
new Path(p.toVector)
new Path(root, p.toVector)
}
override def toString = "/" + segments.mkString("/")

Expand All @@ -236,6 +265,6 @@ case class Path(segments: Vector[String]) extends BasePathImpl[Path]{
s2 = s2.dropRight(1)
newUps += 1
}
new RelPath(segments.drop(s2.length), newUps)
RelPath(segments.drop(s2.length), newUps)
}
}
2 changes: 1 addition & 1 deletion ops/src/main/scala/ammonite/ops/Shellout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ object Shellable{
implicit def StringShellable(s: String): Shellable = Shellable(Seq(s))
implicit def SeqShellable(s: Seq[String]): Shellable = Shellable(s)
implicit def SymbolShellable(s: Symbol): Shellable = Shellable(Seq(s.name))
implicit def BasePathShellable(s: BasePath[_]): Shellable = Shellable(Seq(s.toString))
implicit def BasePathShellable(s: BasePath): Shellable = Shellable(Seq(s.toString))
implicit def NumericShellable[T: Numeric](s: T): Shellable = Shellable(Seq(s.toString))
}

12 changes: 2 additions & 10 deletions ops/src/main/scala/ammonite/ops/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ package object ops extends Extensions with RelPathStuff{
/**
* The current working directory for this process.
*/
lazy val cwd = ops.Path(new java.io.File(""))
lazy val cwd = ops.Path(new java.io.File("").getCanonicalPath)

/**
* If you want to call subprocesses using [[%]] or [[%%]] and don't care
Expand All @@ -31,14 +31,6 @@ package object ops extends Extensions with RelPathStuff{
implicit lazy val implicitCwd = ops.cwd
}

implicit class Transformable1(p: java.nio.file.Path){
def amm = {
import collection.JavaConversions._
if (p.toAbsolutePath.iterator().size == p.iterator().size) ops.Path(p)
else ops.RelPath(p)
}
}

/**
* Extractor to let you easily pattern match on [[ops.Path]]s. Lets you do
*
Expand All @@ -52,7 +44,7 @@ package object ops extends Extensions with RelPathStuff{
* To break apart a path and extract various pieces of it.
*/
object /{
def unapply[T <: BasePath[T]](p: T): Option[(T, String)] = {
def unapply[T <: BasePath](p: T): Option[(p.ThisType, String)] = {
if (p.segments.length > 0)
Some((p / up, p.last))
else None
Expand Down
Loading

0 comments on commit b96229d

Please sign in to comment.