Pencetakan array asosiatif BASH

17

Apakah ada cara untuk mencetak seluruh array ([kunci] = nilai) tanpa mengulang semua elemen?

Asumsikan saya telah membuat array dengan beberapa elemen:

declare -A array
array=([a1]=1 [a2]=2 ... [b1]=bbb ... [f500]=abcdef)

Saya dapat mencetak kembali seluruh array dengan

for i in "${!array[@]}"
do
echo "${i}=${array[$i]}"
done

Namun, tampaknya bash sudah tahu cara mendapatkan semua elemen array dalam satu "go" - baik kunci ${!array[@]}dan nilai ${array[@]}.

Apakah ada cara untuk membuat bash mencetak info ini tanpa loop?

Sunting:
typeset -p arraylakukan itu!
Namun saya tidak bisa menghapus awalan dan sufiks dalam satu substitusi:

a="$(typeset -p array)"
b="${a##*(}"
c="${b%% )*}"

Apakah ada cara yang lebih bersih untuk mendapatkan / mencetak hanya bagian kunci = nilai dari output?

Dani_l
sumber

Jawaban:

15

Saya pikir Anda menanyakan dua hal berbeda di sana.

Apakah ada cara untuk membuat bash mencetak info ini tanpa loop?

Ya, tetapi mereka tidak sebagus hanya menggunakan loop.

Apakah ada cara yang lebih bersih untuk mendapatkan / mencetak hanya bagian kunci = nilai dari output?

Ya itu for simpulnya. Ini memiliki kelebihan yang tidak memerlukan program eksternal, mudah, dan membuatnya lebih mudah untuk mengontrol format output yang tepat tanpa kejutan.


Solusi apa pun yang mencoba menangani output dari declare -p(typeset -p ) harus berurusan dengan a) kemungkinan variabel itu sendiri mengandung tanda kurung atau tanda kurung, b) kutipan yang declare -pharus ditambahkan untuk membuat output itu valid input untuk shell.

Misalnya, ekspansi Anda b="${a##*(}"memakan beberapa nilai, jika ada kunci / nilai yang berisi tanda kurung buka. Ini karena Anda menggunakan ##, yang menghilangkan awalan terpanjang . Sama untukc="${b%% )*}" . Meskipun tentu saja Anda dapat mencocokkan pelat tungku yang dicetak dengan declarelebih tepat, Anda masih akan mengalami kesulitan jika Anda tidak ingin semua kutipannya cocok.

Ini tidak terlihat bagus kecuali Anda membutuhkannya.

$ declare -A array=([abc]="'foobar'" [def]='"foo bar"')
$ declare -p array
declare -A array='([def]="\"foo bar\"" [abc]="'\''foobar'\''" )'

Dengan forloop, lebih mudah untuk memilih format output sesuka Anda:

# without quoting
$ for x in "${!array[@]}"; do printf "[%s]=%s\n" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'

# with quoting
$ for x in "${!array[@]}"; do printf "[%q]=%q\n" "$x" "${array[$x]}" ; done
[def]=\"foo\ bar\"
[abc]=\'foobar\'

Dari sana, juga mudah untuk mengubah format output jika tidak (hapus tanda kurung di sekitar kunci, letakkan semua pasangan kunci / nilai pada satu baris ...). Jika Anda perlu mengutip sesuatu selain dari shell itu sendiri, Anda masih perlu melakukannya sendiri, tetapi setidaknya Anda memiliki data mentah untuk dikerjakan. (Jika Anda memiliki baris baru di kunci atau nilai, Anda mungkin perlu mengutip.)

Dengan Bash saat ini (4,4, saya pikir), Anda juga bisa menggunakan printf "[%s]=%s" "${x@Q}" "${array[$x]@Q}"bukan printf "%q=%q". Ini menghasilkan format kutipan yang agak lebih bagus, tetapi tentu saja sedikit lebih banyak pekerjaan yang perlu diingat untuk menulis. (Dan itu mengutip kasus sudut @sebagai kunci array, yang %qtidak mengutip.)

Jika for for tampaknya terlalu lelah untuk ditulis, simpan fungsi di suatu tempat (tanpa mengutip di sini):

printarr() { declare -n __p="$1"; for k in "${!__p[@]}"; do printf "%s=%s\n" "$k" "${__p[$k]}" ; done ;  }  

Dan kemudian gunakan itu:

$ declare -A a=([a]=123 [b]="foo bar" [c]="(blah)")
$ printarr a
a=123
b=foo bar
c=(blah)

Bekerja dengan array yang diindeks juga:

