/*
	msub - Read file(s) and perform substitutions using values of
		variables defined in makefile.

	Syntax: msub [ -e ] [ -f makefile ] [ +Rstr -Rstr] [ file ... ]


	-e		Environment variables override makefile variables.
	-f makefile	Specify alternate makefile.  Multiple -f options may
			be given.
	+Rstr		Specify variable reference initiator string.
	-Rstr		Specify variable reference terminator string.

	+R and -R options must be given in pairs, although multiple pairs may
	be specified.

	27 Mar 1990	Paul DuBois	dubois@primate.wisc.edu

	27 Mar 1990 V1.0.  Created.
	16 Apr 1993 V1.1.  Rewritten.
	25 Oct 1993 V1.11.  Added support for -e and var=value on command line.

	18 Sep 1998	Bert Gijsbers	bert@xpilot.org
			Added unistd, stdio, stdlib, string header files.
			Commented out malloc declaration (now in stdlib.h).
			Removed unused variable pi.
	04 Oct 1999	Bert Gijsbers   bert@xpilot.org
			Also commented out other prototype declarations
			for strcpy, strncpy, strcat and getenv.
	19 May 2001	Bert Gijsbers   bert@xpilot.org
			Protoized functions.

	$Id: msub.c,v 5.1 2001/05/19 11:12:23 bertg Exp $
*/

#include	<unistd.h>
#include	<stdlib.h>
#include	<stdio.h>
#include	<string.h>
#include	<ctype.h>
#include	<sys/types.h>
#include	<sys/stat.h>

#define	bufSiz	2048

#define	FromMakefile(vp)	(vp->source == precMakefile)


/* extern char	*malloc (); */
/* extern char	*strcpy (); */
/* extern char	*strncpy (); */
/* extern char	*strcat (); */
/* extern char	*getenv (); */


typedef	struct Var	Var;

struct Var
{
	char	*var;
	char	*value;
	int	expanded;
	int	source;
	Var	*next;
};


static void	ReadMake (FILE *f);
static int	CheckAssignment (char *s, int source);
static Var	*AddVar (char *var, char *value, int source);
static Var	*FindVar (char *var);
static Var	*FindVarUsingEnv (char *var);
static int	FindVarRef (char *s, int nRefSeqs, char **initRef, char **termRef);
static int	FindEndVarRef (char *s, int idx, char *term);
static void	Expand (Var *vp);
static void	Substitute (FILE *f);
static char	*Malloc(int size);
static char	*NewString(char *s);
static void	Panic (char *s);


static char	*usage = "Usage: msub [ +Rstr -Rstr] [ -f makefile ] file";
static char	*makefile = (char *) NULL;

static Var	*vvList = (Var *) NULL;
static int	nVars = 0;

static char	*mfRefInit[2] = { "${", "$(" };
static char	*mfRefTerm[2] = { "}", ")" };

static char	*usrRefInit[10];
static char	*usrRefTerm[10];
static int	usrRefSeqCnt = 0;

static char	**refInit;
static char	**refTerm;
static int	refSeqCnt;

static int	refInitIdx;
static int	varIdx;
static int	refTermIdx;
static int	restIdx;

/*
 * Assignment source precedences.  Command line assignments always override
 * assignments from Makefile or environment variable values.  Normally
 * assignments with Makefiles override environment variables, but if -e is
 * specified on the command line, their precedences are reversed.
 */

static int	precCmdLine = 3;
static int	precMakefile = 2;
static int	precEnvVar = 1;


int
main (int argc, char **argv)
{
FILE	*f;
Var	*vp;
char	*p, *q;
int	pass;
int	needExpand;
int	toggle = 0;

	--argc;
	++argv;

	while (argc > 0)
	{
		/* see if it's a var=value argument */
		if (CheckAssignment (argv[0], precCmdLine))
		{
			--argc;
			++argv;
			continue;
		}
		/* if not, and it's not a flag, stop processing arguments */
		if (argv[0][0] != '-' && argv[0][0] != '+')
			break;
		if (strcmp (argv[0], "-e") == 0)
		{
		int	tmp;

			/* reverse precedence of makefile/env. assignments */
			tmp = precMakefile;
			precMakefile = precEnvVar;
			precEnvVar = tmp;
		}
		else if (strcmp (argv[0], "-f") == 0)
		{
			if (argc < 2)
				Panic (usage);
			if ((f = fopen (makefile = argv[1], "r")) == (FILE *) NULL)
				Panic ("cannot open makefile argument");
			ReadMake (f);
			(void) fclose (f);
			--argc;
			++argv;
		}
		else if (strncmp (argv[0], "+R", 2) == 0)
		{
			if (toggle == 1)
				Panic ("missing -R argument after +R");
			toggle = 1;
			if (strlen (argv[0]) == 2)
				Panic ("empty reference initiator specified");
			usrRefInit[usrRefSeqCnt] = &argv[0][2];
		}
		else if (strncmp (argv[0], "-R", 2) == 0)
		{
			if (toggle == 0)
				Panic ("missing +R argument before -R");
			toggle = 0;
			if (strlen (argv[0]) == 2)
				Panic ("empty reference terminator specified");
			usrRefTerm[usrRefSeqCnt] = &argv[0][2];
			++usrRefSeqCnt;
		}
		else
			Panic (usage);	/* bad flag */
		--argc;
		++argv;
	}

	if (toggle == 1)
		Panic ("missing -R argument after +R");
	if (usrRefSeqCnt == 0)	/* no reference sequences specified */
	{
		refInit = mfRefInit;
		refTerm = mfRefTerm;
		refSeqCnt = 2;
	}
	else
	{
		refInit = usrRefInit;
		refTerm = usrRefTerm;
		refSeqCnt = usrRefSeqCnt;
	}

	if (makefile == (char *) NULL)	/* no -f options were given */
	{
		if ((f = fopen ("makefile", "r")) == (FILE *) NULL)
		{
			if ((f = fopen ("Makefile", "r")) == (FILE *) NULL)
				Panic ("cannot open makefile or Makefile");
		}
		ReadMake (f);
		(void) fclose (f);
	}

	/* Replace instances of '$$' with a single ^A */
	/* (only for assignments from Makefile) */

	for (vp = vvList; vp != (Var *) NULL; vp = vp->next)
	{
		if (!FromMakefile (vp))
			continue;
		for (p = q = vp->value; *p != '\0'; p++, q++)
		{
			*q = *p;
			if (*p == '$' && *(p+1) == '$')
			{
				*q = '\01';
				p++;
			}
		}
		*q = '\0';
	}

	/* determine which variable values contain embedded references */

	for (vp = vvList; vp != (Var *) NULL; vp = vp->next)
	{
		if (!FromMakefile (vp))
			vp->expanded = 1;
		else
			vp->expanded = !FindVarRef (vp->value, 2, mfRefInit,
								mfRefTerm);
	}


	/* expand values to eliminate embedded references */

	for (pass = 0; pass < nVars; pass++)
	{
		needExpand = 0;
		for (vp = vvList; vp != (Var *) NULL; vp = vp->next)
		{
			if (!vp->expanded)
				Expand (vp);
			if (!vp->expanded)
				needExpand = 1;
		}
		if (needExpand == 0)	/* loop while values need expanding */
			break;
	}

	/* sanity check: shouldn't have to make more than nVars passes */
	if (pass >= nVars)
		Panic ("Too many expansion passes.  Something's wrong!");

	/* Replace instances of ^A with a '$' */
	/* (only for assignments from Makefile) */

	for (vp = vvList; vp != (Var *) NULL; vp = vp->next)
	{
		if (!FromMakefile (vp))
			continue;
		for (p = vp->value; *p != '\0'; p++)
		{
			if (*p == '\01')
				*p = '$';
		}
	}

	/* read source file(s) and perform substitutions */
	if (argc == 0)
		Substitute (stdin);
	else while (argc > 0)
	{
		if ((f = fopen (*argv, "r")) == (FILE *) NULL)
		{
			(void) fprintf (stderr, "cannot open \"%s\".\n", *argv);
			Panic ("Quitting.");
		}
		Substitute (f);
		(void) fclose (f);
		--argc;
		++argv;
	}

	return (0);
}


/*
 * Read a makefile, combine continuation lines (by joining lines with a space
 * and stripping leading whitespace on next line), null-terminate each line,
 * find variable assignments
 */

