xargs.c: implement -p, -P and -0, add TODO for -L - sbase - suckless unix tools
 (HTM) git clone git://git.suckless.org/sbase
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 635515f6e3b3d7f311a3ebc04dd94689af1a77a0
 (DIR) parent 7c3ccc8659099199f1b3c24e0c534c1795c88a13
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Thu, 31 Jul 2025 15:30:39 +0200
       
       xargs.c: implement -p, -P and -0, add TODO for -L
       
       From 2350f520a6dd7e293c5505aaa0983853cdd41ee6 Mon Sep 17 00:00:00 2001
       From: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date: Thu, 31 Jul 2025 14:40:43 +0200
       Subject: [PATCH 3/4] xargs.c: implement -p, -P and -0, add TODO for -L
       
       - Add option to read arguments separated by NUL (-0).
         Useful with find -print0 for example.
       - Add very useful parallel option (-P).
         POSIX vaguely mentions parallel operations, but this is commonly supported
         and very useful.  For example OpenBSD xargs supports it since at least 2003.
         GNU xargs since at least 1996.
       - Add prompt option (-p), (POSIX).
       - Add a TODO for xargs -L (POSIX extension).
       - Documentation and man page improvements.
       
       Diffstat:
         M TODO                                |       5 +++++
         M xargs.1                             |      45 ++++++++++++++++++++++---------
         M xargs.c                             |     102 ++++++++++++++++++++++++-------
       
       3 files changed, 116 insertions(+), 36 deletions(-)
       ---
 (DIR) diff --git a/TODO b/TODO
       @@ -85,3 +85,8 @@ tr
        sbase-box
        ---------
        * List of commands does not contain `install` (only `xinstall`).
       +
       +
       +xargs
       +-----
       +* Add -L.
 (DIR) diff --git a/xargs.1 b/xargs.1
       @@ -1,4 +1,4 @@
       -.Dd July 30, 2023
       +.Dd July 30, 2025
        .Dt XARGS 1
        .Os sbase
        .Sh NAME
       @@ -6,10 +6,11 @@
        .Nd construct argument lists and execute command
        .Sh SYNOPSIS
        .Nm
       -.Op Fl rtx
       +.Op Fl 0prtx
        .Op Fl E Ar eofstr
        .Op Fl I Ar replstr
        .Op Fl n Ar num
       +.Op Fl P Ar maxprocs
        .Op Fl s Ar num
        .Op Ar cmd Op Ar arg ...
        .Sh DESCRIPTION
       @@ -26,7 +27,7 @@ stdin.
        The command is repeatedly executed one or more times until stdin is exhausted.
        .Pp
        Spaces, tabs and newlines may be embedded in arguments using single (`'')
       -or double (`"') quotes or backslashes ('\\').
       +or double (`"') quotes or backslashes ('\e').
        Single quotes escape all non-single quote characters, excluding newlines, up
        to the matching single quote.
        Double quotes escape all non-double quote characters, excluding newlines, up
       @@ -34,13 +35,12 @@ to the matching double quote.
        Any single character, including newlines, may be escaped by a backslash.
        .Sh OPTIONS
        .Bl -tag -width Ds
       -.It Fl n Ar num
       -Use at most
       -.Ar num
       -arguments per command line.
       -.It Fl r
       -Do not run the command if there are no arguments.
       -Normally the command is executed at least once even if there are no arguments.
       +.It Fl 0
       +Change
       +.Nm
       +to expect NUL ('\e0') characters as separators, instead of spaces
       +and newlines.
       +The quoting mechanisms described above are not performed.
        .It Fl E Ar eofstr
        Use
        .Ar eofstr
       @@ -51,11 +51,32 @@ Use
        as the placeholder for the argument.
        Sets the arguments count to 1 per command line.
        It also implies the option x.
       +.It Fl n Ar num
       +Use at most
       +.Ar num
       +arguments per command line.
       +.It Fl p
       +Prompt mode: the user is asked whether to execute
       +.Ar cmd
       +at each invocation.
       +Trace mode (-t) is turned on to write the command instance to be executed,
       +followed by a prompt to standard error.
       +An affirmative response read from
       +.Pa /dev/tty
       +executes the command, otherwise it is skipped.
       +.It Fl P Ar maxprocs
       +Parallel mode: run at most maxprocs invocations of
       +.Ar cmd
       +at once.
       +.It Fl r
       +Do not run the command if there are no arguments.
       +Normally the command is executed at least once even if there are no arguments.
        .It Fl s Ar num
        Use at most
        .Ar num
        bytes per command line.
        .It Fl t
       +Enable trace mode.
        Write the command line to stderr before executing it.
        .It Fl x
        Terminate if the command line exceeds the system limit or the number of bytes
       @@ -95,9 +116,7 @@ The
        .Nm
        utility is compliant with the
        .St -p1003.1-2013
       -specification except from the
       -.Op Fl p
       -flag.
       +specification.
        .Pp
        The
        .Op Fl r
 (DIR) diff --git a/xargs.c b/xargs.c
       @@ -19,14 +19,15 @@ static int eatspace(void);
        static int parsequote(int);
        static int parseescape(void);
        static char *poparg(void);
       -static void waitchld(void);
       +static void waitchld(int);
        static void spawn(void);
        
        static size_t argbsz;
        static size_t argbpos;
       -static size_t maxargs = 0;
       -static int    nerrors = 0;
       -static int    rflag = 0, nflag = 0, tflag = 0, xflag = 0, Iflag = 0;
       +static size_t maxargs;
       +static size_t curprocs, maxprocs = 1;
       +static int    nerrors;
       +static int    nulflag, nflag, pflag, rflag, tflag, xflag, Iflag;
        static char  *argb;
        static char  *cmd[NARGS];
        static char  *eofstr;
       @@ -59,10 +60,7 @@ eatspace(void)
                int ch;
        
                while ((ch = inputc()) != EOF) {
       -                switch (ch) {
       -                case ' ': case '\t': case '\n':
       -                        break;
       -                default:
       +                if (nulflag || !(ch == ' ' || ch == '\t' || ch == '\n')) {
                                ungetc(ch, stdin);
                                return ch;
                        }
       @@ -129,6 +127,10 @@ poparg(void)
                                if (parseescape() < 0)
                                        eprintf("backslash at EOF\n");
                                break;
       +                case '\0':
       +                        /* NUL separator: no escaping */
       +                        if (nulflag)
       +                                goto out;
                        default:
                        fill:
                                fillargbuf(ch);
       @@ -143,22 +145,55 @@ out:
        }
        
        static void
       -waitchld(void)
       +waitchld(int waitall)
        {
       +        pid_t pid;
                int status;
        
       -        wait(&status);
       -        if (WIFEXITED(status)) {
       -                if (WEXITSTATUS(status) == 255)
       -                        exit(124);
       -                if (WEXITSTATUS(status) == 127 ||
       -                    WEXITSTATUS(status) == 126)
       -                        exit(WEXITSTATUS(status));
       -                if (status)
       -                        nerrors++;
       +        while ((pid = waitpid(-1, &status, !waitall && curprocs < maxprocs ?
       +               WNOHANG : 0)) > 0) {
       +               curprocs--;
       +
       +                if (WIFEXITED(status)) {
       +                        if (WEXITSTATUS(status) == 255)
       +                                exit(124);
       +                        if (WEXITSTATUS(status) == 127 ||
       +                            WEXITSTATUS(status) == 126)
       +                                exit(WEXITSTATUS(status));
       +                        if (WEXITSTATUS(status))
       +                                nerrors++;
       +                }
       +                if (WIFSIGNALED(status))
       +                        exit(125);
                }
       -        if (WIFSIGNALED(status))
       -                exit(125);
       +        if (pid == -1 && errno != ECHILD)
       +                eprintf("waitpid:");
       +}
       +
       +static int
       +prompt(void)
       +{
       +        FILE *fp;
       +        int ch, ret;
       +
       +        if (!(fp = fopen("/dev/tty", "r")))
       +                return -1;
       +
       +        fputs("?...", stderr);
       +        fflush(stderr);
       +
       +        ch = fgetc(fp);
       +        ret = (ch == 'y' || ch == 'Y');
       +        if (ch != EOF && ch != '\n') {
       +                while ((ch = fgetc(fp)) != EOF) {
       +                        if (ch == '\n')
       +                                break;
       +                }
       +        }
       +
       +        fclose(fp);
       +
       +        return ret;
        }
        
        static void
       @@ -168,16 +203,25 @@ spawn(void)
                int first = 1;
                char **p;
        
       -        if (tflag) {
       +        if (pflag || tflag) {
                        for (p = cmd; *p; p++) {
                                if (!first)
                                        fputc(' ', stderr);
                                fputs(*p, stderr);
                                first = 0;
                        }
       +                if (pflag) {
       +                        switch (prompt()) {
       +                        case -1: break; /* error */
       +                        case 0: return; /* no */
       +                        case 1: goto dospawn; /* yes */
       +                        }
       +                }
                        fputc('\n', stderr);
       +                fflush(stderr);
                }
        
       +dospawn:
                switch (fork()) {
                case -1:
                        eprintf("fork:");
       @@ -187,13 +231,14 @@ spawn(void)
                        weprintf("execvp %s:", *cmd);
                        _exit(126 + (savederrno == ENOENT));
                }
       -        waitchld();
       +        curprocs++;
       +        waitchld(0);
        }
        
        static void
        usage(void)
        {
       -        eprintf("usage: %s [-rtx] [-E eofstr] [-n num] [-s num] "
       +        eprintf("usage: %s [-0prtx] [-E eofstr] [-n num] [-P maxprocs] [-s num] "
                        "[cmd [arg ...]]\n", argv0);
        }
        
       @@ -212,10 +257,16 @@ main(int argc, char *argv[])
                argmaxsz -= 4096;
        
                ARGBEGIN {
       +        case '0':
       +                nulflag = 1;
       +                break;
                case 'n':
                        nflag = 1;
                        maxargs = estrtonum(EARGF(usage()), 1, MIN(SIZE_MAX, LLONG_MAX));
                        break;
       +        case 'p':
       +                pflag = 1;
       +                break;
                case 'r':
                        rflag = 1;
                        break;
       @@ -238,6 +289,9 @@ main(int argc, char *argv[])
                        maxargs = 1;
                        replstr = EARGF(usage());
                        break;
       +        case 'P':
       +                maxprocs = estrtonum(EARGF(usage()), 1, MIN(SIZE_MAX, LLONG_MAX));
       +                break;
                default:
                        usage();
                } ARGEND
       @@ -295,6 +349,8 @@ main(int argc, char *argv[])
        
                free(argb);
        
       +        waitchld(1);
       +
                if (nerrors || (fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>")))
                        ret = 123;