$ b=(abba acdc)
$ printarr b
0=abba
1=acdc
ilkkachu
sumber
Perhatikan bahwa output printf ...%q...varian Anda tidak cocok untuk reinput ke shell jika array memiliki @kunci karena% q tidak mengutipnya dan a=([@]=value)merupakan kesalahan sintaksis dalam bash.
Stéphane Chazelas
@ StéphaneChazelas, rupanya. "${x@Q}"mengutip itu juga, karena mengutip semua string (dan terlihat lebih bagus). menambahkan catatan tentang menggunakan itu.
ilkkachu
Ya, disalin dari mksh. Operator lain dengan bentuk yang berbeda namun tidak dapat digabungkan dengan kebanyakan operator lainnya. Sekali lagi, lihat zshdengan flag ekspansi variabelnya (yang lagi mendahului bash oleh dekade dan dengan mana Anda dapat memilih gaya kutipan: $ {(q) var}, $ {(qq) var} ...) untuk desain yang lebih baik. bash memiliki masalah yang sama dengan mksh karena tidak mengutip string kosong (bukan masalah di sini karena bagaimanapun bash tidak mendukung kunci kosong). Juga, ketika menggunakan mengutip gaya selain kutip tunggal ( ${var@Q}resort untuk $'...'untuk beberapa nilai) penting bahwa kode menjadi reinput di lokasi yang sama.
Stéphane Chazelas
@ StéphaneChazelas, saya pikir maksud Anda nilai yang tidak disetel, bukan string kosong? ( x=; echo "${x@Q}"Apakah memberi '', unset x; echo "${x@Q}"tidak memberi apa-apa.) Bash @Qtampaknya lebih suka $'\n'daripada baris baru yang sebenarnya, yang mungkin sebenarnya baik dalam beberapa situasi (tapi saya tidak bisa mengatakan apa yang disukai orang lain). Tentu saja memiliki pilihan tidak akan ada hal buruk.
ilkkachu
Oh ya maaf, saya belum menyadarinya. Itu perbedaan dari mksh kalau begitu. The $'...'sintaks adalah masalah potensial dalam hal-hal seperti LC_ALL=zh_HK.big5hkscs bash -c 'a=$'\''\n\u3b1'\''; printf "%s\n" "${a@Q}"'yang output $'\n<0xa3><0x5c>'dan 0x5csendirian backslash sehingga Anda akan memiliki masalah jika kutipan yang ditafsirkan di lokasi yang berbeda.
Stéphane Chazelas
9
declare -p array
declare -A array='([a2]="2" [a1]="1" [zz]="Hello World" [b1]="bbb" [f50]="abcd" )'

2 garpu

Mungkin ini:

printf "%s\n" "${!array[@]}"
a2
a1
f50
zz
b1

printf "%s\n" "${array[@]}"
2
1
abcd
Hello World
bbb

printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t
a2                              2
a1                              1
f50                             abcd
zz                              Hello World
b1                              bbb

3 garpu

atau ini:

paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}")
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Tanpa garpu

untuk dibandingkan dengan

for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Perbandingan waktu eksekusi

Karena sintaks terakhir tidak menggunakan fork, mereka bisa lebih cepat:

time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
      5      11      76
real    0m0.005s
user    0m0.000s
sys     0m0.000s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
      5       6      41
real    0m0.008s
user    0m0.000s
sys     0m0.000s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
      5       6      41
real    0m0.002s
user    0m0.000s
sys     0m0.001s

Tetapi penegasan ini tidak tetap benar jika array menjadi besar; jika mengurangi garpu efisien untuk proses kecil, menggunakan alat khusus lebih efisien untuk proses yang lebih besar.

for i in {a..z}{a..z}{a..z};do array[$i]=$RANDOM;done


time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
  17581   35163  292941
real    0m0.150s
user    0m0.124s
sys     0m0.036s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
  17581   17582  169875
real    0m0.140s
user    0m0.000s
sys     0m0.004s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
  17581   17582  169875
real    0m0.312s
user    0m0.268s
sys     0m0.076s

Ucapan

Karena kedua ( bercabang ) solusi menggunakan perataan , tidak satupun dari mereka akan bekerja jika ada variabel yang berisi baris baru . Dalam hal ini, satu-satunya cara adalah forloop.

F. Hauri
sumber
Meskipun terlihat pintar, kedua cara tersebut kurang efisien daripada a for. Sayang sekali, sungguh.
Satō Katsura
@SatoKatsura Saya setuju, tetapi jika lebih lambat, penggunaan sintaks prlebih pendek ... Saya tidak yakin tentang prsintaks tetap lebih lambat, bahkan dengan array besar!
F. Hauri
2
@MiniMax Karena tidak menghasilkan hasil yang benar (elemen yang sama, urutan yang salah). Anda perlu zip array ${!array[@]}dan ${array[@]}pertama untuk itu berfungsi.
Satō Katsura
1
Bahwa potongan terakhir dengan pasteadalah lebih lama daripada forloop dalam pertanyaan tertulis pada satu baris for i in "${!array[@]}"; do echo "$i=${array[$i]}" ; done, tetapi membutuhkan dua subshells dan program eksternal. Bagaimana itu lebih rapi? Solusi dengan prjuga rusak jika ada banyak elemen, karena mencoba untuk memberi peringkat pada output. Anda harus menggunakan sesuatu seperti | pr -2t -l"${#array[@]}"yang mulai sulit diingat dibandingkan dengan loop sederhana, dan sekali lagi, lebih lama dari itu.
ilkkachu
1
In bash, cmd1 | cmd2berarti 2 garpu, bahkan jika cmd1 atau cmd2 atau keduanya dibangun.
Stéphane Chazelas
2

