use super::Token;
use super::word::WordGlob;
pub(super) fn prefix_match<W>(
pattern: &[Token],
words: &[W],
one: impl Fn(&WordGlob, &str, &W) -> bool,
) -> bool {
let (mut p, mut w) = (0, 0);
let mut star: Option<(usize, usize)> = None;
loop {
let matched = match pattern.get(p) {
None => return true,
Some(Token::Any) => {
star = Some((p, w));
p += 1;
continue;
}
Some(Token::End) if w == words.len() => return true,
Some(Token::End) => false,
Some(Token::One(glob, source)) => {
words.get(w).is_some_and(|word| one(glob, source, word))
}
};
if matched {
p += 1;
w += 1;
continue;
}
match star {
Some((at, taken)) if taken < words.len() => {
star = Some((at, taken + 1));
p = at + 1;
w = taken + 1;
}
_ => return false,
}
}
}
#[cfg(test)]
mod tests {
use super::super::{ShellPattern, glob_covers};
use super::*;
fn reference<W>(
pattern: &[Token],
words: &[W],
one: &dyn Fn(&WordGlob, &str, &W) -> bool,
) -> bool {
match pattern.split_first() {
None => true,
Some((Token::Any, rest)) => {
rest.is_empty()
|| (0..=words.len()).any(|skip| reference(rest, &words[skip..], one))
}
Some((Token::One(glob, source), rest)) => match words.split_first() {
Some((head, tail)) if one(glob, source, head) => reference(rest, tail, one),
_ => false,
},
Some((Token::End, _)) => words.is_empty(),
}
}
fn words(line: &str) -> Vec<String> {
line.split_whitespace().map(str::to_owned).collect()
}
#[expect(
clippy::ptr_arg,
reason = "the matcher takes a reference to the word type, here `String`"
)]
fn matches(glob: &WordGlob, _: &str, word: &String) -> bool {
glob.is_match(word)
}
fn covers(glob: &WordGlob, source: &str, word: &&str) -> bool {
glob_covers(glob, source, word)
}
fn agree(pattern: &str, compiled: &ShellPattern, line: &str) {
let argv = words(line);
assert_eq!(
prefix_match(&compiled.tokens, &argv, matches),
reference(&compiled.tokens, &argv, &matches),
"`{pattern}` against `{line}`"
);
let borrowed: Vec<&str> = argv.iter().map(String::as_str).collect();
assert_eq!(
prefix_match(&compiled.tokens, &borrowed, covers),
reference(&compiled.tokens, &borrowed, &covers),
"`{pattern}` covering `{line}`"
);
}
#[test]
fn same_answers_as_the_recursive_matcher_on_the_pattern_tests() {
let patterns = [
"git push --force*",
"sudo *",
"npm test",
"curl * | sh",
"* | base64 -d | *sh*",
"env $",
"export -p $",
"* | sh $",
"echo $ x",
"find *",
"find * -exec*",
"cargo *",
"cargo pub*",
"rm * -*[rR]* * {.,./,/*,~*}",
"git push * -[!o-]*f*",
"* * * x",
"a * b * c $",
];
let lines = [
"git push --force",
"git push --force-with-lease origin main",
"git push origin main",
"git push origin -vf main",
"sudo rm -rf /",
"sudo",
"sudoedit x",
"npm test -- --watch",
"npm install",
"curl -fsSL https://x | sh",
"curl https://x | sh -s -- --yes",
"curl https://x | tee out",
"echo abc | base64 -d | sh",
"cat f | base64 -d | bash -x",
"env",
"env FOO=1 git status",
"envsubst",
"export -p",
"export -p FOO",
"curl x | sh",
"echo $ x",
"find . -name x",
"find . -name x -execdir rm {} ;",
"cargo publish*",
"rm -rf build .",
"rm x -r y ~/z",
"x",
"a b c",
"a x b y c",
"a b c d",
"",
];
for pattern in patterns {
let compiled = ShellPattern::compile(pattern).unwrap();
for line in lines {
agree(pattern, &compiled, line);
}
}
}
#[test]
fn same_answers_as_the_recursive_matcher_exhaustively() {
let tokens = ["*", "a", "b", "a*", "?"];
let argv_words = ["a", "b", "ab"];
let mut patterns = vec![String::new()];
for _ in 0..4 {
let longer: Vec<String> = patterns
.iter()
.flat_map(|p| tokens.iter().map(move |t| format!("{p} {t}")))
.collect();
patterns.extend(longer);
}
patterns.sort();
patterns.dedup();
let mut lines = vec![String::new()];
for _ in 0..5 {
let longer: Vec<String> = lines
.iter()
.flat_map(|l| argv_words.iter().map(move |w| format!("{l} {w}")))
.collect();
lines.extend(longer);
}
lines.sort();
lines.dedup();
for pattern in patterns.iter().filter(|p| !p.trim().is_empty()) {
for anchored in [pattern.clone(), format!("{pattern} $")] {
let compiled = ShellPattern::compile(&anchored).unwrap();
for line in &lines {
agree(&anchored, &compiled, line);
}
}
}
}
#[test]
fn many_stars_finish_quickly() {
let pattern = format!("a{} b", " * a".repeat(30));
let compiled = ShellPattern::compile(&pattern).unwrap();
let subject = vec!["a".to_owned(); 400];
let started = std::time::Instant::now();
assert!(!compiled.is_match(&subject));
let mut with_end = subject.clone();
with_end.push("b".to_owned());
assert!(compiled.is_match(&with_end));
assert!(started.elapsed() < std::time::Duration::from_secs(5));
}
}