/*
 * io.c --- routines for dealing with input and output and records
 */

/* 
 * Copyright (C) 1986, 1988, 1989, 1991-1995 the Free Software Foundation, Inc.
 * 
 * This file is part of GAWK, the GNU implementation of the
 * AWK Programming Language.
 * 
 * GAWK 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.
 * 
 * GAWK 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; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
 */

#include "awk.h"

#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif /* HAVE_SYS_PARAM_H */

#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif /* HAVE_SYS_WAIT_H */

#ifndef O_RDONLY
#include <fcntl.h>
#endif

#include <assert.h>

#if ! defined(S_ISREG) && defined(S_IFREG)
#define	S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#endif

#if ! defined(S_ISDIR) && defined(S_IFDIR)
#define	S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#endif

#ifndef ENFILE
#define ENFILE EMFILE
#endif

#ifdef atarist
#include <stddef.h>
#endif

#if defined(MSDOS) || defined(OS2)
#define PIPES_SIMULATED
#endif

static IOBUF *nextfile P((int skipping));
static int inrec P((IOBUF *iop));
static int iop_close P((IOBUF *iop));
struct redirect *redirect P((NODE *tree, int *errflg));
static void close_one P((void));
static int close_redir P((struct redirect *rp, int exitwarn));
#ifndef PIPES_SIMULATED
static int wait_any P((int interesting));
#endif
static IOBUF *gawk_popen P((char *cmd, struct redirect *rp));
static IOBUF *iop_open P((const char *file, const char *how));
static int gawk_pclose P((struct redirect *rp));
static int do_pathopen P((const char *file));
static int get_a_record P((char **out, IOBUF *iop, int rs, Regexp *RSre, int *errcode));
static int str2mode P((const char *mode));
static void spec_setup P((IOBUF *iop, int len, int allocate));
static int specfdopen P((IOBUF *iop, const char *name, const char *mode));
static int pidopen P((IOBUF *iop, const char *name, const char *mode));
static int useropen P((IOBUF *iop, const char *name, const char *mode));

#if defined (MSDOS) && !defined (__GO32__)
#include "popen.h"
#define popen(c, m)	os_popen(c, m)
#define pclose(f)	os_pclose(f)
#else
#if defined (OS2)	/* OS/2, but not family mode */
#if defined (_MSC_VER)
#define popen(c, m)	_popen(c, m)
#define pclose(f)	_pclose(f)
#endif
#else
extern FILE	*popen();
#endif
#endif

static struct redirect *red_head = NULL;
static NODE *RS;
static Regexp *RS_regexp;

int RS_is_null;

extern int output_is_tty;
extern NODE *ARGC_node;
extern NODE *ARGV_node;
extern NODE *ARGIND_node;
extern NODE *ERRNO_node;
extern NODE **fields_arr;

static jmp_buf filebuf;		/* for do_nextfile() */

/* do_nextfile --- implement gawk "next file" extension */

void
do_nextfile()
{
	(void) nextfile(TRUE);
	longjmp(filebuf, 1);
}

/* nextfile --- move to the next input data file */

static IOBUF *
nextfile(skipping)
int skipping;
{
	static long i = 1;
	static int files = 0;
	NODE *arg;
	static IOBUF *curfile = NULL;

	if (skipping) {
		if (curfile != NULL)
			iop_close(curfile);
		curfile = NULL;
		return NULL;
	}
	if (curfile != NULL) {
		if (curfile->cnt == EOF) {
			(void) iop_close(curfile);
			curfile = NULL;
		} else
			return curfile;
	}
	for (; i < (long) (ARGC_node->lnode->numbr); i++) {
		arg = *assoc_lookup(ARGV_node, tmp_number((AWKNUM) i));
		if (arg->stlen == 0)
			continue;
		arg->stptr[arg->stlen] = '\0';
		if (! do_traditional) {
			unref(ARGIND_node->var_value);
			ARGIND_node->var_value = make_number((AWKNUM) i);
		}
		if (! arg_assign(arg->stptr)) {
			files++;
			curfile = iop_open(arg->stptr, "r");
			if (curfile == NULL)
				fatal("cannot open file `%s' for reading (%s)",
					arg->stptr, strerror(errno));
				/* NOTREACHED */
			/* This is a kludge.  */
			unref(FILENAME_node->var_value);
			FILENAME_node->var_value = dupnode(arg);
			FNR = 0;
			i++;
			break;
		}
	}
	if (files == 0) {
		files++;
		/* no args. -- use stdin */
		/* FNR is init'ed to 0 */
		FILENAME_node->var_value = make_string("-", 1);
		curfile = iop_alloc(fileno(stdin), "stdin");
	}
	return curfile;
}

/* set_FNR --- update internal FNR from awk variable */

void
set_FNR()
{
	FNR = (long) FNR_node->var_value->numbr;
}

/* set_NR --- update internal NR from awk variable */

void
set_NR()
{
	NR = (long) NR_node->var_value->numbr;
}

/* inrec --- This reads in a record from the input file */

static int
inrec(iop)
IOBUF *iop;
{
	char *begin;
	register int cnt;
	int retval = 0;

	cnt = get_a_record(&begin, iop, RS->stptr[0], RS_regexp, NULL);
	if (cnt == EOF) {
		cnt = 0;
		retval = 1;
	} else {
		NR += 1;
		FNR += 1;
		set_record(begin, cnt, TRUE);
	}

	return retval;
}

/* iop_close --- close an open IOP */

