use fspath/fspath
let p = fspath.normalize("a/./b/../c") # "a/c"
let abs = fspath.absolute("src/main.dawn", "/srv") # "/srv/src/main.dawn"
let dir = fspath.parent(abs) # Some("/srv/src")
let ext = fspath.extension("main.dawn") # Some("dawn")
The separator is /. Nothing here reads the filesystem or the working
directory: absolute takes its base as an argument.
| Function | What it answers |
|---|---|
is_absolute(p) | whether p starts at the root |
normalize(p) | p without empty segments, . segments and name/.. pairs; a .. above the root is dropped, a leading .. on a relative path is kept |
absolute(p, base) | p resolved against base when relative, then normalized |
parent(p) | everything above p, or None for a bare name or the root |
join(base, p) | p under base; an absolute p ignores base |
basename(p) | the last segment, "" for the root and the empty path |
extension(p) | the extension without its dot, or None; .gitignore has none |
with_extension(p, ext) | p with its extension replaced; an empty ext removes it |
Changes between versions: CHANGELOG.md.
API#
fspath/fspath
fspath.normalize
fn normalize(p: String) -> String
p with empty segments, . segments and name/.. pairs removed.
A .. that would climb past the root is dropped (/../a is /a), because
the kernel resolves the root's parent to the root. A leading .. on a
relative path is kept -- there it still means something. Both match
java.nio.file.Path.normalize, which scripts/path-contract checks; the
root-climbing case was written down here as a divergence first and measured
as agreement second.
fspath.absolute
fn absolute(p: String, base: String) -> String
p resolved against base when it is relative, then normalized. base is
the working directory at the call site -- passed in rather than read here,
because reading it is an effect and normalizing is not.
Subject first, like every other function in this module. It used to read
absolute(base, p), which put the one path it does not always look at in
front of the one it always does, and was the module's only disagreement with
itself (audit RD-09).
fspath.parent
fn parent(p: String) -> Option[String]
Everything above p, or None when p is a bare name or the root itself.
p is normalized first, so a/b/../c has parent a.
fspath.join
fn join(base: String, p: String) -> String
p resolved against directory base, normalized.
An absolute p ignores base entirely -- the same rule absolute and the
shell's cd follow -- and an empty base or p leaves the other one
normalized rather than growing a stray separator. This is the function the
41 ++ "/" ++ sites wanted: io.list_dir answers with bare names, and a
bare name joined onto a directory is the shape every one of them had.
fspath.basename
fn basename(p: String) -> String
The last segment of p: its name, with every directory above it removed.
p is normalized first, so a/b/ and a/b/. both name b.
"" for the root and for the empty path, which are the two paths that have
no name. Not Option: every call site here holds a path it just built or
just listed, and a branch none of them can take is a branch all of them
would have to write.
fspath.extension
fn extension(p: String) -> Option[String]
The extension of basename(p), written without its dot, or None.
A dot only starts an extension when there is a name before it and something
after it: .gitignore is a dotfile and not an extension, and x. has none
either. Inquiry (spec §4.8, criterion 2) -- "this file has no extension" is
a case every caller distinguishes, which is precisely what the two - 5
sites could not.
fspath.with_extension
fn with_extension(p: String, ext: String) -> String
p (normalized) with its extension replaced by ext, itself written
without a dot. A path with no extension gains one, and an empty ext
removes it -- so with_extension(p, "") is how you drop a suffix without
counting its characters.