/*
 * This file is part of the Minicom Communications Program,
 * written by Miquel van Smoorenburg 1991/1992/1993.
 *
 * Runscript    Run a login-or-something script.
 *		A basic like "programming language".
 *		This program also looks like a basic interpreter :
 *		a bit messy. (But hey, I'm no compiler writer :-))
 *
 * Version:	1.01
 *
 * Author:	Miquel van Smoorenburg, miquels@maestro.htsa.aha.nl
 *
 * Bugs:	The "expect" routine is, unlike gosub, NOT reentrant !
 *
 */
#include <sys/types.h>
#if defined (_POSIX_SOURCE) || defined(_BSD43)
#include <stdlib.h>
#include <unistd.h>
#else
char *getenv();
#endif
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#undef NULL
#include <stdio.h>

/*
 * Run a script.
 */
 
#define OK	0
#define ERR	-1
#define RETURN	1
#define BREAK	2

#ifndef lint
char *Version = "@(#)Runscript V1.1 1992";
#endif

struct line {
  char *line;
  int labelcount;
  int lineno;
  struct line *next;
};

struct var {
  char *name;
  int value;
  struct var *next;
};

#define LNULL ((struct line *)0)
#define VNULL ((struct var  *)0)
#ifndef CNULL
#define CNULL ((char *)0)
#endif


/*
 * Structure describing the script we are currently executing.
 */
struct env {
  struct line *lines;		/* Start of all lines */
  struct var *vars;		/* Start of all variables */
  char *scriptname;		/* Name of this script */
  int verbose;			/* Are we verbose? */
  jmp_buf ebuf;			/* For exit */
  int exstat;			/* For exit */
};
  
struct env *curenv;		/* Execution environment */  
int gtimeout = 120;		/* Global Timeout */
int etimeout = 0;		/* Timeout in expect routine */
jmp_buf ejmp;			/* To jump to if expect times out */
int inexpect = 0;		/* Are we in the expect routine */
char *newline;			/* What to print for '\n'. */
char *s_login = "name";		/* User's login name */
char *s_pass = "password";	/* User's password */
struct line *thisline;		/* Line to be executed */ 
int laststatus = 0;		/* Status of last command */

/* Forward declarations */
int s_exec();
int execscript();

/*
 * Print something formatted to stderr.
 */
/*VARARGS*/ 
void s_error(fmt, a1, a2, a3)
char *fmt;
char *a1, *a2, *a3;
{
  fprintf(stderr, fmt, a1, a2, a3);
}

/*
 * Walk through the environment, see if LOGIN and/or PASS are present.
 * If so, delete them. (Someone using "ps" might see them!)
 */
void init_env()
{
  extern char **environ;
  char **e;

  for(e = environ; *e; e++) {
	if (!strncmp(*e, "LOGIN=", 6)) {
		s_login = (*e) + 6;
		*e = "LOGIN=";
	}
	if (!strncmp(*e, "PASS=", 5)) {
		s_pass = (*e) + 5;
		*e = "PASS=";
	}
  }
}


/*
 * Return an environment variable.
 */
char *mygetenv(env)
char *env;
{
  if (!strcmp(env, "LOGIN")) return(s_login);
  if (!strcmp(env, "PASS")) return(s_pass);
  return(getenv(env));
}

/*
 * Display a syntax error and exit.
 */
void syntaxerr(s)
char *s;
{
  s_error("script \"%s\": syntax error in line %d %s\r\n", curenv->scriptname,
  	thisline->lineno, s);
  exit(1);	
}

/*
 * Skip all space
 */
void skipspace(s)
char **s;
{
  while(**s == ' ' || **s == '\t') (*s)++;
}
  
/*
 * Our clock. This gets called every second.
 */
/*ARGSUSED*/
void myclock(dummy)
int dummy;
{
  signal(SIGALRM, myclock);
  alarm(1);

  if (--gtimeout == 0) {
  	s_error("script \"%s\": global timeout\r\n", curenv->scriptname);
  	exit(1);
  }	
  if (inexpect && etimeout && --etimeout == 0) longjmp(ejmp, 1);
}

/*
 * Read a word and advance pointer.
 * Also processes quoting, variable substituting, and \ escapes.
 */
