39

How can I make this code work?

#!/bin/bash
ARRAYNAME='FRUITS'
FRUITS=( APPLE BANANA ORANGE )
for FRUIT in ${!ARRAYNAME[@]}
do
    echo ${FRUIT}
done

This code:

echo ${!ARRAYNAME[0]}

Prints APPLE. I'm tryng to do something similar but with "[@]" to iterate over the array.

Thanks in advance,

Neuquino
  • 11,580
  • 20
  • 62
  • 76

7 Answers7

42

${!ARRAYNAME[@]} means "the indices of ARRAYNAME". As stated in the bash man page since ARRAYNAME is set, but as a string, not an array, it returns 0.

Here's a solution using eval.

#!/usr/bin/env bash

ARRAYNAME='FRUITS'
FRUITS=( APPLE BANANA ORANGE )

eval array=\( \${${ARRAYNAME}[@]} \)

for fruit in "${array[@]}"; do
  echo ${fruit}
done

What you were originally trying to do was create an Indirect Reference. These were introduced in bash version 2 and were meant to largely replace the need for eval when trying to achieve reflection-like behavior in the shell.

What you have to do when using indirect references with arrays is include the [@] in your guess at the variable name:

#!/usr/bin/env bash

ARRAYNAME='FRUITS'
FRUITS=( APPLE BANANA ORANGE )

array="${ARRAYNAME}[@]"
for fruit in "${!array}"; do
  echo $fruit
done

All that said, it's one thing to use Indirect References in this trivial example, but, as indicated in the link provided by Dennis Williamson, you should be hesitant to use them in real-world scripts. They are all but guaranteed to make your code more confusing than necessary. Usually you can get the functionality you need with an Associative Array.

Tim Pote
  • 27,191
  • 6
  • 63
  • 65
  • Tim: your 2nd part of the answer is not useful, because ARRAYNAME is an input parameter of my script. But your first solution let me find a more elegant solution: ARRAY=${!ARRAYNAME}; for FRUIT in ${ARRAY[@]}... Thanks – Neuquino Jun 24 '12 at 20:38
  • 1
    @Neuquino See my edited answer. All you need to do is concatenate `[@]` on the end of your input variable. – Tim Pote Jun 24 '12 at 20:42
  • 2
    @Neuquino If ARRAYNAME is an input parameter to your script then you're doing something terribly wrong. There are zero reasons to mix user input with variable names. In this case (combined with indirect expansion), it allows arbitrary code injection. The ONLY valid reason for these techniques are for use in functions, never in the global scope and never combined with user input. – ormaaj Jul 04 '12 at 11:30
  • @ormaaj ARRAYNAME is not an input parameter. I have a config file (config.conf) with KEYS=( FRUITS VEGETABLES MEAT ), and then multiple lines with FRUITS=( APPLE BANANA ORANGE )\n VEGETABLES=( BEANS LETTUCE )\n MEAT=( CHICKEN ROAST_BEAF ). Then in my script I do source config.conf. – Neuquino Jul 19 '12 at 15:37
  • @Neuquino Should be ok if you can trust the input (file in this case). – ormaaj Jul 19 '12 at 17:21
  • 1
    Neither "solution" works with elements containing whitespace. –  Apr 30 '14 at 01:40
  • I just want to throw in here that this solution worked really well for me but took some understanding. The way I interpret this is that array"${ARRAYNAME}[@]" is not special syntax, it's just string concatenation (as Tim Pote says) When you do run use "${!array}" later, it's like doing "${fruits[@]}" since that's the string you end up with. You can just as easily produce other reference strings in this way – Ben Thayer Sep 01 '21 at 04:58
20

Here's a way to do it without eval.

See Bash trick #2 described here: http://mywiki.wooledge.org/BashFAQ/006

Seems to work in bash 3 and up.

#!/bin/bash