static void
ReadMake (FILE *f)
{
struct stat	st;
size_t	mfSize;
int	fd;
char	*p, *s;
char	*sNext;
char	*mfBuf;		/* makefile buffer */

	fd = fileno (f);
	if (fstat (fd, &st) != 0)
		Panic ("cannot stat Makefile");
	mfSize = st.st_size;
	mfBuf = Malloc (mfSize + 1);
	if (read (fd, mfBuf, mfSize) != mfSize)
		Panic ("cannot read Makefile");
	mfBuf[mfSize] = '\0';	/* make sure it's null-terminated */

	p = s = mfBuf;
	while (s <= mfBuf + mfSize)
	{
		if (*s == '\\' && *(s+1) == '\n')
		{
			*p++ = ' ';
			s += 2;		/* skip continuation characters */
			while (isspace (*s))
				++s;
		}
		else
		{
			if (*s == '\n')	/* convert linefeeds to nulls */
				*s = '\0';
			*p++ = *s++;
		}
	}
	*p = '\0';

	s = mfBuf;
	while (s <= p)
	{
		/* need sNext because CheckAssignment() modifies current line */
		sNext = s + strlen (s) + 1;	/* find next line */
		(void) CheckAssignment (s, precMakefile);
		s = sNext;
	}

	free (mfBuf);		/* done with Makefile; jettison it */
}


/*
 * Determine whether the line contains a variable assignment, i.e.,
 * an identifier, an equals sign, and optionally a value following.
 *
 * If the value contains a comment afterward, the comment becomes
 * part of the value.  This is a bug, but it's too much trouble to
 * do the parsing necessary to find such things.  Just don't use them.
 *
 * This is called to process assignments from command line arguments and
 * from the makefile.
 *
 * Return non-zero if assignment was found, zero otherwise.
 */

static int
CheckAssignment (char *s, int source)
{
char	name[bufSiz], *np;
int	len;

	while (isspace (*s))		/* skip whitespace */
		++s;
	if (!isalpha (*s) && *s != '_')		/* no variable */
		return (0);
	np = name;
	while (isalnum (*s) || *s == '_')
		*np++ = *s++;
	*np = '\0';
	while (isspace (*s))		/* skip whitespace */
		++s;
	if (*s != '=')			/* no '=' */
		return (0);
	++s;				/* skip '=' */
	while (isspace (*s))		/* skip whitespace */
		++s;
	/* take rest of line as definition after trimming trailing whitespace */
	len = strlen (s);
	while (len > 0 && isspace (s[len-1]))
		s[--len] = '\0';
	(void) AddVar (name, s, source);
	return (1);
}


static Var *
FindVar (char *var)
{
Var	*vp;

	for (vp = vvList; vp != (Var *) NULL; vp = vp->next)
	{
		if (strcmp (vp->var, var) == 0)
			return (vp);
	}
	return ((Var *) NULL);
}


/*
 * Find a variable and return pointer to Var structure.  Never returns NULL.
 * If the variable isn't found, create an instance (either with an empty
 * value or the value from the environment variable of the same name if one
 * exists).  If the variable is found and is from the makefile, override it
 * from the environment if environment variabless have higher precedence (i.e.,
 * if -e was specified on the command line).
 */

static Var *
FindVarUsingEnv (char *var)
{
Var	*vp;
char	*val;

	if ((vp = FindVar (var)) == (Var *) NULL)
	{
		if ((val = (char *) getenv (var)) == (char *) NULL)
			val = "";
		vp = AddVar (var, val, precEnvVar);
	}
	else if (FromMakefile (vp) && precMakefile < precEnvVar)
	{
		if ((val = (char *) getenv (var)) != (char *) NULL)
			vp = AddVar (var, val, precEnvVar);
	}
	return (vp);
}

static Var *
AddVar (char *var, char *value, int source)
{
Var	*vp;

	if ((vp = FindVar (var)) == (Var *) NULL)
	{
		vp = (Var *) Malloc (sizeof (Var));
		vp->var = NewString (var);
		vp->value = NewString (value);
		vp->source = source;
		vp->expanded = 0;
		vp->next = vvList;
		vvList = vp;
		++nVars;
	}
	else if (vp->source <= source)	/* exists; replace previous value if */
	{				/* new source has higher priority */
		free (vp->value);
		vp->value = NewString (value);
		vp->source = source;
	}
	return (vp);
}


