Path API
Pure-string utilities for manipulating filesystem paths portably across operating systems.
Overview
The Path API is a namespace of pure-string path utilities, registered at VM startup as the global identifier Path. All methods are invoked as Path.method(...), and every method is a string-in / string-or-bool-out transformation. Path never accesses the filesystem. Anything that asks “does it exist?” / “is it readable?” / “how big is it?” lives in File (File.exists, File.size, File.modificationTime) and Dir (Dir.exists, Dir.list, Dir.makeRecursive). Path is exclusively a string library.
For OS-specific values that feed these utilities (user home, data/config/cache directories, executable path, environment variables) see System. Path.join(System.dataDir(), ...) is the recommended pattern for building portable per-user output paths.
Conventions
Strings only. Every input is a string; every result is a string or boolean. Bad argument types raise a Zym runtime error of the form Path.method(args) expects a string.
No filesystem I/O. Path.normalize does not resolve symlinks or check existence; it is purely textual .. / . / multi-slash collapse. To follow symlinks or canonicalize against the live filesystem, combine Path.normalize with Dir.exists / File.exists checks.
POSIX-style names. “basename” means different things in different ecosystems, so this API exposes both forms under explicit names: stem for the trailing component minus extension, and basename for the whole path minus extension. dirname / filename / extension follow POSIX basename, Python os.path, and Node path conventions.
Cross-platform separator. Methods that build paths use the host platform's native separator (/ on Linux/macOS, \ on Windows). A literal / in a script is accepted as a separator on every platform for path input, but Path.separator() is the source of truth for the output separator on the current host.
Querying Paths
Returns true if p starts at a root (Unix /foo, Windows C:\foo).
Returns the inverse of isAbsolute. The empty string returns true, since it is relative.
Returns true for UNC paths like //server/share or \\server\share.
Path.isAbsolute("/etc/hosts") // true Path.isAbsolute("logs/run.txt") // false Path.isRelative("") // true (empty string is relative) Path.isNetworkShare("//server/share") // true (UNC path)
Splitting Paths
Returns everything up to (but not including) the trailing component. Path.dirname("/a/b/c.txt") returns "/a/b"; Path.dirname("/") returns "/".
Returns the trailing component. Path.filename("/a/b/c.txt") returns "c.txt"; Path.filename("/a/b/") returns "".
Returns the rightmost extension, without the leading dot: "/a/b.tar.gz" yields "gz". Returns an empty string if there is no extension.
Returns the trailing component minus the rightmost extension. "/a/b/c.txt" yields "c"; "/a/b.tar.gz" yields "b.tar".
Returns the whole path minus the rightmost extension. "/a/b/c.txt" yields "/a/b/c". Use stem for only the trailing component without its extension.
archive.tar.gz report "gz", with stem = "archive.tar". This matches POSIX and Python conventions.
var p = "/var/log/zym/run.2024-01-15.log" print("dir : %s", Path.dirname(p)) // /var/log/zym print("file: %s", Path.filename(p)) // run.2024-01-15.log print("stem: %s", Path.stem(p)) // run.2024-01-15 print("ext : %s", Path.extension(p)) // log
Building Paths
Joins any number of string segments into one path using the host platform's native separator. Empty segments are skipped. If a later segment is itself absolute, it rebases the result, matching POSIX os.path.join. Calling Path.join() with no arguments returns "".
Path.join("a", "", "b") // "a/b" on Linux/macOS Path.join("a/b", "/etc") // "/etc" (absolute segment rebases) Path.join() // ""
Textually resolves . and .. segments, collapses doubled separators, and trims a trailing separator. Does not touch the filesystem: symbolic links are not resolved, ~ is not dereferenced, and existence is never checked. Compose with Path.expandUser and Dir.exists / File.exists for richer behavior.
Returns the relative path from from to to. from is treated as a file: its parent directory is the basis for the result. Path.relative("/a/b", "/a/c") returns "../c". For directory-to-directory relative paths, append an explicit trailing component to from. See the worked example below.
from(string) — starting path, treated as a fileto(string) — destination path
Replaces (or appends) the rightmost extension.
ext(string) — new extension; accepts both"md"and".md", and""drops the extension
var out = Path.withExtension("/tmp/data.json", "min.json.gz") // out -> "/tmp/data.min.json.gz"
Expands a leading ~ or ~/... to the current user's home directory ($HOME on Unix, %USERPROFILE% or %HOMEDRIVE%%HOMEPATH% on Windows). Non-tilde paths pass through unchanged.
~user form is intentionally not expanded. The host has no portable way to resolve another user's home from script-side without shelling out. If needed, drive getent passwd <user> via Process.exec.
var cfg = Path.normalize(Path.expandUser("~/.config/my-tool")) // cfg -> "/home/me/.config/my-tool" on Linux
Separator
Returns the host platform's path separator: "/" on Linux/macOS, "\\" on Windows. Rarely needed in scripts that use Path.join.
Examples
Build a Per-User Output Path
var dir = Path.join(System.dataDir(), "my-tool") Dir.makeRecursive(dir) File.writeAllText(Path.join(dir, "config.json"), "{}")
Compute a Relative Path
var rel = Path.relative("/srv/app/web/index.html", "/srv/app/static/logo.svg") // rel -> "../static/logo.svg" // relative() treats the from argument as a file. To compare two // directories, append a sentinel trailing component: var fromDir = "/srv/app/web" rel = Path.relative(Path.join(fromDir, ".sentinel"), "/srv/app/static/logo.svg") // rel -> "../static/logo.svg"