diff --git a/brush-interactive/src/basic/input_backend.rs b/brush-interactive/src/basic/input_backend.rs index 5cd95ba..17dc77e 100644 --- a/brush-interactive/src/basic/input_backend.rs +++ b/brush-interactive/src/basic/input_backend.rs @@ -4,7 +4,7 @@ use brush_core::Shell; use crate::{ InputBackend, ShellError, completion, - input_backend::{InteractivePrompt, ReadResult}, + input_backend::{InteractivePrompt, ReadResult, parse_result_is_complete}, }; use super::{non_term_line_reader, term_line_reader}; @@ -82,18 +82,7 @@ impl BasicInputBackend { } fn is_valid_input(shell: &Shell, input: &str) -> bool { - match shell.parse_string(input.to_owned()) { - // Incomplete tokenizing (unclosed quotes, etc.) - need more input - Err(brush_parser::ParseError::Tokenizing { inner, position: _ }) - if inner.is_incomplete() => - { - false - } - // Parse error at end of input - could be incomplete - Err(brush_parser::ParseError::ParsingAtEndOfInput) => false, - // Parse error at a specific position OR successful parse - complete - _ => true, - } + parse_result_is_complete(&shell.parse_string(input.to_owned())) } fn generate_completions( diff --git a/brush-interactive/src/input_backend.rs b/brush-interactive/src/input_backend.rs index efe2c62..c3cf5e5 100644 --- a/brush-interactive/src/input_backend.rs +++ b/brush-interactive/src/input_backend.rs @@ -49,3 +49,24 @@ pub struct InteractivePrompt { /// Prompt to display on a continuation line of input. pub continuation_prompt: String, } + +/// Returns whether a parser result represents a complete command that can be +/// executed, as opposed to a command that needs more physical input lines. +pub(crate) fn parse_result_is_complete( + result: &Result, +) -> bool { + match result { + // Unclosed quotes and similar tokenizer states need another line. + Err(brush_parser::ParseError::Tokenizing { inner, position: _ }) + if inner.is_incomplete() => + { + false + } + // An otherwise-valid compound command may simply be missing its + // closing token (`fi`, `done`, `}`, ...). + Err(brush_parser::ParseError::ParsingAtEndOfInput) => false, + // A located syntax error is complete input and should be surfaced. + // A successful parse is ready to execute. + _ => true, + } +} diff --git a/brush-interactive/src/minimal/input_backend.rs b/brush-interactive/src/minimal/input_backend.rs index 48732fb..02a51e2 100644 --- a/brush-interactive/src/minimal/input_backend.rs +++ b/brush-interactive/src/minimal/input_backend.rs @@ -2,12 +2,21 @@ use std::io::{IsTerminal, Write}; use crate::{ InputBackend, ShellError, - input_backend::{InteractivePrompt, ReadResult}, + input_backend::{InteractivePrompt, ReadResult, parse_result_is_complete}, }; /// Represents a minimal shell input backend, capable of taking commands from standard input. #[derive(Default)] -pub struct MinimalInputBackend; +pub struct MinimalInputBackend { + parser_options: brush_parser::ParserOptions, +} + +impl MinimalInputBackend { + /// Creates a minimal backend using the shell's active parser options. + pub const fn new(parser_options: brush_parser::ParserOptions) -> Self { + Self { parser_options } + } +} impl InputBackend for MinimalInputBackend { fn read_line( @@ -17,12 +26,26 @@ impl InputBackend for MinimalInputBackend { ) -> Result { self.display_prompt(&prompt)?; - let result = match Self::read_input_line()? { - ReadResult::Input(s) => s, - ReadResult::BoundCommand(s) => s, - ReadResult::Eof => return Ok(ReadResult::Eof), - ReadResult::Interrupted => return Ok(ReadResult::Interrupted), - }; + let mut result = String::new(); + loop { + match Self::read_input_line()? { + ReadResult::Input(s) | ReadResult::BoundCommand(s) => { + result.push_str(&s); + if self.is_complete_input(&result) { + break; + } + } + ReadResult::Eof => { + if result.is_empty() { + return Ok(ReadResult::Eof); + } + // Return the partial command so the normal parser path can + // report the incomplete construct instead of dropping it. + break; + } + ReadResult::Interrupted => return Ok(ReadResult::Interrupted), + } + } if result.is_empty() { Ok(ReadResult::Eof) @@ -33,6 +56,11 @@ impl InputBackend for MinimalInputBackend { } impl MinimalInputBackend { + fn is_complete_input(&self, input: &str) -> bool { + let mut parser = brush_parser::Parser::new(input.as_bytes(), &self.parser_options); + parse_result_is_complete(&parser.parse_program()) + } + #[expect(clippy::unused_self)] fn should_display_prompt(&self) -> bool { std::io::stdin().is_terminal() diff --git a/brush-shell/src/entry.rs b/brush-shell/src/entry.rs index 8c5cb2a..f3264c0 100644 --- a/brush-shell/src/entry.rs +++ b/brush-shell/src/entry.rs @@ -307,7 +307,8 @@ async fn run_async( #[cfg(feature = "minimal")] InputBackendType::Minimal => { - let mut input_backend = brush_interactive::MinimalInputBackend; + let parser_options = shell.lock().await.parser_options(); + let mut input_backend = brush_interactive::MinimalInputBackend::new(parser_options); run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await } #[cfg(not(feature = "minimal"))]