[BACK]Return to ex_argv.c CVS log [TXT][DIR] Up to [local] / src / usr.bin / vi / ex

File: [local] / src / usr.bin / vi / ex / ex_argv.c (download)

Revision 1.13, Tue Oct 27 23:59:47 2009 UTC (14 years, 7 months ago) by deraadt
Branch: MAIN
CVS Tags: OPENBSD_5_6_BASE, OPENBSD_5_6, OPENBSD_5_5_BASE, OPENBSD_5_5, OPENBSD_5_4_BASE, OPENBSD_5_4, OPENBSD_5_3_BASE, OPENBSD_5_3, OPENBSD_5_2_BASE, OPENBSD_5_2, OPENBSD_5_1_BASE, OPENBSD_5_1, OPENBSD_5_0_BASE, OPENBSD_5_0, OPENBSD_4_9_BASE, OPENBSD_4_9, OPENBSD_4_8_BASE, OPENBSD_4_8, OPENBSD_4_7_BASE, OPENBSD_4_7
Changes since 1.12: +1 -5 lines

rcsid[] and sccsid[] and copyright[] are essentially unmaintained (and
unmaintainable).  these days, people use source.  these id's do not provide
any benefit, and do hurt the small install media
(the 33,000 line diff is essentially mechanical)
ok with the idea millert, ok dms

/*	$OpenBSD: ex_argv.c,v 1.13 2009/10/27 23:59:47 deraadt Exp $	*/

/*-
 * Copyright (c) 1993, 1994
 *	The Regents of the University of California.  All rights reserved.
 * Copyright (c) 1993, 1994, 1995, 1996
 *	Keith Bostic.  All rights reserved.
 *
 * See the LICENSE file for redistribution information.
 */

#include "config.h"

#include <sys/types.h>
#include <sys/queue.h>

#include <bitstring.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "../common/common.h"

static int argv_alloc(SCR *, size_t);
static int argv_comp(const void *, const void *);
static int argv_fexp(SCR *, EXCMD *,
	char *, size_t, char *, size_t *, char **, size_t *, int);
static int argv_lexp(SCR *, EXCMD *, char *);
static int argv_sexp(SCR *, char **, size_t *, size_t *);

/*
 * argv_init --
 *	Build  a prototype arguments list.
 *
 * PUBLIC: int argv_init(SCR *, EXCMD *);
 */
int
argv_init(sp, excp)
	SCR *sp;
	EXCMD *excp;
{
	EX_PRIVATE *exp;

	exp = EXP(sp);
	exp->argsoff = 0;
	argv_alloc(sp, 1);

	excp->argv = exp->args;
	excp->argc = exp->argsoff;
	return (0);
}

/*
 * argv_exp0 --
 *	Append a string to the argument list.
 *
 * PUBLIC: int argv_exp0(SCR *, EXCMD *, char *, size_t);
 */
int
argv_exp0(sp, excp, cmd, cmdlen)
	SCR *sp;
	EXCMD *excp;
	char *cmd;
	size_t cmdlen;
{
	EX_PRIVATE *exp;

	exp = EXP(sp);
	argv_alloc(sp, cmdlen);
	memcpy(exp->args[exp->argsoff]->bp, cmd, cmdlen);
	exp->args[exp->argsoff]->bp[cmdlen] = '\0';
	exp->args[exp->argsoff]->len = cmdlen;
	++exp->argsoff;
	excp->argv = exp->args;
	excp->argc = exp->argsoff;
	return (0);
}

/*
 * argv_exp1 --
 *	Do file name expansion on a string, and append it to the
 *	argument list.
 *
 * PUBLIC: int argv_exp1(SCR *, EXCMD *, char *, size_t, int);
 */