/*
	Find a variable reference in a string.  Return zero if no reference
	found.  Otherwise return non-zero and set four globals:

	refInitIdx	index within s of beginning of reference initiator
	varIdx		index within s of beginning of variable
	refTermIdx	index within s of beginning of reference terminator
	restIdx		index within s of rest of string following reference
*/

static int
FindVarRef (char *s, int nRefSeqs, char **initRef, char **termRef)
{
char	*p;
int	len, i;

	p = s;
	while (*p != '\0')
	{
		/* look for reference initiation sequence */
		for (i = 0; i < nRefSeqs; i++)
		{
			len = strlen (initRef[i]);
			if (strncmp (p, initRef[i], len) == 0
				&& FindEndVarRef (s, p-s+len, termRef[i]))
			{
				refInitIdx = p - s;
				varIdx = p - s + len;
				return (1);
			}
		}
		++p;
	}
	return (0);
}


static int
FindEndVarRef (char *s, int idx, char *term)
{
char	*p = s + idx;	/* point to first char past reference initiator */
int	len = strlen (term);

	if (!isalpha (*p) && *p != '_')	/* no variable name present */
		return (0);
	while (isalnum (*p) || *p == '_')
	{
		if (strncmp (p+1, term, len) == 0)
		{
			refTermIdx = p + 1 - s;
			restIdx = refTermIdx + len;
			return (1);
		}
		++p;
	}
	return (0);
}


/*
	Look for variable references in a variable value and expand them
	when possible.  If a variable is referenced that has not itself
	been fully expanded, defer expansion until another pass, at which
	time the referenced variable might then be expanded.  This
	prevents infinite expansions on circular references.
*/

static void
Expand (Var *vp)
{
Var	*vp2;
char	buf[bufSiz * 4], *p;

	while (FindVarRef (vp->value, 2, mfRefInit, mfRefTerm))
	{
		(void) strncpy (buf, vp->value+varIdx, refTermIdx-varIdx);
		buf[refTermIdx-varIdx] = '\0';
		if ((vp2 = FindVarUsingEnv (buf)) != (Var *) NULL)
		{
			if (!vp2->expanded)
				return;		/* can't substitute yet */
			p = vp2->value;
		}
		else
			p = "";
		/* substitute value for reference, replace value */
		(void) strncpy (buf, vp->value, refInitIdx);
		(void) strcpy (buf+refInitIdx, p);
		(void) strcpy (buf+refInitIdx+strlen(p), vp->value+restIdx);
		free (vp->value);
		vp->value = NewString (buf);
	}
	vp->expanded = 1;	/* no more embedded references */
}


/*
	Read through file, substituting variable values for variable
	references.  If a variable reference is found that is for an
	unknown variable, substitute the empty string.

	This really should check write() return values...
*/

static void
Substitute (FILE *f)
{
Var	*vp;
char	buf[bufSiz * 4], name[bufSiz], *p;

	while (fgets (buf, (int) sizeof (buf), f) != (char *) NULL)
	{
		p = buf;
		while (FindVarRef (p, refSeqCnt, refInit, refTerm))
		{
			(void) write (1, p, refInitIdx);
			(void) strncpy (name, p+varIdx, refTermIdx-varIdx);
			name[refTermIdx-varIdx] = '\0';
			if ((vp = FindVarUsingEnv (name)) != (Var *) NULL)
				(void) write (1, vp->value, strlen (vp->value));
			p += restIdx;
		}
		(void) write (1, p, strlen (p));
	}
}


/*
	Allocate space.
*/

static char
*Malloc (int size)
{
char	*p;

	if ((p = (char *) malloc (size)) == (char *) NULL)
		Panic ("Malloc: out of space");
	return (p);
}


/*
	Allocate space for a string, copy the string into it, and return
	a pointer to the copy.
*/

static char
*NewString (char *s)
{
	return (strcpy (Malloc (strlen (s) + 1), s));
}


static void
Panic (char *s)
{
	(void) fprintf (stderr, "msub: %s\n", s);
	exit (1);
}
