Skip to content

Commit 29312c2

Browse files
committed
feat(bash): arithmetic exponentiation, base literals, mapfile builtin
- Arithmetic ** (power) operator with correct precedence over * - Base#value literals (16#ff, 2#1010, 8#77) and 0x/077 prefixes - Unary operators: !, ~, - in arithmetic expressions - mapfile/readarray builtin with -t flag and custom array name - expand_arithmetic_vars tracks numeric literal context to avoid expanding hex digits as variable names - 13 new spec tests (10 arithmetic, 3 mapfile) https://claude.ai/code/session_012rzB3FRw7yoQWCG1mxyW7J
1 parent e2bfa60 commit 29312c2

File tree

3 files changed

+243
-3
lines changed

3 files changed

+243
-3
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2475,6 +2475,11 @@ impl Interpreter {
24752475
return self.execute_getopts(&args, &command.redirects).await;
24762476
}
24772477

2478+
// Handle `mapfile`/`readarray` - needs direct access to arrays
2479+
if name == "mapfile" || name == "readarray" {
2480+
return self.execute_mapfile(&args, stdin.as_deref()).await;
2481+
}
2482+
24782483
// Check for builtins
24792484
if let Some(builtin) = self.builtins.get(name) {
24802485
let ctx = builtins::Context {
@@ -2854,6 +2859,53 @@ impl Interpreter {
28542859

28552860
/// Execute the `getopts` builtin (POSIX option parsing).
28562861
///
2862+
/// Execute mapfile/readarray builtin — reads lines into an indexed array.
2863+
/// Handled inline because it needs direct access to self.arrays.
2864+
async fn execute_mapfile(
2865+
&mut self,
2866+
args: &[String],
2867+
stdin_data: Option<&str>,
2868+
) -> Result<ExecResult> {
2869+
let mut trim_trailing = false; // -t: strip trailing newlines
2870+
let mut array_name = "MAPFILE".to_string();
2871+
let mut positional = Vec::new();
2872+
2873+
for arg in args {
2874+
match arg.as_str() {
2875+
"-t" => trim_trailing = true,
2876+
a if a.starts_with('-') => {} // skip unknown flags
2877+
_ => positional.push(arg.clone()),
2878+
}
2879+
}
2880+
2881+
if let Some(name) = positional.first() {
2882+
array_name = name.clone();
2883+
}
2884+
2885+
let input = stdin_data.unwrap_or("");
2886+
2887+
// Clear existing array
2888+
self.arrays.remove(&array_name);
2889+
2890+
// Split into lines and populate array
2891+
if !input.is_empty() {
2892+
let mut arr = HashMap::new();
2893+
for (idx, line) in input.lines().enumerate() {
2894+
let value = if trim_trailing {
2895+
line.to_string()
2896+
} else {
2897+
format!("{}\n", line)
2898+
};
2899+
arr.insert(idx, value);
2900+
}
2901+
if !arr.is_empty() {
2902+
self.arrays.insert(array_name, arr);
2903+
}
2904+
}
2905+
2906+
Ok(ExecResult::ok(String::new()))
2907+
}
2908+
28572909
/// Usage: `getopts optstring name [args...]`
28582910
///
28592911
/// Parses options from positional params (or `args`).
@@ -4416,9 +4468,12 @@ impl Interpreter {
44164468
fn expand_arithmetic_vars(&self, expr: &str) -> String {
44174469
let mut result = String::new();
44184470
let mut chars = expr.chars().peekable();
4471+
// Track whether we're in a numeric literal context (after # or 0x)
4472+
let mut in_numeric_literal = false;
44194473

44204474
while let Some(ch) = chars.next() {
44214475
if ch == '$' {
4476+
in_numeric_literal = false;
44224477
// Handle $var syntax (common in arithmetic)
44234478
let mut name = String::new();
44244479
while let Some(&c) = chars.peek() {
@@ -4438,7 +4493,26 @@ impl Interpreter {
44384493
} else {
44394494
result.push(ch);
44404495
}
4496+
} else if ch == '#' {
4497+
// base#value syntax: digits before # are base, chars after are literal digits
4498+
result.push(ch);
4499+
in_numeric_literal = true;
4500+
} else if in_numeric_literal && (ch.is_ascii_alphanumeric() || ch == '_') {
4501+
// Part of a base#value literal — don't expand as variable
4502+
result.push(ch);
4503+
} else if ch.is_ascii_digit() {
4504+
result.push(ch);
4505+
// Check for 0x/0X hex prefix
4506+
if ch == '0' {
4507+
if let Some(&next) = chars.peek() {
4508+
if next == 'x' || next == 'X' {
4509+
result.push(chars.next().unwrap());
4510+
in_numeric_literal = true;
4511+
}
4512+
}
4513+
}
44414514
} else if ch.is_ascii_alphabetic() || ch == '_' {
4515+
in_numeric_literal = false;
44424516
// Could be a variable name
44434517
let mut name = String::new();
44444518
name.push(ch);
@@ -4457,6 +4531,7 @@ impl Interpreter {
44574531
result.push_str(&value);
44584532
}
44594533
} else {
4534+
in_numeric_literal = false;
44604535
result.push(ch);
44614536
}
44624537
}
@@ -4688,17 +4763,28 @@ impl Interpreter {
46884763
}
46894764
}
46904765

4691-
// Multiplication/Division/Modulo (higher precedence)
4766+
// Multiplication/Division/Modulo (higher precedence, skip ** which is power)
46924767
depth = 0;
46934768
for i in (0..chars.len()).rev() {
46944769
match chars[i] {
46954770
'(' => depth += 1,
46964771
')' => depth -= 1,
4697-
'*' | '/' | '%' if depth == 0 => {
4772+
'*' if depth == 0 => {
4773+
// Skip ** (power operator handled below)
4774+
if i + 1 < chars.len() && chars[i + 1] == '*' {
4775+
continue;
4776+
}
4777+
if i > 0 && chars[i - 1] == '*' {
4778+
continue;
4779+
}
4780+
let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
4781+
let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
4782+
return left * right;
4783+
}
4784+
'/' | '%' if depth == 0 => {
46984785
let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
46994786
let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
47004787
return match chars[i] {
4701-
'*' => left * right,
47024788
'/' => {
47034789
if right != 0 {
47044790
left / right
@@ -4720,6 +4806,62 @@ impl Interpreter {
47204806
}
47214807
}
47224808

4809+
// Exponentiation ** (right-associative, higher precedence than */%)
4810+
depth = 0;
4811+
for i in 0..chars.len() {
4812+
match chars[i] {
4813+
'(' => depth += 1,
4814+
')' => depth -= 1,
4815+
'*' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '*' => {
4816+
let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
4817+
// Right-associative: parse from i+2 onward (may contain more **)
4818+
let right = self.parse_arithmetic_impl(&expr[i + 2..], arith_depth + 1);
4819+
return left.pow(right as u32);
4820+
}
4821+
_ => {}
4822+
}
4823+
}
4824+
4825+
// Unary negation and bitwise NOT
4826+
if let Some(rest) = expr.strip_prefix('-') {
4827+
let rest = rest.trim();
4828+
if !rest.is_empty() {
4829+
return -self.parse_arithmetic_impl(rest, arith_depth + 1);
4830+
}
4831+
}
4832+
if let Some(rest) = expr.strip_prefix('~') {
4833+
let rest = rest.trim();
4834+
if !rest.is_empty() {
4835+
return !self.parse_arithmetic_impl(rest, arith_depth + 1);
4836+
}
4837+
}
4838+
if let Some(rest) = expr.strip_prefix('!') {
4839+
let rest = rest.trim();
4840+
if !rest.is_empty() {
4841+
let val = self.parse_arithmetic_impl(rest, arith_depth + 1);
4842+
return if val == 0 { 1 } else { 0 };
4843+
}
4844+
}
4845+
4846+
// Base conversion: base#value (e.g., 16#ff = 255, 2#1010 = 10)
4847+
if let Some(hash_pos) = expr.find('#') {
4848+
let base_str = &expr[..hash_pos];
4849+
let value_str = &expr[hash_pos + 1..];
4850+
if let Ok(base) = base_str.parse::<u32>() {
4851+
if (2..=64).contains(&base) {
4852+
return i64::from_str_radix(value_str, base).unwrap_or(0);
4853+
}
4854+
}
4855+
}
4856+
4857+
// Hex (0x...), octal (0...) literals
4858+
if expr.starts_with("0x") || expr.starts_with("0X") {
4859+
return i64::from_str_radix(&expr[2..], 16).unwrap_or(0);
4860+
}
4861+
if expr.starts_with('0') && expr.len() > 1 && expr.chars().all(|c| c.is_ascii_digit()) {
4862+
return i64::from_str_radix(&expr[1..], 8).unwrap_or(0);
4863+
}
4864+
47234865
// Parse as number
47244866
expr.trim().parse().unwrap_or(0)
47254867
}

crates/bashkit/tests/spec_cases/bash/arithmetic.test.sh

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,73 @@ echo $((1 || 0 && 0))
201201
### expect
202202
1
203203
### end
204+
205+
### arith_exponentiation
206+
# ** power operator
207+
echo $((2 ** 10))
208+
### expect
209+
1024
210+
### end
211+
212+
### arith_exponentiation_variable
213+
# ** with variable
214+
x=5; echo $(( x ** 2 ))
215+
### expect
216+
25
217+
### end
218+
219+
### arith_base_hex
220+
# Base conversion: 16#ff = 255
221+
echo $((16#ff))
222+
### expect
223+
255
224+
### end
225+
226+
### arith_base_binary
227+
# Base conversion: 2#1010 = 10
228+
echo $((2#1010))
229+
### expect
230+
10
231+
### end
232+
233+
### arith_base_octal
234+
# Base conversion: 8#77 = 63
235+
echo $((8#77))
236+
### expect
237+
63
238+
### end
239+
240+
### arith_hex_literal
241+
# 0x hex literal
242+
echo $((0xff))
243+
### expect
244+
255
245+
### end
246+
247+
### arith_octal_literal
248+
# Octal literal
249+
echo $((077))
250+
### expect
251+
63
252+
### end
253+
254+
### arith_unary_negate
255+
# Unary negation
256+
echo $((-5))
257+
### expect
258+
-5
259+
### end
260+
261+
### arith_bitwise_not
262+
# Bitwise NOT
263+
echo $((~0))
264+
### expect
265+
-1
266+
### end
267+
268+
### arith_logical_not
269+
# Logical NOT
270+
echo $((!0))
271+
### expect
272+
1
273+
### end

crates/bashkit/tests/spec_cases/bash/arrays.test.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,31 @@ b
154154
2
155155
3
156156
### end
157+
158+
### mapfile_basic
159+
### bash_diff: pipes don't create subshells in bashkit (stateless model)
160+
# mapfile reads lines into array from pipe
161+
printf 'a\nb\nc\n' | mapfile -t lines; echo ${#lines[@]}; echo ${lines[0]}; echo ${lines[1]}; echo ${lines[2]}
162+
### expect
163+
3
164+
a
165+
b
166+
c
167+
### end
168+
169+
### readarray_alias
170+
### bash_diff: pipes don't create subshells in bashkit (stateless model)
171+
# readarray is an alias for mapfile
172+
printf 'x\ny\n' | readarray -t arr; echo ${arr[0]} ${arr[1]}
173+
### expect
174+
x y
175+
### end
176+
177+
### mapfile_default_name
178+
### bash_diff: pipes don't create subshells in bashkit (stateless model)
179+
# mapfile default array name is MAPFILE
180+
printf 'hello\nworld\n' | mapfile -t; echo ${MAPFILE[0]}; echo ${MAPFILE[1]}
181+
### expect
182+
hello
183+
world
184+
### end

0 commit comments

Comments
 (0)