This is how array variable subscripts should expand. The demo code should run in bash or zsh. Array variables work similarly in mksh and most versions of ksh93 until recently. They have some bugs and missing features.
1 #!/usr/bin/env bash
2 if [[ -v ZSH_VERSION ]]; then
3 emulate ksh
4 zmodload zsh/system
5 elif [[ -v BASH_VERSION ]]; then
6 shopt -s lastpipe extglob expand_aliases
7 shopt -u {assoc,array}_expand_once 2>/dev/null
8 enable -f fdflags{,}
9 enable -f tee{,}
10 fi
11
12 function p {
13 printf ' %s ' "$1" >&3
14 IFS= read -rd "" t <&4
15 printf '%d\0' "$((t+1))" | tee /proc/self/fd/3 >&5
16 }
17
18 function f {
19 typeset -a a
20 typeset fd f='a[$(p x)0]'
21 {
22 if [[ -v BASH_VERSION ]]; then fdflags -s +nonblock 4 5
23 elif [[ -v ZSH_VERSION ]]; then sysopen -ro nonblock -u 4 /proc/self/fd/4; sysopen -wao nonblock -u 5 /proc/self/fd/5
24 fi
25 printf '%s\0' -1 >&5
26 (( $(p a)a[\$(p b)f$(p c)]$(p d) ))
27 } 3>&1 4<<<'' 4<$(flock 4)<(flock -w 1 4) 5>/proc/self/fd/4
28 echo
29 }
30
31 function main {
32 set +m
33 typeset BASH_COMPAT=51
34 f
35 unset -v BASH_COMPAT
36 f
37 echo
38 }
39 main
40 typeset -u SHELL=$SHELL
41 typeset -p "${SHELL##*/}_VERSION"
a 0 c 1 d 2 b 3 x 4 a 0 c 1 d 2 b 3 x 4 typeset ZSH_VERSION=5.9 a 0 c 1 d 2 b 3 x 4 a 0 c 1 d 2 b 3 x 4 declare -- BASH_VERSION="5.1.16(1)-release" a 0 c 1 d 2 b 3 x 4 a 0 c 1 d 2/proc/self/fd/9: line 25: $(p b)f: arithmetic syntax error: operand expected (error token is "$(p b)f") declare -- BASH_VERSION="5.3.0(1)-devel"
a, c, and d are never seen by the arithmetic expression. They are pre-expanded because they are unescaped and thus evaluated first from left-to-right before the arithmetic evaluation stage. Because p produces nothing on stdout the overall expression is effectively reduced to a[\$(p b)f].
Next the arithmetic expression encounters the array variable a which must call the variable resolver with the variable name including its subscript. The variable resolver now performs expansions left-to-right on the subscript, evaluating b which again produces no stdout and effectively reduces the overall expression to a[f]. Now the variable resolver calls the arithmetic evaluator again to evaluate the index which has been reduced to just f.
Because f is a string that is not a valid integer literal, bash evaluates its value as an arithmetic expression so we now evaluate a[$(p x)0]. Thus x is the final command substitution to be evaluated and the overall expression is effectively reduced to a[a[0]]. The array variable a is a dummy variable with no elements so the result of the expression is 0 and all of the output was produced as a side-effect of evaluating the command substitutions.
test -v and unset
Example: Filter and uniqify an array.
1 function purify_cows {
2 typeset -n _ref=$1
3 typeset IFS x=$2
4 typeset -A set
5 set -- "${_ref[@]@Q}"
6 eval -- "set+=(${*/*/[&]=})"
7 if BASH_COMPAT=51 test -v 'set[$x]'; then
8 BASH_COMPAT=51 IFS= command -- unset -v -- 'set[${x}${IFS[BASH_COMPAT=${BASH_VERSINFO[*]::2},0]}]'
9 _ref=("${!set[@]}")
10 else
11 return 1
12 fi
13 }
14
15 function _main {
16 local -
17 set -x
18 typeset -a cows=(moo moo moooo! moo oom moo @ @ moo)
19 purify_cows cows @
20 printf '%s ' "${cows[@]}"
21 { BASH_XTRACEFD=3 echo; } 3>/dev/null
22 }
23
24 _main
+ cows=('moo' 'moo' 'moooo!' 'moo' 'oom' 'moo' '@' '@' 'moo') + typeset -a cows + purify_cows cows @ + typeset -n _ref=cows + typeset IFS x=@ + typeset -A set + set -- ''\''moo'\''' ''\''moo'\''' ''\''moooo!'\''' ''\''moo'\''' ''\''oom'\''' ''\''moo'\''' ''\''@'\''' ''\''@'\''' ''\''moo'\''' + eval -- 'set+=(['\''moo'\'']= ['\''moo'\'']= ['\''moooo!'\'']= ['\''moo'\'']= ['\''oom'\'']= ['\''moo'\'']= ['\''@'\'']= ['\''@'\'']= ['\''moo'\'']=)' ++ set+=(['moo']= ['moo']= ['moooo!']= ['moo']= ['oom']= ['moo']= ['@']= ['@']= ['moo']=) + BASH_COMPAT=51 + test -v 'set[$x]' + BASH_COMPAT=51 + IFS= + command -- unset -v -- 'set[${x}${IFS[BASH_COMPAT=${BASH_VERSINFO[*]::2},0]}]' + _ref=("${!set[@]}") + printf '%s ' oom 'moooo!' moo oom moooo! moo