Finch/std/path.fch
Nexoniarz ea6d2a57e5 a standard library in Finch: path, time, proc, json
import json   import path   import time   import proc  -- found in std/ after the program's own files,
so a program's own json.fch always wins.

path: join, base, dir, ext, stem, parts, clean, is_abs.
time: now, millis, sleep, date, local, format, days_in_month (clock_gettime, strftime through C import).
proc: run, output (an error with the command's output when it fails), env, pid, cwd.
json: parse (errors with line and column), stringify, pretty, new_*, get/at/set/push/has/names and
str_or/int_or/float_or/bool_or; \u escapes with surrogate pairs.

The C importer now leaves a plain (non-const) char * parameter a ptr[char], a buffer C writes into;
only const char * is text going in. str(ptr) and str([]u8) are in the self-hosted compiler.

boot.sh: 40 passed, 0 failed.
2026-10-25 20:58:26 +01:00

128 lines
2.8 KiB
Text

// Paths, as text. Elements are separated by "/".
//
// import path
// path.join("a", "b.txt") a/b.txt
// path.base("/tmp/x/a.tar.gz") a.tar.gz path.stem(...) a.tar path.ext(...) .gz
// path.dir("/tmp/x/a.txt") /tmp/x
// path.clean("a//b/../c/./d") a/c/d
// a and b with one separator between; an absolute b replaces a
fn join(str a, str b) -> str {
if a.len == 0 {
return b
}
if b.len == 0 {
return a
}
if b[0] == '/' {
return b
}
if a[a.len - 1] == '/' {
return a + b
}
return a + "/" + b
}
fn is_abs(str p) -> bool {
return p.len > 0 && p[0] == '/'
}
// the last element: "a/b.txt" -> "b.txt", "a/b/" -> "b", "" -> ".", "/" -> "/"
fn base(str p) -> str {
if p.len == 0 {
return "."
}
end := p.len
while end > 1 && p[end - 1] == '/' {
end -= 1
}
if end == 1 && p[0] == '/' {
return "/"
}
start := end
while start > 0 && p[start - 1] != '/' {
start -= 1
}
return p.sub(start, end)
}
// everything before the last element: "a/b.txt" -> "a", "b.txt" -> ".", "/b" -> "/"
fn dir(str p) -> str {
end := p.len
while end > 1 && p[end - 1] == '/' {
end -= 1
}
i := end
while i > 0 && p[i - 1] != '/' {
i -= 1
}
if i == 0 {
return "."
}
while i > 1 && p[i - 1] == '/' {
i -= 1
}
return p.sub(0, i)
}
// the part of the last element from its last dot: "a.tar.gz" -> ".gz"; "" when there is none
fn ext(str p) -> str {
b := base(p)
i := b.len
while i > 0 && b[i - 1] != '.' {
i -= 1
}
if i <= 1 { // no dot, or a name that only starts with one (.bashrc)
return ""
}
return b.sub(i - 1, b.len)
}
// the last element without its extension
fn stem(str p) -> str {
b := base(p)
e := ext(p)
return b.sub(0, b.len - e.len)
}
// the elements of a path: "/a/b/c" -> ["a", "b", "c"]
fn parts(str p) -> []str {
[]str out
for piece in p.split("/") {
if piece.len > 0 {
out.push(piece)
}
}
return out
}
// the shortest path with the same meaning: no "//", no ".", and ".." resolved where it can be
fn clean(str p) -> str {
if p.len == 0 {
return "."
}
abs := is_abs(p)
[]str out
for piece in parts(p) {
if piece == "." {
continue
}
if piece == ".." {
if out.len > 0 && out[out.len - 1] != ".." {
out.pop()
} else if !abs {
out.push("..")
}
continue
}
out.push(piece)
}
joined := out.join("/")
if abs {
return "/" + joined
}
if joined.len == 0 {
return "."
}
return joined
}