diff по строкам, а не по строкам

Я чувствую, что должен быть в состоянии сделать это во сне, но скажем, у меня есть два текстовых файла, каждый из которых имеет один столбец имен модулей apache в произвольном порядке. Один файл содержит 46 уникальных (для себя) строк. Другой имеет 67 строк и 67 уникальных (для файла) строк. Будет много общих строк.

Мне нужно найти имена модулей apache, которых нет в более коротком первом файле, но они есть во втором, более длинном файле.

Я хочу сделать это путем поиска и сравнения строк. Номер строки, порядок или положение совершенно не имеют значения. Я просто хочу знать, какие модули, перечисленные только в более длинном файле, необходимо установить.

По умолчанию uniq, comm и diff работают по строкам и номерам строк. Я не хочу прямого сравнения; Я просто хочу список.


person mr.zog    schedule 21.02.2012    source источник
comment
можешь выложить небольшую выборку из всех задействованных файлов? а также ожидаемый результат?   -  person havexz    schedule 21.02.2012
comment
Вас волнует, есть ли элементы в более коротком файле, которых нет в более длинном файле, или это вообще невозможно?   -  person jbranchaud    schedule 21.02.2012


Ответы (2)


Разбейте ваши строки на строки, отсортируйте и упорядочите их, а затем используйте comm для анализа. (См. BashFAQ #36).

Я собираюсь предположить, чтобы иметь пример, что вы хотите сравнить директивы LoadModule между двумя конфигурационными файлами Apache.

файл1:

...other stuff...
LoadModule foo modules/foo.so
LoadModule bar modules/bar.so
LoadModule baz modules/baz.so
...other stuff...

файл2:

...other stuff...
LoadModule foo modules/foo.so
...other stuff...

Итак, чтобы сделать это:

comm -2 -3 \
  <(gawk '/LoadModule/ { print $2 }' file1 | sort -u)
  <(gawk '/LoadModule/ { print $2 }' file2 | sort -u)

... подавляет любые строки, найденные в обоих или только в более коротком файле, и дает вам имена модулей, найденные в третьем, что дает следующий вывод:

bar
baz

Для людей, рассматривающих этот вопрос с более интересными вариантами использования - к сожалению, в то время как флаг -z сортировки GNU может обрабатывать разделители NUL (чтобы разрешить сравнение строк, содержащих символы новой строки), comm не может. Однако вы можете написать свою собственную реализацию comm в оболочке, которая поддерживает разделители NUL, как в следующем примере:

#!/bin/bash
exec 3<"$1" 4<"$2"

IFS='' read -u 4 -d ''; input_two="$REPLY"

while IFS='' read -u 3 -d '' ; do
    input_one="$REPLY"
    while [[ $input_two < $input_one ]] ; do
        IFS='' read -u 4 -d '' || exit 0
        input_two="$REPLY"
    done
    if [[ $input_two = "$input_one" ]] ; then
        printf '%s\0' "$input_two"
    fi
done
person Charles Duffy    schedule 21.02.2012
comment
Оба файла представляют собой просто списки файлов модулей Apache, например: mod_vhost_alias.so mod_mem_cache.so mod_status.so mod_ext_filter.so mod_authz_user.so mod_rewrite.so mod_imagemap.so mod_cgi.so - person mr.zog; 21.02.2012
comment
@user189395 user189395, если это так, вы можете опустить биты gawk и просто отправить контент напрямую через sort -u. Если файлы разделены пробелами, вы можете передать их через tr ' ' '\n' перед sort, чтобы изменить это на разделение строк. - person Charles Duffy; 21.02.2012

Я бы запустил такой небольшой скрипт bash (differ.bash):

#!/bin/bash
f1=$1; # longer file
f2=$2; # shorter file

for item in `cat $f1`
do
    match=0
    for other in `cat $f2`
    do
        if [ "$item" == "$other" ]
        then
            match=1
            break
        fi
    done
    if [ $match != 1 ]
    then
        echo $item
    fi
done

exit 0

Запустите его так:

$ ./differ.bash file1 file2

По сути, я просто устанавливаю двойной цикл for с более длинным файлом во внешнем цикле и более коротким файлом во внутреннем цикле. Таким образом, каждый элемент в более длинном списке сравнивается с элементами в более коротком списке. Это позволяет нам найти все элементы, которые не соответствуют чему-либо в меньшем списке.


Изменить: я попытался ответить на первый комментарий Чарльза с помощью этого обновленного скрипта:

#!/bin/bash
f1=$1; # longer file
f2=$2; # shorter file

while read item
do
    others=( "${others[@]}" "$item" )
done < $f2

while read item
do
    match=0
    for other in $others
    do
        if [ "$item" == "$other" ]
        then
            match=1
            break
        fi
    done
    if [ $match != 1 ]
    then
        echo $item
    fi
done < $f1

exit 0
person jbranchaud    schedule 21.02.2012
comment
Использование голого cat $f1 подвергает содержимое файла не только разбиению строки (что, вероятно, необходимо в данном случае), но и расширению с помощью подстановочных знаков, поэтому, если, например, в файле есть запись f*, она будет заменена на имена всех файлов в текущем каталоге, начинающиеся с f. Вероятно, это не то поведение, которое вы хотите. Кроме того, чтение файла внутри внутреннего цикла вместо сохранения его содержимого в массиве только один раз заранее является излишне неэффективным. - person Charles Duffy; 21.02.2012
comment
... и в дополнение к сложности O(n*m) с высокими постоянными значениями, этот подход требует, чтобы оба списка могли помещаться в памяти одновременно; comm считывает записи только по одной строке за раз, а сортировка GNU может использовать временные файлы для сортировки и объединения входных данных, объем которых превышает объем доступной оперативной памяти. - person Charles Duffy; 21.02.2012
comment
Согласен, если OP пытается сравнить файлы размером 1G+ каждый, у него могут возникнуть проблемы. Я не думал, что это необходимо учитывать, поскольку его файлы содержат 46 и 67 элементов. Если его файлы для сравнения станут чрезвычайно большими, ему, возможно, придется найти другой подход. - person jbranchaud; 21.02.2012
comment
Я думаю, вам нужен for other in "${others[@]}", если вы используете его как массив. Кроме того, вы можете добавлять к массивам как my_array+=( "new_item" ) в не древних версиях оболочки. - person Charles Duffy; 21.02.2012
comment
... также, поскольку вы используете bash, я бы рассмотрел (( match != 1 )) (контекст совпадения) или [[ $match != 1 ]] (контекст строки, но как ключевое слово оболочки без разделения строки на $match; в противном случае вы можете получить синтаксическую ошибку, если $match были пусты). И echo "$item", а не echo $item, чтобы избежать расширения содержимого вашей переменной. - person Charles Duffy; 21.02.2012
comment
... кстати, инициализируйте свои массивы (others=() или declare -a others) - person Charles Duffy; 21.02.2012
comment
эрг. математический контекст, не соответствующий контексту. - person Charles Duffy; 22.02.2012