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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,42 @@ relative to a base the same way. It also has some functions to work with the

It assumes either Windows or POSIX-style separators.

### Glob matching

`matches?` checks a path against a glob pattern, and `matching` keeps the paths
in an array that match one, in order. Both are lexical as well and never touch
the filesystem, and both take the path first, like everything else here — note
that this is the opposite of `Pattern.matches?` in core.

```clojure
(Path.matches? "src/main.carp" "src/*.carp") ; => true
(Path.matching &[@"a.carp" @"b.c"] "*.carp") ; => [@"a.carp"]
(Path.matching &[@"a.carp" @"t/b.carp"] "**/*.carp") ; => [@"a.carp" @"t/b.carp"]
```

| Form | Meaning |
|----------|------------------------------------------------------|
| `?` | exactly one character, never a separator |
| `*` | zero or more characters, never a separator |
| `**` | zero or more whole segments, as an entire segment |
| `[abc]` | one of the characters in the class |
| `[a-z]` | one character from the range |
| `[!abc]` | one character not in the class (`[^abc]` also works) |
| `\*` | a literal `*`, on POSIX |

`**` is only a segment wildcard when it is the whole segment, so `**/*.carp`
matches both `a.carp` and `x/y/a.carp`, while `a**b` is just `a*b`. On POSIX a
`\` escapes the next pattern character; on Windows `\` is a separator, so
escaping is disabled there. A trailing `\` and an unterminated `[` are matched
as literal characters, which is why `matches?` is a plain `Bool` and not a
`Result`. Separators are structural and cannot be escaped away: `a\/b` splits
into segments just like `a/b`, and a class holding one, such as `[a/]`, matches
its other members but never the separator.

Leading dots are not special: `*` matches `.hidden`. Matching happens on the
path exactly as given, with no normalization, so run it through `normalize`
first if `.`, `..` or repeated separators should not get in the way.

Look at [the documentation](https://carpentry.dev/path) for more information.

<hr/>
Expand Down
71 changes: 71 additions & 0 deletions docs/Path.html
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,77 @@ <h3 id="join">

</p>
</div>
<div class="binder">
<a class="anchor" href="#matches?">
<h3 id="matches?">
matches?
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref String a), (Ref String b)] Bool)
</p>
<pre class="args">
(matches? p pattern)
</pre>
<p class="doc">
<p>checks whether the path <code>p</code> matches the glob <code>pattern</code>.</p>
<p>The path comes first, like everywhere else in this module — note that this is
the opposite of <code>Pattern.matches?</code> in core:</p>
<pre><code>(matches? &quot;src/main.carp&quot; &quot;src/*.carp&quot;) ; =&gt; true
</code></pre>
<p>The pattern language is:</p>
<pre><code>? exactly one character, never a separator
* zero or more characters, never a separator
** zero or more whole segments, as an entire segment
[abc] one of the characters in the class
[a-z] one character from the range
[!abc] one character not in the class ([^abc] works too)
\* a literal *, on POSIX (see below)
</code></pre>
<p><code>**</code> is only a segment wildcard when it is the whole segment, so <code>**/*.carp</code>
matches both <code>a.carp</code> and <code>x/y/a.carp</code>, while <code>a**b</code> is just <code>a*b</code>. Inside a
class, a <code>]</code> directly after the <code>[</code> (or after the <code>!</code>/<code>^</code>) is a literal <code>]</code>, and
a <code>-</code> first or last is a literal <code>-</code>.</p>
<p>On POSIX a <code>\</code> escapes the next pattern character. On Windows <code>\</code> is a
separator, so escaping is disabled there. A trailing <code>\</code> and an unterminated
<code>[</code> are matched as literal characters, which is why this is a total function and
not a <code>Result</code>.</p>
<p>Separators are structural and cannot be escaped away: <code>a\/b</code> splits into
segments just like <code>a/b</code>, and a class holding one, such as <code>[a/]</code>, matches its
other members but never the separator.</p>
<p>Leading dots are not special: <code>*</code> matches <code>.hidden</code>. Matching happens on the
path exactly as given, with no normalization and no collapsing of repeated
separators — run it through <a href="#normalize">normalize</a> first if you want that.</p>

</p>
</div>
<div class="binder">
<a class="anchor" href="#matching">
<h3 id="matching">
matching
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref (Array String) a), (Ref String b)] (Array String))
</p>
<pre class="args">
(matching ps pattern)
</pre>
<p class="doc">
<p>keeps the paths in <code>ps</code> that match the glob <code>pattern</code>,
preserving their order.</p>
<p>It is <a href="#matches?">matches?</a> over an array, which is usually what you want:</p>
<pre><code>(matching &amp;[@&quot;a.carp&quot; @&quot;a.c&quot;] &quot;*.carp&quot;) ; =&gt; [@&quot;a.carp&quot;]
</code></pre>

</p>
</div>
<div class="binder">
<a class="anchor" href="#normalize">
<h3 id="normalize">
Expand Down
218 changes: 216 additions & 2 deletions path.carp
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,24 @@ variable we use on this OS.")
(hidden extension-pat)
(private sep-string)
(hidden sep-string)
(private escapes?)
(hidden escapes?)
(windows-only
(defn absolute? [p] (Pattern.matches? #"[A-Za-z]:\\" p))
(def separator \\)
(def separators [\/ \\])
(def search-path-separator \;)
(def extension-pat #"\.[^\\/\.]*$")
(def sep-string "\\"))
(def sep-string "\\")
(def escapes? false))
(posix-only
(defn absolute? [p] (String.starts-with? p "/"))
(def separator \/)
(def separators [\/])
(def search-path-separator \:)
(def extension-pat #"\.[^/\.]*$")
(def sep-string "/"))
(def sep-string "/")
(def escapes? true))

(doc relative? "checks whether a path is relative.

Expand Down Expand Up @@ -225,6 +229,216 @@ drives. Equal paths yield `Just \".\"`.")
joined (join &comps)]
(Maybe.Just (if (String.empty? &joined) @"." joined))))))))