int
argv_exp1(sp, excp, cmd, cmdlen, is_bang)
	SCR *sp;
	EXCMD *excp;
	char *cmd;
	size_t cmdlen;
	int is_bang;
{
	size_t blen, len;
	char *bp, *p, *t;

	GET_SPACE_RET(sp, bp, blen, 512);

	len = 0;
	if (argv_fexp(sp, excp, cmd, cmdlen, bp, &len, &bp, &blen, is_bang)) {
		FREE_SPACE(sp, bp, blen);
		return (1);
	}

	/* If it's empty, we're done. */
	if (len != 0) {
		for (p = bp, t = bp + len; p < t; ++p)
			if (!isblank(*p))
				break;
		if (p == t)
			goto ret;
	} else
		goto ret;

	(void)argv_exp0(sp, excp, bp, len);

ret:	FREE_SPACE(sp, bp, blen);
	return (0);
}

/*
 * argv_exp2 --
 *	Do file name and shell expansion on a string, and append it to
 *	the argument list.
 *
 * PUBLIC: int argv_exp2(SCR *, EXCMD *, char *, size_t);
 */
int
argv_exp2(sp, excp, cmd, cmdlen)
	SCR *sp;
	EXCMD *excp;
	char *cmd;
	size_t cmdlen;
{
	size_t blen, len, n;
	int rval;
	char *bp, *mp, *p;

	GET_SPACE_RET(sp, bp, blen, 512);

#define	SHELLECHO	"echo "
#define	SHELLOFFSET	(sizeof(SHELLECHO) - 1)
	memcpy(bp, SHELLECHO, SHELLOFFSET);
	p = bp + SHELLOFFSET;
	len = SHELLOFFSET;

#if defined(DEBUG) && 0
	TRACE(sp, "file_argv: {%.*s}\n", (int)cmdlen, cmd);
#endif

	if (argv_fexp(sp, excp, cmd, cmdlen, p, &len, &bp, &blen, 0)) {
		rval = 1;
		goto err;
	}

#if defined(DEBUG) && 0
	TRACE(sp, "before shell: %d: {%s}\n", len, bp);
#endif

	/*
	 * Do shell word expansion -- it's very, very hard to figure out what
	 * magic characters the user's shell expects.  Historically, it was a
	 * union of v7 shell and csh meta characters.  We match that practice
	 * by default, so ":read \%" tries to read a file named '%'.  It would
	 * make more sense to pass any special characters through the shell,
	 * but then, if your shell was csh, the above example will behave
	 * differently in nvi than in vi.  If you want to get other characters
	 * passed through to your shell, change the "meta" option.
	 *
	 * To avoid a function call per character, we do a first pass through
	 * the meta characters looking for characters that aren't expected
	 * to be there, and then we can ignore them in the user's argument.
	 */
	if (opts_empty(sp, O_SHELL, 1) || opts_empty(sp, O_SHELLMETA, 1))
		n = 0;
	else {
		for (p = mp = O_STR(sp, O_SHELLMETA); *p != '\0'; ++p)
			if (isblank(*p) || isalnum(*p))
				break;
		p = bp + SHELLOFFSET;
		n = len - SHELLOFFSET;
		if (*p != '\0') {
			for (; n > 0; --n, ++p)
				if (strchr(mp, *p) != NULL)
					break;
		} else
			for (; n > 0; --n, ++p)
				if (!isblank(*p) &&
				    !isalnum(*p) && strchr(mp, *p) != NULL)
					break;
	}

	/*
	 * If we found a meta character in the string, fork a shell to expand
	 * it.  Unfortunately, this is comparatively slow.  Historically, it
	 * didn't matter much, since users don't enter meta characters as part
	 * of pathnames that frequently.  The addition of filename completion
	 * broke that assumption because it's easy to use.  As a result, lots
	 * folks have complained that the expansion code is too slow.  So, we
	 * detect filename completion as a special case, and do it internally.
	 * Note that this code assumes that the <asterisk> character is the
	 * match-anything meta character.  That feels safe -- if anyone writes
	 * a shell that doesn't follow that convention, I'd suggest giving them
	 * a festive hot-lead enema.
	 */
	switch (n) {
	case 0:
		p = bp + SHELLOFFSET;
		len -= SHELLOFFSET;
		rval = argv_exp3(sp, excp, p, len);
		break;
	case 1:
		if (*p == '*') {
			*p = '\0';
			rval = argv_lexp(sp, excp, bp + SHELLOFFSET);
			break;
		}
		/* FALLTHROUGH */
	default:
		if (argv_sexp(sp, &bp, &blen, &len)) {
			rval = 1;
			goto err;
		}
		p = bp;
		rval = argv_exp3(sp, excp, p, len);
		break;
	}

err:	FREE_SPACE(sp, bp, blen);
	return (rval);
}

