filter: support nested {} alternates in glob filters - fixes #7220

This commit is contained in:
maximilize
2026-06-28 00:16:46 +02:00
committed by Nick Craig-Wood
parent b59a17e5e8
commit 294e985e2e
2 changed files with 24 additions and 16 deletions

View File

@@ -83,7 +83,7 @@ func globToRegexp(glob string, pathMode bool, addAnchors bool, ignoreCase bool)
buf := re.Bytes()
buf[len(buf)-1] = c
}
inBraces := false
braceDepth := 0
inBrackets := 0
slashed := false
inRegexp := false // inside {{ ... }}
@@ -152,25 +152,27 @@ func globToRegexp(glob string, pathMode bool, addAnchors bool, ignoreCase bool)
case ']':
return nil, fmt.Errorf("mismatched ']' in glob %q", glob)
case '{':
if inBraces {
if last == '{' {
inRegexp = true
inBraces = false
} else {
return nil, fmt.Errorf("can't nest '{' '}' in glob %q", glob)
}
if braceDepth > 0 && last == '{' {
// {{ starts a raw regexp section. The '(' written by
// the first '{' wraps the regexp, so undo its depth.
// (An escaped \{ does not open a brace, so braceDepth
// guards against treating \{{ as a regexp section.)
inRegexp = true
braceDepth--
} else {
inBraces = true
// Open a brace group. These may nest, so each level
// emits its own balanced '(' ... ')'.
braceDepth++
_ = re.WriteByte('(')
}
case '}':
if !inBraces {
if braceDepth <= 0 {
return nil, fmt.Errorf("mismatched '{' and '}' in glob %q", glob)
}
_ = re.WriteByte(')')
inBraces = false
braceDepth--
case ',':
if inBraces {
if braceDepth > 0 {
_ = re.WriteByte('|')
} else {
_, _ = re.WriteRune(c)
@@ -189,7 +191,7 @@ func globToRegexp(glob string, pathMode bool, addAnchors bool, ignoreCase bool)
if inBrackets > 0 {
return nil, fmt.Errorf("mismatched '[' and ']' in glob %q", glob)
}
if inBraces {
if braceDepth != 0 {
return nil, fmt.Errorf("mismatched '{' and '}' in glob %q", glob)
}
if inRegexp {
@@ -209,7 +211,8 @@ var (
// Can't deal with
// / or ** in {}
// {{ regexp }}
tooHardRe = regexp.MustCompile(`({[^{}]*(\*\*|/)[^{}]*})|\{\{|\}\}`)
// nested {} (the inner braces are opaque to the directory split below)
tooHardRe = regexp.MustCompile(`({[^{}]*(\*\*|/)[^{}]*})|\{\{|\}\}|\{[^{}]*\{`)
// Squash all /
squashSlash = regexp.MustCompile(`/{2,}`)

View File

@@ -24,6 +24,8 @@ func TestGlobStringToRegexp(t *testing.T) {
{`'.' '+' '(' ')' '|' '^' '$'`, `'\.' '\+' '\(' '\)' '\|' '\^' '\$'`, ``},
{`*.jpg`, `.*\.jpg`, ``},
{`a{b,c,d}e`, `a(b|c|d)e`, ``},
{`a{b,{c,d}}e`, `a(b|(c|d))e`, ``},
{`{a,{b,c},{d,{e,f}}}`, `(a|(b|c)|(d|(e|f)))`, ``},
{`potato**`, ``, `too many stars`},
{`potato**sausage`, ``, `too many stars`},
{`*.p[lm]`, `.*\.p[lm]`, ``},
@@ -32,7 +34,7 @@ func TestGlobStringToRegexp(t *testing.T) {
{`***`, ``, `too many stars`},
{`ab]c`, ``, `mismatched ']'`},
{`ab[c`, ``, `mismatched '[' and ']'`},
{`ab{x{cd`, ``, `can't nest`},
{`ab{x{cd`, ``, `mismatched '{' and '}'`},
{`ab{}}cd`, ``, `mismatched '{' and '}'`},
{`ab}c`, ``, `mismatched '{' and '}'`},
{`ab{c`, ``, `mismatched '{' and '}'`},
@@ -92,6 +94,8 @@ func TestGlobPathToRegexp(t *testing.T) {
{`'.' '+' '(' ')' '|' '^' '$'`, `(^|/)'\.' '\+' '\(' '\)' '\|' '\^' '\$'$`, ``},
{`*.jpg`, `(^|/)[^/]*\.jpg$`, ``},
{`a{b,c,d}e`, `(^|/)a(b|c|d)e$`, ``},
{`a{b,{c,d}}e`, `(^|/)a(b|(c|d))e$`, ``},
{`{a,b/{c,d}/**}`, `(^|/)(a|b/(c|d)/.*)$`, ``},
{`potato**`, `(^|/)potato.*$`, ``},
{`potato**sausage`, `(^|/)potato.*sausage$`, ``},
{`*.p[lm]`, `(^|/)[^/]*\.p[lm]$`, ``},
@@ -100,7 +104,7 @@ func TestGlobPathToRegexp(t *testing.T) {
{`***`, ``, `too many stars`},
{`ab]c`, ``, `mismatched ']'`},
{`ab[c`, ``, `mismatched '[' and ']'`},
{`ab{x{cd`, ``, `can't nest`},
{`ab{x{cd`, ``, `mismatched '{' and '}'`},
{`ab{}}cd`, ``, `mismatched '{' and '}'`},
{`ab}c`, ``, `mismatched '{' and '}'`},
{`ab{c`, ``, `mismatched '{' and '}'`},
@@ -158,6 +162,7 @@ func TestGlobToDirGlobs(t *testing.T) {
{`a/b`, []string{"a/"}},
{`a/b/*.{jpg,png,gif}`, []string{"a/b/", "a/"}},
{`/a/{jpg,png,gif}/*.{jpg,png,gif}`, []string{"/a/{jpg,png,gif}/", "/a/", "/"}},
{`a/{b,{c,d}}/*.jpg`, []string{"/**"}},
{`a/{a,a*b,a**c}/d/`, []string{"/**"}},
{`/a/{a,a*b,a/c,d}/d/`, []string{"/**"}},
{`/a/{{.*}}/d/`, []string{"/**"}},