Як я можу отримати унікальні значення з масиву в Bash?


93

У мене майже таке саме запитання, як і тут .

У мене є масив, який містить aa ab aa ac aa adтощо. Тепер я хочу вибрати всі унікальні елементи з цього масиву. Думав, це було б просто з sort | uniqабо з, sort -uяк вони згадували в тому іншому питанні, але в масиві нічого не змінилося ... Код:

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

Що я роблю не так?

Відповіді:


131

Трохи хакі, але це має зробити це:

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

Щоб зберегти відсортовані унікальні результати назад у масив, виконайте призначення Array :

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

Якщо ваша оболонка підтримує тут ( bashслід), ви можете позбавити echoпроцес, змінивши його на:

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

Вхідні дані:

ids=(aa ab aa ac aa ad)

Вихід:

aa ab ac ad

Пояснення:

  • "${ids[@]}"- Синтаксис для роботи з масивами оболонки, незалежно від того, використовується він як частина echoабо тут. У @частині означає «все елементи в масиві»
  • tr ' ' '\n'- Перетворити всі пробіли в нові рядки. Оскільки ваш масив розглядається оболонкою як елементи в одному рядку, розділені пробілами; і оскільки сортування очікує, що введення буде в окремих рядках.
  • sort -u - сортувати та зберігати лише унікальні елементи
  • tr '\n' ' ' - перетворити нові рядки, які ми додавали раніше, назад у пробіли.
  • $(...)- Заміна команди
  • Крім: tr ' ' '\n' <<< "${ids[@]}"це більш ефективний спосіб зробити:echo "${ids[@]}" | tr ' ' '\n'

37
+1. Трохи акуратніше: зберігаємо елементи uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
uniq

@glennjackman о, охайно! Я навіть не здогадувався, що ти можеш цим користуватися printf(наводити більше аргументів, ніж форматувати рядки)
sampson-chen

4
+1 Я не впевнений , якщо це одиничний випадок, але покласти унікальні речі назад в масив необхідні додаткові круглі дужки , наприклад: sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')). Без додаткових дужок він давав його як рядок.
2014 року

3
Якщо ви не хочете змінювати порядок елементів, використовуйте ... | uniq | ...замість ... | sort -u | ....
Jesse Chisholm

2
@Jesse, uniqвидаляє лише послідовні дублікати. У прикладі в цій відповіді sorted_unique_idsв кінцевому підсумку буде ідентично оригіналу ids. Щоб зберегти порядок, спробуйте ... | awk '!seen[$0]++'. Див. Також stackoverflow.com/questions/1444406/… .
Роб Кеннеді

29

Якщо у вас запущена версія Bash 4 або вище (що має бути в будь-якій сучасній версії Linux), ви можете отримати унікальні значення масиву в bash, створивши новий асоціативний масив, що містить кожне зі значень вихідного масиву. Щось на зразок цього:

$ 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

Це працює, оскільки в будь-якому масиві (асоціативному чи традиційному, будь-якою мовою) кожна клавіша може відображатися лише один раз. Коли forцикл отримує друге значення aain a[2], він перезаписує те, b[aa]що було встановлено спочатку a[0].

Робити речі в рідній bash може бути швидше, ніж за допомогою конвеєрів та зовнішніх інструментів, таких як sortі uniq, хоча для більших наборів даних ви, швидше за все, побачите кращу продуктивність, якщо ви використовуєте більш потужну мову, таку як awk, python тощо.

Якщо ви впевнені в собі, ви можете уникнути forциклу, скориставшись printfможливістю переробки формату для кількох аргументів, хоча це, здається, вимагає eval. (Припиніть читати зараз, якщо у вас це добре.)

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

Причиною цього рішення evalє те, що значення масиву визначаються перед розбиттям слів. Це означає, що результат заміни команди вважається одним словом, а не набором пар ключ = значення.

Незважаючи на те, що для цього використовується підшарка, вона використовує лише вбудовані bash для обробки значень масиву. Не забудьте оцінити своє вживання evalкритичним оком. Якщо ви не впевнені на 100%, що Чепнер або Глен Джекмен або сірий не знайдуть вади у вашому коді, використовуйте замість цього цикл for.


видає помилку: рівень рекурсії виразу перевищений
Benubird

1
@Benubird - чи можете ви, можливо, вставити вміст вашого терміналу? Для мене це чудово працює, тому я найкраще здогадуюсь, що у вас є (1) друкарська помилка, (2) старіша версія bash (асоціативні масиви були додані до v4) або (3) смішно великий приплив космічного фону випромінювання, спричинене квантовою чорною дірою у підвалі вашого сусіда, створюючи перешкоди для сигналів у вашому комп'ютері.
ghoti

1
не можу, не зберіг той, який не працював. але я спробував запустити твій щойно, і це спрацювало, так що, мабуть, космічна радіація.
Benubird,

припускаючи, що ця відповідь використовує bash v4 (асоціативні масиви), і якщо хтось намагається в bash v3, це не спрацює (можливо, не те, що бачив @Benubird). Bash v3 все ще є типовим у багатьох середовищах
2015 року

1
@nhed, точка взята. Я бачу, що у мого сучасного Yosemite Macbook така сама версія в основі, хоча я встановив v4 з macports. Це запитання позначено тегом "Linux", але я оновив свою відповідь, щоб вказати на цю вимогу.
ghoti

18

Я розумію, що на це вже відповіли, але воно виявилось досить високо в результатах пошуку, і це могло комусь допомогти.

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

Приклад:

~> 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
щоб виправити масив, мене змусили зробити це:, ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`)тому я додав IFS=$'\n'запропоновану @gniourf_gniourf
Aquarius Power

Мені також довелося зробити резервну копію та після команди відновити значення IFS! або це псує інші речі ..
Водолій

@Jetse Це повинна бути прийнята відповідь, оскільки вона використовує лише дві команди, без циклів, без eval і є найбільш компактною версією.
mgutt

1
@AquariusPower Обережно, ви в основному робите:, IFS=$'\n'; ids2=(...)оскільки тимчасове призначення перед призначеннями змінних неможливе. Замість того, щоб використовувати цю конструкцію: IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)".
Єті

13

Якщо у ваших елементах масиву є пробіли або будь-який інший спеціальний символ оболонки (і чи можете ви бути впевнені, що вони цього не роблять?), То для того, щоб зафіксувати їх насамперед (а ви повинні це робити завжди), висловіть свій масив у подвійних лапках! напр "${a[@]}". Bash буде буквально трактувати це як "кожен елемент масиву в окремому аргументі ". У межах bash це просто завжди працює, завжди.

Потім, щоб отримати відсортований (і унікальний) масив, ми повинні перетворити його на зрозумілий формат і мати можливість перетворити його назад в елементи масиву bash. Це найкраще, що я придумав:

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

На жаль, це не вдається в особливому випадку порожнього масиву, перетворюючи порожній масив на масив з 1 порожнім елементом (оскільки printf мав 0 аргументів, але все одно друкує так, ніби він мав один порожній аргумент - див. Пояснення). Отже, ви повинні вловити це в тому чи іншому випадку.

Пояснення: Формат% q для printf "убігає" надрукованого аргументу, саме таким чином, як bash може відновити щось на зразок eval! Оскільки кожен елемент надрукований оболонкою, екранованою у власному рядку, єдиним роздільником між елементами є новий рядок, і присвоєння масиву приймає кожен рядок як елемент, аналізуючи виведені значення в буквальний текст.

напр

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

Оператор eval необхідний для вилучення екрануючого кожного значення, що повертається в масив.


Це єдиний код, який працював у мене, оскільки мій масив рядків мав пробіли. % Q - це те, що зробило фокус. Дякую :)
Somaiah Kumbera

А якщо ви не хочете змінювати порядок елементів, використовуйте uniqзамість sort -u.
Jesse Chisholm

Зверніть увагу, що uniqвін не працює належним чином у несортованих списках, тому його слід завжди використовувати в поєднанні з sort.
Jean Paul

uniq у несортованому списку видалить послідовні дублікати. Він не видалить однакові елементи списку, розділені чимось іншим між ними. uniq може бути досить корисним залежно від очікуваних даних та бажання підтримувати оригінальне замовлення.
vontrapp

10

'sort' можна використовувати для впорядкування виводу циклу for:

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

і усунути дублікати з "-u":

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

Нарешті, ви можете просто переписати свій масив унікальними елементами:

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

І якщо ви не хочете змінювати порядок того, що залишилось, вам не потрібно:ids=( `for i in ${ids[@]}; do echo $i; done | uniq` )
Джессі Чисхолм

3

цей також збереже порядок:

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

та змінити оригінальний масив з унікальними значеннями:

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

Не використовуйте uniq. Він потребує сортування, де awk - ні, і метою цієї відповіді є збереження впорядкування, коли введені дані несортовані.
bukzor

2

Щоб створити новий масив, що складається з унікальних значень, переконайтеся, що ваш масив не порожній, тоді виконайте одне з наступного:

Видалити повторювані записи (із сортуванням)

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

Видалити повторювані записи (без сортування)

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

Попередження: Не намагайтеся робити щось подібне NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) ). Він порветься на просторах.


Видалення повторюваних записів (без сортування) відбувається так само, як (із сортуванням), за винятком зміни sort -uна uniq.
Jesse Chisholm

@JesseChisholm uniqоб'єднує лише сусідні повторювані рядки, тому це не те саме, що awk '!x[$0]++'.
Шість

@JesseChisholm Будь ласка, видаліть оманливий коментар.
bukzor

2

номер кота.txt

1 2 3 4 4 3 2 5 6

друк рядка в стовпець: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'

1
2
3
4
4
3
2
5
6

знайти дублікати записів: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk 'x[$0]++'

4
3
2

Замінити повторювані записи: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk '!x[$0]++'

1
2
3
4
5
6

Знайти лише записи Uniq: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i|"sort|uniq -u"}

1
5
6

1

Не втрачаючи оригінальне замовлення:

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

1

Якщо вам потрібне рішення, яке використовує лише внутрішні компоненти bash, ви можете встановити значення як ключі в асоціативному масиві, а потім витягти ключі:

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

Це виведе

bar
foo
bar none

Я щойно помітив, що це, по суті, те саме, що відповідь @ghotis вище, за винятком того, що його рішення не враховує елементи списку з пробілами.
rln

Гарна думка. Я додав лапки у своє рішення, тому воно тепер обробляє пробіли. Спочатку я писав це просто для обробки зразків даних у питанні, але завжди добре охоплювати такі випадки. Дякую за пропозицію.
ghoti 02

1

Іншим варіантом роботи із вбудованим пробілом є нульове розмежування printf, sortвиділення, а потім використання циклу, щоб упакувати його назад у масив:

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)

Наприкінці цього inputі outputмістити бажані значення (за умови, що порядок не важливий):

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

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


0

Спробуйте це, щоб отримати значення uniq для першого стовпця у файлі

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.