/*
 * argv_exp3 --
 *	Take a string and break it up into an argv, which is appended
 *	to the argument list.
 *
 * PUBLIC: int argv_exp3(SCR *, EXCMD *, char *, size_t);
 */
int
argv_exp3(sp, excp, cmd, cmdlen)
	SCR *sp;
	EXCMD *excp;
	char *cmd;
	size_t cmdlen;
{
	EX_PRIVATE *exp;
	size_t len;
	int ch, off;
	char *ap, *p;

	for (exp = EXP(sp); cmdlen > 0; ++exp->argsoff) {
		/* Skip any leading whitespace. */
		for (; cmdlen > 0; --cmdlen, ++cmd) {
			ch = *cmd;
			if (!isblank(ch))
				break;
		}
		if (cmdlen == 0)
			break;

		/*
		 * Determine the length of this whitespace delimited
		 * argument.
		 *
		 * QUOTING NOTE:
		 *
		 * Skip any character preceded by the user's quoting
		 * character.
		 */
		for (ap = cmd, len = 0; cmdlen > 0; ++cmd, --cmdlen, ++len) {
			ch = *cmd;
			if (IS_ESCAPE(sp, excp, ch) && cmdlen > 1) {
				++cmd;
				--cmdlen;
			} else if (isblank(ch))
				break;
		}

		/*
		 * Copy the argument into place.
		 *
		 * QUOTING NOTE:
		 *
		 * Lose quote chars.
		 */
		argv_alloc(sp, len);
		off = exp->argsoff;
		exp->args[off]->len = len;
		for (p = exp->args[off]->bp; len > 0; --len, *p++ = *ap++)
			if (IS_ESCAPE(sp, excp, *ap))
				++ap;
		*p = '\0';
	}
	excp->argv = exp->args;
	excp->argc = exp->argsoff;

#if defined(DEBUG) && 0
	for (cnt = 0; cnt < exp->argsoff; ++cnt)
		TRACE(sp, "arg %d: {%s}\n", cnt, exp->argv[cnt]);
#endif
	return (0);
}

/*
 * argv_fexp --
 *	Do file name and bang command expansion.
 */
