Path
Every file a program touches is named by a path: Bin/reports/summary.txt. A path looks like a string, and the first lesson of this part is why it is not one. The Path package gives paths their own types, and this lesson takes one apart — into its components, its file name, stem and extension, and its parent — without ever asking the disk whether the file exists.
A path is not text
The operating system decides what a file name may hold, and its answer is not "valid text". On Windows a name is a run of 16-bit units that need not form valid UTF-16; on Linux and macOS it is bytes that need not form valid UTF-8. A type that insisted on text could not name every file that exists. So the Path package has its own types, built on the units the system uses:
| Type | What it is |
|---|---|
OsString | owns a name in the system's own units; needs an allocator |
OsStringView | a borrowed, read-only look at such units |
Path | a borrowed view with path meaning attached: components, parent, extension |
PathBuffer | an owned path that can grow — the subject of Path join |
Text goes in through OsString::FromText, which converts it to the system's units and so needs memory — an Allocator, the interface from Allocator:
var system = SystemAllocator();
let allocator: Allocator = system;
var holder = OsString::FromText(allocator, "Bin//reports/summary.txt")?;
let path = Path::FromView(holder.View());
FromText returns OsString ! TextError, so the ? passes a failure on, and Main is fallible — func Main() -> ! TextError, as in Fallible main. The OsString is the holder: it owns the units. path is only a view of them, so the holder must stay alive for as long as the path is used.
flowchart LR
t["text<br/>char8[..]"] -- "OsString::FromText(…)?" --> h["OsString<br/>owns the units"]
h -- ".View()" --> v["OsStringView"]
v -- "Path::FromView" --> p["Path<br/>a view with path meaning"]
p -- "Components, FileName,<br/>Stem, Extension, Parent" --> parts["more views<br/>of the same units"]
p -- ".AsView().ToText(…)?" --> s["String<br/>text again"]Components
A path is a sequence of components, and the separators between them belong to the platform: Windows accepts / and \, the others only /. Components returns an iterator, so for walks it:
for part in path.Components() {
Print(" [{}]", part);
}
The output is [Bin] [reports] [summary.txt]. A run of separators counts as one, so the doubled slash yields no empty component — which is exactly what splitting the text on "/" by hand would get wrong.
Named parts may be missing
FileName, Stem and Extension each return an OsStringView?, because each can be absent: a root such as / has no file name, and Makefile has no extension. ShowPart matches the optional before printing:
func ShowPart(label: char8[..], part: OsStringView?) {
match part {
name? => PrintLine("{:10} {}", label, name),
none => PrintLine("{:10} (none)", label)
}
}
| Call | For Bin//reports/summary.txt |
|---|---|
FileName() | summary.txt |
Stem() | summary |
Extension() | txt — without the dot |
Parent() | Bin//reports |
Parent drops the last component and returns a Path?, absent once there is nothing left to drop. Here ?? supplies an empty Path() in that case:
let parent = path.Parent() ?? Path();
The parent keeps the doubled slash: these calls only pick out part of the units, and never rewrite them. Tidying a path is the job of Path normalize.
Back to text
Printing with {} always works: any unit that is not text is swapped for the replacement character U+FFFD. That is fine for a message and useless for reopening the file. The exact way back is ToText, and it is fallible, because a name from the system may not be text at all:
let text = path.AsView().ToText(allocator)?;
Here it succeeds, because this name began as text. A name read from a directory listing comes with no such guarantee.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A path looks like a string and is not one.
//
// The operating system decides what a file name may hold, and its answer is not "valid text".
// On Windows a name is 16-bit units that need not form valid UTF-16; on Linux and macOS it is
// bytes that need not form valid UTF-8. A type that insisted on text could not name every file
// that exists. So the `Path` package has `OsString`, which holds the units the system holds,
// and `Path`, a borrowed view of them with path meaning attached.
//
// That meaning is structure. A path is a sequence of components, and the separators between
// them belong to the platform: Windows accepts `/` and `\`, the others only `/`. Runs of
// separators count as one, so splitting the text on "/" by hand would invent empty pieces.
// Nothing here asks the filesystem anything: a path need not name a file that exists.
//
// Text goes in through `OsString::FromText` and comes back out through `ToText`. Both return
// `T ! TextError`, and the way out is the one that can really fail: a name from the system may
// not be text at all. `{}` shows a path anyway, swapping any unit that is not text for U+FFFD,
// which is fine for a message and useless for reopening the file.
import Allocator::{ Allocator, SystemAllocator };
import Io::{ Print, PrintLine };
import Path::{ OsString, OsStringView, Path };
import Text::TextError;
// The named parts are optional: a root has no file name, and `Makefile` has no extension.
func ShowPart(label: char8[..], part: OsStringView?) {
match part {
name? => PrintLine("{:10} {}", label, name),
none => PrintLine("{:10} (none)", label)
}
}
func Main() -> ! TextError {
var system = SystemAllocator();
let allocator: Allocator = system;
var holder = OsString::FromText(allocator, "Bin//reports/summary.txt")?;
let path = Path::FromView(holder.View());
PrintLine("{:10} {}", "path", path);
// `Components` is an iterator, so `for` walks it. The doubled slash yields no empty part.
Print("{:10}", "components");
for part in path.Components() {
Print(" [{}]", part);
}
PrintLine();
ShowPart("file name", path.FileName());
ShowPart("stem", path.Stem());
ShowPart("extension", path.Extension());
// `Parent` drops the last component, and is absent once there is nothing left to drop.
let parent = path.Parent() ?? Path();
PrintLine("{:10} {}", "parent", parent);
// The exact conversion back to text. It succeeds here because the name began as text.
let text = path.AsView().ToText(allocator)?;
PrintLine("{:10} {}", "as text", text);
}
Besides Io, its Rux.toml lists Allocator, Path and Text under [Dependencies].
Run it
cd Examples/Files/Path
rux run
path Bin//reports/summary.txt
components [Bin] [reports] [summary.txt]
file name summary.txt
stem summary
extension txt
parent Bin//reports
as text Bin//reports/summary.txt
Common mistakes
? after FromText.Without it,
holder is the whole OsString ! TextError, not the string, and holder.View() fails with error: type 'OsString ! TextError' has no field 'View'. Unwrap the fallible first.? in a Main that returns int.? needs somewhere to send the failure. In func Main() -> int, it fails with error: '?' propagates native fallible 'OsString ! TextError', but the enclosing function returns 'int', and the help suggests the fix: declare the error channel, as Main here does with -> ! TextError.Path::FromView("Bin/notes.txt") fails with error: argument 1 to 'Path::FromView' has type 'char8[..]', but parameter 'view' requires 'OsStringView'. Text never becomes a path on its own; it goes through OsString::FromText first.PrintLine("{}", path.Parent()) fails with variadic parameter 'args' requires 'Display', naming 'Path?'. An optional has no text of its own; match it or supply a fallback with ??."C:\Users\me" fails to compile with error: escape sequence '\U' is not recognized, because \ starts an escape. Write "C:\\Users\\me", or simply use /, which Windows accepts too.Try it yourself
- Change the path to
archive.tar.gzand predict the stem and extension before you run it. - Try
.gitignoreandMakefile. Which parts arenone, and which are just short? - Walk up the tree: call
Parentin a loop, printing each path, until it returnsnone(current = current.Parent() ?? break;). What is the last path printed before that? - Print the number of components by counting them in the
forloop.
Learn more
Overview
Paths, files and directories with the Path and FileSystem packages: ten lessons on taking paths apart and building them, reading and writing text and binary files, listing directories, buffering, and replacing files safely.
19.2 Path join
Join paths with Join and Push, which place separators correctly, instead of gluing strings.