[NOTE]

February 1, 2026 ยท View on GitHub

= OS-Lib :version: 0.11.7 :toc-placement: preamble :toclevels: 3 :toc: :link-geny: https://github.com/com-lihaoyi/geny :link-oslib: https://github.com/com-lihaoyi/os-lib :link-upickle-doc: https://com-lihaoyi.github.io/upickle :link-scalatags-doc: https://com-lihaoyi.github.io/scalatags/ :idprefix: :idseparator: -

image:https://img.shields.io/badge/patreon-sponsor-ff69b4.svg[Patreon,link=https://www.patreon.com/lihaoyi] image:https://javadoc.io/badge2/com.lihaoyi/os-lib_3/scaladoc.svg[API Docs (Scala 3),link=https://javadoc.io/doc/com.lihaoyi/os-lib_3]

[source,scala]

// Make sure working directory exists and is empty val wd = os.pwd/"out/splash" os.remove.all(wd) os.makeDir.all(wd)

// Read/write files os.write(wd/"file.txt", "hello") os.read(wd/"file.txt") ==> "hello"

// Perform filesystem operations os.copy(wd/"file.txt", wd/"copied.txt") os.list(wd) ==> Seq(wd/"copied.txt", wd/"file.txt")

// Invoke subprocesses val invoked = os.proc("cat", wd/"file.txt", wd/"copied.txt").call(cwd = wd) invoked.out.trim ==> "hellohello"

// Chain multiple subprocess' stdin/stdout together val curl = os.proc("curl", "-L" , "https://git.io/fpvpS").spawn(stderr = os.Inherit) val gzip = os.proc("gzip", "-n").spawn(stdin = curl.stdout) val sha = os.proc("shasum", "-a", "256").spawn(stdin = gzip.stdout) sha.stdout.trim ==> "acc142175fa520a1cb2be5b97cbbe9bea092e8bba3fe2e95afa645615908229e -"

OS-Lib is a simple Scala interface to common OS filesystem and subprocess APIs. OS-Lib aims to make working with files and processes in Scala as simple as any scripting language, while still providing the safety, flexibility and performance you would expect from Scala.

OS-Lib aims to be a complete replacement for the java.nio.file.Files/java.nio.file.Paths, java.lang.ProcessBuilder scala.io and scala.sys APIs. You should not need to drop down to underlying Java APIs, as OS-Lib exposes all relevant capabilities in an intuitive and performant way. OS-Lib has no dependencies and is unopinionated: it exposes the underlying APIs in a concise but straightforward way, without introducing it's own idiosyncrasies, quirks, or clever DSLs.

If you use OS-Lib and like it, you will probably enjoy the following book by the Author:

Hands-on Scala uses OS-Lib extensively throughout the book, and has the entirety of Chapter 7: Files and Subprocesses dedicated to OS-Lib. Hands-on Scala is a great way to level up your skills in Scala in general and OS-Lib in particular.

You can also support it by donating to our Patreon:

For a hands-on introduction to the library, take a look at these two blog posts:

== Getting Started

To begin using OS-Lib, first add it as a dependency to your project's build:

[source,scala,subs="attributes,verbatim"]

// Mill ivy"com.lihaoyi::os-lib:{version}" // SBT "com.lihaoyi" %% "os-lib" % "{version}"

https://javadoc.io/doc/com.lihaoyi/os-lib_3[API Documentation (Scala 3)]

== Cookbook

Most operations in OS-Lib take place on <>s, which are constructed from a base path or working directory wd. Most often, the first thing to do is to define a wd path representing the folder you want to work with:

[source,scala]

val wd = os.pwd / "my-test-folder"

You can of course have multiple base paths, to use in different parts of your program where convenient, or simply work with one of the pre-defined paths os.pwd, os.root, or os.home.

=== Concatenate text files

[source,scala]

// Find and concatenate all .txt files directly in the working directory os.write( wd / "all.txt", os.list(wd).filter(_.ext == "txt").map(os.read) )

os.read(wd / "all.txt") ==> """I am cowI am cow |Hear me moo |I weigh twice as much as you |And I look good on the barbecue""".stripMargin

=== Spawning a subprocess on multiple files

[source,scala]

// Find and concatenate all .txt files directly in the working directory using cat os.proc("cat", os.list(wd).filter(_.ext == "txt")).call(stdout = wd / "all.txt")

os.read(wd / "all.txt") ==> """I am cowI am cow |Hear me moo |I weigh twice as much as you |And I look good on the barbecue""".stripMargin

=== Curl URL to temporary file

[source,scala]

// Curl to temporary file val temp = os.temp() os.proc("curl", "-L" , "https://git.io/fpfTs").call(stdout = temp)

os.size(temp) ==> 53814

// Curl to temporary file val temp2 = os.temp() val proc = os.proc("curl", "-L" , "https://git.io/fpfTJ").spawn()

os.write.over(temp2, proc.stdout) os.size(temp2) ==> 53814

=== Recursive line count

[source,scala]

// Line-count of all .txt files recursively in wd val lineCount = os.walk(wd) .filter(.ext == "txt") .map(os.read.lines) .map(.size) .sum

lineCount ==> 9

=== Largest Three Files

[source,scala]

// Find the largest three files in the given folder tree val largestThree = os.walk(wd) .filter(os.isFile(, followLinks = false)) .map(x => os.size(x) -> x).sortBy(-._1) .take(3)

largestThree ==> Seq( (711, wd / "misc/binary.png"), (81, wd / "Multi Line.txt"), (22, wd / "folder1/one.txt") )

=== Moving files out of folder

[source,scala]

// Move all files inside the "misc" folder out of it import os./ os.list(wd / "misc").map(os.move.matching { case p/"misc"/x => p/x } )

=== Calculate word frequencies

[source,scala]

// Calculate the word frequency of all the text files in the folder tree def txt = os.walk(wd).filter(.ext == "txt").map(os.read) def freq(s: Seq[String]) = s.groupBy(x => x).mapValues(.length).toSeq val map = freq(txt.flatMap(.split("[^a-zA-Z0-9]"))).sortBy(-_._2) map

== Operations

=== Reading & Writing

==== os.read

[source,scala]

os.read(arg: os.ReadablePath): String os.read(arg: os.ReadablePath, charSet: Codec): String os.read(arg: os.Path, offset: Long = 0, count: Int = Int.MaxValue, charSet: Codec = java.nio.charset.StandardCharsets.UTF_8): String

Reads the contents of a <> or other <> as a java.lang.String. Defaults to reading the entire file as UTF-8, but you can also select a different charSet to use, and provide an offset/count to read from if the source supports seeking.

[source,scala]

os.read(wd / "File.txt") ==> "I am cow" os.read(wd / "folder1/one.txt") ==> "Contents of folder one" os.read(wd / "Multi Line.txt") ==> """I am cow |Hear me moo |I weigh twice as much as you |And I look good on the barbecue""".stripMargin

==== os.read.bytes

[source,scala]

os.read.bytes(arg: os.ReadablePath): Array[Byte] os.read.bytes(arg: os.Path, offset: Long, count: Int): Array[Byte]

Reads the contents of a <> or <> as an Array[Byte]; you can provide an offset/count to read from if the source supports seeking.

[source,scala]

os.read.bytes(wd / "File.txt") ==> "I am cow".getBytes os.read.bytes(wd / "misc/binary.png").length ==> 711

==== os.read.chunks

[source,scala]

os.read.chunks(p: ReadablePath, chunkSize: Int): os.Generator[(Array[Byte], Int)] os.read.chunks(p: ReadablePath, buffer: Array[Byte]): os.Generator[(Array[Byte], Int)]

Reads the contents of the given path in chunks of the given size; returns a generator which provides a byte array and an offset into that array which contains the data for that chunk. All chunks will be of the given size, except for the last chunk which may be smaller.

Note that the array returned by the generator is shared between each callback; make sure you copy the bytes/array somewhere else if you want to keep them around.

Optionally takes in a provided input buffer instead of a chunkSize, allowing you to re-use the buffer between invocations.

[source,scala]

val chunks = os.read.chunks(wd / "File.txt", chunkSize = 2) .map{case (buf, n) => buf.take(n).toSeq } // copy the buffer to save the data .toSeq

chunks ==> Seq( SeqByte, SeqByte, Seq[Byte](' ', 'c'), SeqByte )

==== os.read.lines

[source,scala]

os.read.lines(arg: os.ReadablePath): IndexedSeq[String] os.read.lines(arg: os.ReadablePath, charSet: Codec): IndexedSeq[String]

Reads the given <> or other <> as a string and splits it into lines; defaults to reading as UTF-8, which you can override by specifying a charSet.

