#!/usr/misc/bin/perl5

#
# cdiff -- Do a cleardiff with lines of context
#
# Created 03/10/95 by Brad Appleton <brad@bradapp.net>
#
#-------------------------------------------------------------------------
# COPYING
# =======
# This file/program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself. Please refer to the
# license that came with your Perl distribution for more details.
#
# DISCLAIMER
# ===========
# This software is distributed in the hope that it will be useful, but
# is provided "AS IS" WITHOUT WARRANTY OF ANY KIND, either expressed or
# implied, INCLUDING, without limitation, the implied warranties of
# MERCHANTABILITY and FITNESS FOR A PARTICULAR PURPOSE.
#
# The ENTIRE RISK as to the quality and performance of the software
# IS WITH YOU (the holder of the software).  Should the software prove
# defective, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
# CORRECTION.
#
# IN NO EVENT WILL ANY COPYRIGHT HOLDER OR ANY OTHER PARTY WHO MAY CREATE,
# MODIFY, OR DISTRIBUTE THE SOFTWARE BE LIABLE OR RESPONSIBLE TO YOU OR TO
# ANY OTHER ENTITY FOR ANY KIND OF DAMAGES (no matter how awful - not even
# if they arise from known or unknown flaws in the software).
##########################################################################

package CDiff;

my $VERSION  = '1.0.12';
my $MODIFIED = '15-Apr-2003';
my $AUTHOR   = 'Brad Appleton <brad@bradapp.net>';

use strict qw(vars subs);
use Config;
use File::Basename qw(&basename &dirname);
use Text::Tabs ();

#-------------------------------------------------------------------------
#                     BEGIN CONFIGURATION SECTION
# 
# (dont forget to change the '#!' line at the top of the file to
#  use the appropriate path/invocation for Perl on your system)
#-------------------------------------------------------------------------
   # You *must* have Johan Vromans "newgetopt.pl" option parsing package
   # for Perl (version 1.14 or later) installed as part of your standard
   # perl library (it is in the Perl5 distribution, but not in all Perl4
   # distributions).
use Getopt::Long;
   # The path to the Atria installation area
my $ATRIAHOME = '/usr/atria';
   # The path to the directory of executable ClearCase commands
my $CLEARBIN  = "${ATRIAHOME}/bin";
   # The path to the 'cleartool' command
my $CLEARTOOL = "${CLEARBIN}/cleartool";
   # The extended version naming suffix to use.
my $CLEARCASE_XN_SFX = "\@\@";
   # In order to use the '-fmt patch' feature of cdiff, you will need a
   # program on your system that does a file comparison displaying lines
   # of context in a format that is accepted by Larry Wall's "patch"
   # program. Most versions of diff(1) take a '-c' or '-C' option to do
   # this (not all of them will allow you to specify the number of lines
   # of context). You will need to set the definition of the PATCHDIFF
   # variable to the appropriate command-line invocation (note that a
   # "%d" will be replaced with the desired number of lines of context
   # to display).
my $PATCHDIFF = 'diff -C %d';
#-------------------------------------------------------------------------
#                      END CONFIGURATION SECTION
#-------------------------------------------------------------------------


## Get basename and dirname of program path
my $Is_WinDos     = ( (uc $Config{'osname'}) =~ /(?:MSDOS|MSWIN|WIN32)/ );
my $Path_Sep      = ( $Is_WinDos ? "\\" : "/" );
my $PathList_Sep  = ( $Is_WinDos ? ";" : ":" );
my ($NAME, $EXECDIR) = (basename($0), dirname($0) || ".");

## Option settings
my %OPTS = ();

### Some constants that the user can override from the command-line
my %FMT = ('patch',     0,
           'deleted',   "<",
           'inserted',  ">",
           'movedfrom', "",
           'movedto',   "",
           'inserted',  ">",
           'common',    "",
           'separator', "",
           'lheader',   "-----",
           'rheader',   "-----",
           'divider',   "********************************",
           'base',      "<<<",
           'contrib',   ">>>",
           'numwidth',  4,
           'reverse',   0);

=head1 NAME

cdiff - perform a cleardiff which includes lines of surrounding context

=head1 SYNOPSIS


 cdiff [-Help] [-Debug] [-after NUMBER] [-before NUMBER] [-blank_ignore]
       [-fmt FORMAT] [-linenumbers] [-nocontext] [-noheaders] [-noprolog]
       [-pred] [-unlimited] [-tabstop NUMBER] [-output FILE] XPNAME ...

=head1 OPTIONS AND ARGUMENTS

=over 6

=item B<-Help>

Print this help message and exit.

=item B<-Version>

Print version information and exit

=item B<-Debug>

Enable debugging output.

=item B<-after> I<NUMBER>

Number of lines of context to show after a difference.

=item B<-before> I<NUMBER>

Number of lines of context to show before a difference.

=item B<-unlimited>

Show unlimited lines of context (all common lines).

=item B<-blank_ignore>

Pass the B<-blank_ignore> option to B<cleardiff(1)>.

=item B<-linenumbers>