char *getword(s)
char **s;
{
  static char buf[81];
  int len, f;
  int idx = 0;
  char *t = *s;
  int sawesc = 0;
  int sawq = 0;
  char *env, envbuf[32];

  if (**s == 0) return(CNULL);

  for(len = 0; len < 81; len++) {
  	if (sawesc && t[len]) {
  		sawesc = 0;
  		if (t[len] <= '7' && t[len] >= '0') {
  			buf[idx] = 0;
  			for(f = 0; f < 4 && len < 81 && t[len] <= '7' &&
  			    t[len] >= '0'; f++) 
  				buf[idx] = 8*buf[idx] + t[len++] - '0';
  			if (buf[idx] == 0) buf[idx] = '@';	
  			idx++; len--;
  			continue;
  		}
  		switch(t[len]) {
  			case 'r':
  				buf[idx++] = '\r';
  				break;
  			case 'n':
  				buf[idx++] = '\n';
  				break;
  			case 'b':
  				buf[idx++] = '\b';
  				break;
#ifndef _HPUX_SOURCE
  			case 'a':
  				buf[idx++] = '\a';	
  				break;
#endif
  			case 'f':
  				buf[idx++] = '\f';
  				break;
  			case 'c':
  				buf[idx++] = 255;
  				break;
  			default:
  				buf[idx++] = t[len];
  				break;
  		}	
  		sawesc = 0;
  		continue;
  	}
  	if (t[len] == '\\') {
  		sawesc = 1;
  		continue;
  	}
  	if (t[len] == '"') {
  		if (sawq == 1) {
  			sawq = 0;
  			len++;
  			break;
  		}	
  		sawq = 1;
  		continue;
  	}
  	if (t[len] == '$' && t[len + 1] == '(') {
  		for(f = len; t[f] && t[f] != ')'; f++)
  			;
  		if (t[f] == ')') {
  			strncpy(envbuf, &t[len + 2], f - len - 2);
  			envbuf[f - len - 2] = 0;
  			len = f;
  			env = mygetenv(envbuf);
  			if (env == CNULL) env = "";
  			while(*env) buf[idx++] = *env++;
  			continue;
  		}
  	}
  	if((!sawq && (t[len] == ' ' || t[len] == '\t')) || t[len] == 0) break;
  	buf[idx++] = t[len];
  }
  buf[idx] = 0;	
  (*s) += len;
  skipspace(s);	
  if (sawesc || sawq) syntaxerr("(badly formed word)");
  return(buf);
}

/*
 * Save a string to memory. Strip trailing '\n'.
 */
char *strsave(s)
char *s;
{
  char *t;
  int len;

  len = strlen(s);
  if (len && s[len - 1] == '\n') s[--len] = 0;
  if ((t = (char *)malloc(len + 1)) == (char *)0) return(t);
  strcpy(t, s);
  return(t);
}

/*
 * Throw away all malloced memory.
 */
void freemem()
{
  struct line *l;
  struct var *v;

  for(l = curenv->lines; l; l = l->next) {
  	free((char *)l->line);
  	free((char *)l);
  }
  for(v = curenv->vars; v; v = v->next) {
  	free(v->name);
  	free((char *)v);
  }
}

/*
 * Read a script into memory.
 */
int readscript(s)
char *s;
{
  FILE *fp;
  struct line *tl, *prev = LNULL;
  char *t;
  char buf[81];
  int no = 0;

  if ((fp = fopen(s, "r")) == (FILE *)0) {
  	s_error("runscript: couldn't open %s\r\n", s);
  	exit(1);
  }
  
  /* Read all the lines into a linked list in memory. */
  while((t = fgets(buf, 80, fp)) != CNULL) {
  	no++;
  	skipspace(&t);
  	if (*t == '\n') continue;
  	if (  ((tl = (struct line *)malloc(sizeof (struct line))) == LNULL) ||
  	      ((tl->line = strsave(t)) == CNULL)) {
  			s_error("script %s: out of memory\r\n", s);
  			exit(1);
  	}
  	if (prev)
  		prev->next = tl;
  	else
  		curenv->lines = tl;
  	tl->next = (struct line *)0;
  	tl->labelcount = 0;
  	tl->lineno = no;
  	prev = tl;
  }
  return(0);
}

/*
 * Read the next character, and return a string of characters last read.
 */
char *readchar()
{
  static char buf[65];
  static int bufp = 64;
  char c;

  buf[64] = 0;
  
#ifdef _SYSV
  memcpy(buf, buf + 1, 63);
#else
  /* This is Posix, I believe. */
  memmove(buf, buf + 1, 63);
#endif

  if (bufp) bufp--;	
  while(read(0, &c, 1) != 1)
  	;
  if (curenv->verbose) fputc(c, stderr);	
  buf[63] = c;
  return(buf + bufp);
}

