diff --git a/README.md b/README.md index 1e2bd03..22e589c 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ is a simple file path library for Carp. The `Path` module mostly operates on `String` arguments. It allows you to split, join, merge, and normalize paths and extensions in a lot of different ways. `normalize` resolves `.`/`..` segments and collapses repeated separators -lexically (without touching the filesystem). It also has some functions to work -with the `PATH` environment variable. +lexically (without touching the filesystem), and `relative` expresses one path +relative to a base the same way. It also has some functions to work with the +`PATH` environment variable. It assumes either Windows or POSIX-style separators. diff --git a/docs/Path.html b/docs/Path.html index 1e371ee..e016ca0 100644 --- a/docs/Path.html +++ b/docs/Path.html @@ -363,6 +363,31 @@

+
+ +

+ relative +

+
+
+ defn +
+

+ (Fn [(Ref String a), (Ref String b)] (Maybe String)) +

+
+                        (relative target base)
+                    
+

+

computes the path to target relative to base, purely +lexically. It is the companion to absolute.

+

Both paths are normalized first, so ., .., and redundant separators are +resolved before comparing. Returns Nothing when the paths cannot be related +lexically: one absolute and one relative, or, on Windows, rooted at different +drives. Equal paths yield Just ".".

+ +

+

diff --git a/docs/index.html b/docs/index.html index 1e371ee..e016ca0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -363,6 +363,31 @@

+
+ +

+ relative +

+
+
+ defn +
+

+ (Fn [(Ref String a), (Ref String b)] (Maybe String)) +

+
+                        (relative target base)
+                    
+

+

computes the path to target relative to base, purely +lexically. It is the companion to absolute.

+

Both paths are normalized first, so ., .., and redundant separators are +resolved before comparing. Returns Nothing when the paths cannot be related +lexically: one absolute and one relative, or, on Windows, rooted at different +drives. Equal paths yield Just ".".

+ +

+

diff --git a/path.carp b/path.carp index 0d7ae9b..1af9466 100644 --- a/path.carp +++ b/path.carp @@ -191,6 +191,40 @@ an extension if there previously was none.") (Maybe.Just @p) (Maybe.apply (cwd) &(fn [d] (join &[d @p]))))) + (doc relative "computes the path to `target` relative to `base`, purely +lexically. It is the companion to [absolute](#absolute). + +Both paths are normalized first, so `.`, `..`, and redundant separators are +resolved before comparing. Returns `Nothing` when the paths cannot be related +lexically: one absolute and one relative, or, on Windows, rooted at different +drives. Equal paths yield `Just \".\"`.") + (defn relative [target base] + (let [nt (normalize target) + nb (normalize base) + abs (absolute? &nt)] + (if (/= abs (absolute? &nb)) + (Maybe.Nothing) + (let [tc (split &nt) + bc (split &nb)] + (if (and abs (/= (Array.unsafe-nth &tc 0) (Array.unsafe-nth &bc 0))) + (Maybe.Nothing) + (let [non-empty? (fn [s] (not (String.empty? s))) + tf (Array.copy-filter &non-empty? &tc) + bf (Array.copy-filter &non-empty? &bc) + tn (Array.length &tf) + bn (Array.length &bf) + common (let-do [k 0] + (while (and (< k tn) + (< k bn) + (= (Array.unsafe-nth &tf k) + (Array.unsafe-nth &bf k))) + (set! k (inc k))) + k) + ups (Array.replicate (- bn common) "..") + comps (Array.concat &[ups (Array.suffix &tf common)]) + joined (join &comps)] + (Maybe.Just (if (String.empty? &joined) @"." joined)))))))) + (doc split-search-path "splits a `PATH` environment variable `p`.") (defn split-search-path [p] (String.split-by p &[search-path-separator])) (doc get-search-path "gets the `PATH` environment variable and splits it.") diff --git a/test/path.carp b/test/path.carp index b34f843..508ed77 100644 --- a/test/path.carp +++ b/test/path.carp @@ -91,6 +91,42 @@ "a" &(normalize "a/") "normalize strips a trailing separator") + (assert-equal test + &(Maybe.Just @"c") + &(relative "/a/b/c" "/a/b") + "relative descends into a subdirectory") + (assert-equal test + &(Maybe.Just @"..") + &(relative "/a/b" "/a/b/c") + "relative walks up with ..") + (assert-equal test + &(Maybe.Just @"../x") + &(relative "/a/x" "/a/b") + "relative walks up and back down") + (assert-equal test + &(Maybe.Just @".") + &(relative "/a/b" "/a/b") + "relative of equal paths is .") + (assert-equal test + &(Maybe.Just @"../../a/b/c") + &(relative "/a/b/c" "/x/y") + "relative with no shared components") + (assert-equal test + &(Maybe.Just @"c") + &(relative "/a/./b/../c" "/a") + "relative normalizes both paths first") + (assert-equal test + &(Maybe.Just @"c") + &(relative "a/b/c" "a/b") + "relative works on relative paths") + (assert-equal test + &(Maybe.Nothing) + &(relative "/a/b" "a/b") + "relative returns Nothing for an absolute target and relative base") + (assert-equal test + &(Maybe.Nothing) + &(relative "a/b" "/a/b") + "relative returns Nothing for a relative target and absolute base") (assert-equal test &(Maybe.Just @"file.txt") &(filename "path/to/file.txt")