/*
 * pipeline.c
 * 
 * $Id: pipeline.c,v 1.13 2003/11/29 10:11:16 dgc Exp $
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/signal.h>
#include <sys/wait.h>
#include "dbuf.h"
#ifdef EMBED_SH
# include EMBED_SH
#endif

char *A0;			/* remember argv[0] */
int Debug = 0;			/* for -d */
int Invert = 0;			/* for -v */
int Suppress = 0;		/* for -s */
int Stderr = 0;			/* for -e */
int Shell = 0;			/* for -c */

char *_s = "(null)";		/* for S() */
#define S(s) (s?s:_s)		/* non-null string */

#define ST_TRUE		1		/* State, for truth evaluation */
#define ST_FALSE	~ST_TRUE	/* State, for truth evaluation */
#define OP_AND		1		/* AND conjunction for truth eval. */
#define OP_OR		2		/* OR conjunction for truth eval. */
#define OP_THEN		3		/* THEN conjunction for truth eval. */
#define OP_IF		4		/* IF conjunction for truth eval. */

#define STATE(s)	((s) == ST_TRUE ? "true" : "false")
#define OP(o)		((o) == OP_AND ? "and" : ((o) == OP_OR ? "or" : ((o) == OP_THEN ? "then" : ((o) == OP_IF ? "if" : "unknown"))))


/* RCS/CVS Id, for ident(1) and for -V */
static const char rcsid[] = "$Id: pipeline.c,v 1.13 2003/11/29 10:11:16 dgc Exp $";


/*
 * Show usage.
 */
void
usage(void)
{
	fprintf(stderr, "usage: %s [-cdesvV] [-i file] [-o file] [--] \\\n",
		A0);
	fprintf(stderr, "       [-c] cmdA [{conj} [-c] cmdB [...]]\n");
	fprintf(stderr, "       conjunctions are: || && | ? (-or -and -then -if)\n");
}


/*
 * Set up stdio and create a filter process.
 */
int
mkfilter(int argc, char **argv, int *in, int *out, int shell)
{
	int	 pid, status;
	int	 i, j, n;
	char	*buf;
	char	*shargv[] = { "/bin/sh", "-c", "dummy", NULL };

	pid = fork();
	if (pid < 0)
		return -errno;

	if (pid > 0) {
		/* parent */
		close(in[0]);
		close(out[1]);
		return pid;
	}

	else {
		/* child */
		close(in[1]);
		close(out[0]);
		dup2(in[0], 0);
		dup2(out[1], 1);
		if (Stderr)
			dup2(out[1], 2);

		if (shell) {
			n = 0;
			for (i = 0; i < argc; ++i) {
				n += strlen(argv[i]) + 1;
			}
			buf = malloc(n+1);
			n = 0;
			for (i = 0; i < argc; ++i) {
				for (j = 0; argv[i][j]; ++j) {
					buf[n++] = argv[i][j];
				}
				buf[n++] = ' ';
			}
			buf[--n] = '\0';
			if (Debug)
				fprintf(stderr, "%s: shell: \"%s\"\n", A0, buf);

			argv = shargv;
			argv[2] = buf;
		}

		execvp(argv[0], argv);
		fprintf(stderr, "%s: cannot exec \"%s\": %s\n",
			A0, S(argv[0]), strerror(errno));
		exit(20);
	}
}


/*
 * Create a filter, feed input, accumulate output, watch for process death.
 */
int
filter(int argc, char **argv, dbuf_t *in, dbuf_t *out, int shell, int passno)
{
	int pid;
	int fin[2], fout[2];
	int status;

#ifdef DEBUG
	{
		int i;
		printf("Next filter [%d]\n", passno);
		for (i = 0; argv[i]; ++i) {
			printf ("\t%02d: argv[%d] = %s\n",
				passno, i, S(argv[i]));
		}
	}
#endif

	if (pipe(fin)) {
		fprintf(stderr, "%s: cannot create fd pair: %s\n",
			A0, strerror(errno));
		return 2;
	}
	if (pipe(fout)) {
		fprintf(stderr, "%s: cannot create fd pair: %s\n",
			A0, strerror(errno));
		return 2;
	}

	pid = mkfilter(argc, argv, fin, fout, shell);
	if (pid < 0) {
		fprintf(stderr, "%s: cannot create filter: %s\n",
			A0, strerror(-pid));
		return 2;
	}

	if (waitpid(pid, &status, WNOHANG) == 0) {
		/* process is still going */

		dbuf_write(in, fin[1], DBUF_ALL);
	}

	close(fin[1]);

	dbuf_read(out, fout[0], DBUF_ALL);
	close(fout[0]);

	waitpid(pid, &status, 0);

	return status;
}


