#Fs
Status: standard library specification draft.
This chapter defines std.fs, which lib/std writes in ordinary hd
over the language tier:
- the host capability traits
FsReadandFsWrite; - the error enum
FsError, and theEntryvalue that describes a file; - a free helper for each method of
FsReadandFsWrite; walk!andglob!, which find entries below a directory;MemoryFs, the deterministic provider of both traits.
Paths are Path values. Which provider a command binds is CLI
tier (Host Capabilities).
#File System Errors
Every file system operation reports one error enum:
pub enum FsError:
NotFound(path: Path)
PermissionDenied(path: Path)
NotGranted(path: Path)
AlreadyExists(path: Path)
NotADirectory(path: Path)
IsADirectory(path: Path)
InvalidUtf8(path: Path)
Other(message: string)
pub enum EntryKind:
File
Directory
Symlink
pub struct Entry:
pub path: Path
pub kind: EntryKind
pub size: u64
pub modified: Timestamp? = .None- std-fs.types.entry-metadata
std.fsdeclaresFsError,EntryKind, andEntryas above.Entryis a struct value: a copy is independent of the entry it was copied from. Code imports them, as inuse std.fs.{Entry, FsError}. - std-fs.error.one-enumEvery method of
FsReadandFsWritereports its failure as anFsError. - std-fs.error.other
Otherholds a failure that no other variant names, with the host's message, or the message of a std call such as an invalid glob pattern. - std-fs.error.traits
FsError,EntryKind, andEntryimplementEq, andFsErrorimplementsDisplay. - std-fs.entry.sizeAn
Entry'ssizeis the file's length in bytes. - std-fs.entry.modified
Entry.modifiedis the entry's modification time as aTimestamp?, defaulting to.None. - std-fs.entry.modified-hostHost providers fill
modifiedfrom filesystem metadata when its time is available and representable as aTimestamp. - std-fs.entry.modified-unknownA provider that cannot report the modification time returns
.Nonefor that field. - std-fs.error.not-granted
NotGranted(path)reports that the program's capability grant does not coverpath, by Partial Deny. - std-fs.error.permission-denied
PermissionDenied(path)reports that the operating system refused the access. - std-fs.error.not-granted.plainThe
Displaytext ofNotGranted(path)isaccess to PATH is not granted, with the path forPATH. - std-fs.error.eq.variant-payloadTwo
FsErrorvalues compare equal only when their variants and corresponding payloads are equal.
Why.
NotGrantedandPermissionDeniedhave different fixes: a flag or anhd.tomlentry for the first, file modes for the second.
#Reading
pub trait FsRead:
fn read_bytes!(self, path: Path) -> Result[List[u8], FsError]
fn read_text!(self, path: Path) -> Result[string, FsError]
fn list_dir!(self, path: Path) -> Result[List[Entry], FsError]
fn stat!(self, path: Path) -> Result[Entry?, FsError]- std-fs.read.decl
std.fsdeclares the host capability traitFsReadwith the methods above. Code imports it, as inuse std.fs.FsRead. - std-fs.read.bytes
read_bytes!returns the whole content of the file atpath. - std-fs.read.text
read_text!returns that content as a string. Content that is not valid UTF-8 is.Err(FsError.InvalidUtf8(path)). - std-fs.read.list-dir
list_dir!returns oneEntryfor each entry of the directory atpath. - std-fs.read.stat
stat!returns theEntryforpath, or.Nonewhen nothing is there.
#Writing
pub trait FsWrite:
fn write_bytes!(&mut self, path: Path, bytes: List[u8]) -> Result[void, FsError]
fn write_text!(&mut self, path: Path, text: string) -> Result[void, FsError]
fn append_text!(&mut self, path: Path, text: string) -> Result[void, FsError]
fn create_dir_all!(&mut self, path: Path) -> Result[void, FsError]
fn remove!(&mut self, path: Path) -> Result[void, FsError]
fn rename!(&mut self, from: Path, to: Path) -> Result[void, FsError]
fn copy!(&mut self, from: Path, to: Path) -> Result[void, FsError]- std-fs.write.copy-decl
std.fsdeclares the host capability traitFsWritewith the methods above. Code imports it, as inuse std.fs.FsWrite. - std-fs.write.replace
write_bytes!andwrite_text!create the file atpath, or replace its whole content. - std-fs.write.append
append_text!addstextat the end of the file atpath, and creates the file when there is none. - std-fs.write.create-dir-all
create_dir_all!creates the directory atpathand every missing directory above it. - std-fs.write.remove
remove!removes the file or the empty directory atpath. - std-fs.write.rename
rename!moves the entry atfromtoto. - std-fs.write.mut-ref.reference-valuesEach
FsWritemethod takes&mut self, soFsWriteis a mutable requirement trait and$.use(FsWrite)is a&mut dyn FsWrite, byreq.ref.use.reference-values. EachFsReadmethod takesself, soFsReadis a readonly one, and a reference installed as its provider reads the place it refers to, byreq.ref.share.reference-values. - std-fs.cancel.writeCancelling the suspension of
write_bytes!,write_text!, orappend_text!may leave a partial file atpath.
#Copying Files
- std-fs.copy.bytes
FsWrite.copy!(from: Path, to: Path) -> Result[void, FsError]copies the source file's bytes to the destination. - std-fs.copy.replaceCopying creates a missing destination file or replaces its entire content.
- std-fs.copy.hostA host provider copies inside the host, without transferring the file's bytes into the program.
- std-fs.copy.read-grantThe host checks the source against the
FsReadgrant, as forread_bytes!. - std-fs.copy.write-grantThe host checks the destination against the
FsWritegrant, as forwrite_bytes!. - std-fs.copy.refusedA refused endpoint returns
NotGrantednaming that endpoint, before copying any bytes. - std-fs.copy.source-missingA missing source returns
NotFound(from). - std-fs.copy.source-directoryA directory source returns
IsADirectory(from). - std-fs.copy.destination-directoryA destination directory returns
IsADirectory(to). - std-fs.copy.destination-parent-fileA destination below a file returns
NotADirectory(to). - std-fs.copy.aliasCopying a file to another path for the same file succeeds without changing its bytes.
- std-fs.memory.copy
MemoryFs.copy!reads the source bytes and writes them to the destination with its existing read and write errors. - std-fs.helper.copy
pub fn copy!(from: Path, to: Path) -> Result[void, FsError] $ FsWritecallscopy!(from, to)on the coveringFsWriteprovider.
Why. Copying is one host operation: a program need not read bytes only to send them straight back. Separate source and destination grants preserve the authority of the corresponding read and write operations.
#Suspension
- std-fs.suspendsFile system access is I/O, so every method of
FsReadandFsWriteis a bang method. - std-fs.special-fileA read of a special file, such as a FIFO, a device, or a named pipe, lets other tasks run while it waits, by
std-host.op.wait. - std-fs.special-file.deadlineSuch a read has no other deadline guarantee.
use std.fs.FsRead
use std.path.Path
fn show!(path: Path) -> string $ FsRead:
match $.use(FsRead).read_text!(path): # a bang call: it suspends on I/O
.Ok(text) => text
.Err(_) => ""Why. Reading and writing are separate traits, so a row says which one a function needs. A program that only reads gains no authority to change files.
#File Helpers
use std.fs.{FsError, FsRead, FsWrite, read_text, write_text}
use std.path.Path
fn copy_notes!() -> Result[void, FsError] $ FsRead + FsWrite:
text := read_text!(Path("notes.txt"))?
write_text!(Path("notes.bak"), text)- std-fs.helper.read-text
std.fsdeclarespub fn read_text!(path: Path) -> Result[string, FsError] $ FsRead, which callsread_text!(path)on theFsReadprovider that covers the call. - std-fs.helper.write-text
std.fsdeclarespub fn write_text!(path: Path, text: string) -> Result[void, FsError] $ FsWrite, which callswrite_text!(path, text)on theFsWriteprovider that covers the call. - std-fs.helper.suspendsEvery helper in this section is a bang function.
stddeclares no helper that reads or writes a file without suspending. - std-fs.helper.read-bytes
std.fsdeclarespub fn read_bytes!(path: Path) -> Result[List[u8], FsError] $ FsRead, which callsread_bytes!(path)on the coveringFsReadprovider. - std-fs.helper.list-dir
std.fsdeclarespub fn list_dir!(path: Path) -> Result[List[Entry], FsError] $ FsRead, which callslist_dir!(path)on the coveringFsReadprovider. - std-fs.helper.stat
std.fsdeclarespub fn stat!(path: Path) -> Result[Entry?, FsError] $ FsRead, which callsstat!(path)on the coveringFsReadprovider. - std-fs.helper.write-bytes
std.fsdeclarespub fn write_bytes!(path: Path, bytes: List[u8]) -> Result[void, FsError] $ FsWrite, which callswrite_bytes!(path, bytes)on the coveringFsWriteprovider. - std-fs.helper.append-text
std.fsdeclarespub fn append_text!(path: Path, text: string) -> Result[void, FsError] $ FsWrite, which callsappend_text!(path, text)on the coveringFsWriteprovider. - std-fs.helper.create-dir-all
std.fsdeclarespub fn create_dir_all!(path: Path) -> Result[void, FsError] $ FsWrite, which callscreate_dir_all!(path)on the coveringFsWriteprovider. - std-fs.helper.remove
std.fsdeclarespub fn remove!(path: Path) -> Result[void, FsError] $ FsWrite, which callsremove!(path)on the coveringFsWriteprovider. - std-fs.helper.rename
std.fsdeclarespub fn rename!(from: Path, to: Path) -> Result[void, FsError] $ FsWrite, which callsrename!(from, to)on the coveringFsWriteprovider. - std-fs.helper.rowA helper's row names exactly the capability of the method it calls:
FsReadforread_text!,read_bytes!,list_dir!, andstat!, andFsWritefor the others.
use std.fs.{FsError, append_text, list_dir, stat}
use std.fs.{EntryKind, FsRead, FsWrite}
use std.path.Path
fn log_line!(path: Path, line: string) -> Result[bool, FsError] $ FsRead + FsWrite:
append_text!(path, "${line}\n")?
.Ok(stat!(path)?.is_some())
fn count_files!(dir: Path) -> Result[usize, FsError] $ FsRead:
let mut count: usize = 0
for entry in list_dir!(dir)?:
if entry.kind == EntryKind.File:
count = count + 1
.Ok(count)Why. A non-suspending wrapper would give each file operation two spellings. A program that reads files declares
main!.No
exists!.stat!already answers a missing path with.None, sostat!(path)?.is_some()says whether anything is there, and keeps a failed check, such as a denied path, apart from "missing". One atom, not two.
#Walking And Globbing
use std.fs.{EntryKind, FsError, FsRead, glob, walk}
use std.path.Path
fn markdown!(root: Path) -> Result[List[Path], FsError] $ FsRead:
let mut found: List[Path] = []
for entry in walk!(root)?:
if entry.kind == EntryKind.File && entry.path.extension() == .Some("md"):
found.push(entry.path)
.Ok(found)
fn markdown_again!() -> Result[List[Path], FsError] $ FsRead:
glob!("docs/**/*.md")walk! lists a whole tree and glob! lists the paths that fit a
pattern. Both read only through list_dir! and stat! of the covering
FsRead provider.
- std-fs.walk.decl
std.fsdeclarespub fn walk!(root: Path) -> Result[List[Entry], FsError] $ FsRead. - std-fs.walk.entries
walk!returns oneEntryfor every entry belowroot, at any depth, and none forrootitself. EachEntryis the onelist_dir!returns for it. - std-fs.walk.orderThe order is depth first: the entries of one directory come in the byte order of their path text, and a directory's entry comes directly before the entries under it.
- std-fs.walk.symlinksA symlink is one entry, with the kind
Symlink.walk!does not look into it. - std-fs.walk.emptyA directory with no entries adds only its own entry, and an empty
rootgives an empty list. - std-fs.walk.errorWhen a
list_dir!call fails,walk!returns that call's.Errand no list. So a missingrootisNotFound(root), and a file asrootisNotADirectory(root)on a provider that reports it. - std-fs.glob.decl
std.fsdeclarespub fn glob!(pattern: string) -> Result[List[Path], FsError] $ FsRead. - std-fs.glob.components
glob!splitspatternat/into components and ignores empty ones. A pattern with no component matches nothing. - std-fs.glob.starWithin a component,
*matches any run of characters, the empty run included. - std-fs.glob.one
?matches exactly one character. - std-fs.glob.class
[starts a class that ends at the next]and matches one character: one of the listed characters, or one in a rangelow-high. A!right after[makes the class match any character it does not list. A-is a range only between two characters, and is a plain character first or last in the class. - std-fs.glob.literalAny other character matches itself, and there is no escape. To match
*,?, or[itself, write it in a class, as in[*]. - std-fs.glob.dot
*,?, and a class match a leading.of a name like any other character. - std-fs.glob.deepA component that is exactly
**matches zero or more directories. The pieces**inside a longer component are two*. - std-fs.glob.deep-lastA
**as the last component matches every entry below the directory before it, at any depth, and not that directory itself. - std-fs.glob.directoriesA component before the last matches only a directory. The last component matches a file, a directory, or a symlink.
- std-fs.glob.symlinksNo component,
**included, looks into a symlink. - std-fs.glob.relativeA pattern that does not start with
/is relative:glob!matches below the current directory, and its results do not start with/. - std-fs.glob.absoluteA pattern that starts with
/is absolute:glob!matches below/, and its results start with/. - std-fs.glob.resultEach result is the matched components and entry names joined by
/. The list is in the byte order of the path text, with no path twice. - std-fs.glob.noneA pattern that matches nothing gives
.Ok([]). A directory that is missing or is a file matches nothing, and is not an error. - std-fs.glob.errorA pattern with an unclosed
[, an empty class ([]or[!]), or a range whose low end is after its high end is.Err(FsError.Other(message)), wheremessageisinvalid glob pattern PATTERN: REASON.REASONisunclosed [,empty class, orrange LOW-HIGH runs backwards. - std-fs.glob.error.first
glob!checks every component before it reads the file system, so a bad pattern fails even when an earlier component matches nothing. Alist_dir!orstat!failure endsglob!with that call's.Err. - std-fs.glob.row
walk!andglob!have the rowFsRead, and they are bang functions bystd-fs.helper.suspends.
Why. The tree walk is the loop every file tool writes by hand, and a pattern is how people name a set of files. Both build on
FsRead, soMemoryFstests them with no host support. Go'sfilepath.WalkDirand Rust'swalkdiralso visit a directory before its contents. A fixed order makes the result the same on every host.Why symlinks stay closed. A symlink to a parent directory would make a walk run forever.
walkdirfollows links only on request, and Go'sWalkDirnever follows them. A program that wants links followed reads theSymlinkentry and walks its target.Why
glob!is relative to the current directory. Python'sgloband theglobcrate resolve a relative pattern the same way, and!negates a class in both.
#Memory File System
MemoryFs is the deterministic provider of FsRead and FsWrite. It
holds its files in memory, and a test supplies it with $.with:
use std.fs.{FsError, FsRead, FsWrite, MemoryFs, read_text, write_text}
use std.path.Path
fn copy_notes!() -> Result[void, FsError] $ FsRead + FsWrite:
text := read_text!(Path("notes.txt"))?
write_text!(Path("notes.bak"), text)
tests:
use std.testing.assert
it("copies in memory"):
files := MemoryFs::new()
assert(files.write_text!(Path("notes.txt"), "tea").is_ok(), reason="seeded")
$.with(FsRead=files, FsWrite=files):
assert(copy_notes!().is_ok(), reason="copied")
assert(files.read_text!(Path("notes.bak")).unwrap_or("") == "tea", reason="the copy")- std-fs.memory.decl
std.fsdeclaresMemoryFs, which implementsFsReadandFsWrite, with private fields. Code imports it, as inuse std.fs.MemoryFs. - std-fs.memory.new-ref
MemoryFs::new() -> *mut MemoryFsreturns a new object holding a file system with no files and no directories.MemoryFsis a struct used behind that reference: installing it asFsRead=files, FsWrite=filesmakes both providers refer to the one object, so a test reads back throughfileswhat the code under test wrote. - std-fs.memory.normalized-keysEvery
MemoryFsoperation drops empty and.components from its path keys, so./a.txtanda.txtname one entry. - std-fs.memory.rooted-keysA normalized key keeps a leading
/when the supplied path starts with/. - std-fs.memory.literal-parentNormalization leaves
..components unchanged, without resolving parent directories. - std-fs.memory.normalized-rootA relative path with no remaining components names
., and an absolute one names/. - std-fs.memory.supplied-errorsErrors retain the caller's supplied path, including its original spelling.
- std-fs.memory.dirsA directory exists when
create_dir_all!made it, or when its path is the text before a/in the path of a file. So writing a file makes the directories above it. - std-fs.memory.create-dir-all
create_dir_all!recordspathand every directory above it, so each stays after the entries inside it are removed. - std-fs.memory.entryA file's
Entryhas the kindFileand its length in bytes. A directory's has the kindDirectoryand the size 0. No entry is aSymlink. - std-fs.memory.normalized-list-order
list_dir!returns the entries directly inside the directory, in the byte order of their path text. The returned entry paths use normalized keys. - std-fs.memory.normalized-root-directoryThe roots
.and/always exist, andstat!returns aDirectoryentry for either. - std-fs.memory.relative-root-list
list_dir!(Path("."))lists direct relative children with bare paths, such asa.md. - std-fs.memory.rename
rename!of a directory moves every entry under it as well. - std-fs.memory.no-hostA
MemoryFsnever reads or changes the host's file system.
Its failures are these, and it reports no other. Each error holds the
path that fails, which for a rename is to unless from is missing:
| Rule | Operation | Error |
|---|---|---|
| std-fs.memory.error.not-foundNothing there | reading, listing, removing, or renaming a path where no entry is | NotFound(path), with the missing path |
| std-fs.memory.error.is-directoryA directory as a file | reading a directory, or writing, appending, or renaming a file to a directory's path | IsADirectory(path) |
| std-fs.memory.error.not-directoryA file as a directory | listing a file, creating a directory at a file's path, renaming a directory onto a file, or writing, appending, creating, or renaming to a path below a file | NotADirectory(path) |
| std-fs.memory.error.not-emptyA directory with entries | removing it | Other(message) |
| std-fs.memory.error.utf8Not text | read_text! of content that is not valid UTF-8 |
InvalidUtf8(path), as std-fs.read.text states |
Why. A test states the files its code reads, and reads back what the code wrote, with nothing on disk to set up or clean. Go's
fstest.MapFSserves tests the same way.
See also: Path, Mutable Providers.
#Scripted Modification Times
MemoryFs records only the times a test supplies; it has no clock.
- std-fs.memory.modified-noneA
MemoryFsentry hasmodifiedequal to.Noneuntil its time is scripted. - std-fs.memory.set-modified
set_modified(&mut self, path: Path, time: Timestamp) -> Result[void, FsError]sets an existing file's or directory's modification time. - std-fs.memory.modified-missingSetting a time on a missing path returns
.Err(FsError.NotFound(path))with the supplied path. - std-fs.memory.modified-writeA successful
write_bytes!,write_text!, orappend_text!clears that entry's scripted time to.None. - std-fs.memory.modified-renameRenaming an entry moves its scripted time, including the times of descendants moved with a directory.
- std-fs.memory.modified-removeRemoving an entry removes its scripted time.
Why. A test can script deterministic ordering without reading a clock. After a write, retaining an old scripted time could hide stale metadata, so the new time is unknown until the test supplies it again.