ディレクトリ内の重複ファイルを検索し、1 つを除いてすべて削除してスペースを確保したいと考えています。シェル スクリプトを使用してこれを実現するにはどうすればよいでしょうか。
例えば:
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
同じ機能を備えたGUI ベースのツールもあります。
答え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
2 つの要素と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