{"id":13713516,"url":"https://github.com/com-lihaoyi/os-lib","last_synced_at":"2026-02-10T09:13:15.933Z","repository":{"id":39675413,"uuid":"155298585","full_name":"com-lihaoyi/os-lib","owner":"com-lihaoyi","description":"OS-Lib is a simple, flexible, high-performance Scala interface to common OS filesystem and subprocess APIs","archived":false,"fork":false,"pushed_at":"2025-05-02T11:36:28.000Z","size":717,"stargazers_count":715,"open_issues_count":31,"forks_count":78,"subscribers_count":14,"default_branch":"main","last_synced_at":"2025-05-02T12:23:33.863Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/com-lihaoyi.png","metadata":{"files":{"readme":"Readme.adoc","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["lihaoyi","lefou"]}},"created_at":"2018-10-30T00:14:05.000Z","updated_at":"2025-05-02T11:36:01.000Z","dependencies_parsed_at":"2023-09-22T08:55:15.701Z","dependency_job_id":"ae16e103-e253-42d4-b7e2-4cc73c8378e2","html_url":"https://github.com/com-lihaoyi/os-lib","commit_stats":null,"previous_names":["lihaoyi/os"],"tags_count":63,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/com-lihaoyi%2Fos-lib","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/com-lihaoyi%2Fos-lib/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/com-lihaoyi%2Fos-lib/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/com-lihaoyi%2Fos-lib/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/com-lihaoyi","download_url":"https://codeload.github.com/com-lihaoyi/os-lib/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254070107,"owners_count":22009559,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T23:01:38.359Z","updated_at":"2026-02-10T09:13:15.922Z","avatar_url":"https://github.com/com-lihaoyi.png","language":"Scala","funding_links":["https://github.com/sponsors/lihaoyi","https://github.com/sponsors/lefou","https://www.patreon.com/lihaoyi"],"categories":["Containers \u0026 Language Extentions \u0026 Linting"],"sub_categories":["For Scala"],"readme":"= OS-Lib\n:version: 0.11.7\n:toc-placement: preamble\n:toclevels: 3\n:toc:\n:link-geny: https://github.com/com-lihaoyi/geny\n:link-oslib: https://github.com/com-lihaoyi/os-lib\n:link-upickle-doc: https://com-lihaoyi.github.io/upickle\n:link-scalatags-doc: https://com-lihaoyi.github.io/scalatags/\n:idprefix:\n:idseparator: -\n\nimage:https://img.shields.io/badge/patreon-sponsor-ff69b4.svg[Patreon,link=https://www.patreon.com/lihaoyi]\nimage: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]\n\n[source,scala]\n----\n// Make sure working directory exists and is empty\nval wd = os.pwd/\"out/splash\"\nos.remove.all(wd)\nos.makeDir.all(wd)\n\n// Read/write files\nos.write(wd/\"file.txt\", \"hello\")\nos.read(wd/\"file.txt\") ==\u003e \"hello\"\n\n// Perform filesystem operations\nos.copy(wd/\"file.txt\", wd/\"copied.txt\")\nos.list(wd) ==\u003e Seq(wd/\"copied.txt\", wd/\"file.txt\")\n\n// Invoke subprocesses\nval invoked = os.proc(\"cat\", wd/\"file.txt\", wd/\"copied.txt\").call(cwd = wd)\ninvoked.out.trim ==\u003e \"hellohello\"\n\n// Chain multiple subprocess' stdin/stdout together\nval curl = os.proc(\"curl\", \"-L\" , \"https://git.io/fpvpS\").spawn(stderr = os.Inherit)\nval gzip = os.proc(\"gzip\", \"-n\").spawn(stdin = curl.stdout)\nval sha = os.proc(\"shasum\", \"-a\", \"256\").spawn(stdin = gzip.stdout)\nsha.stdout.trim ==\u003e \"acc142175fa520a1cb2be5b97cbbe9bea092e8bba3fe2e95afa645615908229e  -\"\n----\n\nOS-Lib is a simple Scala interface to common OS filesystem and subprocess APIs.\nOS-Lib aims to make working with files and processes in Scala as simple as any\nscripting language, while still providing the safety, flexibility and\nperformance you would expect from Scala.\n\nOS-Lib aims to be a complete replacement for the\n`java.nio.file.Files`/`java.nio.file.Paths`, `java.lang.ProcessBuilder`\n`scala.io` and `scala.sys` APIs. You should not need to drop down to underlying\nJava APIs, as OS-Lib exposes all relevant capabilities in an intuitive and\nperformant way. OS-Lib has no dependencies and is unopinionated: it exposes the\nunderlying APIs in a concise but straightforward way, without introducing it's\nown idiosyncrasies, quirks, or clever DSLs.\n\nIf you use OS-Lib and like it, you will probably enjoy the following book by the Author:\n\n* https://www.handsonscala.com/[_Hands-on Scala Programming_]\n\n_Hands-on Scala_ uses OS-Lib extensively throughout the book, and has\nthe entirety of _Chapter 7: Files and Subprocesses_ dedicated to\nOS-Lib. _Hands-on Scala_ is a great way to level up your skills in Scala\nin general and OS-Lib in particular.\n\nYou can also support it by donating to our Patreon:\n\n* https://www.patreon.com/lihaoyi\n\nFor a hands-on introduction to the library, take a look at these two blog posts:\n\n* http://www.lihaoyi.com/post/HowtoworkwithFilesinScala.html[How to work with Files in Scala]\n* http://www.lihaoyi.com/post/HowtoworkwithSubprocessesinScala.html[How to work with Subprocesses in Scala]\n\n\n\n== Getting Started\n\nTo begin using OS-Lib, first add it as a dependency to your project's build:\n\n[source,scala,subs=\"attributes,verbatim\"]\n----\n// Mill\nivy\"com.lihaoyi::os-lib:{version}\"\n// SBT\n\"com.lihaoyi\" %% \"os-lib\" % \"{version}\"\n----\n\nhttps://javadoc.io/doc/com.lihaoyi/os-lib_3[API Documentation (Scala 3)]\n\n== Cookbook\n\nMost operations in OS-Lib take place on \u003c\u003cos-path\u003e\u003es, which are\nconstructed from a base path or working directory `wd`. Most often, the first\nthing to do is to define a `wd` path representing the folder you want to work\nwith:\n\n[source,scala]\n----\nval wd = os.pwd / \"my-test-folder\"\n----\n\nYou can of course have multiple base paths, to use in different parts of your program\nwhere convenient, or simply work with one of the pre-defined paths `os.pwd`,\n`os.root`, or `os.home`.\n\n=== Concatenate text files\n\n[source,scala]\n----\n// Find and concatenate all .txt files directly in the working directory\nos.write(\n  wd / \"all.txt\",\n  os.list(wd).filter(_.ext == \"txt\").map(os.read)\n)\n\nos.read(wd / \"all.txt\") ==\u003e\n  \"\"\"I am cowI am cow\n    |Hear me moo\n    |I weigh twice as much as you\n    |And I look good on the barbecue\"\"\".stripMargin\n----\n\n=== Spawning a subprocess on multiple files\n\n[source,scala]\n----\n// Find and concatenate all .txt files directly in the working directory using `cat`\nos.proc(\"cat\", os.list(wd).filter(_.ext == \"txt\")).call(stdout = wd / \"all.txt\")\n\nos.read(wd / \"all.txt\") ==\u003e\n  \"\"\"I am cowI am cow\n    |Hear me moo\n    |I weigh twice as much as you\n    |And I look good on the barbecue\"\"\".stripMargin\n----\n\n=== Curl URL to temporary file\n\n[source,scala]\n----\n// Curl to temporary file\nval temp = os.temp()\nos.proc(\"curl\", \"-L\" , \"https://git.io/fpfTs\").call(stdout = temp)\n\nos.size(temp) ==\u003e 53814\n\n// Curl to temporary file\nval temp2 = os.temp()\nval proc = os.proc(\"curl\", \"-L\" , \"https://git.io/fpfTJ\").spawn()\n\nos.write.over(temp2, proc.stdout)\nos.size(temp2) ==\u003e 53814\n----\n\n=== Recursive line count\n\n[source,scala]\n----\n// Line-count of all .txt files recursively in wd\nval lineCount = os.walk(wd)\n  .filter(_.ext == \"txt\")\n  .map(os.read.lines)\n  .map(_.size)\n  .sum\n\nlineCount ==\u003e 9\n----\n\n=== Largest Three Files\n\n[source,scala]\n----\n// Find the largest three files in the given folder tree\nval largestThree = os.walk(wd)\n  .filter(os.isFile(_, followLinks = false))\n  .map(x =\u003e os.size(x) -\u003e x).sortBy(-_._1)\n  .take(3)\n\nlargestThree ==\u003e Seq(\n  (711, wd / \"misc/binary.png\"),\n  (81, wd / \"Multi Line.txt\"),\n  (22, wd / \"folder1/one.txt\")\n)\n----\n\n=== Moving files out of folder\n\n[source,scala]\n----\n// Move all files inside the \"misc\" folder out of it\nimport os./\nos.list(wd / \"misc\").map(os.move.matching { case p/\"misc\"/x =\u003e p/x } )\n----\n\n=== Calculate word frequencies\n\n[source,scala]\n----\n// Calculate the word frequency of all the text files in the folder tree\ndef txt = os.walk(wd).filter(_.ext == \"txt\").map(os.read)\ndef freq(s: Seq[String]) = s.groupBy(x =\u003e x).mapValues(_.length).toSeq\nval map = freq(txt.flatMap(_.split(\"[^a-zA-Z0-9_]\"))).sortBy(-_._2)\nmap\n----\n\n== Operations\n\n=== Reading \u0026 Writing\n\n==== `os.read`\n\n[source,scala]\n----\nos.read(arg: os.ReadablePath): String\nos.read(arg: os.ReadablePath, charSet: Codec): String\nos.read(arg: os.Path,\n        offset: Long = 0,\n        count: Int = Int.MaxValue,\n        charSet: Codec = java.nio.charset.StandardCharsets.UTF_8): String\n----\n\nReads the contents of a \u003c\u003cos-path\u003e\u003e or other \u003c\u003cos-source\u003e\u003e as a\n`java.lang.String`. Defaults to reading the entire file as UTF-8, but you can\nalso select a different `charSet` to use, and provide an `offset`/`count` to\nread from if the source supports seeking.\n\n[source,scala]\n----\nos.read(wd / \"File.txt\") ==\u003e \"I am cow\"\nos.read(wd / \"folder1/one.txt\") ==\u003e \"Contents of folder one\"\nos.read(wd / \"Multi Line.txt\") ==\u003e\n  \"\"\"I am cow\n    |Hear me moo\n    |I weigh twice as much as you\n    |And I look good on the barbecue\"\"\".stripMargin\n----\n\n==== `os.read.bytes`\n\n[source,scala]\n----\nos.read.bytes(arg: os.ReadablePath): Array[Byte]\nos.read.bytes(arg: os.Path, offset: Long, count: Int): Array[Byte]\n----\n\nReads the contents of a \u003c\u003cos-path\u003e\u003e or \u003c\u003cos-source\u003e\u003e as an\n`Array[Byte]`; you can provide an `offset`/`count` to read from if the source\nsupports seeking.\n\n[source,scala]\n----\nos.read.bytes(wd / \"File.txt\") ==\u003e \"I am cow\".getBytes\nos.read.bytes(wd / \"misc/binary.png\").length ==\u003e 711\n----\n\n==== `os.read.chunks`\n\n[source,scala]\n----\nos.read.chunks(p: ReadablePath, chunkSize: Int): os.Generator[(Array[Byte], Int)]\nos.read.chunks(p: ReadablePath, buffer: Array[Byte]): os.Generator[(Array[Byte], Int)]\n----\n\nReads the contents of the given path in chunks of the given size;\nreturns a generator which provides a byte array and an offset into that\narray which contains the data for that chunk. All chunks will be of the\ngiven size, except for the last chunk which may be smaller.\n\nNote that the array returned by the generator is shared between each\ncallback; make sure you copy the bytes/array somewhere else if you want\nto keep them around.\n\nOptionally takes in a provided input `buffer` instead of a `chunkSize`,\nallowing you to re-use the buffer between invocations.\n\n[source,scala]\n----\nval chunks = os.read.chunks(wd / \"File.txt\", chunkSize = 2)\n  .map{case (buf, n) =\u003e buf.take(n).toSeq } // copy the buffer to save the data\n  .toSeq\n\nchunks ==\u003e Seq(\n  Seq[Byte]('I', ' '),\n  Seq[Byte]('a', 'm'),\n  Seq[Byte](' ', 'c'),\n  Seq[Byte]('o', 'w')\n)\n----\n\n==== `os.read.lines`\n\n[source,scala]\n----\nos.read.lines(arg: os.ReadablePath): IndexedSeq[String]\nos.read.lines(arg: os.ReadablePath, charSet: Codec): IndexedSeq[String]\n----\n\nReads the given \u003c\u003cos-path\u003e\u003e or other \u003c\u003cos-source\u003e\u003e as a string\nand splits it into lines; defaults to reading as UTF-8, which you can override\nby specifying a `charSet`.\n\n[source,scala]\n----\nos.read.lines(wd / \"File.txt\") ==\u003e Seq(\"I am cow\")\nos.read.lines(wd / \"Multi Line.txt\") ==\u003e Seq(\n  \"I am cow\",\n  \"Hear me moo\",\n  \"I weigh twice as much as you\",\n  \"And I look good on the barbecue\"\n)\n----\n\n==== `os.read.lines.stream`\n\n[source,scala]\n----\nos.read.lines(arg: os.ReadablePath): os.Generator[String]\nos.read.lines(arg: os.ReadablePath, charSet: Codec): os.Generator[String]\n----\n\nIdentical to \u003c\u003cos-read-lines\u003e\u003e, but streams the results back to you\nin a \u003c\u003cos-generator\u003e\u003e rather than accumulating them in memory.\nUseful if the file is large.\n\n[source,scala]\n----\nos.read.lines.stream(wd / \"File.txt\").count() ==\u003e 1\nos.read.lines.stream(wd / \"Multi Line.txt\").count() ==\u003e 4\n\n// Streaming the lines to the console\nfor(line \u003c- os.read.lines.stream(wd / \"Multi Line.txt\")){\n  println(line)\n}\n----\n\n==== `os.read.inputStream`\n\n[source,scala]\n----\nos.read.inputStream(p: ReadablePath): java.io.InputStream\n----\n\nOpens a `java.io.InputStream` to read from the given file.\n\n[source,scala]\n----\nval is = os.read.inputStream(wd / \"File.txt\") // ==\u003e \"I am cow\"\nis.read() ==\u003e 'I'\nis.read() ==\u003e ' '\nis.read() ==\u003e 'a'\nis.read() ==\u003e 'm'\nis.read() ==\u003e ' '\nis.read() ==\u003e 'c'\nis.read() ==\u003e 'o'\nis.read() ==\u003e 'w'\nis.read() ==\u003e -1\nis.close()\n----\n\n==== `os.read.stream`\n\n[source,scala]\n----\nos.read.stream(p: ReadablePath): geny.Readable\n----\n\nOpens a {link-geny}#readable[geny.Readable] to read from\nthe given file. This allows you to stream data to any other library that\nsupports `Readable` without buffering the data in memory, e.g. parsing it via\nFastParse, deserializing it via uPickle, uploading it via Requests-Scala, etc.\n\n[source,scala]\n----\nval readable: geny.Readable = os.read.stream(wd / \"File.json\")\n\nrequests.post(\"https://httpbin.org/post\", data = readable)\n\nupickle.default.read(readable)\n\nujson.read(readable)\n----\n\n==== `os.write`\n\n[source,scala]\n----\nos.write(target: Path,\n         data: os.Source,\n         perms: PermSet = null,\n         createFolders: Boolean = false): Unit\n----\n\nWrites data from the given file or \u003c\u003cos-source\u003e\u003e to a file at the\ntarget \u003c\u003cos-path\u003e\u003e. You can specify the filesystem permissions of the\nnewly created file by passing in a \u003c\u003cos-permset\u003e\u003e.\n\nThis throws an exception if the file already exists. To over-write or append to\nan existing file, see \u003c\u003cos-write-over\u003e\u003e or\n\u003c\u003cos-write-append\u003e\u003e.\n\nBy default, this doesn't create enclosing folders; you can enable this\nbehavior by setting `createFolders = true`\n\n[source,scala]\n----\nos.write(wd / \"New File.txt\", \"New File Contents\")\nos.read(wd / \"New File.txt\") ==\u003e \"New File Contents\"\n\nos.write(wd / \"NewBinary.bin\", Array[Byte](0, 1, 2, 3))\nos.read.bytes(wd / \"NewBinary.bin\") ==\u003e Array[Byte](0, 1, 2, 3)\n----\n\n==== `os.write.append`\n\n[source,scala]\n----\nos.write.append(target: Path,\n                data: os.Source,\n                perms: PermSet = null,\n                createFolders: Boolean = false): Unit\n----\n\nSimilar to \u003c\u003cos-write\u003e\u003e, except if the file already exists this appends\nthe written data to the existing file contents.\n\n[source,scala]\n----\nos.read(wd / \"File.txt\") ==\u003e \"I am cow\"\n\nos.write.append(wd / \"File.txt\", \", hear me moo\")\nos.read(wd / \"File.txt\") ==\u003e \"I am cow, hear me moo\"\n\nos.write.append(wd / \"File.txt\", \",\\nI weigh twice as much as you\")\nos.read(wd / \"File.txt\") ==\u003e\n  \"I am cow, hear me moo,\\nI weigh twice as much as you\"\n\nos.read.bytes(wd / \"misc/binary.png\").length ==\u003e 711\nos.write.append(wd / \"misc/binary.png\", Array[Byte](1, 2, 3))\nos.read.bytes(wd / \"misc/binary.png\").length ==\u003e 714\n----\n\n==== `os.write.over`\n\n[source,scala]\n----\nos.write.over(target: Path,\n              data: os.Source,\n              perms: PermSet = null,\n              offset: Long = 0,\n              createFolders: Boolean = false,\n              truncate: Boolean = true): Unit\n----\n\nSimilar to \u003c\u003cos-write\u003e\u003e, except if the file already exists this\nover-writes the existing file contents. You can also pass in `truncate = false`\nto avoid truncating the file if the new contents is shorter than the old\ncontents, and an `offset` to the file you want to write to.\n\n[source,scala]\n----\nos.read(wd / \"File.txt\") ==\u003e \"I am cow\"\nos.write.over(wd / \"File.txt\", \"You are cow\")\n\nos.read(wd / \"File.txt\") ==\u003e \"You are cow\"\n\nos.write.over(wd / \"File.txt\", \"We \", truncate = false)\nos.read(wd / \"File.txt\") ==\u003e \"We  are cow\"\n\nos.write.over(wd / \"File.txt\", \"s\", offset = 8, truncate = false)\nos.read(wd / \"File.txt\") ==\u003e \"We  are sow\"\n----\n\n==== `os.write.outputStream`\n\n[source,scala]\n----\nos.write.outputStream(target: Path,\n                      perms: PermSet = null,\n                      createFolders: Boolean = false,\n                      openOptions: Seq[OpenOption] = Seq(CREATE, WRITE))\n----\n\nOpen a `java.io.OutputStream` to write to the given file.\n\n[source,scala]\n----\nval out = os.write.outputStream(wd / \"New File.txt\")\nout.write('H')\nout.write('e')\nout.write('l')\nout.write('l')\nout.write('o')\nout.close()\n\nos.read(wd / \"New File.txt\") ==\u003e \"Hello\"\n----\n\n==== `os.truncate`\n\n[source,scala]\n----\nos.truncate(p: Path, size: Long): Unit\n----\n\nTruncate the given file to the given size. If the file is smaller than the\ngiven size, does nothing.\n\n[source,scala]\n----\nos.read(wd / \"File.txt\") ==\u003e \"I am cow\"\n\nos.truncate(wd / \"File.txt\", 4)\nos.read(wd / \"File.txt\") ==\u003e \"I am\"\n----\n\n=== Listing \u0026 Walking\n\n==== `os.list`\n\n[source,scala]\n----\nos.list(p: Path): IndexedSeq[Path]\nos.list(p: Path, sort: Boolean = true): IndexedSeq[Path]\n----\n\nReturns all the files and folders directly within the given folder. If the given\npath is not a folder, raises an error. Can be called via\n\u003c\u003cos-list-stream\u003e\u003e to stream the results. To list files recursively,\nuse \u003c\u003cos-walk\u003e\u003e.\n\nFor convenience `os.list` sorts the entries in the folder before returning\nthem. You can disable sorted by passing in the flag `sort = false`.\n\n[source,scala]\n----\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/one.txt\")\nos.list(wd / \"folder2\") ==\u003e Seq(\n  wd / \"folder2/nestedA\",\n  wd / \"folder2/nestedB\"\n)\n----\n\n==== `os.list.stream`\n\n[source,scala]\n----\nos.list.stream(p: Path): os.Generator[Path]\n----\n\nSimilar to \u003c\u003cos-list\u003e\u003e, except provides a \u003c\u003cos-generator\u003e\u003e of\nresults rather than accumulating all of them in memory. Useful if the result set\nis large.\n\n[source,scala]\n----\nos.list.stream(wd / \"folder2\").count() ==\u003e 2\n\n// Streaming the listed files to the console\nfor(line \u003c- os.list.stream(wd / \"folder2\")){\n  println(line)\n}\n----\n\n==== `os.walk`\n\n[source,scala]\n----\nos.walk(path: Path,\n        skip: Path =\u003e Boolean = _ =\u003e false,\n        preOrder: Boolean = true,\n        followLinks: Boolean = false,\n        maxDepth: Int = Int.MaxValue,\n        includeTarget: Boolean = false): IndexedSeq[Path]\n----\n\nRecursively walks the given folder and returns the paths of every file or folder\nwithin.\n\nYou can pass in a `skip` callback to skip files or folders you are not\ninterested in. This can avoid walking entire parts of the folder hierarchy,\nsaving time as compared to filtering them after the fact.\n\nBy default, the paths are returned as a pre-order traversal: the enclosing\nfolder is occurs first before any of it's contents. You can pass in `preOrder =\nfalse` to turn it into a post-order traversal, such that the enclosing folder\noccurs last after all it's contents.\n\n`os.walk` returns but does not follow symlinks; pass in `followLinks = true` to\noverride that behavior. You can also specify a maximum depth you wish to walk\nvia the `maxDepth` parameter.\n\n`os.walk` does not include the path given to it as part of the traversal by\ndefault. Pass in `includeTarget = true` to make it do so. The path appears at\nthe start of the traversal of `preOrder = true`, and at the end of the traversal\nif `preOrder = false`.\n\n[source,scala]\n----\nos.walk(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/one.txt\")\n\nos.walk(wd / \"folder1\", includeTarget = true) ==\u003e Seq(\n  wd / \"folder1\",\n  wd / \"folder1/one.txt\"\n)\n\nos.walk(wd / \"folder2\") ==\u003e Seq(\n  wd / \"folder2/nestedA\",\n  wd / \"folder2/nestedA/a.txt\",\n  wd / \"folder2/nestedB\",\n  wd / \"folder2/nestedB/b.txt\"\n)\n\nos.walk(wd / \"folder2\", preOrder = false) ==\u003e Seq(\n  wd / \"folder2/nestedA/a.txt\",\n  wd / \"folder2/nestedA\",\n  wd / \"folder2/nestedB/b.txt\",\n  wd / \"folder2/nestedB\"\n)\n\nos.walk(wd / \"folder2\", maxDepth = 1) ==\u003e Seq(\n  wd / \"folder2/nestedA\",\n  wd / \"folder2/nestedB\"\n)\n\nos.walk(wd / \"folder2\", skip = _.last == \"nestedA\") ==\u003e Seq(\n  wd / \"folder2/nestedB\",\n  wd / \"folder2/nestedB/b.txt\"\n)\n----\n\n==== `os.walk.attrs`\n\n[source,scala]\n----\nos.walk.attrs(path: Path,\n              skip: (Path, os.StatInfo) =\u003e Boolean = (_, _) =\u003e false,\n              preOrder: Boolean = true,\n              followLinks: Boolean = false,\n              maxDepth: Int = Int.MaxValue,\n              includeTarget: Boolean = false): IndexedSeq[(Path, os.StatInfo)]\n----\n\nSimilar to \u003c\u003cos-walk\u003e\u003e, except it also provides the `os.StatInfo`\nfilesystem metadata of every path that it returns. Can save time by allowing you\nto avoid querying the filesystem for metadata later. Note that `os.StatInfo`\ndoes not include filesystem ownership and permissions data; use `os.stat.posix` on\nthe path if you need those attributes.\n\n[source,scala]\n----\nval filesSortedBySize = os.walk.attrs(wd / \"misc\", followLinks = true)\n  .sortBy{case (p, attrs) =\u003e attrs.size}\n  .collect{case (p, attrs) if attrsisFile =\u003e p}\n\nfilesSortedBySize ==\u003e Seq(\n  wd / \"misc/echo\",\n  wd / \"misc/file-symlink\",\n  wd / \"misc/echo_with_wd\",\n  wd / \"misc/folder-symlink/one.txt\",\n  wd / \"misc/binary.png\"\n)\n----\n\n==== `os.walk.stream`\n\n[source,scala]\n----\nos.walk.stream(path: Path,\n              skip: Path =\u003e Boolean = _ =\u003e false,\n              preOrder: Boolean = true,\n              followLinks: Boolean = false,\n              maxDepth: Int = Int.MaxValue,\n              includeTarget: Boolean = false): os.Generator[Path]\n----\n\nSimilar to \u003c\u003cos-walk\u003e\u003e, except returns a \u003c\u003cos-generator\u003e\u003e of\nthe results rather than accumulating them in memory. Useful if you are walking\nvery large folder hierarchies, or if you wish to begin processing the output\neven before the walk has completed.\n\n[source,scala]\n----\nos.walk.stream(wd / \"folder1\").count() ==\u003e 1\n\nos.walk.stream(wd / \"folder2\").count() ==\u003e 4\n\nos.walk.stream(wd / \"folder2\", skip = _.last == \"nestedA\").count() ==\u003e 2\n----\n\n==== `os.walk.stream.attrs`\n\n[source,scala]\n----\nos.walk.stream.attrs(path: Path,\n                     skip: (Path, os.StatInfo) =\u003e Boolean = (_, _) =\u003e false,\n                     preOrder: Boolean = true,\n                     followLinks: Boolean = false,\n                     maxDepth: Int = Int.MaxValue,\n                     includeTarget: Boolean = false): os.Generator[(Path, os.StatInfo)]\n----\n\nSimilar to \u003c\u003cos-walk-stream\u003e\u003e, except it also provides the filesystem\nmetadata of every path that it returns. Can save time by allowing you to avoid\nquerying the filesystem for metadata later.\n\n[source,scala]\n----\ndef totalFileSizes(p: os.Path) = os.walk.stream.attrs(p)\n  .collect{case (p, attrs) if attrs.isFile =\u003e attrs.size}\n  .sum\n\ntotalFileSizes(wd / \"folder1\") ==\u003e 22\ntotalFileSizes(wd / \"folder2\") ==\u003e 40\n----\n\n=== Manipulating Files \u0026 Folders\n\n==== `os.exists`\n\n[source,scala]\n----\nos.exists(p: Path, followLinks: Boolean = true): Boolean\n----\n\nChecks if a file or folder exists at the specified path\n\n[source,scala]\n----\nos.exists(wd / \"File.txt\") ==\u003e true\nos.exists(wd / \"folder1\") ==\u003e true\nos.exists(wd / \"doesnt-exist\") ==\u003e false\n\nos.exists(wd / \"misc/file-symlink\") ==\u003e true\nos.exists(wd / \"misc/folder-symlink\") ==\u003e true\nos.exists(wd / \"misc/broken-symlink\") ==\u003e false\nos.exists(wd / \"misc/broken-symlink\", followLinks = false) ==\u003e true\n----\n\n==== `os.move`\n\n[source,scala]\n----\nos.move(from: Path, to: Path): Unit\nos.move(from: Path, to: Path, createFolders: Boolean): Unit\n----\n\nMoves a file or folder from one path to another. Errors out if the destination\npath already exists, or is within the source path.\n\n[source,scala]\n----\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/one.txt\")\nos.move(wd / \"folder1/one.txt\", wd / \"folder1/first.txt\")\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/first.txt\")\n\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/nestedA\", wd / \"folder2/nestedB\")\nos.move(wd / \"folder2/nestedA\", wd / \"folder2/nestedC\")\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/nestedB\", wd / \"folder2/nestedC\")\n\nos.read(wd / \"File.txt\") ==\u003e \"I am cow\"\nos.move(wd / \"Multi Line.txt\", wd / \"File.txt\", replaceExisting = true)\nos.read(wd / \"File.txt\") ==\u003e\n  \"\"\"I am cow\n    |Hear me moo\n    |I weigh twice as much as you\n    |And I look good on the barbecue\"\"\".stripMargin\n----\n\n==== `os.move.matching`\n\n[source,scala]\n----\nos.move.matching(t: PartialFunction[Path, Path]): PartialFunction[Path, Unit]\n----\n\n`os.move` can also be used as a transformer, via `os.move.matching`. This lets\nyou use `.map` or `.collect` on a list of paths, and move all of them at once,\ne.g. to rename all `.txt` files within a folder tree to `.data`:\n\n[source,scala]\n----\nimport os.{GlobSyntax, /}\nos.walk(wd / \"folder2\") ==\u003e Seq(\n  wd / \"folder2/nestedA\",\n  wd / \"folder2/nestedA/a.txt\",\n  wd / \"folder2/nestedB\",\n  wd / \"folder2/nestedB/b.txt\"\n)\n\nos.walk(wd/'folder2).collect(os.move.matching{case p/g\"$x.txt\" =\u003e p/g\"$x.data\"})\n\nos.walk(wd / \"folder2\") ==\u003e Seq(\n  wd / \"folder2/nestedA\",\n  wd / \"folder2/nestedA/a.data\",\n  wd / \"folder2/nestedB\",\n  wd / \"folder2/nestedB/b.data\"\n)\n----\n\n==== `os.move.into`\n\n[source,scala]\n----\nos.move.into(from: Path, to: Path): Unit\n----\n\nMove the given file or folder _into_ the destination folder\n\n[source,scala]\n----\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/one.txt\")\nos.move.into(wd / \"File.txt\", wd / \"folder1\")\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/File.txt\", wd / \"folder1/one.txt\")\n----\n\n==== `os.move.over`\n\n[source,scala]\n----\nos.move.over(from: Path, to: Path): Unit\n----\n\nMove a file or folder from one path to another, and _overwrite_ any file or\nfolder than may already be present at that path\n\n[source,scala]\n----\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/nestedA\", wd / \"folder2/nestedB\")\nos.move.over(wd / \"folder1\", wd / \"folder2\")\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/one.txt\")\n----\n\n==== `os.copy`\n\n[source,scala]\n----\nos.copy(from: Path, to: Path): Unit\nos.copy(from: Path, to: Path, createFolders: Boolean): Unit\n----\n\nCopy a file or folder from one path to another. Recursively copies folders with\nall their contents. Errors out if the destination path already exists, or is\nwithin the source path.\n\n[source,scala]\n----\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/one.txt\")\nos.copy(wd / \"folder1/one.txt\", wd / \"folder1/first.txt\")\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/first.txt\", wd / \"folder1/one.txt\")\n\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/nestedA\", wd / \"folder2/nestedB\")\nos.copy(wd / \"folder2/nestedA\", wd / \"folder2/nestedC\")\nos.list(wd / \"folder2\") ==\u003e Seq(\n  wd / \"folder2/nestedA\",\n  wd / \"folder2/nestedB\",\n  wd / \"folder2/nestedC\"\n)\n\nos.read(wd / \"File.txt\") ==\u003e \"I am cow\"\nos.copy(wd / \"Multi Line.txt\", wd / \"File.txt\", replaceExisting = true)\nos.read(wd / \"File.txt\") ==\u003e\n  \"\"\"I am cow\n    |Hear me moo\n    |I weigh twice as much as you\n    |And I look good on the barbecue\"\"\".stripMargin\n    ```\n\n`os.copy` can also be used as a transformer:\n\n```scala\nos.copy.matching(t: PartialFunction[Path, Path]): PartialFunction[Path, Unit]\n----\n\nThis lets you use `.map` or `.collect` on a list of paths, and copy all of them\nat once:\n\n[source,scala]\n----\npaths.map(os.copy.matching{case p/\"scala\"/file =\u003e p/\"java\"/file})\n----\n\n==== `os.copy.into`\n\n[source,scala]\n----\nos.copy.into(from: Path, to: Path): Unit\n----\n\nCopy the given file or folder _into_ the destination folder\n\n[source,scala]\n----\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/one.txt\")\nos.copy.into(wd / \"File.txt\", wd / \"folder1\")\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/File.txt\", wd / \"folder1/one.txt\")\n----\n\n==== `os.copy.over`\n\n[source,scala]\n----\nos.copy.over(from: Path, to: Path): Unit\n----\n\nSimilar to \u003c\u003cos-copy\u003e\u003e, but if the destination file already exists then\noverwrite it instead of erroring out.\n\n[source,scala]\n----\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/nestedA\", wd / \"folder2/nestedB\")\nos.copy.over(wd / \"folder1\", wd / \"folder2\")\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/one.txt\")\n----\n\n==== `os.copy` with `mergeFolders`\n\n_Since 0.7.5_\n\nIf you want to copy a directory over another but don't want to overwrite the whole destination directory (and loose it's content),\nyou can use the `mergeFolders` option of \u003c\u003cos-copy\u003e\u003e.\n\n[source,scala]\n----\nos.list(wd / \"folder1\") ==\u003e Seq(wd / \"folder1/one.txt\")\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/nestedA\", wd / \"folder2/nestedB\")\nos.copy(wd / \"folder1\", wd / \"folder2\", mergeFolders = true)\nos.list(wd / \"folder2\") ==\u003e Seq(wd / \"folder2/one.txt\", wd / \"folder2/nestedA\", wd / \"folder2/nestedB\")\n----\n\n==== `os.makeDir`\n\n[source,scala]\n----\nos.makeDir(path: Path): Unit\nos.makeDir(path: Path, perms: PermSet): Unit\n----\n\nCreate a single directory at the specified path. Optionally takes in a\n\u003c\u003cos-permset\u003e\u003e to specify the filesystem permissions of the created\ndirectory.\n\nErrors out if the directory already exists, or if the parent directory of the\nspecified path does not exist. To automatically create enclosing directories and\nignore the destination if it already exists, using\n\u003c\u003cos-makedir-all\u003e\u003e\n\n[source,scala]\n----\nos.exists(wd / \"new_folder\") ==\u003e false\nos.makeDir(wd / \"new_folder\")\nos.exists(wd / \"new_folder\") ==\u003e true\n----\n\n==== `os.makeDir.all`\n\n[source,scala]\n----\nos.makeDir.all(path: Path): Unit\nos.makeDir.all(path: Path,\n               perms: PermSet = null,\n               acceptLinkedDirectory: Boolean = true): Unit\n----\n\nSimilar to \u003c\u003cos-makedir\u003e\u003e, but automatically creates any necessary\nenclosing directories if they do not exist, and does not raise an error if the\ndestination path already contains a directory. Also does not raise an error if\nthe destination path contains a symlink to a directory, though you can force it\nto error out in that case by passing in `acceptLinkedDirectory = false`\n\n[source,scala]\n----\nos.exists(wd / \"new_folder\") ==\u003e false\nos.makeDir.all(wd / \"new_folder/inner/deep\")\nos.exists(wd / \"new_folder/inner/deep\") ==\u003e true\n----\n\n==== `os.remove`\n\n[source,scala]\n----\nos.remove(target: Path): Boolean\nos.remove(target: Path, checkExists: Boolean = false): Boolean\n----\n\nRemove the target file or folder. Folders need to be empty to be removed; if you\nwant to remove a folder tree recursively, use \u003c\u003cos-remove-all\u003e\u003e.\nReturns `true` if the file was present before.\nIt will fail with an exception when the file is missing but `checkExists` is `true`,\nor when the directory to remove is not empty.\n\n[source,scala]\n----\nos.exists(wd / \"File.txt\") ==\u003e true\nos.remove(wd / \"File.txt\")\nos.exists(wd / \"File.txt\") ==\u003e false\n\nos.exists(wd / \"folder1/one.txt\") ==\u003e true\nos.remove(wd / \"folder1/one.txt\")\nos.remove(wd / \"folder1\")\nos.exists(wd / \"folder1/one.txt\") ==\u003e false\nos.exists(wd / \"folder1\") ==\u003e false\n----\n\nWhen removing symbolic links, it is the link that gets removed, and not its\ndestination:\n\n[source,scala]\n----\nos.remove(wd / \"misc/file-symlink\")\nos.exists(wd / \"misc/file-symlink\", followLinks = false) ==\u003e false\nos.exists(wd / \"File.txt\", followLinks = false) ==\u003e true\n\nos.remove(wd / \"misc/folder-symlink\")\nos.exists(wd / \"misc/folder-symlink\", followLinks = false) ==\u003e false\nos.exists(wd / \"folder1\", followLinks = false) ==\u003e true\nos.exists(wd / \"folder1/one.txt\", followLinks = false) ==\u003e true\n\nos.remove(wd / \"misc/broken-symlink\")\nos.exists(wd / \"misc/broken-symlink\", followLinks = false) ==\u003e false\n----\n\nIf you wish to remove the destination of a symlink, use\n\u003c\u003cos-readlink\u003e\u003e.\n\n==== `os.remove.all`\n\n[source,scala]\n----\nos.remove.all(target: Path, ignoreErrors: Boolean = false): Unit\n----\n\nRemove the target file or folder; if it is a folder and not empty, recursively\nremoving all it's contents before deleting it.\n\n[source,scala]\n----\nos.exists(wd / \"folder1/one.txt\") ==\u003e true\nos.remove.all(wd / \"folder1\")\nos.exists(wd / \"folder1/one.txt\") ==\u003e false\nos.exists(wd / \"folder1\") ==\u003e false\n----\n\nWhen removing symbolic links, it is the links that gets removed, and not it's\ndestination:\n\n[source,scala]\n----\nos.remove.all(wd / \"misc/file-symlink\")\nos.exists(wd / \"misc/file-symlink\", followLinks = false) ==\u003e false\nos.exists(wd / \"File.txt\", followLinks = false) ==\u003e true\n\nos.remove.all(wd / \"misc/folder-symlink\")\nos.exists(wd / \"misc/folder-symlink\", followLinks = false) ==\u003e false\nos.exists(wd / \"folder1\", followLinks = false) ==\u003e true\nos.exists(wd / \"folder1/one.txt\", followLinks = false) ==\u003e true\n\nos.remove.all(wd / \"misc/broken-symlink\")\nos.exists(wd / \"misc/broken-symlink\", followLinks = false) ==\u003e false\n----\n\nIf you wish to remove the destination of a symlink, use\n\u003c\u003cos-readlink\u003e\u003e.\n\n``os.remove.all`` removes nested files and folders one at a time, and any failure\nin removing a file (e.g. due to permissions) or folder (e.g. due to someone concurrently\ncreating a file within it) causes an error to be thrown and terminates the removal early.\nYou can pass `ignoreErrors = false` to continue with the deletion of other files\neven if some files or folders failed to be removed.\n\n==== `os.hardlink`\n\n[source,scala]\n----\nos.hardlink(src: Path, dest: Path, perms): Unit\n----\n\nCreate a hardlink to the source path from the destination path\n\n[source,scala]\n----\nos.hardlink(wd / \"File.txt\", wd / \"Linked.txt\")\nos.exists(wd / \"Linked.txt\")\nos.read(wd / \"Linked.txt\") ==\u003e \"I am cow\"\nos.isLink(wd / \"Linked.txt\") ==\u003e false\n----\n\n==== `os.symlink`\n\n[source,scala]\n----\nos.symlink(link: Path, dest: FilePath, perms: PermSet = null): Unit\n----\n\nCreate a symbolic to the source path from the destination path. Optionally takes\na \u003c\u003cos-permset\u003e\u003e to customize the filesystem permissions of the symbolic\nlink.\n\n[source,scala]\n----\nos.symlink(wd / \"File.txt\", wd / \"Linked.txt\")\nos.exists(wd / \"Linked.txt\")\nos.read(wd / \"Linked.txt\") ==\u003e \"I am cow\"\nos.isLink(wd / \"Linked.txt\") ==\u003e true\n----\n\nYou can create symlinks with either absolute ``os.Path``s or relative ``os.RelPath``s:\n\n[source,scala]\n----\nos.symlink(wd / \"File.txt\", os.rel/ \"Linked2.txt\")\nos.exists(wd / \"Linked2.txt\")\nos.read(wd / \"Linked2.txt\") ==\u003e \"I am cow\"\nos.isLink(wd / \"Linked2.txt\") ==\u003e true\n----\n\nCreating absolute and relative symlinks respectively. Relative symlinks are\nresolved relative to the enclosing folder of the link.\n\n==== `os.readLink`\n\n[source,scala]\n----\nos.readLink(src: Path): os.FilePath\nos.readLink.absolute(src: Path): os.Path\n----\n\nReturns the immediate destination of the given symbolic link.\n\n[source,scala]\n----\nos.readLink(wd / \"misc/file-symlink\") ==\u003e os.up / \"File.txt\"\nos.readLink(wd / \"misc/folder-symlink\") ==\u003e os.up / \"folder1\"\nos.readLink(wd / \"misc/broken-symlink\") ==\u003e os.rel / \"broken\"\nos.readLink(wd / \"misc/broken-abs-symlink\") ==\u003e os.root / \"doesnt/exist\"\n----\n\nNote that symbolic links can be either absolute ``os.Path``s or relative\n``os.RelPath``s, represented by `os.FilePath`. You can also use `os.readLink.absolute`\nto automatically resolve relative symbolic links to their absolute destination:\n\n[source,scala]\n----\nos.readLink.absolute(wd / \"misc/file-symlink\") ==\u003e wd / \"File.txt\"\nos.readLink.absolute(wd / \"misc/folder-symlink\") ==\u003e wd / \"folder1\"\nos.readLink.absolute(wd / \"misc/broken-symlink\") ==\u003e wd / \"misc/broken\"\nos.readLink.absolute(wd / \"misc/broken-abs-symlink\") ==\u003e os.root / \"doesnt/exist\"\n----\n\n==== `os.followLink`\n\n[source,scala]\n----\nos.followLink(src: Path): Option[Path]\n----\n\nAttempts to any deference symbolic links in the given path, recursively, and return the\ncanonical path. Returns `None` if the path cannot be resolved (i.e. some\nsymbolic link in the given path is broken)\n\n[source,scala]\n----\nos.followLink(wd / \"misc/file-symlink\") ==\u003e Some(wd / \"File.txt\")\nos.followLink(wd / \"misc/folder-symlink\") ==\u003e Some(wd / \"folder1\")\nos.followLink(wd / \"misc/broken-symlink\") ==\u003e None\n----\n\n==== `os.temp`\n\n[source,scala]\n----\nos.temp(contents: os.Source = null,\n        dir: Path = null,\n        prefix: String = null,\n        suffix: String = null,\n        deleteOnExit: Boolean = true,\n        perms: PermSet = null): Path\n----\n\nCreates a temporary file. You can optionally provide a `dir` to specify where\nthis file lives, file-`prefix` and file-`suffix` to customize what it looks\nlike, and a \u003c\u003cos-permset\u003e\u003e to customize its filesystem permissions.\n\nPassing in a \u003c\u003cos-source\u003e\u003e will initialize the contents of that file to\nthe provided data; otherwise it is created empty.\n\nBy default, temporary files are deleted on JVM exit. You can disable that\nbehavior by setting `deleteOnExit = false`\n\n[source,scala]\n----\nval tempOne = os.temp(\"default content\")\nos.read(tempOne) ==\u003e \"default content\"\nos.write.over(tempOne, \"Hello\")\nos.read(tempOne) ==\u003e \"Hello\"\n----\n\n==== `os.temp.dir`\n\n[source,scala]\n----\nos.temp.dir(dir: Path = null,\n            prefix: String = null,\n            deleteOnExit: Boolean = true,\n            perms: PermSet = null): Path\n----\n\nCreates a temporary directory. You can optionally provide a `dir` to specify\nwhere this file lives, a `prefix` to customize what it looks like, and a\n\u003c\u003cos-permset\u003e\u003e to customize its filesystem permissions.\n\nBy default, temporary directories are deleted on JVM exit. You can disable that\nbehavior by setting `deleteOnExit = false`\n\n[source,scala]\n----\nval tempDir = os.temp.dir()\nos.list(tempDir) ==\u003e Nil\nos.write(tempDir / \"file\", \"Hello\")\nos.list(tempDir) ==\u003e Seq(tempDir / \"file\")\n----\n\n=== Zip \u0026 Unzip Files\n\n[NOTE]\n====\nJVM 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`.\n====\n\n==== `os.zip`\n\n[source,scala]\n----\ndef apply(dest: os.Path,\n          sources: Seq[ZipSource] = List(),\n          excludePatterns: Seq[Regex] = List(),\n          includePatterns: Seq[Regex] = List(),\n          preserveMtimes: Boolean = false,\n          deletePatterns: Seq[Regex] = List(),\n          compressionLevel: Int = -1, /* 0-9 */\n          followLinks: Boolean = true): os.Path\n----\n\nThe zip object provides functionality to create or modify zip archives. It supports:\n\n- Zipping Files and Directories: You can zip both individual files and entire directories.\n- Appending to Existing Archives: Files can be appended to an existing zip archive.\n- Exclude Patterns (-x): You can specify files or patterns to exclude while zipping.\n- Include Patterns (-i): You can include specific files or patterns while zipping.\n- Delete Patterns (-d): You can delete specific files from an existing zip archive.\n- 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.\n- Configuring whether or not to preserve filesyste mtimes.\n- Preserving Unix file permissions.\n\nThis will create a new zip archive at `dest` containing `file1.txt` and everything\ninside `sources`. If `dest` already exists as a zip, the files will be appended to the\nexisting zip, and any existing zip entries matching `deletePatterns` will be removed.\n\nWhen modifying an existing zip file,\n- Unix file permissions will be preserved if Java Runtime Version \u003e= 14.\n- If using Java Runtime Version \u003c 14, Unix file permissions are not preserved, even for existing zip entries.\n- Symbolics links will always be stored as the referenced files.\n- Existing symbolic links stored in the zip might lose their symbolic link file type field and become broken.\n\n===== Zipping Files and Folders\n\nThe example below demonstrates the core workflows: creating a zip, appending to it, and\nunzipping it:\n\n[source,scala]\n----\n// Zipping files and folders in a new zip file\nval zipFileName = \"zip-file-test.zip\"\nval zipFile1: os.Path = os.zip(\n  destination = wd / zipFileName,\n  sourcePaths = Seq(\n    wd / \"File.txt\",\n    wd / \"folder1\"\n  )\n)\n\n// Adding files and folders to an existing zip file\nos.zip(\n  destination = zipFile1,\n  sourcePaths = Seq(\n    wd / \"folder2\",\n    wd / \"Multi Line.txt\"\n  )\n)\n\n// Unzip file to a destination folder\nval unzippedFolder = os.unzip(\n  source = wd / zipFileName,\n  destination = wd / \"unzipped folder\"\n)\n\nval paths = os.walk(unzippedFolder)\nval expected = Seq(\n  // Files get included in the zip root using their name\n  wd / \"unzipped folder/File.txt\",\n  wd / \"unzipped folder/Multi Line.txt\",\n  // Folder contents get included relative to the source root\n  wd / \"unzipped folder/nestedA\",\n  wd / \"unzipped folder/nestedB\",\n  wd / \"unzipped folder/one.txt\",\n  wd / \"unzipped folder/nestedA/a.txt\",\n  wd / \"unzipped folder/nestedB/b.txt\",\n)\nassert(paths.sorted == expected)\n----\n\n===== Renaming files in the zip\n\nYou can also pass in a mapping to `os.zip` to specify exactly where in the zip each\ninput source file or folder should go:\n\n```scala\nval zipFileName = \"zip-file-test.zip\"\nval zipFile1: os.Path = os.zip(\n  destination = wd / zipFileName,\n  sourcePaths = List(\n    // renaming files and folders\n    wd / \"File.txt\" -\u003e os.sub / \"renamed-file.txt\",\n    wd / \"folder1\" -\u003e os.sub / \"renamed-folder\"\n  )\n)\n\nval unzippedFolder = os.unzip(\n  source = zipFile1,\n  destination = wd / \"unzipped folder\"\n)\n\nval paths = os.walk(unzippedFolder)\nval expected = Seq(\n  wd / \"unzipped folder/renamed-file.txt\",\n  wd / \"unzipped folder/renamed-folder\",\n  wd / \"unzipped folder/renamed-folder/one.txt\",\n)\nassert(paths.sorted == expected)\n```\n\n===== Excluding/Including Files in Zip\n\nYou can specify files or folders to be excluded or included when creating the zip:\n\n[source,scala]\n----\nos.zip(\n  os.Path(\"/path/to/destination.zip\"),\n  List(os.Path(\"/path/to/folder\")),\n  excludePatterns = List(\".*\\\\.log\".r, \"temp/.*\".r),  // Exclude log files and \"temp\" folder\n  includePatterns = List(\".*\\\\.txt\".r)              // Include only .txt files\n)\n\n----\n\nThis will include only `.txt` files, excluding any `.log` files and anything inside\nthe `temp` folder.\n\n==== `os.zip.stream`\n\nYou can use `os.zip.stream` to write the final zip to an `OutputStream` rather than a\nconcrete `os.Path`. `os.zip.stream` returns a `geny.Writable`, which has a `writeBytesToStream`\nmethod:\n\n```scala\nval zipFileName = \"zipStreamFunction.zip\"\n\nval stream = os.write.outputStream(wd / \"zipStreamFunction.zip\")\n\nval writable = zip.stream(sources = Seq(wd / \"File.txt\"))\n\nwritable.writeBytesTo(stream)\nstream.close()\n\nval unzippedFolder = os.unzip(\n  source = wd / zipFileName,\n  dest = wd / \"zipStreamFunction\"\n)\n\nval paths = os.walk(unzippedFolder)\nassert(paths == Seq(unzippedFolder / \"File.txt\"))\n```\n\nThis can be useful for streaming the zipped data to places which are not files:\nover the network, over a pipe, etc.\n\nFile 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`.\n\n==== `os.unzip`\n\n===== Unzipping Files\n[source,scala]\n\n----\nos.unzip(os.Path(\"/path/to/archive.zip\"), Some(os.Path(\"/path/to/destination\")))\n----\n\nThis extracts the contents of `archive.zip` to the specified destination. It supports preserving file permissions and symbolic links.\n\n\n===== Excluding Files While Unzipping\nYou can exclude certain files from being extracted using patterns:\n\n[source,scala]\n----\nos.unzip(\n  os.Path(\"/path/to/archive.zip\"),\n  Some(os.Path(\"/path/to/destination\")),\n  excludePatterns = List(\".*\\\\.log\".r, \"temp/.*\".r)  // Exclude log files and the \"temp\" folder\n)\n----\n\n===== `os.unzip.list`\nYou can list the contents of the zip file without extracting them:\n\n[source,scala]\n----\nos.unzip.list(os.Path(\"/path/to/archive.zip\"))\n----\n\nThis will print all the file paths contained in the zip archive. File permissions and symbolic links will not be preserved.\n\n==== `os.unzip.stream`\n\nYou can unzip a zip file from any arbitrary `java.io.InputStream` containing its binary data\nusing the `os.unzip.stream` method:\n\n```scala\nval readableZipStream: java.io.InputStream = ???\n\n// Unzipping the stream to the destination folder\nos.unzip.stream(\n  source = readableZipStream,\n  dest = unzippedFolder\n)\n```\n\nThis can be useful if the zip file does not exist on disk, e.g. if it is received over the network\nor produced in-memory by application logic.\n\nFile 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.\nFor more a more detailed explanation see the `ZipArchiveInputStream` vs `ZipFile` section at https://commons.apache.org/proper/commons-compress/zip.html.\n\nOS-Lib also provides the `os.unzip.streamRaw` API, which is a lower level API used internally\nwithin `os.unzip.stream` but can also be used directly if lower-level control is necessary.\n\n==== `os.zip.open`\n\n```scala\nos.zip.open(path: Path): ZipRoot\n```\n\n`os.zip.open` allows you to treat zip files as filesystems, using normal `os.*` operations\non them. This provides a move flexible way to manipulate the contents of the zip in a fine-grained\nmanner when the normal `os.zip` or `os.unzip` operations do not suffice.\n\n```scala\nval zipFile = os.zip.open(wd / \"zip-test.zip\")\ntry {\n  os.copy(wd / \"File.txt\", zipFile / \"File.txt\")\n  os.copy(wd / \"folder1\", zipFile / \"folder1\")\n  os.copy(wd / \"folder2\", zipFile / \"folder2\")\n}finally zipFile.close()\n\nval zipFile2 = os.zip.open(wd / \"zip-test.zip\")\ntry{\n  os.list(zipFile2) ==\u003e Vector(zipFile2 / \"File.txt\", zipFile2 / \"folder1\", zipFile2 / \"folder2\")\n  os.remove.all(zipFile2 / \"folder2\")\n  os.remove(zipFile2 / \"File.txt\")\n}finally zipFile2.close()\n\nval zipFile3 = os.zip.open(wd / \"zip-test.zip\")\ntry os.list(zipFile3) ==\u003e Vector(zipFile3 / \"folder1\")\nfinally zipFile3.close()\n```\n\n`os.zip.open` returns a `ZipRoot`, which is identical to `os.Path` except it references the root\nof the zip file rather than a bare path on the filesystem. Note that you need to call `ZipRoot#close()`\nwhen you are done with it to avoid leaking filesystem resources.\n\nFile permissions are only supported for Java Runtime Version \u003e= 14. Symbolic links are not supported. Using `os.zip.open` on a zip archive that contains symbolic links might break the links.\n\n=== Filesystem Metadata\n\n==== `os.stat`\n\n[source,scala]\n----\nos.stat(p: os.Path, followLinks: Boolean = true): os.StatInfo\n----\n\nReads in the basic filesystem metadata for the given file. By default, follows\nsymbolic links to read the metadata of whatever the link is pointing at; set\n`followLinks = false` to disable that and instead read the metadata of the\nsymbolic link itself.\n\n[source,scala]\n----\nos.stat(wd / \"File.txt\").size ==\u003e 8\nos.stat(wd / \"Multi Line.txt\").size ==\u003e 81\nos.stat(wd / \"folder1\").fileType ==\u003e os.FileType.Dir\n----\n\n==== `os.stat.posix`\n\n[source,scala]\n----\nos.stat.posix(p: os.Path, followLinks: Boolean = true): os.PosixStatInfo\n----\n\nReads in the posix filesystem metadata for the given file, providing\ninformation on permissions and ownership. By default, follows symbolic\nlinks to read the metadata of whatever the link is pointing at; set\n`followLinks = false` to disable that and instead read the metadata of\nthe symbolic link itself.\n\n==== `os.isFile`\n\n[source,scala]\n----\nos.isFile(p: Path, followLinks: Boolean = true): Boolean\n----\n\nReturns `true` if the given path is a file. Follows symbolic links by default,\npass in `followLinks = false` to not do so.\n\n[source,scala]\n----\nos.isFile(wd / \"File.txt\") ==\u003e true\nos.isFile(wd / \"folder1\") ==\u003e false\n\nos.isFile(wd / \"misc/file-symlink\") ==\u003e true\nos.isFile(wd / \"misc/folder-symlink\") ==\u003e false\nos.isFile(wd / \"misc/file-symlink\", followLinks = false) ==\u003e false\n----\n\n==== `os.isDir`\n\n[source,scala]\n----\nos.isDir(p: Path, followLinks: Boolean = true): Boolean\n----\n\nReturns `true` if the given path is a folder. Follows symbolic links by default,\npass in `followLinks = false` to not do so.\n\n[source,scala]\n----\nos.isDir(wd / \"File.txt\") ==\u003e false\nos.isDir(wd / \"folder1\") ==\u003e true\n\nos.isDir(wd / \"misc/file-symlink\") ==\u003e false\nos.isDir(wd / \"misc/folder-symlink\") ==\u003e true\nos.isDir(wd / \"misc/folder-symlink\", followLinks = false) ==\u003e false\n----\n\n==== `os.isLink`\n\n[source,scala]\n----\nos.isLink(p: Path, followLinks: Boolean = true): Boolean\n----\n\nReturns `true` if the given path is a symbolic link. Follows symbolic links by\ndefault, pass in `followLinks = false` to not do so.\n\n[source,scala]\n----\nos.isLink(wd / \"misc/file-symlink\") ==\u003e true\nos.isLink(wd / \"misc/folder-symlink\") ==\u003e true\nos.isLink(wd / \"folder1\") ==\u003e false\n----\n\n==== `os.isReadable`\n\n[source,scala]\n----\nos.isReadable(p: Path): Boolean\n----\n\nReturns `true` if the given path is readable.\n\n[source,scala]\n----\nos.isReadable(wd / \"misc/file1\") ==\u003e false\nos.isReadable(wd / \"misc/file2\") ==\u003e true\n----\n\n==== `os.isWritable`\n\n[source,scala]\n----\nos.isWritable(p: Path): Boolean\n----\n\nReturns `true` if the given path is writable.\n\n[source,scala]\n----\nos.isWritable(wd / \"misc/file1\") ==\u003e false\nos.isWritable(wd / \"misc/file2\") ==\u003e true\n----\n\n==== `os.isExecutable`\n\n[source,scala]\n----\nos.isExecutable(p: Path): Boolean\n----\n\nReturns `true` if the given path is executable.\n\n[source,scala]\n----\nos.isExecutable(wd / \"misc/file1\") ==\u003e false\nos.isExecutable(wd / \"misc/file2.sh\") ==\u003e true\n----\n\n==== `os.size`\n\n[source,scala]\n----\nos.size(p: Path): Long\n----\n\nReturns the size of the given file, in bytes\n\n[source,scala]\n----\nos.size(wd / \"File.txt\") ==\u003e 8\nos.size(wd / \"Multi Line.txt\") ==\u003e 81\n----\n\n==== `os.mtime`\n\n[source,scala]\n----\nos.mtime(p: Path): Long\nos.mtime.set(p: Path, millis: Long): Unit\n----\n\nGets or sets the last-modified timestamp of the given file, in milliseconds\n\n[source,scala]\n----\nos.mtime.set(wd / \"File.txt\", 0)\nos.mtime(wd / \"File.txt\") ==\u003e 0\n\nos.mtime.set(wd / \"File.txt\", 90000)\nos.mtime(wd / \"File.txt\") ==\u003e 90000\nos.mtime(wd / \"misc/file-symlink\") ==\u003e 90000\n\nos.mtime.set(wd / \"misc/file-symlink\", 70000)\nos.mtime(wd / \"File.txt\") ==\u003e 70000\nos.mtime(wd / \"misc/file-symlink\") ==\u003e 70000\nassert(os.mtime(wd / \"misc/file-symlink\", followLinks = false) != 40000)\n----\n\n=== Filesystem Permissions\n\n==== `os.perms`\n\n[source,scala]\n----\nos.perms(p: Path, followLinks: Boolean = true): PermSet\nos.perms.set(p: Path, arg2: PermSet): Unit\n----\n\nGets or sets the filesystem permissions of the given file or folder, as an\n\u003c\u003cos-permset\u003e\u003e.\n\nNote that if you want to create a file or folder with a given set of\npermissions, you can pass in an \u003c\u003cos-permset\u003e\u003e to \u003c\u003cos-write\u003e\u003e\nor \u003c\u003cos-makedir\u003e\u003e. That will ensure the file or folder is created\natomically with the given permissions, rather than being created with the\ndefault set of permissions and having `os.perms.set` over-write them later\n\n[source,scala]\n----\nos.perms.set(wd / \"File.txt\", \"rwxrwxrwx\")\nos.perms(wd / \"File.txt\").toString() ==\u003e \"rwxrwxrwx\"\nos.perms(wd / \"File.txt\").toInt() ==\u003e Integer.parseInt(\"777\", 8)\n\nos.perms.set(wd / \"File.txt\", Integer.parseInt(\"755\", 8))\nos.perms(wd / \"File.txt\").toString() ==\u003e \"rwxr-xr-x\"\n\nos.perms.set(wd / \"File.txt\", \"r-xr-xr-x\")\nos.perms.set(wd / \"File.txt\", Integer.parseInt(\"555\", 8))\n----\n\n==== `os.owner`\n\n[source,scala]\n----\nos.owner(p: Path, followLinks: Boolean = true): UserPrincipal\nos.owner.set(arg1: Path, arg2: UserPrincipal): Unit\nos.owner.set(arg1: Path, arg2: String): Unit\n----\n\nGets or sets the owner of the given file or folder. Note that your process needs\nto be running as the `root` user in order to do this.\n\n[source,scala]\n----\nval originalOwner = os.owner(wd / \"File.txt\")\n\nos.owner.set(wd / \"File.txt\", \"nobody\")\nos.owner(wd / \"File.txt\").getName ==\u003e \"nobody\"\n\nos.owner.set(wd / \"File.txt\", originalOwner)\n----\n\n==== `os.group`\n\n[source,scala]\n----\nos.group(p: Path, followLinks: Boolean = true): GroupPrincipal\nos.group.set(arg1: Path, arg2: GroupPrincipal): Unit\nos.group.set(arg1: Path, arg2: String): Unit\n----\n\nGets or sets the owning group of the given file or folder. Note that your\nprocess needs to be running as the `root` user in order to do this.\n\n[source,scala]\n----\nval originalOwner = os.owner(wd / \"File.txt\")\n\nos.owner.set(wd / \"File.txt\", \"nobody\")\nos.owner(wd / \"File.txt\").getName ==\u003e \"nobody\"\n\nos.owner.set(wd / \"File.txt\", originalOwner)\n----\n\n=== Spawning Subprocesses\n\nSubprocess are spawned using `+os.call(cmd: os.Shellable, ...)+` or\n`+os.spawn(cmd: os.Shellable, ...)+` calls,\nwhere the `cmd: Shellable` sets up the basic command you wish to run and\n`+.foo(...)+` specifies how you want to run it. `os.Shellable` represents a value\nthat can make up part of your subprocess command, and the following values can\nbe used as ``os.Shellable``s:\n\n* `java.lang.String`\n* `scala.Symbol`\n* `os.Path`\n* `os.RelPath`\n* `T: Numeric`\n* ``Iterable[T]``s of any of the above\n* ``TupleN[T1, T2, ...Tn]``s of any of the above\n\nMost of the subprocess commands also let you redirect the subprocess's\n`stdin`/`stdout`/`stderr` streams via `os.ProcessInput` or `os.ProcessOutput`\nvalues: whether to inherit them from the parent process, stream them into\nbuffers, or output them to files. The following values are common to both input\nand output:\n\n* `os.Pipe`: the default, this connects the subprocess's stream to the parent\nprocess via pipes; if used on its stdin this lets the parent process write to\nthe subprocess via `os.SubProcess#stdin`, and if used on its stdout it lets the\nparent process read from the subprocess via `os.SubProcess#stdout`\nand `os.SubProcess#stderr`.\n* `os.Inherit`: inherits the stream from the parent process. This lets the\nsubprocess read directly from the parent process's standard input or write\ndirectly to the parent process's standard output or error. `os.Inherit`\ncan be redirected on a threadlocal basis via `os.Inherit.in`, `.out`, or `.err`.\n* `os.InheritRaw`: identical to `os.Inherit`, but without being affected by\nredirects.\n* `os.Path`: connects the subprocess's stream to the given filesystem\npath, reading its standard input from a file or writing its standard\noutput/error to the file.\n\nIn addition, you can pass any \u003c\u003cos-source\u003e\u003es to a Subprocess's `stdin`\n(``String``s, ``InputStream``s, ``Array[Byte]``s, ...), and pass in a\n`os.ProcessOutput` value to `stdout`/`stderr` to register callbacks that are run\nwhen output is received on those streams.\n\nOften, if you are only interested in capturing the standard output of the\nsubprocess but want any errors sent to the console, you might set `stderr =\nos.Inherit` while leaving `stdout = os.Pipe`.\n\n==== `os.call`\n\n[source,scala]\n----\nos.call(cmd: os.Shellable,\n        cwd: Path = null,\n        env: Map[String, String] = null,\n        stdin: ProcessInput = Pipe,\n        stdout: ProcessOutput = Pipe,\n        stderr: ProcessOutput = Pipe,\n        mergeErrIntoOut: Boolean = false,\n        timeout: Long = Long.MaxValue,\n        check: Boolean = true,\n        propagateEnv: Boolean = true,\n        shutdownGracePeriod: Long = 100,\n        destroyOnExit: Boolean = true): os.CommandResult\n----\n\n_Also callable via `os.proc(cmd).call(...)`_\n\nInvokes the given subprocess like a function, passing in input and returning a\n`CommandResult`. You can then call `result.exitCode` to see how it exited, or\n`result.out.bytes` or `result.err.string` to access the aggregated stdout and\nstderr of the subprocess in a number of convenient ways.\n\n`call` provides a number of parameters that let you configure how the subprocess\nis run:\n\n* `cwd`: the working directory of the subprocess\n* `env`: any additional environment variables you wish to set in the subprocess\n  in addition to those passed via `propagateEnv`. You can also set their values\n  to `null` to remove specific variables.\n* `stdin`: any data you wish to pass to the subprocess's standard input\n* `stdout`/`stderr`: these are ``os.Redirect``s that let you configure how the\nprocesses output/error streams are configured.\n* `mergeErrIntoOut`: merges the subprocess's stderr stream into it's stdout\n* `timeout`: how long to wait for the subprocess to complete\n* `check`: disable this to avoid throwing an exception if the subprocess fails\nwith a non-zero exit code\n* `propagateEnv`: disable this to avoid passing in this parent process's\nenvironment variables to the subprocess\n\nNote that redirecting `stdout`/`stderr` elsewhere means that the respective\n`CommandResult#out`/`CommandResult#err` values will be empty.\n\n[source,scala]\n----\nval res = os.call(cmd = ('ls, wd/\"folder2\"))\n\nres.exitCode ==\u003e 0\n\nres.out.text() ==\u003e\n  \"\"\"nestedA\n    |nestedB\n    |\"\"\".stripMargin\n\nres.out.trim() ==\u003e\n  \"\"\"nestedA\n    |nestedB\"\"\".stripMargin\n\nres.out.lines() ==\u003e Seq(\n  \"nestedA\",\n  \"nestedB\"\n)\n\nres.out.bytes\n\n\n// Non-zero exit codes throw an exception by default\nval thrown = intercept[os.SubprocessException]{\n  os.call(cmd = ('ls, \"doesnt-exist\"), cwd = wd)\n}\n\nassert(thrown.result.exitCode != 0)\n\n// Though you can avoid throwing by setting `check = false`\nval fail = os.call(cmd = ('ls, \"doesnt-exist\"), cwd = wd, check = false)\n\nassert(fail.exitCode != 0)\n\n\nfail.out.text() ==\u003e \"\"\n\nassert(fail.err.text().contains(\"No such file or directory\"))\n\n// You can pass in data to a subprocess' stdin\nval hash = os.call(cmd = (\"shasum\", \"-a\", \"256\"), stdin = \"Hello World\")\nhash.out.trim() ==\u003e \"a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e  -\"\n\n// Taking input from a file and directing output to another file\nos.call(cmd = (\"base64\"), stdin = wd / \"File.txt\", stdout = wd / \"File.txt.b64\")\n\nos.read(wd / \"File.txt.b64\") ==\u003e \"SSBhbSBjb3c=\"\n----\n\nIf you want to spawn an interactive subprocess, such as `vim`, `less`, or a\n`python` shell, set all of `stdin`/`stdout`/`stderr` to `os.Inherit`:\n\n[source,scala]\n----\nos.proc(\"vim\").call(stdin = os.Inherit, stdout = os.Inherit, stderr = os.Inherit)\n----\n\nNote that by customizing `stdout` and `stderr`, you can use the results\nof `os.proc.call` in a streaming fashion, either on groups of bytes:\n\n[source,scala]\n----\nvar lineCount = 1\nos.call(\n  cmd = ('find, \".\"),\n  cwd = wd,\n  stdout = os.ProcessOutput(\n    (buf, len) =\u003e lineCount += buf.slice(0, len).count(_ == '\\n')\n  ),\n)\n----\n\nOr on lines of output:\n\n[source,scala]\n----\nlineCount ==\u003e 22\nvar lineCount = 1\nos.call(\n  cmd = ('find, \".\"),\n  cwd = wd,\n  stdout = os.ProcessOutput.Readlines(\n    line =\u003e lineCount += 1\n  ),\n)\nlineCount ==\u003e 22\n----\n\n==== `os.spawn`\n\n[source,scala]\n----\nos.spawn(cmd: os.Shellable,\n         cwd: Path = null,\n         env: Map[String, String] = null,\n         stdin: os.ProcessInput = os.Pipe,\n         stdout: os.ProcessOutput = os.Pipe,\n         stderr: os.ProcessOutput = os.Pipe,\n         mergeErrIntoOut: Boolean = false,\n         propagateEnv: Boolean = true,\n         shutdownGracePeriod: Long = 100,\n         destroyOnExit: Boolean = true): os.SubProcess\n----\n\n_Also callable via `os.proc(cmd).spawn(...)`_\n\nThe most flexible of the `os.proc` calls, `os.spawn` simply configures and\nstarts a subprocess, and returns it as a `os.SubProcess`. `os.SubProcess` is a\nsimple wrapper around `java.lang.Process`, which provides `stdin`, `stdout`, and\n`stderr` streams for you to interact with however you like. e.g. You can sending\ncommands to it's `stdin` and reading from it's `stdout`.\n\nTo implement pipes, you can spawn a process, take its stdout, and pass it\nas the stdin of a second spawned process.\n\nNote that if you provide `ProcessOutput` callbacks to `stdout`/`stderr`, the\ncalls to those callbacks take place on newly spawned threads that execute in\nparallel with the main thread. Thus make sure any data processing you do in\nthose callbacks is thread safe!\n\n`stdin`, `stdout` and `stderr` are ``java.lang.OutputStream``s and\n``java.lang.InputStream``s enhanced with the `.writeLine(s: String)`/`.readLine()`\nmethods for easy reading and writing of character and line-based data.\n\n[source,scala]\n----\n// Start a long-lived python process which you can communicate with\nval sub = os.spawn(\n  cmd = (\"python\", \"-u\", \"-c\", \"while True: print(eval(raw_input()))\"),\n  cwd = wd\n)\n\n// Sending some text to the subprocess\nsub.stdin.write(\"1 + 2\")\nsub.stdin.writeLine(\"+ 4\")\nsub.stdin.flush()\nsub.stdout.readLine() ==\u003e \"7\"\n\nsub.stdin.write(\"'1' + '2'\")\nsub.stdin.writeLine(\"+ '4'\")\nsub.stdin.flush()\nsub.stdout.readLine() ==\u003e \"124\"\n\n// Sending some bytes to the subprocess\nsub.stdin.write(\"1 * 2\".getBytes)\nsub.stdin.write(\"* 4\\n\".getBytes)\nsub.stdin.flush()\nsub.stdout.read() ==\u003e '8'.toByte\n\nsub.destroy()\n\n// You can chain multiple subprocess' stdin/stdout together\nval curl = os.spawn(cmd = (\"curl\", \"-L\" , \"https://git.io/fpfTs\"), stderr = os.Inherit)\nval gzip = os.spawn(cmd = (\"gzip\", \"-n\"), stdin = curl.stdout)\nval sha = os.spawn(cmd = (\"shasum\", \"-a\", \"256\"), stdin = gzip.stdout)\nsha.stdout.trim ==\u003e \"acc142175fa520a1cb2be5b97cbbe9bea092e8bba3fe2e95afa645615908229e  -\"\n----\n\n==== Customizing the default environment\n\nClient-server CLI applications sometimes want to run subprocesses on the server based on the environment of the client.\nIt is possible to customize the default environment passed to subprocesses by setting the `os.SubProcess.env` threadlocal:\n\n[source,scala]\n----\nval clientEnvironment: Map[String, String] = ???\nos.SubProcess.env.withValue(clientEnvironment) {\n  os.call(command) // clientEnvironment is passed by default instead of the system environment\n}\n----\n\n== Spawning Pipelines of Subprocesses\n\nAfter constructing a subprocess with `os.proc`, you can use the `pipeTo` method\nto pipe its output to another subprocess:\n\n[source,scala]\n----\nval wc = os.proc(\"ls\", \"-l\")\n  .pipeTo(os.proc(\"wc\", \"-l\"))\n  .call()\n  .out.text()\n----\n\nThis is equivalent to the shell command `ls -l | wc -l`. You can chain together\nas many subprocesses as you like. Note that by using this API you can utilize\nthe broken pipe behaviour of Unix systems. For example, you can take 10 first elements\nof output from the `yes` command, and after the `head` command terminates, the `yes`\ncommand will be terminated as well:\n\n[source,scala]\n----\nval yes10 = os.proc(\"yes\")\n  .pipeTo(os.proc(\"head\", \"-n\", \"10\"))\n  .call()\n  .out.text()\n----\n\nThis feature is implemented inside the library and will terminate any process reading the\nstdin of other process in pipeline on every IO error. This behavior can be disabled via the\n`handleBrokenPipe` flag on `call` and `spawn` methods. Note that Windows does not support\nbroken pipe behaviour, so a command like`yes` would run forever. `handleBrokenPipe` is set\nto false by default on Windows.\n\nBoth `call` and `spawn` correspond in their behavior to their counterparts in the `os.proc`,\nbut `spawn` returns the `os.ProcessPipeline` instance instead. It offers the same\n`API` as `SubProcess`, but will operate on the set of processes instead of a single one.\n\n`Pipefail` is enabled by default, so if any of the processes in the pipeline fails, the whole\npipeline will have a non-zero exit code. This behavior can be disabled via the `pipefail` flag\non `call` and `spawn` methods. Note that the pipefail does not kill the processes in the pipeline,\nit just sets the exit code of the pipeline to the exit code of the failed process.\n\n=== Watching for Changes\n\n==== `os.watch.watch`\n\n[source,scala]\n----\nos.watch.watch(roots: Seq[os.Path], onEvent: Set[os.Path] =\u003e Unit): Unit\n----\n\n[source,scala,subs=\"attributes,verbatim\"]\n----\n// Mill\nivy\"com.lihaoyi::os-lib-watch:{version}\"\n// SBT\n\"com.lihaoyi\" %% \"os-lib-watch\" % \"{version}\"\n----\n\nEfficiently watches the given `roots` folders for changes. Any time the\nfilesystem is modified within those folders, the `onEvent` callback is\ncalled with the paths to the changed files or folders. Note that\n`os.watch.watch` is under a different artifact than the rest of the\n`os.*` functions, and you need to add a separate dependency to\n`os-lib-watch` in order to pull it in.\n\nOnce the call to `watch` returns, `onEvent` is guaranteed to receive a\nan event containing the path for:\n\n* Every file or folder that gets created, deleted, updated or moved\nwithin the watched folders\n* For copied or moved folders, the path of the new folder as well as\nevery file or folder within it.\n* For deleted or moved folders, the root folder which was deleted/moved,\nbut _without_ the paths of every file that was within it at the\noriginal location\n\nNote that `watch` does not provide any additional information about the\nchanges happening within the watched `roots` folder, apart from the path\nat which the change happened. It is up to the `onEvent` handler to query\nthe filesystem and figure out what happened, and what it wants to do.\n\nHere is an example of use from the Ammonite REPL:\n\n[source,scala,subs=\"attributes,verbatim\"]\n----\n@ import $ivy.`com.lihaoyi::os-lib-watch:{version}`\n\n@ os.watch.watch(Seq(os.pwd / \"out\"), paths =\u003e println(\"paths changed: \" + paths.mkString(\", \")))\n\n@ os.write(os.pwd / \"out/i am\", \"cow\")\n\npaths changed: /Users/lihaoyi/Github/Ammonite/out/i am\n\n@ os.move(os.pwd / \"out/i am\", os.pwd / \"out/hear me\")\n\npaths changed: /Users/lihaoyi/Github/Ammonite/out/i am,/Users/lihaoyi/Github/Ammonite/out/hear me\n\n@ os.remove.all(os.pwd / \"out/version\")\n\npaths changed: /Users/lihaoyi/Github/Ammonite/out/version/log,/Users/lihaoyi/Github/Ammonite/out/version/meta.json,/Users/lihaoyi/Github/Ammonite/out/version\n----\n\n== Data Types\n\n=== `os.Path`\n\nOS-Lib uses strongly-typed data-structures to represent filesystem paths. The\ntwo basic versions are:\n\n* \u003c\u003cos-path\u003e\u003e: an absolute path, starting from the root\n* \u003c\u003cos-relpath\u003e\u003e: a relative path, not rooted anywhere\n* \u003c\u003cos-subpath\u003e\u003e: a sub path, without any `..` segments, not\nrooted anywhere\n\nGenerally, almost all commands take absolute ``os.Path``s. These are\nbasically ``java.nio.file.Path``s with additional guarantees:\n\n* ``os.Path``s are always absolute. Relative paths are a separate type\n\u003c\u003cos-relpath\u003e\u003e\n* ``os.Path``s are always canonical. You will never find `.` or `..` segments in\nthem, and never need to worry about calling `.normalize` before operations.\n\nAbsolute paths can be created in a few ways:\n\n[source,scala]\n----\n// Get the process' Current Working Directory. As a convention\n// the directory that \"this\" code cares about (which may differ\n// from the pwd) is called `wd`\nval wd = os.pwd\n\n// A path nested inside `wd` in multiple segments\nwd / \"folder\" / \"file\"\n\n// The RHS of `/` can have multiple segments if-and-only-if it is a literal string\nwd / \"folder/file\"\n\n// Literal syntax for absolute `os.Path`\nval p: os.Path = \"/folder/file\"\n\n// A path starting from the root\nos.root / \"folder/file\"\n\n// A path with spaces or other special characters\nwd / \"My Folder/My File.txt\"\n\n// Up one level from the wd\nwd / os.up\n\n// Up two levels from the wd\nwd / os.up / os.up\n----\n\nWhen constructing ``os.Path``s, the right-hand-side of the `/` operator must be either a non-literal\na string expression containing a single path segment or a literal string containing one-or-more\npath segments. If a non-literal string expression on the RHS contains multiple segments, you need\nto wrap the RHS in an explicit `os.RelPath(...)` or `os.SubPath(...)` constructor to tell OS-Lib\nhow to interpret it. The single-segment limitation is intended to avoid the developer accidentally\nintroducing https://en.wikipedia.org/wiki/Directory_traversal_attack[Directory Traversal Attacks]\nor other related bugs when naively constructing paths out of dynamic and potentially untrusted\ninputs, which is not an issue for literal string since the string value is directly written in\nthe source code and immediately visible.\n\n`os.pwd` can be modified in certain scopes via the `os.dynamicPwd` dynamic variable, but\nbest practice is not to change it. Instead simply define a new path, e.g.\n\n[source,scala]\n----\nval target = os.pwd / \"target\"\n----\n\nShould be sufficient for most needs.\n\nAbove, we made use of the `os.pwd` built-in path. There are a number of Paths\nbuilt into OS-Lib:\n\n* `os.pwd`: The current working directory of the process. This can't be changed\nin Java, so if you need another path to work with the convention is to define\na `wd` variable.\n* `os.root`: The root of the filesystem.\n* `os.home`: The home directory of the current user.\n* `os.temp()`/`os.temp.dir()`: Creates a temporary file/folder and returns the\npath.\n\n==== `os.RelPath`\n\n``os.RelPath``s represent relative paths. These are basically defined as:\n\n[source,scala]\n----\nclass RelPath private[ops] (segments0: Array[String], val ups: Int)\n----\n\nThe same data structure as Paths, except that they can represent a number of ups\nbefore the relative path is applied. They can be created in the following ways:\n\n[source,scala]\n----\n// The path \"folder/file\" in multiple segments\nval rel1 = os.rel / \"folder\" / \"file\"\n// RHS of `/` can have multiple segments if-and-only-if it is a literal string\nval rel2 = os.rel / \"folder/file\"\n// Literal syntax for `os.RelPath`\nval rel3: os.RelPath = \"folder/file\"\n\n// The path \"file\"\nval rel4 = os.rel / \"file\"\n\n// The relative difference between two paths\nval target = os.pwd / \"target/file\"\nassert((target.relativeTo(os.pwd)) == os.rel / \"target/file\")\n\n// `up`s get resolved automatically\nval minus = os.pwd.relativeTo(target)\nval ups = os.up / os.up\nassert(minus == ups)\n----\n\nIn general, very few APIs take relative paths. Their main purpose is to be\ncombined with absolute paths in order to create new absolute paths. e.g.\n\n[source,scala]\n----\nval target = os.pwd / \"target/file\"\nval difference = target.relativeTo(os.pwd)\nval newBase = os.root / \"code/server\"\nassert(newBase / difference == os.root / \"code/server/target/file\")\n----\n\n`os.up` is a relative path that comes in-built:\n\n[source,scala]\n----\nval target = os.root / \"target/file\"\nassert(target / os.up == os.root / \"target\")\n----\n\nNote that all paths, both relative and absolute, are always expressed in a\ncanonical manner:\n\n[source,scala]\n----\nassert((os.root / \"folder/file\" / os.up).toString == \"/folder\")\n// not \"/folder/file/..\"\n\nassert((os.rel / \"folder/file\" / os.up).toString == \"folder\")\n// not \"folder/file/..\"\n----\n\nSo you don't need to worry about canonicalizing your paths before comparing them\nfor equality or otherwise manipulating them.\n\n==== `os.SubPath`\n\n``os.SubPath``s represent relative paths without any `..` segments. These\nare basically defined as:\n\n[source,scala]\n----\nclass SubPath private[ops] (segments0: Array[String])\n----\n\nThey can be created in the following ways:\n\n[source,scala]\n----\n// The path \"folder/file\" in multiple segments\nval sub1 = os.sub / \"folder\" / \"file\"\n// RHS of `/` can have multiple segments if-and-only-if it is a literal string\nval sub2 = os.sub / \"folder/file\"\n// Literal syntax for `os.SubPath`\nval sub2: os.Subpath = \"folder/file\"\n\n// The relative difference between two paths\nval target = os.pwd / \"out/scratch/file\"\nassert((target subRelativeTo os.pwd) == os.sub / \"out/scratch/file\")\n\n// Converting os.RelPath to os.SubPath\nval rel3 = os.rel / \"folder/file\"\nval sub4 = rel3.asSubPath\n----\n\n``os.SubPath``s are useful for representing paths within a particular\nfolder or directory. You can combine them with absolute ``os.Path``s to\nresolve paths within them, without needing to worry about\nhttps://en.wikipedia.org/wiki/Directory_traversal_attack[Directory Traversal Attacks]\ndu to accidentally accessing paths outside the destination folder.\n\n[source,scala]\n----\nval target = os.pwd / \"target/file\"\nval difference = target.relativeTo(os.pwd)\nval newBase = os.root / \"code/server\"\nassert(newBase / difference == os.root / \"code/server/target/file\")\n----\n\nAttempting to construct an `os.SubPath` with `..` segments results in an\nexception being thrown:\n\n[source,scala]\n----\nval target = os.pwd / \"out/scratch\" /\n\n// `up`s are not allowed in sub paths\nintercept[Exception](os.pwd subRelativeTo target)\n----\n\nLike ``os.Path``s and `os.RelPath`, ``os.SubPath``s are always canonicalized\nand can be compared for equality without worrying about different\nrepresentations.\n\n==== Path Operations\n\nOS-Lib's paths are transparent data-structures, and you can always access the\nsegments and ups directly. Nevertheless, OS-Lib defines a number of useful\noperations that handle the common cases of dealing with these paths:\n\nIn this definition, ThisType represents the same type as the current path; e.g.\na Path's / returns a Path while a RelPath's / returns a RelPath. Similarly, you\ncan only compare or subtract paths of the same type.\n\nApart from \u003c\u003cos-relpath\u003e\u003es themselves, a number of other data\nstructures are convertible into \u003c\u003cos-relpath\u003e\u003es when spliced into a\npath using `/`:\n\n* ``String``s\n* ``Symbol``s\n* ``Array[T]``s where `T` is convertible into a RelPath\n* ``Seq[T]``s where `T` is convertible into a RelPath\n\n==== Constructing Paths\n\nApart from built-ins like `os.pwd` or `os.root` or `os.home`, you can also\nconstruct Paths from ``String``s, ``java.io.File``s or ``java.nio.file.Path``s:\n\n[source,scala]\n----\nval relStr = \"hello/cow/world/..\"\nval absStr = \"/hello/world\"\n\nassert(\n  RelPath(relStr) == \"hello/cow\",\n  // Path(...) also allows paths starting with ~,\n  // which is expanded to become your home directory\n  Path(absStr) == os.root / \"hello/world\"\n)\n\n// You can also pass in java.io.File and java.nio.file.Path\n// objects instead of Strings when constructing paths\nval relIoFile = new java.io.File(relStr)\nval absNioFile = java.nio.file.Paths.get(absStr)\n\nassert(\n  RelPath(relIoFile) ==  \"hello/cow\",\n  Path(absNioFile) == os.root / \"hello/world\",\n  Path(relIoFile, root / \"base\") == os.root / \"base/hello/cow\"\n)\n----\n\nTrying to construct invalid paths fails with exceptions:\n\n[source,scala]\n----\nval relStr = \"hello/..\"\nintercept[java.lang.IllegalArgumentException]{\n  Path(relStr)\n}\n\nval absStr = \"/hello\"\nintercept[java.lang.IllegalArgumentException]{\n  RelPath(absStr)\n}\n\nval tooManyUpsStr = \"/hello/../..\"\nintercept[PathError.AbsolutePathOutsideRoot.type]{\n  Path(tooManyUpsStr)\n}\n----\n\nAs you can see, attempting to parse a relative path with \u003c\u003cos-path\u003e\u003e or\nan absolute path with \u003c\u003cos-relpath\u003e\u003e throws an exception. If you're\nuncertain about what kind of path you are getting, you could use `BasePath` to\nparse it :\n\n[source,scala]\n----\nval relStr = \"hello/cow/world/..\"\nval absStr = \"/hello/world\"\nassert(\n  FilePath(relStr) == \"hello/cow\",\n  FilePath(absStr) == os.root / \"hello/world\"\n)\n----\n\nThis converts it into a `BasePath`, which is either a \u003c\u003cos-path\u003e\u003e or\n\u003c\u003cos-relpath\u003e\u003e. It's then up to you to pattern-match on the types and\ndecide what you want to do in each case.\n\nYou can also pass in a second argument to `+Path(..., base)+`. If the path being\nparsed is a relative path, this base will be used to coerce it into an absolute\npath:\n\n[source,scala]\n----\nval relStr = \"hello/cow/world/..\"\nval absStr = \"/hello/world\"\nval basePath: FilePath = FilePath(relStr)\nassert(\n  os.Path(relStr,   os.root / \"base\") == os.root / \"base/hello/cow\",\n  os.Path(absStr,   os.root / \"base\") == os.root / \"hello/world\",\n  os.Path(basePath, os.root / \"base\") == os.root / \"base/hello/cow\",\n  os.Path(\".\", os.pwd).last != \"\"\n)\n----\n\nFor example, if you wanted the common behavior of converting relative paths to\nabsolute based on your current working directory, you can pass in `os.pwd` as\nthe second argument to `+Path(...)+`. Apart from passing in Strings or\njava.io.Files or java.nio.file.Paths, you can also pass in BasePaths you parsed\nearly as a convenient way of converting it to a absolute path, if it isn't\nalready one.\n\nIn general, OS-Lib is very picky about the distinction between relative and\nabsolute paths, and doesn't allow \"automatic\" conversion between them based on\ncurrent-working-directory the same way many other filesystem APIs (Bash, Java,\nPython, ...) do. Even in cases where it's uncertain, e.g. you're taking user\ninput as a String, you have to either handle both possibilities with BasePath or\nexplicitly choose to convert relative paths to absolute using some base.\n\n==== Roots and filesystems\n\nIf you are using a system that supports different roots of paths, e.g. Windows,\nyou can use the argument of `os.root` to specify which root you want to use.\nIf not specified, the default root will be used (usually, C on Windows, / on Unix).\n\n[source,scala]\n----\nval root = os.root(\"C:\\\\\") / \"Users/me\"\nassert(root == os.Path(\"C:\\\\Users\\\\me\"))\n----\n\nAdditionally, custom filesystems can be specified by passing a `FileSystem` to\n`os.root`. This allows you to use OS-Lib with non-standard filesystems, such as\njar filesystems or in-memory filesystems.\n\n[source,scala]\n----\nval uri = new URI(\"jar\", Paths.get(\"foo.jar\").toURI().toString, null);\nval env = new HashMap[String, String]();\nenv.put(\"create\", \"true\");\nval fs = FileSystems.newFileSystem(uri, env);\nval path = os.root(\"/\", fs) / \"dir\"\n----\n\nNote that the jar file system operations suchs as writing to a file are supported\nonly on JVM 11+. Depending on the filesystem, some operations may not be supported -\nfor example, running an `os.proc` with pwd in a jar file won't work. You may also\nmeet limitations imposed by the implementations - in jar file system, the files are\ncreated only after the file system is closed. Until that, the ones created in your\nprogram are kept in memory.\n\n==== `os.ResourcePath`\n\nIn addition to manipulating paths on the filesystem, you can also manipulate\n`os.ResourcePath` in order to read resources off of the Java classpath. By\ndefault, the path used to load resources is absolute, using the\n`Thread.currentThread().getContextClassLoader`.\n\n[source,scala]\n----\nval contents = os.read(os.resource / \"test/ammonite/ops/folder/file.txt\")\nassert(contents.contains(\"file contents lols\"))\n----\n\nYou can also pass in a classloader explicitly to the resource call:\n\n[source,scala]\n----\nval cl = getClass.getClassLoader\nval contents2 = os.read(os.resource(cl)/ \"test/ammonite/ops/folder/file.txt\")\nassert(contents2.contains(\"file contents lols\"))\n----\n\nIf you want to load resources relative to a particular class, pass in a class\nfor the resource to be relative, or getClass to get something relative to the\ncurrent class.\n\n[source,scala]\n----\nval cls = classOf[test.os.Testing]\nval contents = os.read(os.resource(cls) / \"folder/file.txt\")\nassert(contents.contains(\"file contents lols\"))\n\nval contents2 = os.read(os.resource(getClass) / \"folder/file.txt\")\nassert(contents2.contains(\"file contents lols\"))\n----\n\nIn both cases, reading resources is performed as if you did not pass a leading\nslash into the `getResource(\"foo/bar\")` call. In the case of\n`ClassLoader#getResource`, passing in a leading slash is never valid, and in the\ncase of `Class#getResource`, passing in a leading slash is equivalent to calling\n`getResource` on the ClassLoader.\n\nOS-Lib ensures you only use the two valid cases in the API, without a leading\nslash, and not the two cases with a leading slash which are redundant (in the\ncase of `Class#getResource`, which can be replaced by `ClassLoader#getResource`)\nor invalid (a leading slash with `ClassLoader#getResource`)\n\nNote that you can only use `os.read` from resource paths; you can't write to them or\nperform any other filesystem operations on them, since they're not really files.\n\nNote also that resources belong to classloaders, and you may have multiple\nclassloaders in your application e.g. if you are running in a servlet or REPL.\nMake sure you use the correct classloader (or a class belonging to the correct\nclassloader) to load the resources you want, or else it might not find them.\n\n=== `os.Source`\n\nMany operations in OS-Lib operate on ``os.Source``s. These represent values that\ncan provide data which you can then use to write, transmit, etc.\n\nBy default, the following types of values can be used where-ever ``os.Source``s\nare required:\n\n* Any `geny.Writable` data type:\n** `Array[Byte]`\n** `java.lang.String` (these are treated as UTF-8)\n** `java.io.InputStream`\n* `java.nio.channels.SeekableByteChannel`\n* Any `TraversableOnce[T]` of the above: e.g. `Seq[String]`,\n`List[Array[Byte]]`, etc.\n\nSome operations only work on `os.SeekableSource`, because they need the ability\nto seek to specific offsets in the data. Only the following types of values can\nbe used where `os.SeekableSource` is required:\n\n* `java.nio.channels.SeekableByteChannel`\n\n`os.Source` also supports anything that implements the\n{link-geny}#writable[Writable] interface, such as\n{link-upickle-doc}/#uJson[`ujson.Value`]s,\n{link-upickle-doc}[uPickle]'s `upickle.default.writable` values,\nor {link-scalatags-doc}[Scalatags]'s ``Tag``s\n\nYou can also convert an `os.Path` or `os.ResourcePath` to an `os.Source` via\n`.toSource`.\n\n=== `os.Generator`\n\nTaken from the {link-geny}[geny] library, ``os.Generator``s\nare similar to iterators except instead of providing:\n\n* `def hasNext(): Boolean`\n* `def next(): T`\n\n``os.Generator``s provide:\n\n* `+def generate(handleItem: A =\u003e Generator.Action): Generator.Action+`\n\nIn general, you should not notice much of a difference using ``Generator``s vs\nusing `Iterators`: you can use the same `.map`/`.filter`/`.reduce`/etc.\noperations on them, and convert them to collections via the same\n`.toList`/`.toArray`/etc. conversions. The main difference is that ``Generator``s\ncan enforce cleanup after traversal completes, so we can ensure open files are\nclosed and resources are released without any accidental leaks.\n\n=== `os.PermSet`\n\n``os.PermSet``s represent the filesystem permissions on a single file or folder.\nAnywhere an `os.PermSet` is required, you can pass in values of these types:\n\n* ``java.lang.String``s of the form `\"rw-r-xrwx\"`, with `r`/`w`/`x` representing\nthe permissions that are present or dashes `-` representing the permissions\nwhich are absent\n* Octal ``Int``s of the form `Integer.parseInt(\"777\", 8)`, matching the octal\n`755` or `666` syntax used on the command line\n* `Set[PosixFilePermission]`\n\nIn places where ``os.PermSet``s are returned to you, you can then extract the\nstring, int or set representations of the `os.PermSet` via:\n\n* `perms.toInt(): Int`\n* `perms.toString(): String`\n* `perms.value: Set[PosixFilePermission]`\n\n\n== Changelog\n\n=== 0.11.8\n\n* Silence noisy exceptions in SubProcess input stream handling and WatchServiceWatcher\n\n\n=== 0.11.6\n\n* Re-enabled Scala Native builds (tested with Scala Native 0.5.8). Zip APIs remain JVM-only.\n\n=== 0.11.5\n\n* Dropped support for Scala-Native, until https://github.com/com-lihaoyi/os-lib/issues/395[Fix and re-enable Scala-Native build (500USD Bounty)]\n  is resolved. Scala-Native users can continue to use 0.11.4, but I need help maintaining the Scala-Native integration\n  of OS-Lib if we're going to continue supporting it going forward. See the linked ticket for more info\n\n* Properly support permissions and symlinks in Zip files and other improvements\n  https://github.com/com-lihaoyi/os-lib/issues/374[#374]\n  https://github.com/com-lihaoyi/os-lib/issues/387[#387]\n  https://github.com/com-lihaoyi/os-lib/issues/388[#388]\n  https://github.com/com-lihaoyi/os-lib/issues/369[#369]\n\n* Many improvements to the `os-lib-watch` module, improving stability and reliability\n  https://github.com/com-lihaoyi/os-lib/issues/398[#398]\n  https://github.com/com-lihaoyi/os-lib/issues/393[#393]\n\n* Fix destroyOnExit default forwarding, make destroy recursive by default\n  https://github.com/com-lihaoyi/os-lib/issues/359[#359]\n\n* Added `os.Path#toURI` and `os.Path#toURL` helpers\n  https://github.com/com-lihaoyi/os-lib/issues/399[#399]\n\n=== 0.11.4\n\n* Add ability to instrument path based operations using hooks  https://github.com/com-lihaoyi/os-lib/pull/325[#325]\n* Add compile-time validation of literal paths containing \"..\" https://github.com/com-lihaoyi/os-lib/pull/329[#329]\n* Add literal string syntax for `os.Path`, `os.SubPath`, and `os.RelPath` https://github.com/com-lihaoyi/os-lib/pull/353[#353]\n\n[#0-11-3]\n=== 0.11.3\n\n* `SubProcess` spawning operations now take an `destroyOnExit = true` flag to try and shut them\n  down when the host JVM exits, `SubProcess#destroy` now takes a configurable\n  `(shutdownGracePeriod: Long, async: Boolean)` flags to configure the behavior (superseding\n  the old `destroy()`/`destroyForcibly()` methods), and `timeoutGracePeriod` has been renamed to\n  `shutdownGracePeriod` https://github.com/com-lihaoyi/os-lib/pull/324[#324]\n\n[#0-11-2]\n=== 0.11.2\n\n* Use `java.nio.files.Files.newOutputStream` instead of `java.io.FileOutputStream` to\n  try and avoid problems with windows open file deletion https://github.com/com-lihaoyi/os-lib/pull/323[#323]\n\n[#0-11-1]\n=== 0.11.1\n\n* Propagate content length from filesystem through `geny.Writable` and `os.Source`\nhttps://github.com/com-lihaoyi/os-lib/pull/320[#320]\n\n[#0-11-0]\n=== 0.11.0\n\n* Added APIs to \u003c\u003cZip \u0026 Unzip Files\u003e\u003e via `os.zip`, `os.unzip`, `os.zip.stream`, `os.unzip.stream`,\n`os.unzip.list`, `os.unzip.streamRaw`, `os.zip.open` https://github.com/com-lihaoyi/os-lib/pull/317[#317]\n\n* Minimum officially supported Java version raised from 8 to 11\n\n[#0-10-7]\n=== 0.10.7\n\n* Allow multi-segment paths segments for literals https://github.com/com-lihaoyi/os-lib/pull/297: You\ncan now write `os.pwd / \"foo/bar/qux\"` rather than `os.pwd / \"foo\" / \"bar\" / \"qux\"`. Note that this\nis only allowed for string literals, and non-literal path segments still need to be wrapped e.g.\n`def myString = \"foo/bar/qux\"; os.pwd / os.SubPath(myString)` for security and safety purposes\n\n[#0-10-6]\n=== 0.10.6\n\n* Make `os.pwd` modifiable via the `os.dynamicPwd` dynamic variable https://github.com/com-lihaoyi/os-lib/pull/298\n\n[#0-10-5]\n=== 0.10.5\n\n* Introduce `os.SubProcess.env` `DynamicVariable` to override default `env`\n(https://github.com/com-lihaoyi/os-lib/pull/295)\n\n\n[#0-10-4]\n=== 0.10.4\n\n* Add a lightweight syntax for `os.call()` and `os.spawn` APIs\n(https://github.com/com-lihaoyi/os-lib/pull/292)\n* Add a configurable grace period when subprocesses timeout and have to\nbe terminated to give a chance for shutdown logic to run\n(https://github.com/com-lihaoyi/os-lib/pull/286)\n\n[#0-10-3]\n=== 0.10.3\n\n* `os.Inherit` now can be redirected on a threadlocal basis via `os.Inherit.in`, `.out`, or `.err`.\n`os.InheritRaw` is available if you do not want the redirects to take effect\n\n\n[#0-10-2]\n=== 0.10.2\n\n* Support `os.proc` on Scala Native (https://github.com/com-lihaoyi/os-lib/pull/257)\n\n[#0-10-1]\n=== 0.10.1\n\n* Fix `os.copy` and `os.move` directories to root (#267)\n\n[#0-10-0]\n=== 0.10.0\n\n* Support for Scala-Native 0.5.0\n* Dropped support for Scala 2.11.x\n* Minimum version of Scala 3 increased to 3.3.1\n\n\n[#0-9-3]\n=== 0.9.3 - 2024-01-01\n\n* Fix `os.watch` on Windows (#236)\n* Fix propagateEnv = false to not propagate env (#238)\n* Make os.home a def (#239)\n\n[#0-9-2]\n=== 0.9.2 - 2023-11-05\n\n* Added new convenience API to create pipes between processes with `.pipeTo`\n* Fixed issue with leading `..` / `os.up` in path segments created from a `Seq`\n* Fixed Windows-specific issues with relative paths with leading (back)slashes\n* Removed some internal use of deprecated API\n* ScalaDoc now maps some external references to their online sites\n* Dependency updates: sourcecode 0.3.1\n* 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\n\n[#0-9-1]\n=== 0.9.1 - 2023-03-07\n\n* Refined return types when constructing paths with `/` and get rid of long `ThisType#ThisType` cascades.\n* Added a new `PathConvertible` to support `URI`s when constructing paths.\n\n[#0-9-0]\n=== 0.9.0 - 2022-11-28\n\n* `os.proc` now also supports `CharSequence(s)` as `Shellable`\n* `ProcessResult` now also contains the actual used command\n* Fixed handling of `atime` and `ctime` in `StatInfo`\n* Deleted `ConcurrentLinkedQueue` from Scala Native jars, as it is now provided by Scala Native 0.4 itself\n* Enabled MiMa checks to CI setup and officially support early semantic versioning since this release\n* Documentation improvements\n\n\n=== Older releases\n:leveloffset: +1\n\n[discrete]\n=== 0.8.1 - 2022-01-31\n\n* Added support for Scala Native on Scala 3\n\n[discrete]\n=== 0.8.0 - 2021-12-11\n\n* Avoid throwing an exception when sorting identical paths {link-oslib}/pull/90[#90]\n* Make `os.remove` behave more like `Files.deleteIfExists` {link-oslib}/pull/89[#89]\n* Make `.ext` on empty paths return `\"\"` rather than crashing {link-oslib}/pull/87[#87]\n\n[discrete]\n=== 0.7.8 - 2021-05-27\n\n* Restored binary compatibility in `os.copy` and `os.copy.into` to os-lib versions before 0.7.5\n\n[discrete]\n=== 0.7.7 - 2021-05-14\n\n* Add support for Scala 3.0.0\n\n[discrete]\n=== 0.7.6 - 2021-04-28\n\n* Add support for Scala 3.0.0-RC3\n\n[discrete]\n=== 0.7.5 - 2021-04-21\n\n* Re-added support for Scala 2.11\n* Added new option `mergeFolders` to `os.copy`\n* os.copy now honors `followLinks` when copying symbolic links to directories\n\n[discrete]\n=== 0.7.4\n\n* Add support for Scala 3.0.0-RC2\n\n[discrete]\n=== 0.7.3\n\n* Add support for Scala 3.0.0-RC1\n* Migration of the CI system from Travis CI to GitHub Actions\n\n[discrete]\n=== 0.7.2\n\n* Add support for Scala 3.0.0-M3\n\n[discrete]\n=== 0.7.1\n\n* Improve performance of `os.write` by buffering output stream to files\n\n[discrete]\n=== 0.6.2\n\n* Moved the `os.Bytes`, `os.StreamValue` (now named `ByteData`) interfaces into\n`geny` package, for sharing with Requests-Scala\n* Add `os.read.stream` function, that returns a `geny.Readable`\n\n[discrete]\n=== 0.5.0\n\n* `os.Source` now supports any data type that is `geny.Writable`\n\n[discrete]\n=== 0.4.2\n\n* Added a new \u003c\u003cos-subpath\u003e\u003e data type, for safer handling of\nsub-paths within a directory.\n* Removed `os.proc.stream`, since you can now customize the `stdout` or\n`stderr` of `os.proc.call` to handle output in a streaming fashion\n* `stderr` in `os.proc.call` and `os.proc.spawn` defaults to\n`os.Inherit` rather than `os.Pipe`; pass in `stderr = os.Pipe`\nexplicitly to get back the old behavior\n* Fix timeout not working with `os.proc.call`\n{link-oslib}/issues/27[#27]\n* Attempt to fix crasher accessing `os.pwd`\n{link-oslib}/issues/24[#24]\n* Added an \u003c\u003cos-watch-watch,os-lib-watch\u003e\u003e package, which can be used to\nefficiently recursively watch folders for updates\n{link-oslib}/issues/23[#23]\n* `os.stat` no longer provides POSIX owner/permissions related metadata\nby default {link-oslib}/issues/15[#15], use\n`os.stat.posix` to fetch that separately\n* `os.stat.full` has been superseded by `os.stat` and `os.stat.posix`\n* Removed `os.BasicStatInfo`, which has been superseded by `os.StatInfo`\n\n[discrete]\n=== 0.3.0\n\n* Support for Scala 2.13.0 final\n\n[discrete]\n=== 0.2.8\n\n* `os.ProcessOutput` trait is no longer sealed\n\n[discrete]\n=== 0.2.7\n\n* Narrow return type of `readLink.absolute` from `FilePath` to `Path`\n* Fix handling of standaline `\\r` in `os.SubProcess#stdout.readLine`\n\n[discrete]\n=== 0.2.6\n\n* Remove `os.StatInfo#name`, `os.BasicStatInfo#name` and `os.FullStatInfo#name`,\nsince it is just the last path segment of the stat call and doesn't properly\nreflect the actual name of the file on disk (e.g. on case-insensitive filesystems)\n* `os.walk.attrs` and `os.walk.stream.attrs` now provides a `os.BasicFileInfo`\nto the `skip` predicate.\n* Add `os.BasePath#baseName`, which returns the section of the path before the\n`os.BasePath#ext` extension.\n\n[discrete]\n=== 0.2.5\n\n* New `os.readLink`/`os.readLink.absolute` methods to read the contents of\nsymbolic links without dereferencing them.\n* New `os.read.chunked(p: Path, chunkSize: Int): os.Generator[(Array[Byte],\nInt)]` method for conveniently iterating over chunks of a file\n* New `os.truncate(p: Path, size: Int)` method\n* `SubProcess` streams now implement `java.io.DataInput`/`DataOutput` for convenience\n* `SubProcess` streams are now synchronized for thread-safety\n* `os.write` now has `createFolders` default to `false`\n* `os.Generator` now has a `.withFilter` method\n* `os.symlink` now allows relative paths\n* `os.remove.all` now properly removes broken symlinks, and no longer recurses\ninto the symlink's contents\n* `os.SubProcess` now implements `java.lang.AutoCloseable`\n* New `write.channel` counterpart to `read.channel` (and `write.over.channel`\nand `write.append.channel`)\n* `os.PermSet` is now modelled internally as a boxed `Int` for performance, and\nis a case class with proper `equals`/`hashcode`\n* `os.read.bytes(arg: Path, offset: Long, count: Int)` no longer leaks open file\nchannels\n* Reversed the order of arguments in `os.symlink` and `os.hardlink`, to match\nthe order of the underlying java NIO functions.\n\n[discrete]\n=== 0.2.2\n\n* Allow chaining of multiple subprocesses `stdin`/`stdout`\n\n[discrete]\n=== 0.2.0\n\n* First release\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcom-lihaoyi%2Fos-lib","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcom-lihaoyi%2Fos-lib","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcom-lihaoyi%2Fos-lib/lists"}