/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/path/path.nv
227 строк
8 KB
Evgeniy Golovin
merge: comment-hygiene-2 — чистка комментариев std (batch 1-20) + линт-свип 330→13
01 авг 2026, 18:13
01 авг 2026, 18:13
bb8b33d
Код
Авторство
О чём код?
// stdlib/path.nv — path manipulation (Unix-style). // // API: // Path.join(parts ...str) -> str // Path.parent(p str) -> Option[str] // Path.basename(p str) -> str // Path.extension(p str) -> Option[str] // Path.normalize(p str) -> str // Path.is_absolute(p str) -> bool // // Использует "/" как separator. Windows paths требуют отдельной либы. //! Plan 45 Ф.35.4 — Unix-style path manipulation (slash-separated). //! //! Treats `"/"` as the separator. Not platform-dependent — Windows paths //! (backslash, drive letters) would need a separate module. All functions //! are pure (no I/O); the `Path` namespace groups the static methods. //! //! # Examples //! //! ```nova //! let p = Path.join(["/usr", "local", "bin"]) //! assert(p == "/usr/local/bin") //! assert(Path.parent(p) == Some("/usr/local")) //! assert(Path.basename(p) == "bin") //! ``` module path.path /// Join path components with a `/` separator. Variadic — takes positional /// args or a spread `...arr`. /// /// Stripping logic: /// - empty parts skipped; /// - a leading `/` on a non-first part is dropped (avoids `a//b`); /// - exactly one `/` is always inserted between non-empty components. /// /// # Examples /// ```nova /// assert(Path.join("a", "b", "c") == "a/b/c") // positional /// assert(Path.join(...["usr", "bin"]) == "usr/bin") // spread /// assert(Path.join("a", "/b") == "a/b") // strip leading / /// ``` #stable(since = "0.1") export fn Path.join(...parts []str) -> str { consume buf = StringBuilder.new() mut first = true for part in parts { if part.byte_len() == 0 { continue } // Между непустыми частями всегда вставляем "/"; для не-первой части, // которая сама начинается с "/", strip лидирующий — иначе вышло бы "//". // (Случай "/usr/" + "..." с trailing "/" не возникает в join'е — каждая // часть передаётся отдельной строкой без своего trailing slash.) if !first { buf.append('/') } ro cleaned = if !first && part.starts_with("/") { part[1..] } else { part } buf.append(cleaned) first = false } buf } /// The parent directory component. `None` if the path has no parent (`""`, `"/"`, bare basename). /// /// A trailing `/` is dropped before the search (i.e. `"/dir/"` treated as `"/dir"`). /// /// # Examples /// ```nova /// assert(Path.parent("/usr/bin/ls") == Some("/usr/bin")) /// assert(Path.parent("/usr") == Some("/")) /// assert(Path.parent("/") == None) /// assert(Path.parent("file.txt") == None) /// ``` #stable(since = "0.1") export fn Path.parent(p str) -> Option[str] { if p.byte_len() == 0 || p == "/" { return None } ro trimmed = if p.ends_with("/") { p[..p.byte_len() - 1] } else { p } match find_last_slash(trimmed) { None => None Some(0) => Some("/") // "/foo" → "/" Some(i) => Some(trimmed[..i]) } } /// Last path component (final filename / directory name). /// /// A trailing `/` is dropped before extraction. The root path `/` returns /// an empty string (rare case — usually `parent` is used first). /// /// # Examples /// ```nova /// assert(Path.basename("/usr/bin/ls") == "ls") /// assert(Path.basename("file.txt") == "file.txt") /// assert(Path.basename("/dir/") == "dir") /// ``` #stable(since = "0.1") export fn Path.basename(p str) -> str { ro trimmed = if p.ends_with("/") && p.byte_len() > 1 { p[..p.byte_len() - 1] } else { p } match find_last_slash(trimmed) { None => trimmed Some(i) => trimmed[i + 1..] } } /// File extension — the substring after the last `.` in the basename, without the dot. /// /// `None` if: /// - the basename is empty; /// - the basename is `"."` or `".."`; /// - a leading `.` (dotfile like `.bashrc` — treated as a name without an extension). /// /// Multi-dot files (e.g. `archive.tar.gz`) return only the last /// extension (`"gz"`). /// /// # Examples /// ```nova /// assert(Path.extension("file.txt") == Some("txt")) /// assert(Path.extension("archive.tar.gz") == Some("gz")) /// assert(Path.extension(".bashrc") == None) /// ``` #stable(since = "0.1") export fn Path.extension(p str) -> Option[str] { ro base = Path.basename(p) if base.byte_len() == 0 || base == "." || base == ".." { return None } match find_last_dot(base) { None => None Some(0) => None // dotfile (".bashrc") — без extension Some(i) => Some(base[i + 1..]) } } /// True if the path starts with `/` (absolute Unix path). /// /// # Examples /// ```nova /// assert(Path.is_absolute("/usr/bin")) /// assert(!Path.is_absolute("relative/path")) /// ``` #stable(since = "0.1") export fn Path.is_absolute(p str) -> bool => p.starts_with("/") /// Normalize path — collapse `.`, `..`, and multiple `/` separators. /// /// Rules: /// - `.` segments dropped; /// - `..` segments pop the previous segment (if any); /// - for absolute paths `..` at root is ignored (cannot go above `/`); /// - empty result → `"."` (current directory convention). /// /// **Does not** resolve symlinks (this is a pure-text operation; for filesystem-resolution /// an `IO` effect is needed). /// /// # Examples /// ```nova /// assert(Path.normalize("/a/b/../c") == "/a/c") /// assert(Path.normalize("//a//b//") == "/a/b") /// assert(Path.normalize("a/b/..") == "a") /// ``` #stable(since = "0.1") export fn Path.normalize(p str) -> str { ro absolute = p.starts_with("/") ro parts = p.split("/") mut stack []str = [] for part in parts { if part.byte_len() == 0 || part == "." { continue } if part == ".." { if stack.len() > 0 { // pop() возвращает Option[str]; результат не нужен — явно отбрасываем // (workaround compiler bug с разнотипными ветками if-expr в statement-position). ro _ = stack.pop() } else if !absolute { stack.push("..") } // если absolute — игнорируем .. наверх корня } else { stack.push(part) } } consume buf = StringBuilder.new() if absolute { buf.append('/') } mut first = true for s in stack { if !first { buf.append('/') } buf.append(s) first = false } ro result = buf.into_str() if result.byte_len() == 0 { "." } else { result } } // Helpers // `/` и `.` — ASCII байты; UTF-8 continuation/lead-байты многобайтовых // codepoint'ов никогда не совпадают с ASCII-диапазоном (self-sync), поэтому // прямой byte-scan корректен даже над non-ASCII путями — O(1) индекс вместо // ретрактированного `chars().nth(i)` O(n) скана (D260-амендмент). Байтовая // позиция при этом совместима с byte-range `s[a..b]` slicing, используемым // вызывающими функциями (в отличие от codepoint-индекса `nth`, который был // бы несовместим на non-ASCII путях). fn find_last_slash(s str) -> Option[int] { ro sb = s.bytes() mut i = s.byte_len() - 1 while i >= 0 { if sb[i] == ('/' as u8) { return Some(i) } i -= 1 } None } fn find_last_dot(s str) -> Option[int] { ro sb = s.bytes() mut i = s.byte_len() - 1 while i >= 0 { if sb[i] == ('.' as u8) { return Some(i) } i -= 1 } None } // Тесты — см. peer-файл path_test.nv (module path.path_test).