static int
argv_fexp(sp, excp, cmd, cmdlen, p, lenp, bpp, blenp, is_bang)
	SCR *sp;
	EXCMD *excp;
	char *cmd, *p, **bpp;
	size_t cmdlen, *lenp, *blenp;
	int is_bang;
{
	EX_PRIVATE *exp;
	char *bp, *t;
	size_t blen, len, off, tlen;

	/* Replace file name characters. */
	for (bp = *bpp, blen = *blenp, len = *lenp; cmdlen > 0; --cmdlen, ++cmd)
		switch (*cmd) {
		case '!':
			if (!is_bang)
				goto ins_ch;
			exp = EXP(sp);
			if (exp->lastbcomm == NULL) {
				msgq(sp, M_ERR,
				    "115|No previous command to replace \"!\"");
				return (1);
			}
			len += tlen = strlen(exp->lastbcomm);
			off = p - bp;
			ADD_SPACE_RET(sp, bp, blen, len);
			p = bp + off;
			memcpy(p, exp->lastbcomm, tlen);
			p += tlen;
			F_SET(excp, E_MODIFY);
			break;
		case '%':
			if ((t = sp->frp->name) == NULL) {
				msgq(sp, M_ERR,
				    "116|No filename to substitute for %%");
				return (1);
			}
			tlen = strlen(t);
			len += tlen;
			off = p - bp;
			ADD_SPACE_RET(sp, bp, blen, len);
			p = bp + off;
			memcpy(p, t, tlen);
			p += tlen;
			F_SET(excp, E_MODIFY);
			break;
		case '#':
			if ((t = sp->alt_name) == NULL) {
				msgq(sp, M_ERR,
				    "117|No filename to substitute for #");
				return (1);
			}
			len += tlen = strlen(t);
			off = p - bp;
			ADD_SPACE_RET(sp, bp, blen, len);
			p = bp + off;
			memcpy(p, t, tlen);
			p += tlen;
			F_SET(excp, E_MODIFY);
			break;
		case '\\':
			/*
			 * QUOTING NOTE:
			 *
			 * Strip any backslashes that protected the file
			 * expansion characters.
			 */
			if (cmdlen > 1 &&
			    (cmd[1] == '%' || cmd[1] == '#' || cmd[1] == '!')) {
				++cmd;
				--cmdlen;
			}
			/* FALLTHROUGH */
		default:
ins_ch:			++len;
			off = p - bp;
			ADD_SPACE_RET(sp, bp, blen, len);
			p = bp + off;
			*p++ = *cmd;
		}

	/* Nul termination. */
	++len;
	off = p - bp;
	ADD_SPACE_RET(sp, bp, blen, len);
	p = bp + off;
	*p = '\0';

	/* Return the new string length, buffer, buffer length. */
	*lenp = len - 1;
	*bpp = bp;
	*blenp = blen;
	return (0);
}

/*
 * argv_alloc --
 *	Make more space for arguments.
 */
static int
argv_alloc(sp, len)
	SCR *sp;
	size_t len;
{
	ARGS *ap;
	EX_PRIVATE *exp;
	int cnt, off;

	/*
	 * Allocate room for another argument, always leaving
	 * enough room for an ARGS structure with a length of 0.
	 */
#define	INCREMENT	20
	exp = EXP(sp);
	off = exp->argsoff;
	if (exp->argscnt == 0 || off + 2 >= exp->argscnt - 1) {
		cnt = exp->argscnt + INCREMENT;
		REALLOC(sp, exp->args, ARGS **, cnt * sizeof(ARGS *));
		if (exp->args == NULL) {
			(void)argv_free(sp);
			goto mem;
		}
		memset(&exp->args[exp->argscnt], 0, INCREMENT * sizeof(ARGS *));
		exp->argscnt = cnt;
	}

	/* First argument. */
	if (exp->args[off] == NULL) {
		CALLOC(sp, exp->args[off], ARGS *, 1, sizeof(ARGS));
		if (exp->args[off] == NULL)
			goto mem;
	}

	/* First argument buffer. */
	ap = exp->args[off];
	ap->len = 0;
	if (ap->blen < len + 1) {
		ap->blen = len + 1;
		REALLOC(sp, ap->bp, CHAR_T *, ap->blen * sizeof(CHAR_T));
		if (ap->bp == NULL) {
			ap->bp = NULL;
			ap->blen = 0;
			F_CLR(ap, A_ALLOCATED);
mem:			msgq(sp, M_SYSERR, NULL);
			return (1);
		}
		F_SET(ap, A_ALLOCATED);
	}

	/* Second argument. */
	if (exp->args[++off] == NULL) {
		CALLOC(sp, exp->args[off], ARGS *, 1, sizeof(ARGS));
		if (exp->args[off] == NULL)
			goto mem;
	}
	/* 0 length serves as end-of-argument marker. */
	exp->args[off]->len = 0;
	return (0);
}

/*
 * argv_free --
 *	Free up argument structures.
 *
 * PUBLIC: int argv_free(SCR *);
 */
int
argv_free(sp)
	SCR *sp;
{
	EX_PRIVATE *exp;
	int off;

	exp = EXP(sp);
	if (exp->args != NULL) {
		for (off = 0; off < exp->argscnt; ++off) {
			if (exp->args[off] == NULL)
				continue;
			if (F_ISSET(exp->args[off], A_ALLOCATED))
				free(exp->args[off]->bp);
			free(exp->args[off]);
		}
		free(exp->args);
	}
	exp->args = NULL;
	exp->argscnt = 0;
	exp->argsoff = 0;
	return (0);
}

