From 9b1ca8197efb81f4b1bed1a16d0503c3b9e360b6 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Sun, 5 Apr 2026 23:35:34 +0000 Subject: [PATCH] fix(parser): handle all token types in process substitution reconstruction Add explicit handlers for DoubleLeftBracket, DoubleRightBracket, DoubleLeftParen, DoubleRightParen, DoubleSemicolon, SemiAmp, DoubleSemiAmp, Assignment, RedirectBoth, Clobber, DupInput, HereDoc, HereDocStrip, RedirectFdAppend, DupFd, DupFdIn, DupFdClose, RedirectFdIn, ProcessSubIn, ProcessSubOut, and Error tokens in the process substitution token reconstruction loop. Previously these fell through to the catch-all branch and were silently dropped. --- crates/bashkit/src/interpreter/mod.rs | 23 ++++++ crates/bashkit/src/parser/mod.rs | 100 +++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 23a6b64a..a96d8f41 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -10742,4 +10742,27 @@ echo "count=$COUNT" let result = run_script(r#"paste <(echo -e "a\nb") <(echo -e "1\n2")"#).await; assert_eq!(result.stdout, "a\t1\nb\t2\n"); } + + #[tokio::test] + async fn test_process_sub_conditional_bracket() { + // [[ ]] inside process substitution must be preserved during token reconstruction + let result = run_script(r#"cat <( [[ 1 = 1 ]] && echo MATCH )"#).await; + assert_eq!(result.stdout.trim(), "MATCH"); + } + + #[tokio::test] + async fn test_process_sub_while_break_with_condition() { + // while+break with conditional inside process substitution + let result = + run_script(r#"cat <( x=1; while true; do [[ $x -eq 1 ]] && break; done; echo OK )"#) + .await; + assert_eq!(result.stdout.trim(), "OK"); + } + + #[tokio::test] + async fn test_process_sub_arithmetic() { + // (( )) inside process substitution must be preserved + let result = run_script(r#"cat <( x=5; (( x > 3 )) && echo YES )"#).await; + assert_eq!(result.stdout.trim(), "YES"); + } } diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index b6c7ee7b..12463e05 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -2599,13 +2599,111 @@ impl<'a> Parser<'a> { cmd_str.push('\n'); self.advance(); } + Some(tokens::Token::DoubleLeftBracket) => { + if !cmd_str.is_empty() { + cmd_str.push(' '); + } + cmd_str.push_str("[["); + self.advance(); + } + Some(tokens::Token::DoubleRightBracket) => { + cmd_str.push_str(" ]]"); + self.advance(); + } + Some(tokens::Token::DoubleLeftParen) => { + if !cmd_str.is_empty() { + cmd_str.push(' '); + } + cmd_str.push_str("(("); + self.advance(); + } + Some(tokens::Token::DoubleRightParen) => { + cmd_str.push_str("))"); + self.advance(); + } + Some(tokens::Token::DoubleSemicolon) => { + cmd_str.push_str(";;"); + self.advance(); + } + Some(tokens::Token::SemiAmp) => { + cmd_str.push_str(";&"); + self.advance(); + } + Some(tokens::Token::DoubleSemiAmp) => { + cmd_str.push_str(";;&"); + self.advance(); + } + Some(tokens::Token::Assignment) => { + cmd_str.push('='); + self.advance(); + } + Some(tokens::Token::RedirectBoth) => { + cmd_str.push_str(" &>"); + self.advance(); + } + Some(tokens::Token::Clobber) => { + cmd_str.push_str(" >|"); + self.advance(); + } + Some(tokens::Token::DupInput) => { + cmd_str.push_str(" <&"); + self.advance(); + } + Some(tokens::Token::HereDoc) => { + cmd_str.push_str(" <<"); + self.advance(); + } + Some(tokens::Token::HereDocStrip) => { + cmd_str.push_str(" <<-"); + self.advance(); + } + Some(tokens::Token::RedirectFdAppend(fd)) => { + cmd_str.push_str(&format!(" {}>>", fd)); + self.advance(); + } + Some(tokens::Token::DupFd(src, dst)) => { + cmd_str.push_str(&format!(" {}>& {}", src, dst)); + self.advance(); + } + Some(tokens::Token::DupFdIn(src, dst)) => { + cmd_str.push_str(&format!(" {}<& {}", src, dst)); + self.advance(); + } + Some(tokens::Token::DupFdClose(fd)) => { + cmd_str.push_str(&format!(" {}<&-", fd)); + self.advance(); + } + Some(tokens::Token::RedirectFdIn(fd)) => { + cmd_str.push_str(&format!(" {}< ", fd)); + self.advance(); + } + Some(tokens::Token::ProcessSubIn) => { + cmd_str.push_str(" <("); + depth += 1; + self.advance(); + } + Some(tokens::Token::ProcessSubOut) => { + cmd_str.push_str(" >("); + depth += 1; + self.advance(); + } + Some(tokens::Token::Error(e)) => { + // Propagate lexer errors + let msg = e.clone(); + self.advance(); + return Err(Error::parse(format!( + "lexer error in process substitution: {}", + msg + ))); + } None => { return Err(Error::parse( "unexpected end of input in process substitution".to_string(), )); } + #[allow(unreachable_patterns)] _ => { - // Skip unknown tokens but don't silently lose them + // Safety net for future Token variants self.advance(); } }