/*  $Revision: 1.3 $
**
**  A C version of Henry Spencer's "subst" script.
*/
#include <stdio.h>
#include <errno.h>
#include <signal.h>

#define LINESIZE		1024
#define FNAMESIZE		1024
#define PARAMSIZE		128
#define WHITE(c)		((c) == ' ' || (c) == '\t')


/*
**  AFS doesn't support hard links, so enable this #define.
#define USE_RENAME
*/

/*
**  If you don't have getopt in your C library, enable this #define.
#define NEED_GETOPT
*/


typedef struct _PAIR {
    char	*Name;
    int		Length;
    char	*Value;
} PAIR;

static char	*argv0;
extern char	*optarg;
extern int	optind;

extern void	exit();
extern char	*malloc();
extern char	*strcpy();



/*
**  Local implementations of common C library functions.
*/

/*
**  Return string represtation of errno.
*/
static char *
xstrerror()
{
    extern int	sys_nerr;
#if !defined(__FreeBSD__) && !defined(__NetBSD__) && !defined(__GLIBC__)
    extern char	*sys_errlist[];
#endif
    extern int	errno;
    static char	buff[30];

    if (errno >= 0 && errno < sys_nerr)
	return sys_errlist[errno];
    (void)sprintf(buff, "Error code %d\n", errno);
    return buff;
}


/*
**  Return the first occurrence of 'c' in 'p' or NULL if not there.
*/
static char *
xstrchr(p, c)
    register char	*p;
    register char	c;
{
    for ( ; *p; p++)
	if (*p == c)
	    return p;
    return NULL;
}


/*
**  Return the last occurrence of 'c' in 'p' or NULL if not there.
*/
static char *
xstrrchr(p, c)
    register char	*p;
    register char	c;
{
    register char	*ret;

    for (ret = NULL; *p; p++)
	if (*p == c)
	    ret = p;
    return ret;
}


/*
**  Copy a string to malloc'd memory or exit.
*/
static char *
xstrdup(p)
    char	*p;
{
    char	*new;

    if ((new = malloc(strlen(p) + 1)) == NULL) {
	(void)fprintf(stderr, "%s: Can't copy \"%s\", %s\n",
		argv0, p, xstrerror());
	exit(1);
    }
    return strcpy(new, p);
}

#if	defined(NEED_GETOPT)

#define TYPE	int

#define ERR(s, c)					\
    if (opterr) {					\
	char buff[2];					\
	buff[0] = c; buff[1] = '\n';			\
	(void)write(2, av[0], (TYPE)strlen(av[0]));	\
	(void)write(2, s, (TYPE)strlen(s));		\
	(void)write(2, buff, 2);			\
    }

int	 opterr = 1;
int	 optind = 1;
int	 optopt;
char	*optarg;

/*
**  Return options and their values from the command line.
**  This comes from the AT&T public-domain getopt published in mod.sources
**  (i.e., comp.sources.unix before the great Usenet renaming).
*/
int
getopt(ac, av, opts)
    int		ac;
    char	*av[];
    char	*opts;
{
    static int	i = 1;
    char	*p;

    /* Move to next value from argv? */
    if (i == 1) {
	if (optind >= ac || av[optind][0] != '-' || av[optind][1] == '\0')
	    return EOF;
	if (strcmp(av[optind], "--") == 0) {
	    optind++;
	    return EOF;
	}
    }

    /* Get next option character. */
    if ((optopt = av[optind][i]) == ':' || (p = IDX(opts,  optopt)) == NULL) {
	ERR(": illegal option -- ", optopt);
	if (av[optind][++i] == '\0') {
	    optind++;
	    i = 1;
	}
	return '?';
    }

    /* Snarf argument? */
    if (*++p == ':') {
	if (av[optind][i + 1] != '\0')
	    optarg = &av[optind++][i + 1];
	else {
	    if (++optind >= ac) {
		ERR(": option requires an argument -- ", optopt);
		i = 1;
		return '?';
	    }
	    optarg = av[optind++];
	}
	i = 1;
    }
    else {
	if (av[optind][++i] == '\0') {
	    i = 1;
	    optind++;
	}
	optarg = NULL;
    }

    return optopt;
}
#endif	/* defined(NEED_GETOPT) */



