root/usr.bin/vi/ex/ex_argv.c
/*      $OpenBSD: ex_argv.c,v 1.21 2025/08/02 20:14:58 millert 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(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 <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(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 (sp->frp == NULL || (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);
}