Include line numbers (for each file) in the output.

=item B<-reflinenumbers>

Include absolute reference line numbers (for each file) in the output.

=item B<-pred>

Pass the B<-pred> option to B<cleardiff(1)>.

=item B<-nocontext>

Don't print lines of context.

=item B<-noheaders>

Don't include change headers in the output.

=item B<-noprolog>

Don't print the "legend" prolog at the beginning that shows
which file names correspond to which file numbers.

=item B<-tabstop> I<NUMBER>

If specified, then expand tab into spaces using the given argument
as the tabstop setting (each tab-setting corresponds to the given
number of spaces). If unspecified, no tab-expansion is performed.

=item B<-fmt> I<FORMAT>

Specify output format attributes. Valid I<FORMAT> values are:

=over 3

=item patch

Specify that output should be in the context-diff format
used by Larry Wall's patch program. Causes all other
formats to be ignored (Note: requires C<diff -C>).

=item deleted=I<PREFIX>

Specify the prefix for a deleted line (default = "<")

=item inserted=I<PREFIX>

Specify the prefix for an inserted line (default = ">")

=item movedfrom=I<PREFIX>

Specify the prefix for a deleted/moved line
(default: same as for C<deleted>)

=item movedto=I<PREFIX>

Specify the prefix for an inserted/moved line
(default: same as for C<inserted>)

=item common=I<PREFIX>

Specify the prefix for a line of context that is common to
all files (default = "")

=item numwidth=I<NUMBER>

Specify the width of any line numbers that are to appear in the
output (for use with the B<-linenumbers> and B<-reflinenumbers>
options).  I<NUMBER> must be a positive integer (default = 4)

=item reverse

Specify that line numbers should be printed in reverse
order (for use with the B<-linenumbers> option). By default
line numbers are printed left-to-right in the order:

        file 1, file 2, ..., file N

Specifying C<reverse> causes them to be printed in reverse
order as in:

        file N, ..., file 2, file 1

=item separator=I<STRING>

Specify the text to separate a deleted/inserted/common
prefix from the actual source (default = "")

=item lheader=I<PREFIX>

Specify the prefix (left-header) for header-lines that
indicate a change was made (default = "-----")

=item rheader=I<SUFFIX>

Specify the suffix (right-header) for header-lines that
indicate a change was made (default = "-----")

=item header=I<STRING>

Specify the suffix+prefix for header-lines that indicate
a change was made

=item divider=I<STRING>

Specify the contents of the dividing line that separates
a section of context from the following/preceding section
(default = "*********************************")

=item base=I<PREFIX>

Specify the prefix for the name of the base-file in the
prologue section (default = "<<<")

=item contrib=I<PREFIX>

Specify the prefix for the name of a contributor-file in
the prologue section (default = ">>>")

=back

I<FORMAT> keywords may be abbreviated to a unique prefix and are
case insensitive. This option may be specified multiple times.

=item B<-output> I<FILE>

Redirect output to the named file (should have the same effect as
redirecting STDOUT on the cmdline using "> I<FILE>").

=item I<XPNAME>

Extended pathname to the desired element in your view
(the version path may be abbreviated).

=back

Option names are case insensitive and only a unique prefix is required.

=head1 DESCRIPTION

B<cdiff> will produce a serial-format difference listing with surrounding
lines of context for the named files. A filename may be specified as a
version-extended pathname (abbreviated or otherwise), or simply by a
version selector (in which case the corresponding element is assumed to
be the same as the element associated with the previously specified
filename).

Exit status is 0 if there are no differences to report, 1 if there are,
and 2 if an error occurred.

=head1 EXAMPLES

