10

Brief statement of the question:

Is there built-in bash method to count number of elements in bash array, where the name of the array is dynamic (i.e. stored in a variable), without resorting to making a fully copy of the array or using eval?

More information:

Using bash parameter substitution, one can do the following:

  • Determine the length of an array:
    myArr=(A B C); echo ${#myArr[@]}.
  • Indirectly reference a variable by name:
    NAME=myVar; echo ${!NAME}
    (this also applies to array elements):
    NAME=myArr[1]; echo ${!NAME}

But if the name of an array is stored in another variable, how can one determine the number of elements in the array? (One might consider this a combination of the above two parameter substitutions.) For example:

myArr=(A B C D)
NAME=myArr
# Get the number of elements in the array indirectly referenced by NAME.
count=${#$NAME[@]}  # This syntax is invalid. What is the right way?

Below are multiple attempts that all FAIL:

  # Setup for following attempts:
  myArr=(A B C D)
  NAME=myArr
  EXPR1=$NAME[@]          # i.e. EXPR1='myArr[@]'
  EXPR2=#$NAME[@]         # i.e. EXPR2='#myArr[@]'

  # Failed attempts to get the lengh of the array indirectly:
  1.  count=${#$NAME[@]}  # ERROR: bash: ...: bad substitution
  2.  count=${#!EXPR1}    # ERROR: bash: !EXPR}: event not found
  3.  count=${#\!EXPR1}   # ERROR: bash: ...: bad substitution
  4.  count=${!#EXPR1}    # ERROR: bash: ...: bad substitution
  5.  count=${!EXPR2}     # Returns NULL

I've also tried some other variants of the above, but have not yet found anything that works without either: (A) making a copy of the array or (B) by using eval.

Working Methods:

There are a couple ways of solving this that are probably not optimal (but correct me if I'm wrong):

Method 1: Copy the Array

Assign the array to another (statically-named) variable and get the number of elements in it.

EXPR=$NAME[@]
arrCopy=( "${!EXPR}" )
count=${#arrCopy}

Method 2: Use eval

EXPR="count=\${#$NAME[@]}"  # i.e. 'count=${myArr[@]}'
eval $EXPR
# Now count is set to the length of the array

Summary:

Is there any built-in method (i.e. parameter substitution syntax) in bash to determine the length of an array indirectly? If not, what is the most efficient way to do this? I assume it is the eval method above, but are there security or performance issues with eval?

  • 2
    Ugh. Nested variables. I'd rethink whatever approach got me here than use nested variables. What's the actual problem here? – muru Nov 10 '15 at 22:58
  • 1
    It's an interesting question. The only thing I would caution you against is assuming something has or does not have a performance issue. I found during pretty rigorous testing to optimize very large bash scripts that some bash builtins were terrible in terms of performance, in fact, by simply removing one start up test in a large script, that used what you might have expected to be efficient, ie, variable expansion, in fact, that single line slowed the entire execution down by about 10 to 20%. Test methods in big loops with timers, results may surprise you. – Lizardx Nov 10 '15 at 23:15
  • 2
    bash namerefs?. declare -n ref=abc; abc=(A B C D); printf '%s\n' "${ref[@]}" – iruvar Nov 10 '15 at 23:21
  • @muru - This is just semantics, but the term "nested variables" relates more to bash before version 2. Bash v2 added a syntax for "indirect variable references". I'm just asking whether there is a specific syntax to get the length of an indirectly referenced array. I assume the bash authors wouldn't have gone to the effort of implementing variable indirection for scalars and arrays if it were not a requested, useful technique--not simply a hack warranting an immediate "Ugh", although I'm sure that's debatable. – drwatsoncode Nov 11 '15 at 5:08
  • 1
    I did a bit of a benchmark: time bash -c 'a=(1 a +); c=a; for ((i=0;i<100000;i++)); do eval "echo \${#$c[@]}"; done' > /dev/null, and similarly with e=$c[@]; d=("${!e}); echo ${#d[@]} in the loop. The eval took about 90% of the time taken by copying. And I suppose that gap will only increase the larger the array and its elements are. – muru Nov 11 '15 at 6:06
4

you should handle that stuff in the index evals. and you can indirect through your indirection variable's indices if you make it an array.

a=(abc1 def2 ghi3 jkl4 mno5)
r=('a[c=${#a[@]}]' a\[i] a\[@])
for   i in   0 1 2 3 4 5
do    c=
      printf "<%s>\n" "${!r-${!r[i<c?1:2]}}"
      printf "\n\tindex is $i and count is $c\n\n"
done

<abc1>

    index is 0 and count is 5

<def2>

    index is 1 and count is 5

<ghi3>

    index is 2 and count is 5

<jkl4>

    index is 3 and count is 5

<mno5>

    index is 4 and count is 5

<abc1>
<def2>
<ghi3>
<jkl4>
<mno5>

    index is 5 and count is 5

Because bash's indices are 0-based, the total count of array objects will always work out to one more than than the highest set index, and so:

c=
echo "${a[c=${#a[@]}]-this index is unset}" "$c"

this index is unset 5

...the parameter expands out to the default word if any is provided.

If one is not provided:

c=
${!r}
echo "$c"

5

...there's no harm done.

In the loop I track an $index variable and check if it is at least as large as $count. When it is lesser I expand the $reference var to a[i] because it is a valid index, but when it is equal or greater I expand the $ref to the entire $array.

Here it is in a function:

ref_arr(){
    local    index=-1 count=
    local    ref=(   "$1[ count= \${#$1[@]}  ]"
                     "$1[ index ]"    "$1[ @ ]"
    )  &&    printf  "input array '%s' has '%d' members.\n" \
                     "$1"  "${!ref-${count:?invalid array name: "'$1'"}}"
    while    [ "$((index+=1))" -lt "$count"  ]
    do       printf  "$1[$index]  ==  '%s'\n"  "${!ref[1]}"
    done
}
some_array=(some "dumb
            stuff" 12345\'67890 "" \
          '$(kill my computer)')
ref_arr some_array
ref_arr '$(echo won'\''t work)'

input array 'some_array' has '5' members.
some_array[0]  ==  'some'
some_array[1]  ==  'dumb
                stuff'
some_array[2]  ==  '12345'67890'
some_array[3]  ==  ''
some_array[4]  ==  '$(kill my computer)'
bash: count: invalid array name: '$(echo won't work)'
0

bash 4.3 namerefs are a godsend. However, you can do this:

$ myArr=(A B C D)
$ NAME=myArr
$ tmp="${NAME}[@]"
$ copy=( "${!tmp}" )
$ echo "${#copy[@]}"
4
  • Thanks for replying, but your answer is what I already described under the section "Method 1: Copy the Array". The question also specifically stated that the array length should be determined "without resorting to making a fully copy of the array", which is exactly what you did. – drwatsoncode Nov 11 '15 at 4:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.