Merge pull request #7215 from Lstarsky0/fix/grep-glob-forms

fix(engine): match brace and nested-directory globs in grep_files
This commit is contained in:
Hunter Bown authored and GitHub committed 2026-11-06 08:57:28 +08:00
commit d3e2edf231
1 file changed
+81 -1
+81 -1
View File
@@ -669,9 +669,33 @@ fn should_include(path: &str, patterns: &[String]) -> bool {
false
}
const MAX_BRACE_COMBINATIONS: usize = 64;
/// Simple glob pattern matching
/// Supports: * (any chars), ** (any path), ? (single char)
/// Supports: * (any chars), ** (any path), ? (single char), {a,b} (either)
pub fn matches_glob(path: &str, pattern: &str) -> bool {
if let Some(alternatives) = expand_braces(pattern) {
return alternatives.iter().any(|alt| matches_glob(path, alt));
}
if let Some(rest) = pattern.strip_prefix("**/")
&& rest.contains('/')
{
return std::iter::once(path)
.chain(path.match_indices('/').map(|(i, _)| &path[i + 1..]))
.any(|tail| matches_glob(tail, rest));
}
if let Some((head, tail)) = pattern.split_once("/**/")
&& tail.contains('/')
&& !head.contains(['*', '?'])
{
return path
.strip_prefix(head)
.and_then(|rest| rest.strip_prefix('/'))
.is_some_and(|rest| matches_glob(rest, &format!("**/{tail}")));
}
// Handle ** for any path
if pattern.contains("**") {
let parts: Vec<&str> = pattern.split("**").collect();
@@ -694,6 +718,10 @@ pub fn matches_glob(path: &str, pattern: &str) -> bool {
return false;
}
if !suffix.is_empty() {
if suffix.contains(['*', '?']) {
let filename = path.rsplit('/').next().unwrap_or(path);
return matches_simple_glob(filename, suffix);
}
return path.ends_with(suffix)
|| path
.split('/')
@@ -719,6 +747,58 @@ pub fn matches_glob(path: &str, pattern: &str) -> bool {
matches_simple_glob(filename, pattern)
}
fn expand_braces(pattern: &str) -> Option<Vec<String>> {
let open = pattern.find('{')?;
let combinations = pattern.split('{').skip(1).fold(1_usize, |total, group| {
let alternatives = group
.split('}')
.next()
.unwrap_or(group)
.matches(',')
.count()
+ 1;
total.saturating_mul(alternatives)
});
if combinations > MAX_BRACE_COMBINATIONS {
return None;
}
let mut depth = 0;
let mut bounds = vec![open];
let mut close = None;
for (i, c) in pattern.char_indices().skip_while(|(i, _)| *i < open) {
match c {
'{' => depth += 1,
',' if depth == 1 => bounds.push(i),
'}' => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let close = close?;
if bounds.len() < 2 {
return None;
}
bounds.push(close);
Some(
bounds
.windows(2)
.map(|pair| {
format!(
"{}{}{}",
&pattern[..open],
&pattern[pair[0] + 1..pair[1]],
&pattern[close + 1..]
)
})
.collect(),
)
}
/// Simple glob matching for single path component
fn matches_simple_glob(text: &str, pattern: &str) -> bool {
let mut text_chars = text.chars().peekable();