pub(super) const BLANK_OPENERS: [&str; 2] = ["{", "[["];
pub(super) const KEYWORD_OPENERS: [&str; 6] = ["if", "for", "while", "until", "case", "function"];
pub(super) fn opens_compound(input: &str) -> bool {
for opener in BLANK_OPENERS {
if let Some(rest) = input.strip_prefix(opener) {
return rest.starts_with([' ', '\t', '\n']);
}
}
KEYWORD_OPENERS.iter().any(|kw| {
input.strip_prefix(kw).is_some_and(|rest| {
rest.is_empty() || rest.starts_with([' ', '\t', '\n', ';', '&', '|', '(', ')', '<', '>'])
})
})
}
#[cfg(test)]
mod tests {
use super::opens_compound;
#[test]
fn a_reserved_word_standing_alone_opens_a_compound() {
for input in [
"{ ls; }", "{\nls\n}", "[[ -f x ]]", "if true; then ls; fi", "for x in a; do ls; done",
"while true; do ls; done", "until true; do ls; done", "case x in a) ls;; esac",
"function f { ls; }", "if", "if;", "for(", "case\tx",
] {
assert!(opens_compound(input), "{input:?} should commit to a compound");
}
}
#[test]
fn a_word_that_only_starts_like_one_does_not() {
for input in [
"{a,b}", "{}", "{", "[[x", "[", "if=1 ls", "for_each", "iffy", "cases", "functions",
"whiled", "ls {", "echo if", "", "done",
] {
assert!(!opens_compound(input), "{input:?} is not a reserved word");
}
}
}