static int
iop_close(iop)
IOBUF *iop;
{
	int ret;

	if (iop == NULL)
		return 0;
	errno = 0;

#ifdef _CRAY
	/* Work around bug in UNICOS popen */
	if (iop->fd < 3)
		ret = 0;
	else
#endif
	/* save these for re-use; don't free the storage */
	if ((iop->flag & IOP_IS_INTERNAL) != 0) {
		iop->off = iop->buf;
		iop->end = iop->buf + strlen(iop->buf);
		iop->cnt = 0;
		iop->secsiz = 0;
		return 0;
	}

	/* Don't close standard files or else crufty code elsewhere will lose */
	if (iop->fd == fileno(stdin)
	    || iop->fd == fileno(stdout)
	    || iop->fd == fileno(stderr))
		ret = 0;
	else
		ret = close(iop->fd);
	if (ret == -1)
		warning("close of fd %d (`%s') failed (%s)", iop->fd,
				iop->name, strerror(errno));
	if ((iop->flag & IOP_NO_FREE) == 0) {
		/*
		 * be careful -- $0 may still reference the buffer even though
		 * an explicit close is being done; in the future, maybe we
		 * can do this a bit better
		 */
		if (iop->buf) {
			if ((fields_arr[0]->stptr >= iop->buf)
			    && (fields_arr[0]->stptr < iop->end)) {
				NODE *t;
	
				t = make_string(fields_arr[0]->stptr,
						fields_arr[0]->stlen);
				unref(fields_arr[0]);
				fields_arr [0] = t;
				reset_record();
			}
  			free(iop->buf);
		}
		free((char *) iop);
	}
	return ret == -1 ? 1 : 0;
}

/* do_input --- the main input processing loop */

void
do_input()
{
	IOBUF *iop;
	extern int exiting;

	(void) setjmp(filebuf);	/* for `nextfile' */

	while ((iop = nextfile(FALSE)) != NULL) {
		if (inrec(iop) == 0)
			while (interpret(expression_value) && inrec(iop) == 0)
				continue;
#ifdef C_ALLOCA
		/* recover any space from C based alloca */
		(void) alloca(0);
#endif
		if (exiting)
			break;
	}
}

/* redirect --- Redirection for printf and print commands */

struct redirect *
redirect(tree, errflg)
NODE *tree;
int *errflg;
{
	register NODE *tmp;
	register struct redirect *rp;
	register char *str;
	int tflag = 0;
	int outflag = 0;
	const char *direction = "to";
	const char *mode;
	int fd;
	const char *what = NULL;

	switch (tree->type) {
	case Node_redirect_append:
		tflag = RED_APPEND;
		/* FALL THROUGH */
	case Node_redirect_output:
		outflag = (RED_FILE|RED_WRITE);
		tflag |= outflag;
		if (tree->type == Node_redirect_output)
			what = ">";
		else
			what = ">>";
		break;
	case Node_redirect_pipe:
		tflag = (RED_PIPE|RED_WRITE);
		what = "|";
		break;
	case Node_redirect_pipein:
		tflag = (RED_PIPE|RED_READ);
		what = "|";
		break;
	case Node_redirect_input:
		tflag = (RED_FILE|RED_READ);
		what = "<";
		break;
	default:
		fatal("invalid tree type %d in redirect()", tree->type);
		break;
	}
	tmp = tree_eval(tree->subnode);
	if (do_lint && (tmp->flags & STR) == 0)
		warning("expression in `%s' redirection only has numeric value",
			what);
	tmp = force_string(tmp);
	str = tmp->stptr;
	if (str == NULL || *str == '\0')
		fatal("expression for `%s' redirection has null string value",
			what);
	if (do_lint
	    && (STREQN(str, "0", tmp->stlen) || STREQN(str, "1", tmp->stlen)))
		warning("filename `%s' for `%s' redirection may be result of logical expression", str, what);
	for (rp = red_head; rp != NULL; rp = rp->next)
		if (strlen(rp->value) == tmp->stlen
		    && STREQN(rp->value, str, tmp->stlen)
		    && ((rp->flag & ~(RED_NOBUF|RED_EOF)) == tflag
			|| (outflag != 0
			    && (rp->flag & (RED_FILE|RED_WRITE)) == outflag)))
			break;
	if (rp == NULL) {
		emalloc(rp, struct redirect *, sizeof(struct redirect),
			"redirect");
		emalloc(str, char *, tmp->stlen+1, "redirect");
		memcpy(str, tmp->stptr, tmp->stlen);
		str[tmp->stlen] = '\0';
		rp->value = str;
		rp->flag = tflag;
		rp->fp = NULL;
		rp->iop = NULL;
		rp->pid = 0;	/* unlikely that we're worried about init */
		rp->status = 0;
		/* maintain list in most-recently-used first order */
		if (red_head != NULL)
			red_head->prev = rp;
		rp->prev = NULL;
		rp->next = red_head;
		red_head = rp;
	}
	while (rp->fp == NULL && rp->iop == NULL) {
		if (rp->flag & RED_EOF)
			/*
			 * encountered EOF on file or pipe -- must be cleared
			 * by explicit close() before reading more
			 */
			return rp;
		mode = NULL;
		errno = 0;
		switch (tree->type) {
		case Node_redirect_output:
			mode = "w";
			if ((rp->flag & RED_USED) != 0)
				mode = "a";
			break;
		case Node_redirect_append:
			mode = "a";
			break;
		case Node_redirect_pipe:
			if ((rp->fp = popen(str, "w")) == NULL)
				fatal("can't open pipe (\"%s\") for output (%s)",
					str, strerror(errno));
			rp->flag |= RED_NOBUF;
			break;
		case Node_redirect_pipein:
			direction = "from";
			if (gawk_popen(str, rp) == NULL)
				fatal("can't open pipe (\"%s\") for input (%s)",
					str, strerror(errno));
			break;
		case Node_redirect_input:
			direction = "from";
			rp->iop = iop_open(str, "r");
			break;
		default:
			cant_happen();
		}
		if (mode != NULL) {
			fd = devopen(str, mode);
			if (fd > INVALID_HANDLE) {
				if (fd == fileno(stdin))
					rp->fp = stdin;
				else if (fd == fileno(stdout))
					rp->fp = stdout;
				else if (fd == fileno(stderr))
					rp->fp = stderr;
				else {
					rp->fp = fdopen(fd, (char *) mode);
					/* don't leak file descriptors */
					if (rp->fp == NULL)
						close(fd);
				}
				if (rp->fp != NULL && isatty(fd))
					rp->flag |= RED_NOBUF;
			}
		}
		if (rp->fp == NULL && rp->iop == NULL) {
			/* too many files open -- close one and try again */
			if (errno == EMFILE || errno == ENFILE)
				close_one();
			else {
				/*
				 * Some other reason for failure.
				 *
				 * On redirection of input from a file,
				 * just return an error, so e.g. getline
				 * can return -1.  For output to file,
				 * complain. The shell will complain on
				 * a bad command to a pipe.
				 */
				if (errflg != NULL)
					*errflg = errno;
				if (tree->type == Node_redirect_output
				    || tree->type == Node_redirect_append)
					fatal("can't redirect %s `%s' (%s)",
					    direction, str, strerror(errno));
				else {
					free_temp(tmp);
					return NULL;
				}
			}
		}
	}
	free_temp(tmp);
	return rp;
}

