root/usr/src/cmd/xargs/xargs.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2014 Garrett D'Amore <garrett@damore.org>
 * Copyright 2012 DEY Storage Systems, Inc.
 * Copyright (c) 2018, Joyent, Inc.
 *
 * Portions of this file developed by DEY Storage Systems, Inc. are licensed
 * under the terms of the Common Development and Distribution License (CDDL)
 * version 1.0 only.  The use of subsequent versions of the License are
 * is specifically prohibited unless those terms are not in conflict with
 * version 1.0 of the License.  You can find this license on-line at
 * http://www.illumos.org/license/CDDL
 */
/*
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*      Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */
/*        All Rights Reserved   */


#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <limits.h>
#include <wchar.h>
#include <locale.h>
#include <langinfo.h>
#include <stropts.h>
#include <poll.h>
#include <errno.h>
#include <stdarg.h>
#include <sys/fork.h>
#include "getresponse.h"

#define HEAD    0
#define TAIL    1
#define FALSE 0
#define TRUE 1
#define MAXSBUF 255
#define MAXIBUF 512
#define MAXINSERTS 5
#define BUFSIZE LINE_MAX
#define MAXARGS 255
#define INSPAT_STR      "{}"    /* default replstr string for -[Ii]     */
#define FORK_RETRY      5

#define QBUF_STARTLEN 255  /* start size of growable string buffer */
#define QBUF_INC 100       /* how much to grow a growable string by */

/* We use these macros to help make formatting look "consistent" */
#define EMSG(s)         ermsg(gettext(s "\n"))
#define EMSG2(s, a)     ermsg(gettext(s "\n"), a)
#define PERR(s)         perror(gettext("xargs: " s))

/* Some common error messages */

#define LIST2LONG       "Argument list too long"
#define ARG2LONG        "A single argument was greater than %d bytes"
#define MALLOCFAIL      "Memory allocation failure"
#define CORRUPTFILE     "Corrupt input file"
#define WAITFAIL        "Wait failure"
#define CHILDSIG        "Child killed with signal %d"
#define CHILDFAIL       "Command could not continue processing data"
#define FORKFAIL        "Could not fork child"
#define EXECFAIL        "Could not exec command"
#define MISSQUOTE       "Missing quote"
#define BADESCAPE       "Incomplete escape"
#define IBUFOVERFLOW    "Insert buffer overflow"
#define NOCHILDSLOT     "No free child slot available"

#define _(x)    gettext(x)

static wctype_t blank;
static char     *arglist[MAXARGS+1];
static char     argbuf[BUFSIZE * 2 + 1];
static char     lastarg[BUFSIZE + 1];
static char     **ARGV = arglist;
static char     *LEOF = "_";
static char     *INSPAT = INSPAT_STR;
static char     ins_buf[MAXIBUF];
static char     *p_ibuf;

static struct inserts {
        char    **p_ARGV;       /* where to put newarg ptr in arg list */
        char    *p_skel;        /* ptr to arg template */
} saveargv[MAXINSERTS];

static int      PROMPT = -1;
static int      BUFLIM = BUFSIZE;
static int      MAXPROCS = 1;
static int      N_ARGS = 0;
static int      N_args = 0;
static int      N_lines = 0;
static int      DASHX = FALSE;
static int      MORE = TRUE;
static int      PER_LINE = FALSE;
static int      LINE_CONT = FALSE;
static int      EAT_LEAD = FALSE;
static int      ERR = FALSE;
static int      OK = TRUE;
static int      LEGAL = FALSE;
static int      TRACE = FALSE;
static int      INSERT = FALSE;
static int      ZERO = FALSE;
static int      linesize = 0;
static int      ibufsize = 0;
static int      exitstat = 0;   /* our exit status                      */
static int      mac;            /* modified argc, after parsing         */
static char     **mav;          /* modified argv, after parsing         */
static int      n_inserts;      /* # of insertions.                     */
static pid_t    *procs;         /* pids of children                     */
static int      n_procs;        /* # of child processes.                */

