std/path¶
Pure path string manipulation on the POSIX / separator. Every function
here is plain string work. Nothing in this module touches the disk: it
never checks whether a file exists, never reads a directory, never
resolves a real path. For anything that talks to the filesystem, reach for
std/fs instead.
import std/path { join, basename, dirname, extension, stem, is_absolute }
fun main() {
let p = join("src", "main.rv") // src/main.rv
print(dirname(p)) // src
print(basename(p)) // main.rv
print(extension(p)) // rv
}
Importing¶
The functions have no natural single receiver, so they are free functions brought in by a selective import:
Pull in only the names you use. The module is built on std/string's
String methods, and the import merges that dependency transitively, so
you do not need a separate import std/string.
The path model¶
A path is a String whose components are separated by /. That is the
only separator this module understands. Windows backslash (\) paths,
drive letters, and UNC paths are out of scope: a \ is treated as an
ordinary path byte, not a separator.
Indices are byte offsets, consistent with std/string. Paths are assumed
to be valid UTF-8 with the separator and the dot appearing only as their
own single-byte ASCII forms.
Surface¶
| Function | Result | Summary |
|---|---|---|
join(a: String, b: String) -> String |
String |
join two parts with a single / |
basename(p: String) -> String |
String |
final component after the last / |
dirname(p: String) -> String |
String |
everything up to the last / |
extension(p: String) -> String |
String |
text after the last . in the basename |
stem(p: String) -> String |
String |
basename without its extension |
is_absolute(p: String) -> Bool |
Bool |
whether p starts with / |
is_relative(p: String) -> Bool |
Bool |
whether p does not start with / |
components(p: String) -> List<String> |
List<String> |
the non-empty /-separated segments |
normalize(p: String) -> String |
String |
collapse ., .., and redundant slashes |
with_extension(p: String, ext: String) -> String |
String |
replace the extension |
Building a path¶
join(a: String, b: String) -> String¶
Join two path parts with a single /. The result never doubles the
separator: it stays one / whether a ends with a slash, b begins with
one, or both. If either operand is empty, the other is returned unchanged.
import std/path { join }
fun main() {
print(join("src", "main.rv")) // src/main.rv
print(join("src/", "main.rv")) // src/main.rv
print(join("src", "/main.rv")) // src/main.rv
print(join("src/", "/main.rv")) // src/main.rv
print(join("", "main.rv")) // main.rv
print(join("src", "")) // src
}
Decomposing a path¶
basename(p: String) -> String¶
The final component, everything after the last /. When p contains no
/, the whole string is the basename.
import std/path { basename }
fun main() {
print(basename("src/lib/main.rv")) // main.rv
print(basename("main.rv")) // main.rv (no slash)
print(basename("/etc/hosts")) // hosts
}
dirname(p: String) -> String¶
Everything up to the last /. Two edge cases are worth remembering:
- When there is no
/,dirnamereturns"."(the current directory). - When the only
/is at index 0, it returns"/"(the root).
import std/path { dirname }
fun main() {
print(dirname("src/lib/main.rv")) // src/lib
print(dirname("main.rv")) // . (no slash)
print(dirname("/hosts")) // / (slash only at index 0)
}
extension(p: String) -> String¶
The substring after the last . in the basename, or "" when there is no
dot. The dot is inspected on the basename only, so a . in a parent
directory name does not count. A leading dot marks a dotfile, not an
extension: the extension of .gitignore is "".
import std/path { extension }
fun main() {
print(extension("archive.tar.gz")) // gz (last dot wins)
print(extension("README")) // "" (no dot)
print(extension(".gitignore")) // "" (leading dot, a dotfile)
print(extension("a.b/main")) // "" (dot is in the directory part)
}
stem(p: String) -> String¶
The basename with its extension removed: the part extension leaves
behind. For a name without an extension, the whole basename is the stem,
and a dotfile keeps its leading dot.
import std/path { stem }
fun main() {
print(stem("src/main.rv")) // main
print(stem("archive.tar.gz")) // archive.tar (only the last dot)
print(stem("README")) // README (no extension)
print(stem(".gitignore")) // .gitignore (dotfile, no extension)
}
is_absolute(p: String) -> Bool¶
True when p starts with /. The empty string is relative.
import std/path { is_absolute }
fun main() {
print(is_absolute("/usr/bin")) // true
print(is_absolute("usr/bin")) // false
print(is_absolute("")) // false
}
is_relative(p: String) -> Bool¶
True when p does not start with /. The exact complement of
is_absolute, so the empty string is relative.
import std/path { is_relative }
fun main() {
print(is_relative("usr/bin")) // true
print(is_relative("/usr/bin")) // false
print(is_relative("")) // true
}
components(p: String) -> List<String>¶
The non-empty segments of p, splitting on /. Leading, trailing, and
repeated separators produce no empty entries, so a root path and a bare name
both reduce to their real parts.
import std/path { components }
fun main() {
let parts = components("/a/b/c") // ["a", "b", "c"]
for part in parts {
print(part)
}
}
Resolving a path¶
normalize(p: String) -> String¶
Collapse . segments, resolve .. against the preceding segment, and remove
redundant slashes. A leading / is preserved. The result is pure string work:
no .. is resolved against the real filesystem.
import std/path { normalize }
fun main() {
print(normalize("/a/b/../c/./d")) // /a/c/d
print(normalize("a//b/")) // a/b
}
with_extension(p: String, ext: String) -> String¶
Replace the extension of p with ext (no leading dot). When p has no
extension, ext is appended. An empty ext removes the extension.
import std/path { with_extension }
fun main() {
print(with_extension("report.txt", "md")) // report.md
print(with_extension("report", "md")) // report.md
print(with_extension("report.txt", "")) // report
}
Worked example: rename a path's extension¶
This walks a path apart, swaps its extension, and joins it back together using only documented functions.
import std/path { join, dirname, stem, is_absolute }
fun with_extension(p: String, ext: String) -> String {
let dir = dirname(p)
let name = stem(p).concat(".").concat(ext)
if dir == "." {
return name
}
return join(dir, name)
}
fun main() {
print(with_extension("src/report.txt", "md")) // src/report.md
print(with_extension("notes.txt", "md")) // notes.md
print(is_absolute("/tmp/a")) // true
}
Relationship to std/fs¶
std/path and std/fs split cleanly along one line: std/path is
strings, std/fs is the disk.
std/pathanswers questions about the shape of a path string (dirname,basename,extension) and builds new ones (join). It never opens, reads, or stats anything, so its results hold even for paths that do not exist.- std/fs is where existence checks, reading, writing, and
directory listing live. When you need to know whether a path is really
on disk, use
std/fs.
A common pattern is to compute a target path with std/path, then hand
that string to std/fs to actually read or write it.
See also¶
- std/fs for filesystem access (existence, reading, writing).
- std/string for the
Stringmethods this module builds on.