Подсчитать количество элементов в массиве bash, где имя массива является динамическим (т.е. хранится в переменной)

11

Краткое изложение вопроса:

Существует ли встроенный метод bash для подсчета количества элементов в массиве bash, где имя массива является динамическим (т.е. хранится в переменной), не прибегая к созданию полной копии массива или его использованию eval?

Дополнительная информация:

Используя подстановку параметров bash, можно сделать следующее:

  • Определить длину массива:
    myArr=(A B C); echo ${#myArr[@]}.
  • Косвенная ссылка на переменную по имени:
    NAME=myVar; echo ${!NAME}
    (это также относится к элементам массива):
    NAME=myArr[1]; echo ${!NAME}

Но если имя массива хранится в другой переменной, как можно определить количество элементов в массиве? (Можно рассматривать это как комбинацию двух приведенных выше подстановок параметров.) Например:

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?

Ниже приведены несколько попыток, которые все НЕУДАЧИ:

  # 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

Я также попробовал некоторые другие варианты вышеупомянутого, но еще не нашел ничего, что работает без: (A) создания копии массива или (B) с помощью eval.

Методы работы:

Есть несколько способов решения этой проблемы, которые, вероятно, не оптимальны (но исправьте меня, если я ошибаюсь):

Способ 1: скопировать массив

Присвойте массив другой переменной (со статическим именем) и получите количество элементов в нем.

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

Способ 2: использование eval

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

Резюме:

Есть ли в bash встроенный метод (т. Е. Синтаксис подстановки параметров) для косвенного определения длины массива? Если нет, то какой самый эффективный способ сделать это? Я предполагаю, что это evalметод выше, но есть проблемы с безопасностью или производительностью eval?

drwatsoncode
источник
2
Тьфу. Вложенные переменные. Я бы переосмыслил тот подход, который меня привлек, чем использовать вложенные переменные. В чем здесь проблема?
Муру
1
Это интересный вопрос. Единственное, против чего я бы вас предостерегал, это предположить, что что-то имеет или не имеет проблемы с производительностью. Во время довольно строгого тестирования, чтобы оптимизировать очень большие сценарии bash, я обнаружил, что некоторые встроенные средства bash были ужасны с точки зрения производительности, на самом деле, просто удалив один тест запуска в большом сценарии, который использовал то, что вы, возможно, ожидали быть эффективным, т.е. расширение переменной, фактически, в одну строку замедлило все выполнение примерно на 10-20%. Методы испытаний в больших циклах с таймерами, результаты могут вас удивить.
Lizardx
2
bash namerefs? , declare -n ref=abc; abc=(A B C D); printf '%s\n' "${ref[@]}"
iruvar
@muru - это всего лишь семантика, но термин «вложенные переменные» больше относится к bash до версии 2. Bash v2 добавил синтаксис для «косвенных ссылок на переменные». Я просто спрашиваю, существует ли определенный синтаксис для получения длины массива с косвенной ссылкой. Я предполагаю , что авторы Баша не пошли бы к усилию реализации переменной косвенности для скаляров и массивов , если бы оно не было предложено, полезным методом - не просто хак гарантирования немедленно «тьфу», хотя я уверен , что спорно ,
drwatsoncode
1
Я сделал немного теста: time bash -c 'a=(1 a +); c=a; for ((i=0;i<100000;i++)); do eval "echo \${#$c[@]}"; done' > /dev/nullи аналогично с e=$c[@]; d=("${!e}); echo ${#d[@]}циклом. Эвал занимал около 90% времени, затрачиваемого на копирование. И я полагаю, что разрыв будет только увеличиваться, чем больше массив и его элементы.
Муру

Ответы:

4

вы должны справиться с этим в индексе evals. и вы можете выполнять косвенные действия через индексы вашей косвенной переменной, если вы сделаете ее массивом.

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

Поскольку bashиндексы основаны на 0, общее количество объектов массива всегда будет на единицу больше, чем индекс с наибольшим набором, и поэтому:

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

this index is unset 5

... параметр расширяется до слова по умолчанию, если оно есть.

Если один не предоставлен:

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

5

... нет никакого вреда

В цикле я отслеживаю $iпеременную ndex и проверяю, не меньше ли она размера $count. Когда оно меньше, я расширяю $rпеременную var до, a[i]потому что это допустимый индекс, но когда он равен или больше, я расширяю $ref на весь $aмассив.

Вот это в функции:

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)'
mikeserv
источник
0

bash 4.3 nameref являются находкой. Тем не менее, вы можете сделать это:

$ myArr=(A B C D)
$ NAME=myArr
$ tmp="${NAME}[@]"
$ copy=( "${!tmp}" )
$ echo "${#copy[@]}"
4
Гленн Джекман
источник
Спасибо за ответ, но ваш ответ - это то, что я уже описал в разделе «Метод 1: Копирование массива». В вопросе также конкретно указывалось, что длину массива следует определять «не прибегая к созданию полной копии массива», что вы и сделали.
drwatsoncode