Bagaimana saya bisa mendapatkan nilai unik dari array di Bash?


93

Saya punya pertanyaan yang hampir sama seperti di sini .

Saya memiliki sebuah array yang berisi aa ab aa ac aa ad, dll. Sekarang saya ingin memilih semua elemen unik dari array ini. Pikir, ini akan sederhana dengan sort | uniqatau dengan sort -useperti yang mereka sebutkan di pertanyaan lain itu, tetapi tidak ada yang berubah dalam larik ... Kodenya adalah:

echo `echo "${ids[@]}" | sort | uniq`

Apa yang saya lakukan salah?

Jawaban:


131

Agak hacky, tetapi ini harus dilakukan:

echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '

Untuk menyimpan hasil unik yang diurutkan kembali ke dalam array, lakukan penugasan Array :

sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))

Jika shell Anda mendukung herestrings ( bashharus), Anda dapat menghemat echoproses dengan mengubahnya menjadi:

tr ' ' '\n' <<< "${ids[@]}" | sort -u | tr '\n' ' '

Memasukkan:

ids=(aa ab aa ac aa ad)

Keluaran:

aa ab ac ad

Penjelasan:

  • "${ids[@]}"- Sintaks untuk bekerja dengan array shell, baik digunakan sebagai bagian dari echoatau herestring. Bagian @berarti "semua elemen dalam array"
  • tr ' ' '\n'- Ubah semua spasi menjadi baris baru. Karena array Anda dilihat oleh shell sebagai elemen pada satu baris, dipisahkan oleh spasi; dan karena sort mengharapkan input berada di baris terpisah.
  • sort -u - urutkan dan pertahankan hanya elemen unik
  • tr '\n' ' ' - ubah baris baru yang kami tambahkan sebelumnya kembali ke spasi.
  • $(...)- Pergantian Perintah
  • Selain: tr ' ' '\n' <<< "${ids[@]}"adalah cara yang lebih efisien untuk melakukan:echo "${ids[@]}" | tr ' ' '\n'

37
+1. Sedikit lebih rapi: simpan elemen uniq dalam array baru:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
glenn jackman

@glennjman oh rapi! Saya bahkan tidak menyadari Anda dapat menggunakan printfcara itu (berikan lebih banyak argumen daripada format string)
sampson-chen

4
1 Saya tidak yakin apakah ini adalah kasus yang terisolasi, tetapi menempatkan barang-barang unik kembali ke array yang dibutuhkan kurung tambahan seperti: sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')). Tanpa tanda kurung tambahan, ini diberikan sebagai string.
whla

3
Jika Anda tidak ingin mengubah urutan unsur-unsur, penggunaan ... | uniq | ...bukan ... | sort -u | ....
Jesse Chisholm

2
@ Jesse, uniqhanya menghapus duplikat yang berurutan . Dalam contoh di jawaban ini, sorted_unique_idsakan berakhir identik dengan aslinya ids. Untuk menjaga ketertiban, cobalah ... | awk '!seen[$0]++'. Lihat juga stackoverflow.com/questions/1444406/… .
Rob Kennedy

29

Jika Anda menjalankan Bash versi 4 atau lebih tinggi (yang seharusnya terjadi pada versi Linux modern), Anda bisa mendapatkan nilai array unik di bash dengan membuat array asosiatif baru yang berisi setiap nilai dari array asli. Sesuatu seperti ini:

$ a=(aa ac aa ad "ac ad")
$ declare -A b
$ for i in "${a[@]}"; do b["$i"]=1; done
$ printf '%s\n' "${!b[@]}"
ac ad
ac
aa
ad

Ini berfungsi karena dalam larik apa pun (asosiatif atau tradisional, dalam bahasa apa pun), setiap kunci hanya dapat muncul sekali. Ketika forperulangan tiba di nilai kedua dari aadalam a[2], itu menimpa b[aa]yang awalnya ditetapkan untuk a[0].

