Directory
A directory holds names: files, and other directories. The FileSystem package makes one, lists what is in it and removes it, and this program does all three to a scratch directory of its own, Bin/scratch, inside the package's build folder. The interesting part is the listing, because a directory can hold a great many names, and reading it can fail part-way through.
Four calls
| Call | Does | Fails when |
|---|---|---|
MakeDirectory(allocator, path) | creates one directory | the name is already taken |
ReadDirectory(allocator, path) | starts a listing | the directory cannot be read |
DeleteFile(allocator, path) | removes one file | it is missing |
DeleteDirectory(allocator, path) | removes one empty directory | anything is still inside |
All four return fallibles with IoError on the failure side, and none of them touches more than the one name it is given.
The files go in with two small helpers. Each converts a name to an OsString, joins it onto the directory with Join, and acts on the result:
func MakeFile(allocator: Allocator, directory: Path, name: char8[..]) -> ! IoError | TextError {
var holder = OsString::FromText(allocator, name)?;
let path = Join(allocator, directory, holder.View())?;
var file = File::Open(allocator, path.AsPath(), OpenOptions::Writing())?;
WriteAll(file, name)?;
file.Close()?;
}
Listing, one name at a time
ReadDirectory does not return every name at once — that would mean holding them all at once. It returns a cursor, a DirectoryIterator, whose Next hands out one name per call:
func Next(self: &var DirectoryIterator) -> OsString? ! IoError
That is the nested shape from Nested fallible: two questions in one return type. The outer one is "could the directory be read?"; the inner one is "is there another name?". The loop answers both in one line:
var listing = ReadDirectory(allocator, directory)?;
var count = 0;
loop {
let name = listing.Next()? ?? break;
PrintLine(" {}", name);
count += 1;
}
flowchart LR
n["listing.Next()"] --> f{"failed?"}
f -- "yes" --> p["? passes the IoError on:<br/>Main ends with status 1"]
f -- "no" --> o{"a name, or none?"}
o -- "none" --> b["?? break:<br/>every name handed out"]
o -- "a name" --> use["bind it to name,<br/>print it, loop again"]
use --> n? strips the outer fallible, and ?? break strips the inner optional — leaving the loop when it is none. What is left is an OsString, a name in the system's own units, for the reason Path gave.
Three more things about a listing:
.and..are skipped; you get only real entries.- The order is whatever the filesystem keeps. This program happens to print
first.txtbeforesecond.txt; do not rely on it. Sort the names if order matters. - The cursor holds a system handle.
listing.Release()gives it back as soon as you are done, rather than at the end of the scope.
Removal is strict
DeleteDirectory refuses a directory that still has anything in it, so removing the scratch directory too early fails. This program expects that, and handles the failure rather than passing it on:
DeleteDirectory(allocator, directory) catch {
else => {
PrintLine("refused: the directory is not empty");
}
};
IoErrorKind has no case for "not empty", so the failure arrives as Other, with the system's own code beside it. That is why this catch takes every failure with else rather than matching one kind. Then the files go first, and the empty directory after them:
RemoveFile(allocator, directory, "first.txt")?;
RemoveFile(allocator, directory, "second.txt")?;
DeleteDirectory(allocator, directory)?;
Nothing here deletes a whole tree in one call. Removing a directory and everything below it means listing it, deleting each file, and descending into each subdirectory — which is exactly why it is not a single innocent-looking function.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A directory is made, listed and removed through the `FileSystem` package, and this program does
// all three to a scratch directory it owns: `Bin/scratch`, inside this package's build folder.
//
// Listing is the interesting part. `ReadDirectory` returns a cursor, and its `Next` hands out one
// name at a time, because a directory can hold a great many and returning them all at once would
// mean holding them all at once. `Next` has the nested shape from the Errors part:
//
// func Next(self: &var DirectoryIterator) -> OsString? ! IoError
//
// The outer failure means the directory could not be read. A success holds the next name, or
// `none` once every name has been handed out. So `listing.Next()? ?? break` passes a failure on,
// stops at the end, and otherwise binds a name. The names are `OsString`s, not text, for the
// reason the Path lesson gave; `.` and `..` are skipped. The order is whatever the filesystem
// keeps, so do not rely on it.
//
// Removal is strict. `MakeDirectory` fails if the name is taken, and `DeleteDirectory` refuses a
// directory that still has anything in it, so the files go first.
import Allocator::{ Allocator, SystemAllocator };
import FileSystem::{ DeleteDirectory, DeleteFile, File, MakeDirectory, OpenOptions, ReadDirectory };
import Io::{ IoError, PrintLine, WriteAll };
import Path::{ Join, OsString, Path };
import Text::TextError;
// Writes a small file called `name` inside `directory`.
func MakeFile(allocator: Allocator, directory: Path, name: char8[..]) -> ! IoError | TextError {
var holder = OsString::FromText(allocator, name)?;
let path = Join(allocator, directory, holder.View())?;
var file = File::Open(allocator, path.AsPath(), OpenOptions::Writing())?;
WriteAll(file, name)?;
file.Close()?;
}
// Deletes the file called `name` inside `directory`.
func RemoveFile(allocator: Allocator, directory: Path, name: char8[..]) -> ! IoError | TextError {
var holder = OsString::FromText(allocator, name)?;
let path = Join(allocator, directory, holder.View())?;
DeleteFile(allocator, path.AsPath())?;
}
func Main() -> ! IoError | TextError {
var system = SystemAllocator();
let allocator: Allocator = system;
var holder = OsString::FromText(allocator, "Bin/scratch")?;
let directory = Path::FromView(holder.View());
MakeDirectory(allocator, directory)?;
MakeFile(allocator, directory, "first.txt")?;
MakeFile(allocator, directory, "second.txt")?;
PrintLine("made {} with two files", directory);
var listing = ReadDirectory(allocator, directory)?;
var count = 0;
loop {
let name = listing.Next()? ?? break;
PrintLine(" {}", name);
count += 1;
}
// The cursor holds a system handle; `Release` gives it back now rather than at scope end.
listing.Release();
PrintLine("listed {} entries", count);
// Removing it too early is refused, and this failure is handled rather than passed on.
DeleteDirectory(allocator, directory) catch {
else => {
PrintLine("refused: the directory is not empty");
}
};
RemoveFile(allocator, directory, "first.txt")?;
RemoveFile(allocator, directory, "second.txt")?;
DeleteDirectory(allocator, directory)?;
PrintLine("removed {}", directory);
}
Besides Io, its Rux.toml lists Allocator, FileSystem, Path and Text under [Dependencies].
Run it
cd Examples/Files/Directory
rux run
made Bin/scratch with two files
first.txt
second.txt
listed 2 entries
refused: the directory is not empty
removed Bin/scratch
Common mistakes
?.listing.Next() ?? break fails with error: operator '??' cannot take 'OsString? ! IoError', and the note explains why: coalescing tests one optional level and would silently discard an error. Deal with the failure first: listing.Next()? ?? break.?? break.let name = listing.Next()?; leaves name an OsString?, and printing it fails with variadic parameter 'args' requires 'Display'. Without the ??, nothing ends the loop either.If a run stops part-way — a failure passed on with
?, or a change of yours — Bin/scratch may still exist, and the next run fails at once at MakeDirectory, with exit status 1 and no output. Delete the folder by hand and run again.The names come back in whatever order the filesystem keeps them, which differs between systems and can change. Sort them when the order matters.
Try it yourself
- Add a third file, and a subdirectory made with
MakeDirectory. What does the listing show for the subdirectory, and what must change before the scratch directory can be removed? - Call
MakeDirectorytwice on the same path and handle the second failure with amatchthat checks forIoErrorKind::AlreadyExists. - Convert each name to text with
name.View().ToText(allocator)?before printing it. When could that conversion fail, when printing with{}never does? - List the package's own
Binfolder instead of the scratch directory.
Learn more
- Nested fallible and Coalesce exit — the two halves of
Next()? ?? break - Path join — building each file's path inside the directory
- Metadata — asking whether a name is a file or a directory
- Temporary file — scratch space that removes itself