/*
 * yes
 */
main(int argc, char *argv[])
{
	char	**v0, **v1, *vsaved;
	dbuf_t	*buf1, *buf2, *bin, *bout, *bswap;
	int	 c, errs, stage, status, state, op, fd, output;
	char	*infile = NULL, *outfile = NULL;

	/* Remember what we're called */
	if ((A0 = strrchr(argv[0], '/')) == NULL)
		A0 = argv[0];
	else
		++A0;


	/* Parse options */
	errs = 0;
	while ((c = getopt(argc, argv, "cdehni:o:svV")) != EOF) {
		switch (c) {
			case 'c':
				Shell = 1;
				break;

			case 'd':
				Debug = 1;
				break;

			case 'e':
				Stderr = 1;
				break;

			case 'h':
				++errs;
				break;

			case 'i':
				infile = optarg;
				break;

			case 'o':
				outfile = optarg;
				break;

			case 's':
				Suppress = 1;
				break;

			case 'v':
				Invert = 1;
				break;

			case 'V':
				fprintf(stderr, "\n%s: %s\n", A0, rcsid);
#ifdef EMBED_SH
				fprintf(stderr, "with embed: %s\n\n",
					embed_rcsid);
#endif /* EMBED_SH */
				break;

			default:
				fprintf(stderr, "%s: -%c: unknown option\n",
					A0, optopt);
				++errs;
				break;
		}
	}

	/* Make sure we have a non-option argument */
	if (optind == argc)
		++errs;

	/* Fail usage? */
	if (errs) {
		usage();
		fprintf(stderr, "%s: %d error%s\n",
			A0, errs, (errs == 1 ? "" : "s"));
		exit(2);
	}


	/* Alloc swap buffers for passing data in/out of pipe stages */
	buf1 = dbuf_new(2048);	/* 2048 is size of one extent */
	buf2 = dbuf_new(2048);


	v0 = &argv[optind];	/* Point to first non-option */
	bin = buf1;		/* Set up input buffer */
	bout = buf2;		/* and output buffer */
	stage = 0;		/* Tracks which stage we're on */


	/* Read initial pipeline input from somewhere */
	if (infile && !strcmp(infile, "-")) {
		/* From stdin */
		dbuf_read(bout, 0, DBUF_ALL);
	}
	else if (infile) {
		/* From -i file */
		fd = open(infile, O_RDONLY);
		if (fd < 0) {
			fprintf(stderr, "%s: cannot open \"%s\": %s\n",
				A0, S(infile), strerror(errno));
			dbuf_destroy(buf1);
			dbuf_destroy(buf2);
			exit(10);
		}
		dbuf_read(bout, fd, DBUF_ALL);
		close(fd);
	}
	else {
		/* no input */
		int fd;
		fd = open("/dev/null", O_RDONLY);
		dbuf_read(bout, fd, DBUF_ALL);
		close(fd);
	}

#ifdef DEBUG
	dbuf_write(bout, 1, DBUF_ALL);
#endif


	state = ST_TRUE;	/* Truth state of logical conjuctions */
	op = OP_AND;		/* Current truth operation */


	/*
	 * Loop over pipe stages. With each pass over this while,
	 * we begin a new stage of the pipeline.
	 */
	while (*v0) {
		++stage;
		v1 = v0;

		if (Debug)
			fprintf(stderr, "\n%s: stage %d\n", A0, stage);

		/* Look ahead to end of this command argv */
		while (*v1 && strcmp(*v1, "&&")   && strcmp(*v1, "-and") &&
		              strcmp(*v1, "||") && strcmp(*v1, "-or") &&
		              strcmp(*v1, "|") && strcmp(*v1, "-then") &&
		              strcmp(*v1, "?") && strcmp(*v1, "-if"))
			++v1;

		/*
		 * Save what we point at for later use,
		 * and terminate for execv()'s use.
		 */
		vsaved = *v1;
		*v1 = '\0';


		/*
		 * Check truth table. If state and operation tell us
		 * to evaluate the current stage, do so.
		 */
		if ((state == ST_TRUE && op == OP_AND) ||
		    (state == ST_FALSE && op == OP_OR) ||
		    (op == OP_IF && dbuf_size(bout)) ||
		    (op == OP_THEN)) {

			/*
			 * Swap buffers: last stage's output becomes this
			 * stage's input; last stage's input is no longer
			 * useful, so we reuse the buffer for output.
			 */
			bswap = bout;
			bout = bin;
			bin = bswap;
			dbuf_clear(bout);

			/* Report state/op/cmd */
			if (Debug) {
				fprintf(stderr,
					"%s: state=%s, op=%s: exec %s\n",
					A0, STATE(state), OP(op), *v0);
			}

#ifdef DEBUG
			fprintf(stderr, "%s input:\n", v0[0]);
			fflush(stderr);
			dbuf_write(bin, 2, DBUF_ALL);
#endif

			/* Execute this stage. Status is waitpid() status. */
			if (Shell)
				status = filter(v1-v0, v0, bin, bout, 1, stage);
			else if (!strcmp(*v0, "-c"))
				status = filter(v1-v0-1, ++v0, bin, bout, 1,
					stage);
			else
				status = filter(v1-v0, v0, bin, bout, 0, stage);

			/* ... */
			if (Debug) {
				fprintf(stderr, "%s: %s exited %d\n",
					A0, v0[0], WEXITSTATUS(status));
				fflush(stderr);
			}

			/* Calculate state for next TT evaluation */
			state = WEXITSTATUS(status) ? ST_FALSE : ST_TRUE;
			if (Debug) {
				fprintf(stderr, "%s: state := %s\n",
					A0, STATE(state));
			}
		}

		/* If truth table says no go, just report it (under -d). */
		else {
			if (Debug) {
				fprintf(stderr,
					"%s: state=%s, op=%s: noexec %s\n",
					A0, STATE(state), OP(op), *v0);
			}
		}

		/* If !vsaved, we're at end of arg list -- no more stages. */
		if (!vsaved)
			break;


		/* Otherwise, evaluate conjunction for next stage. */
		if (!strcmp(vsaved, "||") || !strcmp(vsaved, "-or"))
			op = OP_OR;
		else if (!strcmp(vsaved, "&&") || !strcmp(vsaved, "-and"))
			op = OP_AND;
		else if (!strcmp(vsaved, "|") || !strcmp(vsaved, "-then"))
			op = OP_THEN;
		else if (!strcmp(vsaved, "?") || !strcmp(vsaved, "-if"))
			op = OP_IF;

		/* (report it?) */
		if (Debug) {
			fprintf(stderr, "%s: op := %s\n",
				A0, OP(op));
		}

		/* Point up to next command */
		v0 = ++v1;
	}

	/* Report final state? */
	if (Debug) {
		fprintf(stderr, "%s: final state=%s\n", A0, STATE(state));
	}

	/* Invert state? */
	if (Invert) {
		state = ~state;
		fprintf(stderr, "%s: inverting state := %s\n",
			A0, STATE(state));
	}


	/*
	 * If final state is false and Suppress (-s) is in effect,
	 * disable output.
	 */
	output = 1;
	if (state == ST_FALSE && Suppress)
		output = 0;


	/* Show output? */
	if (output && (!outfile || !strcmp(outfile, "-"))) {
		/* Yes, to stdout */
		dbuf_write(bout, 1, DBUF_ALL);
	}
	else if (output && outfile) {
		/* Yes, to -o file */
		fd = open(outfile, O_WRONLY|O_CREAT|O_TRUNC, 0666);
		if (fd < 0) {
			fprintf(stderr, "%s: cannot open \"%s\": %s\n",
				A0, S(outfile), strerror(errno));
			dbuf_destroy(buf1);
			dbuf_destroy(buf2);
			exit(15);
		}
		dbuf_write(bout, fd, DBUF_ALL);
		close(fd);
	}

	/* Clean up swap buffers */
	dbuf_destroy(buf1);
	dbuf_destroy(buf2);

	/* Bye */
	exit(WEXITSTATUS(status));
}