ARRAYNAME='FRUITS'
tmp=$ARRAYNAME[@]
FRUITS=( APPLE BANANA ORANGE "STAR FRUIT" )
for FRUIT in "${!tmp}"
do
    echo "${FRUIT}"
done

Here's a more realistic example showing how to pass an array by reference to a function:

pretty_print_array () {
  local arrayname=$1
  local tmp=$arrayname[@]
  local array=( "${!tmp}" )
  local FS=', ' # Field seperator
  local var
  # Print each element enclosed in quotes and separated by $FS
  printf -v var "\"%s\"$FS" "${array[@]}"
  # Chop trailing $FS
  var=${var%$FS}
  echo "$arrayname=($var)"
}
FRUITS=( APPLE BANANA ORANGE "STAR FRUIT" )
pretty_print_array FRUITS
# prints FRUITS=("APPLE", "BANANA", "ORANGE", "STAR FRUIT")
Robin A. Meade
  • 1,946
  • 18
  • 17
9

eval executes code containing array elements, even if they contain, for example, command substitutions. It also changes the array elements by interpreting bash metacharacters in them.

A tool that avoids these problems is the declare reference, see man bash under declare:

-n Give each name the nameref attribute, making it a name reference to another variable. That other variable is defined by the value of name. All references, assignments, and attribute modifications to name, except those using or changing the -n attribute itself, are performed on the variable referenced by name's value. The nameref attribute cannot be applied to array variables.

#!/bin/bash
declare -n ARRAYNAME='FRUITS'
FRUITS=(APPLE BANANA ORANGE "BITTER LEMON")
for FRUIT in "${ARRAYNAME[@]}"
do
    echo "${FRUIT}"
done
Hans Klünder
  • 2,176
  • 12
  • 8
  • 1
    Thank you. At this time, and using Bash 4.3+, this seems to be the best and simplest answer. It should be voted to the top. Note that the sentence in the doc, "The nameref attribute cannot be applied to array variables," is confusing and perhaps misleading, as it would seem to imply that it cannot be used to access array variables. However, it does, in fact, work. –  Nov 18 '18 at 01:47
  • Thank you! None of the other tricks worked for me on bash-5.1.16 and this one is super straight forward and worked perfectly for me. – Shadowcat Aug 31 '22 at 22:20
  • @user712624 the wording is a little unclear: it means you cannot have an array of namerefs, you can have a nameref to an array as you note (correct up to bash-5.2). – mr.spuratic Nov 21 '22 at 17:28
6

This answer comes very late, but I guess there is much cleaner approach than those presented up to this moment (with all the respects to their authors).

It is about using -n option of declare/local bash built-in. (For more info type help declare in your bash).

So here we go:

ARRAYNAME='FRUITS';
FRUITS=(APPLE BANANA ORANGE);

# This is the critical addition. With help of option `-n` we declare
# variable `fruits` as indirect reference to another variable. Anytime
# we refer to ${fruits} we would actually refer to a variable whose
# name is stored in `fruits` variable:
declare -n fruits="${ARRAYNAME}";

# Here we use ${fruits} as ordinary variable, but in reality it refers
# to `FRUITS` variable:
for FRUIT in ${fruits[@]}; do
    echo "${FRUIT}";
done;

And the result is:

APPLE
BANANA
ORANGE
Cromax
  • 1,822
  • 1
  • 23
  • 35
1

Despite the simple OP question, these answers won't scale for the most common, real use-cases, i.e., array elements containing whitespace or wildcards that should not yet be expanded to filenames.

FRUITS=( APPLE BANANA ORANGE 'not broken' '*.h')
ARRAYNAME=FRUITS
eval ARRAY=\(\${$ARRAYNAME[@]}\)

$ echo "${ARRAY[4]}"
broken
$ echo "${ARRAY[5]}"
config.h
$

This works:

FRUITS=( APPLE BANANA ORANGE 'not broken' '*.h')
ARRAYNAME=FRUITS
eval ARRAY="(\"\${$ARRAYNAME[@]}\")"

