Skip to content

Commit d8c1aa1

Browse files
committed
fix(interpreter): expand parameter operators inside arithmetic base# expressions
Closes #944
1 parent 8c03e30 commit d8c1aa1

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7516,10 +7516,67 @@ impl Interpreter {
75167516
return String::new();
75177517
}
75187518

7519+
// Check for parameter expansion operators (%, %%, #, ##, :-, etc.)
7520+
// If present, handle expansion with the operator applied.
7521+
let has_operator = inner.contains("%%")
7522+
|| inner.contains('%')
7523+
|| (inner.contains('#') && !inner.starts_with('#'))
7524+
|| inner.contains(":-");
7525+
if has_operator {
7526+
return self.expand_param_op_in_arithmetic(inner);
7527+
}
7528+
75197529
// ${var} — plain variable
75207530
self.expand_variable(inner)
75217531
}
75227532

7533+
/// Expand a parameter expansion with operators inside arithmetic context.
7534+
/// Handles common cases like ${var%%-*}, ${var##prefix}, etc.
7535+
fn expand_param_op_in_arithmetic(&self, inner: &str) -> String {
7536+
// ${var%%pattern} — remove longest suffix
7537+
if let Some(pos) = inner.find("%%") {
7538+
let name = &inner[..pos];
7539+
let pattern = &inner[pos + 2..];
7540+
let value = self.expand_variable(name);
7541+
return self.remove_pattern(&value, pattern, false, true);
7542+
}
7543+
// ${var%pattern} — remove shortest suffix
7544+
if let Some(pos) = inner.find('%') {
7545+
let name = &inner[..pos];
7546+
let pattern = &inner[pos + 1..];
7547+
let value = self.expand_variable(name);
7548+
return self.remove_pattern(&value, pattern, false, false);
7549+
}
7550+
// ${var##pattern} — remove longest prefix
7551+
if let Some(pos) = inner.find("##") {
7552+
let name = &inner[..pos];
7553+
let pattern = &inner[pos + 2..];
7554+
let value = self.expand_variable(name);
7555+
return self.remove_pattern(&value, pattern, true, true);
7556+
}
7557+
// ${var#pattern} — remove shortest prefix (but not ${#var} length)
7558+
if let Some(pos) = inner.find('#') {
7559+
if pos > 0 {
7560+
let name = &inner[..pos];
7561+
let pattern = &inner[pos + 1..];
7562+
let value = self.expand_variable(name);
7563+
return self.remove_pattern(&value, pattern, true, false);
7564+
}
7565+
}
7566+
// ${var:-default}
7567+
if let Some(pos) = inner.find(":-") {
7568+
let name = &inner[..pos];
7569+
let default = &inner[pos + 2..];
7570+
let value = self.expand_variable(name);
7571+
if value.is_empty() {
7572+
return default.to_string();
7573+
}
7574+
return value;
7575+
}
7576+
// Fallback
7577+
self.expand_variable(inner)
7578+
}
7579+
75237580
/// Parse and evaluate a simple arithmetic expression with depth tracking.
75247581
/// THREAT[TM-DOS-026]: `arith_depth` prevents stack overflow from deeply nested expressions.
75257582
/// Parse an arithmetic atom: unary operators, parenthesized expressions, and literals.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
### arithmetic_base_with_suffix_removal
2+
# 10#${var%%-*} should expand then convert
3+
last="0003-assistant.md"
4+
echo $(( 10#${last%%-*} ))
5+
### expect
6+
3
7+
### end
8+
9+
### arithmetic_base_with_prefix_removal
10+
# 10#${var##0} should expand then convert
11+
val="007"
12+
echo $(( 10#${val##0} ))
13+
### expect
14+
7
15+
### end
16+
17+
### arithmetic_base_with_expansion_plus
18+
# 10#${var%%-*} + 1 in arithmetic
19+
seq="0041-user.md"
20+
echo $(( 10#${seq%%-*} + 1 ))
21+
### expect
22+
42
23+
### end
24+
25+
### arithmetic_base_simple_var
26+
# 10#${var} without operators (verify no regression)
27+
x="0099"
28+
echo $(( 10#${x} ))
29+
### expect
30+
99
31+
### end

0 commit comments

Comments
 (0)