Сравнение/разница двух массивов в Bash

Можно ли взять разницу двух массивов в Bash. Как это сделать?

Код:

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" ) 

Array3 =diff(Array1, Array2)

Array3 ideally should be :
Array3=( "key7" "key8" "key9" "key10" )

person Kiran    schedule 22.02.2010    source источник
comment
Пробежавшись по решениям, я решил не использовать массивы в тех случаях, когда мне нужно их сравнить.   -  person x-yuri    schedule 24.10.2018


Ответы (7)


Если вы строго хотите Array1 - Array2, то

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" )

Array3=()
for i in "${Array1[@]}"; do
    skip=
    for j in "${Array2[@]}"; do
        [[ $i == $j ]] && { skip=1; break; }
    done
    [[ -n $skip ]] || Array3+=("$i")
done
declare -p Array3

Время выполнения можно было бы улучшить с помощью ассоциативных массивов, но лично я бы не стал заморачиваться. Если вы манипулируете достаточным количеством данных, чтобы это имело значение, оболочка — не тот инструмент.


Для симметричной разницы, такой как ответ Денниса, существующие инструменты, такие как comm, работают, если мы немного массируем ввод и вывод (поскольку они работают со строковыми файлами, а не с переменными оболочки).

Здесь мы указываем оболочке использовать символы новой строки для объединения массива в одну строку и отбрасывать табуляцию при чтении строк из comm обратно в массив.

$ oldIFS=$IFS IFS=$'\n\t'
$ Array3=($(comm -3 <(echo "${Array1[*]}") <(echo "${Array2[*]}")))
comm: file 1 is not in sorted order
$ IFS=$oldIFS
$ declare -p Array3
declare -a Array3='([0]="key7" [1]="key8" [2]="key9" [3]="key10")'

Он жалуется, потому что при лексографической сортировке key1 < … < key9 > key10. Но поскольку оба входных массива отсортированы одинаково, можно игнорировать это предупреждение. Вы можете использовать --nocheck-order, чтобы избавиться от предупреждения, или добавить | sort -u внутри подстановки процесса <(…), если вы не можете гарантировать порядок и уникальность входных массивов.

person ephemient    schedule 23.02.2010
comment
+1 за 1-й фрагмент, который также работает с элементами со встроенными пробелами. Второй фрагмент работает только с элементами со встроенными пробелами. Вы можете отказаться от сохранения и восстановления $IFS, если просто добавите IFS=$'\n\t' непосредственно к команде Array3=.... - person mklement0; 01.06.2014
comment
@mklement0 Команда, которую вы предлагаете: IFS=$'\n\t' Array3=( ... ) будет устанавливать IFS глобально. Попробуй! - person gniourf_gniourf; 01.06.2014
comment
@gniourf_gniourf: Спасибо, что поймали! Поскольку мое заблуждение может быть соблазнительным и для других, я оставлю свой первоначальный комментарий и объясню здесь: хотя это распространенная и полезная идиома — добавлять нерегламентированное назначение локальной команды к простому здесь она НЕ работает, потому что моя команда полностью состоит из присваиваний. Никакое имя команды (внешняя исполняемая, встроенная) не следует за назначениями, что делает все глобальными (в контексте текущей оболочки); см. man bash, раздел SIMPLE COMMAND EXPANSION). - person mklement0; 01.06.2014
comment
Можете привести пример, как это сделать в C-shell (csh)? - person Stefan; 11.11.2014
comment
@Stefan: Тьфу, csh никогда не следует использовать. set Array3 = ( ) foreach i ( $Array1 ) set skip = 0 foreach j ( $Array2 ) if ( "$i" == "$j" ) then set skip = 1 break endif end if ( "$skip" == 0 ) then set Array3 = ( $Array3:q "$i" ) endif end Все операторы управления должны находиться на отдельных строках. - person ephemient; 12.11.2014
comment
@ephemient Спасибо за ваш пост! - person Stefan; 12.11.2014

echo ${Array1[@]} ${Array2[@]} | tr ' ' '\n' | sort | uniq -u

Вывод

key10
key7
key8
key9

Вы можете добавить сортировку, если вам нужно

