From 9b7534db05b8ebf58caa2edf47c0ccb9393841ea Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 26 Mar 2026 13:24:27 +0000 Subject: [PATCH] fix(parser): preserve double quotes inside $() in double-quoted strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #803 — single-quoted strings inside command substitutions within double-quoted word segments now preserve embedded double quotes. Root cause: the inline double-quote handler in read_word() didn't track $(...) command substitutions, so a " inside a single-quoted string within $() was treated as the closing delimiter of the outer double-quoted segment. Fix: when encountering $( inside an inline double-quoted segment, delegate to read_command_subst_into() which properly handles nested quotes. --- crates/bashkit/src/parser/lexer.rs | 13 +++++++++ crates/bashkit/tests/cmdsub_quote_test.rs | 34 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 crates/bashkit/tests/cmdsub_quote_test.rs 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}"#); +}