Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ is a simple file path library for Carp.
### Usage

The `Path` module mostly operates on `String` arguments. It allows you to
split, join, and merge paths and extensions in a lot of different ways. It also
has some functions to work with the `PATH` environment variable.
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.

It assumes either Windows or POSIX-style separators.

Expand Down
47 changes: 47 additions & 0 deletions path.carp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,53 @@ Otherwise returns `Just` of the last component.")
but-last (Array.prefix &split (dec (Array.length &split)))]
(String.join sep-string &but-last)))

(doc normalize "normalizes the path `p` lexically, without touching the
filesystem.

It collapses repeated separators, drops `.` components, and resolves each `..`
against the preceding component. Leading `..` components are kept in relative
paths (they cannot be resolved without a base), and `..` never escapes the root
of an absolute path. An empty or fully-cancelling relative path normalizes to
`.`, and a fully-cancelling absolute path normalizes to its root (a lone
separator on POSIX, the drive root such as `C:\\` on Windows).

Examples on POSIX:
```
(normalize \"a/./b\") ; => \"a/b\"
(normalize \"a/b/../c\") ; => \"a/c\"
(normalize \"//a///b\") ; => \"/a/b\"
(normalize \"/a/../..\") ; => \"/\"
(normalize \"a/../..\") ; => \"..\"
(normalize \"\") ; => \".\"
```")
(defn normalize [p]
(if (String.empty? p)
@"."
(let-do [abs (absolute? p)
comps (split p)
out []]
; on an absolute path the first component is the root marker — "" on
; POSIX, the drive (e.g. "C:") on Windows — so skip it here and re-attach
; it verbatim below: that preserves the drive and stops `..` escaping root
(for [i (if abs 1 0) (Array.length &comps)]
(let [c (Array.unsafe-nth &comps i)]
(cond
(or (String.empty? c) (= c ".")) ()
(= c "..")
(let [n (Array.length &out)]
(cond
(= n 0) (unless abs (set! out (Array.push-back out @c)))
(= (Array.unsafe-nth &out (dec n)) "..")
(set! out (Array.push-back out @c))
(set! out (Array.prefix &out (dec n)))))
(set! out (Array.push-back out @c)))))
(let [joined (String.join sep-string &out)]
(cond
abs
(String.concat &[@(Array.unsafe-nth &comps 0) @sep-string joined])
(String.empty? &joined) @"."
joined)))))

(doc split-extension "splits the path `p` on its extension.

It will return a `(Maybe (Pair String String))`. `Maybe` because there might not
Expand Down
31 changes: 31 additions & 0 deletions test/path.carp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@
"path/to/file"
&(join &[@"path" @"to" @"file"])
"join works")
(assert-equal test "a/b" &(normalize "a/./b") "normalize drops . components")
(assert-equal test "a/c" &(normalize "a/b/../c") "normalize resolves ..")
(assert-equal test
"/a/b"
&(normalize "//a///b")
"normalize collapses repeated separators")
(assert-equal test
"/"
&(normalize "/a/../..")
"normalize keeps absolute .. from escaping root")
(assert-equal test "/" &(normalize "/") "normalize keeps the root")
(assert-equal test
".."
&(normalize "a/../..")
"normalize keeps a leading .. in a relative path")
(assert-equal test
"../b"
&(normalize "../a/../b")
"normalize preserves unresolvable leading ..")
(assert-equal test
"."
&(normalize "")
"normalize turns the empty path into .")
(assert-equal test
"."
&(normalize "a/b/../..")
"normalize turns a fully-cancelling relative path into .")
(assert-equal test
"a"
&(normalize "a/")
"normalize strips a trailing separator")
(assert-equal test
&(Maybe.Just @"file.txt")
&(filename "path/to/file.txt")
Expand Down
Loading