-
Notifications
You must be signed in to change notification settings - Fork 1
/
matcher.go
180 lines (145 loc) · 4.01 KB
/
matcher.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package matcher
import (
"context"
"os"
"path"
"path/filepath"
"strings"
"sync"
"github.com/saracen/walker"
)
type Result int
const (
separator = "/"
globstar = "**"
)
const (
NotMatched Result = iota
Matched
Follow
)
// Matcher is an interface used for matching a path against a pattern.
type Matcher interface {
Match(pathname string) (Result, error)
}
type matcher struct {
pattern []string
options matchOptions
}
// New returns a new Matcher.
//
// The Matcher returned uses the same rules as Match, but returns a result of
// either NotMatched, Matched or Follow.
//
// Follow hints to the caller that whilst the pattern wasn't matched, path
// traversal might yield matches. This allows for more efficient globbing,
// preventing path traversal where a match is impossible.
func New(pattern string, opts ...MatchOption) Matcher {
matcher := matcher{pattern: strings.Split(pattern, separator)}
for _, o := range opts {
o(&matcher.options)
}
if matcher.options.MatchFn == nil {
matcher.options.MatchFn = path.Match
}
return matcher
}
// Match has similar behaviour to path.Match, but supports globstar.
//
// The pattern term '**' in a path portion matches zero or more subdirectories.
//
// The only possible returned error is ErrBadPattern, when the pattern
// is malformed.
func Match(pattern, pathname string, opts ...MatchOption) (bool, error) {
result, err := New(pattern, opts...).Match(pathname)
return result == Matched, err
}
func (p matcher) Match(pathname string) (Result, error) {
return match(p.pattern, strings.Split(pathname, separator), p.options.MatchFn)
}
func match(pattern, parts []string, matchFn func(pattern, name string) (matched bool, err error)) (Result, error) {
for {
switch {
case len(pattern) == 0 && len(parts) == 0:
return Matched, nil
case len(parts) == 0:
return Follow, nil
case len(pattern) == 0:
return NotMatched, nil
case pattern[0] == globstar && len(pattern) == 1:
return Matched, nil
case pattern[0] == globstar:
for i := range parts {
result, err := match(pattern[1:], parts[i:], matchFn)
if result == Matched || err != nil {
return result, err
}
}
return Follow, nil
}
matched, err := matchFn(pattern[0], parts[0])
switch {
case err != nil:
return NotMatched, err
case !matched && len(parts) == 1 && parts[0] == "":
return Follow, nil
case !matched:
return NotMatched, nil
}
pattern = pattern[1:]
parts = parts[1:]
}
}
// Glob returns the pathnames and their associated os.FileInfos of all files
// matching with the Matcher provided.
//
// Patterns are matched against the path relative to the directory provided
// and path seperators are converted to '/'. Be aware that the matching
// performed by this library's Matchers are case sensitive (even on
// case-insensitive filesystems). Use WithPathTransformer(strings.ToLower)
// and NewMatcher(strings.ToLower(pattern)) to perform case-insensitive
// matching.
//
// Glob ignores any permission and I/O errors.
func Glob(ctx context.Context, dir string, matcher Matcher, opts ...GlobOption) (map[string]os.FileInfo, error) {
var options globOptions
for _, o := range opts {
err := o(&options)
if err != nil {
return nil, err
}
}
matches := make(map[string]os.FileInfo)
var m sync.Mutex
ignoreErrors := walker.WithErrorCallback(func(pathname string, err error) error {
return nil
})
walkFn := func(pathname string, fi os.FileInfo) error {
rel := strings.TrimPrefix(pathname, dir)
rel = strings.TrimPrefix(filepath.ToSlash(rel), "/")
if rel == "" {
return nil
}
if fi.IsDir() {
rel += "/"
}
if options.PathTransform != nil {
rel = options.PathTransform(rel)
}
result, err := matcher.Match(rel)
if err != nil {
return err
}
if result == Matched {
m.Lock()
defer m.Unlock()
matches[pathname] = fi
}
follow := result == Matched || result == Follow
if fi.IsDir() && !follow {
return filepath.SkipDir
}
return nil
}
return matches, walker.WalkWithContext(ctx, dir, walkFn, ignoreErrors)
}