# fs

> Read, write and manage files and directories, and take paths apart and put them together.

This is the Markdown version of https://osl-lang.org/packages/fs. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:fs"
```

`std:fs` reads and writes files, lists and walks directories, copies and moves things, and builds paths. Most functions return a [result](/packages/result.md), because files go missing and disks fill up. Pure path functions such as `base` and `ext` only work on strings and never touch the disk.

```osl
import "std:fs"
```

## Write and read text

`fs.writeFile(path, text)` replaces a file's contents, creating the file if needed. `fs.readFile(path)` returns the whole file as a string. Both return results.

```osl
import "std:fs"

fs.writeFile("hello.txt", "Hello, file!\nSecond line\n").unwrap()
string text = fs.readFile("hello.txt").unwrap()
log text.len
log text.trim()
```

Output:

```text
25
Hello, file!
Second line
```

Writing is atomic: another reader sees either the old contents or the new, never half a file, even if the program crashes mid-write. `appendFile` adds to the end instead of replacing:

```osl
import "std:fs"

fs.remove("log.txt", {force: true}).unwrap()
fs.appendFile("log.txt", "start\n").unwrap()
fs.appendFile("log.txt", "next\n").unwrap()
log fs.readFile("log.txt").unwrap().trim()
```

Output:

```text
start
next
```

## Files that are not there

A missing file is not a crash, it is an error value with a `kind`. Check for it and decide what to do.

```osl
import "std:fs"

switch fs.readFile("no-such-file.txt") (
  case ok(text)
    log "read" text.len
  case err(e)
    log e.kind
)

string config = fs.readFile("settings.txt").unwrapOr("default")
log config
```

Output:

```text
NotFoundError
default
```

Kinds you will meet are `NotFoundError`, `PermissionError` and `IOError` (everything else, such as reading a directory as a file). `fs.exists(path)` is true when something is there.

```osl
import "std:fs"

fs.mkdir("somedir", {recursive: true}).unwrap()
log fs.exists("somedir") fs.isDir("somedir") fs.isFile("somedir")
log fs.readFile("somedir").unwrapErr().kind
log fs.writeFile("no/such/dir/file.txt", "x").unwrapErr().kind
```

Output:

```text
true true false
IOError
NotFoundError
```

Writing needs the parent directory to exist. Create it first with `mkdir`.

Try it: write `settings.txt` first and watch the fallback disappear.

## Paths

Paths are strings, and `fs` has functions to build them and take them apart, so you never join with `"/"` by hand. These work on the text only, and the file doesn't need to exist.

```osl
import "std:fs"

string p = fs.joinPath("projects", "osl", "main.osl")
log p
log fs.base(p) fs.dir(p)
log fs.ext(p) fs.stem(p)
log fs.splitPath(p) fs.splitExt(p)
log fs.segments("/usr/local/bin")
```

Output:

```text
projects/osl/main.osl
main.osl projects/osl
.osl main
["projects/osl/","main.osl"] ["projects/osl/main",".osl"]
["usr","local","bin"]
```

`joinPath` accepts any number of parts and cleans the result. `cleanPath` resolves `.`, `..` and doubled separators. Changing parts of a path is common enough to have helpers:

```osl
import "std:fs"

log fs.withExt("notes/todo.txt", "md")
log fs.withExt("notes/todo.txt", "")
log fs.withName("notes/todo.txt", "done.txt")
log fs.cleanPath("a//b/./c/../d")
log fs.joinPath("a", "../b", "c.txt")
```

Output:

```text
notes/todo.md
notes/todo
notes/done.txt
a/b/d
b/c.txt
```

Relative and absolute paths:

```osl
import "std:fs"

log fs.isAbs("/etc/hosts") fs.isAbs("etc/hosts")
log fs.relPath("/a/b", "/a/b/c/d.txt")
log fs.relPath("/a/b", "/a/x")
log fs.pathStartsWith("/a/b/c", "/a/b") fs.pathStartsWith("/a/bc", "/a/b")
log fs.toPosix("a/b")
log fs.absPath("x.txt").unwrap().endsWith("/x.txt")
```

Output:

```text
true false
c/d.txt
../x
true false
a/b
true
```

`pathStartsWith` compares whole elements, so `/a/bc` is not inside `/a/b`. That is the check to use before acting on a path someone gave you, instead of comparing strings with `startsWith`.

Try it: call `fs.ext` and `fs.stem` on a name like `archive.tar.gz`.

## Reading line by line

Reading a whole file is fine for small ones. For logs and data files, loop over the lines. `fs.lines` returns an iterator inside a result, reads as it goes and closes the file when the loop ends.

```osl
import "std:fs"

