From 10186d5f3361523271d3825fd031d6ed582a3b10 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 8 Oct 2026 02:00:35 +0000 Subject: [PATCH] fix(interpreter): ${v@a} prints every attribute letter Reuses declare -p's attribute letters, follows namerefs, and repeats per element for ${arr[@]@a}. It only knew r and x. --- crates/bashkit/src/interpreter/declare.rs | 2 +- crates/bashkit/src/interpreter/expansion.rs | 29 +++++++----- .../bash/param-attr-transform.test.sh | 45 +++++++++++++++++++ knowledge/log.md | 1 + 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 crates/bashkit/tests/spec_cases/bash/param-attr-transform.test.sh diff --git a/crates/bashkit/src/interpreter/declare.rs b/crates/bashkit/src/interpreter/declare.rs index 39b62e4b7..9af04aba5 100644 --- a/crates/bashkit/src/interpreter/declare.rs +++ b/crates/bashkit/src/interpreter/declare.rs @@ -376,7 +376,7 @@ impl Interpreter { } /// Attribute letters of `name` in bash's order (`aAinrxlu`), or `-`. - fn attr_letters(&self, name: &str) -> String { + pub(super) fn attr_letters(&self, name: &str) -> String { let attrs = self.var_attrs_get(name); let mut s = String::new(); match self.var_kind(name) { diff --git a/crates/bashkit/src/interpreter/expansion.rs b/crates/bashkit/src/interpreter/expansion.rs index 479009aff..6f756d879 100644 --- a/crates/bashkit/src/interpreter/expansion.rs +++ b/crates/bashkit/src/interpreter/expansion.rs @@ -122,6 +122,25 @@ impl Interpreter { /// Apply a `${var@operator}` transformation. pub(super) fn apply_transformation(&self, name: &str, operator: char) -> String { + // `${v@a}`: the variable's attribute letters, the same ones + // `declare -p` prints. A subscript picks the array, a nameref its + // target, and `${a[@]@a}` repeats the letters once per element. + if operator == 'a' { + let base = name.split('[').next().unwrap_or(name); + let letters = self.attr_letters(self.resolve_nameref(base)); + let letters = if letters == "-" { + String::new() + } else { + letters + }; + if name.ends_with("[@]") || name.ends_with("[*]") { + let count = self + .resolve_param_expansion_elements(name) + .map_or(0, |elems| elems.len()); + return vec![letters; count].join(" "); + } + return letters; + } // `${a[@]@Q}`, `${@@U}`: value transforms apply to each element. if matches!(operator, 'Q' | 'E' | 'P' | 'u' | 'U' | 'L') && let Some(elems) = self.resolve_param_expansion_elements(name) @@ -147,16 +166,6 @@ impl Interpreter { }; match operator { 'A' => format!("{}='{}'", name, value.replace('\'', "'\\''")), - 'a' => { - let mut attrs = String::new(); - if self.is_var_readonly(name) { - attrs.push('r'); - } - if self.env.contains_key(name) { - attrs.push('x'); - } - attrs - } _ => Self::transform_value(&value, operator), } } diff --git a/crates/bashkit/tests/spec_cases/bash/param-attr-transform.test.sh b/crates/bashkit/tests/spec_cases/bash/param-attr-transform.test.sh new file mode 100644 index 000000000..95ef5c6ea --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/param-attr-transform.test.sh @@ -0,0 +1,45 @@ +### attr_transform_scalar_flags +declare -irx a=1 +echo "${a@a}" +declare -u up=x +echo "${up@a}" +plain=1 +echo "[${plain@a}]" +echo "[${never_set@a}]" +### expect +irx +u +[] +[] +### end + +### attr_transform_arrays +declare -alr b=(X) +echo "${b@a}" +declare -a e +echo "[${e@a}]" +declare -A f=([k]=v) +echo "${f[k]@a} ${f@a}" +g=(1 2) +echo "${g[@]@a}|${g[1]@a}" +### expect +arl +[a] +A A +a a|a +### end + +### attr_transform_follows_nameref +declare -i target=3 +declare -n ref=target +echo "${ref@a}" +### expect +i +### end + +### attr_transform_local_integer +f() { local -i z=1; echo "${z@a}"; } +f +### expect +i +### end diff --git a/knowledge/log.md b/knowledge/log.md index dc56341d8..792c71bef 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -2,6 +2,7 @@ ## 2026-10-07 +* **Fix**: `${v@a}` prints every attribute letter `declare -p` shows, in bash's order (`a`/`A`, `i`, `n`, `r`, `x`, `l`, `u`), follows namerefs, takes the array from a subscript, and repeats per element for `${arr[@]@a}`. It only knew `r` and `x`, so `declare -i n; ${n@a}` was empty. - Added the bash-oracle scoreboard (`scripts/bash-oracle`, `just bash-oracle`): 108 whole-script cases replayed against output recorded from real bash 5.2, scored as `match` (status + stdout, 90/108) and `strict` (also stderr,