Làm cách nào để nhận các giá trị duy nhất từ ​​một mảng trong Bash?


93

Tôi có câu hỏi gần giống như ở đây .

Tôi có một mảng chứa aa ab aa ac aa ad, v.v. Bây giờ tôi muốn chọn tất cả các phần tử duy nhất từ ​​mảng này. Tôi nghĩ, điều này sẽ đơn giản với sort | uniqhoặc với sort -unhư họ đã đề cập trong câu hỏi khác, nhưng không có gì thay đổi trong mảng ... Mã là:

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

Tôi đang làm gì sai?

Câu trả lời:


131

Một chút hacky, nhưng điều này sẽ làm được:

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

Để lưu các kết quả duy nhất đã được sắp xếp trở lại vào một mảng, hãy thực hiện phép gán Mảng :

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

Nếu trình bao của bạn hỗ trợ các chuỗi này ( bashnên), bạn có thể dự phòng một echoquy trình bằng cách thay đổi nó thành:

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

Đầu vào:

ids=(aa ab aa ac aa ad)

Đầu ra:

aa ab ac ad

Giải trình:

  • "${ids[@]}"- Cú pháp để làm việc với mảng shell, cho dù được sử dụng như một phần của chuỗi này echohay một chuỗi. Phần @có nghĩa là "tất cả các phần tử trong mảng"
  • tr ' ' '\n'- Chuyển đổi tất cả các khoảng trắng sang dòng mới. Bởi vì mảng của bạn được shell xem như các phần tử trên một dòng, được phân tách bằng dấu cách; và vì sắp xếp yêu cầu đầu vào nằm trên các dòng riêng biệt.
  • sort -u - sắp xếp và chỉ giữ lại các phần tử duy nhất
  • tr '\n' ' ' - chuyển đổi các dòng mới mà chúng tôi đã thêm vào trước đó trở lại dấu cách.
  • $(...)- Thay thế lệnh
  • Ngoài ra: tr ' ' '\n' <<< "${ids[@]}"là một cách hiệu quả hơn để làm:echo "${ids[@]}" | tr ' ' '\n'

37
+1. Một chút ngăn nắp: cửa hàng uniq yếu tố trong một mảng mới:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
glenn Jackman

@glennjackman ôi thật gọn gàng! Tôi thậm chí còn không nhận ra bạn có thể sử dụng printftheo cách đó (cho lập luận hơn định dạng chuỗi)
sampson-chen

4
1 Tôi không chắc chắn nếu điều này là một trường hợp cá biệt, nhưng đặt mục độc đáo trở lại vào một mảng cần ngoặc bổ sung như: sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')). Nếu không có dấu ngoặc đơn bổ sung, nó đã cho nó dưới dạng một chuỗi.
whla

3
Nếu bạn không muốn thay đổi thứ tự của các phần tử, hãy sử dụng ... | uniq | ...thay vì ... | sort -u | ....
Jesse Chisholm

2
@Jesse, uniqchỉ xóa các bản sao liên tiếp . Trong ví dụ trong câu trả lời này, sorted_unique_idssẽ kết thúc giống hệt với bản gốc ids. Để duy trì trật tự, hãy thử ... | awk '!seen[$0]++'. Xem thêm stackoverflow.com/questions/1444406/… .
Rob Kennedy

29

Nếu bạn đang chạy phiên bản Bash 4 trở lên (trường hợp này xảy ra trong bất kỳ phiên bản Linux hiện đại nào), bạn có thể nhận các giá trị mảng duy nhất trong bash bằng cách tạo một mảng kết hợp mới chứa từng giá trị của mảng ban đầu. Một cái gì đó như thế này:

$ 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

Điều này hoạt động vì trong bất kỳ mảng nào (liên kết hoặc truyền thống, bằng bất kỳ ngôn ngữ nào), mỗi khóa chỉ có thể xuất hiện một lần. Khi forvòng lặp đến giá trị thứ hai của aain a[2], nó sẽ ghi đè giá trị b[aa]được đặt ban đầu cho a[0].

Thực hiện mọi thứ trong native bash có thể nhanh hơn so với việc sử dụng các đường dẫn và các công cụ bên ngoài như sortuniq, mặc dù đối với các bộ dữ liệu lớn hơn, bạn có thể sẽ thấy hiệu suất tốt hơn nếu bạn sử dụng một ngôn ngữ mạnh mẽ hơn như awk, python, v.v.

Nếu bạn cảm thấy tự tin, bạn có thể tránh forvòng lặp bằng cách sử dụng printfkhả năng tái chế định dạng của nó cho nhiều đối số, mặc dù điều này dường như yêu cầu eval. (Ngừng đọc ngay bây giờ nếu bạn thấy ổn với điều đó.)

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