You typically give cdiff a single file if you just want to compare
against the predecessor version in ClearCase (the B<-pred> option
is the default if only one filename is given):

  $ cdiff dir.c

  ********************************
  <<< file 1: /vobs/myproj/dir.c@@/main/4
  >>> file 2: dir.c
  ********************************

    static	Dta_buf		dtabuf;
    static	Dta_buf		*dtapnt = &dtabuf;
    static	union REGS	reg, nreg;

  -----[after 57 inserted 58-61]-----
  > #if	defined(M_I86LM)
  > static	struct SREGS	sreg;
  > #endif
  > 
  -----[end of inserted lines]-----
    DIR	*
    opendir(name)
          char	*name;
    {
          struct	stat		statb;
  ********************************
    void
    closedir(dirp)
          DIR	*dirp;
    {
          free_dircontents(dirp->dd_contents);
  -----[108 changed to 112]-----
  < 	free((void *) dirp);
  ----------
  > 	free((char *) dirp);
  -----[end of changed lines]-----
    }

    struct direct	*
    readdir(dirp)
          DIR	*dirp;

You can also specify the I<FROM> and I<TO> files directly

  $ cdiff dir.c.OLD dir.c

  ********************************
  <<< file 1: dir.c.orig
  >>> file 2: dir.c
  ********************************

    static	Dta_buf		dtabuf;
    static	Dta_buf		*dtapnt = &dtabuf;
    static	union REGS	reg, nreg;

  -----[after 57 inserted 58-61]-----
  > #if	defined(M_I86LM)
  > static	struct SREGS	sreg;
  > #endif
  > 
  -----[end of inserted lines]-----
    DIR	*
    opendir(name)
          char	*name;
    {
          struct	stat		statb;
  ********************************
    void
    closedir(dirp)
          DIR	*dirp;
    {
          free_dircontents(dirp->dd_contents);
  -----[108 changed to 112]-----
  < 	free((void *) dirp);
  ----------
  > 	free((char *) dirp);
  -----[end of changed lines]-----
    }

    struct direct	*
    readdir(dirp)
          DIR	*dirp;

The output is similar to that of C<cleardiff -serial> except that lines
of context are included. The default is to print 5 lines of surrounding
context before and after each insert/change/delete.  There is a B<-before>
and a B<-after> option to change the number of lines of context. There
is also a B<-unlimited> option if you want I<all> lines of context to be
included. You can use the B<-linenumbers> option to include lines
numbers for each file. You can also use the B<-noheaders> option if you
dont like seeing the comments in the square brackets with the dashed
lines. Options may be abbreviated to a unique prefix. For example:

  $ cdiff -nohead -bef 1 -aft 1 -line dir.c.orig dir.c

  ********************************
  <<< file 1: dir.c.orig
  >>> file 2: dir.c
  ********************************
      57   57 
  >        58  #if defined(M_I86LM)
  >        59  static      struct SREGS    sreg;
  >        60  #endif
  >        61  
      58   62  DIR *
  ********************************
     107  111      free_dircontents(dirp->dd_contents);
  <  108           free((void *) dirp);
  >       112      free((char *) dirp);
     109  113  }
  ********************************
     141  145      dirp->dd_cp = dp;
  <  142       
  <  143               return  1;
     144  146  }

  $ cdiff -nohead -bef 1 -aft 1 -line -refline dir.c.orig dir.c

You can add reference line-numbers as well, using the B<-reflinenumbers>
to give a unique number to each output line:

  ********************************
  <<< file 1: dir.c.orig
  >>> file 2: dir.c
  ********************************
  1      57   57 
  2  >        58  #if defined(M_I86LM)
  3  >        59  static      struct SREGS    sreg;
  4  >        60  #endif
  5  >        61  
  6  >   58   62  DIR *
  ********************************
  7     107  111      free_dircontents(dirp->dd_contents);
  8  <  108           free((void *) dirp);
  9  >       112      free((char *) dirp);
  10    109  113  }
  ********************************
  11    141  145      dirp->dd_cp = dp;
  12 <  142       
  13 <  143               return  1;
  14    144  146  }


There is also a B<-fmt> option which can take several different
keyword settings to change the output format:

  $ cdiff -fmt del=DELETE -fmt ins=INSERT -fmt sep='|'  \
          -noheaders -bef 2 -aft 2 -line  \
          dir.c.orig dir.c

  ********************************
  <<< file 1: dir.c.orig
  >>> file 2: dir.c
  ********************************
           56   56 | static       union REGS      reg, nreg;
           57   57 | 
  INSERT        58 | #if  defined(M_I86LM)
  INSERT        59 | static       struct SREGS    sreg;
  INSERT        60 | #endif
  INSERT        61 | 
           58   62 | DIR  *
           59   63 | opendir(name)
  ********************************
          106  110 | {
          107  111 |      free_dircontents(dirp->dd_contents);
  DELETE  108      |      free((void *) dirp);
  INSERT       112 |      free((char *) dirp);
          109  113 | }
          110  114 | 
  ********************************
          140  144 |      dirp->dd_loc = off - (i + 1);
          141  145 |      dirp->dd_cp = dp;
  DELETE  142      | 
  DELETE  143      |         return  1;
          144  146 | }
          145  147 | 


Hopefully, this "tutorial" should have provided you with a good set of
cookbook examples for using B<cdiff>. if you ever want to see this manual
page, you need only invoke B<cdiff> using the B<-help> option:

  $ cdiff -help

=head1 SEE ALSO

L<sclc> -- B<sclc> uses B<cdiff> to compare versions of files when
counting lines of source code

=head1 AUTHOR

Brad Appleton  E<lt>brad@bradapp.net<gt>

=cut

##----------------------------------------------------------------------
## FUNCTION:
##   ErrorMsg -- Print an error message (prefixed by "$NAME: ").
##
## SYNOPSIS:
##   &ErrorMsg("message text");
##
## ARGUMENTS:
##   A single string containing the message text.
##
## RETURN VALUE: None.
##
## PRECONDITIONS:
##   The global variable $NAME should be set to the name of the running
##   script.
##
## SIDE EFFECTS:
##   - Prints to STDERR
##----------------------------------------------------------------------
sub ErrorMsg {
   print STDERR ("${NAME}: ", @_, "\n");
}