Melakukan hal-hal di bash asli bisa lebih cepat daripada menggunakan pipa dan alat eksternal seperti sortdan uniq, meskipun untuk kumpulan data yang lebih besar Anda kemungkinan akan melihat kinerja yang lebih baik jika Anda menggunakan bahasa yang lebih kuat seperti awk, python, dll.

Jika Anda merasa yakin, Anda dapat menghindari forpengulangan dengan menggunakan printfkemampuan untuk mendaur ulang formatnya untuk beberapa argumen, meskipun ini tampaknya membutuhkan eval. (Berhenti membaca sekarang jika Anda setuju dengan itu.)

$ eval b=( $(printf ' ["%s"]=1' "${a[@]}") )
$ declare -p b
declare -A b=(["ac ad"]="1" [ac]="1" [aa]="1" [ad]="1" )

Alasan yang dibutuhkan oleh solusi ini evaladalah karena nilai array ditentukan sebelum pemisahan kata. Itu berarti bahwa output dari substitusi perintah dianggap sebagai kata tunggal daripada sekumpulan pasangan kunci = nilai.

Meskipun ini menggunakan subkulit, ini hanya menggunakan bash bawaan untuk memproses nilai array. Pastikan untuk mengevaluasi penggunaan Anda evaldengan mata kritis. Jika Anda tidak 100% yakin bahwa chepner atau glenn jackman atau greycat tidak akan menemukan kesalahan pada kode Anda, gunakan loop for sebagai gantinya.


menghasilkan kesalahan: tingkat rekursi ekspresi terlampaui
Benubird

1
@ Benubird - dapatkah Anda menyisipkan konten terminal Anda? Ini berfungsi dengan sempurna untuk saya, jadi tebakan terbaik saya adalah Anda memiliki (1) kesalahan ketik, (2) versi bash yang lebih lama (array asosiatif ditambahkan ke v4), atau (3) arus latar belakang kosmik yang sangat besar radiasi yang disebabkan oleh lubang hitam kuantum di ruang bawah tanah tetangga Anda, menghasilkan gangguan dengan sinyal di dalam komputer Anda.
ghoti

1
tidak bisa, tidak menyimpan yang tidak berfungsi. tetapi, saya mencoba menjalankan milik Anda sekarang dan berhasil, jadi mungkin hal radiasi kosmik.
Benubird

menebak bahwa jawaban ini menggunakan bash v4 (array asosiatif) dan jika seseorang mencoba di bash v3 itu tidak akan berfungsi (mungkin bukan yang dilihat @Benubird). Bash v3 masih default di banyak lingkungan
nhed

1
@nhed, poin sudah diambil. Saya melihat bahwa Yosemite Macbook saya yang terbaru memiliki versi yang sama pada dasarnya, meskipun saya telah menginstal v4 dari macports. Pertanyaan ini diberi tag "linux", tetapi saya telah memperbarui jawaban saya untuk menunjukkan persyaratan tersebut.
ghoti

18

Saya menyadari ini sudah terjawab, tetapi muncul cukup tinggi dalam hasil penelusuran, dan mungkin membantu seseorang.

printf "%s\n" "${IDS[@]}" | sort -u

Contoh:

~> IDS=( "aa" "ab" "aa" "ac" "aa" "ad" )
~> echo  "${IDS[@]}"
aa ab aa ac aa ad
~>
~> printf "%s\n" "${IDS[@]}" | sort -u
aa
ab
ac
ad
~> UNIQ_IDS=($(printf "%s\n" "${IDS[@]}" | sort -u))
~> echo "${UNIQ_IDS[@]}"
aa ab ac ad
~>