[source,scala]

os.read.lines(wd / "File.txt") ==> Seq("I am cow") os.read.lines(wd / "Multi Line.txt") ==> Seq( "I am cow", "Hear me moo", "I weigh twice as much as you", "And I look good on the barbecue" )

==== os.read.lines.stream

[source,scala]

os.read.lines(arg: os.ReadablePath): os.Generator[String] os.read.lines(arg: os.ReadablePath, charSet: Codec): os.Generator[String]

Identical to <>, but streams the results back to you in a <> rather than accumulating them in memory. Useful if the file is large.

[source,scala]

os.read.lines.stream(wd / "File.txt").count() ==> 1 os.read.lines.stream(wd / "Multi Line.txt").count() ==> 4

// Streaming the lines to the console for(line <- os.read.lines.stream(wd / "Multi Line.txt")){ println(line) }

==== os.read.inputStream

[source,scala]

os.read.inputStream(p: ReadablePath): java.io.InputStream

Opens a java.io.InputStream to read from the given file.

[source,scala]

val is = os.read.inputStream(wd / "File.txt") // ==> "I am cow" is.read() ==> 'I' is.read() ==> ' ' is.read() ==> 'a' is.read() ==> 'm' is.read() ==> ' ' is.read() ==> 'c' is.read() ==> 'o' is.read() ==> 'w' is.read() ==> -1 is.close()

==== os.read.stream

[source,scala]

os.read.stream(p: ReadablePath): geny.Readable

Opens a {link-geny}#readable[geny.Readable] to read from the given file. This allows you to stream data to any other library that supports Readable without buffering the data in memory, e.g. parsing it via FastParse, deserializing it via uPickle, uploading it via Requests-Scala, etc.

[source,scala]

val readable: geny.Readable = os.read.stream(wd / "File.json")

requests.post("https://httpbin.org/post", data = readable)

upickle.default.read(readable)

ujson.read(readable)

==== os.write

[source,scala]

os.write(target: Path, data: os.Source, perms: PermSet = null, createFolders: Boolean = false): Unit

Writes data from the given file or <> to a file at the target <>. You can specify the filesystem permissions of the newly created file by passing in a <>.

This throws an exception if the file already exists. To over-write or append to an existing file, see <> or <>.

By default, this doesn't create enclosing folders; you can enable this behavior by setting createFolders = true

[source,scala]

os.write(wd / "New File.txt", "New File Contents") os.read(wd / "New File.txt") ==> "New File Contents"

os.write(wd / "NewBinary.bin", Array[Byte](0, 1, 2, 3)) os.read.bytes(wd / "NewBinary.bin") ==> Array[Byte](0, 1, 2, 3)

==== os.write.append

[source,scala]

os.write.append(target: Path, data: os.Source, perms: PermSet = null, createFolders: Boolean = false): Unit

Similar to <>, except if the file already exists this appends the written data to the existing file contents.

[source,scala]

os.read(wd / "File.txt") ==> "I am cow"

os.write.append(wd / "File.txt", ", hear me moo") os.read(wd / "File.txt") ==> "I am cow, hear me moo"

os.write.append(wd / "File.txt", ",\nI weigh twice as much as you") os.read(wd / "File.txt") ==> "I am cow, hear me moo,\nI weigh twice as much as you"

os.read.bytes(wd / "misc/binary.png").length ==> 711 os.write.append(wd / "misc/binary.png", Array[Byte](1, 2, 3)) os.read.bytes(wd / "misc/binary.png").length ==> 714

==== os.write.over

[source,scala]

os.write.over(target: Path, data: os.Source, perms: PermSet = null, offset: Long = 0, createFolders: Boolean = false, truncate: Boolean = true): Unit

Similar to <>, except if the file already exists this over-writes the existing file contents. You can also pass in truncate = false to avoid truncating the file if the new contents is shorter than the old contents, and an offset to the file you want to write to.

[source,scala]

os.read(wd / "File.txt") ==> "I am cow" os.write.over(wd / "File.txt", "You are cow")

os.read(wd / "File.txt") ==> "You are cow"

os.write.over(wd / "File.txt", "We ", truncate = false) os.read(wd / "File.txt") ==> "We are cow"

os.write.over(wd / "File.txt", "s", offset = 8, truncate = false) os.read(wd / "File.txt") ==> "We are sow"

==== os.write.outputStream

[source,scala]

os.write.outputStream(target: Path, perms: PermSet = null, createFolders: Boolean = false, openOptions: Seq[OpenOption] = Seq(CREATE, WRITE))

Open a java.io.OutputStream to write to the given file.

[source,scala]

val out = os.write.outputStream(wd / "New File.txt") out.write('H') out.write('e') out.write('l') out.write('l') out.write('o') out.close()

os.read(wd / "New File.txt") ==> "Hello"

==== os.truncate

[source,scala]

os.truncate(p: Path, size: Long): Unit

Truncate the given file to the given size. If the file is smaller than the given size, does nothing.

[source,scala]

os.read(wd / "File.txt") ==> "I am cow"

os.truncate(wd / "File.txt", 4) os.read(wd / "File.txt") ==> "I am"

=== Listing & Walking

==== os.list

[source,scala]

os.list(p: Path): IndexedSeq[Path] os.list(p: Path, sort: Boolean = true): IndexedSeq[Path]

Returns all the files and folders directly within the given folder. If the given path is not a folder, raises an error. Can be called via <> to stream the results. To list files recursively, use <>.

For convenience os.list sorts the entries in the folder before returning them. You can disable sorted by passing in the flag sort = false.

[source,scala]

os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") os.list(wd / "folder2") ==> Seq( wd / "folder2/nestedA", wd / "folder2/nestedB" )

==== os.list.stream

[source,scala]

os.list.stream(p: Path): os.Generator[Path]

Similar to <>, except provides a <> of results rather than accumulating all of them in memory. Useful if the result set is large.

[source,scala]

os.list.stream(wd / "folder2").count() ==> 2

// Streaming the listed files to the console for(line <- os.list.stream(wd / "folder2")){ println(line) }

==== os.walk

[source,scala]

Recursively walks the given folder and returns the paths of every file or folder within.

You can pass in a skip callback to skip files or folders you are not interested in. This can avoid walking entire parts of the folder hierarchy, saving time as compared to filtering them after the fact.

By default, the paths are returned as a pre-order traversal: the enclosing folder is occurs first before any of it's contents. You can pass in preOrder = false to turn it into a post-order traversal, such that the enclosing folder occurs last after all it's contents.

os.walk returns but does not follow symlinks; pass in followLinks = true to override that behavior. You can also specify a maximum depth you wish to walk via the maxDepth parameter.

os.walk does not include the path given to it as part of the traversal by default. Pass in includeTarget = true to make it do so. The path appears at the start of the traversal of preOrder = true, and at the end of the traversal if preOrder = false.

[source,scala]

os.walk(wd / "folder1") ==> Seq(wd / "folder1/one.txt")

os.walk(wd / "folder1", includeTarget = true) ==> Seq( wd / "folder1", wd / "folder1/one.txt" )

os.walk(wd / "folder2") ==> Seq( wd / "folder2/nestedA", wd / "folder2/nestedA/a.txt", wd / "folder2/nestedB", wd / "folder2/nestedB/b.txt" )

os.walk(wd / "folder2", preOrder = false) ==> Seq( wd / "folder2/nestedA/a.txt", wd / "folder2/nestedA", wd / "folder2/nestedB/b.txt", wd / "folder2/nestedB" )

os.walk(wd / "folder2", maxDepth = 1) ==> Seq( wd / "folder2/nestedA", wd / "folder2/nestedB" )

os.walk(wd / "folder2", skip = _.last == "nestedA") ==> Seq( wd / "folder2/nestedB", wd / "folder2/nestedB/b.txt" )

==== os.walk.attrs

[source,scala]

Similar to <>, except it also provides the os.StatInfo filesystem metadata of every path that it returns. Can save time by allowing you to avoid querying the filesystem for metadata later. Note that os.StatInfo does not include filesystem ownership and permissions data; use os.stat.posix on the path if you need those attributes.

[source,scala]

val filesSortedBySize = os.walk.attrs(wd / "misc", followLinks = true) .sortBy{case (p, attrs) => attrs.size} .collect{case (p, attrs) if attrsisFile => p}

==== os.walk.stream

[source,scala]

Similar to <>, except returns a <> of the results rather than accumulating them in memory. Useful if you are walking very large folder hierarchies, or if you wish to begin processing the output even before the walk has completed.

[source,scala]

os.walk.stream(wd / "folder1").count() ==> 1

os.walk.stream(wd / "folder2").count() ==> 4