##----------------------------------------------------------------------
## FUNCTION:
##   VersionMsg -- Print a version message and exit
##
## SYNOPSIS:
##   &VersionMsg($exit_status);
##
## ARGUMENTS:
##   Exit status to terminate with after printing the message
##
## RETURN VALUE: None.
##
## PRECONDITIONS:
##   Requires the following global variables to be set:
##    $NAME -- the name of the running script.
##    $VERSION -- the version-id of the running script.
##    $MODIFIED -- the last-modified date of the script.
##    $AUTHOR -- the current author of the script
##
## SIDE EFFECTS:
##   - Prints to STDERR
##----------------------------------------------------------------------
sub VersionMsg {
   &ErrorMsg("version $VERSION, $MODIFIED by $AUTHOR");
   exit($_[0]||1);
}

##----------------------------------------------------------------------
## FUNCTION:
##   FatalMsg -- Print an error message and then abort
##
## SYNOPSIS:
##   &FatalMsg("message text");
##
## ARGUMENTS:
##   A single string containing the message text.
##
## RETURN VALUE: None.
##
## PRECONDITIONS:
##   The global variable $NAME should be set to the name of the running
##   script.
##
## SIDE EFFECTS:
##   - Prints an error message
##   - Exits the script using exit(2)
##----------------------------------------------------------------------
sub FatalMsg {
   &ErrorMsg(@_);
   exit(2);
}

##----------------------------------------------------------------------
## FUNCTION:
##   Usage -- Print a Usage message and then exit with the specified
##            exit-value. If the exit-value is > 1, then usage is terse
##            (synopsis only) and goes to STDERR. Otherwise, usage is
##            verbose and goes to STDOUT.
##
## SYNOPSIS:
##   &Usage([$val]);
##
## ARGUMENTS:
##   $val : (optional) The integer exit value to use (defaults to 2).
##
## RETURN VALUE: None.
##
## PRECONDITIONS: None.
##
## SIDE EFFECTS:
##   Exits the script using exit with the given exit-status.
##----------------------------------------------------------------------
sub Usage {
   local($_) = shift;
   my ($exitval, $message) = (1, '');
   ## Collect arguments
   if ( /^[-+]?\d+$/ ) {
      ($exitval, $message) = ($_, '');
   }
   elsif (defined $_  and  length $_) {
      ($exitval, $message) = (2, $_);
   }
   printf STDERR ("%s: %s\n", basename($0), $message)  if ($message);
   if ($exitval > 1) {
      &FatalMsg("Invoke as \"$NAME -help\" for complete usage documentation");
   }
   else {
      $ENV{PATH} .= $PathList_Sep . $Config{bin};
      system( $Config{bin} . $Path_Sep . 'perldoc', $0 );
   }
   exit($exitval);
}

##----------------------------------------------------------------------
## FUNCTION:
##   DbgPrint -- Print debugging output. Prints the specified arguments
##               only if '-debug' was specified on the command-line.
##
## SYNOPSIS:
##   &DbgPrint(">debug message text\n");
##
## ARGUMENTS:
##   The string containing the debug message text (a newline is not
##   automatically printed so include it yourself if you need it).
##
## RETURN VALUE: None.
##
## PRECONDITIONS:
##   Assumes that the global variable named $OPTS{'debug'} will evaluate to
##   TRUE if '-debug' was given on the command-line.
##
## SIDE EFFECTS:
##   Prints to STDERR
##----------------------------------------------------------------------
sub DbgPrint {
   print STDERR (@_) if ($OPTS{'debug'});
}

####################################################################
## The following global variables are *very* important:
## -----------------------------------------------------------------

## The text of the current difference as reported by cleardiff
my $DIFFERENCE = '';

## The type of the current difference
my $Changed     = 'changed';
my $ChangedFrom = 'changed from';
my $ChangedTo   = 'changed to';
my $Deleted     = 'deleted';
my $Inserted    = 'inserted';
my $MovedFrom   = 'deleted/moved';
my $MovedTo     = 'inserted/moved';
my $EndOfDiff   = 'end of differences'; ## indicator for end-of-diff!

## Save the original file-list passed in and parse the new one from the
## beginning of the cleardiff-header. 
my @FILES = ();

## Keep an array of filehandles where $HANDLES[$i] is the filehandle used
## for file #(i-1). If it exists, it means the file is open. Since
## cleardiff starts file numbers at '1', we will keep the name of file
## number N in $FILES[N - 1].
my @HANDLES = ();

## Keep an array of line numbers where $LINES[$i] is the line-number of
## file #(i-1) that was last displayed as part of the output of the
## context-diff.
my @LINES = ();

## Keep a count of the number of source-lines for printing 
## reference line-numbers
my $REFLINE = 0;

####################################################################

## Get the file index from the file spec. The file-spec is of the
## format "file N" where 1 <= N <= 32.
sub FileIndex {
   local $_ = shift;
   return  /^file\s+(\d+)$/ ? ($1 - 1) : undef;
}

## Get the line range from the given spec. It may be a single number, or a
## range as in 5-10. In either case, return the pair ($begin, $end)
sub LineRange {
   local $_ = shift;
   my @range = ();
   /^(\d+)(?:-(\d+))?$/  and  push @range, $1, (length($2) ? $2 : $1);
   return  @range;
}