/*
 * See if an expected string has yet been seen.
 */
int expfound(already, seek)
char *already, *seek;
{
  int len1, len2;
  int start = 0;

  len1 = strlen(already);
  len2 = strlen(seek);
  
  if (len1 > len2) start = len1 - len2;
  return(!strcmp(already + start, seek));
}

/*
 * Send text to a file (stdout or stderr).
 */
int output(text, fp)
char *text;
FILE *fp;
{
  unsigned char *w;
  int first = 1;
  int donl = 1;

  while((w = (unsigned char *)getword(&text)) != (unsigned char *)0) {
  	if (!first) fputc(' ', fp);
  	first = 0;
  	for(; *w; w++) {
  		if (*w == 255) {
  			donl = 0;
  			continue;
  		}
  		if (*w == '\n')
  			fputs(newline, fp);
  		else	
  			fputc(*w, fp);
  	}
  }
  if (donl) fputs(newline, fp);
  fflush(fp);
  return(OK);
}

/*
 * Find a variable in the list.
 * If it is not there, create it.
 */
struct var *getvar(name, cr)
char *name;
int cr;
{
  struct var *v, *end = VNULL;
  
  for(v = curenv->vars; v; v = v->next) {
  	end = v;
  	if (!strcmp(v->name, name)) return(v);
  }
  if (!cr) {
  	s_error("script \"%s\" line %d: unknown variable \"%s\"\r\n",
  		curenv->scriptname, thisline->lineno, name);
  	exit(1);
  }
  if ((v = (struct var *)malloc(sizeof(struct var))) == VNULL ||
      (v->name = strsave(name)) == CNULL) {
  	s_error("script \"%s\": out of memory\r\n", curenv->scriptname);
  	exit(1);
  }
  if (end)
	end->next = v;
  else
  	curenv->vars = v;	
  v->next = VNULL;
  v->value = 0;
  return(v);
}

/*
 * Read a number or variable.
 */
int getnum(text)
char *text;
{
  int val;

  if (!strcmp(text, "$?")) return(laststatus);
  if ((val = atoi(text)) != 0 || *text == '0') return(val);
  return(getvar(text, 0)->value);
}

  	
/*
 * Get the lines following "expect" into memory.
 */
struct line **buildexpect()
{
  static struct line *seq[17];
  int f;
  char *w, *t;
  
  for(f = 0; f < 16; f++) {
  	if (thisline == LNULL) {
  		s_error("script \"%s\": unexpected end of file\r\n", 
  			curenv->scriptname);
  		exit(1);	
  	}
  	t = thisline->line;
  	w = getword(&t);
  	if (!strcmp(w, "}")) {
  		if (*t) syntaxerr("(garbage after })"); 
  		return(seq);
  	}
  	seq[f] = thisline;
  	thisline = thisline->next;
  }
  if (f == 16) syntaxerr("(too many arguments)");
  return(seq);
}

/*
 * Our "expect" function.
 */
int expect(text)
char *text;
{
  char *s, *w;
  struct line **seq;
  struct line oneline;
  struct line *dflseq[2];
  char *toact = "exit 1";
  int found = 0, f, val, c;
  char *already, *action = CNULL;

  if (inexpect) {
  	s_error("script \"%s\" line %d: nested expect\r\n", curenv->scriptname,
  		thisline->lineno);
  	exit(1);
  }
  etimeout = 120;
  inexpect = 1;

  s = getword(&text);
  if (!strcmp(s, "{")) {
  	if (*text) syntaxerr("(garbage after {)");
  	thisline = thisline->next;
  	seq = buildexpect();
  } else {
 	dflseq[1] = LNULL;
 	dflseq[0] = &oneline;
 	oneline.line = text;
 	oneline.next = LNULL;
  }
  /* Seek a timeout command */
  for(f = 0; seq[f]; f++) {
  	if (!strncmp(seq[f]->line, "timeout", 7)) {
  		c = seq[f]->line[7];
  		if (c == 0 || (c != ' ' && c != '\t')) continue;
  		s = seq[f]->line + 7;
  		seq[f] = LNULL;
  		skipspace(&s);
  		w = getword(&s);
  		if (w == CNULL) syntaxerr("(argument expected)");
  		val = getnum(w);
  		if (val == 0) syntaxerr("(invalid argument)");
  		etimeout = val;
  		skipspace(&s);
  		if (*s != 0) toact = s;
  		break;
	}
  }
  if (setjmp(ejmp) != 0) {
  	f = s_exec(toact);
  	inexpect = 0;
  	return(f);
  }

  /* Allright. Now do the expect. */
  c = OK;
  while(!found) {
  	action = CNULL;
  	already = readchar();
  	for(f = 0; seq[f]; f++) {
  		s = seq[f]->line;
  		w = getword(&s);
  		if (expfound(already, w)) {
  			action = s;
  			found = 1;
  			break;
  		}
  	}
  	if (action != CNULL && *action) {
  		found = 0;
  		/* Maybe BREAK or RETURN */
  		if ((c = s_exec(action)) != OK) found = 1;
  	}
  }
  inexpect = 0;
  etimeout = 0;
  return(c);
}

