diff --git a/crates/bashkit/src/parser/lexer.rs b/crates/bashkit/src/parser/lexer.rs index bcaa6bb8..ae24f2c5 100644 --- a/crates/bashkit/src/parser/lexer.rs +++ b/crates/bashkit/src/parser/lexer.rs @@ -416,7 +416,14 @@ impl<'a> Lexer<'a> { '\n' => { self.advance(); } - '"' | '\\' | '$' | '`' => { + '$' => { + // Use NUL sentinel so parse_word() treats this + // as a literal '$' rather than a variable expansion. + word.push('\x00'); + word.push('$'); + self.advance(); + } + '"' | '\\' | '`' => { word.push(next); self.advance(); } @@ -512,7 +519,14 @@ impl<'a> Lexer<'a> { // \ is line continuation: discard both self.advance(); } - '"' | '\\' | '$' | '`' => { + '$' => { + // Use NUL sentinel so parse_word() treats this + // as a literal '$' rather than a variable expansion. + word.push('\x00'); + word.push('$'); + self.advance(); + } + '"' | '\\' | '`' => { word.push(next); self.advance(); } @@ -568,7 +582,12 @@ impl<'a> Lexer<'a> { '\n' => { self.advance(); } - '"' | '\\' | '$' | '`' => { + '$' => { + word.push('\x00'); + word.push('$'); + self.advance(); + } + '"' | '\\' | '`' => { word.push(next); self.advance(); } @@ -937,7 +956,12 @@ impl<'a> Lexer<'a> { self.advance(); if let Some(next) = self.peek_char() { match next { - '"' | '\\' | '$' | '`' => { + '$' => { + content.push('\x00'); + content.push('$'); + self.advance(); + } + '"' | '\\' | '`' => { content.push(next); self.advance(); } @@ -1108,7 +1132,14 @@ impl<'a> Lexer<'a> { // \ is line continuation: discard both self.advance(); } - '"' | '\\' | '$' | '`' => { + '$' => { + // Use NUL sentinel so parse_word() treats this + // as a literal '$' rather than a variable expansion. + content.push('\x00'); + content.push('$'); + self.advance(); + } + '"' | '\\' | '`' => { content.push(next); self.advance(); } @@ -1332,7 +1363,12 @@ impl<'a> Lexer<'a> { self.advance(); if let Some(esc) = self.peek_char() { match esc { - '"' | '\\' | '$' | '`' => { + '$' => { + content.push('\x00'); + content.push('$'); + self.advance(); + } + '"' | '\\' | '`' => { content.push(esc); self.advance(); } diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index d26e61a8..46d1cc5e 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -2539,7 +2539,12 @@ impl<'a> Parser<'a> { let mut current = String::new(); while let Some(ch) = chars.next() { - if ch == '$' { + if ch == '\x00' { + // NUL sentinel from lexer: next char is a literal (escaped in source). + if let Some(literal_ch) = chars.next() { + current.push(literal_ch); + } + } else if ch == '$' { // Flush current literal if !current.is_empty() { parts.push(WordPart::Literal(std::mem::take(&mut current))); diff --git a/crates/bashkit/tests/spec_cases/bash/blackbox-exploration.test.sh b/crates/bashkit/tests/spec_cases/bash/blackbox-exploration.test.sh index 11d36968..16926418 100644 --- a/crates/bashkit/tests/spec_cases/bash/blackbox-exploration.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/blackbox-exploration.test.sh @@ -1370,11 +1370,10 @@ say "hello" ### end ### escaped_dollar -### bash_diff: echo "\$HOME" — bashkit expands the variable instead of treating \$ as literal (#668) -# Escaped dollar sign — bash: "$HOME", bashkit: "/home/sandbox" +# Escaped dollar sign — bash treats \$ in double quotes as literal $ echo "\$HOME" ### expect -/home/sandbox +$HOME ### end ### escaped_backtick