(defn- class-end [pcs i]
(let-do [n (Array.length pcs)
j (inc i)
res -1]
(when (and (< j n)
(let [c @(Array.unsafe-nth pcs j)] (or (= c \!) (= c \^))))
(set! j (inc j)))
(when (and (< j n) (= @(Array.unsafe-nth pcs j) \])) (set! j (inc j)))
(while-do (and (= res -1) (< j n))
(let [c @(Array.unsafe-nth pcs j)]
(cond
(= c \]) (set! res (inc j))
(and escapes? (= c \\) (< (inc j) n)) (set! j (+ j 2))
(set! j (inc j)))))
res))

(defn- class-matches? [pcs i end c]
(let-do [j (inc i)
last (dec end)
neg false
found false]
(when-do (and (< j last)
(let [m @(Array.unsafe-nth pcs j)] (or (= m \!) (= m \^))))
(set! neg true)
(set! j (inc j)))
(while-do (< j last)
(let-do [lo @(Array.unsafe-nth pcs j)]
(set! j (inc j))
(when-do (and escapes? (= lo \\) (< j last))
(set! lo @(Array.unsafe-nth pcs j))
(set! j (inc j)))
(if (and (< (inc j) last) (= @(Array.unsafe-nth pcs j) \-))
(let-do [hi @(Array.unsafe-nth pcs (inc j))]
(set! j (+ j 2))
(when-do (and escapes? (= hi \\) (< j last))
(set! hi @(Array.unsafe-nth pcs j))
(set! j (inc j)))
(when (and (<= lo c) (<= c hi)) (set! found true)))
(when (= lo c) (set! found true)))))
(if neg (not found) found)))

