localize 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 eles têm nomes diferentes. Eu preciso remover duplicatas, mas sem ferramentas externas apenas com um script bash . Eu sou um iniciante no Linux. Eu tentei aninhado para loop para comparar md5 somas e dependendo do resultado remover, mas algo está errado com a sintaxe e não funciona. alguma 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

    
por linuxbegin 24.11.2013 / 17:12

2 respostas

27

Existem alguns problemas no seu script.

  • Primeiro, para atribuir o resultado de um comando a uma variável, é necessário incluí-lo no backtics ( 'command' ) ou, preferencialmente, $(command) . Você tem isso entre aspas simples ( 'command' ), que ao invés de atribuir o resultado do seu comando à sua variável, atribui o próprio comando como uma string. Portanto, seu test é, na verdade:

    $ echo "test $sum1=$sum2"
    test find $i -type f -iname "*.jpg" -exec md5sum {} \;=find $j -type f -iname "*.jpg" -exec md5sum {} \;
    
  • A próxima questão é que o comando md5sum retorna mais do que apenas o hash:

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

    Você só deseja comparar o primeiro campo, portanto, deve analisar o md5sum output passando-o 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 comando find retornará muitas correspondências, não apenas uma e cada uma dessas correspondências será duplicada pela segunda find . Isso significa que em algum momento você estará comparando o mesmo arquivo a si mesmo, o md5sum será idêntico e você acabará excluindo todos seus arquivos (executei isso em um diretório de teste contendo a.jpg e 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 for i in directory_path , a menos 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 percorrer todos os arquivos.

  • É uma má idéia de usar for loops com a saída de encontrar. Você deve usar while loops 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 até para arquivos em subdiretórios, mas não vamos entrar aqui.

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

Os nomes dos arquivos podem conter espaços, novas linhas, barras invertidas e outros caracteres estranhos, para lidar com eles corretamente em um loop while , você precisará adicionar mais algumas opções. O que você quer 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 em nomes de arquivos:

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

Esse pequeno script Perl será executado pelos resultados do comando find (ou seja, o md5sum e o nome do arquivo). A opção -a para perl divide as linhas de entrada no espaço em branco e as salva na matriz F , portanto $F[0] será o md5sum e $F[1] o nome do arquivo. O md5sum é salvo no hash k e o script verifica se o hash já foi visto ( if $k{$F[0]}>1 ) e exclui o arquivo se tiver ( system("rm $F[1]") ).

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

por 24.11.2013 / 18:48
12

Existe um programa bacana chamado fdupes que simplifica todo o processo e solicita ao usuário a exclusão de duplicatas. 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 induziu para qual arquivo manter , eu digitei 1 e ele removeu 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 fdupes para todas as opções disponíveis.

    
por 24.11.2013 / 18:26