/* getredirect --- find the struct redirect for this file or pipe */

struct redirect *
getredirect(str, len)
char *str;
int len;
{
	struct redirect *rp;

	for (rp = red_head; rp != NULL; rp = rp->next)
		if (strlen(rp->value) == len && STREQN(rp->value, str, len))
			return rp;

	return NULL;
}

/* close_one --- temporarily close an open file to re-use the fd */

static void
close_one()
{
	register struct redirect *rp;
	register struct redirect *rplast = NULL;

	/* go to end of list first, to pick up least recently used entry */
	for (rp = red_head; rp != NULL; rp = rp->next)
		rplast = rp;
	/* now work back up through the list */
	for (rp = rplast; rp != NULL; rp = rp->prev)
		if (rp->fp != NULL && (rp->flag & RED_FILE) != 0) {
			rp->flag |= RED_USED;
			errno = 0;
			if (/* do_lint && */ fclose(rp->fp) != 0)
				warning("close of \"%s\" failed (%s).",
					rp->value, strerror(errno));
			rp->fp = NULL;
			break;
		}
	if (rp == NULL)
		/* surely this is the only reason ??? */
		fatal("too many pipes or input files open"); 
}

/* do_close --- completely close an open file or pipe */

NODE *
do_close(tree)
NODE *tree;
{
	NODE *tmp;
	register struct redirect *rp;

	tmp = force_string(tree_eval(tree->subnode));
	for (rp = red_head; rp != NULL; rp = rp->next) {
		if (strlen(rp->value) == tmp->stlen
		    && STREQN(rp->value, tmp->stptr, tmp->stlen))
			break;
	}
	free_temp(tmp);
	if (rp == NULL) /* no match */
		return tmp_number((AWKNUM) 0.0);
	fflush(stdout);	/* synchronize regular output */
	tmp = tmp_number((AWKNUM) close_redir(rp, FALSE));
	rp = NULL;
	return tmp;
}

/* close_redir --- close an open file or pipe */

static int
close_redir(rp, exitwarn)
register struct redirect *rp;
int exitwarn;
{
	int status = 0;
	char *what;

	if (rp == NULL)
		return 0;
	if (rp->fp == stdout || rp->fp == stderr)
		return 0;
	errno = 0;
	if ((rp->flag & (RED_PIPE|RED_WRITE)) == (RED_PIPE|RED_WRITE))
		status = pclose(rp->fp);
	else if (rp->fp != NULL)
		status = fclose(rp->fp);
	else if (rp->iop != NULL) {
		if ((rp->flag & RED_PIPE) != 0)
			status = gawk_pclose(rp);
		else {
			status = iop_close(rp->iop);
			rp->iop = NULL;
		}
	}

	what = ((rp->flag & RED_PIPE) != 0) ? "pipe" : "file";

	if (exitwarn) 
		warning("no explicit close of %s `%s' provided",
			what, rp->value);

	/* SVR4 awk checks and warns about status of close */
	if (do_lint && status != 0) {
		char *s = strerror(errno);

		/*
		 * Too many people have complained about this.
		 * As of 2.15.6, it is now under lint control.
		 */
		if (do_lint)
			warning("failure status (%d) on %s close of \"%s\" (%s)",
				status, what, rp->value, s);

		if (! do_traditional) {
			/* set ERRNO too so that program can get at it */
			unref(ERRNO_node->var_value);
			ERRNO_node->var_value = make_string(s, strlen(s));
		}
	}
	if (rp->next != NULL)
		rp->next->prev = rp->prev;
	if (rp->prev != NULL)
		rp->prev->next = rp->next;
	else
		red_head = rp->next;
	free(rp->value);
	free((char *) rp);
	return status;
}

/* flush_io --- flush all open output files */

int
flush_io()
{
	register struct redirect *rp;
	int status = 0;

	errno = 0;
	if (fflush(stdout)) {
		warning("error writing standard output (%s)", strerror(errno));
		status++;
	}
	if (fflush(stderr)) {
#ifndef __amigados__  /* HACK (fnf) */
		warning("error writing standard error (%s)", strerror(errno));
		status++;
#endif
	}
	for (rp = red_head; rp != NULL; rp = rp->next)
		/* flush both files and pipes, what the heck */
		if ((rp->flag & RED_WRITE) && rp->fp != NULL) {
			if (fflush(rp->fp)) {
				warning("%s flush of \"%s\" failed (%s).",
				    (rp->flag  & RED_PIPE) ? "pipe" :
				    "file", rp->value, strerror(errno));
				status++;
			}
		}
	return status;
}

/* close_io --- close all open files, called when exiting */

int
close_io()
{
	register struct redirect *rp;
	register struct redirect *next;
	int status = 0;

	errno = 0;
	for (rp = red_head; rp != NULL; rp = next) {
		next = rp->next;
		/*
		 * close_redir() will print a message if needed
		 * if do_lint, warn about lack of explicit close
		 */
		if (close_redir(rp, do_lint))
			status++;
		rp = NULL;
	}
	/*
	 * Some of the non-Unix os's have problems doing an fclose
	 * on stdout and stderr.  Since we don't really need to close
	 * them, we just flush them, and do that across the board.
	 */
	if (fflush(stdout)) {
		warning("error writing standard output (%s)", strerror(errno));
		status++;
	}
	if (fflush(stderr)) {
#ifndef __amigados__  /* HACK (fnf) */
		warning("error writing standard error (%s)", strerror(errno));
		status++;
#endif
	}
	return status;
}