/* our usage message:                                                   */
#define USAGEMSG "Usage: xargs: [-t] [-p] [-0] [-e[eofstr]] [-E eofstr] "\
        "[-I replstr] [-i[replstr]] [-L #] [-l[#]] [-n # [-x]] [-P maxprocs] "\
        "[-s size] [cmd [args ...]]\n"

static int      echoargs();
static wint_t   getwchr(char *, size_t *);
static void     lcall(char *sub, char **subargs);
static void     addibuf(struct inserts *p);
static void     ermsg(char *messages, ...);
static char     *addarg(char *arg);
static void     store_str(char **, char *, size_t);
static char     *getarg(char *);
static char     *insert(char *pattern, char *subst);
static void     usage();
static void     parseargs();
static int      procs_find(pid_t child);
static void     procs_store(pid_t child);
static boolean_t procs_delete(pid_t child);
static pid_t    procs_waitpid(boolean_t blocking, int *stat_loc);
static void     procs_wait(boolean_t blocking);

int
main(int argc, char **argv)
{
        int     j;
        long    l;
        struct inserts *psave;
        int c;
        int     initsize;
        char    *cmdname, **initlist;
        char    *arg;
        char    *next;
        char    *eptr;

        /* initialization */
        blank = wctype("blank");
        n_inserts = 0;
        psave = saveargv;
        (void) setlocale(LC_ALL, "");
#if !defined(TEXT_DOMAIN)       /* Should be defined by cc -D           */
#define TEXT_DOMAIN "SYS_TEST"  /* Use this only if it weren't          */
#endif
        (void) textdomain(TEXT_DOMAIN);
        if (init_yes() < 0) {
                ermsg(_(ERR_MSG_INIT_YES), strerror(errno));
                exit(1);
        }

        parseargs(argc, argv);

        /* handling all of xargs arguments:                             */
        while ((c = getopt(mac, mav, "0tpe:E:I:i:L:l:n:P:s:x")) != EOF) {
                switch (c) {
                case '0':
                        ZERO = TRUE;
                        break;

                case 't':       /* -t: turn trace mode on               */
                        TRACE = TRUE;
                        break;

                case 'p':       /* -p: turn on prompt mode.             */
                        if ((PROMPT = open("/dev/tty", O_RDONLY)) == -1) {
                                PERR("can't read from tty for -p");
                        } else {
                                TRACE = TRUE;
                        }
                        break;

                case 'e':
                        /*
                         * -e[eofstr]: set/disable end-of-file.
                         * N.B. that an argument *isn't* required here; but
                         * parseargs forced an argument if not was given.  The
                         * forced argument is the default...
                         */
                        LEOF = optarg; /* can be empty */
                        break;

                case 'E':
                        /*
                         * -E eofstr: change end-of-file string.
                         * eofstr *is* required here, but can be empty:
                         */
                        LEOF = optarg;
                        break;

                case 'I':
                        /* -I replstr: Insert mode. replstr *is* required. */
                        INSERT = PER_LINE = LEGAL = EAT_LEAD = TRUE;
                        LINE_CONT = FALSE;
                        N_ARGS = 0;
                        INSPAT = optarg;
                        if (*optarg == '\0') {
                                ermsg(_("Option requires an argument: -%c\n"),
                                    c);
                        }
                        break;

                case 'i':
                        /*
                         * -i [replstr]: insert mode, with *optional* replstr.
                         * N.B. that an argument *isn't* required here; if
                         * it's not given, then the string INSPAT_STR will
                         * be assumed.
                         *
                         * Since getopt(3C) doesn't handle the case of an
                         * optional variable argument at all, we have to
                         * parse this by hand:
                         */

                        INSERT = PER_LINE = LEGAL = EAT_LEAD = TRUE;
                        LINE_CONT = FALSE;
                        N_ARGS = 0;
                        if ((optarg != NULL) && (*optarg != '\0')) {
                                INSPAT = optarg;
                        } else {
                                /*
                                 * here, there is no next argument. so
                                 * we reset INSPAT to the INSPAT_STR.
                                 * we *have* to do this, as -i/I may have
                                 * been given previously, and XCU4 requires
                                 * that only "the last one specified takes
                                 * effect".
                                 */
                                INSPAT = INSPAT_STR;
                        }
                        break;

                case 'L':
                        /*
                         * -L number: # of times cmd is executed
                         * number *is* required here:
                         */
                        PER_LINE = LINE_CONT = TRUE;
                        N_ARGS = 0;
                        INSERT = EAT_LEAD = FALSE;
                        if ((PER_LINE = atoi(optarg)) <= 0) {
                                ermsg(_("#lines must be positive int: %s\n"),
                                    optarg);
                        }
                        break;

                case 'l':
                        /*
                         * -l [number]: # of times cmd is executed
                         * N.B. that an argument *isn't* required here; if
                         * it's not given, then 1 is assumed.
                         *
                         * parseargs handles the optional arg processing.
                         */

                        PER_LINE = LINE_CONT = LEGAL = TRUE;
                        N_ARGS = 0;
                        INSERT = EAT_LEAD = FALSE;

                        if ((optarg != NULL) && (*optarg != '\0')) {
                                if ((PER_LINE = atoi(optarg)) <= 0)
                                        PER_LINE = 1;
                        }
                        break;

                case 'n':       /* -n number: # stdin args              */
                        /*
                         * -n number: # stdin args.
                         * number *is* required here:
                         */
                        if ((N_ARGS = atoi(optarg)) <= 0) {
                                ermsg(_("#args must be positive int: %s\n"),
                                    optarg);
                        } else {
                                LEGAL = DASHX || N_ARGS == 1;
                                INSERT = PER_LINE = LINE_CONT = FALSE;
                        }
                        break;

                case 'P':       /* -P maxprocs: # of child processses   */
                        errno = 0;
                        l = strtol(optarg, &eptr, 10);
                        if (*eptr != '\0' || errno != 0) {
                                ermsg(_("failed to parse maxprocs (-P): %s\n"),
                                    optarg);
                                break;
                        }

                        if (l < 0) {
                                ermsg(_("maximum number of processes (-P) "
                                    "cannot be negative\n"));
                                break;
                        }

                        /*
                         * Come up with an upper bound that'll probably fit in
                         * memory.
                         */
                        if (l == 0 || l > ((INT_MAX / sizeof (pid_t) >> 1))) {
                                l = INT_MAX / sizeof (pid_t) >> 1;
                        }
                        MAXPROCS = (int)l;
                        break;

                case 's':       /* -s size: set max size of each arg list */
                        BUFLIM = atoi(optarg);
                        if (BUFLIM > BUFSIZE || BUFLIM <= 0) {
                                ermsg(_("0 < max-cmd-line-size <= %d: %s\n"),
                                    BUFSIZE, optarg);
                        }
                        break;

                case 'x':       /* -x: terminate if args > size limit   */
                        DASHX = LEGAL = TRUE;
                        break;

                default:
                        /*
                         * bad argument. complain and get ready to die.
                         */
                        usage();
                        exit(2);
                        break;
                }
        }

        /*
         * if anything called ermsg(), something screwed up, so
         * we exit early.
         */
        if (OK == FALSE) {
                usage();
                exit(2);
        }

        /*
         * we're finished handling xargs's options, so now pick up
         * the command name (if any), and it's options.
         */


        mac -= optind;  /* dec arg count by what we've processed        */
        mav += optind;  /* inc to current mav                           */

        procs = calloc(MAXPROCS, sizeof (pid_t));
        if (procs == NULL) {
                PERR(MALLOCFAIL);
                exit(1);
        }

        if (mac <= 0) { /* if there're no more args to process, */
                cmdname = "/usr/bin/echo";      /* our default command  */
                *ARGV++ = addarg(cmdname);      /* use the default cmd. */
        } else {        /* otherwise keep parsing rest of the string.   */
                /*
                 * note that we can't use getopt(3C), and *must* parse
                 * this by hand, as we don't know apriori what options the
                 * command will take.
                 */
                cmdname = *mav; /* get the command name */


                /* pick up the remaining args from the command line:    */
                while ((OK == TRUE) && (mac-- > 0)) {
                        /*
                         * while we haven't crapped out, and there's
                         * work to do:
                         */
                        if (INSERT && ! ERR) {
                                if (strstr(*mav, INSPAT) != NULL) {
                                        if (++n_inserts > MAXINSERTS) {
                                                ermsg(_("too many args "
                                                    "with %s\n"), INSPAT);
                                                ERR = TRUE;
                                        }
                                        psave->p_ARGV = ARGV;
                                        (psave++)->p_skel = *mav;
                                }
                        }
                        *ARGV++ = addarg(*mav++);
                }
        }

        /* pick up args from standard input */

        initlist = ARGV;
        initsize = linesize;
        lastarg[0] = '\0';

        while (OK) {
                N_args = 0;
                N_lines = 0;
                ARGV = initlist;
                linesize = initsize;
                next = argbuf;

                while (MORE || (lastarg[0] != '\0')) {
                        int l;

                        if (*lastarg != '\0') {
                                arg = strcpy(next, lastarg);
                                *lastarg = '\0';
                        } else if ((arg = getarg(next)) == NULL) {
                                break;
                        }

                        l = strlen(arg) + 1;
                        linesize += l;
                        next += l;

                        /* Inserts are handled specially later. */
                        if ((n_inserts == 0) && (linesize >= BUFLIM)) {
                                /*
                                 * Legal indicates hard fail if the list is
                                 * truncated due to size.  So fail, or if we
                                 * cannot create any list because it would be
                                 * too big.
                                 */
                                if (LEGAL || N_args == 0) {
                                        EMSG(LIST2LONG);
                                        procs_wait(B_TRUE);
                                        exit(2);
                                        /* NOTREACHED */
                                }

                                /*
                                 * Otherwise just save argument for later.
                                 */
                                (void) strcpy(lastarg, arg);
                                break;
                        }

                        *ARGV++ = arg;

                        N_args++;

                        if ((PER_LINE && (N_lines >= PER_LINE)) ||
                            (N_ARGS && (N_args >= N_ARGS))) {
                                break;
                        }


                        if ((ARGV - arglist) == MAXARGS) {
                                break;
                        }
                }

                *ARGV = NULL;
                if (N_args == 0) {
                        /* Reached the end with no more work. */
                        break;
                }

                /* insert arg if requested */

                if (!ERR && INSERT) {

                        p_ibuf = ins_buf;
                        ARGV--;
                        j = ibufsize = 0;
                        for (psave = saveargv; ++j <= n_inserts; ++psave) {
                                addibuf(psave);
                                if (ERR)
                                        break;
                        }
                }
                *ARGV = NULL;

                if (n_inserts > 0) {
                        /*
                         * if we've done any insertions, re-calculate the
                         * linesize. bomb out if we've exceeded our length.
                         */
                        linesize = 0;
                        for (ARGV = arglist; *ARGV != NULL; ARGV++) {
                                linesize += strlen(*ARGV) + 1;
                        }
                        if (linesize >= BUFLIM) {
                                EMSG(LIST2LONG);
                                procs_wait(B_TRUE);
                                exit(2);
                                /* NOTREACHED */
                        }
                }

                /* exec command */

                if (!ERR) {
                        if (!MORE &&
                            (PER_LINE && N_lines == 0 || N_ARGS && N_args == 0))
                                exit(exitstat);
                        OK = TRUE;
                        j = TRACE ? echoargs() : TRUE;
                        if (j) {
                                /*
                                 * for xcu4, all invocations of cmdname must
                                 * return 0, in order for us to return 0.
                                 * so if we have a non-zero status here,
                                 * quit immediately.
                                 */
                                (void) lcall(cmdname, arglist);
                        }
                }
        }

        procs_wait(B_TRUE);

        if (OK)
                return (exitstat);

        /*
         * if exitstat was set, to match XCU4 complience,
         * return that value, otherwise, return 1.
         */
        return (exitstat ? exitstat : 1);
}

