/* $OpenBSD: ex_argv.c,v 1.20 2016/05/27 09:18:12 martijn 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 #include #include #include #include #include #include #include #include #include #include #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(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(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(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(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 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(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(SCR *sp, EXCMD *excp, char *cmd, size_t cmdlen, char *p, size_t *lenp, char **bpp, size_t *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, "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, "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, "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(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; REALLOCARRAY(sp, exp->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], 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; REALLOCARRAY(sp, ap->bp, 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], 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(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(SCR *sp, EXCMD *excp, char *path) { struct dirent *dp; DIR *dirp; EX_PRIVATE *exp; int off; size_t dlen, 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); 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; } else { if (dp->d_namlen < nlen || memcmp(dp->d_name, name, nlen)) continue; } /* Directory + name + slash + null. */ argv_alloc(sp, dlen + dp->d_namlen + 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, dp->d_namlen + 1); exp->args[exp->argsoff]->len = dlen + dp->d_namlen + 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, "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(const void *a, const void *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(SCR *sp, char **bpp, size_t *blenp, size_t *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, "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, "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, "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, "Shell expansion failed"); return (rval == SEXP_OK ? 0 : 1); }