From c564a59813495f53e72b5eab94fef6afe283246f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 06:26:45 +0000 Subject: [PATCH 1/2] Match a literal ] straight after a glob class [patch] DotNet.Glob read a "]" that directly follows a closed bracket class as another member of that class, so "*[[][0-9]]" matched "items[3" and not "items[3]", and the excluded form let through what it was written to hide. ExpandGlobClasses now writes each such "]" as the one-member class "[]]", which DotNet.Glob reads as the literal the pattern means. Fixes #172 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5LUHQ94WSDTYRpSJV --- TextFilter.Test/TextFilterTests.cs | 30 ++++++++++++++++++++++++++++++ TextFilter/TextFilter.cs | 9 +++++++++ 2 files changed, 39 insertions(+) diff --git a/TextFilter.Test/TextFilterTests.cs b/TextFilter.Test/TextFilterTests.cs index 3250a64..1556d2c 100644 --- a/TextFilter.Test/TextFilterTests.cs +++ b/TextFilter.Test/TextFilterTests.cs @@ -985,6 +985,9 @@ public void GlobClassWithATrailingDashMatchesTheDashLiterally() [DataRow("file[0-", "file[0-")] [DataRow("a]b", "a]b")] [DataRow("[a-]x[0-", "[-a]x[0-")] + [DataRow("[a]]", "[a][]]")] + [DataRow("[]]]", "[]][]]")] + [DataRow("*[[][0-9]]", "*[[][0-9][]]")] public void ExpandGlobClassesRewritesOnlyClassesDotNetGlobMisreads(string token, string expected) => Assert.AreEqual(expected, TextFilter.ExpandGlobClasses(token)); @@ -1026,6 +1029,33 @@ public void GlobClassMatchesEveryMemberOfEveryRange(string pattern, string match } } + [TestMethod] + [DataRow("*[[][0-9]]", "items[3]", "items[3")] + [DataRow("[a]]", "a]", "a")] + [DataRow("[ab]]", "b]", "b")] + [DataRow("[!a]]", "b]", "b")] + [DataRow("*[[][0-9a-f]]", "h[c]", "h[c")] + [DataRow("[a]]]", "a]]", "a]")] + [DataRow("*[[]DEBUG]*", "x[DEBUG]y", "xDEBUGy")] + public void GlobClassFollowedByALiteralBracketMatchesTheBracket(string pattern, string matching, string notMatching) + { + // DotNet.Glob read a "]" straight after a class as one more member of that class, so the pattern + // needed one character fewer than it was written with (#172). + foreach (TextFilterCaseSensitivity caseSensitivity in new[] { TextFilterCaseSensitivity.CaseSensitive, TextFilterCaseSensitivity.CaseInsensitive }) + { + Assert.IsTrue(TextFilter.IsMatch(matching, pattern, TextFilterType.Glob, TextFilterMatchOptions.ByWholeString, caseSensitivity), $"{pattern} should match {matching} ({caseSensitivity})"); + Assert.IsFalse(TextFilter.IsMatch(notMatching, pattern, TextFilterType.Glob, TextFilterMatchOptions.ByWholeString, caseSensitivity), $"{pattern} should not match {notMatching} ({caseSensitivity})"); + } + } + + [TestMethod] + public void FilterWithAClassFollowedByALiteralBracketKeepsAndExcludesTheRightItems() + { + List items = ["items[3]", "items[3", "notes.txt"]; + CollectionAssert.AreEqual(new List { "items[3]" }, TextFilter.Filter(items, "*[[][0-9]]").ToList()); + CollectionAssert.AreEqual(new List { "items[3", "notes.txt" }, TextFilter.Filter(items, "-*[[][0-9]]").ToList()); + } + [TestMethod] public void GlobClassWithBothLetterCasesMatchesEachCase() { diff --git a/TextFilter/TextFilter.cs b/TextFilter/TextFilter.cs index 927c00c..f959040 100644 --- a/TextFilter/TextFilter.cs +++ b/TextFilter/TextFilter.cs @@ -535,6 +535,15 @@ internal static IReadOnlyList ExpandGlobStarSegments(string maskedToken) result.Append(token, index, open - index).Append(rewritten); index = close + 1; + + // DotNet.Glob reads a "]" straight after a class as one more member of it, so "[a]]" would + // match "a" and not "a]". Each such "]" is written as the one-member class "[]]", which it + // reads as the literal the pattern means. + while (index < token.Length && token[index] == ']') + { + result.Append("[]]"); + index++; + } } return result.Append(token, index, token.Length - index).ToString(); From f8ebf6d8ceb04e255da6e03ee3bb076771f5cff3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 06:27:24 +0000 Subject: [PATCH 2/2] Move the #172 tests away from the lines #158's and #165's tests add to Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5LUHQ94WSDTYRpSJV --- TextFilter.Test/TextFilterTests.cs | 64 ++++++++++++++++-------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/TextFilter.Test/TextFilterTests.cs b/TextFilter.Test/TextFilterTests.cs index 1556d2c..1f27239 100644 --- a/TextFilter.Test/TextFilterTests.cs +++ b/TextFilter.Test/TextFilterTests.cs @@ -985,9 +985,6 @@ public void GlobClassWithATrailingDashMatchesTheDashLiterally() [DataRow("file[0-", "file[0-")] [DataRow("a]b", "a]b")] [DataRow("[a-]x[0-", "[-a]x[0-")] - [DataRow("[a]]", "[a][]]")] - [DataRow("[]]]", "[]][]]")] - [DataRow("*[[][0-9]]", "*[[][0-9][]]")] public void ExpandGlobClassesRewritesOnlyClassesDotNetGlobMisreads(string token, string expected) => Assert.AreEqual(expected, TextFilter.ExpandGlobClasses(token)); @@ -1029,33 +1026,6 @@ public void GlobClassMatchesEveryMemberOfEveryRange(string pattern, string match } } - [TestMethod] - [DataRow("*[[][0-9]]", "items[3]", "items[3")] - [DataRow("[a]]", "a]", "a")] - [DataRow("[ab]]", "b]", "b")] - [DataRow("[!a]]", "b]", "b")] - [DataRow("*[[][0-9a-f]]", "h[c]", "h[c")] - [DataRow("[a]]]", "a]]", "a]")] - [DataRow("*[[]DEBUG]*", "x[DEBUG]y", "xDEBUGy")] - public void GlobClassFollowedByALiteralBracketMatchesTheBracket(string pattern, string matching, string notMatching) - { - // DotNet.Glob read a "]" straight after a class as one more member of that class, so the pattern - // needed one character fewer than it was written with (#172). - foreach (TextFilterCaseSensitivity caseSensitivity in new[] { TextFilterCaseSensitivity.CaseSensitive, TextFilterCaseSensitivity.CaseInsensitive }) - { - Assert.IsTrue(TextFilter.IsMatch(matching, pattern, TextFilterType.Glob, TextFilterMatchOptions.ByWholeString, caseSensitivity), $"{pattern} should match {matching} ({caseSensitivity})"); - Assert.IsFalse(TextFilter.IsMatch(notMatching, pattern, TextFilterType.Glob, TextFilterMatchOptions.ByWholeString, caseSensitivity), $"{pattern} should not match {notMatching} ({caseSensitivity})"); - } - } - - [TestMethod] - public void FilterWithAClassFollowedByALiteralBracketKeepsAndExcludesTheRightItems() - { - List items = ["items[3]", "items[3", "notes.txt"]; - CollectionAssert.AreEqual(new List { "items[3]" }, TextFilter.Filter(items, "*[[][0-9]]").ToList()); - CollectionAssert.AreEqual(new List { "items[3", "notes.txt" }, TextFilter.Filter(items, "-*[[][0-9]]").ToList()); - } - [TestMethod] public void GlobClassWithBothLetterCasesMatchesEachCase() { @@ -1094,6 +1064,40 @@ public void GlobClassRangeSpanningAPathSeparatorMatchesIt(string pattern, string } } + [TestMethod] + [DataRow("[a]]", "[a][]]")] + [DataRow("[]]]", "[]][]]")] + [DataRow("*[[][0-9]]", "*[[][0-9][]]")] + public void ExpandGlobClassesWritesALiteralBracketAfterAClassAsItsOwnClass(string token, string expected) => + Assert.AreEqual(expected, TextFilter.ExpandGlobClasses(token)); + + [TestMethod] + [DataRow("*[[][0-9]]", "items[3]", "items[3")] + [DataRow("[a]]", "a]", "a")] + [DataRow("[ab]]", "b]", "b")] + [DataRow("[!a]]", "b]", "b")] + [DataRow("*[[][0-9a-f]]", "h[c]", "h[c")] + [DataRow("[a]]]", "a]]", "a]")] + [DataRow("*[[]DEBUG]*", "x[DEBUG]y", "xDEBUGy")] + public void GlobClassFollowedByALiteralBracketMatchesTheBracket(string pattern, string matching, string notMatching) + { + // DotNet.Glob read a "]" straight after a class as one more member of that class, so the pattern + // needed one character fewer than it was written with (#172). + foreach (TextFilterCaseSensitivity caseSensitivity in new[] { TextFilterCaseSensitivity.CaseSensitive, TextFilterCaseSensitivity.CaseInsensitive }) + { + Assert.IsTrue(TextFilter.IsMatch(matching, pattern, TextFilterType.Glob, TextFilterMatchOptions.ByWholeString, caseSensitivity), $"{pattern} should match {matching} ({caseSensitivity})"); + Assert.IsFalse(TextFilter.IsMatch(notMatching, pattern, TextFilterType.Glob, TextFilterMatchOptions.ByWholeString, caseSensitivity), $"{pattern} should not match {notMatching} ({caseSensitivity})"); + } + } + + [TestMethod] + public void FilterWithAClassFollowedByALiteralBracketKeepsAndExcludesTheRightItems() + { + List items = ["items[3]", "items[3", "notes.txt"]; + CollectionAssert.AreEqual(new List { "items[3]" }, TextFilter.Filter(items, "*[[][0-9]]").ToList()); + CollectionAssert.AreEqual(new List { "items[3", "notes.txt" }, TextFilter.Filter(items, "-*[[][0-9]]").ToList()); + } + [TestMethod] [DataRow("-readme[a-")] [DataRow("!readme[a-")]