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(); } }