From 9d2bfa07fe63ebea6ef3b5a6b2d8080d1ca7e48c Mon Sep 17 00:00:00 2001 From: Russole <850905junior@gmail.com> Date: Sat, 30 May 2026 04:29:27 +0800 Subject: [PATCH 1/2] KAFKA-20642: Support character ranges in Kafka shell globs --- .../kafka/shell/glob/GlobComponent.java | 48 ++++++++++++++++++- .../kafka/shell/glob/GlobComponentTest.java | 28 +++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java b/shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java index bca13dd8c8fe4..35fd8e4cfb5dc 100644 --- a/shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java +++ b/shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java @@ -43,11 +43,52 @@ private static boolean isRegularExpressionSpecialCharacter(char ch) { */ private static boolean isGlobSpecialCharacter(char ch) { return switch (ch) { - case '*', '?', '\\', '{', '}' -> true; + case '*', '?', '\\', '{', '}', '[', ']' -> true; default -> false; }; } + private static void appendCharacterClassCharacter(StringBuilder output, char c) { + if (c == '\\' || c == '[') { + output.append('\\'); + } + output.append(c); + } + + private static int appendCharacterClass(String glob, int start, StringBuilder output) { + int i = start + 1; + if (i == glob.length()) { + throw new RuntimeException("Unterminated glob character class."); + } + + output.append('['); + if (glob.charAt(i) == '!' || glob.charAt(i) == '^') { + output.append('^'); + i++; + } + if (i == glob.length() || glob.charAt(i) == ']') { + throw new RuntimeException("Empty glob character class."); + } + + for (; i < glob.length(); i++) { + char c = glob.charAt(i); + if (c == ']') { + output.append(']'); + return i + 1; + } else if (c == '\\') { + if (i + 1 == glob.length()) { + output.append("\\\\"); + } else { + appendCharacterClassCharacter(output, glob.charAt(++i)); + } + } else { + appendCharacterClassCharacter(output, c); + } + } + + throw new RuntimeException("Unterminated glob character class."); + } + /** * Converts a glob string to a regular expression string. * Returns null if the glob should be handled as a literal (can only match one string). @@ -107,7 +148,10 @@ static String toRegularExpression(String glob) { output.append(c); } break; - // TODO: handle character ranges + case '[': + literal = false; + i = appendCharacterClass(glob, i - 1, output); + break; default: if (isRegularExpressionSpecialCharacter(c)) { output.append('\\'); diff --git a/shell/src/test/java/org/apache/kafka/shell/glob/GlobComponentTest.java b/shell/src/test/java/org/apache/kafka/shell/glob/GlobComponentTest.java index e86b471e30154..4206c5d167347 100644 --- a/shell/src/test/java/org/apache/kafka/shell/glob/GlobComponentTest.java +++ b/shell/src/test/java/org/apache/kafka/shell/glob/GlobComponentTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @Timeout(value = 120) @@ -50,6 +51,17 @@ public void testToRegularExpression() { assertEquals("^\\$blah.*$", GlobComponent.toRegularExpression("$blah*")); assertEquals("^.*$", GlobComponent.toRegularExpression("*")); assertEquals("^foo(?:(?:bar)|(?:baz))$", GlobComponent.toRegularExpression("foo{bar,baz}")); + assertEquals("^topic-[0-9]$", GlobComponent.toRegularExpression("topic-[0-9]")); + assertEquals("^topic-[abc]$", GlobComponent.toRegularExpression("topic-[abc]")); + assertEquals("^topic-[^abc]$", GlobComponent.toRegularExpression("topic-[!abc]")); + assertEquals("^topic-[^abc]$", GlobComponent.toRegularExpression("topic-[^abc]")); + } + + @Test + public void testMalformedCharacterClass() { + assertThrows(RuntimeException.class, () -> GlobComponent.toRegularExpression("topic-[abc")); + assertThrows(RuntimeException.class, () -> GlobComponent.toRegularExpression("topic-[]")); + assertThrows(RuntimeException.class, () -> GlobComponent.toRegularExpression("topic-[!]")); } @Test @@ -71,5 +83,21 @@ public void testGlobMatch() { assertFalse(foobarOrFoobaz.matches("foobah")); assertFalse(foobarOrFoobaz.matches("foo")); assertFalse(foobarOrFoobaz.matches("baz")); + GlobComponent digit = new GlobComponent("topic-[0-9]"); + assertFalse(digit.literal()); + assertTrue(digit.matches("topic-0")); + assertTrue(digit.matches("topic-5")); + assertFalse(digit.matches("topic-a")); + assertFalse(digit.matches("topic-10")); + GlobComponent letters = new GlobComponent("topic-[abc]"); + assertFalse(letters.literal()); + assertTrue(letters.matches("topic-a")); + assertTrue(letters.matches("topic-b")); + assertFalse(letters.matches("topic-d")); + GlobComponent notLetters = new GlobComponent("topic-[!abc]"); + assertFalse(notLetters.literal()); + assertTrue(notLetters.matches("topic-d")); + assertTrue(notLetters.matches("topic-1")); + assertFalse(notLetters.matches("topic-a")); } } From a57078d22a44879e81f2fc185ae9e11b33dcccc3 Mon Sep 17 00:00:00 2001 From: Russole <850905junior@gmail.com> Date: Sun, 31 May 2026 16:34:40 +0800 Subject: [PATCH 2/2] Add JavaDoc for glob character class helpers --- .../java/org/apache/kafka/shell/glob/GlobComponent.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java b/shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java index 35fd8e4cfb5dc..a694c317c346f 100644 --- a/shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java +++ b/shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java @@ -48,6 +48,10 @@ private static boolean isGlobSpecialCharacter(char ch) { }; } + /** + * Appends one character inside a regular expression character class, + * escaping characters that would otherwise be interpreted specially. + */ private static void appendCharacterClassCharacter(StringBuilder output, char c) { if (c == '\\' || c == '[') { output.append('\\'); @@ -55,6 +59,10 @@ private static void appendCharacterClassCharacter(StringBuilder output, char c) output.append(c); } + /** + * Appends a glob character class as a regular expression character class. + * Returns the index immediately after the closing bracket. + */ private static int appendCharacterClass(String glob, int start, StringBuilder output) { int i = start + 1; if (i == glob.length()) {