515 lines
14 KiB
Perl
515 lines
14 KiB
Perl
#!/usr/bin/perl -w
|
|
# $Id: slack-diff 122 2006-09-27 07:34:32Z alan $
|
|
# vim:sw=2
|
|
# vim600:fdm=marker
|
|
# Copyright (C) 2004-2006 Alan Sundell <alan@sundell.net>
|
|
# All Rights Reserved. This program comes with ABSOLUTELY NO WARRANTY.
|
|
# See the file COPYING for details.
|
|
#
|
|
# This script is a wrapper for diff that gives output about special files
|
|
# and file modes. (diff can only compare regular files)
|
|
|
|
require 5.006;
|
|
use warnings FATAL => qw(all);
|
|
use strict;
|
|
use sigtrap qw(die untrapped normal-signals
|
|
stack-trace any error-signals);
|
|
|
|
use Errno;
|
|
use File::stat;
|
|
use File::Basename;
|
|
use File::Find;
|
|
use Getopt::Long;
|
|
use POSIX qw(SIGPIPE strftime);
|
|
use Fcntl qw(:mode); # provides things like S_IFMT that POSIX does not
|
|
|
|
|
|
my $VERSION = '0.1';
|
|
(my $PROG = $0) =~ s#.*/##;
|
|
my @diff; # diff program to use
|
|
my $exit = 0; # our exit code
|
|
|
|
sub compare ($$);
|
|
sub recursive_compare ($$);
|
|
sub filetype_to_string ($;$);
|
|
sub compare_files ($$);
|
|
sub diff ($$);
|
|
|
|
########################################
|
|
# Environment
|
|
# Helpful prefix to die messages
|
|
$SIG{__DIE__} = sub { die "FATAL[$PROG]: @_"; };
|
|
# Set a reasonable umask
|
|
umask 077;
|
|
# Autoflush on STDOUT
|
|
$|=1;
|
|
# Autoflush on STDERR
|
|
select((select(STDERR), $|=1)[0]);
|
|
|
|
# Default options
|
|
my %opt = (
|
|
fakediff => 1,
|
|
perms => 1,
|
|
'new-file' => 1,
|
|
diff => 'diff',
|
|
);
|
|
|
|
# Config and option parsing
|
|
my $usage = <<EOF;
|
|
Usage: $PROG [options] <file1> <file2>
|
|
$PROG -r <dir1> <dir2>
|
|
|
|
Options:
|
|
-u, -U NUM, --unified=NUM
|
|
Tell diff to use unified output format.
|
|
--diff PROG
|
|
Use this program for diffing, instead of "$opt{diff}"
|
|
--fakediff
|
|
Make a fake diff for file modes and other things that are not file
|
|
contents. Default is on, can be disabled with --nofakediff.
|
|
--perms
|
|
Care about owner, group, and permissions when doing fakediff.
|
|
Default is on, can be disabled with --noperms.
|
|
-r, --recursive
|
|
Recursively compare directories.
|
|
-N, --new-file
|
|
Treat missing files as empty. Default is on, can be disabled with
|
|
--nonew-file.
|
|
--unidirectional-new-file
|
|
Treat only missing files in the first directory as empty.
|
|
--from-file
|
|
Treat arguments as a list of files from which to read filenames to
|
|
compare, two lines at a time.
|
|
-0, --null
|
|
Use NULLs instead of newlines as the separator in --from-file mode
|
|
--devnullhack
|
|
You have a version of diff that can't deal with -N when not in
|
|
recursive mode, so we need to feed it /dev/null instead of the
|
|
missing file. Default is on, can be disabled with --nodevnullhack.
|
|
--version
|
|
Output version info
|
|
--help
|
|
Output this help text
|
|
|
|
Exit codes:
|
|
0 Found no differences
|
|
1 Found a difference
|
|
2 Had a serious error
|
|
3 Found a difference and had a serious error
|
|
EOF
|
|
|
|
{
|
|
Getopt::Long::Configure ("bundling");
|
|
GetOptions(\%opt,
|
|
'help|h|?',
|
|
'version',
|
|
'null|0',
|
|
'devnullhack',
|
|
'new-file|N',
|
|
'u',
|
|
'unified|U=i',
|
|
'recursive|r',
|
|
'from-file',
|
|
'unidirectional-new-file',
|
|
'fakediff!',
|
|
'perms!',
|
|
'diff=s',
|
|
) or die $usage;
|
|
if ($opt{help}) {
|
|
print $usage;
|
|
exit 0;
|
|
}
|
|
if ($opt{version}) {
|
|
print "$PROG version $VERSION\n";
|
|
exit 0;
|
|
}
|
|
}
|
|
|
|
if ($opt{diff}) {
|
|
# We split on spaces here to be useful -- so that people can give
|
|
# their diff options.
|
|
@diff = split(/\s+/, $opt{diff});
|
|
} else {
|
|
die "$PROG: No diff program!\n";
|
|
}
|
|
|
|
if ($opt{'u'}) {
|
|
push @diff, '-u';
|
|
} elsif ($opt{'unified'}) {
|
|
$opt{'u'} = 1; # We use this value later
|
|
push @diff, "--unified=$opt{'unified'}";
|
|
}
|
|
|
|
if (not $opt{'devnullhack'}) {
|
|
push @diff, '-N';
|
|
}
|
|
|
|
# usually, sigpipe would be someone quitting their pager, so don't sweat it
|
|
$SIG{PIPE} = sub { exit $exit };
|
|
|
|
if ($opt{'from-file'}) {
|
|
local $/ = "\0" if $opt{'null'};
|
|
while (my $old = <>) {
|
|
my $new = <>;
|
|
die "Uneven number of lines in --from-file mode!\n"
|
|
if not defined $new;
|
|
chomp($old);
|
|
chomp($new);
|
|
$exit |= compare($old, $new);
|
|
}
|
|
} else {
|
|
die $usage unless $#ARGV == 1;
|
|
$exit |= compare($ARGV[0], $ARGV[1]);
|
|
}
|
|
exit $exit;
|
|
|
|
##
|
|
# Subroutines
|
|
|
|
sub compare ($$) {
|
|
my ($old, $new) = @_;
|
|
|
|
if ($opt{recursive}) {
|
|
return recursive_compare($old, $new);
|
|
} else {
|
|
return compare_files($old, $new);
|
|
}
|
|
}
|
|
|
|
# compare two directories. We do this by walking down the *new*
|
|
# directory, and comparing everything that's there to the stuff in
|
|
# the old directory
|
|
sub recursive_compare ($$) {
|
|
my ($olddir, $newdir) = @_;
|
|
my ($retval, $basere, $wanted);
|
|
my (%seen);
|
|
|
|
$retval = 0;
|
|
|
|
if (-d $newdir) {
|
|
$basere = qr(^$newdir);
|
|
$wanted = sub {
|
|
my ($newfile) = $_;
|
|
my $oldfile = $newfile;
|
|
|
|
$oldfile =~ s#$basere#$olddir#;
|
|
$seen{$oldfile} = 1;
|
|
$retval |= compare_files($oldfile, $newfile);
|
|
};
|
|
|
|
eval { find({ wanted => $wanted , no_chdir => 1}, $newdir) };
|
|
if ($@) {
|
|
warn "$PROG: error during find: $@\n";
|
|
$retval |= 2;
|
|
}
|
|
}
|
|
return $retval
|
|
if $opt{'unidirectional-new-file'};
|
|
|
|
# If we're not unidirectional, we want to go through the old directory
|
|
# and diff any files we didn't see in the newdir.
|
|
if (-d $olddir) {
|
|
$basere = qr(^$olddir);
|
|
$wanted = sub {
|
|
my ($oldfile) = $_;
|
|
my $newfile;
|
|
|
|
return if $seen{$oldfile};
|
|
$newfile = $oldfile;
|
|
|
|
$newfile =~ s#$basere#$newdir#;
|
|
$retval |= compare_files($oldfile, $newfile);
|
|
};
|
|
|
|
eval { find({ wanted => $wanted , no_chdir => 1}, $olddir) };
|
|
if ($@) {
|
|
warn "$PROG: error during find: $@\n";
|
|
$retval |= 2;
|
|
}
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
# filetype_to_string(mode)
|
|
# filetype_to_string(mode, plural)
|
|
#
|
|
# Takes a mode returned from stat(), returns a noune describing the filetype,
|
|
# e.g. "directory", "symlink".
|
|
# If the "plural" argument is provided and true, returns the plural form of
|
|
# the noun, e.g. "directories", "symlinks".
|
|
sub filetype_to_string ($;$) {
|
|
my ($mode, $plural) = @_;
|
|
|
|
if (S_ISREG($mode)) {
|
|
return "regular file".($plural ? "s" : "");
|
|
} elsif (S_ISDIR($mode)) {
|
|
return "director".($plural ? "ies" : "y");
|
|
} elsif (S_ISLNK($mode)) {
|
|
return "symlink".($plural ? "s" : "");
|
|
} elsif (S_ISBLK($mode)) {
|
|
return "block device".($plural ? "s" : "");
|
|
} elsif (S_ISCHR($mode)) {
|
|
return "character device".($plural ? "s" : "");
|
|
} elsif (S_ISFIFO($mode)) {
|
|
return "fifo".($plural ? "s" : "");
|
|
} elsif (S_ISSOCK($mode)) {
|
|
return "socket".($plural ? "s" : "");
|
|
} else {
|
|
return "unknown filetype".($plural ? "s" : "");
|
|
}
|
|
}
|
|
|
|
# compare_files(oldfile, newfile)
|
|
# This is the actual diffing routine. It's quite long because we need to
|
|
# deal with all sorts of special cases. It will print to STDOUT a
|
|
# description of the differences between the two files. For regular files,
|
|
# diff(1) will be run to show the differences.
|
|
#
|
|
# return codes:
|
|
# 1 found a difference
|
|
# 2 had an error
|
|
# 3 found a difference and had an error
|
|
sub compare_files ($$) {
|
|
my ($oldname, $newname) = @_;
|
|
my ($old, $new); # stat buffers
|
|
my $return = 0;
|
|
|
|
# Get rid of unsightly double slashes
|
|
$oldname =~ s#//#/#g;
|
|
$newname =~ s#//#/#g;
|
|
|
|
eval { $old = lstat($oldname); };
|
|
if (not defined $old and not $!{ENOENT}) {
|
|
warn "$PROG: Could not stat $oldname: $!\n";
|
|
return 2;
|
|
}
|
|
eval { $new = lstat($newname); };
|
|
if (not defined $new and not $!{ENOENT}) {
|
|
warn "$PROG: Could not stat $newname: $!\n";
|
|
return 2;
|
|
}
|
|
# At this point, $old or $new should only be undefined if the
|
|
# file does not exist.
|
|
|
|
if (defined $old and defined $new) {
|
|
if (S_IFMT($old->mode) != S_IFMT($new->mode)) {
|
|
if ($opt{fakediff}) {
|
|
fakediff('filetype',
|
|
$oldname => filetype_to_string($old->mode),
|
|
$newname => filetype_to_string($new->mode),
|
|
);
|
|
} else {
|
|
print "File types differ between ".
|
|
filetype_to_string($old->mode)." $oldname and ".
|
|
filetype_to_string($new->mode)." $newname\n";
|
|
}
|
|
return 1;
|
|
}
|
|
if ($old->nlink != $new->nlink) {
|
|
# In recursive mode, we don't care about link counts in directories,
|
|
# as we'll pick that up with what files do and don't exist.
|
|
unless ($opt{recursive} and S_ISDIR($old->mode)) {
|
|
if ($opt{fakediff}) {
|
|
fakediff('nlink',
|
|
$oldname => $old->nlink,
|
|
$newname => $new->nlink,
|
|
);
|
|
} else {
|
|
print "Link counts differ between ".
|
|
filetype_to_string($old->mode, 1).
|
|
" $oldname and $newname\n";
|
|
}
|
|
$return = 1;
|
|
}
|
|
}
|
|
if ($old->uid != $new->uid and $opt{perms}) {
|
|
if ($opt{fakediff}) {
|
|
fakediff('uid',
|
|
$oldname => $old->uid,
|
|
$newname => $new->uid,
|
|
);
|
|
} else {
|
|
print "Owner differs between ".
|
|
filetype_to_string($old->mode, 1).
|
|
" $oldname and $newname\n";
|
|
}
|
|
$return = 1;
|
|
}
|
|
if ($old->gid != $new->gid and $opt{perms}) {
|
|
if ($opt{fakediff}) {
|
|
fakediff('gid',
|
|
$oldname => $old->gid,
|
|
$newname => $new->gid,
|
|
);
|
|
} else {
|
|
print "Group differs between ".
|
|
filetype_to_string($old->mode, 1).
|
|
" $oldname and $newname\n";
|
|
}
|
|
$return = 1;
|
|
}
|
|
if (S_IMODE($old->mode) != S_IMODE($new->mode) and $opt{perms}) {
|
|
if ($opt{fakediff}) {
|
|
fakediff('mode',
|
|
$oldname => sprintf('%04o', S_IMODE($old->mode)),
|
|
$newname => sprintf('%04o', S_IMODE($new->mode)),
|
|
);
|
|
} else {
|
|
print "Modes differ between ".
|
|
filetype_to_string($old->mode, 1).
|
|
" $oldname and $newname\n";
|
|
}
|
|
$return = 1;
|
|
}
|
|
|
|
# We don't want to compare anything more about sockets, fifos, or
|
|
# directories, once we've checked the permissions and link counts
|
|
if (S_ISSOCK($old->mode) or
|
|
S_ISFIFO($old->mode) or
|
|
S_ISDIR($old->mode)) {
|
|
return $return;
|
|
}
|
|
|
|
# Check device file devs, and that's it for them
|
|
if (S_ISCHR($old->mode) or
|
|
S_ISBLK($old->mode)) {
|
|
if ($old->rdev != $new->rdev) {
|
|
if ($opt{fakediff}) {
|
|
fakediff('rdev',
|
|
$oldname => $old->rdev,
|
|
$newname => $new->rdev,
|
|
);
|
|
} else {
|
|
print "Device numbers differ between ".
|
|
filetype_to_string($old->mode, 1).
|
|
" $oldname and $newname\n";
|
|
}
|
|
$return = 1;
|
|
}
|
|
return $return;
|
|
}
|
|
|
|
# Compare the targets of symlinks
|
|
if (S_ISLNK($old->mode)) {
|
|
my $oldtarget = readlink $oldname
|
|
or (warn("$PROG: Could not readlink($oldname): $!\n"),
|
|
return $return | 2);
|
|
my $newtarget = readlink $newname
|
|
or (warn("$PROG: Could not readlink($newname): $!\n"),
|
|
return $return | 2);
|
|
if ($oldtarget ne $newtarget) {
|
|
if ($opt{fakediff}) {
|
|
fakediff('target',
|
|
$oldname => $oldtarget,
|
|
$newname => $newtarget,
|
|
);
|
|
} else {
|
|
print "Symlink targets differ between $oldname and $newname\n";
|
|
}
|
|
$return = 1;
|
|
}
|
|
return $return;
|
|
}
|
|
|
|
if (not S_ISREG($old->mode)) {
|
|
warn "$PROG: Don't know what to do with file mode $old->mode!\n";
|
|
return 2;
|
|
}
|
|
} elsif (not defined $old and not defined $new) {
|
|
print "Neither $oldname nor $newname exists\n";
|
|
return $return;
|
|
} elsif (not defined $old) {
|
|
if (not S_ISREG($new->mode) or not $opt{'new-file'}) {
|
|
print "Only in ".dirname($newname).": ".
|
|
filetype_to_string($new->mode)." ".basename($newname)."\n";
|
|
return 1;
|
|
} elsif ($opt{'devnullhack'}) {
|
|
$oldname = '/dev/null';
|
|
}
|
|
} elsif (not defined $new) {
|
|
if (not S_ISREG($old->mode) or not $opt{'new-file'}) {
|
|
print "Only in ".dirname($oldname).": ".
|
|
filetype_to_string($old->mode)." ".basename($oldname)."\n";
|
|
return 1;
|
|
} elsif ($opt{'devnullhack'}) {
|
|
$newname = '/dev/null';
|
|
}
|
|
}
|
|
# They are regular files! We can actually run diff!
|
|
return diff($oldname, $newname) | $return;
|
|
}
|
|
|
|
sub diff ($$) {
|
|
my ($oldname, $newname) = @_;
|
|
my @command = (@diff, $oldname, $newname);
|
|
my $status;
|
|
|
|
# If we're not specifying unified diff, we need to print a header
|
|
# to indicate what's being diffed. (I'm not sure if this actually would
|
|
# work for patch, but it does tell our user what's going on).
|
|
# FIXME: We only need to specify this if the files are different
|
|
print "@command\n"
|
|
if not $opt{u};
|
|
|
|
{
|
|
# There is a bug in perl with use warnings FATAL => qw(all)
|
|
# that will cause the child process from system() to stick
|
|
# around if there is a warning generated.
|
|
# Shut off warnings -- we'll catch the error below.
|
|
no warnings;
|
|
$status = system(@command);
|
|
}
|
|
return 0 if ($status == 0);
|
|
if ($? == -1) {
|
|
die "$PROG: failed to execute '@command': $!\n";
|
|
}
|
|
if ($? & 128) {
|
|
die "$PROG: '@command' dumped core\n";
|
|
}
|
|
if (my $sig = $? & 127) {
|
|
die "$PROG: '@command' caught sig $sig\n"
|
|
unless ($sig == SIGPIPE);
|
|
}
|
|
if (my $exit = $? >> 8) {
|
|
if ($exit == 1) {
|
|
return 1;
|
|
} else {
|
|
die "$PROG: '@command' returned $exit\n";
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
sub fakediff ($$) {
|
|
my ($type, $oldname, $oldvalue, $newname, $newvalue) = @_;
|
|
|
|
return unless $opt{fakediff};
|
|
my $time = strftime('%F %T.000000000 %z', localtime(0));
|
|
|
|
# We add a suffix onto the filenames to show we're not actually looking
|
|
# at file contents. There's no good way to indicate this that's compatible
|
|
# with patch, and this is simple enough.
|
|
$oldname .= '#~~' . $type;
|
|
$newname .= '#~~' . $type;
|
|
|
|
if ($opt{u}) {
|
|
# fake up a unified diff
|
|
print <<EOF;
|
|
--- $oldname\t$time
|
|
+++ $newname\t$time
|
|
@@ -1 +1 @@
|
|
-$oldvalue
|
|
+$newvalue
|
|
EOF
|
|
} else {
|
|
print <<EOF;
|
|
diff $oldname $newname
|
|
1c1
|
|
< $oldvalue
|
|
---
|
|
> $newvalue
|
|
EOF
|
|
}
|
|
}
|