person Ilya Bystrov    schedule 27.01.2015
comment
Пришел, поприкалывался и ушел. Если вам интересно, как сохранить значение в массиве, попробуйте следующее: Array3=(`echo ${Array1[@]} ${Array2[@]} | tr ' ' '\n' | sort | uniq -u `) - person Anake; 22.08.2015
comment
Вот что такое программирование оболочки. Будьте проще, используйте доступные инструменты. Если вы хотите реализовать другие решения, вы можете это сделать, но вам будет проще использовать более надежный язык. - person wrangler; 26.08.2015
comment
Невероятно даже по сей день. - person Zee; 25.02.2016
comment
Блестящий. Дополнительное примечание для тех, кому нужна асимметричная разница. Вы можете получить его, выведя дубликаты симметричного различия и массива, который вас интересует. IE, если вы хотите, чтобы значения присутствовали в массиве2, но не в массиве1. echo ${Array2[@]} ${Array3[@]} | tr ' ' '\n' | sort | uniq -D | uniq, где Array3 — результат вышеприведенного. Кроме того, если вы удалите обозначения массива и предполагаете, что переменные представляют собой строки, разделенные пробелами, этот подход совместим с оболочкой posix. - person Arwyn; 29.03.2016
comment
Потрясающее решение. Небольшое улучшение, если элементы массива могут содержать пробелы: printf '%s\n' "${Array1[@]}" "${Array2[@]}" | sort | uniq -u - person misberner; 13.07.2016
comment
Чтобы упростить предложение @Arwyn, вы можете дважды добавить игнорируемый массив, чтобы отображались только различия в Array2. echo ${Array1[@]} ${Array1[@]} ${Array2[@]} | tr ' ' '\n' | sort | uniq -u - person Christopher Markieta; 06.08.2017
comment
один небольшой комментарий к ответу @ChristopherMarkieta: вопрос заключался в том, чтобы вычислить Array1-Array2, в этом случае он должен быть echo ${Array1[@]} ${Array2[@]} ${Array2[@]} | tr ' ' '\n' | sort | uniq -u (Array2 два раза). И спасибо за отличное дополнение к отличному ответу. - person astafev.evgeny; 14.12.2017
comment
Добавление к расширению @misberner: если ваш массив содержит новую строку и пробелы: printf "%s\0" "${Array1[@]}" "${Array2[@]}" | sort -z | uniq -zu . (Вывод будет разделен нулем и должен быть обработан соответствующим образом) - person kvantour; 11.05.2020
comment
Мне не удалось заставить какие-либо из предложений этого потока работать в моей функции. Протяни руку, если сможешь? stackoverflow.com/questions/63314441/ - person JamesIsIn; 08.08.2020
comment
Найден крайний случай, когда это решение не работает - если оба массива пусты, выполнение echo ${Array1[@]} ${Array2[@]} | tr ' ' '\n' | sort | uniq -u | wc -l печатает 1, а не 0 - person kp123; 25.09.2020
comment
На самом деле ответ в том виде, в каком он дан, заключается в вычислении симметричной разницы. Вещи, которые есть в обоих наборах, будут удалены, вещи только в одном останутся: $ echo {1..4} {2..6} | tr ' ' '\n' | sort | uniq -u | tr '\n' ' ' дает 1 5 6. Включение второго набора дважды дает обычную разницу: $ echo {1..4} {2..6} {2..6} | tr ' ' '\n' | sort | uniq -u | tr '\n' ' ' дает 1. - person jspencer; 04.06.2021

Каждый раз, когда возникает вопрос, связанный с уникальными значениями, которые нельзя отсортировать, я сразу же схожу с ума. Вот мой взгляд на это.

Код

#!/bin/bash

diff(){
  awk 'BEGIN{RS=ORS=" "}
       {NR==FNR?a[$0]++:a[$0]--}
       END{for(k in a)if(a[k])print k}' <(echo -n "${!1}") <(echo -n "${!2}")
}

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" )
Array3=($(diff Array1[@] Array2[@]))
echo ${Array3[@]}

Вывод

$ ./diffArray.sh
key10 key7 key8 key9

*Примечание**: как и в других ответах, если в массиве есть повторяющиеся ключи, о них будет сообщено только один раз; это может быть или не быть тем поведением, которое вы ищете. Код awk для обработки этого более грязный и не такой чистый.