fs.writeFile("names.txt", "ada\ngrace\n\nalan\n").unwrap()

for line in fs.lines("names.txt").unwrap() (
  if line == "" continue
  log line.toUpper()
)
```

Output:

```text
ADA
GRACE
ALAN
```

`head` and `tail` give the first or last lines of a file as an array, and `tail` reads backwards from the end, so it is cheap on a huge log. `grep` returns the lines that match a regular expression.

```osl
import "std:fs"

string[] rows = []
for i in 1 to 20 ( rows.append("row " ++ i) )
fs.writeFile("rows.txt", rows.join("\n") ++ "\n").unwrap()

log fs.head("rows.txt", 3).unwrap()
log fs.tail("rows.txt", 2).unwrap()
log fs.head("rows.txt").unwrap().len
log fs.grep("rows.txt", "^row 1.$").unwrap()
log fs.grep("rows.txt", "(").unwrapErr().kind
```

Output:

```text
["row 1","row 2","row 3"]
["row 19","row 20"]
10
["row 10","row 11","row 12","row 13","row 14","row 15","row 16","row 17","row 18","row 19"]
ParseError
```

## Bytes

Text is not always text. Images, archives and binary formats are bytes. `readFileBytes` returns a `byte[]`, and `writeFileBytes` accepts a `byte[]`, a string (written as UTF-8) or an array of numbers from 0 to 255.

```osl
import "std:fs"

fs.writeFileBytes("data.bin", [0, 1, 2, 255]).unwrap()
byte[] data = fs.readFileBytes("data.bin").unwrap()
log data.len data[1] data[4]

fs.appendFileBytes("data.bin", [16, 32]).unwrap()
log fs.size("data.bin").unwrap()
```

Output:

```text
4 0 255
6
```

## Information about files

`size`, `modTime` and `stat` tell you about a file. `stat` returns a record with `name`, `path`, `ext`, `size` (bytes), `modTime` (unix milliseconds), `mode` (such as `-rw-r--r--`), `isDir` and `isSymlink`.

```osl
import "std:fs"

fs.writeFile("report.csv", "a,b\n1,2\n").unwrap()
auto info = fs.stat("report.csv").unwrap()
log info.name info.ext info.size info.isDir
log info.modTime > 0
log fs.size("report.csv").unwrap()
log fs.stat("nothing.here").unwrapErr().kind
```

Output:

```text
report.csv .csv 8 false
true
8
NotFoundError
```

## Directories

`mkdir` makes a directory. By default it fails if a parent is missing; with `{recursive: true}` it creates the whole chain and doesn't mind if it is already there. `readDir` lists names, and `entries` lists records (like `stat`) sorted by name.

```osl
import "std:fs"

fs.remove("proj", {force: true}).unwrap()
fs.mkdir("proj/src/util", {recursive: true}).unwrap()
fs.writeFile("proj/README.md", "# proj").unwrap()
fs.writeFile("proj/src/main.osl", "log 1").unwrap()

log fs.readDir("proj").unwrap()
for e in fs.entries("proj").unwrap() (
  log e.name e.isDir
)
log fs.mkdir("proj").unwrapErr().kind
```

Output:

```text
["README.md","src"]
README.md false
src true
IOError
```

## Walking a tree

`fs.walkTree(path)` visits everything below a directory, depth first and in name order, and the directory itself is not included. Each item is the same record that `entries` gives, so you can filter on `ext` or `isDir` as you go.

```osl
import "std:fs"

fs.remove("site", {force: true}).unwrap()
fs.mkdir("site/css", {recursive: true}).unwrap()
fs.mkdir("site/pages/deep", {recursive: true}).unwrap()
fs.writeFile("site/index.html", "<h1>hi</h1>").unwrap()
fs.writeFile("site/css/main.css", "body{}").unwrap()
fs.writeFile("site/pages/about.html", "about").unwrap()
fs.writeFile("site/pages/deep/team.html", "team").unwrap()

