Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
_ => {}
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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_<name>` 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(),
Expand Down
62 changes: 62 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/declare.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions specs/009-implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}`, `\<newline>` 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 |
Expand Down
Loading