## Print prologue for the file differences. The prolog consists of
## the initial header that shows the names of all files compared
sub PrintPrologue {
   return  if ($OPTS{'noprologue'});
   print "$FMT{'divider'}\n";
   for (0 .. $#FILES) {
      my $prefixKey = ($_ == 0) ? 'base' : 'contrib';
      printf("%s file %d: %s\n", $FMT{$prefixKey}, $_ + 1, $FILES[$_]);
   }
   print "$FMT{'divider'}\n";
}

## Print a change header with the given text
sub PrintChangeHeader {
   return  if ($OPTS{'noheaders'});
   local $_ = shift;
   $_ = "[$_]" if (length $_);
   print "$FMT{'lheader'}$_$FMT{'rheader'}\n" unless ($OPTS{'noheaders'});
}

## Format the line number(s) for this line. Returns a string with all the
## required line numbers printed in the appropriate columns.
sub FormatLineNumbers {
   my($fileIndex, $prefixKey) = @_;
   local $_ = "";
   my @lineNumbers = ();
   ## If this is a context line, we need to print line numbers for all files
   ## in each column. Otherwise, we need to print only the line number for
   ## the current file (but still in the appropriate column)
   my $isContext = ($prefixKey eq 'common');
   for (0 .. $#FILES) {
      if ($LINES[$_] && ($isContext || ($_ == $fileIndex))) {
         push(@lineNumbers, sprintf("%$FMT{'numwidth'}d", $LINES[$_]));
      }
      else {
         push(@lineNumbers, sprintf("%$FMT{'numwidth'}s", " "));
      }
   }
   @lineNumbers = (reverse @lineNumbers) if ($FMT{'reverse'});
   $_ = join(" ", @lineNumbers) . " ";
   return  $_;
}

## Print the given line with the appropriate decorations
sub PrintSrcLine {
   my($srcLineText, $fileIndex, $prefixKey) = @_;
   local $_ = "";
   $_ .= sprintf("%-$FMT{'numwidth'}d ", ++$REFLINE) if ($OPTS{'reflinenumbers'});
   $_ .= "$FMT{$prefixKey} ";
   $_ .= &FormatLineNumbers($fileIndex, $prefixKey) if ($OPTS{'linenumbers'});
   $_ .= "$FMT{'separator'} " if ($FMT{'separator'});
   $_ .= $srcLineText;
   ($_) = &Text::Tabs::expand($_)  if ($OPTS{'tabstop'} && /\t/);
   print "$_";
}

## Read a line from the file corresponding to the given index, update its
## current line number, and return the line that was read. Open the file
## for reading if it wasnt already open!
sub GetLine {
   my($fileIndex) = @_;
   my $fname = $FILES[$fileIndex];
   my $fhandle = undef;
   local $_ = "";
   ## Get the corresponding handle (opening the file if necessary);
   if ($HANDLES[$fileIndex]) {
      $fhandle = $HANDLES[$fileIndex];
   }
   else {
      $fhandle = $fname;
      $fhandle =~ s/\W/_/g;
      open($fhandle, $fname) || &FatalMsg("Can't open $fname for reading: $!");
      $HANDLES[$fileIndex] = $fhandle;
      $LINES[$fileIndex] = 0;
   }
   $_ = <$fhandle>;
   ++$LINES[$fileIndex] if ($_);
   return  $_;
}

## Print the given line range of the given file
##    usage: &PrintSrcRange($fileIndex, $start, $end, $prefixKey);
##       $fileIndex is the index in @FILES
##       $start may be a number, or '.' for the current line position,
##           or '. + N' for N lines past the current position.
##       $end may be a number or '$' for EOF or '. + N' for N lines
##           past the current position
##       $prefix is the key in %FMT of the prefix for the type of lines we
##           are printing (deleted, inserted, or common)
##
sub PrintSrcRange {
   my($fileIndex, $start, $end, $prefixKey) = @_;
   my $i = 0;
   local $_ = "";
   ## Interpret any occurrences of '.' in the line numbers
   my $dot = $LINES[$fileIndex] + 1;
   if ($start =~ /\./) {
      $start =~ s/\./${dot}/g;
      eval "\$start = $start";
   }
   if ($end =~ /\./) {
      $end =~ s/\./${dot}/g;
      eval "\$end = $end";
   }
   ## Print the desired range (if we havent already passed it)
   return if (($end ne '$') && ($LINES[$fileIndex] >= $end));
   my $isContext = ($prefixKey eq 'common');
   while ( defined($_ = &GetLine($fileIndex)) ) {
      ## If this is a line of context then we need to keep all the other
      ## current line numbers from all the other files in sync.
      if ($isContext) {
         for $i (0 .. $#FILES) {
            &GetLine($i) unless ($i == $fileIndex);
         }
      }
      next if ($LINES[$fileIndex] < $start);
      &PrintSrcLine($_, $fileIndex, $prefixKey);
      last if (($end ne '$') && ($LINES[$fileIndex] == $end));
   }
}