static char *
addarg(char *arg)
{
        linesize += (strlen(arg) + 1);
        return (arg);
}


static void
store_str(char **buffer, char *str, size_t len)
{
        (void) memcpy(*buffer, str, len);
        (*buffer)[len] = '\0';
        *buffer += len;
}


static char *
getarg(char *arg)
{
        char    *xarg = arg;
        wchar_t c = 0;
        char    mbc[MB_LEN_MAX];
        size_t  len;
        int     escape = 0;
        int     inquote = 0;
        int     last = 0;

        arg[0] = '\0';

        while (MORE) {

                len = 0;
                last = c;
                c = getwchr(mbc, &len);

                if (((arg - xarg) + len) > BUFLIM) {
                        EMSG2(ARG2LONG, BUFLIM);
                        exit(2);
                        ERR = TRUE;
                        return (NULL);
                }

                switch (c) {
                case '\n':
                        if (ZERO) {
                                store_str(&arg, mbc, len);
                                continue;
                        }
                        /*
                         * NB: Some other versions rip off all of the trailing
                         * blanks.  The spec only claims that this should
                         * be done for a single blank.  We follow the spec.
                         */
                        if (LINE_CONT && iswctype(last, blank)) {
                                len = 0;
                                *arg = 0;
                                continue;
                        }
                        /* FALLTHRU */

                case '\0':
                case WEOF:      /* Note WEOF == EOF */

                        if (escape) {
                                EMSG(BADESCAPE);
                                ERR = TRUE;
                                return (NULL);
                        }
                        if (inquote) {
                                EMSG(MISSQUOTE);
                                ERR = TRUE;
                                return (NULL);
                        }

                        N_lines++;
                        break;

                case '"':
                        if (ZERO || escape || (inquote == 1)) {
                                /* treat it literally */
                                escape = 0;
                                store_str(&arg, mbc, len);

                        } else if (inquote == 2) {
                                /* terminating double quote */
                                inquote = 0;

                        } else {
                                /* starting quoted string */
                                inquote = 2;
                        }
                        continue;

                case '\'':
                        if (ZERO || escape || (inquote == 2)) {
                                /* treat it literally */
                                escape = 0;
                                store_str(&arg, mbc, len);

                        } else if (inquote == 1) {
                                /* terminating single quote */
                                inquote = 0;

                        } else {
                                /* starting quoted string */
                                inquote = 1;
                        }
                        continue;

                case '\\':
                        /*
                         * Any unquoted character can be escaped by
                         * preceding it with a backslash.
                         */
                        if (ZERO || inquote || escape) {
                                escape = 0;
                                store_str(&arg, mbc, len);
                        } else {
                                escape = 1;
                        }
                        continue;

                default:
                        /* most times we will just want to store it */
                        if (inquote || escape || ZERO || !iswctype(c, blank)) {
                                escape = 0;
                                store_str(&arg, mbc, len);
                                continue;
                        }
                        if (EAT_LEAD && last == 0) {
                                c = 0;          /* Roll it back */
                                continue;
                        }
                        if (PER_LINE) {
                                store_str(&arg, mbc, len);
                                continue;
                        }

                        /* unquoted blank without special handling */
                        break;
                }

                /*
                 * At this point we are processing a complete argument.
                 */
                if (strcmp(xarg, LEOF) == 0 && *LEOF != '\0') {
                        MORE = FALSE;
                        return (NULL);
                }
                if (c == WEOF) {
                        MORE = FALSE;
                }
                if (xarg[0] == '\0')
                        continue;
                break;
        }

        return (xarg[0] == '\0' ? NULL : xarg);
}