/*
 * argv_lexp --
 *	Find all file names matching the prefix and append them to the
 *	buffer.
 */
static int
argv_lexp(sp, excp, path)
	SCR *sp;
	EXCMD *excp;
	char *path;
{
	struct dirent *dp;
	DIR *dirp;
	EX_PRIVATE *exp;
	int off;
	size_t dlen, len, nlen;
	char *dname, *name, *p;

	exp = EXP(sp);

	/* Set up the name and length for comparison. */
	if ((p = strrchr(path, '/')) == NULL) {
		dname = ".";
		dlen = 0;
		name = path;
	} else { 
		if (p == path) {
			dname = "/";
			dlen = 1;
		} else {
			*p = '\0';
			dname = path;
			dlen = strlen(path);
		}
		name = p + 1;
	}
	nlen = strlen(name);

	/*
	 * XXX
	 * We don't use the d_namlen field, it's not portable enough; we
	 * assume that d_name is nul terminated, instead.
	 */
	if ((dirp = opendir(dname)) == NULL) {
		msgq_str(sp, M_SYSERR, dname, "%s");
		return (1);
	}
	for (off = exp->argsoff; (dp = readdir(dirp)) != NULL;) {
		if (nlen == 0) {
			if (dp->d_name[0] == '.')
				continue;
			len = strlen(dp->d_name);
		} else {
			len = strlen(dp->d_name);
			if (len < nlen || memcmp(dp->d_name, name, nlen))
				continue;
		}

		/* Directory + name + slash + null. */
		argv_alloc(sp, dlen + len + 2);
		p = exp->args[exp->argsoff]->bp;
		if (dlen != 0) {
			memcpy(p, dname, dlen);
			p += dlen;
			if (dlen > 1 || dname[0] != '/')
				*p++ = '/';
		}
		memcpy(p, dp->d_name, len + 1);
		exp->args[exp->argsoff]->len = dlen + len + 1;
		++exp->argsoff;
		excp->argv = exp->args;
		excp->argc = exp->argsoff;
	}
	closedir(dirp);

	if (off == exp->argsoff) {
		/*
		 * If we didn't find a match, complain that the expansion
		 * failed.  We can't know for certain that's the error, but
		 * it's a good guess, and it matches historic practice. 
		 */
		msgq(sp, M_ERR, "304|Shell expansion failed");
		return (1);
	}
	qsort(exp->args + off, exp->argsoff - off, sizeof(ARGS *), argv_comp);
	return (0);
}

/*
 * argv_comp --
 *	Alphabetic comparison.
 */
static int
argv_comp(a, b)
	const void *a, *b;
{
	return (strcmp((char *)(*(ARGS **)a)->bp, (char *)(*(ARGS **)b)->bp));
}

/*
 * argv_sexp --
 *	Fork a shell, pipe a command through it, and read the output into
 *	a buffer.
 */
