From 294e985e2efce147604cda48937532bb0650579f Mon Sep 17 00:00:00 2001 From: maximilize <3752128+maximilize@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:16:46 +0200 Subject: [PATCH] filter: support nested {} alternates in glob filters - fixes #7220 --- fs/filter/glob.go | 31 +++++++++++++++++-------------- fs/filter/glob_test.go | 9 +++++++-- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/fs/filter/glob.go b/fs/filter/glob.go index 40c0c56e2..ac93f4c7d 100644 --- a/fs/filter/glob.go +++ b/fs/filter/glob.go @@ -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,}`) diff --git a/fs/filter/glob_test.go b/fs/filter/glob_test.go index a20a77775..f1b8e7dc9 100644 --- a/fs/filter/glob_test.go +++ b/fs/filter/glob_test.go @@ -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{"/**"}},