/*
 * ermsg():     print out an error message, and indicate failure globally.
 *
 *      Assumes that message has already been gettext()'d. It would be
 *      nice if we could just do the gettext() here, but we can't, since
 *      since xgettext(1) wouldn't be able to pick up our error message.
 */
/* PRINTFLIKE1 */
static void
ermsg(char *messages, ...)
{
        va_list ap;

        va_start(ap, messages);

        (void) fprintf(stderr, "xargs: ");
        (void) vfprintf(stderr, messages, ap);

        va_end(ap);
        OK = FALSE;
}

static int
echoargs(void)
{
        char    **anarg;
        char    **tanarg;       /* tmp ptr                      */
        int     i;
        char    reply[LINE_MAX];

        tanarg = anarg = arglist-1;

        /*
         * write out each argument, separated by a space. the tanarg
         * nonsense is for xcu4 testsuite compliance - so that an
         * extra space isn't echoed after the last argument.
         */
        while (*++anarg) {              /* while there's an argument    */
                ++tanarg;               /* follow anarg                 */
                (void) write(2, *anarg, strlen(*anarg));

                if (*++tanarg) {        /* if there's another argument: */
                        (void) write(2, " ", 1); /* add a space         */
                        --tanarg;       /* reset back to anarg          */
                }
        }
        if (PROMPT == -1) {
                (void) write(2, "\n", 1);
                return (TRUE);
        }

        (void) write(2, "?...", 4);     /* ask the user for input       */

        for (i = 0; i < LINE_MAX && read(PROMPT, &reply[i], 1) > 0; i++) {
                if (reply[i] == '\n') {
                        if (i == 0)
                                return (FALSE);
                        break;
                }
        }
        if (i < LINE_MAX)
                reply[i] = '\0';
        else
                reply[LINE_MAX - 1] = '\0';

        /* flush remainder of line if necessary */
        if (i == LINE_MAX) {
                char    bitbucket;

                while ((read(PROMPT, &bitbucket, 1) > 0) && (bitbucket != '\n'))
                        ;
        }

        return (yes_check(reply));
}


