Quero encontrar arquivos duplicados, dentro de um diretório, e 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 nele são:
log.bkp
log
extract.bkp
extract
Preciso comparar log.bkp com todos os outros arquivos e se um arquivo duplicado for encontrado (por seu conteúdo), preciso excluí-lo. Da mesma forma, o arquivo 'log' deve ser verificado com todos os outros arquivos a seguir 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
Responder1
Se você quiser simplesmente usar uma ferramenta de linha de comando e não precisar criar um script de shell, o fdupes
programa está disponível na maioria das distros para fazer isso.
Há também a fslint
ferramenta baseada em GUI que possui a mesma funcionalidade.
Responder2
Esta solução encontrará duplicatas em tempo O(n). Cada arquivo tem uma soma de verificação gerada e cada arquivo, por sua vez, é comparado ao conjunto de somas de verificação conhecidas por meio de uma matriz associativa.
#!/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 curingas) na linha de comando, ele usará o conjunto de arquivos no diretório atual. Ele comparará arquivos em vários diretórios, mas não foi escrito para recorrer aos próprios diretórios.
O “primeiro” arquivo do conjunto é sempre considerado a versão definitiva. Nenhuma consideração é levada em consideração nos tempos, permissões ou propriedades dos arquivos. Apenas o conteúdo é considerado.
Remova o echo
da rm -f "$file"
linha quando tiver certeza de que ele faz o que deseja. Observe que se você substituísse essa linha por ln -f "${filecksums[$cksum]}" "$file"
você poderia vincular o conteúdo. A mesma economia de espaço em disco, mas você não perderia os nomes dos arquivos.
Responder3
O principal problema em seu script parece ser que i
os nomes reais dos arquivos são considerados valores, embora j
sejam apenas um número. Levar os nomes para um array e usar ambos 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 Bash e o ksh
/ ksh93
Debian tem.)
A atribuição a=(this that)
inicializaria o array a
com os dois elementos this
e that
(com índices 0 e 1). A divisão de palavras e o globbing funcionam normalmente, portanto files=(*)
inicializa files
com os nomes de todos os arquivos no diretório atual (exceto dotfiles). "${files[@]}"
se expandiria para todos os elementos da matriz, e o sinal de hash solicitaria um comprimento, assim como ${#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 crases aqui são certamente um erro de digitação? Você executaria o primeiro arquivo como um comando e forneceria o restante como argumentos.
Responder4
A propósito, usar checksum ou hash é uma boa ideia. Meu script não usa isso. 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ê tiver 100 arquivos ou mais, 1000 linhas em cada arquivo, o tempo será superior a 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
Correr ./duplicate_removing.sh files/*
e obter saída
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