Removendo arquivos duplicados

De Wiki Fedora

Imagine que você gosta muito de uma música e você tem várias versões e sem se perceber, há versões repetidas, mas com nomes diferentes. Imagine que você tenha vários arquivos com o mesmo nome ou tenha feito backup de seus arquivos em vários lugares diferentes. Bem vindo ao que era meu mundo.

Existe um programa chamado md5sum que cria uma assinatura digital daquele arquivo, é muito utilizado para verificar a integridade dos arquivos disponíveis para download nos repositórios de Linux. Se você baixa um arquivo e a assinatura é diferente da que está no site o arquivo está incompleto ou corrompido. Pelo algoritmo do md5sum, dificilmente você encontrará dois arquivos diferentes com a mesma assinatura. Obviamente só você tem uma seqüência de 32 dígitos em hexa, você está limitado à faixa dele.

Dá pra entender? Vamos ao exemplo. Digamos que eu tenha uma assinatura com 2 dígitos decimais (0 a 9) qual a faixa? É de 00 a 99, são 100 números diferentes. Isto é a base elevada à quantidade de dígitos (102). A assinatura do md5sum pode gerar 1632, isto é, 3,402823669 x 1038 assinaturas diferentes. Logo, num universo de 400.000.000.000.000.000.000.000.000.000.000.000.000 arquivos diferentes, você vai encontrar vários com a mesma assinatura, o que definitivamente não é o meu caso: só tenho 355.606 arquivos.

Tabela de conteúdo

Testando o md5sum

Se tiver curiosidade em testar o md5sum, execute esse simples comando:

$ md5sum teste.tex
ec72626869ba32a88dc538c73f483b68 teste.tex


O Script

Tudo o que eu tinha para fazer é encontrar uma maneira de testar a assinatura do md5sum de cada arquivo com a dos outros 355.605 existentes no meu computador. Para resolver isso, acabei encontrando na rede um script muito interessante, escrito em uma única linha:


OUTF=rem-duplicates.sh; echo "#! /bin/sh" > $OUTF; find "$@" -type f -print0 | xargs -0 -n1 md5sum | 
sort --key=1,32 | uniq -w 32 -d --all-repeated=separate | sed -r 's/^[0-9a-f]*( )*//;s/([^a-zA-Z0-9./
_-])/\\\1/g;s/(.+)/#rm \1/' >> $OUTF; chmod a+x $OUTF; ls -l $OUTF


Primeiro ele lista todos os arquivos do diretório onde ele está com o comando find, depois cria as assinaturas com o md5sum, ordena pela assinatura gerada, separa as assinaturas duplicadas, joga no arquivo rem-duplicates.sh a lista com os nomes dos arquivos antepostos por #rm e com contra-barra para caracteres especiais. Como abaixo:

#! /bin/sh
#rm /home/mythus/Docs/Direito/Penal/Principio\ da\ Insignificância\(resumo\).odt
#rm /home/mythus/Backups/Direito/Res_Princ._da_Insignificância.odt
#rm /home/mythus/temp.odt

#rm /home/mythus/Audio/Blackbird.mp3
#rm /home/mythus/Audio/Beatles-Blackbird.mp3

Depois é só abrir o arquivo e descomentar as linhas que você quer apagar. E rodar o rem-duplicates.sh.

Melhorando o Script

Quase supre minha necessidade. O que eu gostaria é de poder passar os diretórios nos quais ele devesse procurar (/home/user1, /var/ftp/pub, /media/winntfs, /media/winfat, etc) e do jeito que funciona ele vê apenas de uma raiz comum e não seria muito prudente executar a partir de / porque lá também existe /proc e outros diretórios cuja varredura seria inútil.

Daí eu alterei para:

OUTF=rem-duplicates.sh; echo "Digite o(s) caminho(s) (cuidado com espaços e caracteres especiais):" ;
read CAMINHO ; echo "#! /bin/sh" > $OUTF; find $CAMINHO "$@" -type f -print0 | xargs -0 -n1 md5sum |
sort --key=1,32 | uniq -w 32 -d --all-repeated=separate | sed -r 's/^[0-9a-f]*( )*//;s/([^a-zA-Z0-9./
_-])/\\\1/g;s/(.+)/#rm \1/' >> $OUTF; chmod a+x $OUTF; ls -l $OUTF

Resolvido. Mas a solução foi meio burra. O read espera qualquer coisa digitada até ser enviada com , mas não tem o auto-completar pra caminhos longos e, se eu digitar um espaço ou uma letra errada, vai procurar algo que não existe. Como contornar isso?

O ideal seria passar os diretórios que eu gostaria de varrer antes, ainda no shell, como parâmetros e aproveitando-a para auto-completar os diretórios. Então criei o arquivo duplicados.sh, logo abaixo.

#!/bin/sh
OUTF=rem-duplicates.sh
echo "#! /bin/sh" > $OUTF
find "$@" -type f -print0 |\
xargs -0 -n1 md5sum |\
sort --key=1,32 |\
uniq -w 32 -d --all-repeated=separate |\
sed -r 's/^[0-9a-f]*( )*//;s/([^a-zA-Z0-9./_-])/\\1/g;s/(.+)/#rm \1/' >> $OUTF
chmod a+x $OUTF

Depois tornei-o executável:

$ chmod a+x duplicados.sh

Assim ficou fácil:

$ ./duplicados.sh /dir1 /dir/2 /d/i/r/3

No shell existem variáveis pré-programadas como $1, $2, $3, $@, (1º parâmetro, 2º parâmetro, 3º parâmetro, todos os parâmetros, respectivamente). Na verdade o que tive que fazer foi voltar ao script original que já usava a variável $@.

Contribuições e Agradecimentos

Seguindo a linha da GPL, quem me ajudou a chegar nessa solução foi Jarno Elonen (autor do script) e Andrei Formiga (que me deu muitas luzes enquanto discutíamos sobre como passar os parâmetros para o script).

Ferramentas pessoais