Skip to content

std/fs ​

Filesystem metadata, directory listing and traversal, and read-by-path helpers.

milo
from "std/fs" import { readFile, readDir, walkDir, glob, mkdirAll, removeAll, writeFile }

Example ​

milo
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 ​

milo
pub struct DirEntry

Directory entry from readDir().

Fields: name: string, isDir: bool, isFile: bool.

FileInfo ​

milo
pub struct FileInfo

File metadata from stat().

Fields:

  • size: i64
  • mode: 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 ​

milo
pub struct WalkEntry

One 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 ​

milo
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 ​

milo
pub fn changeDir(path: &string): Result<Unit, IoError>

copyFile ​

milo
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 ​

milo
pub fn currentDir(): Result<string, IoError>

dataSyncFile ​

milo
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 ​

milo
pub fn devNull(): string

Path 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 ​

milo
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 ​

milo
pub fn fileSizePath(path: &string): Option<i64>

Get file size in bytes.

None when the file cannot be opened or measured.

glob ​

milo
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 ​

milo
pub fn globMatch(pattern: &string, text: &string): bool

Match 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.

PatternMatches
?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 ​

milo
pub fn globMatchSegment(pat: &string, text: &string): bool

Match 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 ​

milo
pub fn globMatchSplitPattern(patSegs: &Vec<string>, text: &string): bool

The 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.

milo
pub fn hardLink(existing: &string, newPath: &string): Result<Unit, IoError>

isDir ​

milo
pub fn isDir(path: &string): bool

Check if a path is a directory.

isFile ​

milo
pub fn isFile(path: &string): bool

Check 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.

milo
pub fn isSymlink(path: &string): bool

Check if a path is a symbolic link. Does not follow it — a dangling link is still a link.

lstatInfo ​

milo
pub fn lstatInfo(path: &string): Option<FileInfo>

makeDir ​

milo
pub fn makeDir(path: &string, mode: i32): Result<Unit, IoError>

makeTempDir ​

milo
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 ​

milo
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 ​

milo
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 ​

milo
pub fn pathExists(path: &string): bool

Check whether a path is visible to this process.

readDir ​

milo
pub fn readDir(path: &string): Result<Vec<DirEntry>, IoError>

List directory contents. An empty directory is distinct from a failed read.

readFile ​

milo
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 ​

milo
pub fn readLines(path: &string): Result<Vec<string>, IoError>

Read a file and return its contents as a Vec of lines.

milo
pub fn readLink(path: &string): Result<string, IoError>

realPath ​

milo
pub fn realPath(path: &string): Result<string, IoError>

removeAll ​

milo
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 ​

milo
pub fn removeDir(path: &string): Result<Unit, IoError>

removeFile ​

milo
pub fn removeFile(path: &string): Result<Unit, IoError>

renameFile ​

milo
pub fn renameFile(oldPath: &string, newPath: &string): Result<Unit, IoError>

setFileMode ​

milo
pub fn setFileMode(f: &File, mode: i32): Result<Unit, IoError>

Change an open file's permission bits (fchmod).

setFileOwner ​

milo
pub fn setFileOwner(f: &File, uid: u32, gid: u32): Result<Unit, IoError>

Change an open file's owner and group (fchown).

setLinkOwner ​

milo
pub fn setLinkOwner(path: &string, uid: u32, gid: u32): Result<Unit, IoError>

setMode ​

milo
pub fn setMode(path: &string, mode: i32): Result<Unit, IoError>

setOwner ​

milo
pub fn setOwner(path: &string, uid: u32, gid: u32): Result<Unit, IoError>
milo
pub fn softLink(target: &string, path: &string): Result<Unit, IoError>

splitLines ​

milo
pub fn splitLines(content: &string): Vec<string>

Split a string into lines on newline boundaries.

syncFile ​

milo
pub fn syncFile(f: &File): Result<Unit, IoError>

Flush an open file's data and metadata to the device (fsync).

truncateFile ​

milo
pub fn truncateFile(path: &string, length: i64): Result<Unit, IoError>

truncateOpenFile ​

milo
pub fn truncateOpenFile(f: &File, length: i64): Result<Unit, IoError>

Truncate or extend an open file to length bytes (ftruncate).

walkDir ​

milo
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 ​

milo
pub fn writeFile(path: &string, data: &string): Result<i64, IoError>

Write a string to a file, creating or truncating it.