# os-lib: a Scala replacement for java.nio.file and ProcessBuilder

> OS-Lib is a dependency-free Scala interface to filesystem and subprocess operations, built around an immutable Path type with a slash operator and a proc builder that pipes processes together. It aims to be a complete replacement for java.nio.file.Files, ProcessBuilder, scala.io and scala.sys without adding its own idioms.

**com-lihaoyi/os-lib** — OS-Lib is a simple, flexible, high-performance Scala interface to common OS filesystem and subprocess APIs

- Repository: https://github.com/com-lihaoyi/os-lib
- Stars: 743 · Forks: 89
- Language: Scala
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/com-lihaoyi-os-lib

## The design goal is replacement, not an extra layer

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

The stronger claim is the replacement one. OS-Lib aims to be a complete replacement for the java.nio.file.Files and java.nio.file.Paths APIs, java.lang.ProcessBuilder, scala.io and scala.sys, and the stated expectation is that you should not need to drop down to the underlying Java APIs because OS-Lib exposes all the relevant capabilities in an intuitive and performant way.

What comes with that is a constraint. The library has no dependencies and is unopinionated, exposing the underlying APIs in a concise but straightforward way, without introducing its own idiosyncrasies, quirks or clever DSLs. That is a deliberate refusal to be clever, and it is the reason the library reads as a thin layer rather than a framework.

The result is worth understanding as a design position. There is no new filesystem abstraction to learn, so an experienced Java developer can read OS-Lib code without a tutorial. What they get is a Scala-flavoured surface over the same operations, with the path type made first class and the subprocess handling made composable.

The primary language is Scala, the default branch is main, and the release history shows 0.11.9-M6 on 2026-02-11, 0.11.9-M7 on 2026-03-10 and 0.11.9-M8 on 2026-06-01, with the last push the same day. The M suffix is a milestone, so this is a pre-1.0 line shipping milestones rather than point releases.

## Paths are values, and the slash operator builds them

The central type is the path, and the way you build one is the first thing you learn. Most operations take place on paths, constructed from a base path or a working directory, and the first thing the cookbook recommends is to define a wd path for the folder you want to work with:

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

The slash operator is the whole ergonomic trick. It reads like a path, it works uniformly on a base directory and a segment, and it returns a new path rather than mutating anything, so paths are values you can pass around and store.

The library also gives you the pre-defined paths os.pwd, os.root and os.home, and the README notes you can of course have multiple base paths to use in different parts of your program where convenient, or simply work with one.

Because a path is a value, the operations on it compose. The cookbook shows a file listing, a filter on the extension and a map over the reads, all in one expression, and the largest-three-files example sorts by size and takes three:

```scala
val largestThree = os.walk(wd)
  .filter(os.isFile(_, followLinks = false))
  .map(x => os.size(x) -> x).sortBy(-_._1)
  .take(3)
```

The walk function is the recursive traversal, and the followLinks flag on the isFile check is the kind of detail that matters when a tree contains symlinks. The examples are all in the os namespace, which is how the library keeps its surface recognisable and its top level clean.

## Subprocesses are values you can pipe together

The subprocess side is where OS-Lib differs most from ProcessBuilder, and the difference is that a process is a value you can compose before you run it.

The shape of the API is that os.proc takes the command and arguments and gives you back a handle, and then you either call it to run it to completion or spawn it to get a running process. The introductory example shows the simple case, where call returns an object whose out field holds the standard output:

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

The chaining case is the more striking one, where three processes are spawned and their standard streams are connected to each other:

```scala
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  -"
```

A curl process feeding a gzip process feeding a shasum process, with no shell in between. In Java that is a stream of plumbing between three ProcessBuilder objects, and in OS-Lib it is three lines because the stdout of one process is a value you can pass as the stdin of the next.

The stderr argument shows the other side of the same idea. Passing os.Inherit means the child process writes errors to your process's standard error, which is what you want when you are running something interactively rather than capturing it.

The curl-to-temporary-file example shows the other common shape, redirecting standard output into a file with call(stdout = temp), and the spawn variant where you write the process's stdout into a file yourself.

## Filesystem verbs, and the reading and writing API

The operations section documents the API function by function, and the shape of it is the same throughout. os.read takes a readable path and returns a String, with overloads for a charset, and for a path with an offset and a count when the source supports seeking. The defaults are the whole file as UTF-8.

