#!perl
#---------------------------------------------------------------------
# vernum.pl 1.7 1998/11/04 18:24:01 Madsen Exp
# Copyright 1996 Christopher J. Madsen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Perl; see the file COPYING.  If not, write to the
# Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
#
# Insert version numbers in files
# (See documentation at end of this file or use pod2man.)
#---------------------------------------------------------------------

require 5.000;
use strict;

use File::Basename;
use Getopt::Mixed;

my @months = qw(* Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
my $verDigits = 2;              # Number of digits after decimal point

my $version;
my $date;

#=====================================================================
Getopt::Mixed::getOptions(
    'digits=i d>digits',
    'no-master n>no-master',
    'keep-paths p>keep-paths',
    'help ?>help version'
);

usage(0) if $::opt_help or $::opt_help;

if ($::opt_version or $::opt_version) {
    print 'VerNum 1.7 ',"\n";
    exit 0;
}

if (defined $::opt_digits) {
    $verDigits = $::opt_digits;
    $verDigits = '' unless $verDigits > 1;
}

usage(1) unless $#ARGV > 0;     # Must have at least 2 arguments

foreach (@ARGV) { tr|\\|/| }    # Convert backslashes

my $outDir = shift @ARGV;
$outDir =~ s|/$||;           # Delete trailing slash
die "$outDir does not exist" unless -d $outDir;

my $masterFile = shift @ARGV;
my $masterBase = basename($masterFile);
my $outFile = "$outDir/" . ($::opt_keep_paths ? $masterFile : $masterBase);

$outFile = "/dev/null" if $::opt_no_master or $::opt_no_master;

open(IN, "<$masterFile") or die "Can't open $masterFile";
open(OUT,">$outFile")    or die "Can't write to $outFile";

print "Processing $masterFile...\n";

while (<IN>) {
    if (/\$Id:/) {
        s!\$Id\: [^ ]+ ([0-9.]+) ([0-9/]{10}) ([0-9:]{8}) .+?\$!
          sprintf("%s %s (%s %s GMT)",
                  $masterBase, parseVer($1), parseDate($2), $3)!e
                or die "Invalid \$Id\$ string";
    }
    s/\$Revision\: (.+?) \$/parseVer($1)/ge;
    s!\$Date\: ([0-9/]+).+?\$!parseDate($1)!ge;
    next if /^\$\$/;            # $$ indicates comment
    s/\$\%\s*(.+?)\s*\%\$/processCode($1)/ge;
    print OUT;
} # end while

close IN;
close OUT;

utime((stat($masterFile))[8,9], $outFile); # Restore original timestamp

#---------------------------------------------------------------------
my $file;

foreach $file (@ARGV) {
    my $outFile = "$outDir/" . ($::opt_keep_paths ? $file : basename($file));

    open(IN, "<$file")    or die "Can't open $file";
    open(OUT,">$outFile") or die "Can't write to $outFile";

    print "Processing $file...\n";

    while (<IN>) {
        next if /^\$\$/;        # $$ indicates comment
        s/\$\%\s*(.+?)\s*\%\$/processCode($1)/ge;
        print OUT;
    } # end while

    close IN;
    close OUT;

    utime((stat($file))[8,9], $outFile); # Restore original timestamp
} # end foreach

exit;

#=====================================================================
# Subroutines
#---------------------------------------------------------------------
# Parse an RCS date field:
#
# Dies if the date does not match a previously seen date.
#
# Input:
#   The date in yyyy/mm/dd format
#
# Returns:
#   The date in d-mmm-yyyy format

sub parseDate
{
    my $dateStr = shift;

    my ($year,$month,$day) = split(m|/|, $dateStr);
    $dateStr = sprintf("%d-%s-%s",$day, $months[$month], $year);

    die "Dates do not match" if $date and $date ne $dateStr;

    $date = $dateStr;
} # end parseDate

#---------------------------------------------------------------------
# Parse a RCS version number field:
#
# Dies if the version number does not match a previously seen version
# number.
#
# Input:
#   The RCS revision number
#
# Returns:
#   The version number

sub parseVer
{
    my $verStr = shift;

    $verStr =~ m/^(\d+)\.(\d+)([0-9.]*)$/ or die;
    $verStr = sprintf("%d.%0${verDigits}d%s",$1,$2,$3);

    die "Versions do not match" if $version and $version ne $verStr;

    $version = $verStr;
} # end parseVer

#---------------------------------------------------------------------
# Process a special code:
#
# Input:
#   The code minus the identifying marks and whitespace
#
# Returns:
#   The text that should be substituted

sub processCode
{
    my $code = shift;

    $code =~ s/([a-z])$/s/ or die "Invalid format";

    my $data = do {
        if    ($1 eq 'd') { $date }
        elsif ($1 eq 'v') { $version }
        else              { die "Unknown code `$1'"}
    };

    die "No data for code `$1'" unless $data;

    sprintf("%$code", $data);
} # end processCode

#---------------------------------------------------------------------
# Display usage information and exit:
#
# Input:
#   exit status (optional, default is 0)

sub usage
{
    my $exitStatus = shift;
    $exitStatus = 0 unless $exitStatus;

    print <<'EOT';
VerNum 1.7

Usage:  vernum [options] DIRECTORY MASTER [FILE ...]
  -d, --digits=N     Use at least N digits after the decimal point
  -n, --no-master    Do not copy MASTER
  -p, --keep-paths   Keep paths specified on command line
  -?, --help         Display this usage information and exit
      --version      Display version number and exit
EOT
    exit $exitStatus;
} # end usage

__END__

=head1 NAME

vernum - Replace RCS keywords and insert version numbers

=head1 SYNOPSIS

B<vernum> [B<-p>] [B<-d width>] DIRECTORY MASTER [FILE ...]

=head1 DESCRIPTION

B<vernum> copies I<MASTER> and any I<FILE>s to I<DIRECTORY>.  While
doing so, it formats RCS keywords in I<MASTER> (similar to B<co -kv>)
and processes special codes in all files.  It is normally used to
reformat version numbers and to insert them in README files and other
documentation.

B<vernum> processes the following RCS keywords in I<MASTER>:

=over 10

=item C<Date>

Replaced by the date in dd-Mmm-yyyy format.
Single digit dates are not padded.

=item C<Id>

Replaced with "filename version (date time)".  The version number and
date are formatted as explained under the C<Revision> and C<Date>
keywords.

=item C<Revision>

Replaced by the version number.  The second part of the version number
is left padded with zeros.  The width of the second part is set by the
B<-d> option (default 2).  You can use B<-d1> to use the RCS version
number unchanged.

=back

All other RCS keywords are left unchanged.

B<vernum> also processes special codes, which are delimited by `$%'
and `%$'.  Whitespace between the delimiters and the code is ignored.
The only codes are:

=over 4

=item B<d>

The date in dd-Mmm-yyyy format

=item B<v>

The version number

=back

The values are taken from the RCS keywords C<Date>, C<Id>, and
C<Revision> in I<MASTER>.  You can use B<printf> formatting codes to
format the result:

 Example        Result         Notes
 |$%v%$|        |1.03|         Just like Revision keyword
 |$%-7v%$|      |1.03   |      Left justified in 7 char field
 |$%d%$|        |1-Jan-1996|   Just like Date keyword
 |$%    11d%$|  | 1-Jan-1996|  Right justified in 11 char field

Furthermore, any line in I<FILE>s beginning with `$$' is a comment and
is removed.  `$$' comments are not supported in I<MASTER>, since
programming languages usually have their own comment facilities.

=head1 OPTIONS

=over 5

=item B<-d N>, B<--digits=N>

Use at least I<N> digits in the second part of version numbers.  The
default is 2.  Use B<-d1> to keep the RCS version number unchanged.

=item B<-p>, B<--keep-paths>

Normally, B<vernum> copies all files to I<DIRECTORY>, regardless of
their current location.  The B<-p> option causes it to keep the paths
specified on the command line (which must be relative paths).  e.g.,

    vernum dir lib/Getopt/Mixed.pm README

will write to dir/lib/Getopt/Mixed.pm and dir/README.  This will
I<not> create subdirectories; they must already exist.

=item B<-?>, B<--help>

Display usage information and exit.

=item B<--version>

Display version number and exit.

=back

=head1 REQUIREMENTS

B<vernum> requires Getopt::Mixed, available on CPAN as
    "CPAN"/authors/id/CJM/Getopt-Mixed-1.008.tar.gz

=head1 BUGS

I just hacked this together for my own use, because I got tired of
having to update README files and other documents by hand.  Thus, only
features I've needed are included.  Contact me if you want to add some
more features.

B<vernum> does little error checking.  Be careful.

=head1 AUTHOR

Christopher J. Madsen E<lt>F<chris_madsen@geocities.com>E<gt>