static char *
insert(char *pattern, char *subst)
{
        static char     buffer[MAXSBUF+1];
        int             len, ipatlen;
        char    *pat;
        char    *bufend;
        char    *pbuf;

        len = strlen(subst);
        ipatlen = strlen(INSPAT) - 1;
        pat = pattern - 1;
        pbuf = buffer;
        bufend = &buffer[MAXSBUF];

        while (*++pat) {
                if (strncmp(pat, INSPAT, ipatlen + 1) == 0) {
                        if (pbuf + len >= bufend) {
                                break;
                        } else {
                                (void) strcpy(pbuf, subst);
                                pat += ipatlen;
                                pbuf += len;
                        }
                } else {
                        *pbuf++ = *pat;
                        if (pbuf >= bufend)
                                break;
                }
        }

        if (!*pat) {
                *pbuf = '\0';
                return (buffer);
        } else {
                ermsg(gettext("Maximum argument size with insertion via %s's "
                    "exceeded\n"), INSPAT);
                ERR = TRUE;
                return (NULL);
        }
}


static void
addibuf(struct inserts  *p)
{
        char    *newarg, *skel, *sub;
        int             l;

        skel = p->p_skel;
        sub = *ARGV;
        newarg = insert(skel, sub);
        if (ERR)
                return;

        l = strlen(newarg) + 1;
        if ((ibufsize += l) > MAXIBUF) {
                EMSG(IBUFOVERFLOW);
                ERR = TRUE;
        }
        (void) strcpy(p_ibuf, newarg);
        *(p->p_ARGV) = p_ibuf;
        p_ibuf += l;
}