int bytes = 0
for e in fs.walkTree("site") (
  if e.isDir continue
  bytes += e.size
  if e.ext == ".html" (
    log fs.relPath("site", e.path)
  )
)
log "bytes:" bytes
```

Output:

```text
index.html
pages/about.html
pages/deep/team.html
bytes: 26
```

## Glob

`fs.glob` matches a shell-style pattern: `*` and `?` match within one path element, and `[a-z]` a range. It returns paths in name order, and no match is an empty array, not an error. `**` is not special, so use `walkTree` to search all depths.

```osl
import "std:fs"

fs.remove("site", {force: true}).unwrap()
fs.mkdir("site/pages", {recursive: true}).unwrap()
fs.writeFile("site/index.html", "").unwrap()
fs.writeFile("site/logo.svg", "").unwrap()
fs.writeFile("site/pages/about.html", "").unwrap()
fs.writeFile("site/pages/blog.html", "").unwrap()

log fs.glob("site/*.html").unwrap()
log fs.glob("site/*/*.html").unwrap()
log fs.glob("site/pages/[a-b]*").unwrap()
log fs.glob("site/*.png").unwrap()
log fs.glob("[").unwrapErr().kind
```

Output:

```text
["site/index.html"]
["site/pages/about.html","site/pages/blog.html"]
["site/pages/about.html","site/pages/blog.html"]
[]
ParseError
```

## Copy, rename, remove

`copy` copies a file, or a whole directory. It refuses to overwrite an existing destination file unless you pass `{overwrite: true}`. `rename` moves or renames, and replaces an existing file at the destination. `remove` deletes a file or a directory and everything in it.

```osl
import "std:fs"

fs.remove("work", {force: true}).unwrap()
fs.mkdir("work").unwrap()
fs.writeFile("work/a.txt", "A").unwrap()

fs.copy("work/a.txt", "work/b.txt").unwrap()
log fs.copy("work/a.txt", "work/b.txt").unwrapErr().kind
fs.copy("work/a.txt", "work/b.txt", {overwrite: true}).unwrap()
fs.rename("work/b.txt", "work/c.txt").unwrap()
log fs.readDir("work").unwrap()

fs.remove("backup", {force: true}).unwrap()
fs.copy("work", "backup").unwrap()
log fs.readDir("backup").unwrap()
fs.remove("work").unwrap()
log fs.exists("work")
log fs.remove("work").unwrapErr().kind
fs.remove("work", {force: true}).unwrap()
```

Output:

```text
IOError
["a.txt","c.txt"]
["a.txt","c.txt"]
false
NotFoundError
```

`remove` on something that isn't there is an error, which is useful when you want to know. `{force: true}` makes it quietly succeed, which is what you want for cleanup. This is why the examples on this page start by removing what they create.

## File handles

For more control, `fs.open` returns a handle. Open it with mode `"r"` (read, the default), `"w"` (write from the start, creating or emptying the file) or `"a"` (append), and always close it. Writes are buffered until you flush or close.

```osl
import "std:fs"

auto out = fs.open("handle.txt", {mode: "w"}).unwrap()
out.write("first\n").unwrap()
out.write("second\n").unwrap()
out.write([116, 104, 105, 114, 100, 10]).unwrap()
out.close().unwrap()

auto f = fs.open("handle.txt").unwrap()
log f.readLine().unwrap()
log f.eof()
log f.read(3).unwrap()
log f.seek(0).unwrap()
log f.head(2).unwrap()
log f.tail(1).unwrap()
f.close().unwrap()
```

Output:

```text
first
false
sec
0
["first","second"]
["third"]
```

`readLine` returns the next line, and reports an error at the end of the file, so loop with `for line in f.lines()` or check `eof()`. `seek(offset, from)` moves the cursor and takes `"start"`, `"current"` or `"end"`. Offsets are distances in bytes, so `0` is the start of the file.

`fs.openText(text)` gives you a handle over a string, so code that reads from a file handle can be tested without a file:

```osl
import "std:fs"

def countWords(*fs.File source) int (
  int n = 0
  for line in source.lines() ( n += line.split(" ").len )
  return n
)

log countWords(fs.openText("one two\nthree four five"))
```

Output:

```text
5
```

## Update with a backup

Because `writeFile` is atomic, a program can update a settings file and keep the old one as a backup:

```osl
import "std:fs"
import "std:json"

