diff options
-rw-r--r-- | README.md | 68 | ||||
-rw-r--r-- | xa.c | 262 |
2 files changed, 330 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..20aab72 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +## xa: simple xargs and apply replacement + +`xa` is a new tool for constructing command lines from file listings +or arguments, which includes the best features of `xargs(1)` and +`apply(1)`. + +## Benefits + +Over xargs: +* Sane defaults (behaves like `xargs -d'\n' -I{} -L1 -n1 -r`). +* No weird parsing, arguments are seperated linewise or by NUL byte. +* Can also take arguments from command-line. +* No shell involved unless `-s` is used. + +Over apply: +* Parallel mode. +* Sane argument splitting. +* Can use shell-syntax instead of escape characters. + +## Usage: + + xa [-0nv] [-I arg] [-N maxargs] [-j maxjobs] COMMAND... + | -s SHELLSCRIPT + | -a COMMAND... -- ARGS... + | -A ARGSEP COMMAND... ARGSEP ARGS... + +* `-0`: input filenames are seperated by NUL bytes (default: newlines). +* `-n`: don't run the commands, just print them. +* `-v`: print commands before running them. +* `-I`: replace occurences of *arg* with the argument (default: `{}`). +* `-N`: pass upto *maxargs* arguments to each COMMAND (default: 1). +* `-j`: run up to *maxjobs* processes concurrently. +* `COMMAND...`: default operation: each command line argument is + passed as-is, `{}` is replaced by the argument (not with `-N` > 1). +* `-s SHELLSCRIPT`: The argument `SHELLSCRIPT` is evaluated using `/bin/sh` + with the arguments (up to `-N`) passed as `$1`, `$2`, `$3`... + (this behaves as if `/bin/sh -c SHELLSCRIPT -` is passed as plain COMMAND). +* `-a`: take arguments from commandline, starting after the first `--`. +* `-A`: take arguments from commandline, starting after the first *argsep*. + +## Return code + +Like GNU and OpenBSD xargs: + +* 0 on success +* 123 if any invocation of the command exited with status 1-125 +* 124 if the command exited with status 255 +* 125 if the command is killed by a signal +* 126 if the command cannot be run +* 127 if the command is not found +* 1 if some other error occurred. + +## Installation + +Use `make all` to build, `make install` to install relative to `PREFIX` +(`/usr/local` by default). The `DESTDIR` convention is respected. +You can also just copy the binary into your `PATH`. + +## Copyright + +xa is in the public domain. + +To the extent possible under law, +Christian Neukirchen <chneukirchen@gmail.com> +has waived all copyright and related or +neighboring rights to this work. + +http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/xa.c b/xa.c new file mode 100644 index 0000000..666ec3e --- /dev/null +++ b/xa.c @@ -0,0 +1,262 @@ +/* + * xa - simple xargs and apply replacement + * + * To the extent possible under law, + * Christian Neukirchen <chneukirchen@gmail.com> + * has waived all copyright and related or neighboring rights to this work. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> + +static char delim = '\n'; +static char default_replace[] = "{}"; +static char *replace = default_replace; +static char default_argsep[] = "--"; +static char *argsep = default_argsep; +static char **args; +static char *sflag; + +static int maxatonce = 1; +static int maxjobs = 1; +static int runjobs = 0; +static int aflag, nflag, vflag; + +static char * +xstrdup(const char *s) +{ + char *d = strdup(s); + if (!d) + exit(1); + return d; +} + +static char *getarg_line = 0; +static size_t getarg_len = 0; + +static char * +getarg() +{ + if (aflag) { + if (args && *args) + return xstrdup(*args++); + else + return 0; + } + + int read = getdelim(&getarg_line, &getarg_len, delim, stdin); + if (read == -1) { + if (feof(stdin)) + return 0; + else + exit(1); + } + if (getarg_line[read-1] == delim) // strip delimiter + getarg_line[read-1] = 0; + + return xstrdup(getarg_line); +} + +static int +mywait() +{ + int status; + pid_t pid; + + pid = wait(&status); + if (pid < 0) { + if (errno == ECHILD) + return 0; + // no other error possible? + } + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) >= 1 && WEXITSTATUS(status) <= 125) { + exit(123); + } else if (WEXITSTATUS(status) == 255) { + fprintf(stderr, "xa: %d exited with status 255\n", pid); + exit(124); + } else if (WEXITSTATUS(status) > 125) { + exit(WEXITSTATUS(status)); + } + } else if (WIFSIGNALED(status)) { + fprintf(stderr, "xa: %d terminated by signal %d\n", + pid, WTERMSIG(status)); + exit(125); + } + + runjobs--; + return 1; +} + +static void +shquote(const char *s) +{ + if (!strpbrk(s, "\001\002\003\004\005\006\007\010" + "\011\012\013\014\015\016\017\020" + "\021\022\023\024\025\026\027\030" + "\031\032\033\034\035\036\037\040" + "`^#*[]=|\\?${}()'\"<>&;\127")) { + printf("%s", s); + return; + } + + putchar('\''); + for (; *s; s++) + if (*s == '\'') + printf("'\\''"); + else + putchar(*s); + putchar('\''); +} + +static int +trace(char *cmd[]) +{ + int i; + + for (i = 0; cmd[i]; i++) { + if (i > 0) + printf(" "); + shquote(cmd[i]); + } + printf("\n"); + + return 0; +} + +static int +run(char *cmd[]) +{ + pid_t pid; + int i; + + if (runjobs >= maxjobs) + mywait(); + runjobs++; + + if (vflag || nflag) + trace(cmd); + if (nflag) { + runjobs--; + return 0; + } + + pid = fork(); + if (pid == 0) { // in child + // redirect stdin to /dev/null + int fd = open("/dev/null", O_RDONLY); + if (fd >= 0) { + if (dup2(fd, 0) != 0) + exit(1); + close(fd); + execvp(cmd[0], cmd); + } + fprintf(stderr, "xa: %s: %s\n", cmd[0], strerror(errno)); + exit(errno == ENOENT ? 127 : 126); + } + + if (pid < 0) + exit(126); + + for (i = 0; cmd[i]; i++) + free(cmd[i]); + + return 0; +} + +int +main(int argc, char *argv[]) +{ + char c; + int i, cmdend; + char *arg, **cmd; + + while ((c = getopt(argc, argv, "+0A:I:N:anj:s:v")) != -1) + switch(c) { + case '0': delim = '\0'; break; + case 'A': argsep = optarg; aflag++; break; + case 'I': replace = optarg; break; + case 'N': maxatonce = atoi(optarg); break; + case 'a': aflag++; break; + case 'n': nflag++; break; + case 'j': maxjobs = atoi(optarg); break; + case 's': sflag = optarg; break; + case 'v': vflag++; break; + default: + fprintf(stderr, + "Usage: %s [-0nv] [-I arg] [-N maxargs] [-j maxjobs] COMMAND...\n" + " | -s SHELLSCRIPT\n" + " | -a COMMAND... -- ARGS...\n" + " | -A ARGSEP COMMAND... ARGSEP ARGS...\n", + argv[0]); + exit(1); + } + + cmdend = argc; + if (aflag) // find argsep in argv + for (i = optind; i < argc; i++) + if (strcmp(argv[i], argsep) == 0) { + args = argv + i+1; + cmdend = i; + break; + } + + cmd = calloc(argc-optind+maxatonce+1+(sflag ? 4 : 0), sizeof (char *)); + if (!cmd) + exit(1); + + while ((arg = getarg())) { + int l = 0; + + if (sflag) { + cmd[l++] = xstrdup("/bin/sh"); + cmd[l++] = xstrdup("-c"); + cmd[l++] = xstrdup(sflag); + cmd[l++] = xstrdup("-"); + } + + if (maxatonce == 1) { + // substitute {} + int substituted = 0; + for (i = optind; i < cmdend; i++) { + if (strcmp(argv[i], replace) == 0) { + cmd[l++] = arg; + substituted = 1; + } else { + cmd[l++] = xstrdup(argv[i]); + } + } + if (!substituted) + cmd[l++] = arg; + } else { + // just append to cmd + for (i = optind; i < cmdend; i++) + cmd[l++] = xstrdup(argv[i]); + cmd[l++] = arg; + for (i = 0; i < maxatonce - 1; i++) { + cmd[l] = getarg(); + if (!cmd[l++]) + break; + } + } + cmd[l] = 0; + run(cmd); + } + + while (mywait()) + ; + + free(cmd); + free(getarg_line); + return 0; +} |