/*
 * getwchr():   get the next wide character.
 * description:
 *      we get the next character from stdin.  This returns WEOF if no
 *      character is present.  If ZERO is set, it gets a single byte instead
 *      a wide character.
 */
static wint_t
getwchr(char *mbc, size_t *sz)
{
        size_t          i;
        int             c;
        wchar_t         wch;

        i = 0;
        while (i < MB_CUR_MAX) {

                if ((c = fgetc(stdin)) == EOF) {

                        if (i == 0) {
                                /* TRUE EOF has been reached */
                                return (WEOF);
                        }

                        /*
                         * We have some characters in our buffer still so it
                         * must be an invalid character right before EOF.
                         */
                        break;
                }
                mbc[i++] = (char)c;

                /* If this succeeds then we are done */
                if (ZERO) {
                        *sz = i;
                        return ((char)c);
                }
                if (mbtowc(&wch, mbc, i) != -1) {
                        *sz = i;
                        return ((wint_t)wch);
                }
        }

        /*
         * We have now encountered an illegal character sequence.
         * There is nothing much we can do at this point but
         * return an error.  If we attempt to recover we may in fact
         * return garbage as arguments, from the customer's point
         * of view.  After all what if they are feeding us a file
         * generated in another locale?
         */
        errno = EILSEQ;
        PERR(CORRUPTFILE);
        exit(1);
        /* NOTREACHED */
}