fs.remove("settings.json", {force: true}).unwrap()
fs.remove("settings.json.bak", {force: true}).unwrap()
fs.writeFile("settings.json", json.stringify({volume: 5})).unwrap()

def update(string path, int volume) result<unit, error> (
  fs.copy(path, path ++ ".bak", {overwrite: true})!
  return fs.writeFile(path, json.stringify({volume: volume}))
)

update("settings.json", 9).unwrap()
log fs.readFile("settings.json").unwrap()
log fs.readFile("settings.json.bak").unwrap()
fs.remove("settings.json").unwrap()
fs.remove("settings.json.bak").unwrap()
```

Output:

```text
{"volume":9}
{"volume":5}
```

### Exercise: Count lines of code

Write a function `countLines(dir)` that returns how many lines there are in all `.osl` files below `dir`, skipping directories and other files. The starter creates a small project.

Starting code:

```osl
import "std:fs"

fs.remove("code", {force: true}).unwrap()
fs.mkdir("code/lib", {recursive: true}).unwrap()
fs.writeFile("code/main.osl", "a\nb\nc\n").unwrap()
fs.writeFile("code/lib/util.osl", "x\ny\n").unwrap()
fs.writeFile("code/notes.txt", "not code\n").unwrap()

log fs.readDir("code").unwrap().len
```

It should print:

```text
5
```

Hint 1: Loop over `fs.walkTree(dir)`, skip entries where `isDir` is true or `ext` is not `.osl`.

Hint 2: `fs.lines(path).unwrap()` is an iterator, so a nested `for` loop can count its lines.

A solution:

```osl
import "std:fs"

fs.remove("code", {force: true}).unwrap()
fs.mkdir("code/lib", {recursive: true}).unwrap()
fs.writeFile("code/main.osl", "a\nb\nc\n").unwrap()
fs.writeFile("code/lib/util.osl", "x\ny\n").unwrap()
fs.writeFile("code/notes.txt", "not code\n").unwrap()

def countLines(string dir) int (
  int total = 0
  for e in fs.walkTree(dir) (
    if e.isDir or e.ext != ".osl" continue
    for _ in fs.lines(e.path).unwrap() ( total++ )
  )
  return total
)

log countLines("code")
```

**Quiz.** What does `fs.remove("gone.txt")` do when the file doesn't exist?

- [ ] Nothing, quietly.
- [x] Returns a `NotFoundError`.
- [ ] Creates the file.

Answer: Pass `{force: true}` to make it quietly succeed, which is what cleanup wants.

### Exercise: Rename .txt to .md

Rename every `.txt` file in `notes` to `.md`, then print the folder's names.

Starting code:

```osl
import "std:fs"

fs.remove("notes", {force: true}).unwrap()
fs.mkdir("notes").unwrap()
fs.writeFile("notes/a.txt", "a").unwrap()
fs.writeFile("notes/b.txt", "b").unwrap()
fs.writeFile("notes/keep.csv", "c").unwrap()
log fs.readDir("notes").unwrap()
```

It should print:

```text
["a.md","b.md","keep.csv"]
```

Hint 1: `fs.withExt(path, "md")` builds the new name.

Hint 2: `fs.joinPath("notes", name)` builds the path for each file.

A solution:

```osl
import "std:fs"

fs.remove("notes", {force: true}).unwrap()
fs.mkdir("notes").unwrap()
fs.writeFile("notes/a.txt", "a").unwrap()
fs.writeFile("notes/b.txt", "b").unwrap()
fs.writeFile("notes/keep.csv", "c").unwrap()

