fixed backslashes not being stripped for special characters in double quotes

This commit is contained in:
2026-02-19 00:33:02 -05:00
parent abe602c8ce
commit 9ec4ba85ea
2 changed files with 84 additions and 3 deletions

View File

@@ -909,9 +909,16 @@ pub fn unescape_str(raw: &str) -> String {
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
match q_ch { match q_ch {
'\\' => { '\\' => {
result.push(q_ch);
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
result.push(next_ch) match next_ch {
'"' | '\\' | '`' | '$' => {
// discard the backslash
}
_ => {
result.push(q_ch);
}
}
result.push(next_ch);
} }
} }
'$' => { '$' => {

View File

@@ -1,6 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use crate::expand::perform_param_expansion; use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB};
use crate::state::VarFlags; use crate::state::VarFlags;
use super::*; use super::*;
@@ -286,3 +286,77 @@ fn param_expansion_replacesuffix() {
let result = perform_param_expansion("foo/%o/X").unwrap(); let result = perform_param_expansion("foo/%o/X").unwrap();
assert_eq!(result, "foX"); assert_eq!(result, "foX");
} }
// ============================================================================
// Double-Quote Escape Tests (POSIX)
// ============================================================================
#[test]
fn dquote_escape_dollar() {
// "\$foo" should strip backslash, produce literal $foo (no expansion)
let result = unescape_str(r#""\$foo""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not become VAR_SUB");
assert!(result.contains('$'), "Literal $ should be preserved");
assert!(!result.contains('\\'), "Backslash should be stripped");
}
#[test]
fn dquote_escape_backslash() {
// "\\" in double quotes should produce a single backslash
let result = unescape_str(r#""\\""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "\\", "Double backslash should produce single backslash");
}
#[test]
fn dquote_escape_quote() {
// "\"" should produce a literal double quote
let result = unescape_str(r#""\"""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert!(inner.contains('"'), "Escaped quote should produce literal quote");
}
#[test]
fn dquote_escape_backtick() {
// "\`" should strip backslash, produce literal backtick
let result = unescape_str(r#""\`""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "`", "Escaped backtick should produce literal backtick");
}
#[test]
fn dquote_escape_nonspecial_preserves_backslash() {
// "\a" inside double quotes should preserve the backslash (a is not special)
let result = unescape_str(r#""\a""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "\\a", "Backslash before non-special char should be preserved");
}
#[test]
fn dquote_unescaped_dollar_expands() {
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
let result = unescape_str(r#""$foo""#);
assert!(result.contains(VAR_SUB), "Unescaped $ should become VAR_SUB");
}
#[test]
fn dquote_mixed_escapes() {
// "hello \$world \\end" should have literal $, single backslash
let result = unescape_str(r#""hello \$world \\end""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
assert!(result.contains('$'), "Literal $ should be in output");
// Should have exactly one backslash (from \\)
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
}