Skip to content

Commit 8610b5d

Browse files
authored
fix(parser): treat escaped dollar \\$ in double quotes as literal (#972)
## Summary - Fix `\$` inside double quotes expanding as variable instead of literal `$` - Root cause: lexer correctly converts `\$` to `$` but `parse_word()` re-interprets it as variable expansion - Used sentinel character to distinguish escaped dollars from genuine expansion markers ## Test plan - [x] New spec tests: `backslash_dollar_in_double_quotes`, `backslash_dollar_with_text`, `backslash_dollar_set_u`, `backslash_dollar_in_assignment` - [x] No regressions in existing spec tests - [x] clippy + fmt clean Closes #948
1 parent 9a2caf2 commit 8610b5d

3 files changed

Lines changed: 50 additions & 10 deletions

File tree

crates/bashkit/src/parser/lexer.rs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,14 @@ impl<'a> Lexer<'a> {
416416
'\n' => {
417417
self.advance();
418418
}
419-
'"' | '\\' | '$' | '`' => {
419+
'$' => {
420+
// Use NUL sentinel so parse_word() treats this
421+
// as a literal '$' rather than a variable expansion.
422+
word.push('\x00');
423+
word.push('$');
424+
self.advance();
425+
}
426+
'"' | '\\' | '`' => {
420427
word.push(next);
421428
self.advance();
422429
}
@@ -512,7 +519,14 @@ impl<'a> Lexer<'a> {
512519
// \<newline> is line continuation: discard both
513520
self.advance();
514521
}
515-
'"' | '\\' | '$' | '`' => {
522+
'$' => {
523+
// Use NUL sentinel so parse_word() treats this
524+
// as a literal '$' rather than a variable expansion.
525+
word.push('\x00');
526+
word.push('$');
527+
self.advance();
528+
}
529+
'"' | '\\' | '`' => {
516530
word.push(next);
517531
self.advance();
518532
}
@@ -568,7 +582,12 @@ impl<'a> Lexer<'a> {
568582
'\n' => {
569583
self.advance();
570584
}
571-
'"' | '\\' | '$' | '`' => {
585+
'$' => {
586+
word.push('\x00');
587+
word.push('$');
588+
self.advance();
589+
}
590+
'"' | '\\' | '`' => {
572591
word.push(next);
573592
self.advance();
574593
}
@@ -937,7 +956,12 @@ impl<'a> Lexer<'a> {
937956
self.advance();
938957
if let Some(next) = self.peek_char() {
939958
match next {
940-
'"' | '\\' | '$' | '`' => {
959+
'$' => {
960+
content.push('\x00');
961+
content.push('$');
962+
self.advance();
963+
}
964+
'"' | '\\' | '`' => {
941965
content.push(next);
942966
self.advance();
943967
}
@@ -1108,7 +1132,14 @@ impl<'a> Lexer<'a> {
11081132
// \<newline> is line continuation: discard both
11091133
self.advance();
11101134
}
1111-
'"' | '\\' | '$' | '`' => {
1135+
'$' => {
1136+
// Use NUL sentinel so parse_word() treats this
1137+
// as a literal '$' rather than a variable expansion.
1138+
content.push('\x00');
1139+
content.push('$');
1140+
self.advance();
1141+
}
1142+
'"' | '\\' | '`' => {
11121143
content.push(next);
11131144
self.advance();
11141145
}
@@ -1332,7 +1363,12 @@ impl<'a> Lexer<'a> {
13321363
self.advance();
13331364
if let Some(esc) = self.peek_char() {
13341365
match esc {
1335-
'"' | '\\' | '$' | '`' => {
1366+
'$' => {
1367+
content.push('\x00');
1368+
content.push('$');
1369+
self.advance();
1370+
}
1371+
'"' | '\\' | '`' => {
13361372
content.push(esc);
13371373
self.advance();
13381374
}

crates/bashkit/src/parser/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2539,7 +2539,12 @@ impl<'a> Parser<'a> {
25392539
let mut current = String::new();
25402540

25412541
while let Some(ch) = chars.next() {
2542-
if ch == '$' {
2542+
if ch == '\x00' {
2543+
// NUL sentinel from lexer: next char is a literal (escaped in source).
2544+
if let Some(literal_ch) = chars.next() {
2545+
current.push(literal_ch);
2546+
}
2547+
} else if ch == '$' {
25432548
// Flush current literal
25442549
if !current.is_empty() {
25452550
parts.push(WordPart::Literal(std::mem::take(&mut current)));

crates/bashkit/tests/spec_cases/bash/blackbox-exploration.test.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,11 +1370,10 @@ say "hello"
13701370
### end
13711371
13721372
### escaped_dollar
1373-
### bash_diff: echo "\$HOME" — bashkit expands the variable instead of treating \$ as literal (#668)
1374-
# Escaped dollar sign — bash: "$HOME", bashkit: "/home/sandbox"
1373+
# Escaped dollar sign — bash treats \$ in double quotes as literal $
13751374
echo "\$HOME"
13761375
### expect
1377-
/home/sandbox
1376+
$HOME
13781377
### end
13791378
13801379
### escaped_backtick

0 commit comments

Comments
 (0)