Skip to content

Commit

Permalink
[osh] Improve bash compatibility for ${a@a} and ${a[0]@A} (#2208)
Browse files Browse the repository at this point in the history
* [spec] Update test spec var-op-bash#19
* documented with ul-table in doc/ref/chap-word-lang!  Using the concepts of "h-value" and "r-value"

---------

Co-authored-by: Andy C <[email protected]>
  • Loading branch information
akinomyoga and Andy C authored Dec 31, 2024
1 parent b5e15f3 commit f2c84da
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 15 deletions.
5 changes: 3 additions & 2 deletions core/runtime.asdl
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ module runtime

coerced = Int | Float | Neither

# evaluation state for BracedVarSub
VarSubState = (bool join_array, bool is_type_query, Token array_ref)
# evaluation state for BracedVarSub. See "doc/ref/chap-word-lang.md" for
# the description of h-value of a variable substitution.
VarSubState = (bool join_array, value h_value, Token array_ref)

# A Cell is a wrapper for a value.
# TODO: add location for declaration for 'assigning const' error
Expand Down
89 changes: 89 additions & 0 deletions doc/ref/chap-word-lang.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,92 @@ string:
$ unset -v x
$ echo "value = $x, quoted = ${x@Q}."
value = , quoted = .

---

`${x@a}` returns characters that represent the attributes of the `${x}`, or
more precisely, the *h-value* of `${x}`.

Definitions:

- *h-value* is the variable (or the object that the variable directly points)
from which the result of `${x}` would originally come.
- *r-value* is the value of the expansion of `${x}`

For example, with `arr=(1 2 3)`:

<style>
table {
width: 100%;
margin-left: 2em; /* matches p text in manual.css */
}
thead {
text-align: left;
}
</style>

<table>

- thead
- Reference
- Expression
- H-value
- R-value
- Flags returned
- tr
- <!-- empty -->
- `${arr[0]@a}` or <br/> `${arr@a}`
- array<br/> `(1 2 3)`
- string<br/> `1`
- `a`
- tr
- <!-- empty -->
- `${arr[@]@a}`
- array<br/> `(1 2 3)`
- array<br/> `(1 2 3)`
- `a a a`
- tr
- `ref=arr` or `ref=arr[0]`
- `${!ref@a}`
- array<br/> `(1 2 3)`
- string<br/> `1`
- `a`
- <!-- empty -->
- tr
- `ref=arr[@]`
- `${!ref@a}`
- array<br/> `(1 2 3)`
- array<br/> `(1 2 3)`
- `a a a`

</table>

When `${x}` would result in a word list, `${x@a}` returns a word list
containing the attributes of the *h-value* of each word.

---

These characters may be returned:

<table>

- thead
- Character
- Where `${x}` would be obtained
- tr
- `a`
- indexed array
- tr
- `A`
- associative array
- tr
- `r`
- readonly container
- tr
- `x`
- exported variable
- tr
- `n`
- name reference (OSH extension)

</table>
18 changes: 6 additions & 12 deletions osh/word_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ def _Nullary(self, val, op, var_name, vsub_token, vsub_state):
# We're ONLY simluating -a and -A, not -r -x -n for now. See
# spec/ble-idioms.test.sh.
chars = [] # type: List[str]
with tagswitch(val) as case:
with tagswitch(vsub_state.h_value) as case:
if case(value_e.BashArray):
chars.append('a')
elif case(value_e.BashAssoc):
Expand Down Expand Up @@ -1329,8 +1329,7 @@ def _EvalBracketOp(self, val, part, quoted, vsub_state, vtest_place):
else: # no bracket op
var_name = vtest_place.name
if (var_name is not None and
val.tag() in (value_e.BashArray, value_e.BashAssoc) and
not vsub_state.is_type_query):
val.tag() in (value_e.BashArray, value_e.BashAssoc)):
if ShouldArrayDecay(var_name, self.exec_opts,
not (part.prefix_op or part.suffix_op)):
# for ${BASH_SOURCE}, etc.
Expand Down Expand Up @@ -1360,6 +1359,9 @@ def _VarRefValue(self, part, quoted, vsub_state, vtest_place):
# $* decays
val = self._EvalSpecialVar(part.name_tok.id, quoted, vsub_state)

# update h-value (i.e., the holder of the current value)
vsub_state.h_value = val

# We don't need var_index because it's only for L-Values of test ops?
if self.exec_opts.eval_unsafe_arith():
val = self._EvalBracketOp(val, part, quoted, vsub_state,
Expand Down Expand Up @@ -1452,15 +1454,7 @@ def _EvalBracedVarSub(self, part, part_vals, quoted):
suffix_op_ = part.suffix_op
if suffix_op_:
UP_op = suffix_op_
with tagswitch(suffix_op_) as case:
if case(suffix_op_e.Nullary):
suffix_op_ = cast(Token, UP_op)

# Type query ${array@a} is a STRING, not an array
# NOTE: ${array@Q} is ${array[0]@Q} in bash, which is different than
# ${array[@]@Q}
if suffix_op_.id == Id.VOp0_a:
vsub_state.is_type_query = True
vsub_state.h_value = val

# 2. Bracket Op
val = self._EvalBracketOp(val, part, quoted, vsub_state, vtest_place)
Expand Down
46 changes: 45 additions & 1 deletion spec/var-op-bash.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -377,14 +377,24 @@ fail
#### ${!A@a} and ${!A[@]@a}
declare -A A=(["x"]=y)
echo x=${!A[@]@a}
echo x=${!A@a}
echo invalid=${!A@a}

# OSH prints 'a' for indexed array because the AssocArray with ! turns into
# it. Disallowing it would be the other reasonable behavior.

## status: 1
## STDOUT:
x=
## END

# Bash succeeds with ${!A@a}, which references the variable named as $A (i.e.,
# ''). This must be a Bash bug since the behavior is inconsistent with the
# fact that ${!undef@a} and ${!empty@a} fail.

## BUG bash status: 0
## BUG bash STDOUT:
x=
invalid=
## END

#### undef vs. empty string in var ops
Expand Down Expand Up @@ -428,3 +438,37 @@ stat: 1
stat: 1
stat: 1
## END


#### ${a[0]@a} and ${a@a}

a=(1 2 3)
echo "attr = '${a[0]@a}'"
echo "attr = '${a@a}'"

## STDOUT:
attr = 'a'
attr = 'a'
## END


#### ${!r@a} with r='a[0]' (attribute for indirect expansion of an array element)

a=(1 2 3)
r='a'
echo ${!r@a}
r='a[0]'
echo ${!r@a}

declare -A d=([0]=foo [1]=bar)
r='d'
echo ${!r@a}
r='d[0]'
echo ${!r@a}

## STDOUT:
a
a
A
A
## END

0 comments on commit f2c84da

Please sign in to comment.