-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathglob.go
267 lines (233 loc) · 7.01 KB
/
glob.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
// Package ohmyglob provides a minimal glob matching utility.
package ohmyglob
import (
"bytes"
"fmt"
"io"
"os"
"regexp"
"strings"
log "github.com/cihub/seelog"
)
var (
// Logger is used to log trace-level info; logging is completely disabled by default but can be changed by replacing
// this with a configured logger
Logger log.LoggerInterface
// Escaper is the character used to escape a meaningful character
Escaper = '\\'
// Runes that, in addition to the separator, mean something when they appear in the glob (includes Escaper)
expanders = []rune{'?', '*', '!', Escaper}
)
func init() {
if Logger == nil {
var err error
Logger, err = log.LoggerFromWriterWithMinLevel(os.Stderr, log.CriticalLvl) // seelog bug means we can't use log.Off
if err != nil {
panic(err)
}
}
}
type processedToken struct {
contents *bytes.Buffer
tokenType tc
}
type parserState struct {
options *Options
// The regex-escaped separator character
escapedSeparator string
processedTokens []processedToken
}
// GlobMatcher is the basic interface of a Glob or GlobSet. It provides a Regexp-style interface for checking matches.
type GlobMatcher interface {
// Match reports whether the Glob matches the byte slice b
Match(b []byte) bool
// MatchReader reports whether the Glob matches the text read by the RuneReader
MatchReader(r io.RuneReader) bool
// MatchString reports whether the Glob matches the string s
MatchString(s string) bool
}
// Glob is a single glob pattern; implements GlobMatcher. A Glob is immutable.
type Glob interface {
GlobMatcher
// String returns the pattern that was used to create the Glob
String() string
// IsNegative returns whether the pattern was negated (prefixed with !)
IsNegative() bool
}
// Glob is a glob pattern that has been compiled into a regular expression.
type globImpl struct {
*regexp.Regexp
// The separator from options, escaped for appending to the regexBuffer (only available during parsing)
// The input pattern
globPattern string
// State only available during parsing
parserState *parserState
// Set to true if the pattern was negated
negated bool
}
// Options modify the behaviour of Glob parsing
type Options struct {
// The character used to split path components
Separator rune
// Set to false to allow any prefix before the glob match
MatchAtStart bool
// Set to false to allow any suffix after the glob match
MatchAtEnd bool
}
// DefaultOptions are a default set of Options that uses a forward slash as a separator, and require a full match
var DefaultOptions = &Options{
Separator: '/',
MatchAtStart: true,
MatchAtEnd: true,
}
func (g *globImpl) String() string {
return g.globPattern
}
func (g *globImpl) IsNegative() bool {
return g.negated
}
func popLastToken(state *parserState) *processedToken {
state.processedTokens = state.processedTokens[:len(state.processedTokens)-1]
if len(state.processedTokens) > 0 {
return &state.processedTokens[len(state.processedTokens)-1]
}
return &processedToken{}
}
// Compile parses the given glob pattern and convertes it to a Glob. If no options are given, the DefaultOptions are
// used.
func Compile(pattern string, options *Options) (Glob, error) {
pattern = strings.TrimSpace(pattern)
reader := strings.NewReader(pattern)
if options == nil {
options = DefaultOptions
} else {
// Check that the separator is not an expander
for _, expander := range expanders {
if options.Separator == expander {
return nil, fmt.Errorf("'%s' is not allowed as a separator", string(options.Separator))
}
}
}
state := &parserState{
options: options,
escapedSeparator: escapeRegexComponent(string(options.Separator)),
processedTokens: make([]processedToken, 0, 10),
}
glob := &globImpl{
Regexp: nil,
globPattern: pattern,
negated: false,
parserState: state,
}
regexBuf := new(bytes.Buffer)
if options.MatchAtStart {
regexBuf.WriteRune('^')
}
var err error
// Transform into a regular expression pattern
// 1. Parse negation prefixes
err = parseNegation(reader, glob)
if err != nil {
return nil, err
}
// 2. Tokenise and convert!
tokeniser := newGlobTokeniser(reader, options)
lastProcessedToken := &processedToken{}
for tokeniser.Scan() {
if err = tokeniser.Err(); err != nil {
return nil, err
}
token, tokenType := tokeniser.Token()
t := processedToken{
contents: nil,
tokenType: tokenType,
}
// Special cases
if tokenType == tcGlobStar && tokeniser.Peek() {
// If this is a globstar and the next token is a separator, consume it (the globstar pattern itself includes
// a separator)
if err = tokeniser.PeekErr(); err != nil {
return nil, err
}
_, peekedType := tokeniser.PeekToken()
if peekedType == tcSeparator {
tokeniser.Scan()
}
}
if tokenType == tcGlobStar && lastProcessedToken.tokenType == tcGlobStar {
// If the last token was a globstar and this is too, remove the last. We don't remove this globstar because
// it may now be the last in the pattern, which is special
lastProcessedToken = popLastToken(state)
}
if tokenType == tcGlobStar && lastProcessedToken.tokenType == tcSeparator && !tokeniser.Peek() {
// If this is the last token, and it's a globstar, remove a preceeding separator
lastProcessedToken = popLastToken(state)
}
t.contents, err = processToken(token, tokenType, glob, tokeniser)
if err != nil {
return nil, err
}
lastProcessedToken = &t
state.processedTokens = append(state.processedTokens, t)
}
for _, t := range state.processedTokens {
t.contents.WriteTo(regexBuf)
}
if options.MatchAtEnd {
regexBuf.WriteRune('$')
}
regexString := regexBuf.String()
Logger.Tracef("[ohmyglob:Glob] Compiled \"%s\" to regex `%s` (negated: %v)", pattern, regexString, glob.negated)
re, err := regexp.Compile(regexString)
if err != nil {
return nil, err
}
glob.parserState = nil
glob.Regexp = re
return glob, nil
}
func parseNegation(r io.RuneScanner, glob *globImpl) error {
for {
char, _, err := r.ReadRune()
if err != nil {
return err
} else if char == '!' {
glob.negated = !glob.negated
} else {
r.UnreadRune()
return nil
}
}
}
func processToken(token string, tokenType tc, glob *globImpl, tokeniser *globTokeniser) (*bytes.Buffer, error) {
state := glob.parserState
buf := new(bytes.Buffer)
switch tokenType {
case tcGlobStar:
// Globstars also take care of surrounding separators; separator components before and after a globstar are
// suppressed
isLast := !tokeniser.Peek()
buf.WriteString("(?:")
if isLast && len(glob.parserState.processedTokens) > 0 {
buf.WriteString(state.escapedSeparator)
}
buf.WriteString(".+")
if !isLast {
buf.WriteString(state.escapedSeparator)
}
buf.WriteString(")?")
case tcStar:
buf.WriteString("[^")
buf.WriteString(state.escapedSeparator)
buf.WriteString("]*")
case tcAny:
buf.WriteString("[^")
buf.WriteString(state.escapedSeparator)
buf.WriteString("]")
case tcSeparator:
buf.WriteString(state.escapedSeparator)
case tcLiteral:
buf.WriteString(escapeRegexComponent(token))
}
return buf, nil
}