/* str2mode --- convert a string mode to an integer mode */

static int
str2mode(mode)
const char *mode;
{
	int ret;

	switch(mode[0]) {
	case 'r':
		ret = O_RDONLY;
		break;

	case 'w':
		ret = O_WRONLY|O_CREAT|O_TRUNC;
		break;

	case 'a':
		ret = O_WRONLY|O_APPEND|O_CREAT;
		break;

	default:
		ret = 0;		/* lint */
		cant_happen();
	}
	return ret;
}

/* devopen --- handle /dev/std{in,out,err}, /dev/fd/N, regular files */

/*
 * This separate version is still needed for output, since file and pipe
 * output is done with stdio. iop_open() handles input with IOBUFs of
 * more "special" files.  Those files are not handled here since it makes
 * no sense to use them for output.
 */

int
devopen(name, mode)
const char *name, *mode;
{
	int openfd;
	const char *cp;
	char *ptr;
	int flag = 0;
	struct stat buf;
	extern double strtod();

	flag = str2mode(mode);

	if (STREQ(name, "-"))
		openfd = fileno(stdin);
	else
		openfd = INVALID_HANDLE;

	if (do_traditional)
		goto strictopen;

	if ((openfd = os_devopen(name, flag)) >= 0)
		return openfd;

	if (STREQN(name, "/dev/", 5) && stat((char *) name, &buf) == -1) {
		cp = name + 5;
		
		if (STREQ(cp, "stdin") && (flag & O_RDONLY) == O_RDONLY)
			openfd = fileno(stdin);
		else if (STREQ(cp, "stdout") && (flag & O_WRONLY) == O_WRONLY)
			openfd = fileno(stdout);
		else if (STREQ(cp, "stderr") && (flag & O_WRONLY) == O_WRONLY)
			openfd = fileno(stderr);
		else if (STREQN(cp, "fd/", 3)) {
			cp += 3;
			openfd = (int) strtod(cp, &ptr);
			if (openfd <= INVALID_HANDLE || ptr == cp)
				openfd = INVALID_HANDLE;
		}
	}

strictopen:
	if (openfd == INVALID_HANDLE)
		openfd = open(name, flag, 0666);
	if (openfd != INVALID_HANDLE && fstat(openfd, &buf) > 0) 
		if (S_ISDIR(buf.st_mode))
			fatal("file `%s' is a directory", name);
	return openfd;
}


/* spec_setup --- setup an IOBUF for a special internal file */

static void
spec_setup(iop, len, allocate)
IOBUF *iop;
int len;
int allocate;
{
	char *cp;

	if (allocate) {
		emalloc(cp, char *, len+2, "spec_setup");
		iop->buf = cp;
	} else {
		len = strlen(iop->buf);
		iop->buf[len++] = '\n';	/* get_a_record clobbered it */
		iop->buf[len] = '\0';	/* just in case */
	}
	iop->off = iop->buf;
	iop->cnt = 0;
	iop->secsiz = 0;
	iop->size = len;
	iop->end = iop->buf + len;
	iop->fd = -1;
	iop->flag = IOP_IS_INTERNAL;
}

/* specfdopen --- open an fd special file */

static int
specfdopen(iop, name, mode)
IOBUF *iop;
const char *name, *mode;
{
	int fd;
	IOBUF *tp;

	fd = devopen(name, mode);
	if (fd == INVALID_HANDLE)
		return INVALID_HANDLE;
	tp = iop_alloc(fd, name);
	if (tp == NULL) {
		/* don't leak fd's */
		close(fd);
		return INVALID_HANDLE;
	}
	*iop = *tp;
	iop->flag |= IOP_NO_FREE;
	free(tp);
	return 0;
}

/* the autoconf test for GETPGRP doesn't work for qnx because we don't have setpgrp */
#if defined(GETPGRP_VOID) || defined(__QNX__)
#define getpgrp_arg() /* nothing */
#else
#define getpgrp_arg() getpid()
#endif

/* pidopen --- "open" /dev/pid, /dev/ppid, and /dev/pgrpid */

static int
pidopen(iop, name, mode)
IOBUF *iop;
const char *name, *mode;
{
	char tbuf[BUFSIZ];
	int i;

	if (name[6] == 'g')
		sprintf(tbuf, "%d\n", getpgrp(getpgrp_arg()));
	else if (name[6] == 'i')
		sprintf(tbuf, "%d\n", getpid());
	else
		sprintf(tbuf, "%d\n", getppid());
	i = strlen(tbuf);
	spec_setup(iop, i, TRUE);
	strcpy(iop->buf, tbuf);
	return 0;
}

/* useropen --- "open" /dev/user */

/*
 * /dev/user creates a record as follows:
 *	$1 = getuid()
 *	$2 = geteuid()
 *	$3 = getgid()
 *	$4 = getegid()
 * If multiple groups are supported, then $5 through $NF are the
 * supplementary group set.
 */

static int
useropen(iop, name, mode)
IOBUF *iop;
const char *name, *mode;
{
	char tbuf[BUFSIZ], *cp;
	int i;
#if defined(NGROUPS_MAX) && NGROUPS_MAX > 0
	GETGROUPS_T groupset[NGROUPS_MAX];
	int ngroups;
#endif

	sprintf(tbuf, "%d %d %d %d", getuid(), geteuid(), getgid(), getegid());

	cp = tbuf + strlen(tbuf);
#if defined(NGROUPS_MAX) && NGROUPS_MAX > 0
	ngroups = getgroups(NGROUPS_MAX, groupset);
	if (ngroups == -1)
		fatal("could not find groups: %s", strerror(errno));

	for (i = 0; i < ngroups; i++) {
		*cp++ = ' ';
		sprintf(cp, "%d", (int) groupset[i]);
		cp += strlen(cp);
	}
#endif
	*cp++ = '\n';
	*cp++ = '\0';

	i = strlen(tbuf);
	spec_setup(iop, i, TRUE);
	strcpy(iop->buf, tbuf);
	return 0;
}