static int
argv_sexp(sp, bpp, blenp, lenp)
	SCR *sp;
	char **bpp;
	size_t *blenp, *lenp;
{
	enum { SEXP_ERR, SEXP_EXPANSION_ERR, SEXP_OK } rval;
	FILE *ifp;
	pid_t pid;
	size_t blen, len;
	int ch, std_output[2];
	char *bp, *p, *sh, *sh_path;

	/* Secure means no shell access. */
	if (O_ISSET(sp, O_SECURE)) {
		msgq(sp, M_ERR,
"289|Shell expansions not supported when the secure edit option is set");
		return (1);
	}

	sh_path = O_STR(sp, O_SHELL);
	if ((sh = strrchr(sh_path, '/')) == NULL)
		sh = sh_path;
	else
		++sh;

	/* Local copies of the buffer variables. */
	bp = *bpp;
	blen = *blenp;

	/*
	 * There are two different processes running through this code, named
	 * the utility (the shell) and the parent. The utility reads standard
	 * input and writes standard output and standard error output.  The
	 * parent writes to the utility, reads its standard output and ignores
	 * its standard error output.  Historically, the standard error output
	 * was discarded by vi, as it produces a lot of noise when file patterns
	 * don't match.
	 *
	 * The parent reads std_output[0], and the utility writes std_output[1].
	 */
	ifp = NULL;
	std_output[0] = std_output[1] = -1;
	if (pipe(std_output) < 0) {
		msgq(sp, M_SYSERR, "pipe");
		return (1);
	}
	if ((ifp = fdopen(std_output[0], "r")) == NULL) {
		msgq(sp, M_SYSERR, "fdopen");
		goto err;
	}

	/*
	 * Do the minimal amount of work possible, the shell is going to run
	 * briefly and then exit.  We sincerely hope.
	 */
	switch (pid = vfork()) {
	case -1:			/* Error. */
		msgq(sp, M_SYSERR, "vfork");
err:		if (ifp != NULL)
			(void)fclose(ifp);
		else if (std_output[0] != -1)
			close(std_output[0]);
		if (std_output[1] != -1)
			close(std_output[0]);
		return (1);
	case 0:				/* Utility. */
		/* Redirect stdout to the write end of the pipe. */
		(void)dup2(std_output[1], STDOUT_FILENO);

		/* Close the utility's file descriptors. */
		(void)close(std_output[0]);
		(void)close(std_output[1]);
		(void)close(STDERR_FILENO);

		/*
		 * XXX
		 * Assume that all shells have -c.
		 */
		execl(sh_path, sh, "-c", bp, (char *)NULL);
		msgq_str(sp, M_SYSERR, sh_path, "118|Error: execl: %s");
		_exit(127);
	default:			/* Parent. */
		/* Close the pipe ends the parent won't use. */
		(void)close(std_output[1]);
		break;
	}

	/*
	 * Copy process standard output into a buffer.
	 *
	 * !!!
	 * Historic vi apparently discarded leading \n and \r's from
	 * the shell output stream.  We don't on the grounds that any
	 * shell that does that is broken.
	 */
	for (p = bp, len = 0, ch = EOF;
	    (ch = getc(ifp)) != EOF; *p++ = ch, --blen, ++len)
		if (blen < 5) {
			ADD_SPACE_GOTO(sp, bp, *blenp, *blenp * 2);
			p = bp + len;
			blen = *blenp - len;
		}

	/* Delete the final newline, nul terminate the string. */
	if (p > bp && (p[-1] == '\n' || p[-1] == '\r')) {
		--p;
		--len;
	}
	*p = '\0';
	*lenp = len;
	*bpp = bp;		/* *blenp is already updated. */

	if (ferror(ifp))
		goto ioerr;
	if (fclose(ifp)) {
ioerr:		msgq_str(sp, M_ERR, sh, "119|I/O error: %s");
alloc_err:	rval = SEXP_ERR;
	} else
		rval = SEXP_OK;

	/*
	 * Wait for the process.  If the shell process fails (e.g., "echo $q"
	 * where q wasn't a defined variable) or if the returned string has
	 * no characters or only blank characters, (e.g., "echo $5"), complain
	 * that the shell expansion failed.  We can't know for certain that's
	 * the error, but it's a good guess, and it matches historic practice.
	 * This won't catch "echo foo_$5", but that's not a common error and
	 * historic vi didn't catch it either.
	 */
	if (proc_wait(sp, pid, sh, 1, 0))
		rval = SEXP_EXPANSION_ERR;

	for (p = bp; len; ++p, --len)
		if (!isblank(*p))
			break;
	if (len == 0)
		rval = SEXP_EXPANSION_ERR;

	if (rval == SEXP_EXPANSION_ERR)
		msgq(sp, M_ERR, "304|Shell expansion failed");

	return (rval == SEXP_OK ? 0 : 1);
}