path

tape
import path from "path";

let p: string = path.join("assets", "logo.png");   // "assets/logo.png" ("assets\logo.png" on Windows)
let name: []const u8 = path.basename(p);          // "logo.png"
let kind: []const u8 = path.ext(p);               // ".png"
let thumb: string = path.with_ext(p, ".thumb.png");

path works on paths as text. It never looks at the file system, so it can tell you that a/b/../c is a/c but not whether a/c exists (that’s fs). It is a pure module.

Separators

Paths follow the rules of the platform you compile for:

  • Linux, macOS, Uusi: / separates; a path starting with / is absolute.
  • Windows: \ and / both separate, and path writes \. A path can start with a drive (C:\) or a network share (\\server\share\).
ItemIs
path.SEP: u8The separator path writes: \ (92) on Windows, / (47) elsewhere
path.is_separator(c: u8) -> booltrue for /, and on Windows also for \
path.is_absolute(p) -> booltrue when p doesn’t depend on the current directory

On Windows, C:\x and \\server\share\x are absolute, but \x (the current drive’s root) and C:x (relative to drive C’s current directory) are not.

tape
let abs: bool = path.is_absolute("/usr/lib");   // true on Linux and macOS
let rel: bool = path.is_absolute("src/main.tape");   // false everywhere

Taking paths apart

FunctionReturns
basename(p) -> []const u8The last component: "b.txt" for "a/b.txt"
dirname(p) -> []const u8Everything before it: "a" for "a/b.txt"
ext(p) -> []const u8The extension with its dot: ".txt"
stem(p) -> []const u8The last component without its extension: "b"
components(p) -> ComponentIterThe root, then each component, for for

The results are views into p. A trailing separator doesn’t matter: "a/b/" has the basename "b". When there’s no component left the result is empty, and dirname never goes above the root:

tape
path.dirname("a/b/c");      // "a/b"
path.dirname("a");          // ""
path.dirname("/a");         // "/"
path.basename("/");         // ""
path.ext("archive.tar.gz"); // ".gz"
path.stem("archive.tar.gz");// "archive.tar"
path.ext(".bashrc");        // "" (a leading dot isn't an extension)

components skips empty parts (from // or a trailing /) but keeps . and ..:

tape
for (part in path.components("/usr//local/./bin/")) {
    // "/", "usr", "local", ".", "bin"
}

Building paths

FunctionReturns
join(a, b) -> stringb relative to a, with one separator between them
with_ext(p, e) -> stringp with its extension replaced by e
normalize(p) -> stringp with ., .. and repeated separators resolved

join adds a separator only when a doesn’t already end with one. When b is absolute (or has any root), the result is just b; when either side is empty, it’s the other one.

tape
path.join("src", "main.tape");      // "src/main.tape"
path.join("src/", "main.tape");     // "src/main.tape"
path.join("src", "/etc/hosts");     // "/etc/hosts"

with_ext takes the extension with its dot (as ext returns it), and "" removes it. It stops the program with a trap if e doesn’t start with ., contains a separator, or if p has no file name ("", "/", "..").

tape
path.with_ext("notes/todo.txt", ".md");   // "notes/todo.md"
path.with_ext("build/app", ".exe");       // "build/app.exe"
path.with_ext("photo.jpeg", "");          // "photo"

normalize cleans a path up without the file system: it removes ., lets .. cancel the component before it, collapses repeated separators, drops a trailing separator, and on Windows writes every separator as \. A .. that would go above the root is dropped; at the start of a relative path it stays. An empty result is ".".

tape
path.normalize("a/./b/../c/");   // "a/c"
path.normalize("../x/../y");     // "../y"
path.normalize("/../etc");       // "/etc"
path.normalize("a/..");          // "."

Because it doesn’t read the disk, normalize("link/..") is "." even if link is a symbolic link to another directory.

Last modified: