#!/usr/bin/perl -w
########################################################################
#
# filepp 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 of the License, 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 this program; see the file COPYING.  If not, write to
# the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
#
########################################################################
#
#  Project      :  File Preprocessor
#  Filename     :  $RCSfile: filepp.in,v $
#  Author       :  $Author: darren $
#  Maintainer   :  Darren Miller: darren@cabaret.demon.co.uk
#  File version :  $Revision: 1.70 $
#  Last changed :  $Date: 2001/06/07 22:14:42 $
#  Description  :  Main program
#  Licence      :  GNU copyleft
#
########################################################################

package Filepp;

use strict "vars";
use strict "subs";

# version number of program
my $VERSION = '1.3.0';

# list of paths to search for modules, normal Perl list + module dir
push(@INC, "/usr/local/share/filepp/modules");

# index of keywords supported and functions to deal with them
my %Keywords = (
		'comment', \&Comment,
		'define',  \&Define,
		'elif',    \&Elif,
		'else',    \&Else,
		'endif',   \&Endif,
		'error',   \&Error,
		'if',      \&If,
		'ifdef',   \&Ifdef,
		'ifndef',  \&Ifndef,
		'include', \&Include,
		'pragma',  \&Pragma,
		'undef',   \&Undef,
		'warning', \&Warning
		);
# sort keywords index into reverse order, this ensures #if[n]def comes
# before #if when comparing input with keywords
my @Keywords = sort {$b cmp $a} (keys(%Keywords));

# set of functions which process the file in the Parse routine.
# Processors are functions which take in a line and return the processed line.
# Note: this is done as a string rather than pointer to a function because
# it makes list easier to modify/remove from/print.
my @Processors = ( "ReplaceDefines" );

# safe mode is for the paranoid, when enabled turns off #pragma filepp,
# enabled by default
my $safe_mode = 0;

# test for shebang mode, used for "filepp script", ie. executable file with
# "#!/usr/bin/perl /usr/local/bin/filepp" at the top
my $shebang = 1;

# allow $keywordchar and $contchar to be perl regular expressions
my $charperlre = 0;

# character(s) which prefix environment variables - defaults to shell-style '$'
my $envchar = "\$";

# character(s) which replace continuation char(s) - defaults to C-style nothing
my $contrepchar = "";

# character(s) which prefix keywords - defaults to C-style '#'
my $keywordchar;
if($charperlre) { $keywordchar = "\#"; }
else            { $keywordchar = "\Q#\E"; }

# character(s) which signifies continuation of a line - defaults to C-style '\'
my $contchar;
if($charperlre) { $contchar = "\\\\"; }
else            { $contchar = "\Q\\\E"; }

# check if macros must occur as words when replacing, set this to '\b' if
# you prefer cpp style behaviour as default
my $bound = '';

# number of line currently being parsed (int)
my $line = 0;

# file currently being parsed
my $file = "";

# base file currently being parsed
my $base_file = "";

# list of input files
my @Inputfiles;

# flag to control when output is written
my $output = 1;

# name of outputfile - defaults to STDOUT
my $outputfile = "";

# set if file being written to has same name as input file
my $same_file = 0;

# list of keywords which have "if" functionality
my %Ifwords = ('if',     '',
	       'ifdef',  '',
	       'ifndef', '');

# list of keywords which have "else" functionality
my %Elsewords = ('else', '',
		 'elif', '');

# list of keywords which have "endif" functionality
my %Endifwords = ('endif', '');

# current level of include files
my $include_level = -1;

# suppress blank lines in header files (indexed by include level)
my $blanksuppopt = 0;
my @blanksupp;

# counter of recursion level for detecting recursive macros
my $recurse_level = -1;

# debugging info, 1=on, 0=off
my $debug = 0;

# conversions of month number into letters (0-11)
my %MonthChars = ('00', 'Jan',
		  '01', 'Feb',
		  '02', 'Mar',
		  '03', 'Apr',
		  '04', 'May',
		  '05', 'Jun',
		  '06', 'Jul',
		  '07', 'Aug',
		  '08', 'Sep',
		  '09', 'Oct',
		  '10', 'Nov',
		  '11', 'Dec');

#prepare standard defines
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isbst) = 
    localtime(time());
$year += 1900;
$sec  = IntPad($sec,  2);
$min  = IntPad($min,  2);
$hour = IntPad($hour, 2);
$mday = IntPad($mday, 2);
$mon  = IntPad($mon,  2);
my $time = "$hour:$min:$sec";
my $date = "$MonthChars{$mon} $mday $year";
$mon = IntPad(++$mon, 2);
my $isodate = "$year-$mon-$mday";

# hash of macros defined - standard ones already included
my %Defines = (
	       '__BASE_FILE__',     "$base_file",
	       '__DATE__',          "$date",
	       '__FILEPP_INPUT__',  "Generated automatically from __BASE_FILE__ by filepp",
	       '__FILE__',          "$file",
	       '__INCLUDE_LEVEL__', "$include_level",
	       '__ISO_DATE__',      "$isodate",
	       '__LINE__',          "$line",
	       '__NEWLINE__',       "\n",
	       '__NULL__',          "",
	       '__TAB__',           "\t",
	       '__TIME__',          "$time",
	       '__VERSION__',       "$VERSION"
	       );
# hash of first chars in each macro
my %DefineLookup;
# length of longest and shortest define
my ($defmax, $defmin);
GenerateDefinesKeys();

# hash table for arguments to macros which need them
my %DefinesArgs = ();

# eat-trailing-whitespace flag for each macro
my %EatTrail = ();

# list of include paths
my @IncludePaths;

