Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 55 additions & 13 deletions shell/src/main/java/org/apache/kafka/shell/glob/GlobComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private static boolean isRegularExpressionSpecialCharacter(char ch) {
*/
private static boolean isGlobSpecialCharacter(char ch) {
return switch (ch) {
case '*', '?', '\\', '{', '}' -> true;
case '*', '?', '\\', '{', '}', '[', ']' -> true;
default -> false;
};
}
Expand All @@ -70,17 +70,7 @@ static String toRegularExpression(String glob) {
output.append(".*");
break;
case '\\':
if (i == glob.length()) {
output.append(c);
} else {
char next = glob.charAt(i);
i++;
if (isGlobSpecialCharacter(next) ||
isRegularExpressionSpecialCharacter(next)) {
output.append('\\');
}
output.append(next);
}
i = handleEscape(glob, i, output);
break;
case '{':
if (processingGroup) {
Expand All @@ -107,7 +97,10 @@ static String toRegularExpression(String glob) {
output.append(c);
}
break;
// TODO: handle character ranges
case '[':
literal = false;
i = handleBracket(glob, i, output);
break;
default:
if (isRegularExpressionSpecialCharacter(c)) {
output.append('\\');
Expand All @@ -125,6 +118,55 @@ static String toRegularExpression(String glob) {
return output.toString();
}

private static int handleEscape(String glob, int i, StringBuilder output) {
if (i == glob.length()) {
output.append('\\');
} else {
char next = glob.charAt(i++);
if (isGlobSpecialCharacter(next) || isRegularExpressionSpecialCharacter(next)) {
output.append('\\');
}
output.append(next);
}
return i;
}

private static int handleBracket(String glob, int i, StringBuilder output) {
output.append('[');
boolean foundClosingBracket = false;
// Handle negation: [!...] or [^...] -> [^...]
if (i < glob.length() && (glob.charAt(i) == '!' || glob.charAt(i) == '^')) {
output.append('^');
i++;
}
// Handle literal ] as first character: [] or [!]
if (i < glob.length() && glob.charAt(i) == ']') {
output.append(']');
i++;
}
while (i < glob.length()) {
char bracketChar = glob.charAt(i++);
if (bracketChar == ']') {
foundClosingBracket = true;
output.append(']');
break;
} else if (bracketChar == '\\' && i < glob.length()) {
// Handle escaped characters inside brackets
char escapedChar = glob.charAt(i++);
if (escapedChar == ']' || escapedChar == '[' || escapedChar == '\\' || escapedChar == '-' || escapedChar == '^') {
output.append('\\');
}
output.append(escapedChar);
} else {
output.append(bracketChar);
}
}
if (!foundClosingBracket) {
throw new RuntimeException("Unterminated character class.");
}
return i;
}

private final String component;
private final Pattern pattern;

Expand Down
169 changes: 169 additions & 0 deletions shell/src/test/java/org/apache/kafka/shell/glob/GlobComponentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,173 @@ public void testGlobMatch() {
assertFalse(foobarOrFoobaz.matches("foo"));
assertFalse(foobarOrFoobaz.matches("baz"));
}

@Test
public void testCharacterRangeToRegularExpression() {
// Basic character range patterns
assertEquals("^[a-z]$", GlobComponent.toRegularExpression("[a-z]"));
assertEquals("^[0-9]$", GlobComponent.toRegularExpression("[0-9]"));
assertEquals("^[abc]$", GlobComponent.toRegularExpression("[abc]"));
assertEquals("^file[0-9]$", GlobComponent.toRegularExpression("file[0-9]"));
assertEquals("^[a-zA-Z]$", GlobComponent.toRegularExpression("[a-zA-Z]"));

// Negation patterns
assertEquals("^[^a-z]$", GlobComponent.toRegularExpression("[!a-z]"));
assertEquals("^[^abc]$", GlobComponent.toRegularExpression("[^abc]"));

// Literal ] as first character
assertEquals("^[]abc]$", GlobComponent.toRegularExpression("[]abc]"));
assertEquals("^[^]abc]$", GlobComponent.toRegularExpression("[!]abc]"));
assertEquals("^[^]abc]$", GlobComponent.toRegularExpression("[^]abc]"));

// Hyphen variants
assertEquals("^[a-c-e]$", GlobComponent.toRegularExpression("[a-c-e]"));
assertEquals("^[abc-]$", GlobComponent.toRegularExpression("[abc-]"));
assertEquals("^[-abc]$", GlobComponent.toRegularExpression("[-abc]"));

// Special regex characters inside brackets should be literal
assertEquals("^[.*+?|(){}$^]$", GlobComponent.toRegularExpression("[.*+?|(){}$^]"));
}

@Test
public void testCharacterRangeMatch() {
// Test basic range [a-z]
GlobComponent lowerRange = new GlobComponent("[a-z]");
assertFalse(lowerRange.literal());
assertTrue(lowerRange.matches("a"));
assertTrue(lowerRange.matches("m"));
assertTrue(lowerRange.matches("z"));
assertFalse(lowerRange.matches("A"));
assertFalse(lowerRange.matches("5"));
assertFalse(lowerRange.matches("ab"));

// Test numeric range [0-9]
GlobComponent digitRange = new GlobComponent("[0-9]");
assertFalse(digitRange.literal());
assertTrue(digitRange.matches("0"));
assertTrue(digitRange.matches("5"));
assertTrue(digitRange.matches("9"));
assertFalse(digitRange.matches("a"));
assertFalse(digitRange.matches("10"));

// Test individual characters [abc]
GlobComponent charSet = new GlobComponent("[abc]");
assertFalse(charSet.literal());
assertTrue(charSet.matches("a"));
assertTrue(charSet.matches("b"));
assertTrue(charSet.matches("c"));
assertFalse(charSet.matches("d"));
assertFalse(charSet.matches("ab"));

// Test literal ] as first character
GlobComponent firstBracket = new GlobComponent("[]abc]");
assertTrue(firstBracket.matches("]"));
assertTrue(firstBracket.matches("a"));
assertFalse(firstBracket.matches("d"));

// Test pattern with prefix: file[0-3]
GlobComponent filePattern = new GlobComponent("file[0-3]");
assertFalse(filePattern.literal());
assertTrue(filePattern.matches("file0"));
assertTrue(filePattern.matches("file1"));
assertTrue(filePattern.matches("file3"));
assertFalse(filePattern.matches("file4"));
assertFalse(filePattern.matches("file"));

// Test pattern with suffix: [a-c].txt
GlobComponent suffixPattern = new GlobComponent("[a-c].txt");
assertFalse(suffixPattern.literal());
assertTrue(suffixPattern.matches("a.txt"));
assertTrue(suffixPattern.matches("b.txt"));
assertFalse(suffixPattern.matches("d.txt"));
assertFalse(suffixPattern.matches("ab.txt"));

// Test in middle: foo[a-c]bar
GlobComponent middlePattern = new GlobComponent("foo[a-c]bar");
assertTrue(middlePattern.matches("fooabar"));
assertTrue(middlePattern.matches("foocbar"));
assertFalse(middlePattern.matches("foobar"));
}

@Test
public void testCharacterRangeNegation() {
// Test negation with !
GlobComponent notLower = new GlobComponent("[!a-z]");
assertFalse(notLower.literal());
assertTrue(notLower.matches("A"));
assertTrue(notLower.matches("5"));
assertFalse(notLower.matches("a"));
assertFalse(notLower.matches("z"));

// Test negation with ^
GlobComponent notDigit = new GlobComponent("[^0-9]");
assertFalse(notDigit.literal());
assertTrue(notDigit.matches("a"));
assertTrue(notDigit.matches("Z"));
assertFalse(notDigit.matches("0"));
assertFalse(notDigit.matches("9"));

// Test negation with literal ]
GlobComponent notClosing = new GlobComponent("[!]a-c]");
assertTrue(notClosing.matches("x"));
assertFalse(notClosing.matches("]"));
assertFalse(notClosing.matches("a"));
}

@Test
public void testCharacterRangeEscaping() {
// Test escaped hyphen
GlobComponent escapedHyphen = new GlobComponent("[a\\-c]");
assertTrue(escapedHyphen.matches("a"));
assertTrue(escapedHyphen.matches("-"));
assertTrue(escapedHyphen.matches("c"));
assertFalse(escapedHyphen.matches("b"));

// Test escaped brackets
GlobComponent escapedBrackets = new GlobComponent("[\\[\\]]");
assertTrue(escapedBrackets.matches("["));
assertTrue(escapedBrackets.matches("]"));
assertFalse(escapedBrackets.matches("a"));
}

@Test
public void testCharacterRangeCombinedWithOtherPatterns() {
// Combine character range with wildcard
GlobComponent combined = new GlobComponent("[a-z]*");
assertFalse(combined.literal());
assertTrue(combined.matches("a"));
assertTrue(combined.matches("abc"));
assertTrue(combined.matches("z123"));
assertFalse(combined.matches("123"));
assertFalse(combined.matches("Abc"));

// Combine character range with question mark
GlobComponent questionCombined = new GlobComponent("[a-z]?[0-9]");
assertFalse(questionCombined.literal());
assertTrue(questionCombined.matches("aX5"));
assertTrue(questionCombined.matches("z99"));
assertTrue(questionCombined.matches("ab5"));
assertTrue(questionCombined.matches("a55"));
assertFalse(questionCombined.matches("a5"));
assertFalse(questionCombined.matches("abc5"));

// Multiple ranges
GlobComponent multipleRanges = new GlobComponent("[a-z][0-9]");
assertTrue(multipleRanges.matches("a5"));
assertTrue(multipleRanges.matches("z0"));
assertFalse(multipleRanges.matches("aa"));
}

@Test
public void testUnterminatedCharacterRange() {
// Unterminated character range should result in literal matching (exception caught)
GlobComponent unterminated = new GlobComponent("[a-z");
assertTrue(unterminated.literal());
assertTrue(unterminated.matches("[a-z"));
assertFalse(unterminated.matches("a"));

GlobComponent unterminatedNegated = new GlobComponent("[!");
assertTrue(unterminatedNegated.literal());
assertTrue(unterminatedNegated.matches("[!"));
}
}