#!/usr/pkg/bin/perl

=begin metadata

Name: cmp
Description: compare two files
Author: D Roland Walker, walker@pobox.com
License: perl

=end metadata

=cut


# cmp - compare two files
# VERSION 1.0
#
# Notes:
#
# It's nice to see that, speedwise, this beats the pants off
# GNU and IRIX system cmp in many tasks.  If compiled, this
# would probably be faster in every case.  Go Perl !
#
#
# Compat:
#
# GNU, Solaris, and IRIX cmp seem to give the EOF warning
# whenever -l is given and file sizes are not equal.  That
# doesn't match the BSD docs.  We follow the docs.
#
#
# Todo:
#
# revise how zero-length files are handled ?
#

use strict;

use File::Basename qw(basename);
use Getopt::Std qw(getopts);
use List::Util qw(min);

use constant EX_SUCCESS   => 0;
use constant EX_DIFFERENT => 1;
use constant EX_FAILURE   => 2;
use constant EX_USAGE     => 2;

use constant ST_INO  => 1;
use constant ST_SIZE => 7;

use constant READSZ => 10_000;

my $Program = basename($0);

my $volume=1;                # controlled by -s and -l

my $fh1;
my $fh2;
my $file1;
my $file2;
my $skip1;
my $skip2;
my $buffer1;
my $buffer2;
my $read_in1;
my $read_in2;

my $bytes_read = 0;
my $lines_read = 0;
my $saw_difference;

my %opt;
getopts('ls', \%opt) or usage();
if ($opt{'l'}) {
    usage() if $opt{'s'};
    $volume = 2;
}
$volume = 0 if $opt{'s'};

usage() if (scalar(@ARGV) < 2);
$file1 = shift;
$file2 = shift;
$skip1 = skipnum(shift) if @ARGV;
$skip2 = skipnum(shift) if @ARGV;
usage() if @ARGV;

if ($file1 eq '-') {
    if ($file2 eq '-') {
        warn "$Program: standard input is allowed for one argument only\n";
        exit EX_FAILURE;
    }
    $fh1 = *STDIN;
}
if ($file2 eq '-') {
    $fh2 = *STDIN;
}

if (!$fh1 && !$fh2) {
    my @stat1 = stat $file1;
    unless (@stat1) {
        warn "$Program: '$file1': $!\n";
        exit EX_FAILURE;
    }
    my @stat2 = stat $file2;
    unless (@stat2) {
        warn "$Program: '$file2': $!\n";
        exit EX_FAILURE;
    }

    if (defined($stat1[ST_INO]) && $stat1[ST_INO] == $stat2[ST_INO]) {
        exit EX_SUCCESS;                        # hopefully, on platforms where
    }                                           # inode is meaningless, stat
                                                # returns undef.

    if ($stat1[ST_SIZE] == 0 || $stat2[ST_SIZE] == 0) {
        # special handling for zero-length files
        if ($stat1[ST_SIZE] == 0 && $stat2[ST_SIZE] == 0) {
            exit EX_SUCCESS;
        } else {                            # Can't we say 'differ at byte zero'
                                            # and so on here?  That might make
                                            # more sense than this behavior.
                                            # Also, this should be made consistent
                                            # with the behavior when skip >=
                                            # filesize.
            if ($volume) {
                warn "$Program: EOF on $file1 which is empty\n" unless $stat1[ST_SIZE];
                warn "$Program: EOF on $file2 which is empty\n" unless $stat2[ST_SIZE];
            }
            exit EX_DIFFERENT;
        }
    }
}

unless ($fh1) {
    if (-d $file1) {
        warn "$Program: '$file1' is a directory\n";
        exit EX_FAILURE;
    }
    unless (open $fh1, '<', $file1) {
        warn "$Program: cannot open '$file1': $!\n";
        exit EX_FAILURE;
    }
}
unless ($fh2) {
    if (-d $file2) {
        warn "$Program: '$file2' is a directory\n";
        exit EX_FAILURE;
    }
    unless (open $fh2, '<', $file2) {
        warn "$Program: cannot open '$file2': $!\n";
        exit EX_FAILURE;
    }
}

if ($skip1) {
    if (-p $fh1) {
        sysread $fh1, $buffer1, $skip1;
    } else {
        sysseek $fh1, $skip1, 0;
    }
}
if ($skip2) {
    if (-p $fh2) {
        sysread $fh2, $buffer2, $skip2;
    } else {
        sysseek $fh2, $skip2, 0;
    }
}