; the first `[` whose class never closes; every later one is unterminated too
(defn- dead-class [pcs]
(let-do [n (Array.length pcs)
i 0
res -1]
(while-do (and (= res -1) (< i n))
(let [c @(Array.unsafe-nth pcs i)]
(cond
(and escapes? (= c \\) (< (inc i) n)) (set! i (+ i 2))
(= c \[)
(let [e (class-end pcs i)] (if (= e -1) (set! res i) (set! i e)))
(set! i (inc i)))))
res))

(defn- dead? [dead i] (and (<= 0 dead) (>= i dead)))

; index just past the item at `i` when it matches `c`, -1 when it does not
(defn- item-match-end [pcs i dead c]
(let [n (Array.length pcs)
pc @(Array.unsafe-nth pcs i)]
(cond
(and escapes? (= pc \\) (< (inc i) n))
(if (= @(Array.unsafe-nth pcs (inc i)) c) (+ i 2) -1)
(= pc \?) (inc i)
(and (= pc \[) (not (dead? dead i)))
(let [e (class-end pcs i)]
(cond
(= e -1) (if (= pc c) (inc i) -1)
(class-matches? pcs i e c) e
-1))
(if (= pc c) (inc i) -1))))

(defn- star? [pcs i] (= @(Array.unsafe-nth pcs i) \*))

(defn- segment-matches? [scs pcs dead]
(let-do [pn (Array.length pcs)
sn (Array.length scs)
p 0
s 0
star -1
mark 0
ok true]
(while-do (and ok (< s sn))
(let [end (if (< p pn)
(item-match-end pcs p dead @(Array.unsafe-nth scs s))
-1)]
(cond
(and (< p pn) (star? pcs p))
(do (set! star p) (set! mark s) (set! p (inc p)))
(<= 0 end) (do (set! p end) (set! s (inc s)))
(<= 0 star)
(do (set! mark (inc mark)) (set! s mark) (set! p (inc star)))
(set! ok false))))
(while-do (and ok (< p pn) (star? pcs p)) (set! p (inc p)))
(and ok (= p pn))))

(defn- segment [cs pattern?]
(let-do [n (Array.length cs)
out []
cur []
i 0
classes? pattern?]
(while-do (< i n)
(let-do [c @(Array.unsafe-nth cs i)
esc (and pattern? escapes? (= c \\) (< (inc i) n))
nxt (if esc @(Array.unsafe-nth cs (inc i)) c)
cend (if (and classes? (= c \[)) (class-end cs i) -1)]
; once a `[` is unterminated every later one is too
(when (and (= c \[) (= cend -1)) (set! classes? false))
(cond
(and esc (not (separator? &nxt)))
(do
(set! cur (Array.push-back cur c))
(set! cur (Array.push-back cur nxt))
(set! i (+ i 2)))
(/= cend -1)
(while-do (< i cend)
(set! cur (Array.push-back cur @(Array.unsafe-nth cs i)))
(set! i (inc i)))
(separator? &nxt)
(do
(set! out (Array.push-back out @&cur))
(set! cur [])
(set! i (if esc (+ i 2) (inc i))))
(do (set! cur (Array.push-back cur c)) (set! i (inc i))))))
(Array.push-back out cur)))

(defn- globstar? [seg]
(and (= 2 (Array.length seg)) (star? seg 0) (star? seg 1)))

(defn- path-matches? [ss ps]
(let-do [pn (Array.length ps)
sn (Array.length ss)
deads (Array.copy-map &dead-class ps)
p 0
s 0
star -1
mark 0
ok true]
(while-do (and ok (< s sn))
(cond
(and (< p pn) (globstar? (Array.unsafe-nth ps p)))
(do (set! star p) (set! mark s) (set! p (inc p)))
(and (< p pn)
(segment-matches? (Array.unsafe-nth ss s)
(Array.unsafe-nth ps p)
@(Array.unsafe-nth &deads p)))
(do (set! p (inc p)) (set! s (inc s)))
(<= 0 star)
(do (set! mark (inc mark)) (set! s mark) (set! p (inc star)))
(set! ok false)))
(while-do (and ok (< p pn) (globstar? (Array.unsafe-nth ps p)))
(set! p (inc p)))
(and ok (= p pn))))

(doc matches? "checks whether the path `p` matches the glob `pattern`.

The path comes first, like everywhere else in this module — note that this is
the opposite of `Pattern.matches?` in core:
```
(matches? \"src/main.carp\" \"src/*.carp\") ; => true
```

The pattern language is:
```
? exactly one character, never a separator
* zero or more characters, never a separator
** zero or more whole segments, as an entire segment
[abc] one of the characters in the class
[a-z] one character from the range
[!abc] one character not in the class ([^abc] works too)
\\* a literal *, on POSIX (see below)
```

`**` is only a segment wildcard when it is the whole segment, so `**/*.carp`
matches both `a.carp` and `x/y/a.carp`, while `a**b` is just `a*b`. Inside a
class, a `]` directly after the `[` (or after the `!`/`^`) is a literal `]`, and
a `-` first or last is a literal `-`.

On POSIX a `\\` escapes the next pattern character. On Windows `\\` is a
separator, so escaping is disabled there. A trailing `\\` and an unterminated
`[` are matched as literal characters, which is why this is a total function and
not a `Result`.

Separators are structural and cannot be escaped away: `a\\/b` splits into
segments just like `a/b`, and a class holding one, such as `[a/]`, matches its
other members but never the separator.

Leading dots are not special: `*` matches `.hidden`. Matching happens on the
path exactly as given, with no normalization and no collapsing of repeated
separators — run it through [normalize](#normalize) first if you want that.")
(defn matches? [p pattern]
(path-matches? &(segment &(String.chars p) false)
&(segment &(String.chars pattern) true)))

(doc matching "keeps the paths in `ps` that match the glob `pattern`,
preserving their order.

It is [matches?](#matches?) over an array, which is usually what you want:
```
(matching &[@\"a.carp\" @\"a.c\"] \"*.carp\") ; => [@\"a.carp\"]
```")
(defn matching [ps pattern]
(let-do [out []]
(for [i 0 (Array.length ps)]
(let [p (Array.unsafe-nth ps i)]
(when (matches? p pattern) (set! out (Array.push-back out @p)))))
out))

(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.")
Expand Down
Loading