Как я могу получить уникальные значения из массива в Bash?

93

У меня почти такой же вопрос, как здесь .

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

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

Что я делаю не так?

Jetse
источник

Ответы:

131

Немного взломано, но это должно сработать:

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

Чтобы сохранить отсортированные уникальные результаты обратно в массив, выполните Array assignment :

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

Если ваша оболочка поддерживает herestrings ( 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 ожидает ввода в отдельные строки.
  • sort -u - сортировать и сохранять только уникальные элементы
  • tr '\n' ' ' - преобразовать символы новой строки, которые мы добавили ранее, обратно в пробелы.
  • $(...)- Подмена команд
  • Кроме того: tr ' ' '\n' <<< "${ids[@]}"это более эффективный способ:echo "${ids[@]}" | tr ' ' '\n'
Сампсон-Чен
источник
37
+1. Немного аккуратнее: храните элементы uniq в новом массиве:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
Гленн Джекман
@glennjackman о, классно! Я даже не подозревал, что вы можете использовать printfэтот способ (приводите больше аргументов, чем строки формата)
sampson-chen
4
+1 Я не уверен , если это единичный случай, но положить уникальные вещи обратно в массив необходимы дополнительные круглые скобки , например: sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')). Без дополнительных скобок он давал это как строку.
whla
3
Если вы не хотите изменять порядок элементов, используйте ... | uniq | ...вместо ... | sort -u | ....
Джесси Чисхолм
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 по-прежнему используется по умолчанию во многих окружениях
nhed 03
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
~>
das.cyklone
источник
1
чтобы исправить массив, я был вынужден сделать это:, ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`)поэтому я добавил, IFS=$'\n'предложенный @gniourf_gniourf
Сила Водолея
Мне также пришлось сделать резервную копию и после команды восстановить значение IFS! или это портит другие вещи ..
Aquarius Power
@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 аргументов, но все равно печатает, как если бы он имел один пустой аргумент - см. Объяснение). Так что вы должны уловить это в if или чем-то подобном.

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

например

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

Eval необходим для удаления экранирования каждого значения, возвращаемого в массив.

vontrapp
источник
Это единственный код, который у меня сработал, потому что в моем массиве строк были пробелы. % Q - вот что помогло. Спасибо :)
Somaiah Kumbera
А если вы не хотите изменять порядок элементов, используйте uniqвместо sort -u.
Джесси Чизхолм
Обратите внимание, что uniqэто не работает должным образом с несортированными списками, поэтому его всегда следует использовать в сочетании с sort.
Жан Поль
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` )
Corbyn42
источник
И если вы не хотите менять порядок оставшихся вещей, вам не нужно: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.
Джесси Чизхолм
@JesseChisholm uniqобъединяет только повторяющиеся соседние строки, поэтому это не то же самое, что awk '!x[$0]++'.
Six,
@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' ' '))
Estani
источник
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
rln
источник
Я только что заметил, что это практически то же самое, что и ответ @ghotis выше, за исключением того, что его решение не принимает во внимание элементы списка с пробелами.
rln
Хорошая точка зрения. Я добавил кавычки в свое решение, поэтому теперь оно обрабатывает пробелы. Первоначально я написал его просто для обработки выборки данных в вопросе, но всегда полезно учитывать такие непредвиденные обстоятельства. Спасибо за предложение.
Ghoti
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'
Morgen
источник
1

Как насчет этого варианта?

printf '%s\n' "${ids[@]}" | sort -u
jmg
источник
А потом sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u).
водоросли
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
K Закон
источник