$ echo "${ARRAY[3]}"
not broken
$ echo "${ARRAY[4]}"
*.h
$

Just as you should get in the habit of using "$@" not $@, always quote inside ( ) for array expansions, unless you want filename expansion or know there's no possibility of array elements containing whitespace.

Do this: X=("${Y[@]}")

Not this: X=(${Y[@]})

1

I think the proper way and the best answer to his question has to do with an actual indirect reference, makes the least modifications to the asker's original code, and you can even do this with an associative array.

Minimally modified code for the OP

declare -n ARRAYNAME='FRUITS'
declare -a FRUITS=( APPLE BANANA ORANGE )
for FRUIT in ${!ARRAYNAME[@]}
do
    echo "${ARRAYNAME[${FRUIT}]}"
done

Output

APPLE
BANANA
ORANGE

Usage in an associative array

declare -A associative_array
declare -n array_name=associative_array
associative_array[kittens]='cat'
associative_array[puppies]='dog'
associative_array[kitties]='cat'
associative_array[doggies]='dog'
for name in ${!array_name[@]} ; do 
echo $name has the value of "${associative_array[$name]}"
done

Output:

puppies has the value of dog
kittens has the value of cat
kitties has the value of cat
doggies has the value of dog

Rather than having to read the entire man page for bash, simply using the builtin help (

$ help help
help: help [-dms] [pattern ...]
    Display information about builtin commands.
    
    Displays brief summaries of builtin commands.  If PATTERN is
    specified, gives detailed help on all commands matching PATTERN,
    otherwise the list of help topics is printed.
    
    Options:
      -d    output short description for each topic
      -m    display usage in pseudo-manpage format
      -s    output only a short usage synopsis for each topic matching
            PATTERN
    
    Arguments:
      PATTERN   Pattern specifying a help topic
    
    Exit Status:
    Returns success unless PATTERN is not found or an invalid option is given.

)

Declare's usage:

declare: declare [-aAfFgilnrtux] [-p] [name[=value] ...]
    Set variable values and attributes.
    
    Declare variables and give them attributes.  If no NAMEs are given,
    display the attributes and values of all variables.
    
    Options:
      -f    restrict action or display to function names and definitions
      -F    restrict display to function names only (plus line number and
            source file when debugging)
      -g    create global variables when used in a shell function; otherwise
            ignored
      -p    display the attributes and value of each NAME
    
    Options which set attributes:
      -a    to make NAMEs indexed arrays (if supported)
      -A    to make NAMEs associative arrays (if supported)
      -i    to make NAMEs have the `integer' attribute
      -l    to convert the value of each NAME to lower case on assignment
      -n    make NAME a reference to the variable named by its value
      -r    to make NAMEs readonly
      -t    to make NAMEs have the `trace' attribute
      -u    to convert the value of each NAME to upper case on assignment
      -x    to make NAMEs export
    
    Using `+' instead of `-' turns off the given attribute.
    
    Variables with the integer attribute have arithmetic evaluation (see
    the `let' command) performed when the variable is assigned a value.
    
    When used in a function, `declare' makes NAMEs local, as with the `local'
    command.  The `-g' option suppresses this behavior.
    
    Exit Status:
    Returns success unless an invalid option is supplied or a variable
    assignment error occurs.
linuxgeek
  • 57
  • 8
  • declare -n was introduced in a much later version of bash, this requires bash-4.4+ so I apologize for backwards incompatability. – linuxgeek Mar 09 '23 at 05:03
-1

I just wanted to add another useful use-case. I was searching the web for a solution to a different, but related problem

ARRAYNAME=( FRUITS VEG )
FRUITS=( APPLE BANANA ORANGE )
VEG=( CARROT CELERY CUCUMBER )
for I in "${ARRAYNAME[@]}"
do
    array="${I}[@]"
    for fruit in "${!array}"; do
        echo $fruit
    done
done
Felipe Alvarez
  • 3,720
  • 2
  • 33
  • 42