use File::Find; use File::Spec::Functions; #for perl2exe use File::Spec::Unix; use strict; use vars qw($A $B $a $attrib $content $dir $dirty $f $n $patchdir); unless( scalar @ARGV == 2){ print STDERR qq(usage: dirdiff [-f] [-A] [-B] [-a|-n] [-dir] [-attrib[=2 |4|6]] [-content[=2]] [-patchdir] [-dirty] dir1 dir2 Compare two directories -A Specify filename to use for dir1 checksums. -B Specify filename to use for dir2 checksums. -a An alias for: -dir -content=2 -attrib=4 -attrib Files are deemed to be different if any of (mode, uid, gid, size) is different. If the 4's bit is set, the differences in the attribs are displayed If the 2's bit is set, the differences in the mtime are displayed -content Display which files have been modified (determined by MD5 checksum) If equal to 2 the output of diff -u is included -dir Display which files have been added/removed. -dirty Work in a dirty/mixed environment. Files in the dir1 and dir2 proper are excluded from checking. -f Force new checksums to be calculated. -n An alias for: -dir -content -patchdir A directory to save the individual patches from -content saved to. NOTE: dir1 and dir2 MUST be fully qualified paths for -attrib to work. NOTE: You may encounter problems with paths containing spaces. ); exit 0; } #PREP my(%alpha, %beta, %diff); my $PWD = $ENV{PWD} || `pwd`; #need some portable method chomp($PWD); my $alpha = $A; #Default non-qualified filenames to PWD unless( $alpha ){ $alpha = $ARGV[0]; $alpha =~ s%/$%%; $alpha =~ tr[/][%]; $alpha =~ s/\.\./,,/g; } unless( $alpha =~ m%^/.*/% ){ $alpha = "$PWD/$alpha"; } my $beta = $B; #Default non-qualified filenames to PWD unless( $beta ){ $beta = $ARGV[1]; $beta =~ s%/$%%; $beta =~ tr[/][%]; $beta =~ s/\.\./,,/g; } unless( $beta =~ m%^/.*/% ){ $beta = "$PWD/$beta"; } if( $a ){ $dir |= 1; $content |= 2; $attrib |= 4; } if( $n ){ $dir |= 1; $content |= 1; } { my %found; foreach my $prog ( ('md5sum', 'patch') ){ foreach my $dir ( split(':', $ENV{PATH}) ){ $found{$prog}++ if -e "$dir/$prog"; } } unless( $found{md5sum} ){ print STDERR "dirdiff: md5sum is not your path, exiting\n\n"; exit 0; } unless( $found{patch} ){ print STDERR "dirdiff: patch is not your path, disbaling -content\n\n"; $content = $content ? 1 : 0; $patchdir = 0; } } if( $patchdir ){ die("-patchdir requires -content=2") unless $content>1; mkdir($patchdir,0755) unless -d $patchdir; } ##DATA ACCQUISITION if( ! -s $alpha || $f ){ my(@F, @G, %ARC); chdir($ARGV[0]); if( $dirty ){ opendir(DIR, "."); %ARC = map { sprintf("./%s", $_) => 1 } readdir(DIR); close(DIR); } find(sub{ push @G, $File::Find::name if -d $_; push @F, $File::Find::name if -f $_;}, "."); if( $dirty ){ @F = grep(!exists($ARC{$_}), @F); } my $files = join(" ", @F); #potential overflow... `md5sum $files > $alpha`; open(OUT, ">>$alpha"); print OUT map("I_AM_A_DIRECTORY0123456789abcdef $_\n", @G); close(OUT); } open(IN,$alpha); while(){ my($val, $key)=split(/\s+/, $_); $alpha{$key}=$val; } close(IN); chdir($PWD); if( ! -s $beta || $f ){ my(@F, @G, %ARC); chdir($ARGV[1]); if( $dirty ){ opendir(DIR, "."); %ARC = map { sprintf("./%s", $_) => 1 } readdir(DIR); close(DIR); } find(sub{ push @G, $File::Find::name if -d $_; push @F, $File::Find::name if -f $_;}, "."); if( $dirty ){ @F = grep(!exists($ARC{$_}), @F); } my $files = join(" ", @F); #potential overflow... `md5sum $files > $beta`; open(OUT, ">>$beta"); print OUT map("I_AM_A_DIRECTORY0123456789abcdef $_\n", @G); close(OUT); } open(IN,$beta); while(){ my($val, $key)=split(/\s+/, $_); $beta{$key}=$val; } close(IN); ##CHECKS if( $dir ){ foreach my $key0 ( sort {$a cmp $b} keys %alpha ){ print "----- ", canonpath("$ARGV[0]/$key0"), "\n" unless $beta{$key0}; } foreach my $key1 ( sort {$a cmp $b} keys %beta ){ $_ = canonpath("$ARGV[1]/$key1"); if( -d $_ ){ $_ = "mkdir $_"; } print "+++++ ", $_, "\n" unless $alpha{$key1}; } } if( $content ){ foreach my $key ( sort {$a cmp $b} keys %alpha ){ if( exists($beta{$key}) && $alpha{$key} ne $beta{$key} ){ my $fileB = canonpath("$ARGV[1]/$key"); print "~~~~~ $fileB\n"; my $fileA = canonpath("$ARGV[0]/$key"); if( $content > 1 ){ $diff{$fileB} = 1; my $patchContent = `diff -u $fileA $fileB`; print $patchContent; if( $patchdir ){ my $patchName = $key; $patchName =~ s/^\.//; $patchName =~ tr[/][%]; open(OUT, ">$patchdir/$patchName"); print OUT $patchContent; } } } } } if( $attrib ){ my @check = $attrib%4==2 ? (2,4,5,7,9) : (2,4,5,7); foreach my $key ( sort {$a cmp $b} keys %beta ){ if( -d canonpath("$ARGV[1]/$key") ){ $alpha{$key} = $beta{$key}; } } foreach my $key ( sort {$a cmp $b} keys %alpha ){ my @F = (stat("$ARGV[0]/$key"))[@check]; my @G = (stat("$ARGV[1]/$key"))[@check]; if( exists($beta{$key}) && (join(',', @F) ne join(',', @G) ) ){ my $fileB = canonpath("$ARGV[1]/$key"); my $fileA = canonpath("$ARGV[0]/$key"); if( $attrib > 1 ){ if( ! (($attrib%4)%2) ){ if( $G[0] != $F[0] ){ $F[0]%=16384; $G[0]%=16384; # if( -f $fileB){ # $F[0]-=32768; $G[0]-=32768; } # if( -d $fileB ){ # $F[0]-=16384; $G[0]-=16384; } printf "~~~~~ chmod %o $fileB #%o\n", $G[0], $F[0]; } if( $G[1] != $F[1] || $G[2] != $F[2] ){ print "~~~~~ chown $G[1]:$G[2] $fileB #$F[1]:$F[2]\n"; } if( $G[3] != $F[3] ){ print "~~~~~ $fileB #Size differs!\n" unless $diff{$file B}; } } if( $G[4] != $F[4] && $attrib%4 ){ print qq(~~~~~ touch -m --date="), scalar localtime($G[4]), qq(" $fileB #), scalar localtime($F[4]), "\n"; } } else{ print "~~~~~ $fileB\n"; } } } } if( $patchdir ){ print STDERR qq( You may wish to include in your installer for sharc something along the lines of: cd $patchdir; for file in *; do file2=`echo \$file | tr % /` /usr/bin/patch "/\$file2" \$file done cd ..; assuming $patchdir is a subdirectory of the source directory for sharc and the patch files have been renamed to match the files they are meant to patch with / being replaced by %. ) }