std/fs¶
Filesystem access: read and write files, create and remove directories,
list and size entries, query existence, and manipulate path strings. The
fallible operations return Result<T, Error>; the queries return a plain
Bool; the path helpers are pure string work with no runtime call.
import std/fs { write, read }
fun main() {
match write("greeting.txt", "hello") {
Ok(_) -> print("wrote it"),
Err(e) -> print(e.message()),
}
}
Importing¶
import std/fs { read, write, append, read_lines, copy, remove_file, create_dir, create_dir_all, remove_dir, list_dir, walk, size, exists, is_file, is_dir, join, dirname, basename, split }
std/fs is a set of free functions, so use a selective import and list the
names you want. A bare import std/fs does not bring the functions into
scope. Import only what a given file uses:
The error model¶
Fallible operations return Result<T, Error>. On failure the Error is an
std/error value tagged with kind "io". Its message is a short context
prefix (the operation name) joined to the operating system error text, for
example read: No such file or directory.
There are no sentinel return values: a missing file does not come back as an
empty string or a -1, it comes back as an Err. The two ways to consume a
Result are a match and the ? operator.
Handle each arm explicitly with match:
import std/fs { read }
fun main() {
match read("config.txt") {
Ok(text) -> print(text),
Err(e) -> print(e.message()),
}
}
Or propagate the error to the caller with ?, which unwraps Ok and
returns early on Err:
import std/fs { read, write }
fun copy(src: String, dst: String) -> Result<Bool, Error> {
let text = read(src)?
return write(dst, text)
}
Reading and writing¶
read(path: String) -> Result<String, Error>¶
Read the whole file at path and return it as a single String.
import std/fs { read }
fun main() {
match read("notes.txt") {
Ok(text) -> print(text),
Err(e) -> print(e.message()),
}
}
write(path: String, contents: String) -> Result<Bool, Error>¶
Create or truncate path, then write contents. Returns Ok(true) on
success. The Bool payload is always true; it exists only so the success
arm carries a value (the surface uses Result<Bool, Error> rather than a
unit payload).
import std/fs { write }
fun main() {
match write("out.txt", "first line\n") {
Ok(_) -> print("ok"),
Err(e) -> print(e.message()),
}
}
append(path: String, contents: String) -> Result<Bool, Error>¶
Write contents to the end of path, creating the file when it is absent.
Returns Ok(true) on success.
import std/fs { append }
fun log_line(line: String) -> Result<Bool, Error> {
return append("app.log", line.concat("\n"))
}
read_lines(path: String) -> Result<List<String>, Error>¶
Read the file at path and split it into lines, handling both \n and
\r\n line endings. The trailing newline does not produce an empty final
entry.
import std/fs { read_lines }
fun main() {
match read_lines("config.txt") {
Ok(lines) -> {
for line in lines {
print(line)
}
}
Err(e) -> print(e.message()),
}
}
copy(src: String, dst: String) -> Result<Bool, Error>¶
Copy the contents of src to dst, creating or truncating dst. Returns
Ok(true) on success.
import std/fs { copy }
fun main() {
match copy("notes.txt", "notes.bak") {
Ok(_) -> print("copied"),
Err(e) -> print(e.message()),
}
}
Directory and file operations¶
remove_file(path: String) -> Result<Bool, Error>¶
Remove the file at path. Returns Ok(true) on success.
create_dir(path: String) -> Result<Bool, Error>¶
Create the directory at path, including any missing parent directories, so
creating a nested path in one call succeeds. Returns Ok(true) on success.
import std/fs { create_dir }
fun main() {
match create_dir("build/cache/tmp") {
Ok(_) -> print("created"),
Err(e) -> print(e.message()),
}
}
create_dir_all(path: String) -> Result<Bool, Error>¶
Create the directory at path along with any missing parents, like
mkdir -p. An already existing directory is not an error. Returns
Ok(true) on success.
import std/fs { create_dir_all }
fun main() {
match create_dir_all("build/cache/tmp") {
Ok(_) -> print("created"),
Err(e) -> print(e.message()),
}
}
remove_dir(path: String) -> Result<Bool, Error>¶
Remove the directory at path and all of its contents recursively. Returns
Ok(true) on success.
list_dir(path: String) -> Result<List<String>, Error>¶
Return the entry names (not full paths) of the directory at path. An empty
directory yields an empty list, not a one-element list containing "".
import std/fs { list_dir }
fun main() {
match list_dir(".") {
Ok(names) -> {
for name in names {
print(name)
}
}
Err(e) -> print(e.message()),
}
}
walk(path: String) -> Result<List<String>, Error>¶
Recursively list every path under path, depth first. Each entry is a full
path (the directory prefix joined to the name), and subdirectories appear
before their contents.
import std/fs { walk }
fun main() {
match walk("src") {
Ok(entries) -> {
for entry in entries {
print(entry)
}
}
Err(e) -> print(e.message()),
}
}
size(path: String) -> Result<Int, Error>¶
Return the file size at path in bytes.
import std/fs { size }
fun main() {
match size("out.txt") {
Ok(n) -> print(n),
Err(e) -> print("could not stat the file"),
}
}
The ? operator is the shorter form, but it only works inside a function that
itself returns Result, not in main:
import std/fs { size }
fun total(a: String, b: String) -> Result<Int, Error> {
return Ok(size(a)? + size(b)?)
}
Boolean checks¶
These return a plain Bool, never a Result. A missing path is a normal
false, not an Err. is_file is false for a path that exists but is not
a regular file (a directory, say), and is_dir is false for anything that
is not a directory.
import std/fs { exists, is_dir, read }
fun main() {
if exists("config.txt") {
match read("config.txt") {
Ok(text) -> print(text),
Err(e) -> print(e.message()),
}
}
print(is_dir("build"))
}
Path helpers¶
fun join(a: String, b: String) -> String
fun basename(p: String) -> String
fun dirname(p: String) -> String
fun split(p: String) -> List<String>
These are pure string operations with no runtime call and no Result. They
use / as the separator. Forward slash works on Windows for these std ops,
so the operating system separator is not detected. They overlap with
std/path; std/fs duplicates them so the module is
usable without a second import.
join(a: String, b: String) -> String¶
Join two path segments with a single /, collapsing a trailing slash on a
or a leading slash on b so the result never doubles the separator. An empty
segment returns the other unchanged.
import std/fs { join }
fun main() {
print(join("build", "out.txt")) // build/out.txt
print(join("build/", "/out.txt")) // build/out.txt
}
basename(p: String) -> String¶
The final component of p (everything after the last /). A path with no
slash returns unchanged.
import std/fs { basename }
fun main() {
print(basename("a/b/c.txt")) // c.txt
print(basename("c.txt")) // c.txt
}
dirname(p: String) -> String¶
Everything before the last /. A path with no slash returns "."; a path
whose only slash is at the front returns "/".
import std/fs { dirname }
fun main() {
print(dirname("a/b/c.txt")) // a/b
print(dirname("c.txt")) // .
}
split(p: String) -> List<String>¶
Split p on / into its components, dropping empty pieces produced by
leading, trailing, or repeated separators.
import std/fs { split }
fun main() {
let parts = split("/a//b/c/") // ["a", "b", "c"]
for part in parts {
print(part)
}
}
Worked example: write then read a file¶
Write a file, confirm it exists, then read it back. Each fallible step uses
? to bail out on the first error, and the caller decides what to do with it.
import std/fs { write, read, exists }
import std/error { error_kind }
fun round_trip() -> Result<String, Error> {
write("scratch.txt", "saved value")?
if !exists("scratch.txt") {
return Err(error_kind("io", "file vanished after write"))
}
return read("scratch.txt")
}
fun main() {
match round_trip() {
Ok(text) -> print(text), // saved value
Err(e) -> print(e.message()),
}
}