# help string
my $usage = "filepp: generic file preprocessor, version $VERSION
usage: filepp [options] inputfile(s)
options:
 -b\t\tsuppress blank lines from include files
 -c\t\tread input from STDIN instead of file
 -Dmacro[=defn]\tdefine macros (same as #define)
 -d\t\tprint debugging information
 -dd\t\tprint verbose debugging information
 -e\t\tdefine all environment variables as macros
 -ec char\tset environment variable prefix char to \"char\" (default $envchar)
 -ecn\t\tset environment variable prefix char to nothing (default $envchar)
 -h\t\tprint this help message
 -Idir\t\tdirectory to search for include files
 -k\t\tturn off parsing of all keywords, just macro expansion is done
 -kc char\tset keyword prefix char to \"char\" (defaults to #)
 -lc char\tset line continuation character to \"char\" (defaults to \\)
 -lr char\tset line continuation replacement character to \"char\"
 -lrn\t\tset line continuation replacement character to newline
 -m module\tload module
 -Mdir\t\tdirectory to search for filepp modules
 -o output\tname of output file (defaults to stdout)
 -s\t\trun in safe mode (turns off pragma keyword)
 -re\t\ttreat keyword prefix and line continuation chars as reg. exps.
 -u\t\tundefine all predefined macros
 -v\t\tprint version and exit
 -w\t\tturn on word boundaries when replacing macros
 all other arguments are assumed to be input files
";


##############################################################################
# Debugging info
##############################################################################
sub Debug
{
    # print nothing if not debugging
    if($debug == 0) { return; }    
    my $msg = shift;
    # if currently parsing a file show filename and line number
    if($file ne "" && $line > 0) {
	$msg = "$file:$line: $msg";
    }
    # else show program name
    else { $msg = "filepp: $msg"; }
    print(STDERR "$msg\n");
}


##############################################################################
# Standard error handler.
# #error msg  - print error message "msg" and exit
##############################################################################
sub Error
{
    my $msg = shift;
    # close and delete output file if created
    close(OUTPUT);
    if($outputfile ne "-") { # output is not stdout
	my $inputfile;
	my $found = 0;
	# do paranoid check to make sure we are not deleting an input file
	foreach $inputfile (@Inputfiles) {
	    if($outputfile eq $inputfile) { $found = 1; }
	}
	# delete output file
	if($found == 0) { unlink($outputfile); }
    }
    # print error message
    $debug = 1;
    Debug($msg);
    exit(1);
}


##############################################################################
# SafeMode - turns safe mode on
##############################################################################
sub SafeMode
{
    $safe_mode = 1;
    Debug("Filepp safe mode enabled");
}


##############################################################################
# IntPad($int, $pad) Pad an integer $int with zeros to width $pad
##############################################################################
sub IntPad
{
    my $int = shift;
    my $pad = shift;
    while(length($int) < $pad) {
	$int = "0$int";
    }
    return $int;
}


##############################################################################
# CleanStart($sline) - strip leading whitespace from start of $sline.
##############################################################################
sub CleanStart
{
    my $sline = shift;
    for($sline) {
	# '^' = start of line, '\s+' means all whitespace, replace with nothing
	s/^\s+//;
    }
    return $sline;
}


##############################################################################
# CleanStartEnd($sline) - strip leading whitespace from start and end of
# $sline
##############################################################################
sub CleanStartEnd
{
    my $sline = shift;
    for($sline) {
	# '^' = start of line, '\s+' means all whitespace, replace with nothing
	s/^\s+//;
	# '$' = end of line, '\s+' means all whitespace, replace with nothing
	s/\s+$//m;
    }
    return $sline;
}


##############################################################################
# Strip($sline, $char, $level) - strip $char's from start and end of $sline
# removes up to $level $char's from start and end of line, it is not an
# error if $level chars do not exist at the start or end of line
##############################################################################
sub Strip
{
    my $sline = shift;
    my $char = shift;
    my $level = shift;
    # strip leading chars from line
    $sline =~ s/^([$char]{0,$level})//g;
    # strip trailing chars from line
    $sline =~ s/([$char]{0,$level})$//g;
    return $sline;
}


##############################################################################
# SetKeywordchar $string - sets the first char(s) of each keyword to
# something other than "#"
##############################################################################
sub SetKeywordchar
{
    $keywordchar = shift;
    # make sure char will not be treated as a Perl regular expression
    if(!$charperlre) { $keywordchar = "\Q$keywordchar\E"; }
    Debug("Setting keyword prefix character to <$keywordchar>");
}


##############################################################################
# SetContchar $string - sets the line continuation char to something other
# than "\"
##############################################################################
sub SetContchar
{
    $contchar = shift;
    # make sure char will not be treated as a Perl regular expression
    if(!$charperlre) { $contchar = "\Q$contchar\E"; }
    Debug("Setting line continuation character to <$contchar>");
}


##############################################################################
# SetContrepchar $string - sets the replace of the line continuation char to
# something other than ""
##############################################################################
sub SetContrepchar
{
    $contrepchar = shift;
    Debug("Setting line continuation replacement character to <$contrepchar>");
}


##############################################################################
# SetEnvchar $string - sets the first char(s) of each defined environment
# variable to $string - NOTE: change only takes effect when DefineEnv run
##############################################################################
sub SetEnvchar
{
    $envchar = shift;
    Debug("Setting environment variable prefix character to <$envchar>");
}

##############################################################################
# PrintProcessors
# print the current processing chain
##############################################################################
sub PrintProcessors
{
    my $processor;
    Debug("Current processing chain:");
    foreach $processor (@Processors) {
	Debug("$processor");
    }
}

##############################################################################
# AddProcessor(function)
# add a line processor to the processing chain
##############################################################################
sub AddProcessor
{
    my $function = shift;    
    push(@Processors, $function);
    Debug("Added processor $function");
    if($debug > 1) { PrintProcessors(); }
}


##############################################################################
# RemoveProcessor(function)
# remove a processor name "function" from list
##############################################################################
sub RemoveProcessor
{
    my $function = shift;    
    my $i = 0;
    # find function
    while($i <= $#Processors && $Processors[$i] ne $function) { $i++; }
    # check function found
    if($i > $#Processors) {
	Warning("Attempt to remove function $function which does not exist");
	return;
    }
    # remove function
    for(; $i<$#Processors; $i++) {
	$Processors[$i] = $Processors[$i+1];
    }
    pop(@Processors);
    Debug("Removed processor $function");
    if($debug > 1) { PrintProcessors(); }
}


##############################################################################
# AddKeyword(keyword, function)
# Define a new keyword, when keyword (preceded by keyword char) is found,
# function is run on the remainder of the line.
##############################################################################
sub AddKeyword
{
    my $keyword = shift;
    my $function = shift;    
    $Keywords{$keyword} = $function;
    @Keywords = sort {$b cmp $a} (keys(%Keywords));
    Debug("Added keyword $keyword which runs $function");
}


##############################################################################
# RemoveKeyword(keyword)
# Keyword is deleted from list, all occurrences of keyword found in
# document are ignored.
##############################################################################
sub RemoveKeyword
{
    my $keyword = shift;
    delete $Keywords{$keyword};
    # sort keywords index into reverse order, this ensures #if[n]def comes
    # before #if when comparing input with keywords
    @Keywords = sort {$b cmp $a} (keys(%Keywords));
    Debug("Removed keyword $keyword");
}


##############################################################################
# RemoveAllKeywords - removes all current keywords.
##############################################################################
sub RemoveAllKeywords
{
    %Keywords = ();
    @Keywords = keys(%Keywords);
    Debug("Removed all current keywords");
}


##############################################################################
# AddIfword - adds a keyword to ifword hash
##############################################################################
sub AddIfword
{
    my $ifword = shift;
    $Ifwords{$ifword} = '';
    Debug("Added Ifword: $ifword");
}

##############################################################################
# RemoveIfword - removes a keyword from ifword hash
##############################################################################
sub RemoveIfword
{
    my $ifword = shift;
    delete $Ifwords{$ifword};
    Debug("Removed Ifword: $ifword");
}

##############################################################################
# AddElseword - adds a keyword to elseword hash
##############################################################################
sub AddElseword
{
    my $elseword = shift;
    $Elsewords{$elseword} = '';
    Debug("Added Elseword: $elseword");
}

##############################################################################
# RemoveElseword - removes a keyword from elseword hash
##############################################################################
sub RemoveElseword
{
    my $elseword = shift;
    delete $Elsewords{$elseword};
    Debug("Removed Elseword: $elseword");
}

##############################################################################
# AddEndifword - adds a keyword to endifword hash
##############################################################################
sub AddEndifword
{
    my $endifword = shift;
    $Endifwords{$endifword} = '';
    Debug("Added Endifword: $endifword");
}

##############################################################################
# RemoveEndifword - removes a keyword from endifword hash
##############################################################################
sub RemoveEndifword
{
    my $endifword = shift;
    delete $Endifwords{$endifword};
    Debug("Removed Endifword: $endifword");
}


##############################################################################
# AddIncludePath - adds another include path to the list
##############################################################################
sub AddIncludePath
{
    my $path = shift;
    push(@IncludePaths, $path);
    Debug("Added include path: \"$path\"");
}


##############################################################################
# AddModulePath - adds another module search path to the list
##############################################################################
sub AddModulePath
{
    my $path = shift;
    push(@INC, $path);
    Debug("Added module path: \"$path\"");
}


##############################################################################
# OpenOutputFile - opens the output file
##############################################################################
sub OpenOutputFile
{
    my $outputfile = shift;
    Debug("Output file: $outputfile");
    if($outputfile ne "-") { # output is not stdout
	# check input filename is not same as output - will destroy input file
	my $inputfile;
	foreach $inputfile (@Inputfiles) {
	    if($outputfile eq $inputfile) {
		Error("Output file cannot have same name as input file!");
	    }
	}
    }
    if(!open(OUTPUT, ">$outputfile")) {
	Error("Cannot open output file: $outputfile");
    }
}


##############################################################################
# ChangeOutputFile - change the output file
##############################################################################
sub ChangeOutputFile
{
    close(OUTPUT);
    $outputfile = shift;
    OpenOutputFile($outputfile);
}


##############################################################################
# AddInputFile - adds another input file to the list
##############################################################################
sub AddInputFile
{
    my $file = shift;
    push(@Inputfiles, $file);
    Debug("Added input file: \"$file\"");
}


##############################################################################
# UseModule(module)
# Module "module.pm" is used, "module.pm" can be any perl module and can use
# or replace any of the functions in this package
##############################################################################
sub UseModule
{
    my $module = shift;
    Debug("Loading module $module");
    require $module; 
    if($@) { Error($@); }
}


##############################################################################
# find end of next word in $sline, assumes leading whitespace removed
##############################################################################
sub GetNextWordEnd
{
    my $sline = shift;
    my $i;
    # check for space separating word and remainder of line
    $i = index($sline, " ");
    # space not found, check if word separated by tab
    if($i == -1) {
	$i = index($sline, "\t");
    }
    # assume word is last element on line
    if($i == -1) {
	$i = length($sline);
    }
    return $i;
}


##############################################################################
# Print current table of defines - used for debugging
##############################################################################
sub PrintDefines
{
    my $define;
    Debug("Current ".$keywordchar."define's:");
    foreach $define (keys(%Defines)) {
        Debug(" macro:\"".$define."\", definition:\"".$Defines{$define}."\"");
    }
}


##############################################################################
# DefineEnv - define's all environment variables to macros, each prefixed
# by $envchar
##############################################################################
sub DefineEnv
{
    my $macro;
    Debug("Defining environment variables as macros");
    foreach $macro (keys(%ENV)) {
	Define($envchar.$macro." ".$ENV{$macro});
    }
}


##############################################################################
# Find out if arguments have been used with macro
##############################################################################
sub DefineArgsUsed
{
    my $string = shift;
    # check '(' is first non-whitespace char after macro
    if($string =~ /^\s*\(/) { 
	return 1;
    }
    return 0;
}


##############################################################################
# ParseArgs($string) -  find the arguments in a string of form
# (arg1, arg2, arg3...) trailing chars
# or
# arg1, arg2, arg3...
##############################################################################
sub ParseArgs
{
    my $string = shift;
    $string = CleanStart($string);
    my @Chars;
    my $char;
    # split string into chars (can't use split coz it deletes \n at end)
    for($char=0; $char<length($string); $char++) {
	push(@Chars, substr($string, $char, 1));
    }
    my @Args;    # list of Args
    my $arg = "";
    my @Endchar;
    my $s = -1;  # start of chars
    
    # deal with first '(' if there (ie func(args) rather than func args)
    if($#Chars >= 0 && $Chars[0] eq '(') {
	push(@Endchar, ')');
	$Chars[0] = '';
	$s++;
    }

    # replace args with their values
    foreach $char (@Chars) {
	# deal with end of (),"",'' etc.
	if($#Endchar > -1 && $char eq $Endchar[$#Endchar])  {pop(@Endchar);}
	# deal with ()
	elsif($char eq '(')  {push(@Endchar, ')');}
	# deal with "" and ''
	elsif($char eq '"' || $char eq '\'')  {push(@Endchar, $char);}
	# deal with ',', add arg to hash and start search for next one
	elsif($#Endchar == $s && $char eq ',') {
	    push(@Args, CleanStartEnd($arg));
	    $char = '';
	    $arg = "";
	    next;
	}
	# check for end of args string
	if($#Endchar < $s) {
	    push(@Args, CleanStartEnd($arg));
	    $char = '';
	    # put remainder of string back together
	    $arg = join('', @Chars);
	    last;
	}
	$arg = $arg.$char; # add char to current arg
	$char = '';        # set char to null
    }
    
    # deal with last arg or string following args if it exists
    push(@Args, $arg);
    
    return @Args;
}


##############################################################################
# Find the arguments in a macro and replace them
##############################################################################
sub FindDefineArgs
{
    my $substring = shift;
    my $macro = shift;

    # get definition list for this macro
    my @Argnames = split(/\,/, $DefinesArgs{$macro});

    # get arguments passed to this macro
    my @Argvals;
    @Argvals = ParseArgs($substring);
    # check the right number of args have been passed, should be all args 
    # present plus string at end of args
    if($#Argvals != $#Argnames+1) {
	my $realargs = $#Argnames+1;
	Warning("macro \'$macro\' used with $#Argvals args, expected $realargs");
	my $lastarg = $Argvals[$#Argvals]; # get lastarg
	if($#Argvals < $#Argnames+1) {     # make all missing args blanks
	    $Argvals[$#Argvals] = "";
	    while($#Argvals < $#Argnames) {
		push(@Argvals, "");
	    }
	    push(@Argvals, $lastarg);
	}
	else {   # delete all excess args
	    while($#Argvals > $#Argnames) {
		pop(@Argvals);
	    }
	    push(@Argvals, $lastarg);
	}
    }
    
    # replace default args with supplied args
    my $i=0;
    for($i=0; $i<=$#Argvals; $i++) {
	# check if all args replaced
	if($i > $#Argnames)  {last;}
	$Argnames[$i] = $Argvals[$i];
    }
    
    # check for anything after function arglist and append it
    if($i > $#Argnames) {
	$Argnames[$i] = "";
	for(; $i<=$#Argvals; $i++) {
	    $Argnames[$i] = $Argnames[$i].$Argvals[$i];
	}
    }
    
    return @Argnames;
}


##############################################################################
# Replace all defined macro's arguments with their values
# Inputs:
# $macro  = the macro to be replaces
# $string = the string following the occurrence of macro
##############################################################################
sub ReplaceDefineArgs
{
    my ($string, $tail, %Used) = @_;
    # check if args used, if not do nothing
    if(DefineArgsUsed($tail)) {
	my $macro = $string;
	# get arguments following macro
	my @Argvals = FindDefineArgs($tail, $macro);
	my @Argnames = split(/\,/, $DefinesArgs{$macro});
	my $i;
	# replace previous macro with defn + args
	$string = $Defines{$macro};
	    
	# to get args passed to macro to same processed level as rest of
	# macro, they need to be checked for occurrences of all used macros,
	# this is a nasty hack to temporarily change defines list to %Used
 	my %RealDefines = %Defines;
	my $realdefmin = $defmin;
	my $realdefmax = $defmax;
	my %RealDefineLookup = %DefineLookup;
	%Defines = %Used;
	GenerateDefinesKeys();
	for($i=0; $i<=$#Argnames; $i++) {
	    $Argvals[$i] = ReplaceDefines($Argvals[$i]);
	    $string =~ s/$bound$Argnames[$i]$bound/$Argvals[$i]/g;
	}
	# return defines to normal
 	%Defines = %RealDefines;
	$defmin = $realdefmin;
	$defmax = $realdefmax;
	%DefineLookup = %RealDefineLookup;
	# set rest current of string (non-args part) and put macro's
	# replacement into string as it needs parsing for further macros
	if(!$Argvals[$i]) { $Argvals[$i] = ""; }	
	$tail = $Argvals[$i];
	Debug("Replaced \"$macro\" for \"$string\" [$recurse_level]");
    }
    else { Debug("Macro \"$string\" found without args, ignored"); }
    return ($string, $tail);
}


##############################################################################
# When replacing macros with args, the macro and everything following the
# macro (the tail) are passed to ReplaceDefineArgs.  This function extracts
# the args from the tail and then returns the replaced macro and the new
# tail.  This function extracts the remaining part of the real tail from 
# the current input string.
##############################################################################
sub ReclaimTail
{
    my ($input, $tail) = @_;
    # split strings into chars and compare each one until difference found
    my @Input = split(//, $input);
    my @Tail  = split(//, $tail);
    $tail = $input = "";
    while($#Input >= 0 && $#Tail >= 0 && $Input[$#Input] eq $Tail[$#Tail]) {
	$tail = pop(@Tail).$tail;
	pop(@Input);
    }
    while($#Input >=0) { $input = pop(@Input).$input; }
    return ($input, $tail);
}


##############################################################################
# Replace all defined macro's in a line with their value.  Recursively run 
# through macros as many times as needed (to find macros within macros).
# Inputs:
# $input = string to process
# $tail  = rest of line following $string (if any), this will only be used
#          if string contains a macro with args, the args will probably be
#          at the start of the tail
# %Used  = all macros found in $string so far, these will not be checked
#          again to avoid possible recursion
# Initially just $input is passed in, other args are added for recursive calls
##############################################################################
sub ReplaceDefines
{
    my ($input, $tail, %Used) = @_;    
    # check for recursive macro madness (set to same level as Perl warning)
    if(++$recurse_level > 97) {
	$recurse_level--;
	Warning("Recursive macro detected in \"$input\""); 
	if($tail) { return ($input, $tail); } 
	return $input;
    }
    
    my $output = "";   # initialise output to empty string
    OUTER : while($input =~ /\S/) {
	my ($macro, $string);
	my @Words;

        ######################################################################
	# replacing macros which are "words" only - quick and easy
        ######################################################################
	if($bound eq '\b') {
	    @Words = split(/(\w+)/, $input, 2);
	    $output =  $output.$Words[0];
	    if($#Words == 2) { $macro = $Words[1]; $input = $Words[2]; }
	    else             { $input = ""; last OUTER; }
	}

        ######################################################################
	# replacing all types of macro - slow and horrid
        ######################################################################
	else {
	    # forward string to next non-whitespace char that starts a macro
	    while(!exists($DefineLookup{substr($input, 0, $defmin)})) {
		if($input =~ /^\s/ ) { # remove preceding whitespace
		    @Words = split(/^(\s+)/, $input, 2);
		    $output = $output.$Words[1];
		    $input = $Words[2]; 
		}
		else { # skip to next char
		    $output = $output.substr($input, 0, 1, "");
		}
		if($input eq "") { last OUTER; }
	    }
	    # remove the longest possible potential macro (containing no 
	    # whitespace) from the start of input
	    @Words = split(/(\s+)/, $input, 2);
	    $macro = $Words[0];
	    if($#Words == 2) {$input = $Words[1].$Words[2]; }
	    else             {$input = ""; }
	    # shorten macro if too long
	    if(length($macro) > $defmax) {
		$input = substr($macro, $defmax, 0, "").$input;
	    }
	    # see if a macro exists in "macro"
	    while(length($macro) > $defmin &&
		  !(exists($Defines{$macro}) && !exists($Used{$macro}))) {
		# chop a char off macro and try again
		$input = chop($macro).$input;
	    }
	}

	# check if macro is at start of string and has not been used yet
	if(exists($Defines{$macro}) && !exists($Used{$macro})) {
	    # set macro as used
	    $Used{$macro} = $Defines{$macro};
	    # temporarily add tail to input
	    if($tail) { $input = $input.$tail; }
	    # replace macro with defn
	    if(CheckDefineArgs($macro)) {
		($string, $input) = ReplaceDefineArgs($macro, $input, %Used);
	    }
	    else { 
		$string = $Defines{$macro};		
		Debug("Replaced \"$macro\" for \"$string\" [$recurse_level]");
	    }
	    @Words = ReplaceDefines($string, $input, %Used);
	    $output = $output.$Words[0];
	    if($#Words == 0) { $input = ""; }
	    else {
		# remove space up to start of next char
		if(CheckEatTrail($macro)) { $Words[1] =~ s/^[ \t]*//; }
		$input = $Words[1];
	    }
	    delete($Used{$macro});
	    # reclaim all unparsed tail
	    if($tail && $tail ne "" && $input ne "") {
		($input, $tail) = ReclaimTail($input, $tail);
	    }
	}
	# macro not matched, add to output and move swiftly on
	else { 
	    if($bound eq '\b') { $output = $output.$macro; }
	    else { 
		$output = $output.substr($macro, 0, 1, "");
		$input = $macro.$input;
	    }
	}
    }
    $recurse_level--;
    # append any whitespace left in string and return it
    if($tail) { return ($output.$input, $tail); }
    return $output.$input;
}


##############################################################################
# GenerateDefinesKey creates all keys and indices needed for %Defines
##############################################################################
sub GenerateDefinesKeys
{
    # find longest and shortest macro
    my ($define, $length) = each %Defines;
    $defmin = $defmax = length($define);
    %DefineLookup = ();
    foreach $define (keys(%Defines)) {
	$length = length($define);
	if($length > $defmax) { $defmax = $length; }
	if($length < $defmin) { $defmin = $length; }
    }
    # regenerate lookup table of first letters
    foreach $define (keys(%Defines)) {
	$DefineLookup{substr($define, 0, $defmin)} = 1;
    }
}


##############################################################################
# Set a define
##############################################################################
sub SetDefine
{
    my ($macro, $value) = @_;
    # add macro and value to hash table
    $Defines{$macro} = $value;
    # add define to keys
    my $length = length($macro);
    if($length < $defmin || $defmin == 0) { GenerateDefinesKeys(); }
    else {
	if($length > $defmax) { $defmax = $length; }
	$length = substr($macro, 0, $defmin);
	$DefineLookup{$length} = 1;
    }
}


##############################################################################
# Replace a define, checks if macro defined and only redefine's if it is
##############################################################################
sub Redefine
{
    my $macro = shift;
    my $value = shift;
    # check if defined
    if(CheckDefine($macro)) {
	SetDefine($macro, $value);
    }
}


##############################################################################
# Set a define argument list
##############################################################################
sub SetDefineArgs
{
    my $macro = shift;
    my $args = shift;
    # add macro args to hash table
    $DefinesArgs{$macro} = $args;
}


##############################################################################
# Check if a macro is defined
##############################################################################
sub CheckDefine
{
    my $macro = shift;
    return exists($Defines{$macro});
}


##############################################################################
# Check if a macro is defined and has arguments
##############################################################################
sub CheckDefineArgs
{
    my $macro = shift;
    return exists($DefinesArgs{$macro});
}


##############################################################################
# Check if a macro is defined and eats trailing whitespace
##############################################################################
sub CheckEatTrail
{
    my $macro = shift;
    return exists($EatTrail{$macro});
}


##############################################################################
# Set eat-trailing-whitespace for a macro
##############################################################################
sub SetEatTrail
{
    my $macro = shift;
    $EatTrail{$macro} = 1;
}


##############################################################################
# Test if a file exists and is readable
##############################################################################
sub FileExists
{
    my $filename = shift;
    # test if file is readable and not a directory
    if( !(-r $filename) || -d $filename ) {
	Debug("Checking for file: $filename...not found!");
	return 0;
    }
    Debug("Checking for file: $filename...found!");
    return 1;
}


##############################################################################
# #comment  - rest of line ignored as a comment
##############################################################################
sub Comment
{
    # nothing to be done here
    Debug("Commented line");
}


##############################################################################
# Define a variable, accepted inputs:
# $macrodefn = $macro $defn - $macro associated with $defn
#              ie: #define TEST test string
#              $macro = TEST, $defn = "test string"
#              Note: $defn = rest of line after $macro
# $macrodefn = $macro - $macro defined without a defn, rest of line ignored
#              ie: #define TEST_DEFINE
#              $macro = TEST_DEFINE, $defn = "1"
##############################################################################
sub Define
{
    my $macrodefn = shift;
    my $macro;
    my $defn;
    my $i;
    
    # find end of macroword - assume separated by space or tab
    $i = GetNextWordEnd($macrodefn);

    # separate macro and defn (can't use split, doesn't work with '0')
    $macro = substr($macrodefn, 0, GetNextWordEnd($macrodefn));
    $defn  = substr($macrodefn, GetNextWordEnd($macrodefn));
    
    # strip leading whitespace from $defn
    if($defn) {
	$defn = CleanStart($defn);
    }
    else {
	$defn = "";
    }
    
    # check if macro has arguments (will be a '(' in macro)
    if($macro =~ /\(/) {
	# split up macro, args and defn - delimiters = space, (, ), ','
	my @arglist = split(/([\s,\(,\),\,])/, $macro." ".$defn);
	my $macroargs = "";
	my $arg;

	# macro is first element in list, remove it from list
	$macro = $arglist[0];
	$arglist[0] = "";
	# loop through list until ')' and find all args
	foreach $arg (@arglist) {
	    if($arg) {
		# end of arg list, leave loop
		if($arg eq ")") {
		    $arg = "";
		    last;
		}
		# ignore space, ',' and '('
		elsif($arg =~ /([\s,\,,\(])/) {
		    $arg = "";
		}
		# argument found, add to ',' separated list
		else {
		    $macroargs = $macroargs.",".$arg;
		    $arg = "";
		}
	    }
	}
	$macroargs = Strip($macroargs, ",", 1);
	# store args
	SetDefineArgs($macro, $macroargs);
	
	Debug("Define: macro $macro has args ($macroargs)");
	# put rest of defn back together
	$defn = "";
	foreach $arg (@arglist) {
	    $defn = $defn.$arg;
	}
	$defn = CleanStart($defn);
    }
    
    # define the macro defn pair
    SetDefine($macro, $defn);
    
    Debug("Defined \"$macro\" to be \"$defn\"");
    if($debug > 1) { PrintDefines(); }
}
   


##############################################################################
# Else, standard if[n][def]-else-endif
# usage: #else somewhere between #if[n][def] key and #endif
##############################################################################
sub Else
{
    # else always true - only ran when all preceding 'if's have failed
    return 1;
}


##############################################################################
# Endif, standard ifdef-[else]-endif
# usage: #endif somewhere after #ifdef key and optionally #else
##############################################################################
sub Endif
{
    # this always terminates an if block
    return 1;
}


##############################################################################
# If conditionally includes or ignores parts of a file based on expr
# usage: #if expr
# expr is evaluated to true(1) or false(0) and include usual ==, !=, > etc.
# style comparisons. The "defined" keyword can also be used, ie: 
# #if defined MACRO || !defined(MACRO)
##############################################################################
sub If
{
    my $expr = shift;
    my $indefined = 0;
    Debug("If: parsing: \"$expr\"");

    # split expr up into its component parts, the split is done on the
    # following list of chars and strings: '!','(',')','&&','||', whitespace
    my @exprs = split(/([\s,\!,\(,\)]|\&\&|\|\|)/, $expr);
    
    # search through parts for "defined" keyword and check if macros
    # are defined
    foreach $expr (@exprs) {
	if($indefined == 1) {
	    # previously found a defined keyword, check if next word
	    # could be the macro to test for (ie. not any of the listed chars)
	    if($expr && $expr !~ /([\s,\!,\(,\)]|\&\&|\|\|)/) {
		# replace macro with 0 or 1 depending if it is defined
		Debug("If: testing if \"$expr\" defined...");
		if(CheckDefine($expr)) {
		    $expr = 1;
		    Debug("If: defined");
		}
		else {
		    $expr = 0;
		    Debug("If: NOT defined");
		}
		$indefined = 0;
	    }	    
	}
	elsif($expr eq "defined") {
	    # get rid of defined keyword
	    $expr = "";
	    # search for next macro following "defined"
	    $indefined = 1;
	}
    }

    # put full expr string back together
    my $newexpr = "";
    foreach $expr (@exprs) {
	$newexpr = "$newexpr"."$expr";
    }
    
    # pass parsed line though ReplaceDefines
    $expr = ReplaceDefines($newexpr);
    Debug("If: evaluating \"$expr\"");

    # evaluate line and return result (1 = true)
    if(eval($expr)) {
	Debug("If: \"$expr\" true");
	return 1;
    }
    Debug("If: \"$expr\" false");
    return 0;
}


##############################################################################
# Elif equivalent to "else if".  Placed between #if[n][def] and #endif,
# equivalent to nesting #if's
##############################################################################
sub Elif
{
    my $input = shift;
    return If($input);
}


##############################################################################
# Ifdef conditionally includes or ignores parts of a file based on macro,
# usage: #ifdef MACRO
# if macro has been previously #define'd everything following the
# #ifdef will be included, else it will be ignored until #else or #endif
##############################################################################
sub Ifdef
{
    my $macro = shift;
    
    # separate macro from any trailing garbage
    $macro = substr($macro, 0, GetNextWordEnd($macro));
    
    # check if macro defined - if not set to be #ifdef'ed out
    if(CheckDefine($macro)) {
	Debug("Ifdef: $macro defined");
	return 1;
    }
    Debug("Ifdef: $macro not defined");
    return 0;
}


##############################################################################
# Ifndef conditionally includes or ignores parts of a file based on macro,
# usage: #ifndef MACRO
# if macro has been previously #define'd everything following the
# #ifndef will be ignored, else it will be included until #else or #endif
##############################################################################
sub Ifndef
{
    my $macro = shift;

    # separate macro from any trailing garbage
    $macro = substr($macro, 0, GetNextWordEnd($macro));
    
    # check if macro defined - if not set to be #ifdef'ed out
    if(CheckDefine($macro)) {
	Debug("Ifndef: $macro defined");
	return 0;
    }
    Debug("Ifndef: $macro not defined");
    return 1;
}


##############################################################################
# Include $filename in output file, format:
# #include "filename" - local include file, ie. in same directory, try -Ipath
#                       also if not not found in current directory
# #include <filename> - system include file, use -Ipath
##############################################################################
sub Include
{
    my $input = shift;
    my $filename = $input;
    my $fullname;
    my $sysinclude = 0;
    my $found = 0;
    my $i;

    # check for recursive includes (level set to same as Perl recurse warn)
    if($include_level >= 98) { 
	Warning("Include recursion too deep - skipping \"$filename\"\n");
	return;
    }
    
    # replace any defined values in the include line
    $filename = ReplaceDefines($filename);

    # check if it is a system include file (#include <filename>) or a local 
    # include file (#include "filename")
    if(substr($filename, 0, 1) eq "<") {
	$sysinclude = 1;
	# remove <> from filename
	$filename = substr($filename, 1);
	($filename) = split(/\>/, $filename, 2);
    }
    elsif(substr($filename, 0, 1) eq "\"") {
	# remove speech marks from filename
	$filename = substr($filename, 1);
	($filename) = split(/\"/, $filename, 2);
    }
    # else assume filename given without "" or <>, naughty but allowed
    
    # check for file in current directory
    if($sysinclude == 0) {
	# get name of directory base file is in
	my $dir = "";
	if($base_file =~ /\//) {
	    my @Dirs = split(/(\/)/, $base_file);
	    for($i=0; $i<$#Dirs; $i++) {
		$dir = $dir.$Dirs[$i];
	    }
	}
	if(FileExists($dir.$filename)) {
	    $fullname = $dir.$filename;
	    $found = 1;
	}
    }

    # search for file in include paths, first path on command line first
    $i = 0;
    while($found == 0 && $i <= $#IncludePaths) {
	$fullname = "$IncludePaths[$i]/$filename";
	if(FileExists($fullname)) { $found = 1; }
	$i++;
    }
    
    # include file if found, error if not
    if($found == 1) {
	Debug("Including file: \"$fullname\"");
	# recursively call Parse
	Parse($fullname);
    }
    else {
	Warning("Include file \"$filename\" not found");
    }
}



##############################################################################
# Pragma filepp Function Args
# Pragma executes a filepp function, everything following the function name
# is passed as arguments to the function.
# The format is:
# #pragma filepp function args...
# If pragma is not followed by "filepp", it is ignored.
##############################################################################
sub Pragma
{
    my $input = shift;
    
    # check for "filepp" in string
    if($input =~ /^filepp\b/) {
	my ($function, $args);
	($input, $function, $args) = split(/\s/, $input, 3);
	if($function) {
	    if(!$args) { $args = ""; }
	    if($safe_mode) {
		Debug("Safe mode enabled, NOT running: $function($args)");
	    }
	    else {
		my @Args = ParseArgs($args);
		Debug("Running function: $function($args)");
		$function->(@Args);
	    }
	}
    }
}


##############################################################################
# Turn normal output on/off (does not affect any output produced by keywords)
# 1 = on, 0 = off
##############################################################################
sub SetOutput
{
    $output = shift;
    Debug("Output set to $output");
}


##############################################################################
# Turn blank suppression on and off at this include level
# 1 = on, 0 = off
##############################################################################
sub SetBlankSupp
{
    $blanksupp[$include_level] = shift;
    Debug("Blank suppression set to $blanksupp[$include_level]");
}


##############################################################################
# Reset blank suppression to command-line value (except at level 0)
##############################################################################
sub ResetBlankSupp
{
    if($include_level == 0) {
	$blanksupp[$include_level] = 0;
    } else {
	$blanksupp[$include_level] = $blanksuppopt;
    }
    Debug("Blank suppression reset to $blanksupp[$include_level]");
}


##############################################################################
# Set if macros are only replaced if the macro is a 'word'
##############################################################################
sub SetWordBoundaries
{
    my $on = shift;
    if($on) { 
	$bound = '\b';
	Debug("Word Boundaries turned on");
    }
    else { 
	$bound = '';
	Debug("Word Boundaries turned off");
    }
}

##############################################################################
# DEPRECATED - this function will be removed in later versions, use Set
# Toggle if macros are only replaced if the macro is a 'word'
##############################################################################
sub ToggleWordBoundaries
{
    if($bound eq '\b') { SetWordBoundaries(1); }
    else { SetWordBoundaries(0); }
}


##############################################################################
# Set treating keywordchar and contchar as Perl regular expressions
##############################################################################
sub SetCharPerlre
{
    $charperlre = shift;
    Debug("Characters treated as Perl regexp's : $charperlre");
}


##############################################################################
# Undef a previously defined variable, usage:
# #undef $macro
##############################################################################
sub Undef
{
    my $macro = shift;
    my $i;
    
    # separate macro from any trailing garbage
    $macro = substr($macro, 0, GetNextWordEnd($macro));
    
    # delete macro from table
    delete $Defines{$macro};

    # and remove its eat-trailing-whitespace flag
    if(CheckEatTrail($macro)) { delete $EatTrail{$macro}; }

    Debug("Undefined macro \"$macro\"");
    if($debug > 1) { PrintDefines(); }
}


##############################################################################
# UndefAll - undefines ALL macros
##############################################################################
sub UndefAll
{
    %Defines = ();
    %DefineLookup = ();
    %EatTrail = ();
    $defmin = $defmax = 0;
    Debug("Undefined ALL macros");
    if($debug > 1) { PrintDefines(); }
}


##############################################################################
# #warning msg  - print warning message "msg"
##############################################################################
sub Warning
{
    my $msg = shift;
    my $lastdebug = $debug;
    $debug = 1;
    Debug($msg);
    $debug = $lastdebug;
}


##############################################################################
# GetNextLine - returns the next line of the current INPUT line,
# line continuation is taken care of here.
##############################################################################
sub GetNextLine
{
    my $thisline = <INPUT>;
    if($thisline) {
	Redefine("__LINE__", ++$line);
	# check if end of line has a continuation char, if it has get next line
	while($thisline =~ /$contchar$/) {
	    # remove backslash and newline
	    $thisline =~ s/$contchar\n\Z//g;
	    Debug("Line continuation");
	    # get next line and append to current
	    my $nextline = <INPUT>;
	    if(!$nextline) {
		return $thisline;
	    }
	    $thisline = "$thisline"."$contrepchar"."$nextline";
	    # increment line count
	    Redefine("__LINE__", ++$line);
	}
    }
    return $thisline;
}


##############################################################################
# Write($string) - writes $string to OUTPUT file
##############################################################################
sub Write
{
    my $string = shift;
    print(OUTPUT $string);
}


##############################################################################
# Main parsing routine
##############################################################################
sub Parse
{
    # counter for number of #if[n][def] loops currently in
    my $iflevel = 0;
    # flag to control when to write output
    my @Writing = (1); # initialise default to 'writing'
    # flag to show if current 'if' block has passed a 'true if'
    my @Ifdone = (0); # initialise first to 'not passed true if'
    
    # change file being parsed to this file, remember last filename so
    # it can be returned at the end
    my $lastparse = $file;
    $file = shift;

    Debug("Parsing $file...");
    Redefine("__FILE__", $file);
    
    # reset line count, remembering previous count for future reference
    my $lastcount = $line;
    $line = 0;
    Redefine("__LINE__", $line);
    
    # increment include level
    Redefine("__INCLUDE_LEVEL__", ++$include_level);

    # set blank line suppression:
    # no suppression for top level files
    if($include_level == 0) {
	$blanksupp[$include_level] = 0;
    }
    # include level 1 - set suppression to command line given value
    elsif($include_level == 1) {
	# inherit root value if set
	if($blanksupp[0]) { $blanksupp[$include_level] = 1; }
	else {$blanksupp[$include_level] = $blanksuppopt; }
    }
    # all other include levels - keep suppression at existing value
    else {
	$blanksupp[$include_level] = $blanksupp[$include_level - 1];
    }

    # open file and set its handle to INPUT
    local *INPUT;
    if(!open(INPUT, $file)) {
	Error("Could not open file $file");
    }
    
    # parse each line of file
    $_ = GetNextLine();
    # if in "shebang" mode, throw away first line (the #!/blah bit)
    if($shebang) {
	# check for "#!...perl ...filepp..."
	if($_ =~ /^\#\!.*perl.+filepp/) {  
	    Debug("Skipping first line (shebang): ".$_);
	    $_ = GetNextLine();
	}
    }
    while($_) {
  	my $thisline = $_;	
	my $found = 0;
	my $keyword;
	# remove whitespace from start of line
	$thisline = CleanStart($thisline);
	# check if first char on line is a #
	if($thisline && $thisline =~ /^$keywordchar/) {
	    # remove "#" and any following whitespace
	    $thisline =~ s/^$keywordchar//g;
	    $thisline = CleanStart($thisline);
	    # parse line for keywords
	    foreach $keyword (@Keywords) {
		if($thisline && $thisline =~ /^$keyword\b/) {
		    $found = 1;
		    # remove newline from line
		    chomp($thisline);
		    # remove leading whitespace and keyword from line
		    my $input = CleanStart(substr($thisline,length($keyword)));

		    # check for 'if' style keyword
		    if(exists($Ifwords{$keyword})) {
			# increment ifblock level and set ifdone to same
			# value as previous block
			$iflevel++;
			$Ifdone[$iflevel] = 0;
			$Writing[$iflevel] = $Writing[$iflevel - 1];
			if(!$Writing[$iflevel]) { $Ifdone[$iflevel] = 1; }
		    }
		    # check for out of place 'else' or 'endif' style keyword
		    elsif($iflevel <= 0 &&
			  ( exists($Elsewords{$keyword}) ||
			    exists($Endifwords{$keyword}) )) {
			Warning($keywordchar.$keyword.
				" found without preceding ".$keywordchar.
				"[else]ifword");
			last; # leave loop now
		    }
		    
		    # decide if to run 'if' or 'else' keyword
		    if(exists($Ifwords{$keyword}) || 
		       exists($Elsewords{$keyword}) ) {
			if( !($Ifdone[$iflevel]) ) {
			    # check return value of 'if'
			    if($Keywords{$keyword}->($input)) {
				$Ifdone[$iflevel] = 1;
				$Writing[$iflevel] = 1;
			    }
			    else { $Writing[$iflevel] = 0; }
			}
			else { $Writing[$iflevel] = 0; }
		    }
		    # check for 'endif' style keyword
		    elsif(exists($Endifwords{$keyword})) {
			# run endif keyword and decrement iflevel if true
			if($Keywords{$keyword}->($input)) {
			    $iflevel--;
			}
		    }
		    # run all other keywords
		    elsif($Writing[$iflevel]) {
			$Keywords{$keyword}->($input);
		    }
		    last;
		}
	    }
	}
	# no keywords in line - write line to file if not #ifdef'ed out
	if($output && !$found && $Writing[$iflevel]) {
	    # unless blank lines are suppressed at this include level
	    unless($blanksupp[$include_level] && /^\s*$/) {
		# check for #define'd keys in line and replace with values
#		$_ = ReplaceDefines($_);
		# run processing chain
		my $processor;
		foreach $processor (@Processors) {
		    $_ = $processor->($_);
		}
		# write output to file or STDOUT
		Write($_);
	    }
	}
	$_ = GetNextLine();
    }
    # close file
    close(INPUT);
    Debug("Parsing $file done. ($line lines processed)");

    # reset $line
    $line = $lastcount;
    Redefine("__LINE__", $line);

    # reset $file
    $file = $lastparse;
    Redefine("__FILE__", $file);
    if($file ne "") {
	Debug("Parsing returned to $file at line $line");
    }
    
    # decrement include level
    Redefine("__INCLUDE_LEVEL__", --$include_level);
}


##############################################################################
# Main routine
##############################################################################

# parse command line
my $i=0;
my $argc=0;
while($ARGV[$argc]) { $argc++; }

while($ARGV[$i]) {

    # suppress blank lines in header files
    if($ARGV[$i] eq "-b") {
	$blanksuppopt = 1;
    }

    # read from stdin instead of file
    elsif($ARGV[$i] eq "-c") {
	AddInputFile("-");
    }
    
    # Defines: -Dmacro[=defn] or -D macro[=defn]
    elsif(substr($ARGV[$i], 0, 2) eq "-D") {
	my $macrodefn;
	# -D macro[=defn] format
	if(length($ARGV[$i]) == 2) {
	    if($i+1 >= $argc) {
		Error("Argument to `-D' is missing");
	    }
	    $macrodefn = $ARGV[++$i];
	}
	# -Dmacro[=defn] format
	else {
	    $macrodefn = substr($ARGV[$i], 2);
	}
	my $macro = $macrodefn;
	my $defn = "";
	my $j = index($macrodefn, "=");
	if($j > -1) {
	    $defn  = substr($macrodefn, $j+1);
	    $macro = substr($macrodefn, 0, $j);
	}
	$macrodefn = $macro." ".$defn;
	# add macro and defn to hash table
	Define($macrodefn);
    }

    # Debugging turned on: -d
    elsif($ARGV[$i] eq "-d") {
	$debug = 1;
    }

    # Full debugging turned on: -dd
    elsif($ARGV[$i] eq "-dd") {
	$debug = 2;
    }

    # define environment variables as macros: -e
    elsif($ARGV[$i] eq "-e") {
	DefineEnv();
    }

    # set environment variable prefix char
    elsif($ARGV[$i] eq "-ec") {
	if($i+1 >= $argc) {
	    Error("Argument to `-ec' is missing");
	}
	SetEnvchar($ARGV[++$i]);
    }

    # set environment variable prefix char to nothing
    elsif($ARGV[$i] eq "-ecn") {
	SetEnvchar("");
    }

    # show help
    elsif($ARGV[$i] eq "-h") {
	print(STDERR "$usage");
	exit(0);
    }

    # Include paths: -Iinclude or -I include
    elsif(substr($ARGV[$i], 0, 2) eq "-I") {
	# -I include format
	if(length($ARGV[$i]) == 2) {
	    if($i+1 >= $argc) {
		Error("Argument to `-I' is missing");
	    }
	    AddIncludePath($ARGV[++$i]);
	}
	# -Iinclude format
	else {
	    AddIncludePath(substr($ARGV[$i], 2));
	}
    }

    # turn off keywords
    elsif($ARGV[$i] eq "-k") {
	RemoveAllKeywords();
    }

    # set keyword prefix char
    elsif($ARGV[$i] eq "-kc") {
	if($i+1 >= $argc) {
	    Error("Argument to `-kc' is missing");
	}
	SetKeywordchar($ARGV[++$i]);
    }

    # set line continuation character
    elsif($ARGV[$i] eq "-lc") {
	if($i+1 >= $argc) {
	    Error("Argument to `-lc' is missing");
	}
	SetContchar($ARGV[++$i]);
    }

    # set line continuation replacement char to newline
    elsif($ARGV[$i] eq "-lrn") {
	SetContrepchar("\n");
    }

    # set line continuation replacement character
    elsif($ARGV[$i] eq "-lr") {
	if($i+1 >= $argc) {
	    Error("Argument to `-lr' is missing");
	}
	SetContrepchar($ARGV[++$i]);
    }

    # Module paths: -Minclude or -M include
    elsif(substr($ARGV[$i], 0, 2) eq "-M") {
	# -M include format
	if(length($ARGV[$i]) == 2) {
	    if($i+1 >= $argc) {
		Error("Argument to `-M' is missing");
	    }
	    AddModulePath($ARGV[++$i]);
	}
	# -Minclude format
	else {
	    AddModulePath(substr($ARGV[$i], 2));
	}
    }

    # use module
    elsif($ARGV[$i] eq "-m") {
	if($i+1 >= $argc) {
	    Error("Argument to `-m' is missing");
	}
	UseModule("$ARGV[++$i]");
    }
    
    # Output filename: -o filename or -ofilename
    elsif(substr($ARGV[$i], 0, 2) eq "-o") {
	# -o filename
	if(length($ARGV[$i]) == 2) {
	    if($i+1 >= $argc) {
		Error("Argument to `-I' is missing");
	    }
	    $outputfile = $ARGV[++$i];
	}
	# -ofilename
	else {
	    $outputfile = substr($ARGV[$i], 2);
	}
    }

    # treat $keywordchar and $contchar as regular expressions
    elsif($ARGV[$i] eq "-re") {
	if($charperlre) { SetCharPerlre(0); }
	else { SetCharPerlre(1); }
    }
    
    # Safe mode - turns off #pragma
    elsif($ARGV[$i] eq "-s") {
	SafeMode();
    }

    # Undefine all macros
    elsif($ARGV[$i] eq "-u") {
	UndefAll();
    }

    # print version number and exit
    elsif($ARGV[$i] eq "-v") {
	print(STDERR "filepp version $VERSION\n");
	exit(0);
    }

    # only replace macros if they appear as 'words'
    elsif($ARGV[$i] eq "-w") {
	if($bound eq '') { SetWordBoundaries(1); }
	else { SetWordBoundaries(0); }
    }
    
    # default - an input file name
    else {
	if(!FileExists($ARGV[$i])) {
	    Error("Input file \"$ARGV[$i]\" not readable");
	}
	AddInputFile($ARGV[$i]);
    }

    $i++;
}

# print initial defines if debugging
if($debug > 1) { PrintDefines(); }

# check for outputfile name, if not specified use STDOUT
if($outputfile eq "") { $outputfile = "-"; }

# they have the same name, rename the input to input~
if($#Inputfiles == 0 && 
   $Inputfiles[0] eq $outputfile && $Inputfiles[0] ne "-") {
    # paranoid check file is writable and normal file
    if(-w $outputfile && -f $outputfile) {
	$outputfile = "$outputfile.fpp$$";
	$same_file = 1;
    }
    else {
	Error("Cannot read and write to $outputfile");
    }
}

# open the output file
OpenOutputFile($outputfile);

# check input files have been specified
if($#Inputfiles == -1) {
    Error("No input files given");
}

# parse all input files in order given on command line
my $this_file;
foreach $this_file (@Inputfiles) {
    $base_file = $this_file;
    Redefine("__BASE_FILE__", $base_file);
    Parse($base_file);
}

# close output file
close(OUTPUT);

# if input and output have same name, rename output to input now
if($same_file == 1) {
    if(rename($Inputfiles[0], "$Inputfiles[0]~") == -1) {
	Error("Could not rename $Inputfiles[0] $Inputfiles[0]~");
    }
    if(rename($outputfile, $Inputfiles[0]) == -1) {
	Error("Could not rename $outputfile $Inputfiles[0]");
    }
}

exit(0);

# Hey emacs !!
# Local Variables:
# mode: perl
# End:

########################################################################
# End of file
########################################################################