Jika Anda mencari shell dengan dukungan array asosiatif yang lebih baik, coba zsh.

Dalam zsh(di mana array asosiatif ditambahkan pada tahun 1998, dibandingkan dengan 1993 untuk ksh93 dan 2009 untuk bash), $varatau ${(v)var}mengembang ke (non-kosong) nilai hash, ${(k)var}untuk (non-kosong) kunci (dalam urutan yang sama), dan ${(kv)var}untuk kedua kunci dan nilai.

Untuk mempertahankan nilai kosong, seperti untuk array, Anda perlu mengutip dan menggunakan @bendera.

Jadi untuk mencetak kunci dan nilai, itu hanya masalah

printf '%s => %s\n' "${(@kv)var}"

Meskipun untuk memperhitungkan hash yang mungkin kosong, Anda harus melakukan:

(($#var)) &&  printf '%s => %s\n' "${(@kv)var}"

Perhatikan juga bahwa zsh menggunakan sintaks definisi array yang jauh lebih masuk akal dan berguna daripada ksh93(disalin oleh bash):

typeset -A var
var=(k1 v1 k2 v2 '' empty '*' star)

Yang membuatnya lebih mudah untuk menyalin atau menggabungkan array asosiatif:

var2=("${(@kv)var1}")
var3+=("${(@kv)var2}")
var4=("${@kv)var4}" "${(@kv)var5}")

(Anda tidak dapat dengan mudah menyalin hash tanpa loop dengan bash, dan perhatikan bahwa bashsaat ini tidak mendukung kunci kosong atau kunci / nilai dengan NUL byte).

Lihat juga zshberbagai fitur zipping yang biasanya perlu Anda gunakan dengan array asosiatif:

keys=($(<keys.txt)) values=($(<values.txt))
hash=(${keys:^values})
Stéphane Chazelas
sumber
1

Karena typeset melakukan apa yang Anda inginkan mengapa tidak mengedit outputnya saja?

typeset -p array | sed s/^.*\(// | tr -d ")\'\""  | tr "[" "\n" | sed s/]=/' = '/

memberi

a2 = 2  
a1 = 1  
b1 = bbb 

Dimana

array='([a2]="2" [a1]="1" [b1]="bbb" )'

Verbose tetapi cukup mudah untuk melihat bagaimana pemformatan bekerja: cukup jalankan pipeline dengan semakin banyak perintah sed dan tr . Ubah mereka agar sesuai dengan selera cetak yang cantik.

Nadreck
sumber
Pipa semacam itu pasti akan gagal saat beberapa kunci atau nilai array mengandung karakter yang Anda ganti, seperti tanda kurung, tanda kurung, atau tanda kutip. Dan saluran pipa dari seddan trbahkan tidak lebih sederhana dari satu forlingkaran dengan printf.
ilkkachu
Juga, Anda tahu bahwa trmenerjemahkan karakter per karakter, tidak cocok dengan string? tr "]=" " ="perubahan "]" ke spasi dan =ke =, terlepas dari posisi. Jadi Anda mungkin bisa menggabungkan ketiganya trmenjadi satu.
ilkkachu
Sangat benar tentang beberapa karakter non-alfanumerik yang menggabungkan ini. Namun segala sesuatu yang berhubungan dengan mereka mendapatkan urutan besarnya lebih kompleks dan kurang dapat dibaca sehingga kecuali ada alasan yang sangat bagus untuk memilikinya dalam umpan data Anda dan itu dinyatakan dalam pertanyaan saya berasumsi mereka disaring sebelum kami tiba di sini. Harus selalu memiliki peringatan eksplisit Anda tho. Saya menemukan pipa-pipa ini lebih sederhana, misalnya dan tujuan debugging, daripada printf glob yang berfungsi dengan baik atau meledak di wajah Anda. Di sini Anda membuat satu perubahan sederhana per elemen, mengujinya, lalu menambahkan 1 lagi.
Nadreck
Salahku! Membuat _tr_s dan _sed_s saya benar-benar tercampur aduk! Diperbaiki dalam pengeditan terbaru.
Nadreck
1

Satu lagi opsi adalah untuk mendaftar semua variabel dan grep untuk yang Anda inginkan.

set | grep -e '^aa='

Saya menggunakan ini untuk debugging. Saya ragu itu sangat berkinerja karena daftar semua variabel.

Jika Anda sering melakukan ini, Anda bisa membuatnya berfungsi seperti ini:

aap() { set | grep -e "^$1="; }

Sayangnya ketika kami memeriksa kinerja menggunakan waktu:

$ time aap aa aa=([0]="abc") . real 0m0.014s user 0m0.003s sys 0m0.006s

Karena itu, jika Anda melakukan ini sangat sering, Anda ingin versi NO FORKS dari @ F.Hauri karena jauh lebih cepat.

xer0x
sumber