added: os `path` functions
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Sat, 17 Jun 2023 22:08:15 +0000 (17:08 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Sat, 17 Jun 2023 22:08:15 +0000 (17:08 -0500)
CHANGELOG
core/os/path.onyx
tests/stdlib/os_path [new file with mode: 0644]
tests/stdlib/os_path.onyx [new file with mode: 0644]

index fdc946fac7aa47f407762c1460a118c3b51216e6..a4bde1bf53d638d88e6962d04bc841dfe3fc875f 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -18,6 +18,10 @@ Additions:
     - Quickly convert a byte array to and from its hex equivalent.
 - `Allocator.move`. Moves a value into an allocator, returning a pointer to it.
     - This is a copy operation (and might be renamed later)
+- `os.path_clean`
+- `os.path_directory`
+- `os.path_extension`
+- `os.path_split`
 
 Removals:
 - Remove old syntax for quoted blocks, `#quote` and `#()`.
index 9ecdb3d479f5b7d29796607ad475a7787c75ad6f..d0d8312bf8a144024f5b6008ced84378e5d2b843 100644 (file)
@@ -4,24 +4,118 @@ use runtime
 use core.string
 use core.conv
 
-#if runtime.compiler_os == .Windows {
-    PATH_SEP :: '\\'
-} else {
-    PATH_SEP :: '/'
+#local Path_Allocator :: context.temp_allocator
+
+PATH_SEP :: '/'
+// #if runtime.compiler_os == .Windows {
+//     PATH_SEP :: '\\'
+// } else {
+// }
+
+#doc """
+    Removes:
+    - Stray '.' in the path
+    - Stray '..'
+    - Repeated '/'
+    - Trailing '/'
+
+    Modifies the string in place, as the length will never be longer.
+"""
+path_clean :: (path: str, allocator := Path_Allocator) -> str {
+    if path == "" do return string.alloc_copy(".", Path_Allocator);
+
+    rooted := path[0] == PATH_SEP;
+    n := path.length;
+
+    out := make(dyn_str, path.length, allocator=context.temp_allocator);
+    r, dotdot := 0, 0;
+
+    if rooted {
+        string.append(&out, "/");
+        r, dotdot = 1, 1;
+    }
+
+    while r < n {
+        if     path[r] == '/' do r += 1;
+        elseif path[r] == '.' && (r+1 == n || path[r + 1] == '/') do r += 1;
+        elseif path[r] == '.' && path[r + 1] == '.' && (r+2 == n || path[r+2] == '/') {
+            r += 2;
+            if out.length > dotdot {
+                out.length -= 1;
+                while out.length > dotdot && out[out.length] != '/' {
+                    out.length -= 1;
+                }
+            }
+            elseif !rooted {
+                if out.length > 0 {
+                    string.append(&out, "/");
+                }
+                string.append(&out, "..");
+                dotdot = out.length;
+            }
+        }
+        else {
+            if (rooted && out.length != 1) || (!rooted && out.length != 0) {
+                string.append(&out, "/");
+            }
+
+            while r < n && path[r] != '/' {
+                string.append(&out, path[r]);
+                r += 1;
+            }
+        }
+    }
+
+    if out.length == 0 {
+        string.append(&out, ".");
+    }
+
+    return out;
+}
+
+#doc """
+    Concatenates path elements, and returns cleaned output.
+
+    This uses the temporary allocator, so a copy may be needed.
+"""
+path_join :: (path: ..str) -> str {
+    out := make(dyn_str, allocator=context.temp_allocator);
+    
+    for p: path {
+        conv.format(&out, "{}{}", p, PATH_SEP);
+    }
+
+    return path_clean(out);
 }
 
-path_join :: (parent: str, child: str, allocator := context.temp_allocator) -> str {
-    out := make(dyn_str, allocator=allocator);
-    if parent[parent.length - 1] == PATH_SEP {
-        return conv.format(&out, "{}{}", parent, child);
-    } else {
-        return conv.format(&out, "{}{}{}", parent, PATH_SEP, child);
+#doc """
+    Returns everything but the last element in the path.
+
+    This is then cleaned and copied into the temporary allocator.
+"""
+path_directory :: (path: str) -> str {
+    dir, _ := path_split(path);
+    return path_clean(dir);
+}
+
+path_extension :: (path: str) -> str {
+    for i: range.{ path.length - 1, 0, -1 } {
+        if path[i] == '/' do break;
+        if path[i] == '.' do return path[i .. path.length];
     }
+    return "";
 }
 
 path_basename :: (path: str) -> str {
+    if path == "" do return ".";
+
     start := string.last_index_of(path, PATH_SEP);
     end   := string.last_index_of(path, '.');
     return path[start + 1 .. end];
 }
 
+path_split :: (path: str) -> (parent: str, child: str) {
+    index := string.last_index_of(path, PATH_SEP);
+    return path[0 .. index], path[index+1 .. path.length];
+}
+
diff --git a/tests/stdlib/os_path b/tests/stdlib/os_path
new file mode 100644 (file)
index 0000000..11aa95e
--- /dev/null
@@ -0,0 +1,20 @@
+true: .==.
+true: abc==abc
+true: ..==..
+true: ../../abc==../../abc
+true: /==/
+true: abc==abc
+true: a/b/c==a/b/c
+true: /abc==/abc
+true: abc/def/ghi==abc/def/ghi
+true: /abc==/abc
+true: /abc==/abc
+true: abc/def==abc/def
+true: /abc/def==/abc/def
+true: abc==abc
+true: abc/def/jkl==abc/def/jkl
+true: abc/jkl==abc/jkl
+true: def==def
+true: def==def
+true: ../../def==../../def
+true: abc==abc
diff --git a/tests/stdlib/os_path.onyx b/tests/stdlib/os_path.onyx
new file mode 100644 (file)
index 0000000..c216d5d
--- /dev/null
@@ -0,0 +1,35 @@
+use core {*}
+
+main :: () {
+    for t: Pair(str, str).[
+        .{"", "."},
+        .{"abc", "abc"},
+        .{"..", ".."},
+        .{"../../abc", "../../abc"},
+        .{"/", "/"},
+
+        .{"abc/", "abc"},
+        .{"a/b/c/", "a/b/c"},
+        .{"/abc/", "/abc"},
+
+        .{"abc//def//ghi", "abc/def/ghi"},
+        .{"//abc", "/abc"},
+        .{"//abc//", "/abc"},
+
+        .{"abc/./def", "abc/def"},
+        .{"/./abc/def", "/abc/def"},
+        .{"abc/.", "abc"},
+
+        .{"abc/def/ghi/../jkl", "abc/def/jkl"},
+        .{"abc/def/../ghi/../jkl", "abc/jkl"},
+
+       .{"abc/./../def", "def"},
+       .{"abc//./../def", "def"},
+       .{"abc/../../././../def", "../../def"},
+
+        .{"abc/def/..", "abc"}
+    ] {
+        cleaned := os.path_clean(t.first);
+        printf("{}: {}=={}\n", cleaned == t.second, cleaned, t.second);
+    }
+}