Quiero encontrar archivos duplicados, dentro de un directorio, y luego eliminarlos todos menos uno para recuperar espacio. ¿Cómo logro esto usando un script de shell?
Por ejemplo:
pwd
folder
Los archivos que contiene son:
log.bkp
log
extract.bkp
extract
Necesito comparar log.bkp con todos los demás archivos y, si se encuentra un archivo duplicado (por su contenido), debo eliminarlo. De manera similar, el archivo 'log' debe verificarse con todos los demás archivos siguientes, y así sucesivamente.
Hasta ahora he escrito esto, pero no está dando el resultado deseado.
#!/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
Respuesta1
Si está contento de usar simplemente una herramienta de línea de comandos y no tener que crear un script de shell, el fdupes
programa está disponible en la mayoría de las distribuciones para hacerlo.
También existe la fslint
herramienta basada en GUI que tiene la misma funcionalidad.
Respuesta2
Esta solución encontrará duplicados en tiempo O (n). Cada archivo tiene una suma de verificación generada y cada archivo, a su vez, se compara con el conjunto de sumas de verificación conocidas a través de una matriz asociativa.
#!/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
Si no especifica ningún archivo (o comodín) en la línea de comando, utilizará el conjunto de archivos en el directorio actual. Comparará archivos en varios directorios, pero no está escrito para que recurra a los directorios mismos.
El "primer" archivo del conjunto siempre se considera la versión definitiva. No se tienen en cuenta los tiempos, permisos o propiedades de los archivos. Sólo se considera el contenido.
Elimine el echo
de la rm -f "$file"
línea cuando esté seguro de que hace lo que desea. Tenga en cuenta que si reemplazara esa línea con ln -f "${filecksums[$cksum]}" "$file"
podría vincular el contenido. El mismo ahorro de espacio en disco pero no perderá los nombres de los archivos.
Respuesta3
El problema principal en su secuencia de comandos parece ser que i
toma los nombres de archivos reales como valores, mientras que j
son solo un número. Llevar los nombres a una matriz y usar ambos i
y j
como índices debería 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 con Bash y ksh
/ ksh93
Debian).
La asignación a=(this that)
inicializaría la matriz a
con los dos elementos this
y that
(con índices 0 y 1). La división de palabras y el globbing funcionan como de costumbre, por lo que files=(*)
se inicializan files
con los nombres de todos los archivos en el directorio actual (excepto los archivos de puntos). "${files[@]}"
se expandiría a todos los elementos de la matriz, y el signo de almohadilla solicita una longitud, al igual que ${#files[@]}
el número de elementos de la matriz. (Tenga en cuenta que ${files}
sería el primer elemento de la matriz, y ${#files}
es la longitud del primer elemento, ¡no la matriz!)
for i in `/folder/*`
¿Las comillas invertidas aquí seguramente son un error tipográfico? Estaría ejecutando el primer archivo como un comando y dándole el resto como argumentos.
Respuesta4
Por cierto, usar suma de comprobación o hash es una buena idea. Mi guión no lo usa. Pero si los archivos son pequeños y la cantidad de archivos no es grande (como entre 10 y 20 archivos), este script funcionará bastante rápido. Si tiene 100 archivos o más, 1000 líneas en cada archivo, el tiempo será de más 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
Pruebas
Crea archivos aleatorios: ./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/*
y obtener salida
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