Skip to content

Commit 4d514d2

Browse files
chaliyclaude
andauthored
feat(interpreter): implement declare -n nameref variables (#255)
## Summary - Implement `declare -n` nameref variables that act as aliases for other variable names - Add `resolve_nameref()` with chain following (max 10 levels) to prevent infinite loops - Integrate nameref resolution into variable expansion and assignment paths ## Changes - `crates/bashkit/src/interpreter/mod.rs`: Parse `-n` flag, store `_NAMEREF_<name>` markers, add `resolve_nameref()`, integrate into `expand_variable()` and `set_variable()` - `crates/bashkit/tests/spec_cases/bash/declare.test.sh`: 6 new spec tests (basic, assign-through, chain, function param, read-unset, reassign-target) - `specs/009-implementation-status.md`: Update test counts (declare 10→16, Bash 887→893, Total 1305→1311) ## Test plan - [x] `cargo test --all-features` passes - [x] `cargo fmt --check` clean - [x] `cargo clippy --all-targets --all-features -- -D warnings` clean - [x] All 6 nameref spec tests pass - [x] Existing declare tests still pass (no regressions) Co-authored-by: Claude <noreply@anthropic.com>
1 parent ca21002 commit 4d514d2

File tree

3 files changed

+100
-7
lines changed

3 files changed

+100
-7
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3859,6 +3859,7 @@ impl Interpreter {
38593859
let mut is_array = false;
38603860
let mut is_assoc = false;
38613861
let mut is_integer = false;
3862+
let mut is_nameref = false;
38623863
let mut names: Vec<&str> = Vec::new();
38633864

38643865
for arg in args {
@@ -3871,7 +3872,8 @@ impl Interpreter {
38713872
'a' => is_array = true,
38723873
'i' => is_integer = true,
38733874
'A' => is_assoc = true,
3874-
'g' | 'l' | 'n' | 'u' | 't' | 'f' | 'F' => {} // ignored
3875+
'n' => is_nameref = true,
3876+
'g' | 'l' | 'u' | 't' | 'f' | 'F' => {} // ignored
38753877
_ => {}
38763878
}
38773879
}
@@ -4015,6 +4017,10 @@ impl Interpreter {
40154017
arr.insert(idx, val.trim_matches('"').to_string());
40164018
}
40174019
}
4020+
} else if is_nameref {
4021+
// declare -n ref=target: create nameref
4022+
self.variables
4023+
.insert(format!("_NAMEREF_{}", var_name), value.to_string());
40184024
} else if is_integer {
40194025
// Evaluate as arithmetic expression
40204026
let int_val = self.evaluate_arithmetic_with_assign(value);
@@ -4037,7 +4043,10 @@ impl Interpreter {
40374043
}
40384044
} else {
40394045
// Declare without value
4040-
if is_assoc {
4046+
if is_nameref {
4047+
// declare -n ref (without value) - just mark as nameref
4048+
// The target will be set later via assignment
4049+
} else if is_assoc {
40414050
// Initialize empty associative array
40424051
self.assoc_arrays.entry(name.to_string()).or_default();
40434052
} else if is_array {
@@ -5616,18 +5625,40 @@ impl Interpreter {
56165625
/// If the variable is declared `local` in any active call frame, update that frame.
56175626
/// Otherwise, set in global variables.
56185627
fn set_variable(&mut self, name: String, value: String) {
5628+
// Resolve nameref: if `name` is a nameref, assign to the target instead
5629+
let resolved = self.resolve_nameref(&name).to_string();
56195630
for frame in self.call_stack.iter_mut().rev() {
56205631
if let std::collections::hash_map::Entry::Occupied(mut e) =
5621-
frame.locals.entry(name.clone())
5632+
frame.locals.entry(resolved.clone())
56225633
{
56235634
e.insert(value);
56245635
return;
56255636
}
56265637
}
5627-
self.variables.insert(name, value);
5638+
self.variables.insert(resolved, value);
5639+
}
5640+
5641+
/// Resolve nameref chains: if `name` has a `_NAMEREF_<name>` marker,
5642+
/// follow the chain (up to 10 levels to prevent infinite loops).
5643+
fn resolve_nameref<'a>(&'a self, name: &'a str) -> &'a str {
5644+
let mut current = name;
5645+
for _ in 0..10 {
5646+
let key = format!("_NAMEREF_{}", current);
5647+
if let Some(target) = self.variables.get(&key) {
5648+
// target is owned by the HashMap, so we can return a reference to it
5649+
// But we need to work with &str. Let's use a different approach.
5650+
current = target.as_str();
5651+
} else {
5652+
break;
5653+
}
5654+
}
5655+
current
56285656
}
56295657

56305658
fn expand_variable(&self, name: &str) -> String {
5659+
// Resolve nameref before expansion
5660+
let name = self.resolve_nameref(name);
5661+
56315662
// Check for special parameters (POSIX required)
56325663
match name {
56335664
"?" => return self.last_exit_code.to_string(),

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,65 @@ echo "$myvar"
8181
### expect
8282
hello
8383
### end
84+
85+
### nameref_basic
86+
# declare -n creates a name reference
87+
x=hello
88+
declare -n ref=x
89+
echo "$ref"
90+
### expect
91+
hello
92+
### end
93+
94+
### nameref_assign_through
95+
# Assigning to nameref assigns to target variable
96+
x=old
97+
declare -n ref=x
98+
ref=new
99+
echo "$x"
100+
### expect
101+
new
102+
### end
103+
104+
### nameref_chain
105+
# Nameref can chain through another nameref
106+
a=value
107+
declare -n b=a
108+
declare -n c=b
109+
echo "$c"
110+
### expect
111+
value
112+
### end
113+
114+
### nameref_in_function
115+
# Nameref used to pass variable names to functions
116+
set_via_ref() {
117+
declare -n ref=$1
118+
ref="set_by_function"
119+
}
120+
result=""
121+
set_via_ref result
122+
echo "$result"
123+
### expect
124+
set_by_function
125+
### end
126+
127+
### nameref_read_unset
128+
# Reading through nameref to unset variable returns empty
129+
declare -n ref=nonexistent_var
130+
echo "[$ref]"
131+
### expect
132+
[]
133+
### end
134+
135+
### nameref_reassign_target
136+
# Changing the target variable reflects through the nameref
137+
x=first
138+
declare -n ref=x
139+
echo "$ref"
140+
x=second
141+
echo "$ref"
142+
### expect
143+
first
144+
second
145+
### end

specs/009-implementation-status.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,13 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
107107

108108
| Category | Cases | In CI | Pass | Skip | Notes |
109109
|----------|-------|-------|------|------|-------|
110-
| Bash (core) | 887 | Yes | 882 | 5 | `bash_spec_tests` in CI |
110+
| Bash (core) | 893 | Yes | 888 | 5 | `bash_spec_tests` in CI |
111111
| AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g |
112112
| Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect |
113113
| Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E |
114114
| JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env |
115115
| Python | 57 | Yes | 57 | 0 | embedded Python (Monty) |
116-
| **Total** | **1305** | **Yes** | **1300** | **5** | |
116+
| **Total** | **1311** | **Yes** | **1306** | **5** | |
117117

118118
### Bash Spec Tests Breakdown
119119

@@ -160,7 +160,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
160160
| variables.test.sh | 86 | includes special vars, prefix env, PIPESTATUS, trap EXIT, `${var@Q}`, `\<newline>` line continuation, PWD/HOME/USER/HOSTNAME/BASH_VERSION/SECONDS, `set -x` xtrace, `shopt` builtin, nullglob |
161161
| wc.test.sh | 35 | word count (5 skipped) |
162162
| type.test.sh | 15 | `type`, `which`, `hash` builtins |
163-
| declare.test.sh | 10 | `declare`/`typeset`, `-i`, `-r`, `-x`, `-a`, `-p` |
163+
| declare.test.sh | 16 | `declare`/`typeset`, `-i`, `-r`, `-x`, `-a`, `-p`, `-n` nameref |
164164
| ln.test.sh | 5 | `ln -s`, `-f`, symlink creation |
165165
| eval-bugs.test.sh | 4 | regression tests for eval/script bugs |
166166
| script-exec.test.sh | 10 | script execution by path, $PATH search, exit codes |

0 commit comments

Comments
 (0)