diff --git a/compiler/compiler.go b/compiler/compiler.go index 02e7de8..f325157 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -346,7 +346,7 @@ func minimizeTreeAnyOf(tree *ast.Node) *ast.Node { case len(anyOf) == 1 && anyOf[0].Kind != ast.KindNothing: result = append(result, anyOf[0]) case len(anyOf) > 1: - result = append(result, ast.NewNode(ast.KindAnyOf, nil, anyOf...)) + result = append(result, ast.NewNode(ast.KindAnyOf, tree.Value, anyOf...)) } if commonRightCount > 0 { @@ -469,6 +469,12 @@ func compile(tree *ast.Node, sep []rune) (m match.Matcher, err error) { if err != nil { return nil, err } + + v := tree.Value.(ast.AnyOf) + if v.Not { + return match.NewNotAnyOf(matchers...), nil + } + return match.NewAnyOf(matchers...), nil case ast.KindPattern: diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index b58b1eb..5e50bbc 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -1,11 +1,12 @@ package compiler import ( + "reflect" + "testing" + "github.com/gobwas/glob/match" "github.com/gobwas/glob/match/debug" "github.com/gobwas/glob/syntax/ast" - "reflect" - "testing" ) var separators = []rune{'.'} @@ -414,7 +415,7 @@ func TestCompiler(t *testing.T) { { ast: ast.NewNode(ast.KindPattern, nil, ast.NewNode(ast.KindText, ast.Text{"/"}), - ast.NewNode(ast.KindAnyOf, nil, + ast.NewNode(ast.KindAnyOf, ast.AnyOf{}, ast.NewNode(ast.KindText, ast.Text{"z"}), ast.NewNode(ast.KindText, ast.Text{"ab"}), ), @@ -523,9 +524,9 @@ func TestCompiler(t *testing.T) { }, { ast: ast.NewNode(ast.KindPattern, nil, - ast.NewNode(ast.KindAnyOf, nil, + ast.NewNode(ast.KindAnyOf, ast.AnyOf{}, ast.NewNode(ast.KindPattern, nil, - ast.NewNode(ast.KindAnyOf, nil, + ast.NewNode(ast.KindAnyOf, ast.AnyOf{}, ast.NewNode(ast.KindPattern, nil, ast.NewNode(ast.KindText, ast.Text{"abc"}), ), @@ -537,7 +538,7 @@ func TestCompiler(t *testing.T) { }, { ast: ast.NewNode(ast.KindPattern, nil, - ast.NewNode(ast.KindAnyOf, nil, + ast.NewNode(ast.KindAnyOf, ast.AnyOf{}, ast.NewNode(ast.KindPattern, nil, ast.NewNode(ast.KindText, ast.Text{"abc"}), ast.NewNode(ast.KindSingle, nil), @@ -584,7 +585,7 @@ func TestCompiler(t *testing.T) { }, { ast: ast.NewNode(ast.KindPattern, nil, - ast.NewNode(ast.KindAnyOf, nil, + ast.NewNode(ast.KindAnyOf, ast.AnyOf{}, ast.NewNode(ast.KindPattern, nil, ast.NewNode(ast.KindText, ast.Text{"abc"}), ast.NewNode(ast.KindList, ast.List{Chars: "abc"}), @@ -609,6 +610,70 @@ func TestCompiler(t *testing.T) { }..., ), }, + // {!foo,bar} + { + ast: ast.NewNode(ast.KindPattern, nil, + &ast.Node{ + Kind: ast.KindAnyOf, + Value: ast.AnyOf{ + Not: true, + }, + Children: []*ast.Node{ + ast.NewNode(ast.KindPattern, nil, ast.NewNode(ast.KindText, ast.Text{"foo"})), + ast.NewNode(ast.KindPattern, nil, ast.NewNode(ast.KindText, ast.Text{"bar"})), + }, + }, + ), + result: match.NewNotAnyOf( + match.NewText("foo"), + match.NewText("bar"), + ), + }, + // {!foo,bar}baz + { + ast: ast.NewNode(ast.KindPattern, nil, + &ast.Node{ + Kind: ast.KindAnyOf, + Value: ast.AnyOf{ + Not: true, + }, + Children: []*ast.Node{ + ast.NewNode(ast.KindPattern, nil, ast.NewNode(ast.KindText, ast.Text{"foo"})), + ast.NewNode(ast.KindPattern, nil, ast.NewNode(ast.KindText, ast.Text{"bar"})), + }, + }, + ast.NewNode(ast.KindText, ast.Text{"baz"}), + ), + result: match.NewBTree( + match.NewText("baz"), + match.NewNotAnyOf( + match.NewText("foo"), + match.NewText("bar"), + ), + nil, + ), + }, + // {!nginx*} + { + ast: ast.NewNode(ast.KindPattern, nil, + &ast.Node{ + Kind: ast.KindAnyOf, + Value: ast.AnyOf{ + Not: true, + }, + Children: []*ast.Node{ + ast.NewNode(ast.KindPattern, nil, + ast.NewNode(ast.KindText, ast.Text{"nginx"}), + ast.NewNode(ast.KindAny, nil), + ), + }, + }, + ), + sep: nil, + result: match.NewNotAnyOf( + match.NewPrefix("nginx"), + ), + }, } { m, err := Compile(test.ast, test.sep) if err != nil { diff --git a/glob_test.go b/glob_test.go index 810036f..9a57840 100644 --- a/glob_test.go +++ b/glob_test.go @@ -124,6 +124,20 @@ func TestGlob(t *testing.T) { glob(true, "{*.google.*,*.yandex.*}", "www.yandex.com", '.'), glob(false, "{*.google.*,*.yandex.*}", "yandex.com", '.'), glob(false, "{*.google.*,*.yandex.*}", "google.com", '.'), + glob(true, "{!*.google.*,*.yandex.*}", "google.com", '.'), + glob(true, "{!*.google.*,*.yandex.*}", "www.rambler.com", '.'), + + glob(false, "{!abc,cde}", "cde"), + glob(true, "{!abc,cde}", "qwe"), + glob(true, "{!nginx,haproxy}_*", "caddy_abc"), + glob(false, "{!nginx,haproxy}_*", "nginx_abc"), + glob(true, "{!nginx,ha?roxy,caddy_[abc]}_server", "_server"), + glob(false, "{!ngin?,}*", "nginx"), + glob(true, "{!ngin*}-?", "caddy-1"), + glob(true, "{!*ngin?}-?", "caddy-2"), + glob(true, "{!{nginx,caddy}_*,nginx_123}", "haproxy_123"), + glob(true, "{!dev-?,stage-*}.example.{!org,net}", "www.example.com", '.'), + glob(false, "{!dev-?,stage-[0-9]}.example.{!org,net}", "stage-3.example.com", '.'), glob(true, "{*.google.*,yandex.*}", "www.google.com", '.'), glob(true, "{*.google.*,yandex.*}", "yandex.com", '.'), diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1854f22 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/gobwas/glob + +go 1.25.4 diff --git a/match/any_of_test.go b/match/any_of_test.go index 3b478cf..3694e28 100644 --- a/match/any_of_test.go +++ b/match/any_of_test.go @@ -41,8 +41,8 @@ func TestAnyOfIndex(t *testing.T) { []int{1}, }, } { - everyOf := NewAnyOf(test.matchers...) - index, segments := everyOf.Index(test.fixture) + anyOf := NewAnyOf(test.matchers...) + index, segments := anyOf.Index(test.fixture) if index != test.index { t.Errorf("#%d unexpected index: exp: %d, act: %d", id, test.index, index) } diff --git a/match/not_any_of.go b/match/not_any_of.go new file mode 100644 index 0000000..67a4373 --- /dev/null +++ b/match/not_any_of.go @@ -0,0 +1,49 @@ +package match + +import "fmt" + +type NotAnyOf struct { + Matchers Matchers +} + +func NewNotAnyOf(m ...Matcher) NotAnyOf { + return NotAnyOf{Matchers(m)} +} + +func (self NotAnyOf) Match(s string) bool { + for _, m := range self.Matchers { + if m.Match(s) { + return false + } + } + return true +} + +func (self NotAnyOf) Index(s string) (int, []int) { + match := false + + for _, m := range self.Matchers { + if idx, _ := m.Index(s); idx != -1 { + match = true + break // found at least one match for any of the internal matchers + } + } + + if match { + return -1, nil + } + + return 0, []int{len(s)} +} + +func (self NotAnyOf) Len() (l int) { + // The length of a NotAnyOf is typically considered unknown (-1) + // because it negates a set of potentially variable-length patterns. + // Or, if all internal matchers have the same fixed length, you might reuse that. + // For simplicity, returning -1 is safer for negation. + return -1 +} + +func (self NotAnyOf) String() string { + return fmt.Sprintf("", self.Matchers) +} diff --git a/match/not_any_of_test.go b/match/not_any_of_test.go new file mode 100644 index 0000000..546c9e4 --- /dev/null +++ b/match/not_any_of_test.go @@ -0,0 +1,86 @@ +package match + +import ( + "reflect" + "testing" +) + +func TestNotAnyOfIndex(t *testing.T) { + for id, test := range []struct { + matchers Matchers + fixture string + index int + segments []int + }{ + { + Matchers{ + NewText("foo"), + NewText("bar"), + }, + "abc", + 0, + []int{3}, + }, + { + Matchers{ + NewText("a"), + NewText("b"), + NewText("c"), + }, + "b", + -1, + nil, + }, + { + Matchers{ + NewPrefix("caddy"), + }, + "caddy-1", + -1, + nil, + }, + { + Matchers{ + NewText("long_string"), + NewText("short"), + }, + "medium", + 0, + []int{6}, + }, + { + Matchers{ + NewSuper(), + }, + "any_string", + -1, + nil, + }, + { + Matchers{ + NewText("a"), + }, + "", + 0, + []int{0}, + }, + { + Matchers{ + NewText(""), + }, + "", + -1, + nil, + }, + } { + notAnyOf := NewNotAnyOf(test.matchers...) + index, segments := notAnyOf.Index(test.fixture) + if index != test.index { + t.Errorf("#%d unexpected index: exp: %d, act: %d for fixture '%s'", id, test.index, index, test.fixture) + } + + if !reflect.DeepEqual(segments, test.segments) { + t.Errorf("#%d unexpected segments: exp: %v, act: %v for fixture '%s'", id, test.segments, segments, test.fixture) + } + } +} diff --git a/syntax/ast/ast.go b/syntax/ast/ast.go index 3220a69..892e03f 100644 --- a/syntax/ast/ast.go +++ b/syntax/ast/ast.go @@ -82,6 +82,10 @@ type Text struct { Text string } +type AnyOf struct { + Not bool +} + type Kind int const ( diff --git a/syntax/ast/parser.go b/syntax/ast/parser.go index 429b409..2154219 100644 --- a/syntax/ast/parser.go +++ b/syntax/ast/parser.go @@ -3,8 +3,9 @@ package ast import ( "errors" "fmt" - "github.com/gobwas/glob/syntax/lexer" "unicode/utf8" + + "github.com/gobwas/glob/syntax/lexer" ) type Lexer interface { @@ -62,7 +63,7 @@ func parserMain(tree *Node, lex Lexer) (parseFn, *Node, error) { return parserRange, tree, nil case lexer.TermsOpen: - a := NewNode(KindAnyOf, nil) + a := NewNode(KindAnyOf, AnyOf{}) Insert(tree, a) p := NewNode(KindPattern, nil) @@ -79,11 +80,19 @@ func parserMain(tree *Node, lex Lexer) (parseFn, *Node, error) { case lexer.TermsClose: return parserMain, tree.Parent.Parent, nil + case lexer.Not: + if tree.Parent != nil && tree.Parent.Kind == KindAnyOf { + v := tree.Parent.Value.(AnyOf) + v.Not = true + tree.Parent.Value = v + } + + return parserMain, tree, nil + default: return nil, tree, fmt.Errorf("unexpected token: %s", token) } } - return nil, tree, fmt.Errorf("unknown error") } func parserRange(tree *Node, lex Lexer) (parseFn, *Node, error) { diff --git a/syntax/ast/parser_test.go b/syntax/ast/parser_test.go index a469d38..7e62c5a 100644 --- a/syntax/ast/parser_test.go +++ b/syntax/ast/parser_test.go @@ -116,7 +116,7 @@ func TestParseString(t *testing.T) { {lexer.EOF, ""}, }, tree: NewNode(KindPattern, nil, - NewNode(KindAnyOf, nil, + NewNode(KindAnyOf, AnyOf{}, NewNode(KindPattern, nil, NewNode(KindText, Text{Text: "a"}), ), @@ -140,7 +140,7 @@ func TestParseString(t *testing.T) { }, tree: NewNode(KindPattern, nil, NewNode(KindText, Text{Text: "/"}), - NewNode(KindAnyOf, nil, + NewNode(KindAnyOf, AnyOf{}, NewNode(KindPattern, nil, NewNode(KindText, Text{Text: "z"}), ), @@ -179,12 +179,12 @@ func TestParseString(t *testing.T) { {lexer.EOF, ""}, }, tree: NewNode(KindPattern, nil, - NewNode(KindAnyOf, nil, + NewNode(KindAnyOf, AnyOf{}, NewNode(KindPattern, nil, NewNode(KindText, Text{Text: "a"}), ), NewNode(KindPattern, nil, - NewNode(KindAnyOf, nil, + NewNode(KindAnyOf, AnyOf{}, NewNode(KindPattern, nil, NewNode(KindText, Text{Text: "x"}), ), @@ -205,6 +205,30 @@ func TestParseString(t *testing.T) { ), ), }, + { + //pattern: "{!z,ab}*", + tokens: []lexer.Token{ + {lexer.TermsOpen, "{"}, + {lexer.Not, "!"}, + {lexer.Text, "z"}, + {lexer.Separator, ","}, + {lexer.Text, "ab"}, + {lexer.TermsClose, "}"}, + {lexer.Any, "*"}, + {lexer.EOF, ""}, + }, + tree: NewNode(KindPattern, nil, + NewNode(KindAnyOf, AnyOf{true}, + NewNode(KindPattern, nil, + NewNode(KindText, Text{Text: "z"}), + ), + NewNode(KindPattern, nil, + NewNode(KindText, Text{Text: "ab"}), + ), + ), + NewNode(KindAny, nil), + ), + }, } { lexer := &stubLexer{tokens: test.tokens} result, err := Parse(lexer) diff --git a/syntax/lexer/lexer.go b/syntax/lexer/lexer.go index a1c8d19..55cb12b 100644 --- a/syntax/lexer/lexer.go +++ b/syntax/lexer/lexer.go @@ -3,8 +3,9 @@ package lexer import ( "bytes" "fmt" - "github.com/gobwas/glob/util/runes" "unicode/utf8" + + "github.com/gobwas/glob/util/runes" ) const ( @@ -16,7 +17,7 @@ const ( char_range_close = ']' char_terms_open = '{' char_terms_close = '}' - char_range_not = '!' + char_not = '!' char_range_between = '-' ) @@ -159,6 +160,12 @@ func (l *lexer) fetchItem() { l.termsEnter() l.tokens.push(Token{TermsOpen, string(r)}) + if l.read() == char_not { + l.tokens.push(Token{Not, string(char_not)}) + } else { + l.unread() + } + case r == char_comma && l.inTerms(): l.tokens.push(Token{Separator, string(r)}) @@ -220,7 +227,7 @@ func (l *lexer) fetchRange() { continue } - if !seenNot && r == char_range_not { + if !seenNot && r == char_not { l.tokens.push(Token{Not, string(r)}) seenNot = true continue diff --git a/syntax/lexer/lexer_test.go b/syntax/lexer/lexer_test.go index ec35f81..5341f16 100644 --- a/syntax/lexer/lexer_test.go +++ b/syntax/lexer/lexer_test.go @@ -138,6 +138,18 @@ func TestLexGood(t *testing.T) { {EOF, ""}, }, }, + { + pattern: "{!a,b}", + items: []Token{ + {TermsOpen, "{"}, + {Not, "!"}, + {Text, "a"}, + {Separator, ","}, + {Text, "b"}, + {TermsClose, "}"}, + {EOF, ""}, + }, + }, { pattern: "/{z,ab}*", items: []Token{ @@ -177,6 +189,34 @@ func TestLexGood(t *testing.T) { {EOF, ""}, }, }, + { + pattern: "{![!日-語],*,?,{!a,b,\\c}}", + items: []Token{ + {TermsOpen, "{"}, + {Not, "!"}, + {RangeOpen, "["}, + {Not, "!"}, + {RangeLo, "日"}, + {RangeBetween, "-"}, + {RangeHi, "語"}, + {RangeClose, "]"}, + {Separator, ","}, + {Any, "*"}, + {Separator, ","}, + {Single, "?"}, + {Separator, ","}, + {TermsOpen, "{"}, + {Not, "!"}, + {Text, "a"}, + {Separator, ","}, + {Text, "b"}, + {Separator, ","}, + {Text, "c"}, + {TermsClose, "}"}, + {TermsClose, "}"}, + {EOF, ""}, + }, + }, } { lexer := NewLexer(test.pattern) for i, exp := range test.items {