All pastes #829624 Raw Edit

duplicate files script

public text v1 · immutable
#829624 ·published 2007-12-23 09:39 UTC
rendered paste body
#! /usr/bin/perl

# This script parses a whole directory tree for duplicate files. It uses an
# efficient method of checking filesizes first, and hashing only when
# filesizes match. It returns files that are duplicates in groups delineated
# by blank lines to stdout. Man, that sounds nerdy.
#
# Optional first argument is the directory tree to scan. Defaults to cwd.
# Optional remaining arguments are filename regexes to exclude. Defaults to 
# null.
#

# Force me to write this properly.
use warnings;
use strict;

# Get the find and current working directory modules.
use File::Find;
use Cwd;

# Initialise global directory name scalar.
my $dir;

# Initialise a couple of important global-scope hashes.
my ( %sizes, %matches );

# Open output as stdout. You could change this, but it's easier just to pipe it.
open OUTPUT, '>>', '/dev/stdout';

# If no arguments, work with the current working directory.
if ( ! $ARGV[0] )
{
	$dir = cwd;
}

# If arguments, check for existence, then confirm as directory to scan.
elsif ( -d $ARGV[0] )
{
	$dir = shift @ARGV;
}

# If it doesn't exist, halt.
else
{
	print 'Directory '.$ARGV[0].' not found.'."\n";
	exit 1;
}

# Any further arguments are regex exclusions. This could very well be null. That's fine.
my @exclusions = @ARGV;

# Declare a sub that returns filesizes.
sub filesize
{
	return ( stat( shift ) )[7];
}

# Declare a sub that returns inodes.
sub inode
{
	return ( stat( shift ) )[1];
}

# Declare a sub that returns checksums.
sub checksum
{
	my $file = shift;
	return substr( `md5sum "$file"`, 0, 32 );
}

# Declare the wanted sub ...
sub wanted
{
	# If it's a file ...
	if ( -f $File::Find::name )
	{
		# Just to make this easier ...
		my $file = $File::Find::name;
		
		# Check it doesn't match any exclusions.
		my $exclude = 0;
		foreach my $exclusion ( @exclusions )
		{
			$exclude = 1 if ( $file =~ m/$exclusion/ );
		}

		# No? Good! Start processing it.
		if ( ! $exclude )
		{
			# Get filesize and add it to hash.
			my $size = filesize( $file );
			$sizes{ $file } = $size;
		
			# Start a matches array reference.
			$matches{ $file } = [ ];
		}
	}
}

# ... and start the find process on the directory!
find \&wanted, $dir;

# Get a sorted list of the sizes keys by filesize. This groups them nicely.
my @files_sorted = sort {$sizes{$a} <=> $sizes{$b}} keys %sizes;

# Start with an impossible filesize and a null filename.
my $any_matches = 0;
my $current_size = -1;
my $current_matching;

# For each of the files in that sorted array,
foreach my $file ( @files_sorted )
{
	# ... check that:
	if (
		# The filesizes according to the %sizes hash match;
		$sizes{$file} == $current_size
			and 
		# ... they aren't the same inode, in which case it's probably an intentional hard link;
		inode( $current_matching ) != inode( $file )
			and
		# ... the MD5 hashes match.
		checksum( $current_matching ) eq checksum( $file )
	)
	{
		# If so, conclude it's a duplicate, and add it to the matches for this file.
		$any_matches = 1;
		push @{$matches{$current_matching}}, $file;
	}

	# Otherwise, start a new group and keep iterating.
	else
	{
		$current_matching = $file;
		$current_size = $sizes{$file}
	}
}

# The flag for any matches is still false? Success!
if ( ! $any_matches )
{
	print 'No matches.', "\n";
	exit 0;
}

# We don't need these now.
undef $any_matches;
undef $current_matching;
undef $current_size;
undef $dir;
undef @exclusions;
undef @files_sorted;
undef %sizes;

# Start printing some output.
print OUTPUT "\n", 'Summary of matches:', "\n\n";

# Print each group of duplicate files!
foreach my $match ( keys %matches )
{
	# Glean the matches for this file by dereferencing that array reference from the matches hash.
	my @this_matches = @{$matches{$match}};

	# If there are matches, print them.
	if ( $#this_matches > -1 )
	{
		print OUTPUT $match, "\n";
		print OUTPUT $_, "\n" foreach ( @this_matches );
		print OUTPUT "\n";
	}
}

# Close the output, whatever it is, and we're done.
close OUTPUT;
exit 0;