const MAX_ALTERNATIVES: usize = 64;
#[must_use]
pub fn alternatives(pattern: &str) -> Vec<String> {
let mut out = Vec::new();
if expand(pattern, &mut out) {
out
} else {
vec![pattern.to_owned()]
}
}
fn expand(pattern: &str, out: &mut Vec<String>) -> bool {
let Some((open, close, options)) = first_group(pattern) else {
out.push(pattern.to_owned());
return true;
};
let (head, tail) = (&pattern[..open], &pattern[close + 1..]);
options.iter().all(|option| {
expand(&format!("{head}{option}{tail}"), out) && out.len() <= MAX_ALTERNATIVES
})
}
fn first_group(pattern: &str) -> Option<(usize, usize, Vec<&str>)> {
let (mut depth, mut open, mut start) = (0usize, None, 0);
let mut options = Vec::new();
let mut chars = pattern.char_indices();
while let Some((i, c)) = chars.next() {
match c {
'\\' => {
chars.next();
}
'{' => {
if depth == 0 {
open = Some(i);
start = i + 1;
}
depth += 1;
}
',' if depth == 1 => {
options.push(&pattern[start..i]);
start = i + 1;
}
'}' if depth > 0 => {
depth -= 1;
if depth == 0 {
options.push(&pattern[start..i]);
return Some((open?, i, options));
}
}
_ => {}
}
}
None
}
#[cfg(test)]
mod tests {
use super::alternatives;
fn owned(v: &[&str]) -> Vec<String> {
v.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn each_alternative_becomes_a_pattern() {
assert_eq!(
alternatives("/h/{.ssh/**,.aws/**}"),
owned(&["/h/.ssh/**", "/h/.aws/**"])
);
assert_eq!(
alternatives("/a/{b,{c,d}e}/f"),
owned(&["/a/b/f", "/a/ce/f", "/a/de/f"])
);
assert_eq!(
alternatives("!/p/{x,y}/**"),
owned(&["!/p/x/**", "!/p/y/**"])
);
assert_eq!(alternatives("/a/{b"), owned(&["/a/{b"]), "unclosed");
assert_eq!(
alternatives(r"/a/\{b,c}"),
owned(&[r"/a/\{b,c}"]),
"escaped"
);
let wide = "/{a,b}{c,d}{e,f}{g,h}{i,j}{k,l}{m,n}";
assert_eq!(alternatives(wide), owned(&[wide]), "past the bound");
}
}