/* iop_open --- handle special and regular files for input */

static IOBUF *
iop_open(name, mode)
const char *name, *mode;
{
	int openfd = INVALID_HANDLE;
	int flag = 0;
	struct stat buf;
	IOBUF *iop;
	static struct internal {
		const char *name;
		int compare;
		int (*fp) P((IOBUF *, const char *, const char *));
		IOBUF iob;
	} table[] = {
		{ "/dev/fd/",		8,	specfdopen },
		{ "/dev/stdin",		10,	specfdopen },
		{ "/dev/stdout",	11,	specfdopen },
		{ "/dev/stderr",	11,	specfdopen },
		{ "/dev/pid",		8,	pidopen },
		{ "/dev/ppid",		9,	pidopen },
		{ "/dev/pgrpid",	11,	pidopen },
		{ "/dev/user",		9,	useropen },
	};
	int devcount = sizeof(table) / sizeof(table[0]);

	flag = str2mode(mode);

	if (STREQ(name, "-"))
		openfd = fileno(stdin);
	else if (do_traditional)
		goto strictopen;
	else if (STREQN(name, "/dev/", 5) && stat((char *) name, &buf) == -1) {
		int i;

		for (i = 0; i < devcount; i++) {
			if (STREQN(name, table[i].name, table[i].compare)) {
				iop = & table[i].iob;

				if (iop->buf != NULL) {
					spec_setup(iop, 0, FALSE);
					return iop;
				} else if ((*table[i].fp)(iop, name, mode) == 0)
					return iop;
				else {
					warning("could not open %s, mode `%s'",
						name, mode);
					return NULL;
				}
			}
		}
	}

strictopen:
	if (openfd == INVALID_HANDLE)
		openfd = open(name, flag, 0666);
	if (openfd != INVALID_HANDLE && fstat(openfd, &buf) > 0) 
		if ((buf.st_mode & S_IFMT) == S_IFDIR)
			fatal("file `%s' is a directory", name);
	iop = iop_alloc(openfd, name);
	return iop;
}

#ifndef PIPES_SIMULATED		/* real pipes */

/* wait_any --- wait for a child process, close associated pipe */

static int
wait_any(interesting)
int interesting;	/* pid of interest, if any */
{
	RETSIGTYPE (*hstat)(), (*istat)(), (*qstat)();
	int pid;
	int status = 0;
	struct redirect *redp;
	extern int errno;

	hstat = signal(SIGHUP, SIG_IGN);
	istat = signal(SIGINT, SIG_IGN);
	qstat = signal(SIGQUIT, SIG_IGN);
	for (;;) {
#ifdef HAVE_SYS_WAIT_H	/* Posix compatible sys/wait.h */
		pid = wait(&status);
#else
		pid = wait((union wait *)&status);
#endif /* NeXT */
		if (interesting && pid == interesting) {
			break;
		} else if (pid != -1) {
			for (redp = red_head; redp != NULL; redp = redp->next)
				if (pid == redp->pid) {
					redp->pid = -1;
					redp->status = status;
					if (redp->fp != NULL) {
						pclose(redp->fp);
						redp->fp = NULL;
					}
					if (redp->iop != NULL) {
						(void) iop_close(redp->iop);
						redp->iop = NULL;
					}
					break;
				}
		}
		if (pid == -1 && errno == ECHILD)
			break;
	}
	signal(SIGHUP, hstat);
	signal(SIGINT, istat);
	signal(SIGQUIT, qstat);
	return(status);
}

/* gawk_popen --- open an IOBUF on a child process */

static IOBUF *
gawk_popen(cmd, rp)
char *cmd;
struct redirect *rp;
{
	int p[2];
	register int pid;

	/*
	 * used to wait for any children to synchronize input and output,
	 * but this could cause gawk to hang when it is started in a pipeline
	 * and thus has a child process feeding it input (shell dependant)
	 */
	/*(void) wait_any(0);*/	/* wait for outstanding processes */

	if (pipe(p) < 0)
		fatal("cannot open pipe \"%s\" (%s)", cmd, strerror(errno));
	if ((pid = fork()) == 0) {
		if (close(1) == -1)
			fatal("close of stdout in child failed (%s)",
				strerror(errno));
		if (dup(p[1]) != 1)
			fatal("dup of pipe failed (%s)", strerror(errno));
		if (close(p[0]) == -1 || close(p[1]) == -1)
			fatal("close of pipe failed (%s)", strerror(errno));
		execl("/bin/sh", "sh", "-c", cmd, NULL);
		_exit(127);
	}
	if (pid == -1)
		fatal("cannot fork for \"%s\" (%s)", cmd, strerror(errno));
	rp->pid = pid;
	if (close(p[1]) == -1)
		fatal("close of pipe failed (%s)", strerror(errno));
	rp->iop = iop_alloc(p[0], cmd);
	if (rp->iop == NULL)
		(void) close(p[0]);
	return (rp->iop);
}

/* gawk_pclose --- close an open child pipe */

static int
gawk_pclose(rp)
struct redirect *rp;
{
	(void) iop_close(rp->iop);
	rp->iop = NULL;

	/* process previously found, return stored status */
	if (rp->pid == -1)
		return (rp->status >> 8) & 0xFF;
	rp->status = wait_any(rp->pid);
	rp->pid = -1;
	return (rp->status >> 8) & 0xFF;
}

#else	/* PIPES_SIMULATED */

/*
 * use temporary file rather than pipe
 * except if popen() provides real pipes too
 */

#if defined(VMS) || defined(OS2) || defined (MSDOS)

/* gawk_popen --- open an IOBUF on a child process */

static IOBUF *
gawk_popen(cmd, rp)
char *cmd;
struct redirect *rp;
{
	FILE *current;

	if ((current = popen(cmd, "r")) == NULL)
		return NULL;
	rp->iop = iop_alloc(fileno(current), cmd);
	if (rp->iop == NULL) {
		(void) fclose(current);
		current = NULL;
	}
	rp->ifp = current;
	return (rp->iop);
}

/* gawk_pclose --- close an open child pipe */