os.walk.stream(wd / "folder2", skip = _.last == "nestedA").count() ==> 2

==== os.walk.stream.attrs

[source,scala]

Similar to <>, except it also provides the filesystem metadata of every path that it returns. Can save time by allowing you to avoid querying the filesystem for metadata later.

[source,scala]

def totalFileSizes(p: os.Path) = os.walk.stream.attrs(p) .collect{case (p, attrs) if attrs.isFile => attrs.size} .sum

totalFileSizes(wd / "folder1") ==> 22 totalFileSizes(wd / "folder2") ==> 40

=== Manipulating Files & Folders

==== os.exists

[source,scala]

Checks if a file or folder exists at the specified path

[source,scala]

os.exists(wd / "File.txt") ==> true os.exists(wd / "folder1") ==> true os.exists(wd / "doesnt-exist") ==> false

==== os.move

[source,scala]

os.move(from: Path, to: Path): Unit os.move(from: Path, to: Path, createFolders: Boolean): Unit

Moves a file or folder from one path to another. Errors out if the destination path already exists, or is within the source path.

[source,scala]

os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") os.move(wd / "folder1/one.txt", wd / "folder1/first.txt") os.list(wd / "folder1") ==> Seq(wd / "folder1/first.txt")

os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") os.move(wd / "folder2/nestedA", wd / "folder2/nestedC") os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedB", wd / "folder2/nestedC")

os.read(wd / "File.txt") ==> "I am cow" os.move(wd / "Multi Line.txt", wd / "File.txt", replaceExisting = true) os.read(wd / "File.txt") ==> """I am cow |Hear me moo |I weigh twice as much as you |And I look good on the barbecue""".stripMargin

==== os.move.matching

[source,scala]

os.move.matching(t: PartialFunction[Path, Path]): PartialFunction[Path, Unit]

os.move can also be used as a transformer, via os.move.matching. This lets you use .map or .collect on a list of paths, and move all of them at once, e.g. to rename all .txt files within a folder tree to .data:

[source,scala]

import os.{GlobSyntax, /} os.walk(wd / "folder2") ==> Seq( wd / "folder2/nestedA", wd / "folder2/nestedA/a.txt", wd / "folder2/nestedB", wd / "folder2/nestedB/b.txt" )

