Skip to content

Commit abff610

Browse files
authored
fix(interpreter): support recursive function calls inside $() command substitution (#1043)
## Summary - Fix recursive function calls inside `$()` command substitution that were silently failing - Root cause: `expand_command_subs_in_arithmetic` executed `$()` without saving/restoring interpreter state, causing variable mutations to leak from subshell scope - Added save/restore of all mutable interpreter state matching the existing `CommandSubstitution` handler pattern ## Test plan - [ ] `recursive_function_command_subst` — factorial(5) = 120 - [ ] `recursive_depth_3` — nested string building via recursion - [ ] `recursive_cmdsub_var_isolation` — variable mutations inside `$()` in arithmetic don't leak - [ ] All 1928 existing bash spec tests pass Closes #962
1 parent c038e85 commit abff610

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8371,7 +8371,8 @@ impl Interpreter {
83718371
cmd.push(c);
83728372
}
83738373
}
8374-
// Execute the command and substitute
8374+
// Execute the command and substitute in a subshell context:
8375+
// save/restore mutable state so mutations don't leak.
83758376
let parser = Parser::with_limits(
83768377
&cmd,
83778378
self.limits.max_ast_depth,
@@ -8382,8 +8383,24 @@ impl Interpreter {
83828383
if self.counters.push_function(&self.limits).is_err() {
83838384
result.push('0');
83848385
} else {
8386+
let saved_vars = self.variables.clone();
8387+
let saved_arrays = self.arrays.clone();
8388+
let saved_assoc = self.assoc_arrays.clone();
8389+
let saved_functions = self.functions.clone();
8390+
let saved_traps = self.traps.clone();
8391+
let saved_aliases = self.aliases.clone();
8392+
let saved_cwd = self.cwd.clone();
8393+
let saved_memory_budget = self.memory_budget.clone();
83858394
let cmd_result =
83868395
self.execute_command_sequence(&script.commands).await?;
8396+
self.variables = saved_vars;
8397+
self.arrays = saved_arrays;
8398+
self.assoc_arrays = saved_assoc;
8399+
self.functions = saved_functions;
8400+
self.traps = saved_traps;
8401+
self.aliases = saved_aliases;
8402+
self.cwd = saved_cwd;
8403+
self.memory_budget = saved_memory_budget;
83878404
self.counters.pop_function();
83888405
let trimmed = cmd_result.stdout.trim_end_matches('\n');
83898406
if trimmed.is_empty() {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
### recursive_function_command_subst
2+
# Recursive function calls inside $() should work
3+
factorial() {
4+
if (( $1 <= 1 )); then echo 1
5+
else echo $(( $1 * $(factorial $(($1 - 1))) ))
6+
fi
7+
}
8+
factorial 5
9+
### expect
10+
120
11+
### end
12+
13+
### recursive_depth_3
14+
# Recursive function with depth 3
15+
f() {
16+
if (( $1 <= 0 )); then echo "base"
17+
else echo "depth=$1 $(f $(($1 - 1)))"
18+
fi
19+
}
20+
f 3
21+
### expect
22+
depth=3 depth=2 depth=1 base
23+
### end
24+
25+
### recursive_cmdsub_var_isolation
26+
# Variable mutations inside $() in arithmetic should not leak to parent
27+
x=100
28+
inner() {
29+
x=999
30+
echo 42
31+
}
32+
result=$(( $(inner) + x ))
33+
echo "$result $x"
34+
### expect
35+
142 100
36+
### end

0 commit comments

Comments
 (0)