/*
 * Jump to a shell and run a command.
 */
int shell(text)
char *text;
{
  laststatus = system(text);
  return(OK);
}

/*
 * Send output to stdout ( = modem)
 */
int dosend(text)
char *text;
{
  newline = "\r";
  return(output(text, stdout));
}

/*
 * Exit from the script, possibly with a value.
 */
int doexit(text)
char *text;
{
  char *w;
  int ret = 0;

  w = getword(&text);
  if (w != CNULL) ret = getnum(w);
  curenv->exstat = ret;
  longjmp(curenv->ebuf, 1);
  return(0);
}

/*
 * Goto a specific label.
 */
int dogoto(text)
char *text;
{
  char *w;
  struct line *l;
  char buf[32];
  int len;

  w = getword(&text);
  if (w == CNULL || *text) syntaxerr("(in goto/gosub label)");
  sprintf(buf, "%s:", w);
  len = strlen(buf);
  for(l = curenv->lines; l; l = l->next) if (!strncmp(l->line, buf, len)) break;
  if (l == LNULL) {
  	s_error("script \"%s\" line %d: label \"%s\" not found\r\n",
  		curenv->scriptname, thisline->lineno, w);
  	exit(1);
  }
  thisline = l;
  /* We return break, to automatically break out of expect loops. */
  return(BREAK);
}

/*
 * Goto a subroutine.
 */
int dogosub(text)
char *text;
{
  struct line *oldline;
  int ret = OK;

  oldline = thisline;
  dogoto(text);
  
  while(ret != ERR) {
	if ((thisline = thisline->next) == LNULL) {
		s_error("script \"%s\": no return from gosub\r\n", 
					curenv->scriptname);
		exit(1);
	}
	ret = s_exec(thisline->line);
	if (ret == RETURN) {
		ret = OK;
		thisline = oldline;
		break;
	}
  }
  return(ret);
}

/*
 * Return from a subroutine.
 */
int doreturn(text)
char *text;
{
  return(RETURN);
}

/*
 * Print text to stderr.
 */
int print(text)
char *text;
{
  newline = "\r\n";

  return(output(text, stderr));
}

/*
 * Declare a variable (integer)
 */
int doset(text)
char *text;
{
  char *w;
  struct var *v;

  w = getword(&text);
  if (w == CNULL) syntaxerr("(missing var name)");
  v = getvar(w, 1);
  if (*text) v->value = getnum(getword(&text));
  return(OK);
} 

/*
 * Lower the value of a variable.
 */
int dodec(text)
char *text;
{
  char *w;
  struct var *v;
  
  w = getword(&text);
  if (w == CNULL)  syntaxerr("(expected variable)");
  v = getvar(w, 0);
  v->value--;
  return(OK);
}

/*
 * Increase the value of a variable.
 */
int doinc(text)
char *text;
{
  char *w;
  struct var *v;
  
  w = getword(&text);
  if (w == CNULL)  syntaxerr("(expected variable)");
  v = getvar(w, 0);
  v->value++;
  return(OK);
}

/*
 * If syntax: if n1 [><=] n2 command.
 */
int doif(text)
char *text;
{
  char *w;
  int n1;
  int n2;
  char op;
  
  if ((w = getword(&text)) == CNULL) syntaxerr("(if)");
  n1 = getnum(w);
  if ((w = getword(&text)) == CNULL) syntaxerr("(if)");
  if (*w == 0 || w[1] != 0) syntaxerr("(if)");
  op = *w;
  if ((w = getword(&text)) == CNULL) syntaxerr("(if)");
  n2 = getnum(w);
  if (!*text) syntaxerr("(expected command after if)");
  
  if (op == '=') {
  	if (n1 != n2) return(OK);
  } else if (op == '>') {
  	if (n1 <= n2) return(OK);
  } else if (op == '<') {
  	if (n1 >= n2) return(OK);
  } else {
  	syntaxerr("(unknown operator)");
  }
  return(s_exec(text));
}

