найти и удалить дубликаты в каталоге

12

У меня есть каталог с несколькими файлами IMG, и некоторые из них идентичны, но все они имеют разные имена. Мне нужно удалить дубликаты, но без внешних инструментов только с помощью bashскрипта. Я новичок в Linux. Я пытался использовать вложенный цикл для сравнения md5сумм и в зависимости от результата удаления, но что-то не так с синтаксисом, и это не работает. любая помощь?

что я пробовал это ...

for i in directory_path; do
    sum1='find $i -type f -iname "*.jpg" -exec md5sum '{}' \;'
    for j in directory_path; do
        sum2='find $j -type f -iname "*.jpg" -exec md5sum '{}' \;'
        if test $sum1=$sum2 ; then rm $j ; fi
    done
done

Я получил: test: too many arguments

linuxbegin
источник
Пожалуйста, включите любые сообщения об ошибках, которые вы получите в своем вопросе.
Тердон
Почему вы не можете использовать внешние инструменты, такие как fdupes? Ответ @terdon удивителен, но он действительно подчеркивает, почему использование хорошего инструмента - это путь, если это возможно. Если это какое-то специальное оборудование или сервер, вы все равно сможете получить к нему доступ по сети и т. Д. С компьютера, на котором есть такие инструменты, как fdupes.
Джо

Ответы:

28

В вашем скрипте довольно много проблем.

  • Во-первых, чтобы присвоить результат команды переменной, необходимо заключить ее в backtics ( `command`) или, предпочтительно, в $(command). У вас есть это в одинарных кавычках ( 'command'), которые вместо назначения результата вашей команды вашей переменной, назначают саму команду в виде строки. Таким образом, ваш testна самом деле:

    $ echo "test $sum1=$sum2"
    test find $i -type f -iname "*.jpg" -exec md5sum {} \;=find $j -type f -iname "*.jpg" -exec md5sum {} \;
  • Следующая проблема заключается в том, что команда md5sumвозвращает больше, чем просто хэш:

    $ md5sum /etc/fstab
    46f065563c9e88143fa6fb4d3e42a252  /etc/fstab

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

    find $i -type f -iname "*.png" -exec md5sum '{}' \; | cut -f 1 -d ' '

    или

    find $i -type f -iname "*.png" -exec md5sum '{}' \; | awk '{print $1}' 
  • Кроме того, findкоманда будет возвращать много совпадений, а не только одно, и каждое из этих совпадений будет дублироваться вторым find. Это означает, что в какой-то момент вы будете сравнивать один и тот же файл с самим собой, md5sum будет идентичен, и вы в конечном итоге удалите все ваши файлы (я запустил это на тестовой директории, содержащей a.jpgи b.jpg):

    for i in $(find . -iname "*.jpg"); do
      for j in $(find . -iname "*.jpg"); do
         echo "i is: $i and j is: $j"
      done
    done   
    i is: ./a.jpg and j is: ./a.jpg   ## BAD, will delete a.jpg
    i is: ./a.jpg and j is: ./b.jpg
    i is: ./b.jpg and j is: ./a.jpg
    i is: ./b.jpg and j is: ./b.jpg   ## BAD will delete b.jpg
  • Вы не хотите запускаться, for i in directory_pathесли вы не передаете массив каталогов. Если все эти файлы находятся в одном каталоге, вы хотите запустить for i in $(find directory_path -iname "*.jpg"), чтобы просмотреть все файлы.

  • Это плохая идея использовать forциклы с выводом find. Вы должны использовать whileпетли или сглаживание :

    find . -iname "*.jpg" | while read i; do [...] ; done

    или, если все ваши файлы находятся в одном каталоге:

    for i in *jpg; do [...]; done

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

  • Наконец, вы также должны заключить в кавычки ваши переменные, иначе пути к каталогам с пробелами сломают ваш скрипт.

Имена файлов могут содержать пробелы, новые строки, обратную косую черту и другие странные символы, чтобы правильно обрабатывать их в whileцикле, вам потребуется добавить еще несколько параметров. То, что вы хотите написать, это что-то вроде:

find dir_path -type f -iname "*.jpg" -print0 | while IFS= read -r -d '' i; do
  find dir_path -type f -iname "*.jpg" -print0 | while IFS= read -r -d '' j; do
    if [ "$i" != "$j" ]
    then
      sum1=$(md5sum "$i" | cut -f 1 -d ' ' )
      sum2=$(md5sum "$j" | cut -f 1 -d ' ' )
      [ "$sum1" = "$sum2" ] && rm "$j"
    fi
  done
done

Еще более простой способ будет:

find directory_path -name "*.jpg" -exec md5sum '{}' + | 
 perl -ane '$k{$F[0]}++; system("rm $F[1]") if $k{$F[0]}>1'

Лучшая версия, которая может иметь дело с пробелами в именах файлов:

find directory_path -name "*.jpg" -exec md5sum '{}' + | 
 perl -ane '$k{$F[0]}++; system("rm \"@F[1 .. $#F]\"") if $k{$F[0]}>1'

Этот маленький Perl-скрипт будет проходить через результаты findкоманды (т.е. md5sum и имя файла). -aВариант для perlрасколов входных линий пробельных и сохраняет их в Fмассиве, так $F[0]будет md5sum и $F[1]имя файла. Сумма md5 сохраняется в хэше, kи скрипт проверяет, был ли хэш уже просмотрен ( if $k{$F[0]}>1), и удаляет файл, если он имеет ( system("rm $F[1]")).


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

Тердон
источник
+1 за фрагмент Perl. Действительно элегантно! Вы также можете использовать Perl unlinkвместо systemзвонка.
Джозеф Р.
@JosephR. Благодарность :). Однако, если бы произошла ошибка, она не работала бы с именами файлов с пробелами, поскольку в них были бы только первые символы имени до первого пробела $F[1]. Исправлено с помощью кусочков массива. Что касается unlink (), я знаю, но хотел свести к минимуму perlisms, и системный вызов легче понять, если вы не знаете Perl.
Terdon
13

Существует отличная программа, fdupesкоторая упрощает весь процесс и предлагает пользователю удалить дубликаты. Я думаю, что стоит проверить:

$ fdupes --delete DIRECTORY_WITH_DUPLICATES
[1] DIRECTORY_WITH_DUPLICATES/package-0.1-linux.tar.gz        
[2] DIRECTORY_WITH_DUPLICATES/package-0.1-linux.tar.gz.1

Set 1 of 1, preserve files [1 - 2, all]: 1

   [+] DIRECTORY_WITH_DUPLICATES/package-0.1-linux.tar.gz
   [-] DIRECTORY_WITH_DUPLICATES/package-0.1-linux.tar.gz.1

По сути, мне было предложено сохранить файл , я набрал 1 , а второй удалил.

Другие интересные варианты:

-r --recurse
    for every directory given follow subdirectories encountered within

-N --noprompt
    when used together with --delete, preserve the first file in each set of duplicates and delete the others without prompting the user

Из вашего примера вы, вероятно, хотите запустить его как:

fdupes --recurse --delete --noprompt DIRECTORY_WITH_DUPLICATES

Смотрите man fdupesдля всех доступных вариантов.

Тереза ​​и Джуниор
источник