os.walk(wd/'folder2).collect(os.move.matching{case p/g"x.txt"=>p/g"x.txt" => p/g"x.data"})

os.walk(wd / "folder2") ==> Seq( wd / "folder2/nestedA", wd / "folder2/nestedA/a.data", wd / "folder2/nestedB", wd / "folder2/nestedB/b.data" )

==== os.move.into

[source,scala]

os.move.into(from: Path, to: Path): Unit

Move the given file or folder into the destination folder

[source,scala]

os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") os.move.into(wd / "File.txt", wd / "folder1") os.list(wd / "folder1") ==> Seq(wd / "folder1/File.txt", wd / "folder1/one.txt")

==== os.move.over

[source,scala]

os.move.over(from: Path, to: Path): Unit

Move a file or folder from one path to another, and overwrite any file or folder than may already be present at that path

[source,scala]

os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") os.move.over(wd / "folder1", wd / "folder2") os.list(wd / "folder2") ==> Seq(wd / "folder2/one.txt")

==== os.copy

[source,scala]

os.copy(from: Path, to: Path): Unit os.copy(from: Path, to: Path, createFolders: Boolean): Unit

Copy a file or folder from one path to another. Recursively copies folders with all their contents. Errors out if the destination path already exists, or is within the source path.

[source,scala]

os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") os.copy(wd / "folder1/one.txt", wd / "folder1/first.txt") os.list(wd / "folder1") ==> Seq(wd / "folder1/first.txt", wd / "folder1/one.txt")

os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") os.copy(wd / "folder2/nestedA", wd / "folder2/nestedC") os.list(wd / "folder2") ==> Seq( wd / "folder2/nestedA", wd / "folder2/nestedB", wd / "folder2/nestedC" )

os.read(wd / "File.txt") ==> "I am cow" os.copy(wd / "Multi Line.txt", wd / "File.txt", replaceExisting = true) os.read(wd / "File.txt") ==> """I am cow |Hear me moo |I weigh twice as much as you |And I look good on the barbecue""".stripMargin ```

os.copy can also be used as a transformer:

os.copy.matching(t: PartialFunction[Path, Path]): PartialFunction[Path, Unit]
----

This lets you use `.map` or `.collect` on a list of paths, and copy all of them
at once:

[source,scala]
----
paths.map(os.copy.matching{case p/"scala"/file => p/"java"/file})
----

==== `os.copy.into`

[source,scala]
----
os.copy.into(from: Path, to: Path): Unit
----

Copy the given file or folder _into_ the destination folder

[source,scala]
----
os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt")
os.copy.into(wd / "File.txt", wd / "folder1")
os.list(wd / "folder1") ==> Seq(wd / "folder1/File.txt", wd / "folder1/one.txt")
----

==== `os.copy.over`

[source,scala]
----
os.copy.over(from: Path, to: Path): Unit
----

Similar to <<os-copy>>, but if the destination file already exists then
overwrite it instead of erroring out.

[source,scala]
----
os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB")
os.copy.over(wd / "folder1", wd / "folder2")
os.list(wd / "folder2") ==> Seq(wd / "folder2/one.txt")
----

==== `os.copy` with `mergeFolders`

_Since 0.7.5_

If you want to copy a directory over another but don't want to overwrite the whole destination directory (and loose it's content),
you can use the `mergeFolders` option of <<os-copy>>.

[source,scala]
----
os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt")
os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB")
os.copy(wd / "folder1", wd / "folder2", mergeFolders = true)
os.list(wd / "folder2") ==> Seq(wd / "folder2/one.txt", wd / "folder2/nestedA", wd / "folder2/nestedB")
----

==== `os.makeDir`

[source,scala]
----
os.makeDir(path: Path): Unit
os.makeDir(path: Path, perms: PermSet): Unit
----

Create a single directory at the specified path. Optionally takes in a
<<os-permset>> to specify the filesystem permissions of the created
directory.

Errors out if the directory already exists, or if the parent directory of the
specified path does not exist. To automatically create enclosing directories and
ignore the destination if it already exists, using
<<os-makedir-all>>

[source,scala]
----
os.exists(wd / "new_folder") ==> false
os.makeDir(wd / "new_folder")
os.exists(wd / "new_folder") ==> true
----

==== `os.makeDir.all`

[source,scala]
----
os.makeDir.all(path: Path): Unit
os.makeDir.all(path: Path,
               perms: PermSet = null,
               acceptLinkedDirectory: Boolean = true): Unit
----

Similar to <<os-makedir>>, but automatically creates any necessary
enclosing directories if they do not exist, and does not raise an error if the
destination path already contains a directory. Also does not raise an error if
the destination path contains a symlink to a directory, though you can force it
to error out in that case by passing in `acceptLinkedDirectory = false`

[source,scala]
----
os.exists(wd / "new_folder") ==> false
os.makeDir.all(wd / "new_folder/inner/deep")
os.exists(wd / "new_folder/inner/deep") ==> true
----

==== `os.remove`

[source,scala]
----
os.remove(target: Path): Boolean
os.remove(target: Path, checkExists: Boolean = false): Boolean
----

Remove the target file or folder. Folders need to be empty to be removed; if you
want to remove a folder tree recursively, use <<os-remove-all>>.
Returns `true` if the file was present before.
It will fail with an exception when the file is missing but `checkExists` is `true`,
or when the directory to remove is not empty.

[source,scala]
----
os.exists(wd / "File.txt") ==> true
os.remove(wd / "File.txt")
os.exists(wd / "File.txt") ==> false

os.exists(wd / "folder1/one.txt") ==> true
os.remove(wd / "folder1/one.txt")
os.remove(wd / "folder1")
os.exists(wd / "folder1/one.txt") ==> false
os.exists(wd / "folder1") ==> false
----

When removing symbolic links, it is the link that gets removed, and not its
destination:

[source,scala]
----
os.remove(wd / "misc/file-symlink")
os.exists(wd / "misc/file-symlink", followLinks = false) ==> false
os.exists(wd / "File.txt", followLinks = false) ==> true

os.remove(wd / "misc/folder-symlink")
os.exists(wd / "misc/folder-symlink", followLinks = false) ==> false
os.exists(wd / "folder1", followLinks = false) ==> true
os.exists(wd / "folder1/one.txt", followLinks = false) ==> true

os.remove(wd / "misc/broken-symlink")
os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false
----

If you wish to remove the destination of a symlink, use
<<os-readlink>>.

==== `os.remove.all`

[source,scala]
----
os.remove.all(target: Path, ignoreErrors: Boolean = false): Unit
----

Remove the target file or folder; if it is a folder and not empty, recursively
removing all it's contents before deleting it.

[source,scala]
----
os.exists(wd / "folder1/one.txt") ==> true
os.remove.all(wd / "folder1")
os.exists(wd / "folder1/one.txt") ==> false
os.exists(wd / "folder1") ==> false
----

When removing symbolic links, it is the links that gets removed, and not it's
destination:

[source,scala]
----
os.remove.all(wd / "misc/file-symlink")
os.exists(wd / "misc/file-symlink", followLinks = false) ==> false
os.exists(wd / "File.txt", followLinks = false) ==> true

os.remove.all(wd / "misc/folder-symlink")
os.exists(wd / "misc/folder-symlink", followLinks = false) ==> false
os.exists(wd / "folder1", followLinks = false) ==> true
os.exists(wd / "folder1/one.txt", followLinks = false) ==> true

os.remove.all(wd / "misc/broken-symlink")
os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false
----

If you wish to remove the destination of a symlink, use
<<os-readlink>>.

``os.remove.all`` removes nested files and folders one at a time, and any failure
in removing a file (e.g. due to permissions) or folder (e.g. due to someone concurrently
creating a file within it) causes an error to be thrown and terminates the removal early.
You can pass `ignoreErrors = false` to continue with the deletion of other files
even if some files or folders failed to be removed.

==== `os.hardlink`

[source,scala]
----
os.hardlink(src: Path, dest: Path, perms): Unit
----

Create a hardlink to the source path from the destination path

[source,scala]
----
os.hardlink(wd / "File.txt", wd / "Linked.txt")
os.exists(wd / "Linked.txt")
os.read(wd / "Linked.txt") ==> "I am cow"
os.isLink(wd / "Linked.txt") ==> false
----

==== `os.symlink`

[source,scala]
----
os.symlink(link: Path, dest: FilePath, perms: PermSet = null): Unit
----

Create a symbolic to the source path from the destination path. Optionally takes
a <<os-permset>> to customize the filesystem permissions of the symbolic
link.

[source,scala]
----
os.symlink(wd / "File.txt", wd / "Linked.txt")
os.exists(wd / "Linked.txt")
os.read(wd / "Linked.txt") ==> "I am cow"
os.isLink(wd / "Linked.txt") ==> true
----

You can create symlinks with either absolute ``os.Path``s or relative ``os.RelPath``s:

[source,scala]
----
os.symlink(wd / "File.txt", os.rel/ "Linked2.txt")
os.exists(wd / "Linked2.txt")
os.read(wd / "Linked2.txt") ==> "I am cow"
os.isLink(wd / "Linked2.txt") ==> true
----

Creating absolute and relative symlinks respectively. Relative symlinks are
resolved relative to the enclosing folder of the link.

==== `os.readLink`

[source,scala]
----
os.readLink(src: Path): os.FilePath
os.readLink.absolute(src: Path): os.Path
----

Returns the immediate destination of the given symbolic link.

[source,scala]
----
os.readLink(wd / "misc/file-symlink") ==> os.up / "File.txt"
os.readLink(wd / "misc/folder-symlink") ==> os.up / "folder1"
os.readLink(wd / "misc/broken-symlink") ==> os.rel / "broken"
os.readLink(wd / "misc/broken-abs-symlink") ==> os.root / "doesnt/exist"
----

Note that symbolic links can be either absolute ``os.Path``s or relative
``os.RelPath``s, represented by `os.FilePath`. You can also use `os.readLink.absolute`
to automatically resolve relative symbolic links to their absolute destination:

[source,scala]
----
os.readLink.absolute(wd / "misc/file-symlink") ==> wd / "File.txt"
os.readLink.absolute(wd / "misc/folder-symlink") ==> wd / "folder1"
os.readLink.absolute(wd / "misc/broken-symlink") ==> wd / "misc/broken"
os.readLink.absolute(wd / "misc/broken-abs-symlink") ==> os.root / "doesnt/exist"
----

==== `os.followLink`

[source,scala]
----
os.followLink(src: Path): Option[Path]
----

Attempts to any deference symbolic links in the given path, recursively, and return the
canonical path. Returns `None` if the path cannot be resolved (i.e. some
symbolic link in the given path is broken)

[source,scala]
----
os.followLink(wd / "misc/file-symlink") ==> Some(wd / "File.txt")
os.followLink(wd / "misc/folder-symlink") ==> Some(wd / "folder1")
os.followLink(wd / "misc/broken-symlink") ==> None
----

==== `os.temp`

[source,scala]
----
os.temp(contents: os.Source = null,
        dir: Path = null,
        prefix: String = null,
        suffix: String = null,
        deleteOnExit: Boolean = true,
        perms: PermSet = null): Path
----

Creates a temporary file. You can optionally provide a `dir` to specify where
this file lives, file-`prefix` and file-`suffix` to customize what it looks
like, and a <<os-permset>> to customize its filesystem permissions.

Passing in a <<os-source>> will initialize the contents of that file to
the provided data; otherwise it is created empty.

By default, temporary files are deleted on JVM exit. You can disable that
behavior by setting `deleteOnExit = false`

[source,scala]
----
val tempOne = os.temp("default content")
os.read(tempOne) ==> "default content"
os.write.over(tempOne, "Hello")
os.read(tempOne) ==> "Hello"
----

==== `os.temp.dir`

[source,scala]
----
os.temp.dir(dir: Path = null,
            prefix: String = null,
            deleteOnExit: Boolean = true,
            perms: PermSet = null): Path
----

Creates a temporary directory. You can optionally provide a `dir` to specify
where this file lives, a `prefix` to customize what it looks like, and a
<<os-permset>> to customize its filesystem permissions.

By default, temporary directories are deleted on JVM exit. You can disable that
behavior by setting `deleteOnExit = false`

[source,scala]
----
val tempDir = os.temp.dir()
os.list(tempDir) ==> Nil
os.write(tempDir / "file", "Hello")
os.list(tempDir) ==> Seq(tempDir / "file")
----

=== Zip & Unzip Files

[NOTE]
====
JVM only: Zip-related APIs are available on the JVM but not on Scala Native. The following symbols are JVM-only and are not defined on Native builds: `os.zip`, `os.unzip`, `os.zip.stream`, `os.unzip.stream`, `os.unzip.list`, and `os.zip.open`.
====

==== `os.zip`

[source,scala]
----
def apply(dest: os.Path,
          sources: Seq[ZipSource] = List(),
          excludePatterns: Seq[Regex] = List(),
          includePatterns: Seq[Regex] = List(),
          preserveMtimes: Boolean = false,
          deletePatterns: Seq[Regex] = List(),
          compressionLevel: Int = -1, /* 0-9 */
          followLinks: Boolean = true): os.Path
----

The zip object provides functionality to create or modify zip archives. It supports:

- Zipping Files and Directories: You can zip both individual files and entire directories.
- Appending to Existing Archives: Files can be appended to an existing zip archive.
- Exclude Patterns (-x): You can specify files or patterns to exclude while zipping.
- Include Patterns (-i): You can include specific files or patterns while zipping.
- Delete Patterns (-d): You can delete specific files from an existing zip archive.
- Symbolic Links (-y): You can configure to zip symbolic links as symbolic links on Linux/Unix by setting `followLinks = false`. Symbolic links are zipped as the referenced files by default on Linux/Unix, and always on Windows.
- Configuring whether or not to preserve filesyste mtimes.
- Preserving Unix file permissions.

This will create a new zip archive at `dest` containing `file1.txt` and everything
inside `sources`. If `dest` already exists as a zip, the files will be appended to the
existing zip, and any existing zip entries matching `deletePatterns` will be removed.

When modifying an existing zip file,
- Unix file permissions will be preserved if Java Runtime Version >= 14.
- If using Java Runtime Version < 14, Unix file permissions are not preserved, even for existing zip entries.
- Symbolics links will always be stored as the referenced files.
- Existing symbolic links stored in the zip might lose their symbolic link file type field and become broken.

===== Zipping Files and Folders

The example below demonstrates the core workflows: creating a zip, appending to it, and
unzipping it:

[source,scala]
----
// Zipping files and folders in a new zip file
val zipFileName = "zip-file-test.zip"
val zipFile1: os.Path = os.zip(
  destination = wd / zipFileName,
  sourcePaths = Seq(
    wd / "File.txt",
    wd / "folder1"
  )
)

// Adding files and folders to an existing zip file
os.zip(
  destination = zipFile1,
  sourcePaths = Seq(
    wd / "folder2",
    wd / "Multi Line.txt"
  )
)

// Unzip file to a destination folder
val unzippedFolder = os.unzip(
  source = wd / zipFileName,
  destination = wd / "unzipped folder"
)

val paths = os.walk(unzippedFolder)
val expected = Seq(
  // Files get included in the zip root using their name
  wd / "unzipped folder/File.txt",
  wd / "unzipped folder/Multi Line.txt",
  // Folder contents get included relative to the source root
  wd / "unzipped folder/nestedA",
  wd / "unzipped folder/nestedB",
  wd / "unzipped folder/one.txt",
  wd / "unzipped folder/nestedA/a.txt",
  wd / "unzipped folder/nestedB/b.txt",
)
assert(paths.sorted == expected)
----

===== Renaming files in the zip

You can also pass in a mapping to `os.zip` to specify exactly where in the zip each
input source file or folder should go:

```scala
val zipFileName = "zip-file-test.zip"
val zipFile1: os.Path = os.zip(
  destination = wd / zipFileName,
  sourcePaths = List(
    // renaming files and folders
    wd / "File.txt" -> os.sub / "renamed-file.txt",
    wd / "folder1" -> os.sub / "renamed-folder"
  )
)

val unzippedFolder = os.unzip(
  source = zipFile1,
  destination = wd / "unzipped folder"
)

val paths = os.walk(unzippedFolder)
val expected = Seq(
  wd / "unzipped folder/renamed-file.txt",
  wd / "unzipped folder/renamed-folder",
  wd / "unzipped folder/renamed-folder/one.txt",
)
assert(paths.sorted == expected)

===== Excluding/Including Files in Zip

You can specify files or folders to be excluded or included when creating the zip:

[source,scala]

os.zip( os.Path("/path/to/destination.zip"), List(os.Path("/path/to/folder")), excludePatterns = List(".\.log".r, "temp/.".r), // Exclude log files and "temp" folder includePatterns = List(".*\.txt".r) // Include only .txt files )


This will include only .txt files, excluding any .log files and anything inside the temp folder.

==== os.zip.stream

You can use os.zip.stream to write the final zip to an OutputStream rather than a concrete os.Path. os.zip.stream returns a geny.Writable, which has a writeBytesToStream method:

val zipFileName = "zipStreamFunction.zip"

val stream = os.write.outputStream(wd / "zipStreamFunction.zip")

val writable = zip.stream(sources = Seq(wd / "File.txt"))

writable.writeBytesTo(stream)
stream.close()

val unzippedFolder = os.unzip(
  source = wd / zipFileName,
  dest = wd / "zipStreamFunction"
)

val paths = os.walk(unzippedFolder)
assert(paths == Seq(unzippedFolder / "File.txt"))

This can be useful for streaming the zipped data to places which are not files: over the network, over a pipe, etc.

File permissions will be preserved. Symbolic links will be zipped as the referenced files by default on Linux/Unix, and always on Windows. To zip them as symbolic links on Linux/Unix, set followLinks = false.

==== os.unzip

===== Unzipping Files [source,scala]


os.unzip(os.Path("/path/to/archive.zip"), Some(os.Path("/path/to/destination")))

This extracts the contents of archive.zip to the specified destination. It supports preserving file permissions and symbolic links.

===== Excluding Files While Unzipping You can exclude certain files from being extracted using patterns:

[source,scala]

os.unzip( os.Path("/path/to/archive.zip"), Some(os.Path("/path/to/destination")), excludePatterns = List(".\.log".r, "temp/.".r) // Exclude log files and the "temp" folder )

===== os.unzip.list You can list the contents of the zip file without extracting them:

[source,scala]

os.unzip.list(os.Path("/path/to/archive.zip"))

This will print all the file paths contained in the zip archive. File permissions and symbolic links will not be preserved.

==== os.unzip.stream

You can unzip a zip file from any arbitrary java.io.InputStream containing its binary data using the os.unzip.stream method:

val readableZipStream: java.io.InputStream = ???

// Unzipping the stream to the destination folder
os.unzip.stream(
  source = readableZipStream,
  dest = unzippedFolder
)

This can be useful if the zip file does not exist on disk, e.g. if it is received over the network or produced in-memory by application logic.

File permissions and symbolic links are not supported since permissions and symlink mode are stored as external attributes which might reside in the central directory located at the end of the zip archive. For more a more detailed explanation see the ZipArchiveInputStream vs ZipFile section at https://commons.apache.org/proper/commons-compress/zip.html.

OS-Lib also provides the os.unzip.streamRaw API, which is a lower level API used internally within os.unzip.stream but can also be used directly if lower-level control is necessary.

==== os.zip.open

os.zip.open(path: Path): ZipRoot

os.zip.open allows you to treat zip files as filesystems, using normal os.* operations on them. This provides a move flexible way to manipulate the contents of the zip in a fine-grained manner when the normal os.zip or os.unzip operations do not suffice.

val zipFile = os.zip.open(wd / "zip-test.zip")
try {
  os.copy(wd / "File.txt", zipFile / "File.txt")
  os.copy(wd / "folder1", zipFile / "folder1")
  os.copy(wd / "folder2", zipFile / "folder2")
}finally zipFile.close()

val zipFile2 = os.zip.open(wd / "zip-test.zip")
try{
  os.list(zipFile2) ==> Vector(zipFile2 / "File.txt", zipFile2 / "folder1", zipFile2 / "folder2")
  os.remove.all(zipFile2 / "folder2")
  os.remove(zipFile2 / "File.txt")
}finally zipFile2.close()

val zipFile3 = os.zip.open(wd / "zip-test.zip")
try os.list(zipFile3) ==> Vector(zipFile3 / "folder1")
finally zipFile3.close()

os.zip.open returns a ZipRoot, which is identical to os.Path except it references the root of the zip file rather than a bare path on the filesystem. Note that you need to call ZipRoot#close() when you are done with it to avoid leaking filesystem resources.

File permissions are only supported for Java Runtime Version >= 14. Symbolic links are not supported. Using os.zip.open on a zip archive that contains symbolic links might break the links.

=== Filesystem Metadata

==== os.stat

[source,scala]

Reads in the basic filesystem metadata for the given file. By default, follows symbolic links to read the metadata of whatever the link is pointing at; set followLinks = false to disable that and instead read the metadata of the symbolic link itself.

[source,scala]

os.stat(wd / "File.txt").size ==> 8 os.stat(wd / "Multi Line.txt").size ==> 81 os.stat(wd / "folder1").fileType ==> os.FileType.Dir

==== os.stat.posix

[source,scala]

Reads in the posix filesystem metadata for the given file, providing information on permissions and ownership. By default, follows symbolic links to read the metadata of whatever the link is pointing at; set followLinks = false to disable that and instead read the metadata of the symbolic link itself.

==== os.isFile

[source,scala]

Returns true if the given path is a file. Follows symbolic links by default, pass in followLinks = false to not do so.

[source,scala]

os.isFile(wd / "File.txt") ==> true os.isFile(wd / "folder1") ==> false

==== os.isDir

[source,scala]

Returns true if the given path is a folder. Follows symbolic links by default, pass in followLinks = false to not do so.

[source,scala]

os.isDir(wd / "File.txt") ==> false os.isDir(wd / "folder1") ==> true

==== os.isLink

[source,scala]

Returns true if the given path is a symbolic link. Follows symbolic links by default, pass in followLinks = false to not do so.

[source,scala]

==== os.isReadable

[source,scala]

os.isReadable(p: Path): Boolean

Returns true if the given path is readable.

[source,scala]

os.isReadable(wd / "misc/file1") ==> false os.isReadable(wd / "misc/file2") ==> true

==== os.isWritable

[source,scala]

os.isWritable(p: Path): Boolean

Returns true if the given path is writable.

[source,scala]

os.isWritable(wd / "misc/file1") ==> false os.isWritable(wd / "misc/file2") ==> true

==== os.isExecutable

[source,scala]

os.isExecutable(p: Path): Boolean

Returns true if the given path is executable.

[source,scala]

os.isExecutable(wd / "misc/file1") ==> false os.isExecutable(wd / "misc/file2.sh") ==> true

==== os.size

[source,scala]

os.size(p: Path): Long

Returns the size of the given file, in bytes

[source,scala]

os.size(wd / "File.txt") ==> 8 os.size(wd / "Multi Line.txt") ==> 81

==== os.mtime

[source,scala]

os.mtime(p: Path): Long os.mtime.set(p: Path, millis: Long): Unit

Gets or sets the last-modified timestamp of the given file, in milliseconds

[source,scala]

os.mtime.set(wd / "File.txt", 0) os.mtime(wd / "File.txt") ==> 0

os.mtime.set(wd / "File.txt", 90000) os.mtime(wd / "File.txt") ==> 90000 os.mtime(wd / "misc/file-symlink") ==> 90000

=== Filesystem Permissions

==== os.perms

[source,scala]

Gets or sets the filesystem permissions of the given file or folder, as an <>.

Note that if you want to create a file or folder with a given set of permissions, you can pass in an <> to <> or <>. That will ensure the file or folder is created atomically with the given permissions, rather than being created with the default set of permissions and having os.perms.set over-write them later

[source,scala]

os.perms.set(wd / "File.txt", "rwxrwxrwx") os.perms(wd / "File.txt").toString() ==> "rwxrwxrwx" os.perms(wd / "File.txt").toInt() ==> Integer.parseInt("777", 8)

os.perms.set(wd / "File.txt", Integer.parseInt("755", 8)) os.perms(wd / "File.txt").toString() ==> "rwxr-xr-x"

os.perms.set(wd / "File.txt", "r-xr-xr-x") os.perms.set(wd / "File.txt", Integer.parseInt("555", 8))

==== os.owner

[source,scala]

Gets or sets the owner of the given file or folder. Note that your process needs to be running as the root user in order to do this.

[source,scala]

val originalOwner = os.owner(wd / "File.txt")

os.owner.set(wd / "File.txt", "nobody") os.owner(wd / "File.txt").getName ==> "nobody"

os.owner.set(wd / "File.txt", originalOwner)

==== os.group

[source,scala]

Gets or sets the owning group of the given file or folder. Note that your process needs to be running as the root user in order to do this.

[source,scala]

val originalOwner = os.owner(wd / "File.txt")

os.owner.set(wd / "File.txt", "nobody") os.owner(wd / "File.txt").getName ==> "nobody"

os.owner.set(wd / "File.txt", originalOwner)

=== Spawning Subprocesses

Subprocess are spawned using +os.call(cmd: os.Shellable, ...)+ or +os.spawn(cmd: os.Shellable, ...)+ calls, where the cmd: Shellable sets up the basic command you wish to run and +.foo(...)+ specifies how you want to run it. os.Shellable represents a value that can make up part of your subprocess command, and the following values can be used as os.Shellables:

  • java.lang.String
  • scala.Symbol
  • os.Path
  • os.RelPath
  • T: Numeric
  • Iterable[T]s of any of the above
  • TupleN[T1, T2, ...Tn]s of any of the above

Most of the subprocess commands also let you redirect the subprocess's stdin/stdout/stderr streams via os.ProcessInput or os.ProcessOutput values: whether to inherit them from the parent process, stream them into buffers, or output them to files. The following values are common to both input and output:

  • os.Pipe: the default, this connects the subprocess's stream to the parent process via pipes; if used on its stdin this lets the parent process write to the subprocess via os.SubProcess#stdin, and if used on its stdout it lets the parent process read from the subprocess via os.SubProcess#stdout and os.SubProcess#stderr.
  • os.Inherit: inherits the stream from the parent process. This lets the subprocess read directly from the parent process's standard input or write directly to the parent process's standard output or error. os.Inherit can be redirected on a threadlocal basis via os.Inherit.in, .out, or .err.
  • os.InheritRaw: identical to os.Inherit, but without being affected by redirects.
  • os.Path: connects the subprocess's stream to the given filesystem path, reading its standard input from a file or writing its standard output/error to the file.

In addition, you can pass any <>s to a Subprocess's stdin (Strings, InputStreams, Array[Byte]s, ...), and pass in a os.ProcessOutput value to stdout/stderr to register callbacks that are run when output is received on those streams.

Often, if you are only interested in capturing the standard output of the subprocess but want any errors sent to the console, you might set stderr = os.Inherit while leaving stdout = os.Pipe.

==== os.call

[source,scala]

os.call(cmd: os.Shellable, cwd: Path = null, env: Map[String, String] = null, stdin: ProcessInput = Pipe, stdout: ProcessOutput = Pipe, stderr: ProcessOutput = Pipe, mergeErrIntoOut: Boolean = false, timeout: Long = Long.MaxValue, check: Boolean = true, propagateEnv: Boolean = true, shutdownGracePeriod: Long = 100, destroyOnExit: Boolean = true): os.CommandResult

Also callable via os.proc(cmd).call(...)

Invokes the given subprocess like a function, passing in input and returning a CommandResult. You can then call result.exitCode to see how it exited, or result.out.bytes or result.err.string to access the aggregated stdout and stderr of the subprocess in a number of convenient ways.

call provides a number of parameters that let you configure how the subprocess is run:

  • cwd: the working directory of the subprocess
  • env: any additional environment variables you wish to set in the subprocess in addition to those passed via propagateEnv. You can also set their values to null to remove specific variables.
  • stdin: any data you wish to pass to the subprocess's standard input
  • stdout/stderr: these are os.Redirects that let you configure how the processes output/error streams are configured.
  • mergeErrIntoOut: merges the subprocess's stderr stream into it's stdout
  • timeout: how long to wait for the subprocess to complete
  • check: disable this to avoid throwing an exception if the subprocess fails with a non-zero exit code
  • propagateEnv: disable this to avoid passing in this parent process's environment variables to the subprocess

Note that redirecting stdout/stderr elsewhere means that the respective CommandResult#out/CommandResult#err values will be empty.

[source,scala]

val res = os.call(cmd = ('ls, wd/"folder2"))

res.exitCode ==> 0

res.out.text() ==> """nestedA |nestedB |""".stripMargin

res.out.trim() ==> """nestedA |nestedB""".stripMargin

res.out.lines() ==> Seq( "nestedA", "nestedB" )

res.out.bytes

// Non-zero exit codes throw an exception by default val thrown = intercept[os.SubprocessException]{ os.call(cmd = ('ls, "doesnt-exist"), cwd = wd) }

assert(thrown.result.exitCode != 0)

// Though you can avoid throwing by setting check = false val fail = os.call(cmd = ('ls, "doesnt-exist"), cwd = wd, check = false)

assert(fail.exitCode != 0)

fail.out.text() ==> ""

assert(fail.err.text().contains("No such file or directory"))

// You can pass in data to a subprocess' stdin val hash = os.call(cmd = ("shasum", "-a", "256"), stdin = "Hello World") hash.out.trim() ==> "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e -"

// Taking input from a file and directing output to another file os.call(cmd = ("base64"), stdin = wd / "File.txt", stdout = wd / "File.txt.b64")

os.read(wd / "File.txt.b64") ==> "SSBhbSBjb3c="

If you want to spawn an interactive subprocess, such as vim, less, or a python shell, set all of stdin/stdout/stderr to os.Inherit:

[source,scala]

os.proc("vim").call(stdin = os.Inherit, stdout = os.Inherit, stderr = os.Inherit)

Note that by customizing stdout and stderr, you can use the results of os.proc.call in a streaming fashion, either on groups of bytes:

[source,scala]

var lineCount = 1 os.call( cmd = ('find, "."), cwd = wd, stdout = os.ProcessOutput( (buf, len) => lineCount += buf.slice(0, len).count(_ == '\n') ), )

Or on lines of output:

[source,scala]

lineCount ==> 22 var lineCount = 1 os.call( cmd = ('find, "."), cwd = wd, stdout = os.ProcessOutput.Readlines( line => lineCount += 1 ), ) lineCount ==> 22

==== os.spawn

[source,scala]

os.spawn(cmd: os.Shellable, cwd: Path = null, env: Map[String, String] = null, stdin: os.ProcessInput = os.Pipe, stdout: os.ProcessOutput = os.Pipe, stderr: os.ProcessOutput = os.Pipe, mergeErrIntoOut: Boolean = false, propagateEnv: Boolean = true, shutdownGracePeriod: Long = 100, destroyOnExit: Boolean = true): os.SubProcess

Also callable via os.proc(cmd).spawn(...)

The most flexible of the os.proc calls, os.spawn simply configures and starts a subprocess, and returns it as a os.SubProcess. os.SubProcess is a simple wrapper around java.lang.Process, which provides stdin, stdout, and stderr streams for you to interact with however you like. e.g. You can sending commands to it's stdin and reading from it's stdout.

To implement pipes, you can spawn a process, take its stdout, and pass it as the stdin of a second spawned process.

Note that if you provide ProcessOutput callbacks to stdout/stderr, the calls to those callbacks take place on newly spawned threads that execute in parallel with the main thread. Thus make sure any data processing you do in those callbacks is thread safe!

stdin, stdout and stderr are java.lang.OutputStreams and java.lang.InputStreams enhanced with the .writeLine(s: String)/.readLine() methods for easy reading and writing of character and line-based data.

[source,scala]

// Start a long-lived python process which you can communicate with val sub = os.spawn( cmd = ("python", "-u", "-c", "while True: print(eval(raw_input()))"), cwd = wd )

// Sending some text to the subprocess sub.stdin.write("1 + 2") sub.stdin.writeLine("+ 4") sub.stdin.flush() sub.stdout.readLine() ==> "7"

sub.stdin.write("'1' + '2'") sub.stdin.writeLine("+ '4'") sub.stdin.flush() sub.stdout.readLine() ==> "124"

// Sending some bytes to the subprocess sub.stdin.write("1 * 2".getBytes) sub.stdin.write("* 4\n".getBytes) sub.stdin.flush() sub.stdout.read() ==> '8'.toByte

sub.destroy()

// You can chain multiple subprocess' stdin/stdout together val curl = os.spawn(cmd = ("curl", "-L" , "https://git.io/fpfTs"), stderr = os.Inherit) val gzip = os.spawn(cmd = ("gzip", "-n"), stdin = curl.stdout) val sha = os.spawn(cmd = ("shasum", "-a", "256"), stdin = gzip.stdout) sha.stdout.trim ==> "acc142175fa520a1cb2be5b97cbbe9bea092e8bba3fe2e95afa645615908229e -"

==== Customizing the default environment

Client-server CLI applications sometimes want to run subprocesses on the server based on the environment of the client. It is possible to customize the default environment passed to subprocesses by setting the os.SubProcess.env threadlocal:

[source,scala]

val clientEnvironment: Map[String, String] = ??? os.SubProcess.env.withValue(clientEnvironment) { os.call(command) // clientEnvironment is passed by default instead of the system environment }

== Spawning Pipelines of Subprocesses

After constructing a subprocess with os.proc, you can use the pipeTo method to pipe its output to another subprocess:

[source,scala]

val wc = os.proc("ls", "-l") .pipeTo(os.proc("wc", "-l")) .call() .out.text()

This is equivalent to the shell command ls -l | wc -l. You can chain together as many subprocesses as you like. Note that by using this API you can utilize the broken pipe behaviour of Unix systems. For example, you can take 10 first elements of output from the yes command, and after the head command terminates, the yes command will be terminated as well:

[source,scala]

val yes10 = os.proc("yes") .pipeTo(os.proc("head", "-n", "10")) .call() .out.text()

This feature is implemented inside the library and will terminate any process reading the stdin of other process in pipeline on every IO error. This behavior can be disabled via the handleBrokenPipe flag on call and spawn methods. Note that Windows does not support broken pipe behaviour, so a command likeyes would run forever. handleBrokenPipe is set to false by default on Windows.

Both call and spawn correspond in their behavior to their counterparts in the os.proc, but spawn returns the os.ProcessPipeline instance instead. It offers the same API as SubProcess, but will operate on the set of processes instead of a single one.

Pipefail is enabled by default, so if any of the processes in the pipeline fails, the whole pipeline will have a non-zero exit code. This behavior can be disabled via the pipefail flag on call and spawn methods. Note that the pipefail does not kill the processes in the pipeline, it just sets the exit code of the pipeline to the exit code of the failed process.

=== Watching for Changes

==== os.watch.watch

[source,scala]

os.watch.watch(roots: Seq[os.Path], onEvent: Set[os.Path] => Unit): Unit

[source,scala,subs="attributes,verbatim"]

// Mill ivy"com.lihaoyi::os-lib-watch:{version}" // SBT "com.lihaoyi" %% "os-lib-watch" % "{version}"

Efficiently watches the given roots folders for changes. Any time the filesystem is modified within those folders, the onEvent callback is called with the paths to the changed files or folders. Note that os.watch.watch is under a different artifact than the rest of the os.* functions, and you need to add a separate dependency to os-lib-watch in order to pull it in.

Once the call to watch returns, onEvent is guaranteed to receive a an event containing the path for:

  • Every file or folder that gets created, deleted, updated or moved within the watched folders
  • For copied or moved folders, the path of the new folder as well as every file or folder within it.
  • For deleted or moved folders, the root folder which was deleted/moved, but without the paths of every file that was within it at the original location

Note that watch does not provide any additional information about the changes happening within the watched roots folder, apart from the path at which the change happened. It is up to the onEvent handler to query the filesystem and figure out what happened, and what it wants to do.

Here is an example of use from the Ammonite REPL:

[source,scala,subs="attributes,verbatim"]

@ import $ivy.com.lihaoyi::os-lib-watch:{version}

@ os.watch.watch(Seq(os.pwd / "out"), paths => println("paths changed: " + paths.mkString(", ")))

@ os.write(os.pwd / "out/i am", "cow")

paths changed: /Users/lihaoyi/Github/Ammonite/out/i am

@ os.move(os.pwd / "out/i am", os.pwd / "out/hear me")

paths changed: /Users/lihaoyi/Github/Ammonite/out/i am,/Users/lihaoyi/Github/Ammonite/out/hear me

@ os.remove.all(os.pwd / "out/version")

paths changed: /Users/lihaoyi/Github/Ammonite/out/version/log,/Users/lihaoyi/Github/Ammonite/out/version/meta.json,/Users/lihaoyi/Github/Ammonite/out/version

== Data Types

=== os.Path

OS-Lib uses strongly-typed data-structures to represent filesystem paths. The two basic versions are:

  • <>: an absolute path, starting from the root
  • <>: a relative path, not rooted anywhere
  • <>: a sub path, without any .. segments, not rooted anywhere

Generally, almost all commands take absolute os.Paths. These are basically java.nio.file.Paths with additional guarantees:

  • os.Paths are always absolute. Relative paths are a separate type <>
  • os.Paths 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:

[source,scala]

// 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.Paths, 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 https://en.wikipedia.org/wiki/Directory_traversal_attack[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.

[source,scala]

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

os.RelPaths represent relative paths. These are basically defined as:

[source,scala]

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:

[source,scala]

// 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")

// ups 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.

[source,scala]

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:

[source,scala]

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:

[source,scala]

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

os.SubPaths represent relative paths without any .. segments. These are basically defined as:

[source,scala]

class SubPath private[ops] (segments0: Array[String])

They can be created in the following ways:

[source,scala]

// 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.SubPaths are useful for representing paths within a particular folder or directory. You can combine them with absolute os.Paths to resolve paths within them, without needing to worry about https://en.wikipedia.org/wiki/Directory_traversal_attack[Directory Traversal Attacks] du to accidentally accessing paths outside the destination folder.

[source,scala]

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:

[source,scala]

val target = os.pwd / "out/scratch" /

// ups are not allowed in sub paths intercept[Exception](os.pwd subRelativeTo target)

Like os.Paths and os.RelPath, os.SubPaths are always canonicalized and can be compared for equality without worrying about different representations.

==== Path Operations

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 <>s themselves, a number of other data structures are convertible into <>s when spliced into a path using /:

  • Strings
  • Symbols
  • Array[T]s where T is convertible into a RelPath
  • Seq[T]s where T is convertible into a RelPath

==== Constructing Paths

Apart from built-ins like os.pwd or os.root or os.home, you can also construct Paths from Strings, java.io.Files or java.nio.file.Paths:

[source,scala]

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:

[source,scala]

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 <> or an absolute path with <> throws an exception. If you're uncertain about what kind of path you are getting, you could use BasePath to parse it :

[source,scala]

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 <> or <>. 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:

[source,scala]

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.

==== 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 HashMapString, 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 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.

[source,scala]

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:

[source,scala]

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.

[source,scala]

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.

=== os.Source

Many operations in OS-Lib operate on os.Sources. 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.Sources 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 {link-geny}#writable[Writable] interface, such as {link-upickle-doc}/#uJson[ujson.Value]s, {link-upickle-doc}[uPickle]'s upickle.default.writable values, or {link-scalatags-doc}[Scalatags]'s Tags

You can also convert an os.Path or os.ResourcePath to an os.Source via .toSource.

=== os.Generator

Taken from the {link-geny}[geny] library, os.Generators are similar to iterators except instead of providing:

  • def hasNext(): Boolean
  • def next(): T

os.Generators provide:

  • +def generate(handleItem: A => Generator.Action): Generator.Action+

In general, you should not notice much of a difference using Generators vs using Iterators: you can use the same .map/.filter/.reduce/etc. operations on them, and convert them to collections via the same .toList/.toArray/etc. conversions. The main difference is that Generators can enforce cleanup after traversal completes, so we can ensure open files are closed and resources are released without any accidental leaks.

=== os.PermSet

os.PermSets represent the filesystem permissions on a single file or folder. Anywhere an os.PermSet is required, you can pass in values of these types:

  • java.lang.Strings of the form "rw-r-xrwx", with r/w/x representing the permissions that are present or dashes - representing the permissions which are absent
  • Octal Ints of the form Integer.parseInt("777", 8), matching the octal 755 or 666 syntax used on the command line
  • Set[PosixFilePermission]

In places where os.PermSets are returned to you, you can then extract the string, int or set representations of the os.PermSet via:

  • perms.toInt(): Int
  • perms.toString(): String
  • perms.value: Set[PosixFilePermission]

== Changelog

=== 0.11.8

  • Silence noisy exceptions in SubProcess input stream handling and WatchServiceWatcher

=== 0.11.6

  • Re-enabled Scala Native builds (tested with Scala Native 0.5.8). Zip APIs remain JVM-only.

=== 0.11.5

=== 0.11.4

[#0-11-3] === 0.11.3

  • SubProcess spawning operations now take an destroyOnExit = true flag to try and shut them down when the host JVM exits, SubProcess#destroy now takes a configurable (shutdownGracePeriod: Long, async: Boolean) flags to configure the behavior (superseding the old destroy()/destroyForcibly() methods), and timeoutGracePeriod has been renamed to shutdownGracePeriod https://github.com/com-lihaoyi/os-lib/pull/324[#324]

[#0-11-2] === 0.11.2

[#0-11-1] === 0.11.1

[#0-11-0] === 0.11.0

[#0-10-7] === 0.10.7

  • Allow multi-segment paths segments for literals https://github.com/com-lihaoyi/os-lib/pull/297: You can now write os.pwd / "foo/bar/qux" rather than os.pwd / "foo" / "bar" / "qux". Note that this is only allowed for string literals, and non-literal path segments still need to be wrapped e.g. def myString = "foo/bar/qux"; os.pwd / os.SubPath(myString) for security and safety purposes

[#0-10-6] === 0.10.6

[#0-10-5] === 0.10.5

[#0-10-4] === 0.10.4

[#0-10-3] === 0.10.3

  • os.Inherit now can be redirected on a threadlocal basis via os.Inherit.in, .out, or .err. os.InheritRaw is available if you do not want the redirects to take effect

[#0-10-2] === 0.10.2

[#0-10-1] === 0.10.1

  • Fix os.copy and os.move directories to root (#267)

[#0-10-0] === 0.10.0

  • Support for Scala-Native 0.5.0
  • Dropped support for Scala 2.11.x
  • Minimum version of Scala 3 increased to 3.3.1

[#0-9-3] === 0.9.3 - 2024-01-01

  • Fix os.watch on Windows (#236)
  • Fix propagateEnv = false to not propagate env (#238)
  • Make os.home a def (#239)

[#0-9-2] === 0.9.2 - 2023-11-05

  • Added new convenience API to create pipes between processes with .pipeTo
  • Fixed issue with leading .. / os.up in path segments created from a Seq
  • Fixed Windows-specific issues with relative paths with leading (back)slashes
  • Removed some internal use of deprecated API
  • ScalaDoc now maps some external references to their online sites
  • Dependency updates: sourcecode 0.3.1
  • Tooling updates: acyclic 0.3.9, Mill 0.11.5, mill-mima 0.0.24, mill-vcs-version 0.4.0, scalafmt 3.7.15

[#0-9-1] === 0.9.1 - 2023-03-07

  • Refined return types when constructing paths with / and get rid of long ThisType#ThisType cascades.
  • Added a new PathConvertible to support URIs when constructing paths.

[#0-9-0] === 0.9.0 - 2022-11-28

  • os.proc now also supports CharSequence(s) as Shellable
  • ProcessResult now also contains the actual used command
  • Fixed handling of atime and ctime in StatInfo
  • Deleted ConcurrentLinkedQueue from Scala Native jars, as it is now provided by Scala Native 0.4 itself
  • Enabled MiMa checks to CI setup and officially support early semantic versioning since this release
  • Documentation improvements

=== Older releases :leveloffset: +1

[discrete] === 0.8.1 - 2022-01-31

  • Added support for Scala Native on Scala 3

[discrete] === 0.8.0 - 2021-12-11

  • Avoid throwing an exception when sorting identical paths {link-oslib}/pull/90[#90]
  • Make os.remove behave more like Files.deleteIfExists {link-oslib}/pull/89[#89]
  • Make .ext on empty paths return "" rather than crashing {link-oslib}/pull/87[#87]

[discrete] === 0.7.8 - 2021-05-27

  • Restored binary compatibility in os.copy and os.copy.into to os-lib versions before 0.7.5

[discrete] === 0.7.7 - 2021-05-14

  • Add support for Scala 3.0.0

[discrete] === 0.7.6 - 2021-04-28

  • Add support for Scala 3.0.0-RC3

[discrete] === 0.7.5 - 2021-04-21

  • Re-added support for Scala 2.11
  • Added new option mergeFolders to os.copy
  • os.copy now honors followLinks when copying symbolic links to directories

[discrete] === 0.7.4

  • Add support for Scala 3.0.0-RC2

[discrete] === 0.7.3

  • Add support for Scala 3.0.0-RC1
  • Migration of the CI system from Travis CI to GitHub Actions

[discrete] === 0.7.2

  • Add support for Scala 3.0.0-M3

[discrete] === 0.7.1

  • Improve performance of os.write by buffering output stream to files

[discrete] === 0.6.2

  • Moved the os.Bytes, os.StreamValue (now named ByteData) interfaces into geny package, for sharing with Requests-Scala
  • Add os.read.stream function, that returns a geny.Readable

[discrete] === 0.5.0

  • os.Source now supports any data type that is geny.Writable

[discrete] === 0.4.2

  • Added a new <> data type, for safer handling of sub-paths within a directory.
  • Removed os.proc.stream, since you can now customize the stdout or stderr of os.proc.call to handle output in a streaming fashion
  • stderr in os.proc.call and os.proc.spawn defaults to os.Inherit rather than os.Pipe; pass in stderr = os.Pipe explicitly to get back the old behavior
  • Fix timeout not working with os.proc.call {link-oslib}/issues/27[#27]
  • Attempt to fix crasher accessing os.pwd {link-oslib}/issues/24[#24]
  • Added an <<os-watch-watch,os-lib-watch>> package, which can be used to efficiently recursively watch folders for updates {link-oslib}/issues/23[#23]
  • os.stat no longer provides POSIX owner/permissions related metadata by default {link-oslib}/issues/15[#15], use os.stat.posix to fetch that separately
  • os.stat.full has been superseded by os.stat and os.stat.posix
  • Removed os.BasicStatInfo, which has been superseded by os.StatInfo

[discrete] === 0.3.0

  • Support for Scala 2.13.0 final

[discrete] === 0.2.8

  • os.ProcessOutput trait is no longer sealed

[discrete] === 0.2.7

  • Narrow return type of readLink.absolute from FilePath to Path
  • Fix handling of standaline \r in os.SubProcess#stdout.readLine

[discrete] === 0.2.6

  • Remove os.StatInfo#name, os.BasicStatInfo#name and os.FullStatInfo#name, since it is just the last path segment of the stat call and doesn't properly reflect the actual name of the file on disk (e.g. on case-insensitive filesystems)
  • os.walk.attrs and os.walk.stream.attrs now provides a os.BasicFileInfo to the skip predicate.
  • Add os.BasePath#baseName, which returns the section of the path before the os.BasePath#ext extension.

[discrete] === 0.2.5

  • New os.readLink/os.readLink.absolute methods to read the contents of symbolic links without dereferencing them.
  • New os.read.chunked(p: Path, chunkSize: Int): os.Generator[(Array[Byte], Int)] method for conveniently iterating over chunks of a file
  • New os.truncate(p: Path, size: Int) method
  • SubProcess streams now implement java.io.DataInput/DataOutput for convenience
  • SubProcess streams are now synchronized for thread-safety
  • os.write now has createFolders default to false
  • os.Generator now has a .withFilter method
  • os.symlink now allows relative paths
  • os.remove.all now properly removes broken symlinks, and no longer recurses into the symlink's contents
  • os.SubProcess now implements java.lang.AutoCloseable
  • New write.channel counterpart to read.channel (and write.over.channel and write.append.channel)
  • os.PermSet is now modelled internally as a boxed Int for performance, and is a case class with proper equals/hashcode
  • os.read.bytes(arg: Path, offset: Long, count: Int) no longer leaks open file channels
  • Reversed the order of arguments in os.symlink and os.hardlink, to match the order of the underlying java NIO functions.

[discrete] === 0.2.2

  • Allow chaining of multiple subprocesses stdin/stdout

[discrete] === 0.2.0

  • First release