diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 0e5a59af..5e547381 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -3859,6 +3859,7 @@ impl Interpreter { let mut is_array = false; let mut is_assoc = false; let mut is_integer = false; + let mut is_nameref = false; let mut names: Vec<&str> = Vec::new(); for arg in args { @@ -3871,7 +3872,8 @@ impl Interpreter { 'a' => is_array = true, 'i' => is_integer = true, 'A' => is_assoc = true, - 'g' | 'l' | 'n' | 'u' | 't' | 'f' | 'F' => {} // ignored + 'n' => is_nameref = true, + 'g' | 'l' | 'u' | 't' | 'f' | 'F' => {} // ignored _ => {} } } @@ -4015,6 +4017,10 @@ impl Interpreter { arr.insert(idx, val.trim_matches('"').to_string()); } } + } else if is_nameref { + // declare -n ref=target: create nameref + self.variables + .insert(format!("_NAMEREF_{}", var_name), value.to_string()); } else if is_integer { // Evaluate as arithmetic expression let int_val = self.evaluate_arithmetic_with_assign(value); @@ -4037,7 +4043,10 @@ impl Interpreter { } } else { // Declare without value - if is_assoc { + if is_nameref { + // declare -n ref (without value) - just mark as nameref + // The target will be set later via assignment + } else if is_assoc { // Initialize empty associative array self.assoc_arrays.entry(name.to_string()).or_default(); } else if is_array { @@ -5616,18 +5625,40 @@ impl Interpreter { /// If the variable is declared `local` in any active call frame, update that frame. /// Otherwise, set in global variables. fn set_variable(&mut self, name: String, value: String) { + // Resolve nameref: if `name` is a nameref, assign to the target instead + let resolved = self.resolve_nameref(&name).to_string(); for frame in self.call_stack.iter_mut().rev() { if let std::collections::hash_map::Entry::Occupied(mut e) = - frame.locals.entry(name.clone()) + frame.locals.entry(resolved.clone()) { e.insert(value); return; } } - self.variables.insert(name, value); + self.variables.insert(resolved, value); + } + + /// Resolve nameref chains: if `name` has a `_NAMEREF_` marker, + /// follow the chain (up to 10 levels to prevent infinite loops). + fn resolve_nameref<'a>(&'a self, name: &'a str) -> &'a str { + let mut current = name; + for _ in 0..10 { + let key = format!("_NAMEREF_{}", current); + if let Some(target) = self.variables.get(&key) { + // target is owned by the HashMap, so we can return a reference to it + // But we need to work with &str. Let's use a different approach. + current = target.as_str(); + } else { + break; + } + } + current } fn expand_variable(&self, name: &str) -> String { + // Resolve nameref before expansion + let name = self.resolve_nameref(name); + // Check for special parameters (POSIX required) match name { "?" => return self.last_exit_code.to_string(), diff --git a/crates/bashkit/tests/spec_cases/bash/declare.test.sh b/crates/bashkit/tests/spec_cases/bash/declare.test.sh index d9cdf213..e11b1993 100644 --- a/crates/bashkit/tests/spec_cases/bash/declare.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/declare.test.sh @@ -81,3 +81,65 @@ echo "$myvar" ### expect hello ### end + +### nameref_basic +# declare -n creates a name reference +x=hello +declare -n ref=x +echo "$ref" +### expect +hello +### end + +### nameref_assign_through +# Assigning to nameref assigns to target variable +x=old +declare -n ref=x +ref=new +echo "$x" +### expect +new +### end + +### nameref_chain +# Nameref can chain through another nameref +a=value +declare -n b=a +declare -n c=b +echo "$c" +### expect +value +### end + +### nameref_in_function +# Nameref used to pass variable names to functions +set_via_ref() { + declare -n ref=$1 + ref="set_by_function" +} +result="" +set_via_ref result +echo "$result" +### expect +set_by_function +### end + +### nameref_read_unset +# Reading through nameref to unset variable returns empty +declare -n ref=nonexistent_var +echo "[$ref]" +### expect +[] +### end + +### nameref_reassign_target +# Changing the target variable reflects through the nameref +x=first +declare -n ref=x +echo "$ref" +x=second +echo "$ref" +### expect +first +second +### end diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index ec686efe..e3abbc56 100644 --- a/specs/009-implementation-status.md +++ b/specs/009-implementation-status.md @@ -107,13 +107,13 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | Category | Cases | In CI | Pass | Skip | Notes | |----------|-------|-------|------|------|-------| -| Bash (core) | 887 | Yes | 882 | 5 | `bash_spec_tests` in CI | +| Bash (core) | 893 | Yes | 888 | 5 | `bash_spec_tests` in CI | | AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g | | Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect | | Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E | | JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env | | Python | 57 | Yes | 57 | 0 | embedded Python (Monty) | -| **Total** | **1305** | **Yes** | **1300** | **5** | | +| **Total** | **1311** | **Yes** | **1306** | **5** | | ### Bash Spec Tests Breakdown @@ -160,7 +160,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | variables.test.sh | 86 | includes special vars, prefix env, PIPESTATUS, trap EXIT, `${var@Q}`, `\` line continuation, PWD/HOME/USER/HOSTNAME/BASH_VERSION/SECONDS, `set -x` xtrace, `shopt` builtin, nullglob | | wc.test.sh | 35 | word count (5 skipped) | | type.test.sh | 15 | `type`, `which`, `hash` builtins | -| declare.test.sh | 10 | `declare`/`typeset`, `-i`, `-r`, `-x`, `-a`, `-p` | +| declare.test.sh | 16 | `declare`/`typeset`, `-i`, `-r`, `-x`, `-a`, `-p`, `-n` nameref | | ln.test.sh | 5 | `ln -s`, `-f`, symlink creation | | eval-bugs.test.sh | 4 | regression tests for eval/script bugs | | script-exec.test.sh | 10 | script execution by path, $PATH search, exit codes |