From b2c85cb09ce18f446dc04d0d970a8875dd01a9a9 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 26 Mar 2026 23:47:48 +0000 Subject: [PATCH 1/3] fix(interpreter): expand ANSI-C quotes in parameter expansion patterns Closes #847 --- crates/bashkit/src/interpreter/mod.rs | 12 +++-- .../tests/spec_cases/bash/var-op-test.test.sh | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 2a2d5823..526d18fb 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -6438,19 +6438,23 @@ impl Interpreter { } ParameterOp::RemovePrefixShort => { // ${var#pattern} - remove shortest prefix match - self.remove_pattern(value, operand, true, false) + let expanded = self.expand_operand(operand); + self.remove_pattern(value, &expanded, true, false) } ParameterOp::RemovePrefixLong => { // ${var##pattern} - remove longest prefix match - self.remove_pattern(value, operand, true, true) + let expanded = self.expand_operand(operand); + self.remove_pattern(value, &expanded, true, true) } ParameterOp::RemoveSuffixShort => { // ${var%pattern} - remove shortest suffix match - self.remove_pattern(value, operand, false, false) + let expanded = self.expand_operand(operand); + self.remove_pattern(value, &expanded, false, false) } ParameterOp::RemoveSuffixLong => { // ${var%%pattern} - remove longest suffix match - self.remove_pattern(value, operand, false, true) + let expanded = self.expand_operand(operand); + self.remove_pattern(value, &expanded, false, true) } ParameterOp::ReplaceFirst { pattern, diff --git a/crates/bashkit/tests/spec_cases/bash/var-op-test.test.sh b/crates/bashkit/tests/spec_cases/bash/var-op-test.test.sh index 504eb5eb..de3deae2 100644 --- a/crates/bashkit/tests/spec_cases/bash/var-op-test.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/var-op-test.test.sh @@ -248,3 +248,49 @@ empty= assoc=v assoc=plus ### end + +### vop_suffix_removal_ansi_c_newline +# ${var%$'\n'} should strip trailing newline (issue #847) +str=$'abc\n' +r1="${str%$'\n'}" +echo "$r1" +### expect +abc +### end + +### vop_suffix_removal_var_newline +# ${var%${nl}} should strip trailing newline via variable (issue #847) +nl=$'\n' +str=$'abc\n' +r2="${str%${nl}}" +echo "$r2" +### expect +abc +### end + +### vop_prefix_removal_ansi_c_newline +# ${var#$'\n'} should strip leading newline (issue #847) +str=$'\nabc' +r3="${str#$'\n'}" +echo "$r3" +### expect +abc +### end + +### vop_suffix_removal_long_ansi_c +# ${var%%$'\n'} should strip trailing newline (issue #847) +str=$'abc\n' +r4="${str%%$'\n'}" +echo "$r4" +### expect +abc +### end + +### vop_prefix_removal_long_ansi_c +# ${var##$'\n'} should strip leading newline (issue #847) +str=$'\nabc' +r5="${str##$'\n'}" +echo "$r5" +### expect +abc +### end From ffd6197d3935d6d5276628a374f6fc959e12a6ea Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 27 Mar 2026 00:17:04 +0000 Subject: [PATCH 2/3] fix(parser): handle $'...' ANSI-C quoting in parameter expansion patterns Add $'\n', $'\t', etc. support to parse_word so patterns in ${var%$'\n'} are correctly expanded before matching. Also update chained_string_operations test expectation since operand expansion now works correctly. Closes #847 --- crates/bashkit/src/parser/mod.rs | 32 ++++++++++++++++++- .../bash/blackbox-edge-cases.test.sh | 5 ++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index deb5db1a..d96f6f36 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -2534,8 +2534,38 @@ impl<'a> Parser<'a> { parts.push(WordPart::Literal(std::mem::take(&mut current))); } + // Check for $'...' - ANSI-C quoting + if chars.peek() == Some(&'\'') { + chars.next(); // consume opening ' + let mut ansi = String::new(); + while let Some(c) = chars.next() { + if c == '\'' { + break; + } + if c == '\\' { + if let Some(esc) = chars.next() { + match esc { + 'n' => ansi.push('\n'), + 't' => ansi.push('\t'), + 'r' => ansi.push('\r'), + 'a' => ansi.push('\x07'), + 'b' => ansi.push('\x08'), + 'e' | 'E' => ansi.push('\x1B'), + '\\' => ansi.push('\\'), + '\'' => ansi.push('\''), + _ => { + ansi.push('\\'); + ansi.push(esc); + } + } + } + } else { + ansi.push(c); + } + } + parts.push(WordPart::Literal(ansi)); + } else if chars.peek() == Some(&'(') { // Check for $( - command substitution or arithmetic - if chars.peek() == Some(&'(') { chars.next(); // consume first '(' // Check for $(( - arithmetic expansion diff --git a/crates/bashkit/tests/spec_cases/bash/blackbox-edge-cases.test.sh b/crates/bashkit/tests/spec_cases/bash/blackbox-edge-cases.test.sh index dd5af401..b7b07f01 100644 --- a/crates/bashkit/tests/spec_cases/bash/blackbox-edge-cases.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/blackbox-edge-cases.test.sh @@ -587,11 +587,10 @@ file3.txt ### end ### chained_string_operations -### bash_diff: variable expansion in ${x#$var} operand not supported yet -# Multiple string operations — bash: "[hello world ]", bashkit: "[ hello world ]" +# Multiple string operations — strip leading whitespace x=" hello world "; x="${x#"${x%%[![:space:]]*}"}"; echo "[$x]" ### expect -[ hello world ] +[hello world ] ### end ### multiline_if From 161db3d577358e4711fdcf1e2ec9f10e75187e82 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 27 Mar 2026 00:29:03 +0000 Subject: [PATCH 3/3] style: fix formatting --- crates/bashkit/src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index d96f6f36..67f0da9b 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -2565,7 +2565,7 @@ impl<'a> Parser<'a> { } parts.push(WordPart::Literal(ansi)); } else if chars.peek() == Some(&'(') { - // Check for $( - command substitution or arithmetic + // Check for $( - command substitution or arithmetic chars.next(); // consume first '(' // Check for $(( - arithmetic expansion