static int
gawk_pclose(rp)
struct redirect *rp;
{
	int rval, aval, fd = rp->iop->fd;

	rp->iop->fd = dup(fd);	  /* kludge to allow close() + pclose() */
	rval = iop_close(rp->iop);
	rp->iop = NULL;
	aval = pclose(rp->ifp);
	rp->ifp = NULL;
	return (rval < 0 ? rval : aval);
}
#else	/* not (VMS || OS2 || MSDOS) */

static struct pipeinfo {
	char *command;
	char *name;
} pipes[_NFILE];

/* gawk_popen --- open an IOBUF on a child process */

static IOBUF *
gawk_popen(cmd, rp)
char *cmd;
struct redirect *rp;
{
	extern char *strdup P((const char *));
	int current;
	char *name;
	static char cmdbuf[256];

	/* get a name to use */
	if ((name = tempnam(".", "pip")) == NULL)
		return NULL;
	sprintf(cmdbuf, "%s > %s", cmd, name);
	system(cmdbuf);
	if ((current = open(name, O_RDONLY)) == INVALID_HANDLE)
		return NULL;
	pipes[current].name = name;
	pipes[current].command = strdup(cmd);
	rp->iop = iop_alloc(current, name);
	if (rp->iop == NULL)
		(void) close(current);
	return (rp->iop);
}

/* gawk_pclose --- close an open child pipe */

static int
gawk_pclose(rp)
struct redirect *rp;
{
	int cur = rp->iop->fd;
	int rval;

	rval = iop_close(rp->iop);
	rp->iop = NULL;

	/* check for an open file  */
	if (pipes[cur].name == NULL)
		return -1;
	unlink(pipes[cur].name);
	free(pipes[cur].name);
	pipes[cur].name = NULL;
	free(pipes[cur].command);
	return rval;
}
#endif	/* not (VMS || OS2 || MSDOS) */

#endif	/* PIPES_SIMULATED */

/* do_getline --- read in a line, into var and with redirection, as needed */

NODE *
do_getline(tree)
NODE *tree;
{
	struct redirect *rp = NULL;
	IOBUF *iop;
	int cnt = EOF;
	char *s = NULL;
	int errcode;

	while (cnt == EOF) {
		if (tree->rnode == NULL) {	 /* no redirection */
			iop = nextfile(FALSE);
			if (iop == NULL)		/* end of input */
				return tmp_number((AWKNUM) 0.0);
		} else {
			int redir_error = 0;

			rp = redirect(tree->rnode, &redir_error);
			if (rp == NULL && redir_error) { /* failed redirect */
				if (! do_traditional) {
					s = strerror(redir_error);

					unref(ERRNO_node->var_value);
					ERRNO_node->var_value =
						make_string(s, strlen(s));
				}
				return tmp_number((AWKNUM) -1.0);
			}
			iop = rp->iop;
			if (iop == NULL)		/* end of input */
				return tmp_number((AWKNUM) 0.0);
		}
		errcode = 0;
		cnt = get_a_record(&s, iop, RS->stptr[0], RS_regexp, &errcode);
		if (errcode != 0) {
			if (! do_traditional) {
				s = strerror(errcode);

				unref(ERRNO_node->var_value);
				ERRNO_node->var_value = make_string(s, strlen(s));
			}
			return tmp_number((AWKNUM) -1.0);
		}
		if (cnt == EOF) {
			if (rp != NULL) {
				/*
				 * Don't do iop_close() here if we are
				 * reading from a pipe; otherwise
				 * gawk_pclose will not be called.
				 */
				if ((rp->flag & RED_PIPE) == 0) {
					(void) iop_close(iop);
					rp->iop = NULL;
				}
				rp->flag |= RED_EOF;	/* sticky EOF */
				return tmp_number((AWKNUM) 0.0);
			} else
				continue;	/* try another file */
		}
		if (rp == NULL) {
			NR++;
			FNR++;
		}
		if (tree->lnode == NULL)	/* no optional var. */
			set_record(s, cnt, TRUE);
		else {			/* assignment to variable */
			Func_ptr after_assign = NULL;
			NODE **lhs;

			lhs = get_lhs(tree->lnode, &after_assign);
			unref(*lhs);
			*lhs = make_string(s, strlen(s));
			(*lhs)->flags |= MAYBE_NUM;
			/* we may have to regenerate $0 here! */
			if (after_assign != NULL)
				(*after_assign)();
		}
	}
	return tmp_number((AWKNUM) 1.0);
}

/* pathopen --- pathopen with default file extension handling */

int
pathopen(file)
const char *file;
{
	int fd = do_pathopen(file);

#ifdef DEFAULT_FILETYPE
	if (! do_traditional && fd <= INVALID_HANDLE) {
		char *file_awk;
		int save = errno;
#ifdef VMS
		int vms_save = vaxc$errno;
#endif

		/* append ".awk" and try again */
		emalloc(file_awk, char *, strlen(file) +
			sizeof(DEFAULT_FILETYPE) + 1, "pathopen");
		sprintf(file_awk, "%s%s", file, DEFAULT_FILETYPE);
		fd = do_pathopen(file_awk);
		free(file_awk);
		if (fd <= INVALID_HANDLE) {
			errno = save;
#ifdef VMS
			vaxc$errno = vms_save;
#endif
		}
	}
#endif	/*DEFAULT_FILETYPE*/

	return fd;
}

/* do_pathopen --- search $AWKPATH for source file */