for name in fs.readDir("notes").unwrap() (
  string path = fs.joinPath("notes", name)
  if fs.ext(path) == ".txt" (
    fs.rename(path, fs.withExt(path, "md")).unwrap()
  )
)
log fs.readDir("notes").unwrap()
```

## API reference

## API reference

Every member of `std:fs`, generated from the source of OSL.

### Functions

#### `fs.absPath(string path) result<string, error>`

absPath returns path as an absolute, cleaned path, resolved against the
working directory. Fails with IOError when the working directory can't
be read.

#### `fs.append(any path) *fs.File`

**Deprecated.** Deprecated: use fs.open(path, {mode: "a"}) instead.

append opens a file for appending, or returns null.

#### `fs.appendFile(string path, string text) result<unit, error>`

appendFile adds text to the end of a file, creating it if needed. Fails
with NotFoundError when the parent directory doesn't exist,
PermissionError, or IOError.

#### `fs.appendFileBytes(string path, any data) result<unit, error>`

appendFileBytes adds bytes to the end of a file, creating it if needed.
data is read like fs.writeFileBytes data. Fails like fs.appendFile.

#### `fs.appendToFile(any path, any data) boolean`

**Deprecated.** Deprecated: use fs.appendFile(path, text) instead.

appendToFile appends text and reports whether it worked.

#### `fs.base(string path) string`

base returns the last element of a path: fs.base("a/b/c.txt") is
"c.txt". It only reads the string.

#### `fs.chdir(any path) boolean`

**Deprecated.** Deprecated: use process.chdir(path) instead.

chdir changes the working directory and reports whether it worked.

#### `fs.cleanPath(string path) string`

cleanPath returns the shortest equivalent path, resolving "." and ".."
and repeated separators: fs.cleanPath("/a//b/./c") is "/a/b/c".

#### `fs.copy(string from, string to, object? options) result<unit, error>`

copy copies a file, or a directory and everything in it, to a new path,
keeping permissions. Options: overwrite (boolean, default false) replaces
files that already exist at the destination; without it, an existing
destination file fails with IOError. Fails with NotFoundError when from
doesn't exist. Throws ValueError when copying a directory into itself.

#### `fs.copyDir(any srcPath, any dstPath) boolean`

**Deprecated.** Deprecated: use fs.copy(from, to) instead, which copies files and directories.

copyDir copies a directory tree into dstPath and reports whether it
worked.

#### `fs.create(any path) *fs.File`

**Deprecated.** Deprecated: use fs.open(path, {mode: "w"}) instead.

create opens a file for writing, truncating it, or returns null.

#### `fs.dir(string path) string`

dir returns everything but the last element of a path:
fs.dir("a/b/c.txt") is "a/b". It only reads the string.

#### `fs.eachLine(any path, any fn)`

**Deprecated.** Deprecated: use `for line in fs.lines(path)` instead.

eachLine calls fn with every line of a file; a missing file calls it
never.

#### `fs.entries(string path) result<{ext: string, isDir: boolean, isSymlink: boolean, modTime: int, mode: string, name: string, path: string, size: int}[], error>`

entries lists a directory's entries, sorted by name. Each entry has the
keys name, path (path joined with name), ext, isDir, isSymlink, size
(bytes), modTime (unix milliseconds) and mode (such as "-rw-r--r--").
Fails like fs.readDir.

#### `fs.evalSymlinks(string path) result<string, error>`

evalSymlinks resolves every symlink in a path. Fails with NotFoundError
when the path or a link target doesn't exist.

#### `fs.exists(string path) boolean`

exists reports whether something exists at path. A broken symlink
counts as missing.

#### `fs.ext(string path) string`

ext returns the extension of a path's last element, with its dot:
fs.ext("a/b/c.txt") is ".txt", and "" when there is none.

#### `fs.getBase(any path) string`

**Deprecated.** Deprecated: use fs.base(path) instead.

getBase returns the last element of a path.

#### `fs.getDir(any path) string`

**Deprecated.** Deprecated: use fs.dir(path) instead.

getDir returns a path's directory.

#### `fs.getExt(any path) string`

**Deprecated.** Deprecated: use fs.ext(path) instead.

getExt returns a path's extension.

#### `fs.getModTime(any path) number`

**Deprecated.** Deprecated: use fs.modTime(path) instead.

getModTime returns a file's modification time in unix milliseconds, or 0
when it can't be read.

#### `fs.getParts(any path) string[]`

**Deprecated.** Deprecated: use fs.base(path), fs.dir(path) and fs.ext(path) instead.

getParts returns [base, dir, ext] of a path.

#### `fs.getSize(any path) number`

**Deprecated.** Deprecated: use fs.size(path) instead.

getSize returns a file's size in bytes, or 0 when it can't be read.

#### `fs.getStat(any path) object`

**Deprecated.** Deprecated: use fs.stat(path) instead.

getStat describes a file, with success false when it can't be read.

#### `fs.getStem(any path) string`

**Deprecated.** Deprecated: use fs.stem(path) instead.

getStem returns a path's last element without its extension.

#### `fs.getwd() string`

**Deprecated.** Deprecated: use process.cwd() instead.

getwd returns the working directory, or "" when it can't be read.

#### `fs.glob(string pattern) result<string[], error>`

glob lists the paths matching a shell pattern such as "src/*.osl", in
lexical order. * and ? match within one path element and [a-z] matches a
range; ** is not special. Fails with ParseError for a malformed pattern.

#### `fs.grep(string path, string pattern) result<string[], error>`

grep returns the lines of a file that match a regular expression. Fails
with NotFoundError when the file doesn't exist and ParseError for an
invalid pattern.

#### `fs.head(string path, any ...count) result<string[], error>`

head returns up to the first count lines (default 10) of a file. Fails with
NotFoundError when the file doesn't exist; throws ValueError for a
negative count.

#### `fs.isAbs(string path) boolean`

isAbs reports whether a path is absolute.

#### `fs.isDir(string path) boolean`

isDir reports whether path is a directory, following symlinks.

#### `fs.isFile(string path) boolean`

isFile reports whether path is a regular file, following symlinks.

#### `fs.joinPath(any ...path) string`

joinPath joins path elements with the OS separator and cleans the
result: fs.joinPath("a", "b", "c.txt") is "a/b/c.txt".

#### `fs.lines(string path) result<iter<string>, error>`

lines iterates over a file's lines, without line endings, reading as it
goes; the file closes when the loop finishes. Fails with NotFoundError
when the file doesn't exist; a read error throws IOError from the loop.

#### `fs.mkdir(string path, object? options) result<unit, error>`

mkdir creates a directory. Options: recursive (boolean, default false)
also creates missing parents and succeeds when the directory already
exists. Fails with NotFoundError when a parent is missing (without
recursive), IOError when path already exists, or PermissionError.

#### `fs.mkdirAll(any path) boolean`

**Deprecated.** Deprecated: use fs.mkdir(path, {recursive: true}) instead.

mkdirAll creates a directory and any missing parents and reports whether
it worked.

#### `fs.modTime(string path) result<int, error>`

modTime returns when a file was last modified, in unix milliseconds,
following symlinks. Fails with NotFoundError when the file doesn't exist.

#### `fs.open(string path, object? options) result<*fs.File, error>`

open opens a file and returns a handle; close it when done. Options:
mode is "r" (default) to read, "w" to write from the start, creating or
truncating the file, or "a" to append, creating it if needed. Fails with
NotFoundError when a file opened for reading (or the parent directory)
doesn't exist, PermissionError, or IOError. Throws ValueError for an
unknown mode.

#### `fs.openText(string text) *fs.File`

openText returns a readable handle over text, for reading it the way a
file is read.

#### `fs.pathStartsWith(string path, string prefix) boolean`

pathStartsWith reports whether path is prefix or lies below it, comparing
whole elements: "/a/bc" does not start with "/a/b".

#### `fs.readDir(string path) result<string[], error>`

readDir lists the names of the entries in a directory, sorted by name.
Fails with NotFoundError when the directory doesn't exist,
PermissionError, or IOError when path isn't a directory.

#### `fs.readDirAll(any path) {ext: string, isDir: boolean, name: string, path: string, type: any}[]`

**Deprecated.** Deprecated: use fs.entries(path) instead.

readDirAll lists a directory's entries, or [] when it can't be read.

#### `fs.readFile(string path) result<string, error>`

readFile reads a whole file as text. Fails with NotFoundError when the
file doesn't exist, PermissionError when it can't be read, and IOError
for anything else, such as path being a directory.

#### `fs.readFileBytes(string path) result<byte[], error>`

readFileBytes reads a whole file as bytes. Fails like fs.readFile.

#### `fs.relPath(string base, string target) string`

relPath returns target relative to base, with forward slashes:
fs.relPath("/a/b", "/a/b/c") is "c". Throws ValueError when one path is
absolute and the other isn't.

#### `fs.remove(string path, object? options) result<unit, error>`

remove deletes a file, or a directory and everything in it. A symlink is
removed, not its target. Fails with NotFoundError when path doesn't exist,
so used as a boolean it is true only when something was removed, and
PermissionError or IOError when something can't be deleted. Option: force
(boolean, default false) succeeds without doing anything when path
doesn't exist, like rm -rf, for cleanup of a path that may already be
gone.

#### `fs.rename(string from, string to) result<unit, error>`

rename moves or renames a file or directory, replacing an existing file
at to. Fails with NotFoundError when from doesn't exist, PermissionError,
or IOError (for example when to is on another device).

#### `fs.segments(string path) string[]`

segments lists a cleaned path's elements without separators:
fs.segments("/usr/local/bin") is ["usr", "local", "bin"].

#### `fs.size(string path) result<int, error>`

size returns a file's size in bytes, following symlinks. Fails with
NotFoundError when the file doesn't exist.

#### `fs.splitExt(string path) string[]`

splitExt splits a path into [path without extension, extension]:
fs.splitExt("a/b.txt") is ["a/b", ".txt"].

#### `fs.splitPath(string path) string[]`

splitPath splits a path after its last separator into [directory, name]:
fs.splitPath("a/b/c.txt") is ["a/b/", "c.txt"].

#### `fs.stat(string path) result<{ext: string, isDir: boolean, isSymlink: boolean, modTime: int, mode: string, name: string, path: string, size: int}, error>`

stat describes the file, directory or symlink at path, with the same
keys as an fs.entries entry. A symlink describes its target, with
isSymlink true. Fails with NotFoundError when nothing exists at path.

#### `fs.stem(string path) string`

stem returns the last element of a path without its extension:
fs.stem("a/b/c.txt") is "c".

#### `fs.syncFile(string path) result<unit, error>`

syncFile flushes a file's written data to disk. Fails with NotFoundError
when the file doesn't exist; it never creates one.

#### `fs.syncLater(string path) result<unit, error>`

syncLater queues a file to be flushed to disk in the background, merging
repeated requests for the same file; fs.syncPending waits for the queue.
A file removed before its flush is skipped. Throws ValueError for an
empty path.

#### `fs.syncPending() result<unit, error>`

syncPending waits until every file queued with fs.syncLater before the
call is flushed. Fails with the first flush error, usually an IOError,
including one from a flush that finished before the call. Every call
waiting on a flush reports its failure, and a failure no call was
waiting on is reported once, by the next call.

#### `fs.tail(string path, any ...count) result<string[], error>`

tail returns up to the last count lines (default 10) of a file, reading
backwards from the end. Fails with NotFoundError when the file doesn't
exist; throws ValueError for a negative count.

#### `fs.toPosix(string path) string`

toPosix converts the OS separators in a path to forward slashes.

#### `fs.tryAppendToFile(any path, any data) result`

**Deprecated.** Deprecated: use fs.appendFile(path, text) instead, which returns a result.

tryAppendToFile appends to a file into a result with a string error.

#### `fs.tryMkdirAll(any path) result`

**Deprecated.** Deprecated: use fs.mkdir(path, {recursive: true}) instead, which returns a result.

tryMkdirAll creates directories into a result with a string error.

#### `fs.tryReadDir(any path) result`

**Deprecated.** Deprecated: use fs.readDir(path) instead, which returns a result.

tryReadDir lists names into a result with a string error.

#### `fs.tryReadFile(any path) result`

**Deprecated.** Deprecated: use fs.readFile(path) instead, which returns a result.

tryReadFile reads a file into a result with a string error.

#### `fs.tryRemove(any path) result`

**Deprecated.** Deprecated: use fs.remove(path, {force: true}) instead, which returns a result.

tryRemove removes into a result with a string error.

#### `fs.tryRename(any oldPath, any newPath) result`

**Deprecated.** Deprecated: use fs.rename(from, to) instead, which returns a result.

tryRename renames into a result with a string error.

#### `fs.tryWriteFile(any path, any data) result`

**Deprecated.** Deprecated: use fs.writeFile(path, text) instead, which returns a result.

tryWriteFile writes a file into a result with a string error.

#### `fs.walk(any path) {ext: string, isDir: boolean, name: string, path: string, type: any}[]`

**Deprecated.** Deprecated: use `for entry in fs.walkTree(path)` instead.

walk lists everything below path, or [] when it can't be read.

#### `fs.walkDir(any path, any fn)`

**Deprecated.** Deprecated: use `for entry in fs.walkTree(path)` instead.

walkDir calls fn(path, file, control) for every entry under path,
including path itself; setting control.skip skips a directory.

#### `fs.walkTree(string path) iter<{ext: string, isDir: boolean, isSymlink: boolean, modTime: int, mode: string, name: string, path: string, size: int}>`

walkTree visits everything below a directory, depth first in lexical
order, without following symlinks; the root itself is not included. It
yields entries shaped like fs.entries ones. A missing or unreadable root
or subdirectory throws NotFoundError, PermissionError or IOError from
the loop that is iterating.

#### `fs.withExt(string path, string ext) string`

withExt replaces a path's extension; the dot is optional and "" removes
it: fs.withExt("a/b.txt", "md") is "a/b.md".

#### `fs.withName(string path, string name) string`

withName replaces a path's last element: fs.withName("a/b/c.txt",
"d.md") is "a/b/d.md".

#### `fs.writeFile(string path, string text, object? options) result<unit, error>`

writeFile replaces a file's contents with text, creating it if needed.
The write is atomic: readers see the old or the new contents, never a
mix, even after a crash, and an existing file keeps its permissions.
Writing through a symlink replaces its target. Fails with NotFoundError
when the parent directory doesn't exist, PermissionError, or IOError.
Options: durable (boolean, default true) returns only once the new
contents are on stable storage; false keeps the write atomic but may
return first, so a crash right after can leave the old contents. On
macOS false uses a write barrier, and on Linux with ext4, XFS or btrfs
it waits for the data to reach the disk without flushing its cache;
elsewhere it flushes in full, like a durable write.

#### `fs.writeFileBytes(string path, any data, object? options) result<unit, error>`

writeFileBytes replaces a file's contents with bytes, atomically like
fs.writeFile and with the same options. data is a byte[]; a string
writes its UTF-8 bytes and an array of whole numbers from 0 to 255
writes those bytes. Throws ValueError for a number outside that range.

### `*fs.File`

#### `file.close() result<unit, error>`

close flushes buffered writes and closes the file. Closing again does
nothing. Fails with IOError when the final flush fails.

#### `file.eof() boolean`

eof reports whether there is nothing left to read. A closed or
write-only handle has nothing to read.

#### `file.flush() result<unit, error>`

flush writes buffered data to the file. Throws StateError on a closed or
read-only handle; fails with IOError.

#### `file.grep(string pattern) result<string[], error>`

grep reads the remaining lines and returns those matching a regular
expression. Fails with ParseError for an invalid pattern and IOError when
reading fails. Throws StateError on a closed or write-only handle.

#### `file.head(any ...count) result<string[], error>`

head reads up to count lines (default 10) from the current position. Throws
StateError on a closed or write-only handle and ValueError for a negative
count; fails with IOError when reading fails.

#### `file.lines() iter<string>`

lines iterates over the remaining lines, without line endings. A read
error throws IOError from the loop. Throws StateError on a closed or
write-only handle.

#### `file.read(any ...count) result<string, error>`

read reads text from the current position: everything that is left, or
at most count bytes when count is given. At the end of the file it returns "".
Throws StateError on a closed or write-only handle and ValueError for a
negative count; fails with IOError when reading fails.

#### `file.readBytes(any ...count) result<byte[], error>`

readBytes reads bytes from the current position: everything that is
left, or at most count bytes when count is given. Fails like file.read.

#### `file.readLine() result<string, error>`

readLine reads the next line, without its line ending ("\n" or "\r\n").
Fails with IOError at the end of the file, so check file.eof() first or
loop with `for line in file.lines()`. Throws StateError on a closed or
write-only handle.

#### `file.seek(int offset, any ...from) result<int, error>`

seek moves the read and write cursor offset bytes from "start" (the
default), "current" or "end", and returns its new offset from the start:
the number of bytes before it, so 0 is the start of the file. Offsets are
distances, not 1-based positions. Buffered writes are flushed first.
Throws StateError on a closed handle and ValueError for another origin;
fails with IOError.

#### `file.tail(any ...count) result<string[], error>`

tail reads the last count lines (default 10) of the whole file, without
moving the read position. Throws StateError on a closed or write-only
handle and ValueError for a negative count; fails with IOError.

#### `file.write(any data) result<unit, error>`

write writes data at the current position: a string, a byte[] or an
array of whole numbers from 0 to 255; other values are written as their
string form. Writes are buffered until file.flush() or file.close().
Throws StateError on a closed or read-only handle; fails with IOError.
