Skip to content

Commit 4f75c0c

Browse files
authored
fix(parser): handle $'...' ANSI-C quoting in parameter expansion patterns (#856)
Closes #847
1 parent ef3148a commit 4f75c0c

File tree

4 files changed

+88
-9
lines changed

4 files changed

+88
-9
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6438,19 +6438,23 @@ impl Interpreter {
64386438
}
64396439
ParameterOp::RemovePrefixShort => {
64406440
// ${var#pattern} - remove shortest prefix match
6441-
self.remove_pattern(value, operand, true, false)
6441+
let expanded = self.expand_operand(operand);
6442+
self.remove_pattern(value, &expanded, true, false)
64426443
}
64436444
ParameterOp::RemovePrefixLong => {
64446445
// ${var##pattern} - remove longest prefix match
6445-
self.remove_pattern(value, operand, true, true)
6446+
let expanded = self.expand_operand(operand);
6447+
self.remove_pattern(value, &expanded, true, true)
64466448
}
64476449
ParameterOp::RemoveSuffixShort => {
64486450
// ${var%pattern} - remove shortest suffix match
6449-
self.remove_pattern(value, operand, false, false)
6451+
let expanded = self.expand_operand(operand);
6452+
self.remove_pattern(value, &expanded, false, false)
64506453
}
64516454
ParameterOp::RemoveSuffixLong => {
64526455
// ${var%%pattern} - remove longest suffix match
6453-
self.remove_pattern(value, operand, false, true)
6456+
let expanded = self.expand_operand(operand);
6457+
self.remove_pattern(value, &expanded, false, true)
64546458
}
64556459
ParameterOp::ReplaceFirst {
64566460
pattern,

crates/bashkit/src/parser/mod.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2534,8 +2534,38 @@ impl<'a> Parser<'a> {
25342534
parts.push(WordPart::Literal(std::mem::take(&mut current)));
25352535
}
25362536

2537-
// Check for $( - command substitution or arithmetic
2538-
if chars.peek() == Some(&'(') {
2537+
// Check for $'...' - ANSI-C quoting
2538+
if chars.peek() == Some(&'\'') {
2539+
chars.next(); // consume opening '
2540+
let mut ansi = String::new();
2541+
while let Some(c) = chars.next() {
2542+
if c == '\'' {
2543+
break;
2544+
}
2545+
if c == '\\' {
2546+
if let Some(esc) = chars.next() {
2547+
match esc {
2548+
'n' => ansi.push('\n'),
2549+
't' => ansi.push('\t'),
2550+
'r' => ansi.push('\r'),
2551+
'a' => ansi.push('\x07'),
2552+
'b' => ansi.push('\x08'),
2553+
'e' | 'E' => ansi.push('\x1B'),
2554+
'\\' => ansi.push('\\'),
2555+
'\'' => ansi.push('\''),
2556+
_ => {
2557+
ansi.push('\\');
2558+
ansi.push(esc);
2559+
}
2560+
}
2561+
}
2562+
} else {
2563+
ansi.push(c);
2564+
}
2565+
}
2566+
parts.push(WordPart::Literal(ansi));
2567+
} else if chars.peek() == Some(&'(') {
2568+
// Check for $( - command substitution or arithmetic
25392569
chars.next(); // consume first '('
25402570

25412571
// Check for $(( - arithmetic expansion

crates/bashkit/tests/spec_cases/bash/blackbox-edge-cases.test.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -587,11 +587,10 @@ file3.txt
587587
### end
588588

589589
### chained_string_operations
590-
### bash_diff: variable expansion in ${x#$var} operand not supported yet
591-
# Multiple string operations — bash: "[hello world ]", bashkit: "[ hello world ]"
590+
# Multiple string operations — strip leading whitespace
592591
x=" hello world "; x="${x#"${x%%[![:space:]]*}"}"; echo "[$x]"
593592
### expect
594-
[ hello world ]
593+
[hello world ]
595594
### end
596595

597596
### multiline_if

crates/bashkit/tests/spec_cases/bash/var-op-test.test.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,49 @@ empty=
248248
assoc=v
249249
assoc=plus
250250
### end
251+
252+
### vop_suffix_removal_ansi_c_newline
253+
# ${var%$'\n'} should strip trailing newline (issue #847)
254+
str=$'abc\n'
255+
r1="${str%$'\n'}"
256+
echo "$r1"
257+
### expect
258+
abc
259+
### end
260+
261+
### vop_suffix_removal_var_newline
262+
# ${var%${nl}} should strip trailing newline via variable (issue #847)
263+
nl=$'\n'
264+
str=$'abc\n'
265+
r2="${str%${nl}}"
266+
echo "$r2"
267+
### expect
268+
abc
269+
### end
270+
271+
### vop_prefix_removal_ansi_c_newline
272+
# ${var#$'\n'} should strip leading newline (issue #847)
273+
str=$'\nabc'
274+
r3="${str#$'\n'}"
275+
echo "$r3"
276+
### expect
277+
abc
278+
### end
279+
280+
### vop_suffix_removal_long_ansi_c
281+
# ${var%%$'\n'} should strip trailing newline (issue #847)
282+
str=$'abc\n'
283+
r4="${str%%$'\n'}"
284+
echo "$r4"
285+
### expect
286+
abc
287+
### end
288+
289+
### vop_prefix_removal_long_ansi_c
290+
# ${var##$'\n'} should strip leading newline (issue #847)
291+
str=$'\nabc'
292+
r5="${str##$'\n'}"
293+
echo "$r5"
294+
### expect
295+
abc
296+
### end

0 commit comments

Comments
 (0)