static int
do_pathopen(file)
const char *file;
{
	static const char *savepath = NULL;
	static int first = TRUE;
	const char *awkpath;
	char *cp, trypath[BUFSIZ];
	int fd;

	if (STREQ(file, "-"))
		return (0);

	if (do_traditional)
		return (devopen(file, "r"));

	if (first) {
		first = FALSE;
		if ((awkpath = getenv("AWKPATH")) != NULL && *awkpath)
			savepath = awkpath;	/* used for restarting */
		else
			savepath = defpath;
	}
	awkpath = savepath;

	/* some kind of path name, no search */
	if (ispath(file))
		return (devopen(file, "r"));

	do {
		trypath[0] = '\0';
		/* this should take into account limits on size of trypath */
		for (cp = trypath; *awkpath && *awkpath != envsep; )
			*cp++ = *awkpath++;

		if (cp != trypath) {	/* nun-null element in path */
			/* add directory punctuation only if needed */
			if (! isdirpunct(*(cp-1)))
				*cp++ = '/';
			/* append filename */
			strcpy(cp, file);
		} else
			strcpy(trypath, file);
		if ((fd = devopen(trypath, "r")) > INVALID_HANDLE)
			return (fd);

		/* no luck, keep going */
		if(*awkpath == envsep && awkpath[1] != '\0')
			awkpath++;	/* skip colon */
	} while (*awkpath != '\0');
	/*
	 * You might have one of the awk paths defined, WITHOUT the current
	 * working directory in it. Therefore try to open the file in the
	 * current directory.
	 */
	return (devopen(file, "r"));
}

#ifdef TEST
int bufsize = 8192;

void
fatal(s)
char *s;
{
	printf("%s\n", s);
	exit(1);
}
#endif

/* iop_alloc --- allocate an IOBUF structure for an open fd */

IOBUF *
iop_alloc(fd, name)
int fd;
const char *name;
{
	IOBUF *iop;
	struct stat sbuf;

	if (fd == INVALID_HANDLE)
		return NULL;
	emalloc(iop, IOBUF *, sizeof(IOBUF), "iop_alloc");
	iop->flag = 0;
	if (isatty(fd))
		iop->flag |= IOP_IS_TTY;
	iop->size = optimal_bufsize(fd, & sbuf);
	if (do_lint && S_ISREG(sbuf.st_mode) && sbuf.st_size == 0)
		warning("data file `%s' is empty", name);
	iop->secsiz = -2;
	errno = 0;
	iop->fd = fd;
	iop->off = iop->buf = NULL;
	iop->cnt = 0;
	iop->name = name;
	return iop;
}

/*
 * get_a_record:
 * Get the next record.  Uses a "split buffer" where the latter part is
 * the normal read buffer and the head part is an "overflow" area that is used
 * when a record spans the end of the normal buffer, in which case the first
 * part of the record is copied into the overflow area just before the
 * normal buffer.  Thus, the eventual full record can be returned as a
 * contiguous area of memory with a minimum of copying.  The overflow area
 * is expanded as needed, so that records are unlimited in length.
 * We also mark both the end of the buffer and the end of the read() with
 * a sentinel character (the current record separator) so that the inside
 * loop can run as a single test.
 *
 * Note that since we know or can compute the end of the read and the end
 * of the buffer, the sentinel character does not get in the way of regexp
 * based searching, since we simply search up to that character, but not
 * including it.
 */

