Skip to content

Commit 6283fdf

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 <[email protected]>
1 parent 347bb8d commit 6283fdf

File tree

2 files changed

+138
-81
lines changed

2 files changed

+138
-81
lines changed

Diff for: patternmatcher.go

+30
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
//

Diff for: patternmatcher_test.go

+108-81
Original file line numberDiff line numberDiff line change
@@ -106,106 +106,133 @@ 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+
{"a\\\\", "a\\", exact},
182+
{"**/foo/bar", "foo/bar", exact},
183+
{"**/foo/bar", "dir/foo/bar", exact},
184+
{"**/foo/bar", "dir/dir2/foo/bar", exact},
185+
{"abc/**", "abc", fail},
186+
{"abc/**", "abc/def", exact},
187+
{"abc/**", "abc/def/ghi", exact},
188+
{"**/.foo", ".foo", exact},
189+
{"**/.foo", "bar.foo", fail},
190+
{"a(b)c/def", "a(b)c/def", exact},
191+
{"a(b)c/def", "a(b)c/xyz", fail},
192+
{"a.|)$(}+{bc", "a.|)$(}+{bc", exact},
193+
{"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},
194+
{"dist/*.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", exact},
187195
}
188196
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},
197+
{[]string{"**", "!util/docker/web"}, "util/docker/web/foo", fail},
198+
{[]string{"**", "!util/docker/web", "util/docker/web/foo"}, "util/docker/web/foo", exact},
199+
{[]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},
200+
{[]string{"**", "!dist/*.whl"}, "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", fail},
193201
}
194202

195203
if runtime.GOOS != "windows" {
196204
tests = append(tests, []matchesTestCase{
197-
{"a\\*b", "a*b", true},
205+
{"a\\*b", "a*b", exact},
198206
}...)
199207
}
200208

209+
t.Run("MatchesExact", func(t *testing.T) {
210+
check := func(pm *PatternMatcher, text string, pass bool, desc string) {
211+
res, _ := pm.MatchesExact(text)
212+
if pass != res {
213+
t.Errorf("expected: %v, got: %v %s", pass, res, desc)
214+
}
215+
}
216+
217+
for _, test := range tests {
218+
desc := fmt.Sprintf("(pattern=%q text=%q)", test.pattern, test.text)
219+
pm, err := New([]string{test.pattern})
220+
if err != nil {
221+
t.Fatal(err, desc)
222+
}
223+
224+
check(pm, test.text, test.pass == exact, desc)
225+
}
226+
})
227+
201228
t.Run("MatchesOrParentMatches", func(t *testing.T) {
202229
for _, test := range tests {
203230
pm, err := New([]string{test.pattern})
204231
if err != nil {
205232
t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text)
206233
}
207234
res, _ := pm.MatchesOrParentMatches(test.text)
208-
if test.pass != res {
235+
if (test.pass != fail) != res {
209236
t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text)
210237
}
211238
}
@@ -216,7 +243,7 @@ func TestMatches(t *testing.T) {
216243
t.Fatalf("%v (patterns=%q, text=%q)", err, test.patterns, test.text)
217244
}
218245
res, _ := pm.MatchesOrParentMatches(test.text)
219-
if test.pass != res {
246+
if (test.pass != fail) != res {
220247
t.Errorf("expected: %v, got: %v (patterns=%q, text=%q)", test.pass, res, test.patterns, test.text)
221248
}
222249
}
@@ -240,7 +267,7 @@ func TestMatches(t *testing.T) {
240267
}
241268

242269
res, _ := pm.MatchesUsingParentResult(test.text, parentMatched)
243-
if test.pass != res {
270+
if (test.pass != fail) != res {
244271
t.Errorf("expected: %v, got: %v (pattern=%q, text=%q)", test.pass, res, test.pattern, test.text)
245272
}
246273
}
@@ -271,7 +298,7 @@ func TestMatches(t *testing.T) {
271298
t.Fatal(err, desc)
272299
}
273300

274-
check(pm, test.text, test.pass, desc)
301+
check(pm, test.text, test.pass != fail, desc)
275302
}
276303

277304
for _, test := range multiPatternTests {
@@ -281,7 +308,7 @@ func TestMatches(t *testing.T) {
281308
t.Fatal(err, desc)
282309
}
283310

284-
check(pm, test.text, test.pass, desc)
311+
check(pm, test.text, test.pass != fail, desc)
285312
}
286313
})
287314

@@ -300,7 +327,7 @@ func TestMatches(t *testing.T) {
300327
t.Fatal(err, desc)
301328
}
302329

303-
check(pm, test.text, test.pass, desc)
330+
check(pm, test.text, test.pass != fail, desc)
304331
}
305332

306333
for _, test := range multiPatternTests {
@@ -310,7 +337,7 @@ func TestMatches(t *testing.T) {
310337
t.Fatal(err, desc)
311338
}
312339

313-
check(pm, test.text, test.pass, desc)
340+
check(pm, test.text, test.pass != fail, desc)
314341
}
315342
})
316343
}

0 commit comments

Comments
 (0)