/*
 * Set the global timeout-time.
 */
int dotimeout(text)
char *text;
{
  char *w;
  int val;

  w = getword(&text);
  if(w == CNULL) syntaxerr("(argument expected)");
  if ((val = getnum(w)) == 0) syntaxerr("(invalid argument)");
  gtimeout = val;
  return(OK);
}

/*
 * Turn verbose on/off (= echo stdin to stderr)
 */
int doverbose(text)
char *text;
{
  char *w;

  curenv->verbose = 1;

  if ((w = getword(&text)) != CNULL) {
	if (!strcmp(w, "on")) return(OK);
	if (!strcmp(w, "off")) {
		curenv->verbose = 0;
		return(OK);
	}
  }	
  syntaxerr("(unexpected argument)");
  return(ERR);
}

/*
 * Sleep for a certain number of seconds.
 */
int dosleep(text)
char *text;
{
  int foo, tm;
  
  tm = getnum(text);
  
  foo = gtimeout - tm;
  
  /* The alarm goes off every second.. */
  while(gtimeout != foo) pause();
  return(OK);
}

/*
 * Break out of an expect loop.
 */
int dobreak()
{
  if (!inexpect) {
  	s_error("script \"%s\" line %d: break outside of expect\r\n",
  		curenv->scriptname, thisline->lineno);
  	exit(1);
  }
  return(BREAK);
}

/*
 * Call another script!
 */
int docall(text)
char *text;
{
  struct line *oldline;
  struct env *oldenv;
  int er;

  if (*text == 0) syntaxerr("(argument expected)");

  if (inexpect) {
  	s_error("script \"%s\" line %d: call inside expect\r\n",
  			curenv->scriptname);
  	exit(1);
  }

  oldline = thisline;
  oldenv = curenv;
  if ((er = execscript(text)) != 0) exit(er);
  freemem();
  thisline = oldline;
  curenv = oldenv;
  return(0);
}

  
/* KEYWORDS */
struct kw {
  char *command;
  int (*fn)();
} keywords[] = {
  { "expect",	expect },
  { "send",	dosend },
  { "!",	shell },
  { "goto",	dogoto },
  { "gosub",	dogosub },
  { "return",	doreturn },
  { "exit",	doexit },
  { "print",	print },
  { "set",	doset },
  { "inc",	doinc },
  { "dec",	dodec },
  { "if",	doif },
  { "timeout",	dotimeout },
  { "verbose",	doverbose },
  { "sleep",	dosleep },
  { "break",	dobreak },
  { "call",	docall },
  { (char *)0,	(int(*)())0 },
};
 
/*
 * Execute one line.
 */
int s_exec(text)
char *text;
{
  char *w;
  struct kw *k;

  w = getword(&text);

  /* If it is a label or a comment, skip it. */
  if (w == CNULL || *w == '#' || w[strlen(w) - 1] == ':') return(OK);
  
  /* See which command it is. */
  for(k = keywords; k->command; k++)
  	if (!strcmp(w, k->command)) break;

  /* Command not found? */
  if (k->command == (char *)NULL) {
	s_error("script \"%s\" line %d: unknown command \"%s\"\r\n",
  		curenv->scriptname, thisline->lineno, w);
	exit(1);
  }
  return((*(k->fn))(text));
}

/*
 * Run the script by continously executing "thisline".
 */
int execscript(s)
char *s;
{
  struct env env;
  int ret;

  curenv = &env;
  curenv->lines = LNULL;
  curenv->vars  = VNULL;
  curenv->verbose = 1;
  curenv->scriptname = s;

  if (readscript(s) < 0) {
  	freemem();
  	return(ERR);
  }
  
  signal(SIGALRM, myclock);
  alarm(1);
  if (setjmp(curenv->ebuf) == 0) {
	thisline = curenv->lines;
	while(thisline != LNULL && (ret == s_exec(thisline->line)) != ERR)
  		thisline = thisline->next;
  } else
  	ret = curenv->exstat ? ERR : 0;		
  return(ret);
}

int main(argc, argv)
int argc;
char **argv;
{
  signal(SIGHUP, SIG_IGN);

  init_env();

  if (argc < 2) {
  	s_error("Usage: runscript <scriptfile>\r\n");
  	return(1);
  }

  return(execscript(argv[1]));
}
