├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── matcher.go ├── matcher_multi.go ├── matcher_option.go └── matcher_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Arran Walker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matcher 2 | 3 | `matcher` is similar to `path.Match`, but: 4 | 5 | - Supports globstar/doublestar (`**`). 6 | - Provides a fast `Glob` function. 7 | - Supports combining matchers. 8 | 9 | ## Examples 10 | 11 | ### Match 12 | 13 | ```golang 14 | package main 15 | 16 | import "github.com/saracen/matcher" 17 | 18 | func main() { 19 | matched, err := matcher.Match("hello/**/world", "hello/foo/bar/world") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | if matched { 25 | // do something 26 | } 27 | } 28 | ``` 29 | 30 | ### Glob 31 | 32 | ```golang 33 | package main 34 | 35 | import "github.com/saracen/matcher" 36 | 37 | func main() { 38 | matches, err := matcher.Glob(context.Background(), ".", matcher.New("**/*.go")) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | // do something with the matches 44 | _ = matches 45 | } 46 | ``` 47 | 48 | ### Glob with multiple patterns 49 | 50 | ```golang 51 | package main 52 | 53 | import "github.com/saracen/matcher" 54 | 55 | func main() { 56 | matcher := matcher.Multi( 57 | matcher.New("**/*.go"), 58 | matcher.New("**/*.txt")) 59 | 60 | matches, err := matcher.Glob(context.Background(), ".", matcher) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | // do something with the matches 66 | _ = matches 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/saracen/matcher 2 | 3 | go 1.17 4 | 5 | require github.com/saracen/walker v0.1.3 6 | 7 | require golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g= 2 | github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk= 3 | golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= 4 | golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 5 | -------------------------------------------------------------------------------- /matcher.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/saracen/walker" 12 | ) 13 | 14 | type Result int 15 | 16 | const ( 17 | separator = "/" 18 | globstar = "**" 19 | ) 20 | 21 | const ( 22 | NotMatched Result = iota 23 | Matched 24 | Follow 25 | ) 26 | 27 | // Matcher is an interface used for matching a path against a pattern. 28 | type Matcher interface { 29 | Match(pathname string) (Result, error) 30 | } 31 | 32 | type matcher struct { 33 | pattern []string 34 | options matchOptions 35 | } 36 | 37 | // New returns a new Matcher. 38 | // 39 | // The Matcher returned uses the same rules as Match, but returns a result of 40 | // either NotMatched, Matched or Follow. 41 | // 42 | // Follow hints to the caller that whilst the pattern wasn't matched, path 43 | // traversal might yield matches. This allows for more efficient globbing, 44 | // preventing path traversal where a match is impossible. 45 | func New(pattern string, opts ...MatchOption) Matcher { 46 | matcher := matcher{pattern: strings.Split(pattern, separator)} 47 | for _, o := range opts { 48 | o(&matcher.options) 49 | } 50 | 51 | if matcher.options.MatchFn == nil { 52 | matcher.options.MatchFn = path.Match 53 | } 54 | 55 | return matcher 56 | } 57 | 58 | // Match has similar behaviour to path.Match, but supports globstar. 59 | // 60 | // The pattern term '**' in a path portion matches zero or more subdirectories. 61 | // 62 | // The only possible returned error is ErrBadPattern, when the pattern 63 | // is malformed. 64 | func Match(pattern, pathname string, opts ...MatchOption) (bool, error) { 65 | result, err := New(pattern, opts...).Match(pathname) 66 | 67 | return result == Matched, err 68 | } 69 | 70 | func (p matcher) Match(pathname string) (Result, error) { 71 | return match(p.pattern, strings.Split(pathname, separator), p.options.MatchFn) 72 | } 73 | 74 | func match(pattern, parts []string, matchFn func(pattern, name string) (matched bool, err error)) (Result, error) { 75 | for { 76 | switch { 77 | case len(pattern) == 0 && len(parts) == 0: 78 | return Matched, nil 79 | 80 | case len(parts) == 0: 81 | return Follow, nil 82 | 83 | case len(pattern) == 0: 84 | return NotMatched, nil 85 | 86 | case pattern[0] == globstar && len(pattern) == 1: 87 | return Matched, nil 88 | 89 | case pattern[0] == globstar: 90 | for i := range parts { 91 | result, err := match(pattern[1:], parts[i:], matchFn) 92 | if result == Matched || err != nil { 93 | return result, err 94 | } 95 | } 96 | return Follow, nil 97 | } 98 | 99 | matched, err := matchFn(pattern[0], parts[0]) 100 | switch { 101 | case err != nil: 102 | return NotMatched, err 103 | 104 | case !matched && len(parts) == 1 && parts[0] == "": 105 | return Follow, nil 106 | 107 | case !matched: 108 | return NotMatched, nil 109 | } 110 | 111 | pattern = pattern[1:] 112 | parts = parts[1:] 113 | } 114 | } 115 | 116 | // Glob returns the pathnames and their associated os.FileInfos of all files 117 | // matching with the Matcher provided. 118 | // 119 | // Patterns are matched against the path relative to the directory provided 120 | // and path seperators are converted to '/'. Be aware that the matching 121 | // performed by this library's Matchers are case sensitive (even on 122 | // case-insensitive filesystems). Use WithPathTransformer(strings.ToLower) 123 | // and NewMatcher(strings.ToLower(pattern)) to perform case-insensitive 124 | // matching. 125 | // 126 | // Glob ignores any permission and I/O errors. 127 | func Glob(ctx context.Context, dir string, matcher Matcher, opts ...GlobOption) (map[string]os.FileInfo, error) { 128 | var options globOptions 129 | for _, o := range opts { 130 | err := o(&options) 131 | if err != nil { 132 | return nil, err 133 | } 134 | } 135 | 136 | matches := make(map[string]os.FileInfo) 137 | 138 | var m sync.Mutex 139 | 140 | ignoreErrors := walker.WithErrorCallback(func(pathname string, err error) error { 141 | return nil 142 | }) 143 | 144 | walkFn := func(pathname string, fi os.FileInfo) error { 145 | rel := strings.TrimPrefix(pathname, dir) 146 | rel = strings.TrimPrefix(filepath.ToSlash(rel), "/") 147 | if rel == "" { 148 | return nil 149 | } 150 | 151 | if fi.IsDir() { 152 | rel += "/" 153 | } 154 | 155 | if options.PathTransform != nil { 156 | rel = options.PathTransform(rel) 157 | } 158 | 159 | result, err := matcher.Match(rel) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if result == Matched { 165 | m.Lock() 166 | defer m.Unlock() 167 | 168 | matches[pathname] = fi 169 | } 170 | 171 | follow := result == Matched || result == Follow 172 | if fi.IsDir() && !follow { 173 | return filepath.SkipDir 174 | } 175 | 176 | return nil 177 | } 178 | 179 | return matches, walker.WalkWithContext(ctx, dir, walkFn, ignoreErrors) 180 | } 181 | -------------------------------------------------------------------------------- /matcher_multi.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | type multiMatcher []Matcher 4 | 5 | // Multi returns a new Matcher that matches against many matchers. 6 | func Multi(matches ...Matcher) Matcher { 7 | return multiMatcher(matches) 8 | } 9 | 10 | // Match performs a match with all matchers provided and returns a result 11 | // early if one matched. 12 | func (p multiMatcher) Match(pathname string) (Result, error) { 13 | var follow bool 14 | 15 | for _, include := range p { 16 | result, err := include.Match(pathname) 17 | 18 | switch { 19 | case err != nil: 20 | return NotMatched, err 21 | 22 | case result == Matched: 23 | return Matched, nil 24 | 25 | case result == Follow: 26 | follow = true 27 | } 28 | } 29 | 30 | if follow { 31 | return Follow, nil 32 | } 33 | 34 | return NotMatched, nil 35 | } 36 | -------------------------------------------------------------------------------- /matcher_option.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | // GlobOption is an option to configure Glob() behaviour. 4 | type GlobOption func(*globOptions) error 5 | 6 | type globOptions struct { 7 | PathTransform func(string) string 8 | } 9 | 10 | // WithPathTransforms allows a function to transform a path prior to it being 11 | // matched. A common use-case is WithPathTransformer(strings.ToLower) to ensure 12 | // paths have their case folded before matching. 13 | // 14 | // The transformer function should be safe for concurrent use. 15 | func WithPathTransformer(transformer func(pathname string) string) GlobOption { 16 | return func(o *globOptions) error { 17 | o.PathTransform = transformer 18 | return nil 19 | } 20 | } 21 | 22 | // MatchOption is an option to configure Match() behaviour. 23 | type MatchOption func(*matchOptions) 24 | 25 | type matchOptions struct { 26 | MatchFn func(pattern, name string) (matched bool, err error) 27 | } 28 | 29 | // WithMatchFunc allows a user provided matcher to be used in place of 30 | // path.Match for matching path segments. The globstar pattern will always be 31 | // supported, but paths between directory separators will be matched against 32 | // the function provided. 33 | func WithMatchFunc(matcher func(pattern, name string) (matched bool, err error)) MatchOption { 34 | return func(o *matchOptions) { 35 | o.MatchFn = matcher 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /matcher_test.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | "testing" 13 | // "github.com/bmatcuk/doublestar" 14 | // "github.com/saracen/walker" 15 | ) 16 | 17 | var ErrBadPattern = path.ErrBadPattern 18 | 19 | type MatchTest struct { 20 | pattern, s string 21 | result Result 22 | err error 23 | } 24 | 25 | var matchTests = map[string][]MatchTest{ 26 | "path.Match": { 27 | // https://golang.org/src/path/match_test.go 28 | {"abc", "abc", Matched, nil}, 29 | {"*", "abc", Matched, nil}, 30 | {"*c", "abc", Matched, nil}, 31 | {"a*", "a", Matched, nil}, 32 | {"a*", "abc", Matched, nil}, 33 | {"a*", "ab/c", NotMatched, nil}, 34 | {"a*/b", "abc/b", Matched, nil}, 35 | {"a*/b", "a/c/b", NotMatched, nil}, 36 | {"a*b*c*d*e*/f", "axbxcxdxe/f", Matched, nil}, 37 | {"a*b*c*d*e*/f", "axbxcxdxexxx/f", Matched, nil}, 38 | {"a*b*c*d*e*/f", "axbxcxdxe/xxx/f", NotMatched, nil}, 39 | {"a*b*c*d*e*/f", "axbxcxdxexxx/fff", NotMatched, nil}, 40 | {"a*b?c*x", "abxbbxdbxebxczzx", Matched, nil}, 41 | {"a*b?c*x", "abxbbxdbxebxczzy", NotMatched, nil}, 42 | {"ab[c]", "abc", Matched, nil}, 43 | {"ab[b-d]", "abc", Matched, nil}, 44 | {"ab[e-g]", "abc", NotMatched, nil}, 45 | {"ab[^c]", "abc", NotMatched, nil}, 46 | {"ab[^b-d]", "abc", NotMatched, nil}, 47 | {"ab[^e-g]", "abc", Matched, nil}, 48 | {"a\\*b", "a*b", Matched, nil}, 49 | {"a\\*b", "ab", NotMatched, nil}, 50 | {"a?b", "a☺b", Matched, nil}, 51 | {"a[^a]b", "a☺b", Matched, nil}, 52 | {"a???b", "a☺b", NotMatched, nil}, 53 | {"a[^a][^a][^a]b", "a☺b", NotMatched, nil}, 54 | {"[a-ζ]*", "α", Matched, nil}, 55 | {"*[a-ζ]", "A", NotMatched, nil}, 56 | {"a?b", "a/b", NotMatched, nil}, 57 | {"a*b", "a/b", NotMatched, nil}, 58 | {"[\\]a]", "]", Matched, nil}, 59 | {"[\\-]", "-", Matched, nil}, 60 | {"[x\\-]", "x", Matched, nil}, 61 | {"[x\\-]", "-", Matched, nil}, 62 | {"[x\\-]", "z", NotMatched, nil}, 63 | {"[\\-x]", "x", Matched, nil}, 64 | {"[\\-x]", "-", Matched, nil}, 65 | {"[\\-x]", "a", NotMatched, nil}, 66 | {"[]a]", "]", NotMatched, ErrBadPattern}, 67 | {"[-]", "-", NotMatched, ErrBadPattern}, 68 | {"[x-]", "x", NotMatched, ErrBadPattern}, 69 | {"[x-]", "-", NotMatched, ErrBadPattern}, 70 | {"[x-]", "z", NotMatched, ErrBadPattern}, 71 | {"[-x]", "x", NotMatched, ErrBadPattern}, 72 | {"[-x]", "-", NotMatched, ErrBadPattern}, 73 | {"[-x]", "a", NotMatched, ErrBadPattern}, 74 | {"\\", "a", NotMatched, ErrBadPattern}, 75 | {"[a-b-c]", "a", NotMatched, ErrBadPattern}, 76 | {"[", "a", NotMatched, ErrBadPattern}, 77 | {"[^", "a", NotMatched, ErrBadPattern}, 78 | {"[^bc", "a", NotMatched, ErrBadPattern}, 79 | {"a[", "a", NotMatched, ErrBadPattern}, 80 | {"a[", "ab", NotMatched, ErrBadPattern}, 81 | {"*x", "xxx", Matched, nil}, 82 | }, 83 | "t3070-wildmatch basic wildmatch features": { 84 | {"foo", "foo", Matched, nil}, 85 | {"bar", "foo", NotMatched, nil}, 86 | {"", "", Matched, nil}, 87 | {"???", "foo", Matched, nil}, 88 | {"??", "foo", NotMatched, nil}, 89 | {"*", "foo", Matched, nil}, 90 | {"f*", "foo", Matched, nil}, 91 | {"*f", "foo", NotMatched, nil}, 92 | {"*foo*", "foo", Matched, nil}, 93 | {"*ob*a*r*", "foobar", Matched, nil}, 94 | {"*ab", "aaaaaaabababab", Matched, nil}, 95 | {`foo\*`, "foo*", Matched, nil}, 96 | {`foo\*bar`, "foobar", NotMatched, nil}, 97 | {`f\\oo`, `f\oo`, Matched, nil}, 98 | {"*[al]?", "ball", Matched, nil}, 99 | {"[ten]", "ten", NotMatched, nil}, 100 | {"**[^te]", "ten", Matched, nil}, 101 | {"**[^ten]", "ten", NotMatched, nil}, 102 | {"t[a-g]n", "ten", Matched, nil}, 103 | {"t[^a-g]n", "ten", NotMatched, nil}, 104 | {"t[^a-g]n", "ton", Matched, nil}, 105 | {`a[\]]b`, "a]b", Matched, nil}, 106 | {`a[\]\-]b`, "a-b", Matched, nil}, 107 | {`a[\]\-]b`, "a]b", Matched, nil}, 108 | {`a[\]\-]b`, "aab", NotMatched, nil}, 109 | {`a[\]a\-]b`, "aab", Matched, nil}, 110 | {"]", "]", Matched, nil}, 111 | }, 112 | "t3070-wildmatch extended slash-matching features": { 113 | {"foo*bar", "foo/baz/bar", NotMatched, nil}, 114 | {"foo**bar", "foo/baz/bar", NotMatched, nil}, 115 | {"foo**bar", "foobazbar", Matched, nil}, 116 | {"foo/**/bar", "foo/baz/bar", Matched, nil}, 117 | {"foo/**/**/bar", "foo/baz/bar", Matched, nil}, 118 | {"foo/**/bar", "foo/b/a/z/bar", Matched, nil}, 119 | {"foo/**/**/bar", "foo/b/a/z/bar", Matched, nil}, 120 | {"foo/**/bar", "foo/bar", Matched, nil}, 121 | {"foo/**/**/bar", "foo/bar", Matched, nil}, 122 | {"foo?bar", "foo/bar", NotMatched, nil}, 123 | {"foo[/]bar", "foo/bar", NotMatched, ErrBadPattern}, 124 | {"foo[^a-z]bar", "foo/bar", NotMatched, nil}, 125 | {"f[^eiu][^eiu][^eiu][^eiu][^eiu]r", "foo/bar", NotMatched, nil}, 126 | {"f[^eiu][^eiu][^eiu][^eiu][^eiu]r", "foo-bar", Matched, nil}, 127 | {"**/foo", "foo", Matched, nil}, 128 | {"**/foo", "XXX/foo", Matched, nil}, 129 | {"**/foo", "bar/baz/foo", Matched, nil}, 130 | {"*/foo", "bar/baz/foo", NotMatched, nil}, 131 | {"**/bar*", "foo/bar/baz", Follow, nil}, 132 | {"**/bar/*", "deep/foo/bar/baz", Matched, nil}, 133 | {"**/bar/*", "deep/foo/bar/baz/", Follow, nil}, 134 | {"**/bar/**", "deep/foo/bar/baz/", Matched, nil}, 135 | {"**/bar/*", "deep/foo/bar", Follow, nil}, 136 | {"**/bar/**", "deep/foo/bar/", Matched, nil}, 137 | {"**/bar**", "foo/bar/baz", Follow, nil}, 138 | {"*/bar/**", "foo/bar/baz/x", Matched, nil}, 139 | {"*/bar/**", "deep/foo/bar/baz/x", NotMatched, nil}, 140 | {"**/bar/*/*", "deep/foo/bar/baz/x", Matched, nil}, 141 | }, 142 | "t3070-wildmatch various additional tests": { 143 | {"a[c-c]st", "acrt", NotMatched, nil}, 144 | {"a[c-c]rt", "acrt", Matched, nil}, 145 | {"[!]-]", "]", NotMatched, nil}, 146 | {"[!]-]", "a", NotMatched, nil}, 147 | {`\`, "", NotMatched, ErrBadPattern}, 148 | {`\`, `\`, NotMatched, ErrBadPattern}, 149 | {`*/\`, `XXX/\`, NotMatched, ErrBadPattern}, 150 | {`*/\\`, `XXX/\`, Matched, nil}, 151 | {"foo", "foo", Matched, nil}, 152 | {"@foo", "@foo", Matched, nil}, 153 | {"@foo", "foo", NotMatched, nil}, 154 | {`\[ab]`, "[ab]", Matched, nil}, 155 | {"[[]ab]", "[ab]", Matched, nil}, 156 | {"[[:]ab]", "[ab]", Matched, nil}, 157 | {`[\[:]ab]`, "[ab]", Matched, nil}, 158 | {`\??\?b`, "?a?b", Matched, nil}, 159 | {`\a\b\c`, "abc", Matched, nil}, 160 | {"", "foo", NotMatched, nil}, 161 | {"**/t[o]", "foo/bar/baz/to", Matched, nil}, 162 | }, 163 | "t3070-wildmatch additional tests, including malformed wildmatch patterns": { 164 | {`[\\-^]`, "]", Matched, nil}, 165 | {`[\\-^]`, "[", NotMatched, nil}, 166 | {`[\-_]`, "-", Matched, nil}, 167 | {`[\]]`, "]", Matched, nil}, 168 | {`[\]]`, `\]`, NotMatched, nil}, 169 | {`[\]]`, `\`, NotMatched, nil}, 170 | {"a[]b", "ab", NotMatched, ErrBadPattern}, 171 | {"a[]b", "a[]b", NotMatched, ErrBadPattern}, 172 | {"ab[", "ab[", NotMatched, ErrBadPattern}, 173 | {"[^", "ab", NotMatched, ErrBadPattern}, 174 | {"[-", "ab", NotMatched, ErrBadPattern}, 175 | {`[\-]`, "-", Matched, nil}, 176 | {"[a-", "-", NotMatched, ErrBadPattern}, 177 | {"[!a-", "-", NotMatched, ErrBadPattern}, 178 | {`[\--A]`, "-", Matched, nil}, 179 | {`[\--A]`, "5", Matched, nil}, 180 | {`[ -\-]`, " ", Matched, nil}, 181 | {`[ -\-]`, "$", Matched, nil}, 182 | {`[ -\-]`, "-", Matched, nil}, 183 | {`[ -\-]`, "0", NotMatched, nil}, 184 | {`[\--\-]`, "-", Matched, nil}, 185 | {`[\--\-\--\-]`, "-", Matched, nil}, 186 | {`[a-e\-n]`, "j", NotMatched, nil}, 187 | {`[a-e\-n]`, "-", Matched, nil}, 188 | {`[^\--\-\--\-]`, "a", Matched, nil}, 189 | {`[\]-a]`, "[", NotMatched, nil}, 190 | {`[\]-a]`, "^", Matched, nil}, 191 | {`[^\]-a]`, "^", NotMatched, nil}, 192 | {`[^\]-a]`, "[", Matched, nil}, 193 | {"[a^bc]", "^", Matched, nil}, 194 | {`[a\-]b]`, "-b]", Matched, nil}, 195 | {`[\]`, `\`, NotMatched, ErrBadPattern}, 196 | {`[\\]`, `\`, Matched, nil}, 197 | {`[^\\]`, `\`, NotMatched, nil}, 198 | {`[A-\\]`, "G", Matched, nil}, 199 | {"b*a", "aaabbb", NotMatched, nil}, 200 | {"*ba*", "aabcaa", NotMatched, nil}, 201 | {"[,]", ",", Matched, nil}, 202 | {`[\\,]`, ",", Matched, nil}, 203 | {`[\\,]`, `\`, Matched, nil}, 204 | {"[,-.]", "-", Matched, nil}, 205 | {"[,-.]", "+", NotMatched, nil}, 206 | {"[,-.]", "-.]", NotMatched, nil}, 207 | {`[\1-\3]`, "2", Matched, nil}, 208 | {`[\1-\3]`, "3", Matched, nil}, 209 | {`[\1-\3]`, "4", NotMatched, nil}, 210 | {`[[-\]]`, `\`, Matched, nil}, 211 | {`[[-\]]`, "[", Matched, nil}, 212 | {`[[-\]]`, "]", Matched, nil}, 213 | {`[[-\]]`, "-", NotMatched, nil}, 214 | }, 215 | "t3070-wildmatch test recursion": { 216 | {"-*-*-*-*-*-*-12-*-*-*-m-*-*-*", "-adobe-courier-bold-o-normal--12-120-75-75-m-70-iso8859-1", Matched, nil}, 217 | {"-*-*-*-*-*-*-12-*-*-*-m-*-*-*", "-adobe-courier-bold-o-normal--12-120-75-75-X-70-iso8859-1", NotMatched, nil}, 218 | {"-*-*-*-*-*-*-12-*-*-*-m-*-*-*", "-adobe-courier-bold-o-normal--12-120-75-75-/-70-iso8859-1", NotMatched, nil}, 219 | {`XXX/*/*/*/*/*/*/12/*/*/*/m/*/*/*`, `XXX/adobe/courier/bold/o/normal//12/120/75/75/m/70/iso8859/1`, Matched, nil}, 220 | {`XXX/*/*/*/*/*/*/12/*/*/*/m/*/*/*`, `XXX/adobe/courier/bold/o/normal//12/120/75/75/X/70/iso8859/1`, NotMatched, nil}, 221 | {`**/*a*b*g*n*t`, "abcd/abcdefg/abcdefghijk/abcdefghijklmnop.txt", Matched, nil}, 222 | {`**/*a*b*g*n*t`, "abcd/abcdefg/abcdefghijk/abcdefghijklmnop.txtz", Follow, nil}, 223 | {`*/*/*`, "foo", Follow, nil}, 224 | {`*/*/*`, "foo/bar", Follow, nil}, 225 | {`*/*/*`, "foo/bba/arr", Matched, nil}, 226 | {`*/*/*`, "foo/bb/aa/rr", NotMatched, nil}, 227 | {`**/**/**`, "foo/bb/aa/rr", Matched, nil}, 228 | {"*X*i", "abcXdefXghi", Matched, nil}, 229 | {"*X*i", "ab/cXd/efXg/hi", NotMatched, nil}, 230 | {`*/*X*/*/*i`, "ab/cXd/efXg/hi", Matched, nil}, 231 | {`**/*X*/**/*i`, "ab/cXd/efXg/hi", Matched, nil}, 232 | }, 233 | "follow tests": { 234 | {"**/test", "hello/world", Follow, nil}, 235 | {"**/abc/**", "hello/world/abc", Follow, nil}, 236 | {"abc/def/**/xyz", "abc", Follow, nil}, 237 | {"abc/def/**/xyz", "abc/def", Follow, nil}, 238 | {"abc/def/**/xyz", "abc/def/hello", Follow, nil}, 239 | {"abc/def/**/xyz", "abc/def/hello/world", Follow, nil}, 240 | {"abc/def/**/xyz", "abc/def/hello/world/xyz", Matched, nil}, 241 | {"**/*", "hello/world", Matched, nil}, 242 | {"**/abc/**", "hello/world/abc/", Matched, nil}, 243 | {"**/**/**", "hello", Matched, nil}, 244 | {"**/hello/world", "hello/world", Matched, nil}, 245 | {"abc/**/hello/world", "abc/hello/world", Matched, nil}, 246 | {"abc/**/hello/world", "xyz/abc/hello/world", NotMatched, nil}, 247 | {"files/dir1/file1.txt", "files/", Follow, nil}, 248 | {"files/dir1/file1.txt", "files/dir1/", Follow, nil}, 249 | {"files/dir1/file1.txt", "files/dir1/file1.txt", Matched, nil}, 250 | }, 251 | "various tests": { 252 | {"**/doc", "value/volcano/tail/doc", Matched, nil}, 253 | {"**/*lue/vol?ano/ta?l", "value/volcano/tail", Matched, nil}, 254 | {"**/*lue/vol?ano/tail", "head/value/volcano/tail", Matched, nil}, 255 | {"**/*lue/vol?ano/tail", "head/value/Volcano/tail", Follow, nil}, 256 | {"*lue/vol?ano/**", "value/volcano/tail/moretail", Matched, nil}, 257 | {"*lue/**", "value/volcano", Matched, nil}, 258 | {"*lue/vol?ano/**", "value/volcano", Follow, nil}, 259 | {"*lue/**/vol?ano", "value/volcano", Matched, nil}, 260 | {"*lue/**/vol?ano", "value/middle/volcano", Matched, nil}, 261 | {"*lue/**/vol?ano", "value/middle1/middle2/volcano", Matched, nil}, 262 | {"*lue/**foo/vol?ano/tail", "value/foo/volcano/tail", Matched, nil}, 263 | {"**/head/v[ou]l[kc]ano", "value/head/volcano", Matched, nil}, 264 | {"**/head/v[ou]l[", "value/head/volcano", NotMatched, ErrBadPattern}, 265 | {"**/head/v[ou]l[", "value/head/vol[", NotMatched, ErrBadPattern}, 266 | {"value/**/v[ou]l[", "value/head/vol[", NotMatched, ErrBadPattern}, 267 | {"**/android/**/GeneratedPluginRegistrant.java", "packages/flutter_tools/lib/src/android/gradle.dart", Follow, nil}, 268 | {"**/*/unicorns/*.bin", "data/rainbows/unicorns/0ee357d9-bc00-4c78-8738-7debdf909d26.bin", Matched, nil}, 269 | {"**/unicorns/*.bin", "data/rainbows/unicorns/0ee357d9-bc00-4c78-8738-7debdf909d26.bin", Matched, nil}, 270 | }, 271 | } 272 | 273 | func TestMatch(t *testing.T) { 274 | for tn, tests := range matchTests { 275 | tests := tests 276 | t.Run(tn, func(t *testing.T) { 277 | for _, tt := range tests { 278 | matched, err := Match(tt.pattern, tt.s) 279 | 280 | if matched && tt.result != Matched || err != tt.err { 281 | t.Errorf("Match(%#q, %#q) = (%v, %v) want (%v, %v)", tt.pattern, tt.s, matched, err, tt.result == Matched, tt.err) 282 | return 283 | } 284 | } 285 | }) 286 | } 287 | } 288 | 289 | func TestNewMatcher(t *testing.T) { 290 | for tn, tests := range matchTests { 291 | tests := tests 292 | t.Run(tn, func(t *testing.T) { 293 | for _, tt := range tests { 294 | result, err := New(tt.pattern).Match(tt.s) 295 | if result != tt.result || err != tt.err { 296 | t.Errorf("New(%#q).Match(%#q) = (%v, %v) want (%v, %v)", tt.pattern, tt.s, result, err, tt.result, tt.err) 297 | return 298 | } 299 | } 300 | }) 301 | } 302 | } 303 | 304 | func TestMultiMatcher(t *testing.T) { 305 | tests := map[string]Result{ 306 | "aaa/bbb": Follow, 307 | "aaa/bbb/ccc": Follow, 308 | "aaa/bbb/ccc/ddd": Follow, 309 | "aaa/bbb/ccc/ddd/eee": NotMatched, 310 | "aaa/zzz": Follow, 311 | "aaa/zzz/ccc": Follow, 312 | "aaa/zzz/ccc/zzz": Follow, 313 | "aaa/zzz/ccc/zzz/eee": Matched, 314 | "aaa/zzz/zzz": Follow, 315 | "aaa/zzz/zzz/zzz/zzz": Follow, 316 | "aaa/zzz/zzz/zzz/zzz/zzz": Follow, 317 | "aaa/zzz/zzz/zzz/ccc/zzz/zzz/ddd/eee": Matched, 318 | "zzz/aaa/zzz": NotMatched, 319 | "yyy/aaa/yyy": NotMatched, 320 | } 321 | 322 | includes := Multi( 323 | New("aaa/**/ccc/**/eee"), 324 | New("zzz/**"), 325 | ) 326 | excludes := Multi( 327 | New("aaa/bbb/ccc/ddd/eee"), 328 | New("zzz/**"), 329 | ) 330 | 331 | for path, tt := range tests { 332 | exclude, err := excludes.Match(path) 333 | if err != nil { 334 | t.Error(err) 335 | } 336 | 337 | result := exclude 338 | if result == Matched { 339 | result = NotMatched 340 | } else { 341 | result, err = includes.Match(path) 342 | if err != nil { 343 | t.Error(err) 344 | } 345 | } 346 | 347 | if result != tt { 348 | t.Errorf("path %q result was %v expected %v", path, result, tt) 349 | } 350 | } 351 | } 352 | 353 | func TestMatchFunc(t *testing.T) { 354 | tests := map[string]Result{ 355 | "aaa/bbb": Follow, 356 | "aaa/bbb/ccc/ddd/eee": Matched, 357 | "aaaa/zzz/ccc/zzz/eee": Matched, 358 | "aa/zzz/ccc/zzz/eee": NotMatched, 359 | } 360 | 361 | // a custom matcher that uses path.Match, but only succeeds if the path 362 | // segment is more than 2 characters. 363 | match := func(pattern, name string) (matched bool, err error) { 364 | matched, err = path.Match(pattern, name) 365 | if matched && len(name) > 2 { 366 | return true, err 367 | } 368 | return false, err 369 | } 370 | 371 | for path, tt := range tests { 372 | result, err := New("a*/**/ccc/**/eee", WithMatchFunc(match)).Match(path) 373 | if err != nil { 374 | t.Error(err) 375 | } 376 | 377 | if result != tt { 378 | t.Errorf("path %q result was %v expected %v", path, result, tt) 379 | } 380 | } 381 | } 382 | 383 | func TestMultiMatcherInvalid(t *testing.T) { 384 | _, err := Multi( 385 | New("abc"), 386 | New("[]a]"), 387 | ).Match("abcdef") 388 | 389 | if err == nil { 390 | t.Errorf("include pattern was invalid, but not error was returned") 391 | } 392 | } 393 | 394 | func TestGlob(t *testing.T) { 395 | dir, err := ioutil.TempDir("", "") 396 | if err != nil { 397 | t.Error(err) 398 | } 399 | 400 | defer os.RemoveAll(dir) 401 | 402 | os.MkdirAll(filepath.Join(dir, "files", "dir1"), 0o777) 403 | os.MkdirAll(filepath.Join(dir, "files", "dir2"), 0o777) 404 | os.MkdirAll(filepath.Join(dir, "files", "dir3"), 0o111) 405 | os.MkdirAll(filepath.Join(dir, "ignore", "dir4"), 0o777) 406 | 407 | os.WriteFile(filepath.Join(dir, "files", "dir1", "file1.txt"), []byte{}, 0o600) 408 | os.WriteFile(filepath.Join(dir, "files", "dir1", "file2.txt"), []byte{}, 0o600) 409 | os.WriteFile(filepath.Join(dir, "files", "dir2", "file3.ignore"), []byte{}, 0o600) 410 | os.WriteFile(filepath.Join(dir, "files", "dir3", "file4.txt"), []byte{}, 0o600) 411 | os.WriteFile(filepath.Join(dir, "ignore", "dir4", "file5.txt"), []byte{}, 0o600) 412 | 413 | matches, err := Glob(context.Background(), dir, New("files/**/*.txt")) 414 | if err != nil { 415 | t.Error(err) 416 | } 417 | 418 | if len(matches) != 2 { 419 | t.Errorf("was expecting 2 files, got %v", len(matches)) 420 | } 421 | } 422 | 423 | func TestGlobMultiMatcher(t *testing.T) { 424 | dir, err := ioutil.TempDir("", "") 425 | if err != nil { 426 | t.Error(err) 427 | } 428 | 429 | defer os.RemoveAll(dir) 430 | 431 | os.MkdirAll(filepath.Join(dir, "files", "dir1"), 0o777) 432 | os.MkdirAll(filepath.Join(dir, "files", "dir2"), 0o777) 433 | 434 | os.WriteFile(filepath.Join(dir, "files", "dir1", "File1.txt"), []byte{}, 0o600) 435 | os.WriteFile(filepath.Join(dir, "files", "dir1", "File2.txt"), []byte{}, 0o600) 436 | os.WriteFile(filepath.Join(dir, "files", "dir2", "File3.txt"), []byte{}, 0o600) 437 | 438 | matches, err := Glob(context.Background(), dir, Multi( 439 | New(strings.ToLower("files/DIR1/file1.txt")), 440 | New(strings.ToLower("files/DIR1/file2.txt")), 441 | ), WithPathTransformer(strings.ToLower)) 442 | if err != nil { 443 | t.Error(err) 444 | } 445 | 446 | if len(matches) != 2 { 447 | t.Errorf("was expecting 2 files, got %v", len(matches)) 448 | } 449 | } 450 | 451 | var globDir = flag.String("globdir", runtime.GOROOT(), "The directory to use for glob benchmarks") 452 | var globPattern = flag.String("globpattern", "pkg/**/*.go", "The pattern to use for glob benchmarks") 453 | 454 | func BenchmarkGlob(b *testing.B) { 455 | b.ReportAllocs() 456 | 457 | m := New(*globPattern) 458 | for n := 0; n < b.N; n++ { 459 | _, err := Glob(context.Background(), *globDir, m) 460 | if err != nil { 461 | b.Error(err) 462 | } 463 | } 464 | } 465 | 466 | /* 467 | func BenchmarkGlobWithDoublestarMatch(b *testing.B) { 468 | b.ReportAllocs() 469 | 470 | m := New(*globPattern, WithMatchFunc(doublestar.Match)) 471 | for n := 0; n < b.N; n++ { 472 | _, err := Glob(context.Background(), *globDir, m) 473 | if err != nil { 474 | b.Error(err) 475 | } 476 | } 477 | } 478 | 479 | func BenchmarkDoublestarGlob(b *testing.B) { 480 | b.ReportAllocs() 481 | 482 | for n := 0; n < b.N; n++ { 483 | _, err := doublestar.Glob(filepath.Join(*globDir, *globPattern)) 484 | if err != nil { 485 | b.Error(err) 486 | } 487 | } 488 | } 489 | 490 | func benchmarkWalkDoublestarMatch(b *testing.B) { 491 | matches := make(map[string]os.FileInfo) 492 | 493 | var m sync.Mutex 494 | 495 | err := walker.Walk(*globDir, func(pathname string, fi os.FileInfo) error { 496 | rel := strings.TrimPrefix(pathname, *globDir) 497 | rel = filepath.ToSlash(strings.TrimPrefix(rel, "/")) 498 | 499 | if rel == "" { 500 | return nil 501 | } 502 | 503 | match, err := doublestar.Match(*globPattern, rel) 504 | if err != nil { 505 | return err 506 | } 507 | 508 | if match { 509 | m.Lock() 510 | defer m.Unlock() 511 | matches[pathname] = fi 512 | } 513 | 514 | return nil 515 | }) 516 | 517 | if err != nil { 518 | b.Error(err) 519 | } 520 | } 521 | 522 | func BenchmarkWalkDoublestarMatch(b *testing.B) { 523 | b.ReportAllocs() 524 | 525 | for n := 0; n < b.N; n++ { 526 | benchmarkWalkDoublestarMatch(b) 527 | } 528 | } 529 | */ 530 | --------------------------------------------------------------------------------