//! Deciding whether accumulated input forms a complete shell program. //! //! Input backends read a line at a time and need to know when they have enough to hand //! over for execution. use brush_core::Shell; /// Returns whether the given accumulated input is an incomplete shell program, i.e. /// whether more input must be read before it can be executed. /// /// Acquires the shell lock without blocking. Input is read between commands, with no /// other task holding the lock, so acquisition is expected to succeed; blocking instead /// isn't an option because backends also run on single-threaded runtimes. /// /// # Arguments /// /// * `shell_ref` - The shell whose parsing options should be used. /// * `input` - The input accumulated so far. #[allow(dead_code)] pub(crate) fn needs_more_input( shell_ref: &crate::ShellRef, input: &str, ) -> bool { let Ok(shell) = shell_ref.try_lock() else { tracing::warn!("shell was unexpectedly busy; treating input as complete"); return false; }; needs_more_input_locked(&shell, input) } /// Returns whether more input is needed, given an already-acquired shell. #[allow(dead_code)] fn needs_more_input_locked(shell: &Shell, input: &str) -> bool { match shell.parse_string(input) { // Mid-token: unclosed quotes, unterminated here documents, and the like. Err(brush_parser::ParseError::Tokenizing { inner, position: _ }) if inner.is_incomplete() => { true } // Ran out of tokens partway through a construct; more input may complete it. Err(brush_parser::ParseError::ParsingAtEndOfInput) => true, // A bad token at a specific position stays bad no matter what follows it. Err(_) => false, // Parsed cleanly. One catch: a trailing backslash-newline is a line // continuation, which the tokenizer drops silently at end of input. Ask again // with the newline removed; that leaves the input incomplete (e.g., an // unterminated escape, or a here-document body not yet ended) only if that // backslash was really escaping the newline. Ok(_) => ends_with_line_continuation(shell, input), } } /// Returns whether the given input ends with a backslash-newline acting as a line /// continuation. fn ends_with_line_continuation( shell: &Shell, input: &str, ) -> bool { let Some(truncated) = input.strip_suffix('\n') else { return false; }; // Keeps the extra parse off the common path. if !truncated.ends_with('\\') { return false; } matches!( shell.parse_string(truncated), Err(brush_parser::ParseError::Tokenizing { inner, position: _ }) if inner.is_incomplete() ) } #[cfg(test)] mod tests { use super::*; async fn test_shell() -> Shell { brush_core::Shell::builder().build().await.unwrap() } #[tokio::test] async fn treats_trailing_line_continuation_as_incomplete() { let shell = test_shell().await; assert!(needs_more_input_locked(&shell, "echo a\\\n")); assert!(needs_more_input_locked(&shell, "printf '%s' \\\n")); assert!(needs_more_input_locked(&shell, "echo \"quoted\" \\\n")); // A backslash at the very end, with no newline yet. assert!(needs_more_input_locked(&shell, "echo a\\")); } #[tokio::test] async fn treats_input_ending_before_here_tag_as_incomplete() { let shell = test_shell().await; // A line continuation right after the operator: the tag comes on the next line. assert!(needs_more_input_locked(&shell, "cat << \\\n")); // Input ending right at the operator is incomplete too (the parser reports it). assert!(needs_more_input_locked(&shell, "cat <<")); assert!(needs_more_input_locked(&shell, "cat <<\\\n")); // A line continuation after what would be a here-document's end tag joins the next // line onto it, so the body hasn't ended. assert!(needs_more_input_locked(&shell, "cat <