Я хочу найти дубликаты файлов в каталоге, а затем удалить все, кроме одного, чтобы освободить место. Как мне добиться этого с помощью скрипта оболочки?
Например:
pwd
folder
Файлы в нем:
log.bkp
log
extract.bkp
extract
Мне нужно сравнить log.bkp со всеми остальными файлами, и если будет найден дубликат файла (по его содержимому), мне нужно удалить его. Аналогично, файл 'log' должен быть проверен со всеми остальными файлами, которые следуют за ним, и так далее.
Пока что я написал это, но это не дало желаемого результата.
#!/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
решение1
Если вы готовы просто использовать инструмент командной строки и не хотите создавать скрипт оболочки, fdupes
в большинстве дистрибутивов доступна соответствующая программа.
Существует также fslint
инструмент на основе графического интерфейса, обладающий той же функциональностью.
решение2
Это решение найдет дубликаты за время O(n). Для каждого файла генерируется контрольная сумма, и каждый файл в свою очередь сравнивается с набором известных контрольных сумм через ассоциативный массив.
#!/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
Если вы не укажете никаких файлов (или подстановочных знаков) в командной строке, то будет использован набор файлов в текущем каталоге. Он будет сравнивать файлы в нескольких каталогах, но он не написан для рекурсии в сами каталоги.
«Первый» файл в наборе всегда считается окончательной версией. Не принимаются во внимание время файла, разрешения или владельцы. Рассматривается только содержимое.
Удалите echo
из rm -f "$file"
строки, когда вы уверены, что она делает то, что вам нужно. Обратите внимание, что если вы замените эту строку на , ln -f "${filecksums[$cksum]}" "$file"
вы можете сделать жесткую ссылку на содержимое. Та же экономия на диске, но вы не потеряете имена файлов.
решение3
Основная проблема в вашем скрипте, похоже, в том, что он i
принимает фактические имена файлов как значения, в то время как j
это просто число. Передача имен в массив и использование i
и j
в качестве индексов должно работать:
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
(Кажется, работает с Bash и ksh
/ ksh93
Debian.)
Присвоение a=(this that)
инициализирует массив a
двумя элементами this
и that
(с индексами 0 и 1). Разделение слов и подстановка работают как обычно, поэтому files=(*)
инициализируются files
именами всех файлов в текущем каталоге (кроме файлов с точками). "${files[@]}"
будет расширяться до всех элементов массива, а знак решетки запрашивает длину, поэтому ${#files[@]}
это количество элементов в массиве. (Обратите внимание, что это ${files}
будет первый элемент массива, а ${#files}
это длина первого элемента, а не массива!)
for i in `/folder/*`
Обратные кавычки здесь, наверное, опечатка? Вы бы запустили первый файл как команду, а остальные передавали бы ей в качестве аргументов.
решение4
Кстати, использование контрольной суммы или хэша — хорошая идея. Мой скрипт их не использует. Но если файлы маленькие и количество файлов не большое (например, 10-20 файлов), этот скрипт будет работать довольно быстро. Если у вас 100 файлов и больше, по 1000 строк в каждом файле, то время будет больше 10 секунд.
Использование: ./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
Тестирование
Создать случайные файлы: ./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
Бегать ./duplicate_removing.sh files/*
и получить вывод
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