Merge nucleic/clever-quartz-marten-uqnx into dev

This commit is contained in:
2026-07-18 21:19:16 -07:00
parent 109cab4cc7
commit a0c3e601be
4 changed files with 61 additions and 22 deletions
+2 -13
View File
@@ -4,7 +4,7 @@ use brush_core::Shell;
use crate::{ use crate::{
InputBackend, ShellError, completion, InputBackend, ShellError, completion,
input_backend::{InteractivePrompt, ReadResult}, input_backend::{InteractivePrompt, ReadResult, parse_result_is_complete},
}; };
use super::{non_term_line_reader, term_line_reader}; use super::{non_term_line_reader, term_line_reader};
@@ -82,18 +82,7 @@ impl BasicInputBackend {
} }
fn is_valid_input(shell: &Shell<impl brush_core::ShellExtensions>, input: &str) -> bool { fn is_valid_input(shell: &Shell<impl brush_core::ShellExtensions>, input: &str) -> bool {
match shell.parse_string(input.to_owned()) { parse_result_is_complete(&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,
}
} }
fn generate_completions( fn generate_completions(
+21
View File
@@ -49,3 +49,24 @@ pub struct InteractivePrompt {
/// Prompt to display on a continuation line of input. /// Prompt to display on a continuation line of input.
pub continuation_prompt: String, 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<brush_parser::ast::Program, brush_parser::ParseError>,
) -> 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,
}
}
+36 -8
View File
@@ -2,12 +2,21 @@ use std::io::{IsTerminal, Write};
use crate::{ use crate::{
InputBackend, ShellError, 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. /// Represents a minimal shell input backend, capable of taking commands from standard input.
#[derive(Default)] #[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 { impl InputBackend for MinimalInputBackend {
fn read_line( fn read_line(
@@ -17,12 +26,26 @@ impl InputBackend for MinimalInputBackend {
) -> Result<ReadResult, ShellError> { ) -> Result<ReadResult, ShellError> {
self.display_prompt(&prompt)?; self.display_prompt(&prompt)?;
let result = match Self::read_input_line()? { let mut result = String::new();
ReadResult::Input(s) => s, loop {
ReadResult::BoundCommand(s) => s, match Self::read_input_line()? {
ReadResult::Eof => return Ok(ReadResult::Eof), ReadResult::Input(s) | ReadResult::BoundCommand(s) => {
ReadResult::Interrupted => return Ok(ReadResult::Interrupted), 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() { if result.is_empty() {
Ok(ReadResult::Eof) Ok(ReadResult::Eof)
@@ -33,6 +56,11 @@ impl InputBackend for MinimalInputBackend {
} }
impl 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)] #[expect(clippy::unused_self)]
fn should_display_prompt(&self) -> bool { fn should_display_prompt(&self) -> bool {
std::io::stdin().is_terminal() std::io::stdin().is_terminal()
+2 -1
View File
@@ -307,7 +307,8 @@ async fn run_async(
#[cfg(feature = "minimal")] #[cfg(feature = "minimal")]
InputBackendType::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 run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
} }
#[cfg(not(feature = "minimal"))] #[cfg(not(feature = "minimal"))]