## Print the "before" and "after" lines of context for the given range
sub PrintContext {
   my($fileIndex, $begin, $end, $after) = @_;
   my($current, $preContext, $postContext);
   my($needSeparator);
   ## Actually - we wait to print post-context until its time to
   ## print the next pre-context. This way it is easier to tell if
   ## the contexts would overlap, or if the next change will be
   ## part of the previous post-context.
   if ($OPTS{'after'} and ($LINES[0] > 0)) {
      ## Make sure trailing context doesnt overlap with this range
      $current = $LINES[0] + 1;
      $postContext = ($current + $OPTS{'after'}) - 1;
      if ($OPTS{'unlimited'} || (($fileIndex == 0) && ($after < $postContext))) {
         $postContext = $after - 1;
      }
      &PrintSrcRange(0, $current, $postContext, 'common');
      ++$needSeparator;
   }
   if ($OPTS{'before'}) {
      $preContext = ($after - $OPTS{'before'}) + 1;
      $preContext = 1 if ($OPTS{'unlimited'} && (! $LINES[0]));
      if (($preContext > ($LINES[0] + 1)) && $needSeparator) {
         print "$FMT{'divider'}\n";
      }
      &PrintSrcRange(0, $preContext, $after, 'common');
   }
}

## Print the given changed line-range of the given file with lines of context
##
## usage: &PrintDifference($range, $file, $ref);
##     where
##       - $range is the line-range to display
##       - $file is the file to use
##       - $ref is a line (or line-range) indicating where the lines
##         to be displayed are with respect to 'file 1'.  If $ref is
##         omitted, then $file is assumed to be 'file 1'.
sub PrintDifference {
   my($action, $range, $file, $ref) = @_;
   my($begin, $end) = &LineRange($range);
   $end = $begin unless ($end);
   my $fileIndex = &FileIndex($file);
   my $after = $begin - 1;
   if ($ref) {
      my @ary = &LineRange($ref);
      $after = pop(@ary) if (@ary > 0);
   }
   &PrintContext(0, $begin, $end, $after)
         unless ($OPTS{'nocontext'} || ($action eq $ChangedTo));
   my $prefixKey = "";
   local $_ = $action;
   if (/delete/) {
      $prefixKey = 'deleted';
   }
   elsif (/insert/) {
      $prefixKey = 'inserted';
   }
   elsif ($action eq $MovedFrom) {
      ($prefixKey, $_) = qw(movedfrom moved);
   }
   elsif ($action eq $MovedTo) {
      ($prefixKey, $_) = qw(movedto moved);
   }
   elsif ($action eq $ChangedFrom) {
      ($prefixKey, $_) = qw(deleted changed);
   }
   elsif ($action eq $ChangedTo) {
      ($prefixKey, $_) = qw(inserted changed);
   }
   ## Print lines $begin through $end of $file
   my $text = "$DIFFERENCE" unless ($action eq $ChangedTo);
   &PrintChangeHeader($text);
   &PrintSrcRange($fileIndex, $begin, $end, $prefixKey);
   &PrintChangeHeader("end of $_ lines") unless ($action eq $ChangedFrom);
}

## Cleanup -- print final context and close all the files we left open
sub Cleanup {
   local $_ = "";
   my $offset = $OPTS{'after'} - 1;
   ## Print trailing 'file 1' context
   unless ($OPTS{'nocontext'}) {
      my $postContext = ($OPTS{'unlimited'}) ? '$' : ". + $offset";
      &PrintSrcRange(0, ".", $postContext, 'common') if ($LINES[0] > 0);
   }
   ## Close all files
   for (@HANDLES) {
      close($_);
   }
}