READ: while (defined ($read_in1 = sysread $fh1, $buffer1, READSZ)) {
    $read_in2 = sysread $fh2, $buffer2, READSZ;
    $read_in2 = 0 unless defined $read_in2; # sysread failed

    my $checklength = min($read_in1, $read_in2);
    $checklength-- if $checklength;

    if ($checklength != 0 && $buffer1 ne $buffer2) {
        my @bytes1 = unpack 'C*', $buffer1;
        my @bytes2 = unpack 'C*', $buffer2;

        for (0 .. $checklength) {
            $lines_read++ if ($bytes1[$_] == ord("\n"));
            next if ($bytes1[$_] == $bytes2[$_]);
            my $report_lines = $lines_read + 1;
            my $report_bytes = $bytes_read + 1 + $_;
            if ($volume == 1) {
                print "$file1 $file2 differ: char $report_bytes, line $report_lines\n";
                exit EX_DIFFERENT;
            } elsif ($volume == 0) {
                exit EX_DIFFERENT;
            } else {
                $saw_difference ||= 1;
                printf "%6d %3o %3o\n", $report_bytes, $bytes1[$_], $bytes2[$_];
                next READ if $_ == $checklength;
                next;
            }
        }
    } else {
        $lines_read += $buffer1 =~ tr[\n][\n];
    }
    $bytes_read += $checklength;
    my $nlines = $lines_read + 1;
    my $nbytes = $bytes_read + 1;

    if ($read_in1 < $read_in2) {
        warn "$Program: EOF on $file1 after byte $nbytes, in line $nlines\n" unless $saw_difference or !$volume;
        exit EX_DIFFERENT;
    } elsif ($read_in1 > $read_in2) {
        warn "$Program: EOF on $file2 after byte $nbytes, in line $nlines\n" unless $saw_difference or !$volume;
        exit EX_DIFFERENT;
    } elsif ($read_in1 == 0) {
        exit EX_DIFFERENT;
    }
}

close $fh1;
close $fh2;
exit $saw_difference;

sub skipnum {
    my $n = shift;
    return hex($n) if ($n =~ m/\A0[Xx]/);
    return oct($n) if ($n =~ m/\A0o?/);
    return int($n) if ($n =~ m/\A[0-9]+\z/);
    warn "$Program: invalid offset number '$n'\n";
    usage();
}

sub usage {
    warn "usage: $Program [-l | -s] file1 file2 [skip1 [skip2]]\n";
    exit EX_USAGE;
}

__END__

=head1 NAME

cmp - compare two files

=head1 SYNOPSIS

cmp B<[-l | -s]> I<file1> I<file2> I<[skip1 [skip2]]>

=head1 DESCRIPTION

I<cmp> compares two files, byte-by-byte.  The result of the comparison
is always given by the exit status, and may be summarized on the
standard output according to the options given on the command line.

If the two compared files are identical, I<cmp> will exit with
a zero exit status.  If the compared files are not identical, I<cmp>
will exit with a status of 1.

If no options are given, I<cmp> will return (on the standard output),
the byte number and line number where the first difference is encountered.

If one file is an initial subsequence of the other, a message will
also be returned on the standard error indicating that EOF was reached
in the shorter of the two file.

I<skip1> and I<skip2> are optional byte offsets into I<file1> and
I<file2>, respectively, that determine where the file comparison
will begin.  The offsets value is decimal by default.
An octal value can be entered with a leading '0' or '0o'.
A hexadecimal value can be entered with a leading '0x' or '0X'.

file1 or file2 may be given as '-', which reads from standard input.

=head2 OPTIONS

=over

=item -s

silent execution; indicate results only by exit status, suppressing
all output and warnings.

=item -l

list differences; return the byte number and the differing byte values
for each difference between the two files.  The byte number is given
in decimal, and the byte values are given in octal.

=back

=head1 ENVIRONMENT

No environment variables affect the execution of I<cmp>.

=head1 BUGS

No known bugs.

=head1 AUTHOR

D Roland Walker I<E<lt>walker@pobox.comE<gt>>.

=head1 COPYRIGHT and LICENSE

This program is copyright (c) D Roland Walker 1999.

This program is free and open software. You may use, modify, distribute,
and sell this program (and any modified variants) in any way you wish,
provided you do not restrict others from doing the same.

