#[derive(Debug, Clone, Copy)]
struct QuoteState {
in_single_quote: bool,
in_double_quote: bool,
}
impl QuoteState {
fn new() -> Self {
QuoteState {
in_single_quote: false,
in_double_quote: false,
}
}
fn is_quoted(&self) -> bool {
self.in_single_quote || self.in_double_quote
}
}
#[derive(Debug, Clone, Copy)]
struct BracketState {
brace_depth: i32, paren_depth: i32, bracket_depth: i32, }
impl BracketState {
fn new() -> Self {
BracketState {
brace_depth: 0,
paren_depth: 0,
bracket_depth: 0,
}
}
fn has_unclosed(&self) -> bool {
self.brace_depth > 0 || self.paren_depth > 0 || self.bracket_depth > 0
}
}
fn has_backslash_continuation(input: &str) -> bool {
input.trim_end().ends_with('\\')
}
fn analyze_quote_state(input: &str) -> QuoteState {
let mut state = QuoteState::new();
let mut escape_next = false;
for ch in input.chars() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'\'' if !state.in_double_quote => state.in_single_quote = !state.in_single_quote,
'"' if !state.in_single_quote => state.in_double_quote = !state.in_double_quote,
_ => {}
}
}
state
}
fn analyze_bracket_state(input: &str) -> BracketState {
let mut state = BracketState::new();
let mut quote_state = QuoteState::new();
let mut escape_next = false;
for ch in input.chars() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'\'' if !quote_state.in_double_quote => {
quote_state.in_single_quote = !quote_state.in_single_quote;
}
'"' if !quote_state.in_single_quote => {
quote_state.in_double_quote = !quote_state.in_double_quote;
}
'{' if !quote_state.is_quoted() => state.brace_depth += 1,
'}' if !quote_state.is_quoted() => state.brace_depth -= 1,
'(' if !quote_state.is_quoted() => state.paren_depth += 1,
')' if !quote_state.is_quoted() => state.paren_depth -= 1,
'[' if !quote_state.is_quoted() => state.bracket_depth += 1,
']' if !quote_state.is_quoted() => state.bracket_depth -= 1,
_ => {}
}
}
state
}
fn has_closing_keyword(input: &str, keyword: &str) -> bool {
input.split_whitespace().any(|word| {
word.trim_end_matches(';') == keyword
})
}
fn needs_continuation_if(trimmed: &str) -> bool {
trimmed.starts_with("if ") && !has_closing_keyword(trimmed, "fi")
}
fn needs_continuation_for(trimmed: &str) -> bool {
trimmed.starts_with("for ") && !has_closing_keyword(trimmed, "done")
}
fn needs_continuation_while(trimmed: &str) -> bool {
trimmed.starts_with("while ") && !has_closing_keyword(trimmed, "done")
}
fn needs_continuation_until(trimmed: &str) -> bool {
trimmed.starts_with("until ") && !has_closing_keyword(trimmed, "done")
}
fn needs_continuation_case(trimmed: &str) -> bool {
trimmed.starts_with("case ") && !has_closing_keyword(trimmed, "esac")
}
fn needs_continuation_function(trimmed: &str) -> bool {
(trimmed.starts_with("function ") || trimmed.contains("() {")) && !trimmed.ends_with('}')
}
fn needs_continuation_block(trimmed: &str) -> bool {
trimmed.ends_with(" then") || trimmed.ends_with(" do")
}
fn bash_keywords_need_continuation(input: &str) -> bool {
let trimmed = input.trim();
if needs_continuation_if(trimmed) {
return true;
}
if needs_continuation_for(trimmed) {
return true;
}
if needs_continuation_while(trimmed) {
return true;
}
if needs_continuation_until(trimmed) {
return true;
}
if needs_continuation_case(trimmed) {
return true;
}
if needs_continuation_function(trimmed) {
return true;
}
if needs_continuation_block(trimmed) {
return true;
}
false
}
pub fn is_incomplete(input: &str) -> bool {
if has_backslash_continuation(input) {
return true;
}
let quote_state = analyze_quote_state(input);
if quote_state.is_quoted() {
return true;
}
let bracket_state = analyze_bracket_state(input);
if bracket_state.has_unclosed() {
return true;
}
bash_keywords_need_continuation(input)
}
#[cfg(test)]
#[path = "multiline_tests_repl_011.rs"]
mod tests_ext;