## Given a diff-header explaining a change that was made, figure out
## the type of change and then print it.
sub ParseDifference {
   local $_ = shift;
   &DbgPrint("[DEBUG] : -----[$_]-----\n");

   ## globally remember the current difference description
   $DIFFERENCE = $_;

   ## Abbreviations used:
   ##    lr : line range
   ##    fn : file name
   ##    ln : line number
   my($lr, $fn, $ln) = ('\d+-?\d*', 'file\s+\d+', '\d+');
   my($action, $srcRef, $srcFile, $destRef, $destFile);

   ## See what kind of difference this is ...
   if (/^($EndOfDiff)$/) {
       $action = $1;
       &Cleanup();
   }
   elsif (/^($lr)\s*($fn)? ($Changed) to ($lr)\s*($fn)?$/) {
      ($srcRef, $srcFile,
       $action,
       $destRef, $destFile) = ($1, $2, $3, $4, $5);
      $srcFile  = 'file 1' unless ($srcFile);
      $destFile = 'file 2' unless ($destFile);
      &PrintDifference($ChangedFrom, $srcRef, $srcFile);
      &PrintDifference($ChangedTo, $destRef, $destFile, $srcRef);
   }
   elsif (/^($Deleted) ($lr)\s*($fn)? after ($ln)\s*($fn)?$/) {
      ($action,
       $srcRef, $srcFile,
       $destRef, $destFile) = ($1, $2, $3, $4, $5);
      $srcFile  = 'file 1' unless ($srcFile);
      $destFile = 'file 2' unless ($destFile);
      &PrintDifference($action, $srcRef, $srcFile);
   }
   elsif (/^after ($ln)\s*($fn)? ($Inserted) ($lr)\s*($fn)?$/) {
      ($srcRef, $srcFile,
       $action,
       $destRef, $destFile) = ($1, $2, $3, $4, $5);
      $srcFile  = 'file 1' unless ($srcFile);
      $destFile = 'file 2' unless ($destFile);
      &PrintDifference($action, $destRef, $destFile, $srcRef);
   }
   elsif (/^($MovedFrom) ($lr)\s*($fn)? after ($ln) \(now at ($lr)\)\s*($fn)?$/) {
      ($action,
       $srcRef, $srcFile,
       $destRef, $destFile) = ($1, $2, $3, $4, $6);
      $srcFile  = 'file 1' unless ($srcFile);
      $destFile = 'file 2' unless ($destFile);
      &PrintDifference($action, $srcRef, $srcFile);
   }
   elsif (/^after ($ln)\s*($fn)? ($MovedTo) ($lr) \(was at ($lr)\)\s*($fn)?$/) {
      ($srcRef, $srcFile,
       $action,
       $destRef, $destFile) = ($1, $2, $3, $4, $6);
      $srcFile  = 'file 1' unless ($srcFile);
      $destFile = 'file 2' unless ($destFile);
      &PrintDifference($action, $destRef, $destFile, $srcRef);
   }
   else {
      &FatalMsg("Unknown difference description \"$_\".");
   }
}

## Perform a context-diff in 'patch' format for the given files
sub PatchDiff {
   my @files = @_;
   &FatalMsg("Can't use \"-fmt patch\" option without defining \$PATCHDIFF variable") unless (length $PATCHDIFF);
   my $num = ($OPTS{'before'} > $OPTS{'after'}) ? $OPTS{'before'} : $OPTS{'after'};
   local $_ = join(" ", sprintf($PATCHDIFF, $num), @files);
   &FatalMsg("Can't use 'patch' format for more than 2 files") if (@files > 2);
   system($_);
   exit($? / 256);
}

## Perform a context-diff for the given files
sub ContextDiff {
   my @files = @_;
   local $_ = "";
   ## Keep track of the number of "changes" we see
   my $numChanges = 0;
   ## Construct the cleardiff command and read the output
   my $diffCmd = "$CLEARBIN/cleardiff";
   $diffCmd = "$CLEARTOOL diff" if ($OPTS{'predecessor'});
   $diffCmd .= " -serial_format";
   if ($OPTS{'predecessor'}) {
      $diffCmd .= ' -predecessor';
      $diffCmd .= ' -options "-headers_only';
      $diffCmd .= ' -blank_ignore' if ($OPTS{blank_ignore});
      $diffCmd .= '"';
   }
   else {
      $diffCmd .= ' -headers_only';
      $diffCmd .= ' -blank_ignore' if ($OPTS{blank_ignore});
   }
   $diffCmd .= ' ' . join(' ', @files);
   open(CLEARDIFF, "$diffCmd | ") || &FatalMsg("Can't run '$diffCmd': $!");
   while (<CLEARDIFF>) {
      chomp;
      next if /^\*+$/;
      if (/^\s*Files are identical\s*$/i) {
         &ErrorMsg($_);
      }
      elsif (/^(<<<|>>>)\s+file\s+(\d+):\s+(.*)$/) {
         my($fileNum, $fileName) = ($2, $3);
         push(@FILES, $fileName);
         if ($fileNum != @FILES) {
            &FatalMsg("Unexpected file order in diff header.");
         }
      }
      elsif (/^\-+\[(.+)\]\-+$/) {
         &PatchDiff(@FILES) if ($FMT{'patch'});
         &PrintPrologue unless ($numChanges++);
         &ParseDifference($1);
      }
      else {
         &FatalMsg("Unexpected line in diff output:\n\t$_");
      }
   }
   close(CLEARDIFF);
   ## Don't forget to cleanup
   &ParseDifference($EndOfDiff) if ($numChanges);
   return  ($numChanges > 0);
}

## Match a string against a set of keywords (case insensitive and
## allowing unique prefixes). Return the list of matching elements.
sub MatchKeyword {
   my($str, @keywords) = @_;
   local $_ = "";
   my @matches = grep(m/^${str}$/i, @keywords);
   @matches = grep(m/^${str}/i, @keywords) if (@matches == 0);
   return  @matches;
}

## Return the maximum of a list of numbers
sub Max {
   my @nums = @_;
   my $max = shift @nums;
   for (@nums) {
      $max = $_ if ($_ > $max);
   }
   return  $max;
}