/*
**  Simulate "mv $from $to" -- return no useful status.  We know that
**  the $from and $to are on the same filesystem.
*/
static void
mv(from, to)
    char	*from;
    char	*to;
{
    if (unlink(to) < 0 && errno != ENOENT) {
	(void)fprintf(stderr, "%s: Can't unlink %s, %s\n",
	    argv0, to, xstrerror());
	return;
    }
#if	defined(USE_RENAME)
    if (rename(from, to) < 0) {
	(void)fprintf(stderr, "%s: Can't rename %s to %s, %s\n",
		argv0, from, to, xstrerror());
	return;
    }
#else
    if (link(from, to) < 0) {
	(void)fprintf(stderr, "%s: Can't link %s to %s, %s\n",
		argv0, from, to, xstrerror());
	return;
    }
    if (unlink(from) < 0)
	(void)fprintf(stderr, "%s: Can't unlink %s, %s\n",
	    argv0, from, xstrerror());
#endif	/* defined(USE_RENAME) */
}


/*
**  Simulate "cmp -s $n1 $n2" -- return 0 if files are the same.
*/
static int
cmp(n1, n2)
    char	*n1;
    char	*n2;
{
    FILE	*f1;
    FILE	*f2;
    int		c;

    if ((f1 = fopen(n1, "r")) == NULL)
	return 1;
    if ((f2 = fopen(n2, "r")) == NULL) {
	(void)fclose(f1);
	return 1;
    }
    while ((c = getc(f1)) != EOF)
	if (getc(f2) != c) {
	    (void)fclose(f1);
	    (void)fclose(f2);
	    return 1;
	}
    if (getc(f2) != EOF) {
	(void)fclose(f1);
	(void)fclose(f2);
	return 1;
    }
    (void)fclose(f1);
    (void)fclose(f2);
    return 0;
}


/*
**  If line does not look like a template, return NULL, otherwise modify
**  it to delete the trailing gunk and return the start of the template.
*/
static char *
istemplate(line)
    char	*line;
{
    char	*p;
    char	*start;

    /* Find "=()<" and remember where it starts. */
    for (p = line; (p = xstrchr(p, '=')) != NULL; p++)
	if (p[1] == '(' && p[2] == ')' && p[3] == '<')
	    break;
    if (p == NULL)
	return NULL;
    start = &p[4];

    /* Now find ">()=" and nip it off. */
    for (p = start; (p = xstrchr(p, '>')) != NULL; p++)
	if (p[1] == '(' && p[2] == ')' && p[3] == '=') {
	    *p++ = '\n';
	    *p = '\0';
	    return start;
	}
    return NULL;
}


/*
**  Splice three strings together, returning an allocated copy.
*/
static char *
splice(s1, s2, s3)
    char	*s1;
    char	*s2;
    char	*s3;
{
    int		i;
    char	*new;

    i = strlen(s1) + strlen(s2) + strlen(s3) + 1;
    if ((new = malloc(i)) == NULL) {
	(void)fprintf(stderr, "%s: Can't splice %s+%s+%s, %s\n",
	    argv0, s1, s2, s3, xstrerror());
	exit(1);
    }
    (void)sprintf(new, "%s%s%s", s1, s2, s3);
    return new;
}