There is also os.read.bytes, returning an Array of Byte instead of a String, with the same offset and count overload for a path.

The introductory example shows the everyday verbs, and the order matters because it is the order a script would run in. Make sure the working directory exists and is empty, then read and write files, then perform filesystem operations:

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

Three things to note. os.remove.all and os.makeDir.all are the destructive and creating operations with the same all suffix, so they pair naturally in a reset. os.write takes a path and content directly, with os.write.over as the variant that overwrites, which appeared in the curl example. And os.list returns a Seq of paths, so the result of listing is a normal Scala collection you can filter and map.

The path type also carries the small conveniences you would otherwise write yourself. The cookbook filters on the extension with the ext property, and the moving-files example imports os./ to bring an extractor into scope so that a path can be matched against a pattern, with os.move.matching used to move files out of a folder by rewriting the path.

## Where OS-Lib is the wrong tool

The first wrong case is a project on Scala 2 with a frozen dependency set. The current line is 0.11.9 milestones, and the readme's own version attribute still reads 0.11.7, so the documentation and the published artifacts are not in step in a way you have to think about. If your build cannot take a milestone, you are looking at an older release with an older surface.

The second is a team that wants the Java APIs. OS-Lib's premise is that you should not need to drop down to the underlying Java APIs, and it exposes all the relevant capabilities. In practice, a large codebase that already uses java.nio.file in a hundred places does not benefit much from one library with nicer paths, and you now have two ways of doing the same thing.

The third is a project that needs platform behaviour OS-Lib does not model. The library wraps the common filesystem and subprocess APIs, and it is unopinionated about them. Anything beyond that, such as Windows specific file semantics, symbolic link tricks, or process groups and signals, is either absent or something you drop down to Java for, which defeats the purpose.

The fourth is a mixed-language team. This is a Scala library, and it is written in Scala with Scala-specific idioms such as path extractors, extension-style methods and the.mapValues and.sortBy calls in the examples. A Java developer on the same team can read it, but they cannot write it comfortably, and the value of the library comes precisely from those Scala-flavoured choices.

The alternative worth weighing is doing nothing. java.nio.file.Files and ProcessBuilder are perfectly serviceable, they are in the standard library, and a project that touches files once an hour does not need a wrapper. OS-Lib pays for itself in code that touches files and processes constantly, which in a build tool, a data pipeline or a test harness is a lot of code.

## Conclusion

Adopt os-lib if you are writing Scala code that touches files or runs processes and want path handling that reads like a scripting language while keeping the type safety of Scala, since the whole surface is built around an immutable Path and a small set of verbs. Keep away if you are on Scala 2 with a build that cannot resolve a milestone release, because the current line is 0.11.9 milestones and the readme still shows 0.11.7. Before you upgrade, read the Readme.adoc in the repository, since the documentation is AsciiDoc rather than Markdown, and note that the last push was on 2026-06-01 with 0.11.9-M8 published the same day.

## FAQ

### What does OS-Lib replace in Scala?

The README says it aims to be a complete replacement for java.nio.file.Files and java.nio.file.Paths, java.lang.ProcessBuilder, scala.io and scala.sys. The expectation is that you should not need to drop down to the underlying Java APIs, and the library has no dependencies of its own.

### How do I build a path in OS-Lib?

With the slash operator on a base path, for example os.pwd / "my-test-folder". Paths are immutable values, so the operator returns a new path rather than mutating, and there are pre-defined paths os.pwd, os.root and os.home to start from.

### How do I pipe the output of one process into another with OS-Lib?

Spawn each process and pass the stdout of one as the stdin of the next. The README example spawns curl, then gzip with stdin = curl.stdout, then shasum with stdin = gzip.stdout, so a shell is not needed between them.

### Which Scala version does OS-Lib target and how do I add it to a build?

For Mill, ivy"com.lihaoyi::os-lib:{version}", and for SBT, "com.lihaoyi" %% "os-lib" % "{version}". The API documentation is published for Scala 3, and the release line is on 0.11.9 milestones with 0.11.9-M8 published on 2026-06-01.

## Sources

- [com-lihaoyi/os-lib on GitHub](https://github.com/com-lihaoyi/os-lib)
- [Issues](https://github.com/com-lihaoyi/os-lib/issues)
- [README](https://github.com/com-lihaoyi/os-lib/blob/main/README.md)
- [Releases](https://github.com/com-lihaoyi/os-lib/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/com-lihaoyi-os-lib
