#!/usr/bin/perl -w
########################################################################
# Pre and post process files containing cpp directive lines.
# It is necessary to modify update input so that the cpp directive lines
# do not begin with "#" since shell comment lines will be removed by update.
#
# cpp directive lines always begin with # (pound sign) followed by zero
# or more spaces, then the cpp directive name, optionally (depending on the
# directive name) followed by more tokens.
#
# During preprocessing:
#   Prepend the a predefined string to all cpp directive lines
# During post processing:
#   Remove the predefined string from all cpp directive lines
#
# Larry Solheim Nov 10,2008
########################################################################

require 5;
use File::Basename;
use Getopt::Long;
use Text::Tabs;

# define a unique stamp for file name id etc
chomp($stamp = `date "+%j"$$`);

# Identify this script by name
chomp($Runame = `basename $0`);

# pfx is the prefix inserted at the start of every cpp line. This should
# start with the letter "c" and be relatively complex to limit the
# likelyhood of a chance match in any processed script.
$pfx="cpp3075296";

# verbose controls the amount of info written to stdout
$verbose = 0;

# hide_cpp flags addition or deletion of the string $pfx in the input file
# $hide_cpp=1 means prepend the string $pfx to cpp directive lines
# $hide_cpp=0 means remove the string $pfx that was added to the
#             beginning of cpp directive lines on a previous invocation
$hide_cpp=1;

# $expand_tabs flags tabs expansion during pre processing
$expand_tabs = 1;

# $keep_comments flags removal of all shell comment lines, that are not cpp
# directives, from the file being processed.
$keep_comments = 0;

# Flist will contain all non-option command line args which should be file names
my @Flist = ();

# Define a usage function
$Usage = sub {
  my ($msg)=@_;
  if ($msg) {print "${Runame}: $msg\n"};
  print <<EOR;
  Usage: $Runame [options] file ...
Purpose: comment or uncomment all cpp directive lines found in a file
         This was originally intended for use with files passed to/from update
         Note, output files will overwrite input files.
Options:
  --undo              ...remove comments that were previously added to cpp directive lines.
                         By default this program will prepend each cpp directive line found
                         in each input file with a known string. If "--undo" is used on the
                         command line, then the leading string that was added on a previous
                         invocation will be removed from the start of each cpp directive line.
  --[no]expand_tabs   ...expand [do not expand] tabs found in the input file.
                         The default is to expand tabs.
  --[no]keep_comments ...keep [do not keep] any shell comment lines that are not also cpp
                         directive lines. The default is to remove shell comment lines.
  --verbose           ...increase verbosity (additive)
  --help              ...show this usage info
EOR
  die "\n";
};

# Process command line arguments
$Getopt::Long::ignorecase = 0;
$Getopt::Long::order = $PERMUTE;
&GetOptions("help"            => \&$Usage,
            "verbose"         => sub {$verbose++},
            "undo"            => sub {$hide_cpp=0},
            "expand_tabs!"    => \$expand_tabs,
            "keep_comments!"  => \$keep_comments,
            "<>"              => sub {push @Flist,$_[0]})
  or die "Error on command line. Stopped";

&$Usage("At least one file name is required on the command line") unless scalar(@Flist);

