diff --git a/crates/bashkit/src/parser/lexer.rs b/crates/bashkit/src/parser/lexer.rs index d88068e8..57f9f6aa 100644 --- a/crates/bashkit/src/parser/lexer.rs +++ b/crates/bashkit/src/parser/lexer.rs @@ -525,6 +525,19 @@ impl<'a> Lexer<'a> { continue; } } + // Handle $(...) inside double-quoted word segments + // to preserve single-quoted strings within command substitutions + if c == '$' && quote_char == '"' { + word.push(c); + self.advance(); + if self.peek_char() == Some('(') { + word.push('('); + self.advance(); + self.read_command_subst_into(&mut word); + continue; + } + continue; + } word.push(c); self.advance(); } diff --git a/crates/bashkit/tests/cmdsub_quote_test.rs b/crates/bashkit/tests/cmdsub_quote_test.rs new file mode 100644 index 00000000..94cd8d1c --- /dev/null +++ b/crates/bashkit/tests/cmdsub_quote_test.rs @@ -0,0 +1,34 @@ +//! Test for issue #803: single-quoted strings inside $() lose double quotes + +use bashkit::Bash; + +#[tokio::test] +async fn cmdsub_preserves_double_quotes_in_single_quotes() { + let mut bash = Bash::new(); + let result = bash + .exec(r#"x="$(echo '{"a":1}')"; echo "${x}""#) + .await + .unwrap(); + assert_eq!(result.stdout.trim(), r#"{"a":1}"#); +} + +#[tokio::test] +async fn cmdsub_preserves_double_quotes_simple() { + let mut bash = Bash::new(); + let result = bash + .exec(r#"y="$(echo 'say "hello" please')"; echo "${y}""#) + .await + .unwrap(); + assert_eq!(result.stdout.trim(), r#"say "hello" please"#); +} + +/// Without outer double quotes, $() preserves double quotes correctly +#[tokio::test] +async fn cmdsub_without_outer_quotes_works() { + let mut bash = Bash::new(); + let result = bash + .exec(r#"x=$(echo '{"a":1}'); echo "$x""#) + .await + .unwrap(); + assert_eq!(result.stdout.trim(), r#"{"a":1}"#); +}