glob/glob.go

194 lines
4.2 KiB
Go

package glob
import (
"strings"
"github.com/gobwas/glob/match"
"fmt"
)
const (
any = '*'
single = '?'
escape = '\\'
range_open = '['
range_close = ']'
)
const (
inside_range_not = '!'
inside_range_minus = '-'
)
var syntaxPhrases = string([]byte{any, single, escape, range_open, range_close})
// Glob represents compiled glob pattern.
type Glob interface {
Match(string) bool
}
// New creates Glob for given pattern and uses other given (if any) strings as separators.
// The pattern syntax is:
//
// pattern:
// { term }
// term:
// `*` matches any sequence of non-separator characters
// `**` matches any sequence of characters
// `?` matches any single non-separator character
// c matches character c (c != `*`, `**`, `?`, `\`)
// `\` c matches character c
func New(pattern string, separators ...string) (Glob, error) {
chunks, err := parse(pattern, strings.Join(separators, ""), state{})
if err != nil {
return nil, err
}
switch len(chunks) {
case 1:
return chunks[0].matcher, nil
case 2:
if chunks[0].matcher.Kind() == match.KindRaw && chunks[1].matcher.Kind() == match.KindMultipleSuper {
return &match.Prefix{chunks[0].str}, nil
}
if chunks[1].matcher.Kind() == match.KindRaw && chunks[0].matcher.Kind() == match.KindMultipleSuper {
return &match.Suffix{chunks[1].str}, nil
}
case 3:
if chunks[0].matcher.Kind() == match.KindRaw && chunks[1].matcher.Kind() == match.KindMultipleSuper && chunks[2].matcher.Kind() == match.KindRaw {
return &match.PrefixSuffix{chunks[0].str, chunks[2].str}, nil
}
}
var c []match.Matcher
for _, chunk := range chunks {
c = append(c, chunk.matcher)
}
return &match.Composite{c}, nil
}
// parse parsed given pattern into list of tokens
func parse(str string, sep string, st state) ([]token, error) {
if len(str) == 0 {
return st.tokens, nil
}
// if there are no syntax symbols - pattern is simple string
i := strings.IndexAny(str, syntaxPhrases)
if i == -1 {
return append(st.tokens, token{match.Raw{str}, str}), nil
}
c := string(str[i])
// if syntax symbol is not at the start of pattern - add raw part before it
if i > 0 {
st.tokens = append(st.tokens, token{match.Raw{str[0:i]}, str[0:i]})
}
// if we are in escape state
if st.escape {
st.tokens = append(st.tokens, token{match.Raw{c}, c})
st.escape = false
} else {
switch str[i] {
case range_open:
closed := indexByteNonEscaped(str, range_close, escape, 0)
if closed == -1 {
return nil, fmt.Errorf("'%s' should be closed with '%s'", string(range_open), string(range_close))
}
r := str[i+1:closed]
g, err := parseRange(r)
if err != nil {
return nil, err
}
st.tokens = append(st.tokens, token{g, r})
if closed == len(str) -1 {
return st.tokens, nil
}
return parse(str[closed+1:], sep, st)
case escape:
st.escape = true
case any:
if len(str) > i+1 && str[i+1] == any {
st.tokens = append(st.tokens, token{match.Multiple{}, c})
return parse(str[i+len(c)+1:], sep, st)
}
st.tokens = append(st.tokens, token{match.Multiple{sep}, c})
case single:
st.tokens = append(st.tokens, token{match.Single{sep}, c})
}
}
return parse(str[i+len(c):], sep, st)
}
func parseRange(def string) (match.Matcher, error) {
var (
not bool
esc bool
minus bool
minusIndex int
b []byte
)
for i, c := range []byte(def) {
if esc {
b = append(b, c)
esc = false
continue
}
switch c{
case inside_range_not:
if i == 0 {
not = true
}
case escape:
if i == len(def) - 1 {
return nil, fmt.Errorf("there should be any character after '%s'", string(escape))
}
esc = true
case inside_range_minus:
minus = true
minusIndex = len(b)
default:
b = append(b, c)
}
}
if len(b) == 0 {
return nil, fmt.Errorf("range could not be empty")
}
def = string(b)
if minus {
r := []rune(def)
if len(r) != 2 || minusIndex != 1 {
return nil, fmt.Errorf("invalid range syntax: '%s' should be between two characters", string(inside_range_minus))
}
return &match.Between{r[0], r[1], not}, nil
}
return &match.RangeList{def, not}, nil
}
type token struct {
matcher match.Matcher
str string
}
type state struct {
escape bool
tokens []token
}