OS-Lib uses strongly-typed data-structures to represent filesystem paths. The two basic versions are:
os.Path
: an absolute path, starting from the root
os.RelPath
: a relative path, not rooted anywhere
os.SubPath
: a sub path, without any ..
segments, not rooted anywhere
Generally, almost all commands take absolute os.Path
s. These are basically java.nio.file.Path
s with additional guarantees:
os.Path
s are always absolute. Relative paths are a separate type os.RelPath
os.Path
s are always canonical. You will never find .
or ..
segments in them, and never need to worry about calling .normalize
before operations.
Absolute paths can be created in a few ways:
// Get the process' Current Working Directory. As a convention // the directory that "this" code cares about (which may differ // from the pwd) is called `wd` val wd = os.pwd // A path nested inside `wd` in multiple segments 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" // A path with spaces or other special characters wd / "My Folder/My File.txt" // Up one level from the wd wd / os.up // Up two levels from the wd wd / os.up / os.up
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 how to interpret it. The single-segment limitation is intended to avoid the developer accidentally introducing Directory Traversal Attacks or other related bugs when naively constructing paths out of dynamic and potentially untrusted inputs, which is not an issue for literal string since the string value is directly written in the source code and immediately visible.
os.pwd
can be modified in certain scopes via the os.dynamicPwd
dynamic variable, but best practice is not to change it. Instead simply define a new path, e.g.
val target = os.pwd / "target"
Should be sufficient for most needs.
Above, we made use of the os.pwd
built-in path. There are a number of Paths built into OS-Lib:
os.pwd
: The current working directory of the process. This can’t be changed in Java, so if you need another path to work with the convention is to define a wd
variable.
os.root
: The root of the filesystem.
os.home
: The home directory of the current user.
os.temp()
/os.temp.dir()
: Creates a temporary file/folder and returns the path.
os.RelPath
s represent relative paths. These are basically defined as:
class RelPath private[ops] (segments0: Array[String], val ups: Int)
The same data structure as Paths, except that they can represent a number of ups before the relative path is applied. They can be created in the following ways:
// The path "folder/file" in multiple segments 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 rel4 = os.rel / "file" // The relative difference between two paths val target = os.pwd / "target/file" assert((target.relativeTo(os.pwd)) == os.rel / "target/file") // `up`s get resolved automatically val minus = os.pwd.relativeTo(target) val ups = os.up / os.up assert(minus == ups)
In general, very few APIs take relative paths. Their main purpose is to be combined with absolute paths in order to create new absolute paths. e.g.
val target = os.pwd / "target/file" val difference = target.relativeTo(os.pwd) val newBase = os.root / "code/server" assert(newBase / difference == os.root / "code/server/target/file")
os.up
is a relative path that comes in-built:
val target = os.root / "target/file" assert(target / os.up == os.root / "target")
Note that all paths, both relative and absolute, are always expressed in a canonical manner:
assert((os.root / "folder/file" / os.up).toString == "/folder") // not "/folder/file/.." assert((os.rel / "folder/file" / os.up).toString == "folder") // not "folder/file/.."
So you don’t need to worry about canonicalizing your paths before comparing them for equality or otherwise manipulating them.
os.SubPath
s represent relative paths without any ..
segments. These are basically defined as:
class SubPath private[ops] (segments0: Array[String])
They can be created in the following ways:
// The path "folder/file" in multiple segments 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 sub4 = rel3.asSubPath
os.SubPath
s are useful for representing paths within a particular folder or directory. You can combine them with absolute os.Path
s to resolve paths within them, without needing to worry about Directory Traversal Attacks du to accidentally accessing paths outside the destination folder.
val target = os.pwd / "target/file" val difference = target.relativeTo(os.pwd) val newBase = os.root / "code/server" assert(newBase / difference == os.root / "code/server/target/file")
Attempting to construct an os.SubPath
with ..
segments results in an exception being thrown:
val target = os.pwd / "out/scratch" / // `up`s are not allowed in sub paths intercept[Exception](os.pwd subRelativeTo target)
Like os.Path
s and os.RelPath
, os.SubPath
s are always canonicalized and can be compared for equality without worrying about different representations.
OS-Lib’s paths are transparent data-structures, and you can always access the segments and ups directly. Nevertheless, OS-Lib defines a number of useful operations that handle the common cases of dealing with these paths:
In this definition, ThisType represents the same type as the current path; e.g. a Path’s / returns a Path while a RelPath’s / returns a RelPath. Similarly, you can only compare or subtract paths of the same type.
Apart from os.RelPath
s themselves, a number of other data structures are convertible into os.RelPath
s when spliced into a path using /
:
String
s
Symbol
s
Array[T]
s where T
is convertible into a RelPath
Seq[T]
s where T
is convertible into a RelPath
Apart from built-ins like os.pwd
or os.root
or os.home
, you can also construct Paths from String
s, java.io.File
s or java.nio.file.Path
s:
val relStr = "hello/cow/world/.." val absStr = "/hello/world" assert( RelPath(relStr) == "hello/cow", // Path(...) also allows paths starting with ~, // which is expanded to become your home directory Path(absStr) == os.root / "hello/world" ) // You can also pass in java.io.File and java.nio.file.Path // objects instead of Strings when constructing paths val relIoFile = new java.io.File(relStr) val absNioFile = java.nio.file.Paths.get(absStr) assert( RelPath(relIoFile) == "hello/cow", Path(absNioFile) == os.root / "hello/world", Path(relIoFile, root / "base") == os.root / "base/hello/cow" )
Trying to construct invalid paths fails with exceptions:
val relStr = "hello/.." intercept[java.lang.IllegalArgumentException]{ Path(relStr) } val absStr = "/hello" intercept[java.lang.IllegalArgumentException]{ RelPath(absStr) } val tooManyUpsStr = "/hello/../.." intercept[PathError.AbsolutePathOutsideRoot.type]{ Path(tooManyUpsStr) }
As you can see, attempting to parse a relative path with os.Path
or an absolute path with os.RelPath
throws an exception. If you’re uncertain about what kind of path you are getting, you could use BasePath
to parse it :
val relStr = "hello/cow/world/.." val absStr = "/hello/world" assert( FilePath(relStr) == "hello/cow", FilePath(absStr) == os.root / "hello/world" )
This converts it into a BasePath
, which is either a os.Path
or os.RelPath
. It’s then up to you to pattern-match on the types and decide what you want to do in each case.
You can also pass in a second argument to Path(..., base)
. If the path being parsed is a relative path, this base will be used to coerce it into an absolute path:
val relStr = "hello/cow/world/.." val absStr = "/hello/world" val basePath: FilePath = FilePath(relStr) assert( os.Path(relStr, os.root / "base") == os.root / "base/hello/cow", os.Path(absStr, os.root / "base") == os.root / "hello/world", os.Path(basePath, os.root / "base") == os.root / "base/hello/cow", os.Path(".", os.pwd).last != "" )
For example, if you wanted the common behavior of converting relative paths to absolute based on your current working directory, you can pass in os.pwd
as the second argument to Path(...)
. Apart from passing in Strings or java.io.Files or java.nio.file.Paths, you can also pass in BasePaths you parsed early as a convenient way of converting it to a absolute path, if it isn’t already one.
In general, OS-Lib is very picky about the distinction between relative and absolute paths, and doesn’t allow "automatic" conversion between them based on current-working-directory the same way many other filesystem APIs (Bash, Java, 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.
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).
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.
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.
In addition to manipulating paths on the filesystem, you can also manipulate os.ResourcePath
in order to read resources off of the Java classpath. By default, the path used to load resources is absolute, using the Thread.currentThread().getContextClassLoader
.
val contents = os.read(os.resource / "test/ammonite/ops/folder/file.txt") assert(contents.contains("file contents lols"))
You can also pass in a classloader explicitly to the resource call:
val cl = getClass.getClassLoader val contents2 = os.read(os.resource(cl)/ "test/ammonite/ops/folder/file.txt") assert(contents2.contains("file contents lols"))
If you want to load resources relative to a particular class, pass in a class for the resource to be relative, or getClass to get something relative to the current class.
val cls = classOf[test.os.Testing] val contents = os.read(os.resource(cls) / "folder/file.txt") assert(contents.contains("file contents lols")) val contents2 = os.read(os.resource(getClass) / "folder/file.txt") assert(contents2.contains("file contents lols"))
In both cases, reading resources is performed as if you did not pass a leading slash into the getResource("foo/bar")
call. In the case of ClassLoader#getResource
, passing in a leading slash is never valid, and in the case of Class#getResource
, passing in a leading slash is equivalent to calling getResource
on the ClassLoader.
OS-Lib ensures you only use the two valid cases in the API, without a leading slash, and not the two cases with a leading slash which are redundant (in the case of Class#getResource
, which can be replaced by ClassLoader#getResource
) or invalid (a leading slash with ClassLoader#getResource
)
Note that you can only use os.read
from resource paths; you can’t write to them or perform any other filesystem operations on them, since they’re not really files.
Note also that resources belong to classloaders, and you may have multiple classloaders in your application e.g. if you are running in a servlet or REPL. Make sure you use the correct classloader (or a class belonging to the correct classloader) to load the resources you want, or else it might not find them.
Many operations in OS-Lib operate on os.Source
s. These represent values that can provide data which you can then use to write, transmit, etc.
By default, the following types of values can be used where-ever os.Source
s are required:
Any geny.Writable
data type:
Array[Byte]
java.lang.String
(these are treated as UTF-8)
java.io.InputStream
java.nio.channels.SeekableByteChannel
Any TraversableOnce[T]
of the above: e.g. Seq[String]
, List[Array[Byte]]
, etc.
Some operations only work on os.SeekableSource
, because they need the ability to seek to specific offsets in the data. Only the following types of values can be used where os.SeekableSource
is required:
java.nio.channels.SeekableByteChannel
os.Source
also supports anything that implements the Writable interface, such as ujson.Value
s, uPickle's upickle.default.writable
values, or Scalatags's Tag
s
You can also convert an os.Path
or os.ResourcePath
to an os.Source
via .toSource
.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4