person SiegeX    schedule 24.02.2010
comment
Подводя итог по поведению и ограничениям: (a) выполняет симметричное различие: выводит одиночный массив с элементами, уникальными для любого входного массива (который с пример данных OP оказывается таким же, как и только выходные элементы, уникальные для массива first), (b) работает только с элементами, которые не имеют встроенных пробелов (что удовлетворяет требованиям OP), и (c ) порядок элементов в выходном массиве НЕ имеет гарантированной связи с порядком входных элементов из-за безусловного использования awk ассоциативных массивов, о чем свидетельствует образец вывода. - person mklement0; 01.06.2014
comment
Кроме того, в этом ответе используется умный и заслуживающий внимания, но сбивающий с толку, если необъяснимый обходной путь для отсутствия поддержки bash для передачи массивов в качестве аргументов: Array1[@] и Array2[@] передаются как строки - соответствующие имена массивов плюс суффикс всех индексов [@]- для функции оболочки diff() (как обычно, в качестве аргументов $1 и $2). Затем функция оболочки использует переменную bash indirection ({!...}) для косвенной ссылки на все элементы исходных массивов (${!1} и `${!1}'). - person mklement0; 01.06.2014
comment
как преобразовать строку a b C в массив? - person brauliobo; 08.10.2014
comment
обнаружена ошибка: элементы в Array2, которых нет в Array1, будут отображаться в diff() - person brauliobo; 08.10.2014
comment
Это решение не работает для элементов массива, содержащих пробелы. Пример сценария может дать сбой по нескольким причинам из-за того, что строки без кавычек расширяются оболочкой как GLOB. Это не удастся, если вы сделаете touch Array1@ перед запуском сценария, потому что строки Array1[@] и Array2[@] используются как шаблоны оболочки GLOB без кавычек. Это не удается, если один массив содержит элемент *, потому что этот шаблон GLOB без кавычек соответствует всем файлам в текущем каталоге. - person Ian D. Allen; 22.03.2021

Имея ARR1 и ARR2 в качестве аргументов, используйте comm для выполнения задания и mapfile, чтобы поместить его обратно в массив RESULT:

ARR1=("key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10")
ARR2=("key1" "key2" "key3" "key4" "key5" "key6")

mapfile -t RESULT < \
    <(comm -23 \
        <(IFS=$'\n'; echo "${ARR1[*]}" | sort) \
        <(IFS=$'\n'; echo "${ARR2[*]}" | sort) \
    )

echo "${RESULT[@]}" # outputs "key10 key7 key8 key9"

Обратите внимание, что результат может не соответствовать исходному порядку.

Бонус, также известный как «вот для чего вы здесь»:

function array_diff {
    eval local ARR1=\(\"\${$2[@]}\"\)
    eval local ARR2=\(\"\${$3[@]}\"\)
    local IFS=$'\n'
    mapfile -t $1 < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort))
}

# usage:
array_diff RESULT ARR1 ARR2
echo "${RESULT[@]}" # outputs "key10 key7 key8 key9"

Использование этих хитрых вычислений является наименее худшим вариантом среди других, связанных с передачей параметров массива в bash.

Кроме того, взгляните на comm справочную страницу; на основе этого кода очень легко реализовать, например, array_intersect: просто используйте -12 в качестве параметров связи.

person Alex Offshore    schedule 22.02.2017
comment
Отметив, что mapfile нужен bash 4 - person lantrix; 10.07.2017
comment
@lantrix, mapfile можно легко заменить на while..read и даже полностью вырезать, если в результате не нужен массив. Все волшебство происходит в comm. - person Alex Offshore; 11.07.2017

В Баше 4:

declare -A temp    # associative array
for element in "${Array1[@]}" "${Array2[@]}"
do
    ((temp[$element]++))
done
for element in "${!temp[@]}"
do
    if (( ${temp[$element]} > 1 ))
    then
        unset "temp[$element]"
    fi
done
Array3=(${!temp[@]})    # retrieve the keys as values

Изменить:

ephemient указал на потенциально серьезную ошибку. Если элемент существует в одном массиве с одним или несколькими дубликатами и вообще не существует в другом массиве, он будет ошибочно удален из списка уникальных значений. Версия ниже пытается справиться с этой ситуацией.

declare -A temp1 temp2    # associative arrays
for element in "${Array1[@]}"
do
    ((temp1[$element]++))
done

for element in "${Array2[@]}"
do
    ((temp2[$element]++))
done

for element in "${!temp1[@]}"
do
    if (( ${temp1[$element]} >= 1 && ${temp2[$element]-0} >= 1 ))
    then
        unset "temp1[$element]" "temp2[$element]"
    fi
done
Array3=(${!temp1[@]} ${!temp2[@]})
person Dennis Williamson    schedule 22.02.2010
comment
Это выполняет симметричную разницу и предполагает, что исходные массивы не имеют дубликатов. Так что это не то, о чем я бы подумал в первую очередь, но это хорошо работает для одного примера ОП. - person ephemient; 22.02.2010
comment
@ephemient: Правильно, параллель будет diff(1), которая также симметрична. Также этот скрипт будет работать для поиска элементов, уникальных для любого количества массивов, просто добавляя их в список во второй строке первой версии. Я добавил редактирование, которое предоставляет версию для обработки дубликатов в одном массиве, которых нет в другом. - person Dennis Williamson; 23.02.2010
comment
Большое спасибо .. Я думал, есть ли какой-нибудь очевидный способ сделать это .. Если я не знаю какой-либо команды, которая легко даст разницу между двумя массивами .. Спасибо за вашу поддержку и помощь. Я изменил код, чтобы прочитать разницу двух файлов, что было немного проще в программировании. - person Kiran; 23.02.2010
comment
Ваш второй фрагмент не будет работать, потому что > работает только в (( ... )), а не в [[ ... ]]; в последнем случае это должно быть -gt; однако, поскольку вы, вероятно, имели в виду >=, а не >, > следует заменить на -ge. Чтобы пояснить, что означает симметричность в данном контексте: вывод представляет собой одиночный массив, содержащий значения, уникальные для любого массива. - person mklement0; 01.06.2014
comment
@ mklement0: > работает внутри двойных квадратных скобок, но лексически, а не численно. Из-за этого при сравнении целых чисел следует использовать двойные скобки - так что вы правы в этом отношении. Я обновил свой ответ соответственно. - person Dennis Williamson; 01.06.2014
comment
@DennisWilliamson: Спасибо за разъяснение относительно > внутри [[ ... ]] и спасибо за обновление вашего ответа. Однако я думаю, что это должно быть >= 1, а не > 1. Однако, что более важно, условное выражение ((...)) не работает с несуществующими (пустыми) элементами temp2, поэтому вам нужно либо использовать ${temp2[$element]-0}, либо придерживаться [[...]] и -ge. - person mklement0; 01.06.2014
comment
Поскольку я не получил ответа и не хотел, чтобы явно неправильный ответ оставался в силе, я взял на себя смелость исправить его. Пожалуйста, дайте мне знать, если вы считаете, что исправление является неправильным или неуместным. В общем, этот ответ работает только с элементами массива без встроенных пробелов (что, как указано, удовлетворяет требованиям OP). - person mklement0; 02.06.2014

Также можно использовать регулярное выражение (на основе другого ответа: пересечение массива в bash):

list1=( 1 2 3 4   6 7 8 9 10 11 12)
list2=( 1 2 3   5 6   8 9    11 )

l2=" ${list2[*]} "                    # add framing blanks
for item in ${list1[@]}; do
  if ! [[ $l2 =~ " $item " ]] ; then    # use $item as regexp
    result+=($item)
  fi
done
echo  ${result[@]}:

Результат:

$ bash diff-arrays.sh 
4 7 10 12
person Denis Gois    schedule 15.02.2016
comment
кажется странным, что за это проголосовали против без комментариев. Если есть проблема с этим, сделайте всем одолжение и укажите, в чем проблема. - person philwalk; 09.03.2017

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" )
Array3=( "key1" "key2" "key3" "key4" "key5" "key6" "key11" )
a1=${Array1[@]};a2=${Array2[@]}; a3=${Array3[@]}
diff(){
    a1="$1"
    a2="$2"
    awk -va1="$a1" -va2="$a2" '
     BEGIN{
       m= split(a1, A1," ")
       n= split(a2, t," ")
       for(i=1;i<=n;i++) { A2[t[i]] }
       for (i=1;i<=m;i++){
            if( ! (A1[i] in A2)  ){
                printf A1[i]" "
            }
        }
    }'
}
Array4=( $(diff "$a1" "$a2") )  #compare a1 against a2
echo "Array4: ${Array4[@]}"
Array4=( $(diff "$a3" "$a1") )  #compare a3 against a1
echo "Array4: ${Array4[@]}"

вывод

$ ./shell.sh
Array4: key7 key8 key9 key10
Array4: key11
person ghostdog74    schedule 23.02.2010