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
2 changes: 1 addition & 1 deletion crates/bashkit/src/interpreter/declare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
29 changes: 19 additions & 10 deletions crates/bashkit/src/interpreter/expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
}
}
Expand Down
45 changes: 45 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/param-attr-transform.test.sh
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading