# Path — small Unix-style path helpers for Bend. # Publish entry for this package. Depends only on Base. # # Paths are plain Base Strings. v1 uses '/' regardless of host platform. # Path.segments/split discard empty slash-delimited components; normalize # therefore collapses repeated slashes and removes a trailing slash (except # for the root path). '.' and '..' are ordinary components in this light # normalizer; this package does not touch the filesystem. # # Base already provides String.split, String.join, String.append and String # prefix/suffix queries. Path keeps those primitives and adds path-aware # wrappers with root, basename, dirname and extension semantics. import Base # ---- segment views -------------------------------------------------------- # Non-empty Unix path components. Base provides String.split and List.filter; # the predicate only removes boundary/repeated-slash empty components. def Path.segments(path: String) -> List<&2, String>: List.filter(~String, ~(s => Bool.not(String.is_empty(s))), String.split(path, '/')) def Path.split(path: String) -> List<&2, String>: Path.segments(path) def Path.is_absolute(path: String) -> Bool: match path: case SNil{}: False{} case SCon{h, t}: Char.is_eq(h, '/') # ---- normalization and joining ------------------------------------------- def Path.normalized_body(path: String) -> String: String.join(Path.segments(path), "/") def Path.normalize.body(body: String, absolute: Bool, empty: Bool) -> String: match absolute empty: case False{} _: body case True{} True{}: "/" case True{} False{}: String.append("/", body) def Path.normalize(+path: String) -> String: +body = Path.normalized_body(path) Path.normalize.body(body, Path.is_absolute(path), String.is_empty(body)) def Path.join.empty(+a: String, +b: String, a_empty: Bool, b_empty: Bool) -> String: match a_empty b_empty: case True{} True{}: "" case True{} False{}: Path.normalize(b) case False{} True{}: Path.normalize(a) case False{} False{}: Path.normalize(String.append(a, String.append("/", b))) def Path.join.if(+a: String, +b: String, absolute: Bool) -> String: match absolute: case True{}: Path.normalize(b) case False{}: Path.join.empty(a, b, String.is_empty(a), String.is_empty(b)) def Path.join(+a: String, +b: String) -> String: Path.join.if(a, b, Path.is_absolute(b)) def Path.join_all.go(xs: List<&2, String>, acc: String) -> String: match xs: case Nil{}: acc case h <> t: Path.join_all.go(t, Path.join(acc, h)) def Path.join_all(xs: List<&2, String>) -> String: Path.join_all.go(xs, "") # ---- final components ----------------------------------------------------- def Path.basename.go(xs: List<&2, String>, last: String) -> String: match xs: case Nil{}: last case h <> t: Path.basename.go(t, h) def Path.basename(path: String) -> String: Path.basename.go(Path.segments(path), "") def Path.dirname.parts(xs: List<&2, String>) -> List<&2, String>: match xs: case Nil{}: Nil{} case h <> Nil{}: Nil{} case h <> t: h <> Path.dirname.parts(t) def Path.dirname.body(body: String, absolute: Bool, empty: Bool) -> String: match absolute empty: case True{} True{}: "/" case True{} False{}: String.append("/", body) case False{} _: match empty: case True{}: "." case False{}: body def Path.dirname(+path: String) -> String: +body = String.join(Path.dirname.parts(Path.segments(path)), "/") Path.dirname.body(body, Path.is_absolute(path), String.is_empty(body)) # ---- extension ------------------------------------------------------------ def Path.extension.body.if(suffix: String, empty: Bool) -> String: match empty: case True{}: "" case False{}: String.append(".", String.reverse(suffix)) def Path.extension.body.parts(parts: List<&2, String>) -> String: match parts: case Nil{}: "" case +suffix <> t: Path.extension.body.if(suffix, String.is_empty(suffix)) def Path.extension.body(basename: String) -> String: Path.extension.body.parts(String.split(String.reverse(basename), '.')) # A leading dot alone names a hidden file, not an extension. A later dot in # that name still starts an extension (e.g. .profile.bak -> .bak). def Path.extension.hidden(+basename: String) -> Bool: Bool.and( String.starts_with(basename, "."), Bool.not(String.contains(String.drop(basename, 1n), "."))) def Path.extension.if(+basename: String, hidden: Bool, has_dot: Bool) -> String: match hidden has_dot: case True{} _: "" case False{} False{}: "" case False{} True{}: Path.extension.body(basename) def Path.extension(path: String) -> String: +b = Path.basename(path) Path.extension.if(b, Path.extension.hidden(b), String.contains(b, "."))