Skip to content

Commit ab6276d

Browse files
committed
Add MatchesExact function
This function allows matching on the full path, without attempting to match any parent elements of the path. This was *technically* possible before, by calling the deprecated `MatchesUsingParentResult` and always setting `parentMatched` to `false`. However, this is quite hacky and results in quite tricky-to-read code (and also didn't have tests, etc). So this patch adds in a proper function for this, and ensures it works with some refactored tests. Signed-off-by: Justin Chadwell <me@jedevc.com>
1 parent 347bb8d commit ab6276d

2 files changed

Lines changed: 143 additions & 82 deletions

File tree

patternmatcher.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,36 @@ func (pm *PatternMatcher) Matches(file string) (bool, error) {
119119
return matched, nil
120120
}
121121

122+
// MatchesExact returns true if "file" exactly matches any of the patterns.
123+
// Unlike MatchesOrParentMatches, no parent matching is performed.
124+
//
125+
// The "file" argument should be a slash-delimited path.
126+
//
127+
// MatchesExact is not safe to call concurrently.
128+
func (pm *PatternMatcher) MatchesExact(file string) (bool, error) {
129+
matched := false
130+
file = filepath.FromSlash(file)
131+
132+
for _, pattern := range pm.patterns {
133+
// Skip evaluation if this is an inclusion and the filename
134+
// already matched the pattern, or it's an exclusion and it has
135+
// not matched the pattern yet.
136+
if pattern.exclusion != matched {
137+
continue
138+
}
139+
140+
match, err := pattern.match(file)
141+
if err != nil {
142+
return false, err
143+
}
144+
145+
if match {
146+
matched = !pattern.exclusion
147+
}
148+
}
149+
return matched, nil
150+
}
151+
122152
// MatchesOrParentMatches returns true if "file" matches any of the patterns
123153
// and isn't excluded by any of the subsequent patterns.
124154
//

patternmatcher_test.go

Lines changed: 113 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -106,106 +106,137 @@ func TestMatchesWithMalformedPatterns(t *testing.T) {
106106
type matchesTestCase struct {
107107
pattern string
108108
text string
109-
pass bool
109+
pass testMatchType
110110
}
111111

112112
type multiPatternTestCase struct {
113113
patterns []string
114114
text string
115-
pass bool
115+
pass testMatchType
116116
}
117117

118+
type testMatchType int
119+
120+
const (
121+
fail testMatchType = iota
122+
exact testMatchType = iota
123+
inexact testMatchType = iota
124+
)
125+
118126
func TestMatches(t *testing.T) {
119127
tests := []matchesTestCase{
120-
{"**", "file", true},
121-
{"**", "file/", true},
122-
{"**/", "file", true}, // weird one
123-
{"**/", "file/", true},
124-
{"**", "/", true},
125-
{"**/", "/", true},
126-
{"**", "dir/file", true},
127-
{"**/", "dir/file", true},
128-
{"**", "dir/file/", true},
129-
{"**/", "dir/file/", true},
130-
{"**/**", "dir/file", true},
131-
{"**/**", "dir/file/", true},
132-
{"dir/**", "dir/file", true},
133-
{"dir/**", "dir/file/", true},
134-
{"dir/**", "dir/dir2/file", true},
135-
{"dir/**", "dir/dir2/file/", true},
136-
{"**/dir", "dir", true},
137-
{"**/dir", "dir/file", true},
138-
{"**/dir2/*", "dir/dir2/file", true},
139-
{"**/dir2/*", "dir/dir2/file/", true},
140-
{"**/dir2/**", "dir/dir2/dir3/file", true},
141-
{"**/dir2/**", "dir/dir2/dir3/file/", true},
142-
{"**file", "file", true},
143-
{"**file", "dir/file", true},
144-
{"**/file", "dir/file", true},
145-
{"**file", "dir/dir/file", true},
146-
{"**/file", "dir/dir/file", true},
147-
{"**/file*", "dir/dir/file", true},
148-
{"**/file*", "dir/dir/file.txt", true},
149-
{"**/file*txt", "dir/dir/file.txt", true},
150-
{"**/file*.txt", "dir/dir/file.txt", true},
151-
{"**/file*.txt*", "dir/dir/file.txt", true},
152-
{"**/**/*.txt", "dir/dir/file.txt", true},
153-
{"**/**/*.txt2", "dir/dir/file.txt", false},
154-
{"**/*.txt", "file.txt", true},
155-
{"**/**/*.txt", "file.txt", true},
156-
{"a**/*.txt", "a/file.txt", true},
157-
{"a**/*.txt", "a/dir/file.txt", true},
158-
{"a**/*.txt", "a/dir/dir/file.txt", true},
159-
{"a/*.txt", "a/dir/file.txt", false},
160-
{"a/*.txt", "a/file.txt", true},
161-
{"a/*.txt**", "a/file.txt", true},
162-
{"a[b-d]e", "ae", false},
163-
{"a[b-d]e", "ace", true},
164-
{"a[b-d]e", "aae", false},
165-
{"a[^b-d]e", "aze", true},
166-
{".*", ".foo", true},
167-
{".*", "foo", false},
168-
{"abc.def", "abcdef", false},
169-
{"abc.def", "abc.def", true},
170-
{"abc.def", "abcZdef", false},
171-
{"abc?def", "abcZdef", true},
172-
{"abc?def", "abcdef", false},
173-
{"a\\\\", "a\\", true},
174-
{"**/foo/bar", "foo/bar", true},
175-
{"**/foo/bar", "dir/foo/bar", true},
176-
{"**/foo/bar", "dir/dir2/foo/bar", true},
177-
{"abc/**", "abc", false},
178-
{"abc/**", "abc/def", true},
179-
{"abc/**", "abc/def/ghi", true},
180-
{"**/.foo", ".foo", true},
181-
{"**/.foo", "bar.foo", false},
182-
{"a(b)c/def", "a(b)c/def", true},
183-
{"a(b)c/def", "a(b)c/xyz", false},
184-
{"a.|)$(}+{bc", "a.|)$(}+{bc", true},
185-
{"dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", true},
186-
{"dist/*.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", true},
128+
{"**", "file", exact},
129+
{"**", "file/", exact},
130+
{"**/", "file", exact}, // weird one
131+
{"**/", "file/", exact},
132+
{"**", "/", exact},
133+
{"**/", "/", exact},
134+
{"**", "dir/file", exact},
135+
{"**/", "dir/file", exact},
136+
{"**", "dir/file/", exact},
137+
{"**/", "dir/file/", exact},
138+
{"**/**", "dir/file", exact},
139+
{"**/**", "dir/file/", exact},
140+
{"dir/**", "dir/file", exact},
141+
{"dir/**", "dir/file/", exact},
142+
{"dir/**", "dir/dir2/file", exact},
143+
{"dir/**", "dir/dir2/file/", exact},
144+
{"**/dir", "dir", exact},
145+
{"**/dir", "dir/file", inexact},
146+
{"**/dir2/*", "dir/dir2/file", exact},
147+
{"**/dir2/*", "dir/dir2/file/", inexact},
148+
{"**/dir2/**", "dir/dir2/dir3/file", exact},
149+
{"**/dir2/**", "dir/dir2/dir3/file/", exact},
150+
{"**file", "file", exact},
151+
{"**file", "dir/file", exact},
152+
{"**/file", "dir/file", exact},
153+
{"**file", "dir/dir/file", exact},
154+
{"**/file", "dir/dir/file", exact},
155+
{"**/file*", "dir/dir/file", exact},
156+
{"**/file*", "dir/dir/file.txt", exact},
157+
{"**/file*txt", "dir/dir/file.txt", exact},
158+
{"**/file*.txt", "dir/dir/file.txt", exact},
159+
{"**/file*.txt*", "dir/dir/file.txt", exact},
160+
{"**/**/*.txt", "dir/dir/file.txt", exact},
161+
{"**/**/*.txt2", "dir/dir/file.txt", fail},
162+
{"**/*.txt", "file.txt", exact},
163+
{"**/**/*.txt", "file.txt", exact},
164+
{"a**/*.txt", "a/file.txt", exact},
165+
{"a**/*.txt", "a/dir/file.txt", exact},
166+
{"a**/*.txt", "a/dir/dir/file.txt", exact},
167+
{"a/*.txt", "a/dir/file.txt", fail},
168+
{"a/*.txt", "a/file.txt", exact},
169+
{"a/*.txt**", "a/file.txt", exact},
170+
{"a[b-d]e", "ae", fail},
171+
{"a[b-d]e", "ace", exact},
172+
{"a[b-d]e", "aae", fail},
173+
{"a[^b-d]e", "aze", exact},
174+
{".*", ".foo", exact},
175+
{".*", "foo", fail},
176+
{"abc.def", "abcdef", fail},
177+
{"abc.def", "abc.def", exact},
178+
{"abc.def", "abcZdef", fail},
179+
{"abc?def", "abcZdef", exact},
180+
{"abc?def", "abcdef", fail},
181+
{"**/foo/bar", "foo/bar", exact},
182+
{"**/foo/bar", "dir/foo/bar", exact},
183+
{"**/foo/bar", "dir/dir2/foo/bar", exact},
184+
{"abc/**", "abc", fail},
185+
{"abc/**", "abc/def", exact},
186+
{"abc/**", "abc/def/ghi", exact},
187+
{"**/.foo", ".foo", exact},
188+
{"**/.foo", "bar.foo", fail},
189+
{"a(b)c/def", "a(b)c/def", exact},
190+
{"a(b)c/def", "a(b)c/xyz", fail},
191+
{"a.|)$(}+{bc", "a.|)$(}+{bc", exact},
192+
{"dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", exact},
193+
{"dist/*.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", exact},
187194
}
188195
multiPatternTests := []multiPatternTestCase{
189-
{[]string{"**", "!util/docker/web"}, "util/docker/web/foo", false},
190-
{[]string{"**", "!util/docker/web", "util/docker/web/foo"}, "util/docker/web/foo", true},
191-
{[]string{"**", "!dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl"}, "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", false},
192-
{[]string{"**", "!dist/*.whl"}, "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", false},
196+
{[]string{"**", "!util/docker/web"}, "util/docker/web/foo", fail},
197+
{[]string{"**", "!util/docker/web", "util/docker/web/foo"}, "util/docker/web/foo", exact},
198+
{[]string{"**", "!dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl"}, "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", fail},
199+
{[]string{"**", "!dist/*.whl"}, "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", fail},
193200
}
194201

195-
if runtime.GOOS != "windows" {
202+
if runtime.GOOS == "windows" {
196203
tests = append(tests, []matchesTestCase{
197-
{"a\\*b", "a*b", true},
204+
{"a\\\\", "a\\", inexact},
205+
}...)
206+
} else {
207+
tests = append(tests, []matchesTestCase{
208+
{"a\\\\", "a\\", exact},
209+
{"a\\*b", "a*b", exact},
198210
}...)
199211
}
200212

213+
t.Run("MatchesExact", func(t *testing.T) {
214+
check := func(pm *PatternMatcher, text string, pass bool, desc string) {
215+
res, _ := pm.MatchesExact(text)
216+
if pass != res {
217+
t.Errorf("expected: %v, got: %v %s", pass, res, desc)
218+
}
219+
}
220+
221+
for _, test := range tests {
222+
desc := fmt.Sprintf("(pattern=%q text=%q)", test.pattern, test.text)
223+
pm, err := New([]string{test.pattern})
224+
if err != nil {
225+
t.Fatal(err, desc)
226+
}
227+
228+
check(pm, test.text, test.pass == exact, desc)
229+
}
230+
})
231+
201232
t.Run("MatchesOrParentMatches", func(t *testing.T) {
202233
for _, test := range tests {
203234
pm, err := New([]string{test.pattern})
204235
if err != nil {
205236
t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text)
206237
}
207238
res, _ := pm.MatchesOrParentMatches(test.text)
208-
if test.pass != res {
239+
if (test.pass != fail) != res {
209240
t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text)
210241
}
211242
}
@@ -216,7 +247,7 @@ func TestMatches(t *testing.T) {
216247
t.Fatalf("%v (patterns=%q, text=%q)", err, test.patterns, test.text)
217248
}
218249
res, _ := pm.MatchesOrParentMatches(test.text)
219-
if test.pass != res {
250+
if (test.pass != fail) != res {
220251
t.Errorf("expected: %v, got: %v (patterns=%q, text=%q)", test.pass, res, test.patterns, test.text)
221252
}
222253
}
@@ -240,7 +271,7 @@ func TestMatches(t *testing.T) {
240271
}
241272

242273
res, _ := pm.MatchesUsingParentResult(test.text, parentMatched)
243-
if test.pass != res {
274+
if (test.pass != fail) != res {
244275
t.Errorf("expected: %v, got: %v (pattern=%q, text=%q)", test.pass, res, test.pattern, test.text)
245276
}
246277
}
@@ -271,7 +302,7 @@ func TestMatches(t *testing.T) {
271302
t.Fatal(err, desc)
272303
}
273304

274-
check(pm, test.text, test.pass, desc)
305+
check(pm, test.text, test.pass != fail, desc)
275306
}
276307

277308
for _, test := range multiPatternTests {
@@ -281,7 +312,7 @@ func TestMatches(t *testing.T) {
281312
t.Fatal(err, desc)
282313
}
283314

284-
check(pm, test.text, test.pass, desc)
315+
check(pm, test.text, test.pass != fail, desc)
285316
}
286317
})
287318

@@ -300,7 +331,7 @@ func TestMatches(t *testing.T) {
300331
t.Fatal(err, desc)
301332
}
302333

303-
check(pm, test.text, test.pass, desc)
334+
check(pm, test.text, test.pass != fail, desc)
304335
}
305336

306337
for _, test := range multiPatternTests {
@@ -310,7 +341,7 @@ func TestMatches(t *testing.T) {
310341
t.Fatal(err, desc)
311342
}
312343

313-
check(pm, test.text, test.pass, desc)
344+
check(pm, test.text, test.pass != fail, desc)
314345
}
315346
})
316347
}

0 commit comments

Comments
 (0)