encontre e remova duplicatas em um diretório

12

Eu tenho um diretório com vários arquivos img e alguns deles são idênticos, mas todos têm nomes diferentes. Preciso remover duplicatas, mas sem ferramentas externas, apenas com um bashscript. Eu sou iniciante no Linux. Eu tentei aninhado para loop para comparar md5somas e, dependendo do resultado remover, mas algo está errado com a sintaxe e não funciona. qualquer ajuda?

o que eu tentei é ...

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

Eu recebo: test: too many arguments

linuxbegin
fonte
Inclua também todas as mensagens de erro que você receber na sua pergunta.
terdon
Por que você não pode usar ferramentas externas como o fdupes? A resposta do @terdon é incrível, mas realmente destaca por que usar uma boa ferramenta é o caminho a seguir, se possível. Se for algum tipo de hardware ou servidor dedicado, você ainda poderá acessá-lo através de uma rede etc. de uma máquina que possui ferramentas como fdupes disponíveis.
30413 Joe

Respostas:

28

Existem alguns problemas no seu script.

  • Primeiro, para atribuir o resultado de um comando a uma variável, você deve incluí-lo em backtics ( `command`) ou, preferencialmente $(command),. Você o coloca entre aspas simples ( 'command') que, em vez de atribuir o resultado do seu comando à sua variável, atribui o próprio comando como uma string. Portanto, você testé realmente:

    $ echo "test $sum1=$sum2"
    test find $i -type f -iname "*.jpg" -exec md5sum {} \;=find $j -type f -iname "*.jpg" -exec md5sum {} \;
  • O próximo problema é que o comando md5sumretorna mais do que apenas o hash:

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

    Você deseja comparar apenas o primeiro campo, portanto, deve analisar a md5sumsaída passando-a por um comando que imprime apenas o primeiro campo:

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

    ou

    find $i -type f -iname "*.png" -exec md5sum '{}' \; | awk '{print $1}' 
  • Além disso, o findcomando retornará muitas correspondências, não apenas uma e cada uma dessas correspondências será duplicada a cada segundo find. Isso significa que em algum momento você estará comparando o mesmo arquivo, o md5sum será idêntico e você acabará excluindo todos os seus arquivos (eu executei isso em um diretório de teste contendo a.jpge 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
  • Você não deseja executar, a for i in directory_pathmenos que esteja passando uma matriz de diretórios. Se todos esses arquivos estiverem no mesmo diretório, você deseja executar for i in $(find directory_path -iname "*.jpg") para passar por todos os arquivos.

  • É uma má idéia usar forloops com a saída do find. Você deve usar whileloops ou globbing :

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

    ou, se todos os seus arquivos estiverem no mesmo diretório:

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

    Dependendo do seu shell e das opções que você definiu, você pode usar globbing mesmo para arquivos em subdiretórios, mas não vamos entrar aqui.

  • Por fim, você também deve citar suas variáveis, caso contrário, os caminhos de diretório com espaços quebrarão seu script.

Os nomes de arquivos podem conter espaços, novas linhas, barras invertidas e outros caracteres estranhos. Para lidar com aqueles corretamente em um whileloop, você precisará adicionar mais algumas opções. O que você deseja escrever é algo como:

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

Uma maneira ainda mais simples seria:

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

Uma versão melhor que pode lidar com espaços nos nomes dos arquivos:

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

Este pequeno script Perl executará os resultados do findcomando (ou seja, o md5sum e o nome do arquivo). A -aopção para perldividir as linhas de entrada no espaço em branco e salvá-las na Fmatriz, assim $F[0]será o md5sum e $F[1]o nome do arquivo. O md5sum é salvo no hash ke o script verifica se o hash já foi visto ( if $k{$F[0]}>1) e exclui o arquivo se tiver ( system("rm $F[1]")).


Embora isso funcione, será muito lento para grandes coleções de imagens e você não poderá escolher quais arquivos manter. Existem muitos programas que lidam com isso de uma maneira mais elegante, incluindo:

Terdon
fonte
+1 para o snippet Perl. Realmente elegante! Você também pode usar o próprio Perl em unlinkvez de fazer uma systemchamada.
Joseph R.
@JosephR. obrigado :). No entanto, se houvesse um erro, os nomes de arquivos com espaços falhariam, pois somente os primeiros caracteres de um nome até o primeiro espaço estariam $F[1]. Corrigido usando fatias de matriz. Quanto a unlink () eu sei, mas queria manter os perlismos no mínimo e a chamada do sistema é mais fácil de entender se você não conhece o Perl.
terdon
13

Existe um programa bacana chamado fdupesque simplifica todo o processo e solicita ao usuário a exclusão de duplicatas. Eu acho que vale a pena conferir:

$ 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

Basicamente, ele me solicitou qual arquivo manter , digitei 1 e removi o segundo.

Outras opções interessantes são:

-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

Do seu exemplo, você provavelmente deseja executá-lo como:

fdupes --recurse --delete --noprompt DIRECTORY_WITH_DUPLICATES

Veja man fdupespara todas as opções disponíveis.

Teresa e Junior
fonte