-
Notifications
You must be signed in to change notification settings - Fork 7
/
pattern.go
284 lines (243 loc) · 7.99 KB
/
pattern.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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package gitignore
import (
"path/filepath"
"strings"
"github.com/danwakefield/fnmatch"
)
// Pattern represents per-line patterns within a .gitignore file
type Pattern interface {
Match
// Match returns true if the given path matches the name pattern. If the
// pattern is meant for directories only, and the path is not a directory,
// Match will return false. The matching is performed by fnmatch(). It
// is assumed path is relative to the base path of the owning GitIgnore.
Match(string, bool) bool
}
// pattern is the base implementation of a .gitignore pattern
type pattern struct {
_negated bool
_anchored bool
_directory bool
_string string
_fnmatch string
_position Position
} // pattern()
// name represents patterns matching a file or path name (i.e. the last
// component of a path)
type name struct {
pattern
} // name{}
// path represents a pattern that contains at least one path separator within
// the pattern (i.e. not at the start or end of the pattern)
type path struct {
pattern
_depth int
} // path{}
// any represents a pattern that contains at least one "any" token "**"
// allowing for recursive matching.
type any struct {
pattern
_tokens []*Token
} // any{}
// NewPattern returns a Pattern from the ordered slice of Tokens. The tokens are
// assumed to represent a well-formed .gitignore pattern. A Pattern may be
// negated, anchored to the start of the path (relative to the base directory
// of tie containing .gitignore), or match directories only.
func NewPattern(tokens []*Token) Pattern {
// if we have no tokens there is no pattern
if len(tokens) == 0 {
return nil
}
// extract the pattern position from first token
_position := tokens[0].Position
_string := tokenset(tokens).String()
// is this a negated pattern?
_negated := false
if tokens[0].Type == NEGATION {
_negated = true
tokens = tokens[1:]
}
// is this pattern anchored to the start of the path?
_anchored := false
if tokens[0].Type == SEPARATOR {
_anchored = true
tokens = tokens[1:]
}
// is this pattern for directories only?
_directory := false
_last := len(tokens) - 1
if tokens[_last].Type == SEPARATOR {
_directory = true
tokens = tokens[:_last]
}
// build the pattern expression
_fnmatch := tokenset(tokens).String()
_pattern := &pattern{
_negated: _negated,
_anchored: _anchored,
_position: _position,
_directory: _directory,
_string: _string,
_fnmatch: _fnmatch,
}
return _pattern.compile(tokens)
} // NewPattern()
// compile generates a specific Pattern (i.e. name, path or any)
// represented by the list of tokens.
func (p *pattern) compile(tokens []*Token) Pattern {
// what tokens do we have in this pattern?
// - ANY token means we can match to any depth
// - SEPARATOR means we have path rather than file matching
_separator := false
for _, _token := range tokens {
switch _token.Type {
case ANY:
return p.any(tokens)
case SEPARATOR:
_separator = true
}
}
// should we perform path or name/file matching?
if _separator {
return p.path(tokens)
} else {
return p.name(tokens)
}
} // compile()
// Ignore returns true if the pattern describes files or paths that should be
// ignored.
func (p *pattern) Ignore() bool { return !p._negated }
// Include returns true if the pattern describes files or paths that should be
// included (i.e. not ignored)
func (p *pattern) Include() bool { return p._negated }
// Position returns the position of the first token of this pattern.
func (p *pattern) Position() Position { return p._position }
// String returns the string representation of the pattern.
func (p *pattern) String() string { return p._string }
//
// name patterns
// - designed to match trailing file/directory names only
//
// name returns a Pattern designed to match file or directory names, with no
// path elements.
func (p *pattern) name(tokens []*Token) Pattern {
return &name{*p}
} // name()
// Match returns true if the given path matches the name pattern. If the
// pattern is meant for directories only, and the path is not a directory,
// Match will return false. The matching is performed by fnmatch(). It
// is assumed path is relative to the base path of the owning GitIgnore.
func (n *name) Match(path string, isdir bool) bool {
// are we expecting a directory?
if n._directory && !isdir {
return false
}
// should we match the whole path, or just the last component?
if n._anchored {
return fnmatch.Match(n._fnmatch, path, 0)
} else {
_, _base := filepath.Split(path)
return fnmatch.Match(n._fnmatch, _base, 0)
}
} // Match()
//
// path patterns
// - designed to match complete or partial paths (not just filenames)
//
// path returns a Pattern designed to match paths that include at least one
// path separator '/' neither at the end nor the start of the pattern.
func (p *pattern) path(tokens []*Token) Pattern {
// how many directory components are we expecting?
_depth := 0
for _, _token := range tokens {
if _token.Type == SEPARATOR {
_depth++
}
}
// return the pattern instance
return &path{pattern: *p, _depth: _depth}
} // path()
// Match returns true if the given path matches the path pattern. If the
// pattern is meant for directories only, and the path is not a directory,
// Match will return false. The matching is performed by fnmatch()
// with flags set to FNM_PATHNAME. It is assumed path is relative to the
// base path of the owning GitIgnore.
func (p *path) Match(path string, isdir bool) bool {
// are we expecting a directory
if p._directory && !isdir {
return false
}
if fnmatch.Match(p._fnmatch, path, fnmatch.FNM_PATHNAME) {
return true
} else if p._anchored {
return false
}
// match against the trailing path elements
return fnmatch.Match(p._fnmatch, path, fnmatch.FNM_PATHNAME)
} // Match()
//
// "any" patterns
//
// any returns a Pattern designed to match paths that include at least one
// any pattern '**', specifying recursive matching.
func (p *pattern) any(tokens []*Token) Pattern {
// consider only the non-SEPARATOR tokens, as these will be matched
// against the path components
_tokens := make([]*Token, 0)
for _, _token := range tokens {
if _token.Type != SEPARATOR {
_tokens = append(_tokens, _token)
}
}
return &any{*p, _tokens}
} // any()
// Match returns true if the given path matches the any pattern. If the
// pattern is meant for directories only, and the path is not a directory,
// Match will return false. The matching is performed by recursively applying
// fnmatch() with flags set to FNM_PATHNAME. It is assumed path is relative to
// the base path of the owning GitIgnore.
func (a *any) Match(path string, isdir bool) bool {
// are we expecting a directory?
if a._directory && !isdir {
return false
}
// split the path into components
_parts := strings.Split(path, string(_SEPARATOR))
// attempt to match the parts against the pattern tokens
return a.match(_parts, a._tokens)
} // Match()
// match performs the recursive matching for 'any' patterns. An 'any'
// token '**' may match any path component, or no path component.
func (a *any) match(path []string, tokens []*Token) bool {
// if we have no more tokens, then we have matched this path
// if there are also no more path elements, otherwise there's no match
if len(tokens) == 0 {
return len(path) == 0
}
// what token are we trying to match?
_token := tokens[0]
switch _token.Type {
case ANY:
if len(path) == 0 {
return a.match(path, tokens[1:])
} else {
return a.match(path, tokens[1:]) || a.match(path[1:], tokens)
}
default:
// if we have a non-ANY token, then we must have a non-empty path
if len(path) != 0 {
// if the current path element matches this token,
// we match if the remainder of the path matches the
// remaining tokens
if fnmatch.Match(_token.Token(), path[0], fnmatch.FNM_PATHNAME) {
return a.match(path[1:], tokens[1:])
}
}
}
// if we are here, then we have no match
return false
} // match()
// ensure the patterns confirm to the Pattern interface
var _ Pattern = &name{}
var _ Pattern = &path{}
var _ Pattern = &any{}