## Set up and verify the specified formats
sub SetupFormats {
   my @fmtSpecs = @_;
   local $_ = "";
   my($errors, @matches);
   foreach (@fmtSpecs) {
      my($name, $text) = ($_, 1);
      ($name, $text) = ($1, $2) if (/^([^=]*)=(.*)$/);
      @matches = &MatchKeyword($name, 'header', (keys %FMT));
      if (@matches == 1) {
         $name = shift(@matches);
         if ($name eq 'header') {
            $FMT{'lheader'} = $FMT{'rheader'} = $text;
         }
         else {
            $FMT{$name} = $text;
         }
      }
      elsif (@matches == 0) {
         &ErrorMsg("invalid format specifier \"$_\"");
         ++$errors;
      }
      else {
         &ErrorMsg("ambiguous format specifier \"$_\"");
         ++$errors;
      }
   }
   ## Make movedfrom/movedto default to inserted/deleted
   $FMT{'movedfrom'} ||= $FMT{'deleted'};
   $FMT{'movedto'}   ||= $FMT{'inserted'};

   ## Now make sure that $FMT{'deleted'}, $FMT{'inserted'},
   ## and $FMT{'common'} are all the same length (padding them
   ## on the right with spaces if necessary)
   my %len = map { $_ => length $FMT{$_} }
                qw(deleted inserted movedfrom movedto common);
   my $maxlen = &Max(values %len);
   for (keys %len) {
      $FMT{$_}  = sprintf("%-${maxlen}s", $FMT{$_}) if ($len{$_} < $maxlen);
   }

   ## Now do the same for $FMT{'base'} and $FMT{'contrib'}
   %len = map { $_ => length $FMT{$_} } qw(base contrib);
   $maxlen = &Max(values %len);
   for (keys %len) {
      $FMT{$_}  = sprintf("%-${maxlen}s", $FMT{$_}) if ($len{$_} < $maxlen);
   }

   return  (! $errors);
}

## main -- procedural interface to the script.
sub main {
   local @ARGV = @_;
   local $_;

      ## Parse options and check for help
   my $rc = &GetOptions( \%OPTS,
         "help", "version", "debug",
         "output=s",
         "after=i", "before=i", "unlimited",
         "blank_ignore", "predecessor",
         "fmt=s@",
         "linenumbers|lines|numbers",
         "reflinenumbers|reflines|refnumbers",
         "nocontext|ncontext",
         "noheaders|nheaders",
         "noprologue|nprologue",
         "tabstop=i"
   );
   &VersionMsg(1) if ($OPTS{'version'});
   &Usage(1) if ($OPTS{'help'});

   ## Force the -pred option if only one XNAME argument is supplied
   if (@ARGV == 1) {
      $OPTS{'predecessor'} = 1;
   }
   ## Check for errors
   if (@ARGV == 0) {
      &ErrorMsg("Too few arguments given.");
      $rc = 0;
   }
   &Usage(2) if (! $rc);

   ## Redirect STDOUT if requested
   if ($OPTS{'output'}) {
      close STDOUT;
      open(STDOUT, ">$OPTS{'output'}")
         or &FatalMsg("Unable to redirect STDOUT to $OPTS{'output'}: $!");
   }

   ## Set the tabstop if it was given
   $Text::Tabs::tabstop = $OPTS{'tabstop'}  if (defined $OPTS{'tabstop'});

   ## Set up and verify any specified formats
   &SetupFormats(@{ $OPTS{'fmt'} }) || exit(2);

   ## Set up default number of context lines
   my $default_context = ($FMT{'patch'} ? 3 : 5);
   $OPTS{'before'} = $default_context unless (defined $OPTS{'before'});
   $OPTS{'after'}  = $default_context unless (defined $OPTS{'after'});

   ## Verify the versions exist
   my(@filelist, $element, $selector, $verpath, $xpname);
   @filelist = ();
   foreach (@ARGV) {
      if (/^(.+)${CLEARCASE_XN_SFX}(.+)$/) {
         ($element, $selector) = ($1, $2);
         ##$verpath = &clearlib'CanonXPName($element, $selector);
         $verpath = $selector;
         &FatalMsg("Can't find $_") unless ($verpath);
         $xpname = $element . $CLEARCASE_XN_SFX . $verpath;
      }
      else {
         $xpname = $_;
         if (-e $_) {
            $element = $_;
         }
         else {
            ##$verpath = &clearlib'CanonXPName($element, $_) if ($element);
            $verpath = $_  if ($element);
            $xpname = $element . $CLEARCASE_XN_SFX . $verpath;
         }
         &FatalMsg("Can't find $_") unless ($xpname);
      }
      if ($xpname !~ /^.+${CLEARCASE_XN_SFX}.+$/) {
         &FatalMsg("Can't find $_") unless (-e $xpname);
      }
      push(@filelist, $xpname);
      &FatalMsg("$xpname is a directory")   if (-d $xpname);
      &FatalMsg("$xpname is a binary file") if (-B $xpname);
   }

   my $changes = &ContextDiff(@filelist);
   return  $changes;
}


#---------------------------------------------------------------------
package main;

exit( &CDiff::main(@ARGV) );  ##  unless ($DO_NOT_EXECUTE);

