path
stdlib/extra/path.hk: pure-Hanki slash path manipulation.
Lexical, POSIX-style ('/'-separated) path helpers for the release pipeline. Pure Hanki: no effects, no @intrinsic, no sys. Every function is string arithmetic, and the same answer therefore falls out on both tiers without touching the filesystem. Byte-offset semantics throughout (it inherits string's byte view), which is exact for ASCII paths.
Semantics follow Go's path package (the canonical slash-only lexical library): normalize is path.Clean, dirname is path.Dir, basename is path.Base. join inserts one separator at the seam and leaves ./.. for normalize to resolve: one job each. split_ext returns a PathSplit struct (Hanki has no tuple type).
PathSplit
struct PathSplit
root: string
ext: string
end
The two halves of a path split on its extension: root is everything before the extension (directory + stem), ext is the extension without its leading dot. A path with no extension has an empty ext.
join
def join(base: string, child: string) -> string
Join base and child with a single '/' separator. An empty operand yields the other; a leading '/' on child is treated as a separator at the seam and never a re-rooting, which leaves the result under base. Lexical only: call normalize to resolve any ./.. the join introduces.
join("a", "b") => "a/b"
join("a/", "b") => "a/b"
join("a", "/b") => "a/b"
join("", "b") => "b"
dirname
def dirname(p: string) -> string
The directory part of p, cleaned: everything up to the last '/', normalized. A path with no '/' has directory ".".
dirname("a/b/c.txt") => "a/b"
dirname("/etc") => "/"
dirname("readme") => "."
basename
def basename(p: string) -> string
The last path element of p, trailing slashes stripped. The empty path is ".", a path of only slashes is "/".
basename("a/b/c.txt") => "c.txt"
basename("a/b/") => "b"
basename("/") => "/"
normalize
def normalize(p: string) -> string
Lexically clean p: collapse runs of '/', drop "." elements, and resolve ".." against the preceding element (without ever escaping the root of an absolute path). Never touches the filesystem. The empty path cleans to ".".
normalize("a//b/../c") => "a/c"
normalize("/a/../..") => "/"
normalize("a/../../b") => "../b"
split_ext
def split_ext(p: string) -> PathSplit
Split p into its path-without-extension (root) and its extension without the leading dot (ext). The extension is the text after the last '.' in the basename; a '.' that is the basename's first byte (a dotfile like ".bashrc") or sits in a directory component is not an extension, so such paths get an empty ext.
split_ext("a/b.txt").root => "a/b"
split_ext("a/b.txt").ext => "txt"
split_ext("README").ext => ""
split_ext(".bashrc").ext => ""
stripleading_slash
def _strip_leading_slash(p: string) -> string
striptrailing_slashes
def _strip_trailing_slashes(p: string) -> string
lastindex_of
def _last_index_of(s: string, needle: string) -> Option<int>
Byte offset of the last occurrence of needle in s, or None.
cleansegs
def _clean_segs(segs: List<string>, rooted: bool) -> List<string>
Fold the '/'-split segments of a path into the cleaned element stack.
cleanpush
def _clean_push(acc: List<string>, seg: string, rooted: bool) -> List<string>
pushdotdot
def _push_dotdot(acc: List<string>) -> List<string>
A relative ".." pops the previous element, unless the stack is empty or its top is itself a ".." (those cannot be cancelled), where it is kept.
droplast
def _drop_last(lst: List<string>) -> List<string>