/*
**  Substitute all found patterns in the line and print it.  Using the goto
**  makes the code more clear than using do/while.
*/
static int
doline(f, out, line, tp, end)
    char	*f;
    FILE	*out;
    char	*line;
    PAIR	*tp;
    PAIR	*end;
{
    char	*p;
    char	*new;
    char	save;
    int		count;

    for (count = 0, line = xstrdup(line); tp < end; tp++) {
Again:
	for (p = line; (p = xstrchr(p, tp->Name[0])) != NULL; p++)
	    if (strncmp(p, tp->Name, tp->Length) == 0) {
		save = *p;
		*p = '\0';
		count++;
		new = splice(line, tp->Value, p + tp->Length);
		*p = save;
		if (strcmp(new, line) == 0) {
		    (void)fprintf(stderr, "%s:  subst loop in %s:\n\t%s\n",
			    argv0, f, line);
		    free(new);
		    break;
		}
		free(line);
		line = new;
		goto Again;
	    }
    }
    if (count > 0 && fputs(line, out) == EOF) {
	(void)fprintf(stderr, "%s:  can't write %s, %s\n",
		argv0, f, xstrerror());
	free(line);
	return -1;
    }
    free(line);
    return count;
}


/*
**  Process one file, carefully substituting it in place.
*/
static void
Process(f, Table, end)
    char	*f;
    PAIR	*Table;
    PAIR	*end;
{
    char	new[FNAMESIZE];
    char	old[FNAMESIZE];
    char	line[LINESIZE];
    int		bad;
    int		i;
    int		count;
    FILE	*in;
    FILE	*out;
    FILE	*temp;
    char	*p;

    /* First, figure out temporary names. */
    if ((p = xstrrchr(f, '/')) == NULL) {
	(void)strcpy(new, "substtmp.new");
	(void)strcpy(old, "substtmp.old");
    }
    else {
	*p = '\0';
	(void)sprintf(new, "%s/substtmp.new", f);
	(void)sprintf(old, "%s/substtmp.old", f);
	*p = '/';
    }

    /* Test existences. */
    if ((in = fopen(f, "r")) == NULL) {
	(void)fprintf(stderr, "%s: can't open %s, %s\n",
		argv0, f, xstrerror());
	return;
    }
    if ((temp = fopen(new, "r")) != NULL) {
	(void)fclose(in);
	(void)fprintf(stderr, "%s: %s exists, cannot proceed\n",
		argv0, new);
	exit(1);
    }
    if ((temp = fopen(old, "r")) != NULL) {
	(void)fprintf(stderr, "%s: %s exists, cannot proceed\n",
		argv0, old);
	exit(1);
    }
    temp = fopen(old, "w");
    out = fopen(new, "w");
    if (out == NULL || temp == NULL) {
	if (temp != NULL)
	    (void)fclose(temp);
	(void)unlink(old);
	if (out != NULL)
	    (void)fclose(out);
	(void)unlink(new);
	(void)fprintf(stderr, "%s: cannot create temporaries %s and %s\n",
		argv0, old, new);
	exit(1);
    }
    (void)fclose(temp);

    /* Generate the new version. */
    for (i = 1, bad = 0; fgets(line, sizeof line, in) != NULL; i++) {
	if ((p = xstrchr(line, '\n')) == NULL) {
	    (void)fprintf(stderr,
	      "%s: Line %d of %s is too long (or doesn't end with a newline)\n",
		    argv0, i, f);
	    bad++;
	    break;
	}
	(void)fputs(line, out);
	if ((p = istemplate(line)) != NULL) {
	    if ((count = doline(f, out, p, Table, end)) < 0) {
		bad++;
		break;
	    }
	    if (count > 0) {
		(void)fgets(line, sizeof line, in);
		i++;
	    }
	    else
		(void)fprintf(stderr,
			"%s: %s:%d unknown parameter or bad line:\n\t%s",
			argv0, f, i, p);
	}
    }
    (void)fclose(in);
    if (fflush(out) == EOF || fclose(out) == EOF) {
	(void)fprintf(stderr, "%s: can't close %s, %s\n",
		argv0, f, xstrerror());
	bad++;
    }

    if (bad || cmp(new, f) == 0) {
	(void)unlink(old);
	(void)unlink(new);
	(void)printf("%s: unchanged\n", f);
	return;
    }

    /* Substitute new for old the only safe way -- ignore signals. */
    (void)signal(SIGHUP, SIG_IGN);
    (void)signal(SIGINT, SIG_IGN);
    (void)signal(SIGTERM, SIG_IGN);
    mv(f, old);
    mv(new, f);
    (void)signal(SIGHUP, SIG_DFL);
    (void)signal(SIGINT, SIG_DFL);
    (void)signal(SIGTERM, SIG_DFL);
    (void)printf("%s: updated\n", f);
    (void)unlink(old);
}