Lý do giải pháp này yêu cầu evallà các giá trị mảng được xác định trước khi tách từ. Điều đó có nghĩa là đầu ra của lệnh thay thế được coi là một từ duy nhất chứ không phải là một tập hợp các cặp key = value.

Trong khi điều này sử dụng một vỏ con, nó chỉ sử dụng nội trang cơ sở để xử lý các giá trị mảng. Hãy chắc chắn để đánh giá việc sử dụng của bạn evalvới một con mắt quan trọng. Nếu bạn không chắc chắn 100% rằng chepner hoặc glenn jackman hoặc greycat sẽ không tìm thấy lỗi nào với mã của bạn, hãy sử dụng vòng lặp for.


sản xuất lỗi: biểu hiện mức độ đệ quy vượt
Benubird

1
@Benubird - bạn có thể dán nội dung đầu cuối của mình không? Nó hoạt động hoàn hảo đối với tôi, vì vậy dự đoán tốt nhất của tôi là bạn đã mắc (1) lỗi đánh máy, (2) phiên bản bash cũ hơn (các mảng liên kết đã được thêm vào v4), hoặc (3) một lượng lớn nền vũ trụ bức xạ gây ra bởi lỗ đen lượng tử trong tầng hầm của nhà hàng xóm của bạn, tạo ra nhiễu với các tín hiệu trong máy tính của bạn.
ghoti

1
không thể, đã không giữ một trong những không hoạt động. nhưng, tôi đã thử chạy của bạn vừa rồi và nó hoạt động, vì vậy có lẽ là thứ bức xạ vũ trụ.
Benubird

đoán rằng câu trả lời này sử dụng bash v4 (mảng kết hợp) và nếu ai đó cố gắng trong bash v3 thì nó sẽ không hoạt động (có thể không phải những gì @Benubird đã thấy). Bash v3 vẫn mặc định trong nhiều envs
nhed

1
@nhed, lấy điểm. Tôi thấy rằng Macbook Yosemite cập nhật của mình có cùng một phiên bản trong cơ sở, mặc dù tôi đã cài đặt v4 từ macports. Câu hỏi này được gắn thẻ "linux", nhưng tôi đã cập nhật câu trả lời của mình để chỉ ra yêu cầu.
ghoti

18

Tôi nhận ra điều này đã được trả lời, nhưng nó hiển thị khá cao trong kết quả tìm kiếm và nó có thể giúp ích cho ai đó.

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

Thí dụ:

~> 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
để sửa chữa các mảng Tôi đã buộc phải làm điều này: ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`), vì vậy tôi thêm vào IFS=$'\n'đề nghị của @gniourf_gniourf
Bảo Bình điện

Tôi cũng phải sao lưu và sau lệnh, khôi phục giá trị IFS! hoặc nó messes những thứ khác ..
Bảo Bình điện

@Jetse Đây phải là câu trả lời được chấp nhận vì nó chỉ sử dụng hai lệnh, không có vòng lặp, không có eval và là phiên bản nhỏ gọn nhất.
mgutt

1
@AquariusPower Hãy cẩn thận, về cơ bản bạn đang làm : IFS=$'\n'; ids2=(...), vì không thể thực hiện nhiệm vụ tạm thời trước khi thực hiện các nhiệm vụ thay đổi. Thay vì sử dụng xây dựng này: IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)".
Yeti

13

Nếu các phần tử mảng của bạn có khoảng trắng hoặc bất kỳ ký tự đặc biệt nào khác (và bạn có thể chắc chắn là không?) Thì trước hết, để nắm bắt những phần tử đó (và bạn chỉ nên làm điều này), hãy diễn đạt mảng của bạn trong dấu ngoặc kép! vd "${a[@]}". Bash sẽ hiểu theo nghĩa đen điều này là "mỗi phần tử mảng trong một đối số riêng biệt ". Trong phạm vi bash, điều này đơn giản luôn luôn hoạt động, luôn luôn.

Sau đó, để có được một mảng được sắp xếp (và duy nhất), chúng ta phải chuyển đổi nó sang một định dạng sắp xếp có thể hiểu được và có thể chuyển nó trở lại thành các phần tử của mảng bash. Đây là điều tốt nhất mà tôi nghĩ ra:

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

Thật không may, điều này không thành công trong trường hợp đặc biệt của mảng trống, biến mảng trống thành mảng có 1 phần tử trống (vì printf có 0 đối số nhưng vẫn in ra như thể nó có một đối số trống - xem giải thích). Vì vậy, bạn phải nắm bắt điều đó trong if hoặc something.

Giải thích: Định dạng% q cho printf "shell thoát" đối số được in, theo cách mà bash có thể khôi phục trong một cái gì đó như eval! Bởi vì mỗi phần tử được in shell thoát trên dòng riêng của nó, dấu phân tách duy nhất giữa các phần tử là dòng mới và phép gán mảng nhận mỗi dòng làm phần tử, phân tích cú pháp các giá trị đã thoát thành văn bản chữ.

ví dụ

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

Đánh giá là cần thiết để loại bỏ thoát khỏi mỗi giá trị quay trở lại mảng.


Đây là mã duy nhất phù hợp với tôi vì mảng chuỗi của tôi có khoảng trắng. % Q là những gì đã làm thủ thuật. Cảm ơn :)
Somaiah Kumbera

Và nếu bạn không muốn thay đổi thứ tự của các phần tử, hãy sử dụng uniqthay vì sort -u.
Jesse Chisholm

Lưu ý rằng uniqnó không hoạt động đúng trên danh sách chưa được sắp xếp, vì vậy nó phải luôn được sử dụng kết hợp với sort.
Jean Paul,

uniq trên một danh sách không được sắp xếp sẽ loại bỏ các bản sao liên tiếp . Nó sẽ không loại bỏ các phần tử danh sách giống hệt nhau được phân tách bởi một cái gì đó khác ở giữa. uniq có thể đủ hữu ích tùy thuộc vào dữ liệu mong đợi và mong muốn duy trì trật tự ban đầu.
vontrapp

10

'sort' có thể được sử dụng để sắp xếp đầu ra của vòng lặp for:

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

và loại bỏ các bản sao bằng "-u":

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

Cuối cùng, bạn chỉ có thể ghi đè mảng của mình bằng các phần tử duy nhất:

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

Và nếu bạn không muốn thay đổi thứ tự của những thứ còn lại, bạn không cần phải:ids=( `for i in ${ids[@]}; do echo $i; done | uniq` )
Jesse Chisholm

3

cái này cũng sẽ duy trì thứ tự:

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

và để sửa đổi mảng ban đầu với các giá trị duy nhất:

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

Không sử dụng uniq. Nó cần sắp xếp, trong khi awk thì không, và mục đích của câu trả lời này là để duy trì thứ tự khi đầu vào không được sắp xếp.
bukzor

2

Để tạo một mảng mới bao gồm các giá trị duy nhất, hãy đảm bảo mảng của bạn không trống, sau đó thực hiện một trong các thao tác sau:

Loại bỏ các mục nhập trùng lặp (có sắp xếp)

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

Xóa các mục nhập trùng lặp (không cần sắp xếp)

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

Cảnh báo: Đừng cố gắng làm điều gì đó giống như NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) ). Nó sẽ vỡ trên khoảng trống.


Hủy bỏ các mục trùng lặp (không phân loại) chỉ là tương tự (với phân loại) ngoại trừ thay đổi sort -uđược uniq.
Jesse Chisholm

@JesseChisholm uniqchỉ hợp nhất các dòng trùng lặp liền kề, vì vậy nó không giống với awk '!x[$0]++'.
Sáu

@JesseChisholm Vui lòng xóa bình luận gây hiểu lầm.
bukzor

2

cat number.txt

1 2 3 4 4 3 2 5 6

in dòng thành cột: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'

1
2
3
4
4
3
2
5
6

tìm các bản ghi trùng lặp: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk 'x[$0]++'

4
3
2

Thay thế các bản ghi trùng lặp: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk '!x[$0]++'

1
2
3
4
5
6

Chỉ tìm các bản ghi Uniq: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i|"sort|uniq -u"}

1
5
6

1

Không làm mất thứ tự ban đầu:

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

1

Nếu bạn muốn một giải pháp chỉ sử dụng nội bộ bash, bạn có thể đặt các giá trị làm khóa trong một mảng kết hợp, sau đó trích xuất các khóa:

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

Điều này sẽ xuất ra

bar
foo
bar none

Tôi chỉ nhận thấy điều này về cơ bản giống với câu trả lời @ghotis ở trên, ngoại trừ giải pháp của anh ấy không tính đến các mục danh sách có dấu cách.
rln

Điểm tốt. Tôi đã thêm dấu ngoặc kép vào giải pháp của mình để nó hiện xử lý khoảng trắng. Ban đầu tôi viết nó chỉ để xử lý dữ liệu mẫu trong câu hỏi, nhưng luôn tốt nếu đề cập đến những trường hợp bất thường như thế này. Cám ơn vì sự gợi ý.
ghoti

1

Một tùy chọn khác để xử lý khoảng trắng được nhúng, là phân printftách bằng null , phân biệt với sort, sau đó sử dụng một vòng lặp để đóng gói nó lại thành một mảng:

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)

Ở cuối phần này inputoutputchứa các giá trị mong muốn (thứ tự được cung cấp không quan trọng):

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

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

1

Làm thế nào về biến thể này?

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

Và sau đó sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u).
tảo

0

Hãy thử điều này để nhận các giá trị uniq cho cột đầu tiên trong tệp

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.