1
untuk memperbaiki array saya terpaksa melakukan ini :, ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`)jadi saya menambahkan IFS=$'\n'disarankan oleh @gniourf_gniourf
Aquarius Power

Saya juga harus membuat cadangan dan, setelah perintah, mengembalikan nilai IFS! atau mengacaukan hal lain ..
Aquarius Power

@Jetse Ini harus menjadi jawaban yang diterima karena hanya menggunakan dua perintah, tanpa loop, tidak ada eval dan merupakan versi yang paling ringkas.
mgutt

1
@AquariusPower Hati-hati, pada dasarnya Anda melakukan:, IFS=$'\n'; ids2=(...)karena penugasan sementara sebelum penugasan variabel tidak dimungkinkan. Sebaliknya menggunakan konstruksi ini: IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)".
Yeti

13

Jika elemen array Anda memiliki spasi putih atau karakter khusus shell lainnya (dan dapatkah Anda yakin tidak?) Maka untuk menangkap yang pertama-tama (dan Anda harus selalu melakukan ini) ekspresikan array Anda dalam tanda kutip ganda! mis "${a[@]}". Bash secara harfiah akan menafsirkan ini sebagai "setiap elemen array dalam argumen terpisah ". Dalam bash ini selalu berhasil, selalu.

Kemudian, untuk mendapatkan array yang diurutkan (dan unik), kita harus mengubahnya menjadi format yang dipahami oleh sort dan dapat mengubahnya kembali menjadi elemen array bash. Ini adalah yang terbaik yang saya hasilkan:

eval a=($(printf "%q\n" "${a[@]}" | sort -u))

Sayangnya, ini gagal dalam kasus khusus dari array kosong, mengubah array kosong menjadi array 1 elemen kosong (karena printf memiliki 0 argumen tetapi masih mencetak seolah-olah memiliki satu argumen kosong - lihat penjelasannya). Jadi, Anda harus menangkapnya di jika atau sesuatu.

Penjelasan: Format% q untuk printf "shell lolos" dari argumen tercetak, seperti bash dapat dipulihkan dalam sesuatu seperti eval! Karena setiap elemen dicetak shell yang lolos pada barisnya sendiri, satu-satunya pemisah antar elemen adalah baris baru, dan penetapan larik mengambil setiap baris sebagai elemen, mengurai nilai yang lolos ke dalam teks literal.

misalnya

> a=("foo bar" baz)
> printf "%q\n" "${a[@]}"
'foo bar'
baz
> printf "%q\n"
''

Eval diperlukan untuk menghapus setiap nilai yang masuk kembali ke dalam array.


Ini adalah satu-satunya kode yang berfungsi untuk saya karena array string saya memiliki spasi. % Q adalah triknya. Terima kasih :)
Somaiah Kumbera

Dan jika Anda tidak ingin mengubah urutan unsur-unsur, menggunakan uniqbukan sort -u.
Jesse Chisholm

Perhatikan bahwa uniqtidak berfungsi dengan baik pada daftar yang tidak diurutkan, jadi harus selalu digunakan bersama sort.
Jean Paul

uniq pada daftar yang tidak diurutkan akan menghapus duplikat yang berurutan . Ini tidak akan menghapus elemen daftar identik yang dipisahkan oleh sesuatu yang lain di antara. uniq mungkin cukup berguna tergantung pada data yang diharapkan dan keinginan untuk menjaga ketertiban asli.
vontrapp

10

'sort' dapat digunakan untuk mengurutkan keluaran for-loop:

for i in ${ids[@]}; do echo $i; done | sort

dan hilangkan duplikat dengan "-u":

for i in ${ids[@]}; do echo $i; done | sort -u

Akhirnya Anda bisa menimpa array Anda dengan elemen unik:

ids=( `for i in ${ids[@]}; do echo $i; done | sort -u` )

Dan jika Anda tidak ingin mengubah urutan dari yang tersisa, Anda tidak perlu:ids=( `for i in ${ids[@]}; do echo $i; done | uniq` )
Jesse Chisholm

3

yang ini juga akan menjaga ketertiban:

echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'

dan untuk mengubah larik asli dengan nilai unik:

ARRAY=($(echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'))

Jangan gunakan uniq. Perlu diurutkan, di mana awk tidak, dan tujuan dari jawaban ini adalah untuk mempertahankan pengurutan saat input tidak diurutkan.
bukzor

2

Untuk membuat array baru yang terdiri dari nilai-nilai unik, pastikan array Anda tidak kosong, lalu lakukan salah satu hal berikut:

Hapus entri duplikat (dengan penyortiran)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | sort -u)

Hapus entri duplikat (tanpa penyortiran)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | awk '!x[$0]++')

Peringatan: Jangan mencoba melakukan sesuatu seperti NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) ). Ini akan merusak ruang.


Hapus entri duplikat (tanpa penyortiran) sama seperti (dengan penyortiran) kecuali ubah sort -umenjadi uniq.
Jesse Chisholm

@JesseChisholm uniqhanya menggabungkan baris duplikat yang berdekatan, jadi tidak sama dengan awk '!x[$0]++'.
Enam

@JesseChisholm Tolong hapus komentar yang menyesatkan.
bukzor

2

cat number.txt

1 2 3 4 4 3 2 5 6

cetak baris ke dalam kolom: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'

1
2
3
4
4
3
2
5
6

temukan catatan duplikat: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk 'x[$0]++'

4
3
2

Ganti rekaman duplikat: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk '!x[$0]++'

1
2
3
4
5
6

Temukan hanya catatan Uniq: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i|"sort|uniq -u"}

1
5
6

1

Tanpa kehilangan pemesanan asli:

uniques=($(tr ' ' '\n' <<<"${original[@]}" | awk '!u[$0]++' | tr '\n' ' '))

1

Jika Anda menginginkan solusi yang hanya menggunakan internal bash, Anda dapat mengatur nilai sebagai kunci dalam array asosiatif, lalu mengekstrak kunci:

declare -A uniqs
list=(foo bar bar "bar none")
for f in "${list[@]}"; do 
  uniqs["${f}"]=""
done

for thing in "${!uniqs[@]}"; do
  echo "${thing}"
done

Ini akan menghasilkan

bar
foo
bar none

Saya baru saja memperhatikan ini pada dasarnya sama dengan jawaban @ghotis di atas, kecuali solusinya tidak memperhitungkan item daftar dengan spasi.
rln

Poin yang bagus. Saya telah menambahkan kutipan ke solusi saya sehingga sekarang menangani spasi. Saya awalnya menulisnya hanya untuk menangani data sampel dalam pertanyaan, tetapi selalu bagus untuk meliput kemungkinan seperti ini. Terima kasih untuk sarannya.
ghoti

1

Pilihan lain untuk menangani whitespace yang disematkan, adalah dengan null-delimit printf, membuat perbedaan dengan sort, lalu menggunakan loop untuk mengemasnya kembali ke dalam array:

input=(a b c "$(printf "d\ne")" b c "$(printf "d\ne")")
output=()

while read -rd $'' element
do 
  output+=("$element")
done < <(printf "%s\0" "${input[@]}" | sort -uz)

Di akhir ini, inputdan outputberisi nilai yang diinginkan (urutan yang diberikan tidak penting):

$ printf "%q\n" "${input[@]}"
a
b
c
$'d\ne'
b
c
$'d\ne'

$ printf "%q\n" "${output[@]}"
a
b
c
$'d\ne'

1

Bagaimana dengan variasi ini?

printf '%s\n' "${ids[@]}" | sort -u

Dan kemudian sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u).
ganggang

0

Coba ini untuk mendapatkan nilai uniq untuk kolom pertama dalam file

awk -F, '{a[$1];}END{for (i in a)print i;}'

-3
# Read a file into variable
lines=$(cat /path/to/my/file)

# Go through each line the file put in the variable, and assign it a variable called $line
for line in $lines; do
  # Print the line
  echo $line
# End the loop, then sort it (add -u to have unique lines)
done | sort -u
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.