From 5359afc1d5e104e2479b738070363a92af0afb25 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 27 Mar 2026 23:48:06 +0000 Subject: [PATCH] fix(interpreter): handle compound array assignment in local builtin `local arr=(one two three)` produced an empty array because the compound assignment syntax was only recognized when -a or -A flags were present. Detect =(...) syntax regardless of flags in both function-scope and global-scope paths of execute_local_builtin. Also unskip nameref_local_dynamic_scope test that depended on this fix. Closes #877 --- crates/bashkit/src/interpreter/mod.rs | 57 +++++++++++++++++-- .../tests/spec_cases/bash/arrays.test.sh | 23 ++++++++ .../tests/spec_cases/bash/nameref.test.sh | 2 +- crates/bashkit/tests/spec_tests.rs | 4 +- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 362b915e..972495ca 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -4421,11 +4421,9 @@ impl Interpreter { if is_internal_variable(var_name) { continue; } - // Handle compound array assignment: local -a arr=(1 2 3) - if (flags.array || flags.assoc) - && value.starts_with('(') - && value.ends_with(')') - { + // Handle compound array assignment: local arr=(1 2 3) or local -a/-A arr=(...) + let is_compound = value.starts_with('(') && value.ends_with(')'); + if is_compound { let inner = &value[1..value.len() - 1]; if flags.assoc { let arr = self.assoc_arrays.entry(var_name.to_string()).or_default(); @@ -4534,7 +4532,54 @@ impl Interpreter { if is_internal_variable(var_name) { continue; } - if flags.nameref { + let is_compound = value.starts_with('(') && value.ends_with(')'); + if is_compound { + let inner = &value[1..value.len() - 1]; + if flags.assoc { + let arr = self.assoc_arrays.entry(var_name.to_string()).or_default(); + arr.clear(); + let mut rest = inner.trim(); + while let Some(bracket_start) = rest.find('[') { + if let Some(bracket_end) = rest[bracket_start..].find(']') { + let key = &rest[bracket_start + 1..bracket_start + bracket_end]; + let after = &rest[bracket_start + bracket_end + 1..]; + if let Some(eq_rest) = after.strip_prefix('=') { + let eq_rest = eq_rest.trim_start(); + let (val, remainder) = + if let Some(stripped) = eq_rest.strip_prefix('"') { + if let Some(end_q) = stripped.find('"') { + ( + &stripped[..end_q], + stripped[end_q + 1..].trim_start(), + ) + } else { + (stripped.trim_end_matches('"'), "") + } + } else { + match eq_rest.find(char::is_whitespace) { + Some(sp) => { + (&eq_rest[..sp], eq_rest[sp..].trim_start()) + } + None => (eq_rest, ""), + } + }; + arr.insert(key.to_string(), val.to_string()); + rest = remainder; + } else { + break; + } + } else { + break; + } + } + } else { + let arr = self.arrays.entry(var_name.to_string()).or_default(); + arr.clear(); + for (idx, val) in inner.split_whitespace().enumerate() { + arr.insert(idx, val.trim_matches('"').to_string()); + } + } + } else if flags.nameref { self.variables .insert(format!("_NAMEREF_{}", var_name), value.to_string()); } else { diff --git a/crates/bashkit/tests/spec_cases/bash/arrays.test.sh b/crates/bashkit/tests/spec_cases/bash/arrays.test.sh index 825187b0..3a2b5cae 100644 --- a/crates/bashkit/tests/spec_cases/bash/arrays.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/arrays.test.sh @@ -212,3 +212,26 @@ echo "${arr[@]}" ### expect 10 20 30 40 99 ### end + +### local_array_compound_assignment +# local arr=(a b c) should initialize the array +myfunc() { + local arr=(one two three) + echo "count: ${#arr[@]}" + echo "values: ${arr[*]}" +} +myfunc +### expect +count: 3 +values: one two three +### end + +### local_array_compound_in_global +# local arr=(...) at global scope should also work +local arr=(x y z) +echo "${#arr[@]}" +echo "${arr[1]}" +### expect +3 +y +### end diff --git a/crates/bashkit/tests/spec_cases/bash/nameref.test.sh b/crates/bashkit/tests/spec_cases/bash/nameref.test.sh index e6d4c586..29adf5a9 100644 --- a/crates/bashkit/tests/spec_cases/bash/nameref.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/nameref.test.sh @@ -44,7 +44,7 @@ jam ### nameref_local_dynamic_scope # pass local array by reference via dynamic scoping -### skip: TODO parser does not handle local arr=(...) syntax (indexed array after command name) +### bash_diff: nameref + local array by reference show_value() { local -n array_name=$1 local idx=$2 diff --git a/crates/bashkit/tests/spec_tests.rs b/crates/bashkit/tests/spec_tests.rs index dc5e4cac..39e084ca 100644 --- a/crates/bashkit/tests/spec_tests.rs +++ b/crates/bashkit/tests/spec_tests.rs @@ -8,7 +8,7 @@ //! - `### skip: reason` - Skip test entirely (not run in any test) //! - `### bash_diff: reason` - Known difference from real bash (runs in spec tests, excluded from comparison) //! -//! ## Skipped Tests (33 total) +//! ## Skipped Tests (32 total) //! //! Actual `### skip:` markers across spec test files: //! @@ -24,8 +24,6 @@ //! - [ ] od output format varies //! - [ ] hexdump -C output format varies //! -//! ### nameref.test.sh (1 skipped) -//! - [ ] parser does not handle local arr=(...) syntax //! //! ### parse-errors.test.sh (6 skipped) //! - [ ] parser does not reject unexpected 'do' keyword