static int
get_a_record(out, iop, grRS, RSre, errcode)
char **out;		/* pointer to pointer to data */
IOBUF *iop;		/* input IOP */
register int grRS;	/* first char in RS->stptr */
Regexp *RSre;		/* regexp for RS */
int *errcode;		/* pointer to error variable */
{
	register char *bp = iop->off;
	char *bufend;
	char *start = iop->off;			/* beginning of record */
	char rs;
	static Regexp *RS_null_re = NULL;
	Regexp *rsre = NULL;
	int continuing = FALSE;		/* used for re matching */
	int onecase;

#define set_RT_to_null() \
	(void)(! do_traditional && (unref(RT_node->var_value), \
			   RT_node->var_value = Nnull_string))

#define set_RT(str, len) \
	(void)(! do_traditional && (unref(RT_node->var_value), \
			   RT_node->var_value = make_string(str, len)))

	/* first time through */
	if (RS_null_re == NULL) {
		RS_null_re = make_regexp("\n\n+", 3, TRUE, TRUE);
		if (RS_null_re == NULL)
			fatal("internal error: file `%s', line %d\n",
				__FILE__, __LINE__);
	}

	if (iop->cnt == EOF) {	/* previous read hit EOF */
		*out = NULL;
		set_RT_to_null();
		return EOF;
	}

	if (grRS == FALSE)	/* special case:  RS == "" */
		rs = '\n';
	else
		rs = (char) grRS;

	onecase = (IGNORECASE && isalpha(rs));
	if (onecase)
		rs = casetable[rs];

	/* set up sentinel */
	if (iop->buf) {
		bufend = iop->buf + iop->size + iop->secsiz;
		*bufend = rs;		/* add sentinel to buffer */
	} else
		bufend = NULL;

	for (;;) {	/* break on end of record, read error or EOF */
/* buffer mgmt, chunk #1 */
		/*
		 * Following code is entered on the first call of this routine
		 * for a new iop, or when we scan to the end of the buffer.
		 * In the latter case, we copy the current partial record to
		 * the space preceding the normal read buffer.  If necessary,
		 * we expand this space.  This is done so that we can return
		 * the record as a contiguous area of memory.
		 */
		if ((iop->flag & IOP_IS_INTERNAL) == 0 && bp >= bufend) {
			char *oldbuf = NULL;
			char *oldsplit = iop->buf + iop->secsiz;
			long len;	/* record length so far */

			len = bp - start;
			if (len > iop->secsiz) {
				/* expand secondary buffer */
				if (iop->secsiz == -2)
					iop->secsiz = 256;
				while (len > iop->secsiz)
					iop->secsiz *= 2;
				oldbuf = iop->buf;
				emalloc(iop->buf, char *,
				    iop->size+iop->secsiz+2, "get_a_record");
				bufend = iop->buf + iop->size + iop->secsiz;
				*bufend = rs;
			}
			if (len > 0) {
				char *newsplit = iop->buf + iop->secsiz;

				if (start < oldsplit) {
					memcpy(newsplit - len, start,
							oldsplit - start);
					memcpy(newsplit - (bp - oldsplit),
							oldsplit, bp - oldsplit);
				} else
					memcpy(newsplit - len, start, len);
			}
			bp = iop->end = iop->off = iop->buf + iop->secsiz;
			start = bp - len;
			if (oldbuf != NULL) {
				free(oldbuf);
				oldbuf = NULL;
			}
		}
/* buffer mgmt, chunk #2 */
		/*
		 * Following code is entered whenever we have no more data to
		 * scan.  In most cases this will read into the beginning of
		 * the main buffer, but in some cases (terminal, pipe etc.)
		 * we may be doing smallish reads into more advanced positions.
		 */
		if (bp >= iop->end) {
			if ((iop->flag & IOP_IS_INTERNAL) != 0) {
				iop->cnt = EOF;
				break;
			}
			iop->cnt = read(iop->fd, iop->end, bufend - iop->end);
			if (iop->cnt == -1) {
				if (! do_traditional && errcode != NULL) {
					*errcode = errno;
					iop->cnt = EOF;
					break;
				} else
					fatal("error reading input file `%s': %s",
						iop->name, strerror(errno));
			} else if (iop->cnt == 0) {
				/*
				 * hit EOF before matching RS, so end
				 * the record and set RT to ""
				 */
				iop->cnt = EOF;
				/* see comments below about this test */
				if (! continuing) {
					set_RT_to_null();
					break;
				}
			}
			if (iop->cnt != EOF) {
				iop->end += iop->cnt;
				*iop->end = rs;		/* reset the sentinel */
			}
		}
/* buffers are now setup and filled with data */
/* search for RS, #1, regexp based, or RS = "" */
		/*
		 * Attempt to simplify the code a bit. The case where
		 * RS = "" can also be described by a regexp, RS = "\n\n+".
		 * The buffer managment and searching code can thus now
		 * use a common case (the one for regexps) both when RS is
		 * a regexp, and when RS = "". This particularly benefits
		 * us for keeping track of how many newlines were matched
		 * in order to set RT.
		 */
		if (! do_traditional && RSre != NULL)	/* regexp */
			rsre = RSre;
		else if (grRS == FALSE)		/* RS = "" */
			rsre = RS_null_re;
		else
			rsre = NULL;

		/*
		 * Look for regexp match of RS.  Non-match conditions are:
		 *	1. No match at all
		 *	2. Match of a null string
		 *	3. Match ends at exact end of buffer
		 * Number 3 is subtle; we have to add more to the buffer
		 * in case the match would have extended further into the
		 * file, since regexp match by definition always matches the
		 * longest possible match.
		 *
		 * It is even more subtle than you might think. Suppose
		 * the re matches at exactly the end of file. We don't know
		 * that until we try to add more to the buffer. Thus, we
		 * set a flag to indicate, that if eof really does happen,
		 * don't break early.
		 */
		continuing = FALSE;
		if (rsre != NULL) {
		again:
			/* cases 1 and 2 are simple, just keep going */
			if (research(rsre, start, 0, iop->end - start, TRUE) == -1
			    || RESTART(rsre, start) == REEND(rsre, start)) {
				bp = iop->end;
				continue;
			}
			/* case 3, regex match at exact end */
			if (start + REEND(rsre, start) >= iop->end) {
				if (iop->cnt != EOF) {
					bp = iop->end;
					continuing = TRUE;
					continue;
				}
			}
			/* got a match! */
			/*
			 * Leading newlines at the beginning of the file
			 * should be ignored. Whew!
			 */
			if (grRS == FALSE && RESTART(rsre, start) == 0) {
				start += REEND(rsre, start);
				goto again;
			}
			bp = start + RESTART(rsre, start);
			set_RT(bp, REEND(rsre, start) - RESTART(rsre, start));
			*bp = '\0';
			iop->off = start + REEND(rsre, start);
			break;
		}
/* search for RS, #2, RS = <single char> */
		if (onecase) {
			while (casetable[*bp++] != rs)
				continue;
		} else {
			while (*bp++ != rs)
				continue;
		}
		set_RT(bp - 1, 1);

		if (bp <= iop->end)
			break;
		else
			bp--;

		if ((iop->flag & IOP_IS_INTERNAL) != 0)
			iop->cnt = bp - start;
	}
	if (iop->cnt == EOF
	    && (((iop->flag & IOP_IS_INTERNAL) != 0) || start == bp)) {
		*out = NULL;
		set_RT_to_null();
		return EOF;
	}

	if (do_traditional || rsre == NULL) {
		char *bstart;

		bstart = iop->off = bp;
		bp--;
		if (onecase ? casetable[*bp] != rs : *bp != rs) {
			bp++;
			bstart = bp;
		}
		*bp = '\0';
	} else if (grRS == FALSE && iop->cnt == EOF) {
		/*
		 * special case, delete trailing newlines,
		 * should never be more than one.
		 */
		while (bp[-1] == '\n')
			bp--;
		*bp = '\0';
	}

	*out = start;
	return bp - start;
}

#ifdef TEST
int
main(argc, argv)
int argc;
char *argv[];
{
	IOBUF *iop;
	char *out;
	int cnt;
	char rs[2];

	rs[0] = '\0';
	if (argc > 1)
		bufsize = atoi(argv[1]);
	if (argc > 2)
		rs[0] = *argv[2];
	iop = iop_alloc(0, "stdin");
	while ((cnt = get_a_record(&out, iop, rs[0], NULL, NULL)) > 0) {
		fwrite(out, 1, cnt, stdout);
		fwrite(rs, 1, 1, stdout);
	}
	return 0;
}
#endif

/* set_RS --- update things as appropriate when RS is set */

void
set_RS()
{
	static NODE *save_rs = NULL;

	if (save_rs && cmp_nodes(RS_node->var_value, save_rs) == 0)
		return;
	unref(save_rs);
	save_rs = dupnode(RS_node->var_value);
	RS_is_null = FALSE;
	RS = force_string(RS_node->var_value);
	if (RS_regexp != NULL) {
		refree(RS_regexp);
		RS_regexp = NULL;
	}
	if (RS->stlen == 0)
		RS_is_null = TRUE;
	else if (RS->stlen > 1)
		RS_regexp = make_regexp(RS->stptr, RS->stlen, IGNORECASE, TRUE);

	set_FS_if_not_FIELDWIDTHS();
}
