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");
}
}
}