10

The Bash man page describes use of ${!a} to return the contents of the variable whose name is the contents of a (a level of indirection).

I'd like to know how to return all elements in an array using this, i.e.,

a=(one two three)
echo ${a[*]}

returns

one two three

I would like for:

b=a
echo ${!b[*]}

to return the same. Unfortunately, it doesn't, but returns 0 instead.

Update

Given the replies, I now realise that my example was too simple, since of course, something like:

b=("${a[@]}")

Will achieve exactly what I said I needed.

So, here's what I was trying to do:

LIST_lys=(lys1 lys2)
LIST_diaspar=(diaspar1 diaspar2)

whichone=$1   # 'lys' or 'diaspar'

_LIST=LIST_$whichone
LIST=${!_LIST[*]}

Of course, carefully reading the Bash man page shows that this won't work as expected because the last line simply returns the indices of the "array" $_LIST (not an array at all).

In any case, the following should do the job (as pointed out):

LIST=($(eval echo \${$_LIST[*]}))

or ... (the route that I went, eventually):

LIST_lys="lys1 lys2"
...
LIST=(${!_LIST})

Assuming, of course, that elements don't contain whitespace.

MikeD
  • 800
  • 5
  • 16
Eric Smith
  • 1,274
  • 1
  • 9
  • 16
  • Add `[@]` to the pointer `_LIST="LIST_${whichone}[@]"` and then, use `LIST=("${!_LIST}")` to copy the array. It is a good idea to use lower case variable names to avoid conflicts with environment variables (All caps). –  Mar 09 '17 at 02:52

4 Answers4

9

I think the use of indirect reference of bash variable should be treated literally.

Eg. For your original example:

a=(one two three)
echo ${a[*]} # one two three
b=a
echo ${!b[*]} # this would not work, because this notation 
              # gives the indices of the variable b which
              # is a string in this case and could be thought
              # as a array that conatins only one element, so
              # we get 0 which means the first element
c='a[*]'
echo ${!c} # this will do exactly what you want in the first
           # place

For the last real scenario, I believe the code below would do the work.

LIST_lys=(lys1 lys2)
LIST_diaspar=(diaspar1 diaspar2)

whichone=$1   # 'lys' or 'diaspar'

_LIST="LIST_$whichone"[*]
LIST=( "${!_LIST}" ) # Of course for indexed array only 
                     # and not a sparse one

It is better to use notation "${var[@]}" which avoid messing up with the $IFS and parameter expansion. Here is the final code.

LIST_lys=(lys1 lys2)
LIST_diaspar=(diaspar1 diaspar2)

whichone=$1   # 'lys' or 'diaspar'

_LIST="LIST_$whichone"[@]
LIST=( "${!_LIST}" ) # Of course for indexed array only 
                     # and not a sparse one
                     # It is essential to have ${!_LIST} quoted
weynhamz
  • 208
  • 2
  • 3
8

You need to copy the elements explicitly. For an indexed array:

b=("${a[@]}")

For an associative array (note that a is the name of the array variable, not a variable whose value is the name of an array variable):

typeset -A b
for k in "${!a[@]}"; do b[$k]=${a[$k]}; done

If you have the variable name in an array, you can use the element-by-element method with an extra step to retrieve the keys.

eval "keys=(\"\${!$name[@]}\")"
for k in "${keys[@]}"; do eval "b[\$k]=\${$name[\$k]}"; done

(Warning, the code in this post was typed directly in a browser and not tested.)

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
  • You're quite right, but unfortunately, that doesn't solve my problem (my example was too simplistic). I've provided an update to be more clear on my intention. – Eric Smith Sep 06 '11 at 08:52
  • @Eric I think in ksh/bash you need eval at that stage. See my edit. – Gilles 'SO- stop being evil' Sep 06 '11 at 10:21
  • +1, but as per your disclaimer, this actually doesn't quite work (the last bit of code), but only needs a few minor adjustments. Specifically, it should be `\${!$name[@]}` on the first line, so that the first expansion is only of '$name', and the `${!a[@]}` is saved for the eval, and the same thing in the for loop, with `\${$name}`. The other two backslashes before '$k' aren't strictly necessary there either. – krb686 Oct 30 '16 at 23:45
2

${!b[*]} expands to the indices used in array b.

What you would like has to be done in two steps, so eval will help: eval echo \${$b[*]}. (Note the \ which ensures that the first $ will pass the first step, the variable expansion, and will be only expanded in the second step by eval.)

According to Parameter Expansion ! is both used for indirect expansion ({!a}), Names matching prefix (${!a*}) and List of array keys (${!a[*]}). Because List of array keys has the same syntax as your intended indirect expansion+array element expansion, the later is not supported as is.

manatwork
  • 30,549
  • 7
  • 101
  • 91
  • 2
    `${!a}` does expand to the value of the variable whose name is `$a`. This is rather tersely described in the manual, in the parragraph that begins with “If the first character of *parameter* is an exclamation point (`!`), a level of variable indirection is introduced.” – Gilles 'SO- stop being evil' Sep 06 '11 at 07:35
  • Yep - @Gilles is right, but @manatwork, on second reading, I noticed that `${!` is kinda ambigious since if it's an array you're dealing with, the behaviour is different. – Eric Smith Sep 06 '11 at 08:50
  • @Gilles you are right on that sentence, but sadly it not applies as "The exceptions to this are the expansions of ${!prefix*} and ${!name[@]} described below." But my reply is certainly an ambiguous mess, so I will edit it. – manatwork Sep 06 '11 at 09:02
0

To access arrays indirectly, just add [@] to the indirect variable b=a[@].

If this variables are set:

a=(one two three)
printf '<%s> ' "${a[@]}"; echo

Then, this will work:

b="a[@]"
printf '<%s> ' "${!b}"

Or simply:

echo "${!b}"

Such array could be copied as this:

newArr=( "${!b}" )

And then printed with:

declare -p newArr

In one script:

#!/bin/bash
a=(one two three)
echo "original array"
printf '<%s> ' "${a[@]}"; echo

echo "Indirect array with variable b=a[@]"
b="a[@]"
printf '<%s> ' "${!b}"; echo

echo "New array copied in newArr, printed with declare"
newArr=( "${!b}" )
declare -p newArr

Of course, all the above will copy a non-sparse array. One in which all indexes have a value.

sparse arrays

An sparse array is one which may have non-defined elements.
For example a[8]=1234 defines one element, and, in bash, 0 to 7 do not exist.

To copy such sparse array use this method

  1. Print the old array:

    $ oldarr[8]=1234
    $ declare -p oldarr
    declare -a oldarr=([8]="1234")
    
  2. Replace the name of the array and capture the string:

    $ str=$(declare -p oldarr | sed 's/oldarr=/newarr=/')
    
  3. Eval the string so created, the new array has been defined:

    $ eval "$str"
    $ declare -p newarr
    declare -a newarr=([8]="1234")