std/fs
Filesystem metadata, directory listing and traversal, and read-by-path helpers.
from "std/fs" import { readFile, readDir, walkDir, glob, mkdirAll, removeAll, writeFile }Example
let contents = readFile("config.txt")!
for entry in readDir(".")! {
if entry.isFile {
print(entry.name)
}
}
for src in glob("src", "**/*.milo")! {
print(src)
}walkDir and glob never follow symlinks, and one unreadable directory fails the call rather than silently shortening the list. removeAll is rm -rf that removes links instead of following them.
API reference
DirEntry
pub struct DirEntryDirectory entry from readDir().
Fields: name: string, isDir: bool, isFile: bool.
FileInfo
pub struct FileInfoFile metadata from stat().
Fields:
size: i64mode: i32: Do not trust this field yet. It is read at a fixed byte offset into struct stat, and st_mode's offset is arch-specific (24 on x86_64 Linux, 16 on arm64) while std/platform splits by OS only — on arm64 Linux it reads a different field and comes back 0. isDir/isFile/isSymlink deliberately avoid it; so does copyFile.mtimeNs: i64: Last modification time, nanoseconds since the Unix epoch (whole seconds on Windows, whose _stat64 has no sub-second field).
WalkEntry
pub struct WalkEntryOne node of a directory tree, produced by walkDir.
Fields:
path: string: Full path: the walk root joined with relPath.relPath: string: Path relative to the walk root, always "/"-separated. This is what glob patterns are matched against.isDir: bool: A symlink to a directory is NOT a directory here — walkDir never follows links, so it reports one as an ordinary entry with no children.
Functions
appendFile
pub fn appendFile(path: &string, data: &string): Result<i64, IoError>Append a string to a file, creating it if absent (writes go to the end).
changeDir
pub fn changeDir(path: &string): Result<Unit, IoError>copyFile
pub fn copyFile(src: &string, dst: &string): Result<Unit, IoError>Copy the contents of src onto dst, creating or truncating dst.
Streams through a 64 KiB buffer, so a file larger than memory copies fine. Copying a directory is an error, not a recursive copy.
Permissions are not copied verbatim: dst is created 0644 (the same mode writeFile uses), then set to 0755 when src is executable. Reading a file's mode bits portably is not possible today — see FileInfo.mode — and quietly dropping the executable bit off a copied program is the worse failure. The consequence is that copying a 0600 file yields a 0644 one: chmod dst yourself when the copy has to stay private. Timestamps, xattrs and ACLs are not copied either, and a sparse file is copied dense.
On failure dst is left truncated or partially written, not removed.
currentDir
pub fn currentDir(): Result<string, IoError>dataSyncFile
pub fn dataSyncFile(f: &File): Result<Unit, IoError>Flush an open file's data to the device, skipping metadata not needed to read it (fdatasync).
devNull
pub fn devNull(): stringPath of the OS bit-bucket device — /dev/null on POSIX, NUL on Windows. Use this instead of hard-coding "/dev/null", which does not exist on Windows.
fileInfo
pub fn fileInfo(path: &string): Option<FileInfo>Get file metadata, or None when the path cannot be inspected. Use read/open operations when the failure reason matters.
fileSizePath
pub fn fileSizePath(path: &string): Option<i64>Get file size in bytes.
None when the file cannot be opened or measured.
glob
pub fn glob(root: &string, pattern: &string): Result<Vec<string>, IoError>Every path under root whose path relative to root matches pattern. Returned paths are root-prefixed and ready to open; the list is sorted, so a build that globs its inputs is reproducible.
Because matching is on the relative path, glob(dir, "*.milo") finds only the top level and glob(dir, "**/*.milo") finds every depth. Directories are matched too — glob(dir, "**") lists the whole tree.
Walks the whole tree first (see walkDir): the pattern does not prune the walk, so glob("/", "*") reads the entire disk. Inherits walkDir's failure mode — one unreadable directory fails the call rather than silently shortening the list.
globMatch
pub fn globMatch(pattern: &string, text: &string): boolMatch a glob pattern against a path-like string.
Pure — it never touches the filesystem, so it works on anything "/"-separated: archive members, object-store keys, route tables. glob is this function plus a walkDir.
| Pattern | Matches |
|---|---|
? | one byte, never / |
* | any run of bytes inside one segment, never / |
** | as a whole segment: zero or more segments |
[abc] [a-z] | one byte from the set |
[!a-z] [^a-z] | one byte outside the set |
\* | a literal * |
** is only special as a complete segment: src/**/*.milo crosses directories, src/**.milo is just src/*.milo. A trailing / leaves an empty final segment that no path can match, so "everything under src" is src/**, never src/**/. Matching is bytewise, so a range like [à-ø] over multibyte UTF-8 compares bytes, not code points. Nothing here is anchored to a filesystem, so a leading /, ., or .. is an ordinary segment and dotfiles are not special-cased.
globMatchSegment
pub fn globMatchSegment(pat: &string, text: &string): boolMatch one path segment (no '/' inside either side). Single backtrack point, which is enough because the segment has no separator constraint to violate: if a later '' fails, widening an earlier '' can never help.
globMatchSplitPattern
pub fn globMatchSplitPattern(patSegs: &Vec<string>, text: &string): boolThe same match with the pattern's segments already split out. A caller testing one pattern against thousands of paths — an ignore-rule set against a directory walk — splits the pattern once instead of once per path.
hardLink
pub fn hardLink(existing: &string, newPath: &string): Result<Unit, IoError>isDir
pub fn isDir(path: &string): boolCheck if a path is a directory.
isFile
pub fn isFile(path: &string): boolCheck if a path is a regular file. Defined as "exists and is not a directory" so it, like isDir, avoids the struct-stat S_IFREG bit whose offset is arch-specific (see isDir). This treats a socket/fifo/device as a file too, but those do not appear in the file trees these helpers walk; the file-vs-directory distinction the callers actually need is exact.
isSymlink
pub fn isSymlink(path: &string): boolCheck if a path is a symbolic link. Does not follow it — a dangling link is still a link.
lstatInfo
pub fn lstatInfo(path: &string): Option<FileInfo>makeDir
pub fn makeDir(path: &string, mode: i32): Result<Unit, IoError>makeTempDir
pub fn makeTempDir(prefix: &string): Result<string, IoError>Create a new directory with a unique name and return its path. prefix is a path prefix, not a parent directory: makeTempDir("/tmp/build-") creates something like "/tmp/build-a1B2c3".
The directory is created 0700 and is the caller's to remove — nothing cleans it up at exit. removeAll is the usual companion.
Needs POSIX mkdtemp, so it does not link on Windows.
makeTempFile
pub fn makeTempFile(prefix: &string): Result<string, IoError>Create a new empty file with a unique name and return its path. prefix is a path prefix, not a parent directory: makeTempFile("/tmp/build-") creates something like "/tmp/build-a1B2c3".
The file is created 0600 and returned closed — open it with File.openWrite. It is the caller's to remove; nothing cleans it up at exit. Reopening by name is safe against the classic /tmp attack because the file already exists and is owned by this process; an attacker cannot substitute their own file for it in a sticky directory.
Pairs with makeTempDir, and like it needs POSIX mkstemp, so it does not link on Windows.
mkdirAll
pub fn mkdirAll(path: &string, mode: i32): Result<Unit, IoError>Create path and every missing parent, like mkdir -p.
Succeeds when the directory already exists. Fails when a component exists as something other than a directory, or cannot be created. mode applies only to directories this call creates and is filtered by the process umask; an existing directory keeps the mode it has.
Intermediate directories briefly exist before the leaf does, so a failure part way through leaves the prefix it managed to create.
pathExists
pub fn pathExists(path: &string): boolCheck whether a path is visible to this process.
readDir
pub fn readDir(path: &string): Result<Vec<DirEntry>, IoError>List directory contents. An empty directory is distinct from a failed read.
readFile
pub fn readFile(path: &string): Result<string, IoError>Read an entire file into a string. Returns an IoError (NotFound, permission, etc.) rather than throwing; propagate with ? or match on it.
readLines
pub fn readLines(path: &string): Result<Vec<string>, IoError>Read a file and return its contents as a Vec of lines.
readLink
pub fn readLink(path: &string): Result<string, IoError>realPath
pub fn realPath(path: &string): Result<string, IoError>removeAll
pub fn removeAll(path: &string): Result<Unit, IoError>Recursively delete path and everything under it, like rm -rf.
Symlinks are removed, never followed. removeAll(link) where link points at /etc unlinks the link and leaves /etc untouched, and the same holds for every link found inside the tree. This is the whole reason the walk here does not reuse walkDir's entry list.
A path that does not exist is success: the postcondition — it is gone — already holds, matching rm -rf.
Failure is not all-or-nothing. This deletes depth-first and returns the IoError of the first entry it cannot remove, leaving everything not yet visited in place. A caller that needs the tree actually gone must check the result; a partially emptied tree is a real outcome.
Not safe against another process mutating the tree underneath you — the classic rm -rf TOCTOU race applies. Use it on trees you own, such as a build output or a makeTempDir scratch space. Like walkDir it builds absolute paths, so it cannot empty a tree deeper than the OS path limit.
removeDir
pub fn removeDir(path: &string): Result<Unit, IoError>removeFile
pub fn removeFile(path: &string): Result<Unit, IoError>renameFile
pub fn renameFile(oldPath: &string, newPath: &string): Result<Unit, IoError>setFileMode
pub fn setFileMode(f: &File, mode: i32): Result<Unit, IoError>Change an open file's permission bits (fchmod).
setFileOwner
pub fn setFileOwner(f: &File, uid: u32, gid: u32): Result<Unit, IoError>Change an open file's owner and group (fchown).
setLinkOwner
pub fn setLinkOwner(path: &string, uid: u32, gid: u32): Result<Unit, IoError>setMode
pub fn setMode(path: &string, mode: i32): Result<Unit, IoError>setOwner
pub fn setOwner(path: &string, uid: u32, gid: u32): Result<Unit, IoError>softLink
pub fn softLink(target: &string, path: &string): Result<Unit, IoError>splitLines
pub fn splitLines(content: &string): Vec<string>Split a string into lines on newline boundaries.
syncFile
pub fn syncFile(f: &File): Result<Unit, IoError>Flush an open file's data and metadata to the device (fsync).
truncateFile
pub fn truncateFile(path: &string, length: i64): Result<Unit, IoError>truncateOpenFile
pub fn truncateOpenFile(f: &File, length: i64): Result<Unit, IoError>Truncate or extend an open file to length bytes (ftruncate).
walkDir
pub fn walkDir(root: &string): Result<Vec<WalkEntry>, IoError>Walk the tree under root, returning every entry below it. The root itself is not included. A directory always appears before its contents; order within one directory is whatever the filesystem reports, so sort the result if you need determinism (glob already does).
Symlinks are never followed. A link to a directory comes back with isDir: false and no children, so a link cycle cannot hang the walk.
Eager, not lazy. Milo has no lazy iterators, and a cursor type would not pay for itself here: every entry name is an owned string either way, so laziness would save the result Vec and none of the syscalls, while adding a stateful type whose interrupted state is a new thing to get wrong. Walk a subtree instead of the whole disk if the entry list would be too large to hold.
The first unreadable directory aborts the walk with its IoError. Returning a short list that silently omitted a permission-denied subtree is exactly the failure this signature exists to prevent. Paths are built absolute, so a tree deeper than the OS path limit fails here rather than being traversed; rm -rf avoids that with openat, this does not.
writeFile
pub fn writeFile(path: &string, data: &string): Result<i64, IoError>Write a string to a file, creating or truncating it.