Policz liczbę elementów w tablicy bash, gdzie nazwa tablicy jest dynamiczna (tzn. Przechowywana w zmiennej)

11

Krótkie wyjaśnienie pytania:

Czy istnieje wbudowana metoda bash zliczająca liczbę elementów w tablicy bash, w której nazwa tablicy jest dynamiczna (tj. Przechowywana w zmiennej), bez uciekania się do wykonania pełnej kopii tablicy lub użycia eval?

Więcej informacji:

Używając podstawiania parametrów bash, można wykonać następujące czynności:

  • Określić długość tablicy:
    myArr=(A B C); echo ${#myArr[@]}.
  • Pośrednio odwołuje się do zmiennej według nazwy:
    NAME=myVar; echo ${!NAME}
    (dotyczy to również elementów tablicy):
    NAME=myArr[1]; echo ${!NAME}

Ale jeśli nazwa tablicy jest przechowywana w innej zmiennej, w jaki sposób można określić liczbę elementów w tablicy? (Można to uznać za kombinację powyższych dwóch podstawień parametrów.) Na przykład:

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?

Poniżej znajduje się wiele prób, które wszystkie 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

Próbowałem także innych wariantów powyższego, ale nie znalazłem jeszcze niczego, co działałoby bez: (A) wykonania kopii tablicy lub (B) przy użyciu eval.

Metody pracy:

Istnieje kilka sposobów rozwiązania tego problemu, które prawdopodobnie nie są optymalne (ale popraw mnie, jeśli się mylę):

Metoda 1: Skopiuj tablicę

Przypisz tablicę do innej (statycznie nazwanej) zmiennej i uzyskaj w niej liczbę elementów.

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

Metoda 2: Użyj eval

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

Podsumowanie:

Czy istnieje jakaś wbudowana metoda (tj. Składnia podstawiania parametrów) w bash do pośredniego określania długości tablicy? Jeśli nie, jaki jest najbardziej efektywny sposób? Zakładam, że jest to evalpowyższa metoda, ale czy występują problemy z bezpieczeństwem lub wydajnością eval?

drwatsoncode
źródło
2
Ugh. Zmienne zagnieżdżone. Zastanowiłbym się nad tym, co mnie tu zaprowadziło, niż użyć zagnieżdżonych zmiennych. Jaki jest tutaj prawdziwy problem?
muru
1
To interesujące pytanie. Jedyną rzeczą, przed którą chciałbym cię ostrzec, jest założenie, że coś ma lub nie ma problemu z wydajnością. Podczas dość rygorystycznych testów, aby zoptymalizować bardzo duże skrypty bash, okazało się, że niektóre wbudowane bash były straszne pod względem wydajności, po prostu usuwając jeden test startowy w dużym skrypcie, który wykorzystywałby to, czego można oczekiwać od wydajności, tj. , zmienna ekspansja, w rzeczywistości ta pojedyncza linia spowolniła całe wykonanie o około 10 do 20%. Metody testowe w dużych pętlach z licznikami czasu, wyniki mogą cię zaskoczyć.
Lizardx,
2
bash namerefs? . declare -n ref=abc; abc=(A B C D); printf '%s\n' "${ref[@]}"
iruvar,
@muru - To tylko semantyka, ale termin „zmienne zagnieżdżone” odnosi się bardziej do bash przed wersją 2. Bash v2 dodał składnię dla „pośrednich odwołań do zmiennych”. Pytam tylko, czy istnieje określona składnia pozwalająca uzyskać długość tablicy z referencjami pośrednimi. Zakładam, że autorzy bash nie podjęliby wysiłku wdrożenia zmiennej pośredniej dla skalarów i tablic, gdyby nie była to pożądana, użyteczna technika - nie tylko hack gwarantujący natychmiastowe „Ugh”, chociaż jestem pewien, że to dyskusyjne .
drwatsoncode
1
Zrobiłem trochę testu porównawczego: time bash -c 'a=(1 a +); c=a; for ((i=0;i<100000;i++)); do eval "echo \${#$c[@]}"; done' > /dev/nulli podobnie z e=$c[@]; d=("${!e}); echo ${#d[@]}pętlą. Kopiowanie zajęło około 90% czasu. I przypuszczam, że odstęp zwiększy tylko większy rozmiar tablicy i jej elementów.
muru

Odpowiedzi:

4

powinieneś sobie z tym poradzić w ewaluacjach indeksu. i możesz pośrednio poprzez indeksy zmiennej pośredniej, jeśli utworzysz z niej tablicę.

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

Ponieważ bashindeksy są oparte na 0, całkowita liczba obiektów tablicowych zawsze będzie działać do jednego więcej niż najwyższy ustawiony indeks, a zatem:

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

this index is unset 5

... parametr jest interpretowany jako domyślny wyraz, jeśli taki istnieje.

Jeśli nie zostanie dostarczony:

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

5

... nic złego się nie stanie.

W pętli $iśledzę zmienną ndex i sprawdzam, czy jest ona co najmniej tak duża jak $count. Kiedy jest mniejszy, rozwijam $referencję var, a[i]ponieważ jest to poprawny indeks, ale kiedy jest równy lub większy, rozszerzam $ref do całej $atablicy.

Oto funkcja:

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
źródło
Daj nam kontynuować tę dyskusję w czacie .
drwatsoncode
0

Nazwy plików bash 4.3 są darem niebios. Możesz to jednak zrobić:

$ myArr=(A B C D)
$ NAME=myArr
$ tmp="${NAME}[@]"
$ copy=( "${!tmp}" )
$ echo "${#copy[@]}"
4
Glenn Jackman
źródło
Dziękuję za odpowiedź, ale twoja odpowiedź jest taka, jak już opisałem w sekcji „Metoda 1: Skopiuj tablicę”. W pytaniu wyraźnie zaznaczono również, że długość tablicy powinna zostać określona „bez uciekania się do wykonania pełnej kopii tablicy”, co właśnie zrobiłeś.
drwatsoncode