Como localizar e excluir arquivos duplicados no mesmo diretório?

5

Eu quero encontrar arquivos duplicados, dentro de um diretório, e depois excluir todos, exceto um, para recuperar espaço. Como faço para conseguir isso usando um script de shell?

Por exemplo:

pwd
folder

Os arquivos nela são:

log.bkp
log
extract.bkp
extract

Eu preciso comparar log.bkp com todos os outros arquivos e se um arquivo duplicado for encontrado (por seu conteúdo), eu preciso deletá-lo. Da mesma forma, o arquivo 'log' deve ser verificado com todos os outros arquivos, que se seguem, e assim por diante.

Até agora, escrevi isso, mas não está dando o resultado desejado.

#!/usr/bin/env ksh
count='ls -ltrh /folder | grep '^-'|wc -l'
for i in '/folder/*'
do
   for (( j=i+1; j<=count; j++ ))
   do
      echo "Current two files are $i and $j"
      sdiff -s $i  $j
      if [ 'echo $?' -eq  0 ]
      then
         echo "Contents of $i and $j are same"
       fi
    done
 done
    
por Su_scriptingbee 28.05.2017 / 16:41

7 respostas

7

Esta solução encontrará duplicatas no tempo O (n). Cada arquivo tem uma soma de verificação gerada para ele, e cada arquivo, por sua vez, é comparado ao conjunto de checksums conhecidos por meio de um array associativo.

#!/bin/bash
#
# Usage:  ./delete-duplicates.sh  [<files...>]
#
declare -A filecksums

# No args, use files in current directory
test 0 -eq $# && set -- *

for file in "$@"
do
    # Files only (also no symlinks)
    [[ -f "$file" ]] && [[ ! -h "$file" ]] || continue

    # Generate the checksum
    cksum=$(cksum <"$file" | tr ' ' _)

    # Have we already got this one?
    if [[ -n "${filecksums[$cksum]}" ]] && [[ "${filecksums[$cksum]}" != "$file" ]]
    then
        echo "Found '$file' is a duplicate of '${filecksums[$cksum]}'" >&2
        echo rm -f "$file"
    else
        filecksums[$cksum]="$file"
    fi
done

Se você não especificar nenhum arquivo (ou curinga) na linha de comando, ele usará o conjunto de arquivos no diretório atual. Ele irá comparar arquivos em vários diretórios, mas não é escrito para recorrer aos próprios diretórios.

O "primeiro" arquivo do conjunto é sempre considerado a versão definitiva. Nenhuma consideração é tomada em relação a tempos de arquivo, permissões ou propriedades. Apenas o conteúdo é considerado.

Remova o echo da linha rm -f "$file" quando tiver certeza de que faz o que deseja. Observe que, se você substituir essa linha por ln -f "${filecksums[$cksum]}" "$file" , poderá vincular o conteúdo com dificuldade. Mesmo salvando em espaço em disco, mas você não perderia os nomes dos arquivos.

    
por 28.05.2017 / 23:24
6

Se você estiver satisfeito em simplesmente usar uma ferramenta de linha de comando e não precisar criar um script de shell, o programa fdupes estará disponível na maioria das distros para fazer isso.

Há também a ferramenta fslint baseada em GUI que tem a mesma funcionalidade.

    
por 28.05.2017 / 22:19
2

O principal problema em seu script parece ser que i considera os nomes dos arquivos como valores, enquanto j é apenas um número. Levar os nomes para uma matriz e usar i e j como índices deve funcionar:

files=(*)
count=${#files[@]}
for (( i=0 ; i < count ;i++ )); do 
    for (( j=i+1 ; j < count ; j++ )); do
        if diff -q "${files[i]}" "${files[j]}"  >/dev/null ; then
            echo "${files[i]} and ${files[j]} are the same"
        fi
    done
done

(Parece funcionar com o Bash e o ksh / ksh93 do Debian tem.)

A atribuição a=(this that) inicializaria a matriz a com os dois elementos this e that (com os índices 0 e 1). Wordsplit e globbing funcionam normalmente, portanto files=(*) inicializa files com os nomes de todos os arquivos no diretório atual (exceto dotfiles). "${files[@]}" se expande para todos os elementos da matriz, e o sinal de hash pede um comprimento, portanto ${#files[@]} é o número de elementos na matriz. (Observe que ${files} seria o primeiro elemento da matriz e ${#files} é o comprimento do primeiro elemento, não da matriz!)

for i in '/folder/*'

Os backticks aqui são certamente um erro de digitação? Você estaria executando o primeiro arquivo como um comando e dando o resto como argumentos para ele.

    
por 28.05.2017 / 16:58
1

Existem ferramentas que fazem isso e fazem isso de maneira mais eficiente. Sua solução quando está funcionando é O (n²) que é o tempo que leva para executar é proporcional a n² onde n é o tamanho do problema no total de bytes em arquivos. O melhor algoritmo poderia fazer isso perto de O (n). (Estou discutindo a notação big-O, uma maneira de resumir a eficiência de um algoritmo.)

Primeiro você criaria um hash de cada arquivo, e só os compara: isso economiza muito tempo se você tiver muitos arquivos grandes que são quase os mesmos.

Em segundo lugar, você usaria métodos de atalho: se os arquivos tiverem tamanhos diferentes, eles não serão os mesmos. A menos que haja outro arquivo do mesmo tamanho, nem mesmo abra-o.

    
por 28.05.2017 / 18:58
0

A propósito, usar checksum ou hash é uma boa ideia. Meu script não usa. Mas se os arquivos forem pequenos e a quantidade de arquivos não for grande (como 10 a 20 arquivos), esse script funcionará bem rápido. Se você tem 100 arquivos e mais, 1000 linhas em cada arquivo, que o tempo, serão mais de 10 segundos.

Uso: ./duplicate_removing.sh files/*

#!/bin/bash

for target_file in "$@"; do
    shift
    for candidate_file in "$@"; do
        compare=$(diff -q "$target_file" "$candidate_file")
        if [ -z "$compare" ]; then
            echo the "$target_file" is a copy "$candidate_file"
            echo rm -v "$candidate_file"
        fi
    done
done

Teste

Crie arquivos aleatórios: ./creating_random_files.sh

#!/bin/bash

file_amount=10
files_dir="files"

mkdir -p "$files_dir"

while ((file_amount)); do
    content=$(shuf -i 1-1000)
    echo "$RANDOM" "$content" | tee "${files_dir}/${file_amount}".txt{,.copied} > /dev/null
    ((file_amount--))
done

Executar ./duplicate_removing.sh files/* e obter resultados

the files/10.txt is a copy files/10.txt.copied
rm -v files/10.txt.copied
the files/1.txt is a copy files/1.txt.copied
rm -v files/1.txt.copied
the files/2.txt is a copy files/2.txt.copied
rm -v files/2.txt.copied
the files/3.txt is a copy files/3.txt.copied
rm -v files/3.txt.copied
the files/4.txt is a copy files/4.txt.copied
rm -v files/4.txt.copied
the files/5.txt is a copy files/5.txt.copied
rm -v files/5.txt.copied
the files/6.txt is a copy files/6.txt.copied
rm -v files/6.txt.copied
the files/7.txt is a copy files/7.txt.copied
rm -v files/7.txt.copied
the files/8.txt is a copy files/8.txt.copied
rm -v files/8.txt.copied
the files/9.txt is a copy files/9.txt.copied
rm -v files/9.txt.copied
    
por 29.05.2017 / 02:17
0

O que você acha desta pequena solução:

for file in *; do
    find . -type f ! -name "$file" -exec sh -c ' cmp -s '"$file"' {} && echo rm {} ' \;
done

O cmp está comparando dois arquivos byte by byte e não reportará nada se ambos os arquivos forem iguais e se o erro for diferente. O -s é usado para o resultado silencioso.

Observação : remova echo para executar a exclusão de arquivos duplicados ou use mv -t /path/to/check/again {} para mover arquivos duplicados para outro diretório para verificar novamente primeiro.

para testar:

==> file1 <==
this is a $
file1$

==> file2 <==
this is a file2$

==> file3 <==
this is file1 a$

==> file4 <==
thisisafile1$

==> filex <==
this is a $
file1$

==> filey <==
this is a file2$

Resultado:

$ ls
file1  file2  file3  file4
    
por 15.04.2018 / 10:09
-1

Você pode usar finddup para isso. Leia isso!!! link

    
por 01.12.2017 / 22:51