# os-lib **Repository Path**: mirrors_Flowdalic/os-lib ## Basic Information - **Project Name**: os-lib - **Description**: OS-Lib is a simple, flexible, high-performance Scala interface to common OS filesystem and subprocess APIs - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-06-15 - **Last Updated**: 2026-05-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README = OS-Lib :version: 0.9.1 :toc-placement: preamble :toclevels: 3 :toc: :link-geny: https://github.com/com-lihaoyi/geny :link-oslib: https://github.com/com-lihaoyi/os-lib :link-oslib-gitter: https://gitter.im/lihaoyi/os-lib :link-upickle-doc: https://com-lihaoyi.github.io/upickle :link-scalatags-doc: https://com-lihaoyi.github.io/scalatags/ :idprefix: :idseparator: - image:{link-oslib}/actions/workflows/build.yml/badge.svg[Build Status,link={link-oslib}/actions] image:https://badges.gitter.im/Join%20Chat.svg[Gitter Chat,link={link-oslib-gitter}] 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/javadoc.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: * https://www.handsonscala.com/[_Hands-on Scala Programming_] _Hands-on Scala_ has 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: * https://www.patreon.com/lihaoyi For a hands-on introduction to the library, take a look at these two blog posts: * http://www.lihaoyi.com/post/HowtoworkwithFilesinScala.html[How to work with Files in Scala] * http://www.lihaoyi.com/post/HowtoworkwithSubprocessesinScala.html[How to work with Subprocesses in Scala] == 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 operation 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 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.{GlobSyntax, /} 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( Seq[Byte]('I', ' '), Seq[Byte]('a', 'm'), Seq[Byte](' ', 'c'), Seq[Byte]('o', 'w') ) ---- ==== `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] ---- os.walk(path: Path, skip: Path => Boolean = _ => false, preOrder: Boolean = true, followLinks: Boolean = false, maxDepth: Int = Int.MaxValue, includeTarget: Boolean = false): IndexedSeq[Path] ---- 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] ---- os.walk.attrs(path: Path, skip: (Path, os.StatInfo) => Boolean = (_, _) => false, preOrder: Boolean = true, followLinks: Boolean = false, maxDepth: Int = Int.MaxValue, includeTarget: Boolean = false): IndexedSeq[(Path, os.StatInfo)] ---- 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} filesSortedBySize ==> Seq( wd / "misc" / "echo", wd / "misc" / "file-symlink", wd / "misc" / "echo_with_wd", wd / "misc" / "folder-symlink" / "one.txt", wd / "misc" / "binary.png" ) ---- ==== `os.walk.stream` [source,scala] ---- os.walk.stream(path: Path, skip: Path => Boolean = _ => false, preOrder: Boolean = true, followLinks: Boolean = false, maxDepth: Int = Int.MaxValue, includeTarget: Boolean = false): os.Generator[Path] ---- 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] ---- os.walk.stream.attrs(path: Path, skip: (Path, os.StatInfo) => Boolean = (_, _) => false, preOrder: Boolean = true, followLinks: Boolean = false, maxDepth: Int = Int.MaxValue, includeTarget: Boolean = false): os.Generator[(Path, os.StatInfo)] ---- 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] ---- os.exists(p: Path, followLinks: Boolean = true): Boolean ---- 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.exists(wd / "misc" / "file-symlink") ==> true os.exists(wd / "misc" / "folder-symlink") ==> true os.exists(wd / "misc" / "broken-symlink") ==> false os.exists(wd / "misc" / "broken-symlink", followLinks = false) ==> true ---- ==== `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.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: ```scala 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 <>, 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 <>. [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 <> 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 <> [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 <>, 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 <>. 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.remove.all` [source,scala] ---- os.remove.all(target: Path): 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.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 <> 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.all` 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 <> to customize its filesystem permissions. Passing in a <> 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 <> 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") ---- === Filesystem Metadata ==== `os.stat` [source,scala] ---- os.stat(p: os.Path, followLinks: Boolean = true): os.StatInfo ---- 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] ---- os.stat.posix(p: os.Path, followLinks: Boolean = true): os.PosixStatInfo ---- 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] ---- os.isFile(p: Path, followLinks: Boolean = true): Boolean ---- 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.isFile(wd / "misc" / "file-symlink") ==> true os.isFile(wd / "misc" / "folder-symlink") ==> false os.isFile(wd / "misc" / "file-symlink", followLinks = false) ==> false ---- ==== `os.isDir` [source,scala] ---- os.isDir(p: Path, followLinks: Boolean = true): Boolean ---- 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.isDir(wd / "misc" / "file-symlink") ==> false os.isDir(wd / "misc" / "folder-symlink") ==> true os.isDir(wd / "misc" / "folder-symlink", followLinks = false) ==> false ---- ==== `os.isLink` [source,scala] ---- os.isLink(p: Path, followLinks: Boolean = true): Boolean ---- 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.isLink(wd / "misc" / "file-symlink") ==> true os.isLink(wd / "misc" / "folder-symlink") ==> true os.isLink(wd / "folder1") ==> false ---- ==== `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 os.mtime.set(wd / "misc" / "file-symlink", 70000) os.mtime(wd / "File.txt") ==> 70000 os.mtime(wd / "misc" / "file-symlink") ==> 70000 assert(os.mtime(wd / "misc" / "file-symlink", followLinks = false) != 40000) ---- === Filesystem Permissions ==== `os.perms` [source,scala] ---- os.perms(p: Path, followLinks: Boolean = true): PermSet os.perms.set(p: Path, arg2: PermSet): Unit ---- 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] ---- os.owner(p: Path, followLinks: Boolean = true): UserPrincipal os.owner.set(arg1: Path, arg2: UserPrincipal): Unit os.owner.set(arg1: Path, arg2: String): Unit ---- 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] ---- os.group(p: Path, followLinks: Boolean = true): GroupPrincipal os.group.set(arg1: Path, arg2: GroupPrincipal): Unit os.group.set(arg1: Path, arg2: String): Unit ---- 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.proc(command: os.Shellable*).foo(...)+` calls, where the `command: 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.Shellable``s: * `java.lang.String` * `scala.Symbol` * `os.Path` * `os.RelPath` * `T: Numeric` * ``Iterable[T]``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 paren process's standard input or write directly to the parent process's standard output or error * `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` (``String``s, ``InputStream``s, ``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.proc.call` [source,scala] ---- os.proc(command: os.Shellable*) .call(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): os.CommandResult ---- 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 * `stdin`: any data you wish to pass to the subprocess's standard input * `stdout`/`stderr`: these are ``os.Redirect``s 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.proc('ls, wd/"folder2").call() 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.proc('ls, "doesnt-exist").call(cwd = wd) } assert(thrown.result.exitCode != 0) // Though you can avoid throwing by setting `check = false` val fail = os.proc('ls, "doesnt-exist").call(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.proc("shasum", "-a", "256").call(stdin = "Hello World") hash.out.trim() ==> "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e -" // Taking input from a file and directing output to another file os.proc("base64").call(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.proc('find, ".").call( 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.proc('find, ".").call( cwd = wd, stdout = os.ProcessOutput.Readlines( line => lineCount += 1 ), ) lineCount ==> 22 ---- ==== `os.proc.spawn` [source,scala] ---- os.proc(command: os.Shellable*) .spawn(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): os.SubProcess ---- The most flexible of the `os.proc` calls, `os.proc.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! For simpler cases, it may be easier to use `os.proc.stream` which triggers it's `onOut`/`onErr` callbacks all on the calling thread, avoiding needing to think about multithreading and concurrency issues. `stdin`, `stdout` and `stderr` are ``java.lang.OutputStream``s and ``java.lang.InputStream``s 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.proc("python", "-u", "-c", "while True: print(eval(raw_input()))") .spawn(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.proc("curl", "-L" , "https://git.io/fpfTs").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 -" ---- === 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 ---- `watch` currently only supports Linux and Mac-OSX, and not Windows. == 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.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.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: [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` wd / "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 ---- Note that there are no in-built operations to change the `os.pwd`. In general, you should not need to: simply defining 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.RelPath``s 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" val rel1 = os.rel / "folder" / "file" val rel2 = os.rel / "folder" / "file" // The path "file" val rel3 = 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. [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.SubPath``s 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" val sub1 = os.sub / "folder" / "file" val sub2 = os.sub / "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 sub3 = 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 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" / // `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. ==== 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 `/`: * ``String``s * ``Symbol``s * ``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 ``String``s, ``java.io.File``s or ``java.nio.file.Path``s: [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. ==== `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.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 {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 ``Tag``s 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.Generator``s are similar to iterators except instead of providing: * `def hasNext(): Boolean` * `def next(): T` ``os.Generator``s provide: * `+def generate(handleItem: A => Generator.Action): Generator.Action+` In general, you should not notice much of a difference using ``Generator``s 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 ``Generator``s 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.PermSet``s 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.String``s 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 ``Int``s 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.PermSet``s 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.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 `URI`s when constructing paths. === 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 === 0.8.1 - 2022-01-31 * Added support for Scala Native on Scala 3 === 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] === 0.7.8 - 2021-05-27 * Restored binary compatibility in `os.copy` and `os.copy.into` to os-lib versions before 0.7.5 === 0.7.7 - 2021-05-14 * Add support for Scala 3.0.0 === 0.7.6 - 2021-04-28 * Add support for Scala 3.0.0-RC3 === 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 === 0.7.4 * Add support for Scala 3.0.0-RC2 === 0.7.3 * Add support for Scala 3.0.0-RC1 * Migration of the CI system from Travis CI to GitHub Actions === 0.7.2 * Add support for Scala 3.0.0-M3 === 0.7.1 * Improve performance of `os.write` by buffering output stream to files === Older versions ==== 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` ==== 0.5.0 * `os.Source` now supports any data type that is `geny.Writable` ==== 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 <> 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` ==== 0.3.0 * Support for Scala 2.13.0 final ==== 0.2.8 * `os.ProcessOutput` trait is no longer sealed ==== 0.2.7 * Narrow return type of `readLink.absolute` from `FilePath` to `Path` * Fix handling of standaline `\r` in `os.SubProcess#stdout.readLine` ==== 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. ==== 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. ==== 0.2.2 * Allow chaining of multiple subprocesses `stdin`/`stdout` ==== 0.2.0 * First release