/*
**  Print usage message and exit.
*/
static void
Usage()
{
    (void)fprintf(stderr, "Usage: %s -f file victims...\n", argv0);
    exit(1);
}


int
main(ac, av)
    int		ac;
    char	*av[];
{
    static char	NIL[] = "";
    char	*ctlfile;
    char	*p;
    char	*dest;
    FILE	*F;
    int		i;
    char	buff[LINESIZE];
    char	name[PARAMSIZE];
    PAIR	*Table;
    PAIR	*tp;

    /* Set defaults. */
    ctlfile = NULL;
    argv0 = av[0];

    /* Parse JCL. */
    while ((i = getopt(ac, av, "e:f:")) != EOF)
	switch (i) {
	default:
	    Usage();
	    /* NOTREACHED */
	case 'e':
	    break;
	case 'f':
	    if (ctlfile != NULL)
		Usage();
	    ctlfile = optarg;
	    break;
	}
    if (ctlfile == NULL)
	Usage();
    ac -= optind;
    av += optind;

    /* Open control file, count lines, allocate table. */
    if ((F = fopen(ctlfile, "r")) == NULL) {
	(void)fprintf(stderr, "%s: Can't open %s to read it, %s\n",
		argv0, ctlfile, xstrerror());
	exit(1);
    }
    for (i = 0; fgets(buff, sizeof buff, F) != NULL; i++)
	continue;
    if ((Table = (PAIR *)malloc(i * sizeof *Table)) == NULL) {
	(void)fprintf(stderr, "%s: Can't allocate %d table elements, %s\n",
		argv0, i, xstrerror());
	exit(1);
    }

    /* Now parse the table. */
    (void)fseek(F, 0L, 0);
    for (i = 1, tp = Table; fgets(buff, sizeof buff, F) != NULL; i++) {
	if ((p = xstrchr(buff, '\n')) == NULL) {
	    (void)fprintf(stderr, "%s: Line %d of %s is too long\n",
		    argv0, i, ctlfile);
	    exit(1);
	}
	*p = '\0';

	/* Skip empty lines, comment lines, and all-blank lines. */
	if (buff[0] == '\0' || buff[0] == '#')
	    continue;
	for (p = buff; WHITE(*p); p++)
	    continue;
	if (*p == '\0')
	    continue;

	/* Find end of first word, copy second word (or empty string) */
	for (p = buff; *p && !WHITE(*p); p++)
	    continue;
	if (*p == '\0')
	    tp->Value = NIL;
	else {
	    for (*p++ = '\0'; *p && WHITE(*p); p++)
		continue;
	    tp->Value = xstrdup(p);

	    /* Turn things like \& into &. */
	    for (p = dest = tp->Value; *p; p++)
		*dest++ = (*p == '\\' && p[1] != '\0') ? *++p : *p;
	    *dest = '\0';
	}

	/* Turn first word into something directly searchable. */
	if (strlen(buff) > sizeof name - 4) {
	    (void)fprintf(stderr, "%s: Parameter %s is too long\n",
		    argv0, buff);
	    exit(1);
	}
	(void)sprintf(name, "@<%s>@", buff);
	tp->Name = xstrdup(name);
	tp->Length = strlen(tp->Name);
	tp++;
    }
    (void)fclose(F);

    while (*av != NULL)
	Process(*av++, Table, tp);

    exit(0);
    /* NOTREACHED */
}
