Hitung jumlah elemen dalam bash array, di mana nama array dinamis (mis. Disimpan dalam variabel)

11

Pernyataan singkat pertanyaan:

Apakah ada metode bash bawaan untuk menghitung jumlah elemen dalam array bash, di mana nama array dinamis (mis. Disimpan dalam variabel), tanpa menggunakan salinan array penuh atau menggunakan eval?

Informasi lebih lanjut:

Menggunakan substitusi parameter bash, seseorang dapat melakukan hal berikut:

  • Menentukan panjang array:
    myArr=(A B C); echo ${#myArr[@]}.
  • Referensi tidak langsung variabel dengan nama:
    NAME=myVar; echo ${!NAME}
    (ini juga berlaku untuk elemen array):
    NAME=myArr[1]; echo ${!NAME}

Tetapi jika nama array disimpan dalam variabel lain, bagaimana seseorang dapat menentukan jumlah elemen dalam array? (Orang mungkin menganggap ini kombinasi dari dua pergantian parameter di atas.) Misalnya:

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?

Di bawah ini adalah beberapa upaya yang semua GAGAL:

  # 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

Saya juga sudah mencoba beberapa varian lain di atas, tetapi belum menemukan apa pun yang berfungsi tanpa: (A) membuat salinan array atau (B) dengan menggunakan eval.

Metode kerja:

Ada beberapa cara untuk menyelesaikan ini yang mungkin tidak optimal (tapi perbaiki saya jika saya salah):

Metode 1: Salin Array

Tetapkan array ke variabel lain (bernama statis) dan dapatkan jumlah elemen di dalamnya.

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

Metode 2: Gunakan eval

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

Ringkasan:

Apakah ada metode built-in (sintaks substitusi parameter) di bash untuk menentukan panjang array secara tidak langsung? Jika tidak, apa cara paling efisien untuk melakukan ini? Saya menganggap itu adalah evalmetode di atas, tetapi apakah ada masalah keamanan atau kinerja eval?

drwatsoncode
sumber
2
Ugh. Variabel bersarang. Saya akan memikirkan kembali pendekatan apa pun yang membawa saya ke sini daripada menggunakan variabel bersarang. Apa masalah sebenarnya di sini?
muru
1
Itu pertanyaan yang menarik. Satu-satunya hal yang saya akan memperingatkan Anda terhadap adalah dengan mengasumsikan sesuatu memiliki atau tidak memiliki masalah kinerja. Saya menemukan selama pengujian yang cukup ketat untuk mengoptimalkan skrip bash yang sangat besar sehingga beberapa bash builtin sangat buruk dalam hal kinerja, pada kenyataannya, dengan hanya menghapus satu tes awal dalam skrip besar, yang menggunakan apa yang Anda harapkan akan efisien, yaitu , ekspansi variabel, pada kenyataannya, bahwa satu baris memperlambat seluruh eksekusi turun sekitar 10 hingga 20%. Metode pengujian dalam loop besar dengan timer, hasilnya mungkin mengejutkan Anda.
Lizardx
2
bash namerefs? . declare -n ref=abc; abc=(A B C D); printf '%s\n' "${ref[@]}"
iruvar
@muru - Ini hanya semantik, tetapi istilah "variabel bertingkat" lebih terkait dengan bash sebelum versi 2. Bash v2 menambahkan sintaksis untuk "referensi variabel tidak langsung". Saya hanya bertanya apakah ada sintaks tertentu untuk mendapatkan panjang array yang direferensikan secara tidak langsung. Saya menganggap penulis bash tidak akan pergi ke upaya menerapkan tipuan variabel untuk skalar dan array jika itu bukan teknik yang diminta dan bermanfaat - bukan hanya hacking yang menjamin "Ugh" langsung, meskipun saya yakin itu bisa diperdebatkan .
drwatsoncode
1
Saya melakukan sedikit tolok ukur time bash -c 'a=(1 a +); c=a; for ((i=0;i<100000;i++)); do eval "echo \${#$c[@]}"; done' > /dev/null:, dan juga dengan e=$c[@]; d=("${!e}); echo ${#d[@]}dalam lingkaran. Eval mengambil sekitar 90% dari waktu yang diambil dengan menyalin. Dan saya kira celah itu hanya akan meningkatkan array yang lebih besar dan elemen-elemennya.
muru

Jawaban:

4

Anda harus menangani hal-hal di indeks evals. dan Anda bisa secara tidak langsung melalui indeks variabel tipuan Anda jika Anda membuatnya menjadi 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

Karena bashindeks berbasis 0, jumlah total objek array akan selalu bekerja lebih dari satu indeks set tertinggi, jadi:

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

this index is unset 5

... parameter diperluas ke kata default jika ada yang disediakan.

Jika tidak ada:

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

5

... tidak ada salahnya dilakukan.

Dalam loop saya melacak $ivariabel ndex dan memeriksa apakah itu setidaknya sama besar dengan $count. Ketika lebih rendah saya memperluas $reference var ke a[i]karena itu adalah indeks yang valid, tetapi ketika itu sama atau lebih besar saya memperluas $ref ke seluruh $array.

Ini dia dalam fungsi:

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
sumber
Mari kita lanjutkan diskusi ini dalam obrolan .
drwatsoncode
0

bash 4.3 nameref adalah anugerah. Namun, Anda dapat melakukan ini:

$ myArr=(A B C D)
$ NAME=myArr
$ tmp="${NAME}[@]"
$ copy=( "${!tmp}" )
$ echo "${#copy[@]}"
4
glenn jackman
sumber
Terima kasih telah membalas, tetapi jawaban Anda adalah apa yang sudah saya jelaskan di bagian "Metode 1: Salin Array". Pertanyaannya juga secara spesifik menyatakan bahwa panjang array harus ditentukan "tanpa menggunakan untuk membuat salinan penuh array", yang persis seperti yang Anda lakukan.
drwatsoncode