static void
lcall(char *sub, char **subargs)
{
        int     retry = 0;
        pid_t   child;

        for (;;) {
                switch (child = forkx(FORK_NOSIGCHLD)) {
                default:
                        procs_store(child);
                        /*
                         * Note, if we have used up all of our slots, then this
                         * call may end up blocking.
                         */
                        procs_wait(B_FALSE);
                        return;
                case 0:
                        (void) execvp(sub, subargs);
                        PERR(EXECFAIL);
                        if (errno == EACCES)
                                exit(126);
                        exit(127);
                        /* NOTREACHED */
                case -1:
                        if (errno != EAGAIN && retry++ < FORK_RETRY) {
                                PERR(FORKFAIL);
                                exit(123);
                        }
                        (void) sleep(1);
                }
        }
}

/*
 * Return the index of child in the procs array.
 */
static int
procs_find(pid_t child)
{
        int     i;

        for (i = 0; i < MAXPROCS; i++) {
                if (procs[i] == child) {
                        return (i);
                }
        }

        return (-1);
}

static void
procs_store(pid_t child)
{
        int     i;

        i = procs_find(0);
        if (i < 0) {
                EMSG(NOCHILDSLOT);
                exit(1);
        }
        procs[i] = child;
        n_procs++;
}

static boolean_t
procs_delete(pid_t child)
{
        int     i;

        i = procs_find(child);
        if (i < 0) {
                return (B_FALSE);
        }

        procs[i] = (pid_t)0;
        n_procs--;

        return (B_TRUE);
}

static pid_t
procs_waitpid(boolean_t blocking, int *stat_loc)
{
        pid_t   child;
        int     options;

        if (n_procs == 0) {
                errno = ECHILD;
                return (-1);
        }

        options = 0;
        if (!blocking) {
                options |= WNOHANG;
        }

        while ((child = waitpid((pid_t)-1, stat_loc, options)) > 0) {
                if (procs_delete(child)) {
                        break;
                }
        }

        return (child);
}

static void
procs_wait(boolean_t blocking)
{
        pid_t   child;
        int     stat_loc;

        /*
         * If we currently have filled all of our slots, then we need to block
         * further execution.
         */
        if (n_procs >= MAXPROCS)
                blocking = B_TRUE;
        while ((child = procs_waitpid(blocking, &stat_loc)) > 0) {
                if (WIFSIGNALED(stat_loc)) {
                        EMSG2(CHILDSIG, WTERMSIG(stat_loc));
                        exit(125);
                        /* NOTREACHED */
                } else if ((WEXITSTATUS(stat_loc) & 0377) == 0377) {
                        EMSG(CHILDFAIL);
                        exit(124);
                        /* NOTREACHED */
                } else {
                        exitstat |= WEXITSTATUS(stat_loc);
                }
        }

        if (child == (pid_t)(-1) && errno != ECHILD) {
                EMSG(WAITFAIL);
                exit(122);
                /* NOTREACHED */
        }
}

static void
usage()
{
        ermsg(_(USAGEMSG));
        OK = FALSE;
}



/*
 * parseargs():         modify the args
 *      since the -e, -i and -l flags all take optional subarguments,
 *      and getopt(3C) is clueless about this nonsense, we change
 *      our local argument count and strings to separate this out,
 *      and make it easier to handle via getopt(3C).
 *
 *      -e      -> "-e ""
 *      -e3     -> "-e "3"
 *      -Estr   -> "-E "str"
 *      -i      -> "-i "{}"
 *      -irep   -> "-i "rep"
 *      -l      -> "-l "1"
 *      -l10    -> "-l "10"
 *
 *      since the -e, -i and -l flags all take optional subarguments,
 */