foreach my $fname (@Flist) {
  next unless $fname;
  if (-l $fname) {
    # If this is a link then redefine fname as the file the link points to
    $fname = readlink $fname;
  }
  die "$fname is not a regular file. Stopped" unless -f "$fname";

  # Determine directory name and base name from fname
  my ($bname, $fdir) = fileparse("$fname", ());

  $tmpfile = "${bname}_${stamp}";
  open (IFILE, "<$fname")   || die "cannot open $fname for input";
  open (OFILE, ">$tmpfile") || die "cannot open $tmpfile for output";
  while (<IFILE>) {
    my $line = $_;
    if ($verbose > 10) {print "line  in: $line\n"}
    if ($hide_cpp) {
      # Pre processing: Prepend the string $pfx to any cpp directive lines
      # Expand any tabs in the current line, if requested
      $line = expand($line) if $expand_tabs;
      if ($line =~ /!\s*cpp\s*$/) {
        # If the line ends with the string "!cpp" then assume that
        # it is a cpp directive line; prepend the string $pfx and
        # remove the trailing "!cpp"
        $line =~ s/^(.*)!\s*cpp\s*$/$pfx$1\n/;
      } elsif ($line =~ /!\s*nocpp\s*$/) {
        # If the line ends with the string "!nocpp" then assume this is not
        # a cpp directive line and pass the line through unmodified
        if ($verbose > 10) {print "line ends with !nocpp: $line\n"}
        unless ($keep_comments) {
          $line = '' if $line =~ /^\s*#/;
        }
      } else {
        # Test for cpp directive syntax
        my $is_cpp_line = 0;
        if ($line =~ /^#\s*(else|endif)\s*$/) {
          # else
          # endif
          $is_cpp_line=1;
        } elsif ($line =~ /^#\s*define(\s+\w+){1,2}\s*$/) {
          # define Name
          # define Name TokenString
          $is_cpp_line=1;
        } elsif ($line =~ /^#\s*define\s+\w+\(/) {
          # define Name(...) ...
          $is_cpp_line=1;
        } elsif ($line =~ /^#\s*(undef|ifdef|ifndef)\s+\w+\s*$/) {
          # undef  Name
          # ifdef  Name
          # ifndef Name
          $is_cpp_line=1;
        } elsif ($line =~ /^#\s*include\s+/) {
          # include "File"
          # include <File>
          $is_cpp_line=1;
        } elsif ($line =~ /^#\s*(if|elif)\s+[(\s!]*defined[\s(]/) {
          # if defined ...
          # if (defined ...
          # if (defined(...
          # elif defined ...
          # elif (defined ...
          # elif (defined(...
          $is_cpp_line=1;
#XXX        } elsif ($line =~ /^#\s*(if|elif)\s+(defined\s+)(\w+|\(\w+\))/) {
#XXX          # if defined Name
#XXX          # elif defined Name
#FIX        } elsif ($line =~ /^#\s*(if|elif)\s+[a-zA-Z0-9_#(]+/) { # LPS: FIXME
#FIX          # if   EXPRESSION
#FIX          # elif EXPRESSION
#FIX          #   EXPRESSION is a C expression of integer type, subject to stringent
#FIX          #     restrictions.  It may contain
#FIX          #   * Integer constants
#FIX          #   * Character constants, which are interpreted according to the
#FIX          #     character set and conventions of the machine and operating system
#FIX          #     on which the preprocessor is running.  The GNU C preprocessor uses
#FIX          #     the C data type `char' for these character constants; therefore,
#FIX          #     whether some character codes are negative is determined by the C
#FIX          #     compiler used to compile the preprocessor.  If it treats `char' as
#FIX          #     signed, then character codes large enough to set the sign bit will
#FIX          #     be considered negative; otherwise, no character code is considered
#FIX          #     negative.
#FIX          #   * Arithmetic operators for addition, subtraction, multiplication,
#FIX          #     division, bitwise operations, shifts, comparisons, and logical
#FIX          #     operations (`&&' and `||').
#FIX          #   * Identifiers that are not macros, which are all treated as zero(!).
#FIX          #   * Macro calls.  All macro calls in the expression are expanded before
#FIX          #     actual computation of the expression's value begins.
#FIX          #   * the unary operator "defined" which can be used as
#FIX          #     defined (Name) or defined Name.
#FIX          $is_cpp_line=1;
        }
        # If this is a cpp directive line then prepend $pfx
        if ($is_cpp_line) {substr($line,0,0) = $pfx}
        if ($is_cpp_line and $verbose > 10) {print "line out: $line\n"}
        # Remove all other shell comment lines if requested
        unless ($keep_comments) {
          $line = '' if $line =~ /^\s*#/;
        }
      }
    } else {
      # Post processing:
      # Remove the string "cpp", that was inserted during preprocessing,
      # from the start of any cpp directive lines
      $line =~ s/^$pfx//;
    }
    if ($verbose > 10) {print "line out: $line\n"}
    print OFILE "$line" if $line;
  }
  close IFILE;
  close OFILE;

  # Overwrite the original file with the tmp file.
  # If the original file is a link, this link will be replaced
  # by a local copy of the modified file.
  die "Error in \"mv $tmpfile $fname\"\n  $!\n  Stopped"
    unless rename("$tmpfile", "$fname");

}