static void
parseargs(int ac, char **av)
{
        int i;                  /* current argument                     */
        int cflag;              /* 0 = not processing cmd arg           */

        if ((mav = malloc((ac * 2 + 1) * sizeof (char *))) == NULL) {
                PERR(MALLOCFAIL);
                exit(1);
        }

        /* for each argument, see if we need to change things:          */
        for (i = mac = cflag = 0; (av[i] != NULL) && i < ac; i++, mac++) {
                if ((mav[mac] = strdup(av[i])) == NULL) {
                        PERR(MALLOCFAIL);
                        exit(1);
                }

                /* -- has been found or argument list is fully processes */
                if (cflag)
                        continue;

                /*
                 * if we're doing special processing, and we've got a flag
                 */
                else if ((av[i][0] == '-') && (av[i][1] != '\0')) {
                        char    *def;

                        switch (av[i][1]) {
                        case    'e':
                                def = ""; /* -e with no arg turns off eof */
                                goto process_special;
                        case    'i':
                                def = INSPAT_STR;
                                goto process_special;
                        case    'l':
                                def = "1";
process_special:
                                /*
                                 * if there's no sub-option, we *must* add
                                 * a default one. this is because xargs must
                                 * be able to distinguish between a valid
                                 * suboption, and a command name.
                                 */
                                if (av[i][2] == '\0') {
                                        mav[++mac] = strdup(def);
                                } else {
                                        /* clear out our version: */
                                        mav[mac][2] = '\0';
                                        mav[++mac] = strdup(&av[i][2]);
                                }
                                if (mav[mac] == NULL) {
                                        PERR(MALLOCFAIL);
                                        exit(1);
                                }
                                break;

                        /* flags with required subarguments:            */

                        /*
                         * there are two separate cases here. either the
                         * flag can have the normal XCU4 handling
                         * (of the form: -X subargument); or it can have
                         * the old solaris 2.[0-4] handling (of the
                         * form: -Xsubargument). in order to maintain
                         * backwards compatibility, we must support the
                         * latter case. we handle the latter possibility
                         * first so both the old solaris way of handling
                         * and the new XCU4 way of handling things are allowed.
                         */
                        case    'n':    /* FALLTHROUGH                  */
                        case    'P':    /* FALLTHROUGH                  */
                        case    's':    /* FALLTHROUGH                  */
                        case    'E':    /* FALLTHROUGH                  */
                        case    'I':    /* FALLTHROUGH                  */
                        case    'L':
                                /*
                                 * if the second character isn't null, then
                                 * the user has specified the old syntax.
                                 * we move the subargument into our
                                 * mod'd argument list.
                                 */
                                if (av[i][2] != '\0') {
                                        /* first clean things up:       */
                                        mav[mac][2] = '\0';

                                        /* now add the separation:      */
                                        ++mac;  /* inc to next mod'd arg */
                                        if ((mav[mac] = strdup(&av[i][2])) ==
                                            NULL) {
                                                PERR(MALLOCFAIL);
                                                exit(1);
                                        }
                                        break;
                                }
                                i++;
                                mac++;

                                if (av[i] == NULL) {
                                        mav[mac] = NULL;
                                        return;
                                }
                                if ((mav[mac] = strdup(av[i])) == NULL) {
                                        PERR(MALLOCFAIL);
                                        exit(1);
                                }
                                break;

                        /* flags */
                        case 'p' :
                        case 't' :
                        case 'x' :
                        case '0' :
                                break;

                        case '-' :
                        default:
                                /*
                                 * here we've hit the cmd argument. so
                                 * we'll stop special processing, as the
                                 * cmd may have a "-i" etc., argument,
                                 * and we don't want to add a "" to it.
                                 */
                                cflag = 1;
                                break;
                        }
                } else if (i > 0) {     /* if we're not the 1st arg     */
                        /*
                         * if it's not a flag, then it *must* be the cmd.
                         * set cflag, so we don't mishandle the -[eil] flags.
                         */
                        cflag = 1;
                }
        }

        mav[mac] = NULL;
}