diff options
38 files changed, 1031 insertions, 259 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e34313..e9c3f8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,6 @@ jobs: sudo dpkg --add-architecture i386 sudo apt-get update -y sudo apt-get install -y \ - expect \ mandoc \ gcc-multilib \ libgcc-s1:i386 \ @@ -57,7 +56,6 @@ jobs: run: | sudo apt-get update -y sudo apt-get install -y \ - expect \ mandoc \ acl \ libacl1-dev \ @@ -110,10 +108,8 @@ jobs: run: | sudo pkg install -y \ bash \ - expect \ oniguruma \ - pkgconf \ - tcl-wrapper + pkgconf sudo mount -t fdescfs none /dev/fd .github/diag.sh make -j$(nproc) distcheck @@ -139,7 +135,6 @@ jobs: run: | sudo pkg_add \ bash \ - expect \ gmake \ oniguruma jobs=$(sysctl -n hw.ncpu) @@ -170,8 +165,7 @@ jobs: sudo pkgin -y install \ bash \ oniguruma \ - pkgconf \ - tcl-expect + pkgconf jobs=$(sysctl -n hw.ncpu) ./configure .github/diag.sh make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped" @@ -198,11 +192,9 @@ jobs: prepare: | pkg install -y \ bash \ - expect \ oniguruma \ pkgconf \ - sudo \ - tcl-wrapper + sudo pw useradd -n action -m -G wheel -s /usr/local/bin/bash echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers @@ -235,7 +227,6 @@ jobs: pkg install \ bash \ build-essential \ - expect \ gnu-make \ onig \ sudo diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 4cce8ed..e4e8f71 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -13,7 +13,6 @@ jobs: run: | sudo apt-get update -y sudo apt-get install -y \ - expect \ gcc \ acl \ libacl1-dev \ @@ -43,6 +43,7 @@ bfs: bin/bfs BINS := \ bin/bfs \ bin/tests/mksock \ + bin/tests/ptyx \ bin/tests/units \ bin/tests/xspawnee \ bin/tests/xtouch \ @@ -119,6 +120,7 @@ UTEST_BINS := \ # Integration test binaries ITEST_BINS := \ bin/tests/mksock \ + bin/tests/ptyx \ bin/tests/xtouch # Build (but don't run) test binaries @@ -179,6 +181,9 @@ integration-tests: ${INTEGRATION_TESTS} bin/tests/mksock: obj/tests/mksock.o ${LIBBFS} OBJS += obj/tests/mksock.o +bin/tests/ptyx: obj/tests/ptyx.o ${LIBBFS} +OBJS += obj/tests/ptyx.o + bin/tests/xtouch: obj/tests/xtouch.o ${LIBBFS} OBJS += obj/tests/xtouch.o diff --git a/bench/ioq.c b/bench/ioq.c index 5db585a..61c9714 100644 --- a/bench/ioq.c +++ b/bench/ioq.c @@ -177,9 +177,9 @@ int main(int argc, char *argv[]) { setlocale(LC_ALL, ""); // -d: queue depth - long depth = 4096; + unsigned int depth = 4096; // -j: threads - long threads = 0; + unsigned int threads = 0; // -t: timeout double timeout = 5.0; // -L|-H: ioq_nop() type @@ -190,13 +190,13 @@ int main(int argc, char *argv[]) { while (c = getopt(argc, argv, ":d:j:t:LH"), c != -1) { switch (c) { case 'd': - if (xstrtol(optarg, NULL, 10, &depth) != 0) { + if (xstrtoui(optarg, NULL, 10, &depth) != 0) { fprintf(stderr, "%s: Bad depth '%s': %s\n", cmd, optarg, errstr()); return EXIT_FAILURE; } break; case 'j': - if (xstrtol(optarg, NULL, 10, &threads) != 0) { + if (xstrtoui(optarg, NULL, 10, &threads) != 0) { fprintf(stderr, "%s: Bad thread count '%s': %s\n", cmd, optarg, errstr()); return EXIT_FAILURE; } @@ -222,7 +222,7 @@ int main(int argc, char *argv[]) { } } - if (threads <= 0) { + if (!threads) { threads = nproc(); if (threads > 8) { threads = 8; @@ -238,8 +238,8 @@ int main(int argc, char *argv[]) { printf("I/O queue benchmark (%s)\n\n", bfs_version); - printf("[-d] depth: %ld\n", depth); - printf("[-j] threads: %ld (including main)\n", threads + 1); + printf("[-d] depth: %u\n", depth); + printf("[-j] threads: %u (including main)\n", threads + 1); if (type == IOQ_NOP_HEAVY) { printf("[-H] type: heavy (with syscalls)\n"); } else { @@ -252,7 +252,7 @@ int main(int argc, char *argv[]) { fflush(stdout); struct ioq *ioq = ioq_create(depth, threads); - bfs_everify(ioq, "ioq_create(%ld, %ld)", depth, threads); + bfs_everify(ioq, "ioq_create(%u, %u)", depth, threads); // Pre-allocate all the requests while (ioq_capacity(ioq) > 0) { diff --git a/build/has/tcsetwinsize.c b/build/has/tcsetwinsize.c new file mode 100644 index 0000000..6717415 --- /dev/null +++ b/build/has/tcsetwinsize.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <termios.h> + +int main(void) { + const struct winsize ws = {0}; + return tcsetwinsize(0, &ws); +} diff --git a/build/header.mk b/build/header.mk index 0e3af14..13672ba 100644 --- a/build/header.mk +++ b/build/header.mk @@ -56,6 +56,7 @@ HEADERS := \ gen/has/string-to-flags.h \ gen/has/strtofflags.h \ gen/has/tcgetwinsize.h \ + gen/has/tcsetwinsize.h \ gen/has/timegm.h \ gen/has/timer-create.h \ gen/has/tm-gmtoff.h \ @@ -87,7 +87,9 @@ invalid() { # Get the number of cores to use nproc() { { - command nproc \ + # Run command nproc in a subshell to work around a bash 3 bug + # https://stackoverflow.com/q/68143965 + (command nproc) \ || sysctl -n hw.ncpu \ || getconf _NPROCESSORS_ONLN \ || echo 1 @@ -227,7 +229,7 @@ for arg; do done # Set up symbolic links for out-of-tree builds -for f in Makefile build completions docs src tests; do +for f in Makefile bench build completions docs src tests; do test -e "$f" || ln -s "$DIR/$f" "$f" done @@ -18,7 +18,6 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sys/ioctl.h> #include <termios.h> #include <unistd.h> @@ -33,25 +32,14 @@ struct bfs_bar { /** Get the terminal size, if possible. */ static int bfs_bar_getsize(struct bfs_bar *bar) { -#if BFS_HAS_TCGETWINSIZE || defined(TIOCGWINSZ) struct winsize ws; - -# if BFS_HAS_TCGETWINSIZE - int ret = tcgetwinsize(bar->fd, &ws); -# else - int ret = ioctl(bar->fd, TIOCGWINSZ, &ws); -# endif - if (ret != 0) { - return ret; + if (xtcgetwinsize(bar->fd, &ws) != 0) { + return -1; } store(&bar->width, ws.ws_col, relaxed); store(&bar->height, ws.ws_row, relaxed); return 0; -#else - errno = ENOTSUP; - return -1; -#endif } /** Write a string to the status bar (async-signal-safe). */ diff --git a/src/bfstd.c b/src/bfstd.c index 82663eb..b78af7a 100644 --- a/src/bfstd.c +++ b/src/bfstd.c @@ -23,10 +23,12 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/ioctl.h> #include <sys/resource.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> +#include <termios.h> #include <unistd.h> #include <wchar.h> @@ -187,16 +189,6 @@ char *xgetdelim(FILE *file, char delim) { } } -int open_cterm(int flags) { - char path[L_ctermid]; - if (ctermid(path) == NULL || strlen(path) == 0) { - errno = ENOTTY; - return -1; - } - - return open(path, flags); -} - const char *xgetprogname(void) { const char *cmd = NULL; #if BFS_HAS_GETPROGNAME @@ -243,6 +235,36 @@ static int xstrtox_epilogue(const char *str, char **end, char *endp) { return 0; } +int xstrtos(const char *str, char **end, int base, short *value) { + long n; + if (xstrtol(str, end, base, &n) != 0) { + return -1; + } + + if (n < SHRT_MIN || n > SHRT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +int xstrtoi(const char *str, char **end, int base, int *value) { + long n; + if (xstrtol(str, end, base, &n) != 0) { + return -1; + } + + if (n < INT_MIN || n > INT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + int xstrtol(const char *str, char **end, int base, long *value) { if (xstrtox_prologue(str) != 0) { return -1; @@ -283,6 +305,70 @@ int xstrtod(const char *str, char **end, double *value) { return xstrtox_epilogue(str, end, endp); } +int xstrtous(const char *str, char **end, int base, unsigned short *value) { + unsigned long n; + if (xstrtoul(str, end, base, &n) != 0) { + return -1; + } + + if (n > USHRT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +int xstrtoui(const char *str, char **end, int base, unsigned int *value) { + unsigned long n; + if (xstrtoul(str, end, base, &n) != 0) { + return -1; + } + + if (n > UINT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +/** Common epilogue for xstrtou*() wrappers. */ +static int xstrtoux_epilogue(const char *str, char **end, char *endp) { + if (xstrtox_epilogue(str, end, endp) != 0) { + return -1; + } + + if (str[0] == '-') { + errno = ERANGE; + return -1; + } + + return 0; +} + +int xstrtoul(const char *str, char **end, int base, unsigned long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtoul(str, &endp, base); + return xstrtoux_epilogue(str, end, endp); +} + +int xstrtoull(const char *str, char **end, int base, unsigned long long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtoull(str, &endp, base); + return xstrtoux_epilogue(str, end, endp); +} + /** Compile and execute a regular expression for xrpmatch(). */ static int xrpregex(nl_item item, const char *response) { const char *pattern = nl_langinfo(item); @@ -558,6 +644,32 @@ pid_t xwaitpid(pid_t pid, int *status, int flags) { return ret; } +int open_cterm(int flags) { + char path[L_ctermid]; + if (ctermid(path) == NULL || strlen(path) == 0) { + errno = ENOTTY; + return -1; + } + + return open(path, flags); +} + +int xtcgetwinsize(int fd, struct winsize *ws) { +#if BFS_HAS_TCGETWINSIZE + return tcgetwinsize(fd, ws); +#else + return ioctl(fd, TIOCGWINSZ, ws); +#endif +} + +int xtcsetwinsize(int fd, const struct winsize *ws) { +#if BFS_HAS_TCSETWINSIZE + return tcsetwinsize(fd, ws); +#else + return ioctl(fd, TIOCSWINSZ, ws); +#endif +} + int dup_cloexec(int fd) { #ifdef F_DUPFD_CLOEXEC return fcntl(fd, F_DUPFD_CLOEXEC, 0); diff --git a/src/bfstd.h b/src/bfstd.h index 28f473e..15dd949 100644 --- a/src/bfstd.h +++ b/src/bfstd.h @@ -158,16 +158,6 @@ FILE *xfopen(const char *path, int flags); */ char *xgetdelim(FILE *file, char delim); -/** - * Open the controlling terminal. - * - * @flags - * The open() flags. - * @return - * An open file descriptor, or -1 on failure. - */ -int open_cterm(int flags); - // #include <stdlib.h> /** @@ -179,6 +169,16 @@ int open_cterm(int flags); const char *xgetprogname(void); /** + * Like xstrtol(), but for short. + */ +int xstrtos(const char *str, char **end, int base, short *value); + +/** + * Like xstrtol(), but for int. + */ +int xstrtoi(const char *str, char **end, int base, int *value); + +/** * Wrapper for strtol() that forbids leading spaces. */ int xstrtol(const char *str, char **end, int base, long *value); @@ -189,6 +189,26 @@ int xstrtol(const char *str, char **end, int base, long *value); int xstrtoll(const char *str, char **end, int base, long long *value); /** + * Like xstrtoul(), but for unsigned short. + */ +int xstrtous(const char *str, char **end, int base, unsigned short *value); + +/** + * Like xstrtoul(), but for unsigned int. + */ +int xstrtoui(const char *str, char **end, int base, unsigned int *value); + +/** + * Wrapper for strtoul() that forbids leading spaces, negatives. + */ +int xstrtoul(const char *str, char **end, int base, unsigned long *value); + +/** + * Wrapper for strtoull() that forbids leading spaces, negatives. + */ +int xstrtoull(const char *str, char **end, int base, unsigned long long *value); + +/** * Wrapper for strtof() that forbids leading spaces. */ int xstrtof(const char *str, char **end, float *value); @@ -342,6 +362,29 @@ int xminor(dev_t dev); */ pid_t xwaitpid(pid_t pid, int *status, int flags); +#include <sys/ioctl.h> // May be necessary for struct winsize +#include <termios.h> + +/** + * Open the controlling terminal. + * + * @flags + * The open() flags. + * @return + * An open file descriptor, or -1 on failure. + */ +int open_cterm(int flags); + +/** + * tcgetwinsize()/ioctl(TIOCGWINSZ) wrapper. + */ +int xtcgetwinsize(int fd, struct winsize *ws); + +/** + * tcsetwinsize()/ioctl(TIOCSWINSZ) wrapper. + */ +int xtcsetwinsize(int fd, const struct winsize *ws); + // #include <unistd.h> /** diff --git a/src/color.c b/src/color.c index 0cc950b..588dbac 100644 --- a/src/color.c +++ b/src/color.c @@ -1240,6 +1240,33 @@ static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) { _printf(2, 3) static int cbuff(CFILE *cfile, const char *format, ...); +/** Print an expression's name, for diagnostics. */ +static int print_expr_name(CFILE *cfile, const struct bfs_expr *expr) { + switch (expr->kind) { + case BFS_FLAG: + return cbuff(cfile, "${cyn}%pq${rs}", expr->argv[0]); + case BFS_OPERATOR: + return cbuff(cfile, "${red}%pq${rs}", expr->argv[0]); + default: + return cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]); + } +} + +/** Print an expression's args, for diagnostics. */ +static int print_expr_args(CFILE *cfile, const struct bfs_expr *expr) { + if (print_expr_name(cfile, expr) != 0) { + return -1; + } + + for (size_t i = 1; i < expr->argc; ++i) { + if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) { + return -1; + } + } + + return 0; +} + /** Dump a parsed expression tree, for debugging. */ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, int depth) { if (depth >= 2) { @@ -1254,28 +1281,10 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i return -1; } - int ret; - switch (expr->kind) { - case BFS_FLAG: - ret = cbuff(cfile, "${cyn}%pq${rs}", expr->argv[0]); - break; - case BFS_OPERATOR: - ret = cbuff(cfile, "${red}%pq${rs}", expr->argv[0]); - break; - default: - ret = cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]); - break; - } - if (ret < 0) { + if (print_expr_args(cfile, expr) != 0) { return -1; } - for (size_t i = 1; i < expr->argc; ++i) { - if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) { - return -1; - } - } - if (verbose) { double rate = 0.0, time = 0.0; if (expr->evaluations) { @@ -1413,6 +1422,16 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { return -1; } break; + case 'x': + if (print_expr_args(cfile, va_arg(args, const struct bfs_expr *)) != 0) { + return -1; + } + break; + case 'X': + if (print_expr_name(cfile, va_arg(args, const struct bfs_expr *)) != 0) { + return -1; + } + break; default: goto invalid; diff --git a/src/color.h b/src/color.h index 2394af2..aac8b33 100644 --- a/src/color.h +++ b/src/color.h @@ -95,6 +95,8 @@ int cfclose(CFILE *cfile); * %pL: A colored link target, from a const struct BFTW * argument * %pe: Dump a const struct bfs_expr *, for debugging. * %pE: Dump a const struct bfs_expr * in verbose form, for debugging. + * %px: Print a const struct bfs_expr * with syntax highlighting. + * %pX: Print the name of a const struct bfs_expr *, without arguments. * %%: A literal '%' * ${cc}: Change the color to 'cc' * $$: A literal '$' @@ -286,6 +286,7 @@ int bfs_ctx_free(struct bfs_ctx *ctx) { } free(ctx->paths); + free(ctx->kinds); free(ctx->argv); free(ctx); } @@ -29,6 +29,8 @@ struct bfs_ctx { size_t argc; /** The unparsed command line arguments. */ char **argv; + /** The argument token kinds. */ + enum bfs_kind *kinds; /** The root paths. */ const char **paths; @@ -408,7 +408,7 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c if (expr->eval_fn == eval_exec) { if (bfs_exec_finish(expr->exec) != 0) { if (errno != 0) { - bfs_error(ctx, "%s %s: %s.\n", expr->argv[0], expr->argv[1], errstr()); + bfs_error(ctx, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr()); } ret = -1; } @@ -429,7 +429,7 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state) { bool ret = bfs_exec(expr->exec, state->ftwbuf) == 0; if (errno != 0) { - eval_error(state, "%s %s: %s.\n", expr->argv[0], expr->argv[1], errstr()); + eval_error(state, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr()); } return ret; } @@ -19,6 +19,9 @@ * Argument/token/expression kinds. */ enum bfs_kind { + /** A regular argument. */ + BFS_ARG, + /** A flag (-H, -L, etc.). */ BFS_FLAG, @@ -82,11 +82,9 @@ #ifndef BFS_LIST_H #define BFS_LIST_H -#include "bfs.h" #include "diag.h" #include <stddef.h> -#include <stdint.h> #include <string.h> /** @@ -374,24 +372,19 @@ #define SLIST_REMOVE_(list, cursor, ...) \ SLIST_REMOVE__((list), (cursor), LIST_NEXT_(__VA_ARGS__)) -// Scratch variables for the type-safe SLIST_REMOVE() implementation. -// Not a pointer type due to https://github.com/llvm/llvm-project/issues/109718. -_maybe_unused -static thread_local uintptr_t slist_prev_, slist_next_; - -/** Suppress -Wunused-value. */ -_maybe_unused -static inline void *slist_cast_(uintptr_t ptr) { - return (void *)ptr; -} - #define SLIST_REMOVE__(list, cursor, next) \ - (slist_prev_ = (uintptr_t)(void *)*cursor, \ - slist_next_ = (uintptr_t)(void *)(*cursor)->next, \ - (*cursor)->next = NULL, \ - *cursor = (void *)slist_next_, \ - list->tail = *cursor ? list->tail : cursor, \ - slist_cast_(slist_prev_)) + (list->tail = (*cursor)->next ? list->tail : cursor, \ + slist_remove_(*cursor, cursor, &(*cursor)->next, sizeof(*cursor))) + +// Helper for SLIST_REMOVE() +static inline void *slist_remove_(void *ret, void *cursor, void *next, size_t size) { + // ret = *cursor; + // *cursor = ret->next; + memcpy(cursor, next, size); + // ret->next = NULL; + memset(next, 0, size); + return ret; +} /** * Pop the head off a singly-linked list. diff --git a/src/parse.c b/src/parse.c index 42f71cc..9c39d6b 100644 --- a/src/parse.c +++ b/src/parse.c @@ -84,8 +84,6 @@ struct bfs_parser { enum use_color use_color; /** Whether a -print action is implied. */ bool implicit_print; - /** Whether the default root "." should be used. */ - bool implicit_root; /** Whether the expression has started. */ bool expr_started; /** Whether an information option like -help or -version was passed. */ @@ -95,20 +93,20 @@ struct bfs_parser { /** The last non-path argument. */ char **last_arg; - /** A "-depth"-type argument, if any. */ - char **depth_arg; - /** A "-limit" argument, if any. */ - char **limit_arg; - /** A "-prune" argument, if any. */ - char **prune_arg; - /** A "-mount" argument, if any. */ - char **mount_arg; - /** An "-xdev" argument, if any. */ - char **xdev_arg; - /** A "-files0-from -" argument, if any. */ - char **files0_stdin_arg; - /** An "-ok"-type expression, if any. */ - const struct bfs_expr *ok_expr; + /** A "-depth"-type expression, if any. */ + const struct bfs_expr *depth_expr; + /** A "-limit" expression, if any. */ + const struct bfs_expr *limit_expr; + /** A "-prune" expression, if any. */ + const struct bfs_expr *prune_expr; + /** A "-mount" expression, if any. */ + const struct bfs_expr *mount_expr; + /** An "-xdev" expression, if any. */ + const struct bfs_expr *xdev_expr; + /** A "-files0-from" expression, if any. */ + const struct bfs_expr *files0_expr; + /** An expression that consumes stdin, if any. */ + const struct bfs_expr *stdin_expr; /** The current time (maybe modified by -daystart). */ struct timespec now; @@ -176,14 +174,14 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_ /** * Print an error about conflicting command line arguments. */ -_printf(6, 7) -static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { +_printf(4, 5) +static void parse_conflict_error(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) { const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); - highlight_args(ctx, argv1, argc1, highlight); - highlight_args(ctx, argv2, argc2, highlight); + highlight_args(ctx, expr1->argv, expr1->argc, highlight); + highlight_args(ctx, expr2->argv, expr2->argc, highlight); bfs_argv_error(ctx, highlight); va_list args; @@ -231,14 +229,14 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, . /** * Print a warning about conflicting command line arguments. */ -_printf(6, 7) -static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { +_printf(4, 5) +static bool parse_conflict_warning(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) { const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); - highlight_args(ctx, argv1, argc1, highlight); - highlight_args(ctx, argv2, argc2, highlight); + highlight_args(ctx, expr1->argv, expr1->argc, highlight); + highlight_args(ctx, expr2->argv, expr2->argc, highlight); if (!bfs_argv_warning(ctx, highlight)) { return false; } @@ -269,6 +267,21 @@ static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs } /** + * Report an error if stdin is already consumed, then consume it. + */ +static bool consume_stdin(struct bfs_parser *parser, const struct bfs_expr *expr) { + if (parser->stdin_expr) { + parse_conflict_error(parser, parser->stdin_expr, expr, + "%pX and %pX can't both use standard input.\n", + parser->stdin_expr, expr); + return false; + } + + parser->stdin_expr = expr; + return true; +} + +/** * Allocate a new expression. */ static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv, enum bfs_kind kind) { @@ -383,6 +396,8 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser); * Advance by a single token. */ static char **parser_advance(struct bfs_parser *parser, enum bfs_kind kind, size_t argc) { + struct bfs_ctx *ctx = parser->ctx; + if (kind != BFS_FLAG && kind != BFS_PATH) { parser->expr_started = true; } @@ -391,6 +406,9 @@ static char **parser_advance(struct bfs_parser *parser, enum bfs_kind kind, size parser->last_arg = parser->argv; } + size_t i = parser->argv - ctx->argv; + ctx->kinds[i] = kind; + char **argv = parser->argv; parser->argv += argc; return argv; @@ -414,7 +432,6 @@ static int parse_root(struct bfs_parser *parser, const char *path) { return -1; } - parser->implicit_root = false; return 0; } @@ -1158,22 +1175,33 @@ static struct bfs_expr *parse_daystart(struct bfs_parser *parser, int arg1, int * Parse -delete. */ static struct bfs_expr *parse_delete(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(parser, eval_delete); + if (!expr) { + return NULL; + } + struct bfs_ctx *ctx = parser->ctx; ctx->flags |= BFTW_POST_ORDER; ctx->dangerous = true; - parser->depth_arg = parser->argv; - - return parse_nullary_action(parser, eval_delete); + parser->depth_expr = expr; + return expr; } /** * Parse -d. */ -static struct bfs_expr *parse_depth(struct bfs_parser *parser, int arg1, int arg2) { +static struct bfs_expr *parse_depth(struct bfs_parser *parser, int flag, int arg2) { + struct bfs_expr *expr = flag + ? parse_nullary_flag(parser) + : parse_nullary_option(parser); + if (!expr) { + return NULL; + } + parser->ctx->flags |= BFTW_POST_ORDER; - parser->depth_arg = parser->argv; - return parse_nullary_flag(parser); + parser->depth_expr = expr; + return expr; } /** @@ -1273,7 +1301,9 @@ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg } if (execbuf->flags & BFS_EXEC_CONFIRM) { - parser->ok_expr = expr; + if (!consume_stdin(parser, expr)) { + return NULL; + } } else { ctx->dangerous = true; } @@ -1304,11 +1334,17 @@ static struct bfs_expr *parse_exit(struct bfs_parser *parser, int arg1, int arg2 * Parse -f PATH. */ static struct bfs_expr *parse_f(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_ctx *ctx = parser->ctx; + struct bfs_expr *expr = parse_unary_flag(parser); if (!expr) { return NULL; } + // Mark the path as a path, not a regular argument + size_t i = expr->argv - ctx->argv; + ctx->kinds[i + 1] = BFS_PATH; + if (parse_root(parser, expr->argv[1]) != 0) { return NULL; } @@ -1325,50 +1361,14 @@ static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, i return NULL; } - const char *from = expr->argv[1]; - - FILE *file; - if (strcmp(from, "-") == 0) { - file = stdin; - } else { - file = xfopen(from, O_RDONLY | O_CLOEXEC); - } - if (!file) { - parse_expr_error(parser, expr, "%s.\n", errstr()); - return NULL; - } - - while (true) { - char *path = xgetdelim(file, '\0'); - if (!path) { - if (errno) { - goto fail; - } else { - break; - } - } - - int ret = parse_root(parser, path); - free(path); - if (ret != 0) { - goto fail; - } - } - - if (file == stdin) { - parser->files0_stdin_arg = expr->argv; - } else { - fclose(file); - } - - parser->implicit_root = false; + // For compatibility with GNU find, + // + // bfs -files0-from a -files0-from b + // + // should *only* use b, not a. So stash the expression here and only + // process the last one at the end of parsing. + parser->files0_expr = expr; return expr; - -fail: - if (file != stdin) { - fclose(file); - } - return NULL; } /** @@ -1638,11 +1638,11 @@ static struct bfs_expr *parse_limit(struct bfs_parser *parser, int arg1, int arg } if (expr->num <= 0) { - parse_expr_error(parser, expr, "The ${blu}%s${rs} must be at least ${bld}1${rs}.\n", expr->argv[0]); + parse_expr_error(parser, expr, "The %pX must be at least ${bld}1${rs}.\n", expr); return NULL; } - parser->limit_arg = expr->argv; + parser->limit_expr = expr; return expr; } @@ -1676,7 +1676,7 @@ static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg } parser->ctx->flags |= BFTW_SKIP_MOUNTS; - parser->mount_arg = expr->argv; + parser->mount_expr = expr; return expr; } @@ -1855,9 +1855,15 @@ static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int * Parse -noleaf. */ static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) { - parse_warning(parser, "${ex}%s${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", - BFS_COMMAND, parser->argv[0]); - return parse_nullary_option(parser); + struct bfs_expr *expr = parse_nullary_option(parser); + if (!expr) { + return NULL; + } + + parse_expr_warning(parser, expr, + "${ex}%s${rs} does not apply the optimization that %px inhibits.\n\n", + BFS_COMMAND, expr); + return expr; } /** @@ -2193,8 +2199,13 @@ static struct bfs_expr *parse_printx(struct bfs_parser *parser, int arg1, int ar * Parse -prune. */ static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) { - parser->prune_arg = parser->argv; - return parse_nullary_action(parser, eval_prune); + struct bfs_expr *expr = parse_nullary_action(parser, eval_prune); + if (!expr) { + return NULL; + } + + parser->prune_expr = expr; + return expr; } /** @@ -2572,9 +2583,14 @@ static struct bfs_expr *parse_xattrname(struct bfs_parser *parser, int arg1, int * Parse -xdev. */ static struct bfs_expr *parse_xdev(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_option(parser); + if (!expr) { + return NULL; + } + parser->ctx->flags |= BFTW_PRUNE_MOUNTS; - parser->xdev_arg = parser->argv; - return parse_nullary_option(parser); + parser->xdev_expr = expr; + return expr; } /** @@ -3048,10 +3064,10 @@ static const struct table_entry parse_table[] = { {"-context", BFS_TEST, parse_context, true}, {"-csince", BFS_TEST, parse_since, BFS_STAT_CTIME}, {"-ctime", BFS_TEST, parse_time, BFS_STAT_CTIME}, - {"-d", BFS_FLAG, parse_depth}, + {"-d", BFS_FLAG, parse_depth, true}, {"-daystart", BFS_OPTION, parse_daystart}, {"-delete", BFS_ACTION, parse_delete}, - {"-depth", BFS_OPTION, parse_depth_n}, + {"-depth", BFS_OPTION, parse_depth_n, false}, {"-empty", BFS_TEST, parse_empty}, {"-exclude", BFS_OPERATOR}, {"-exec", BFS_ACTION, parse_exec, 0}, @@ -3503,6 +3519,73 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser) { return expr; } +/** Handle -files0-from after parsing. */ +static int parse_files0_roots(struct bfs_parser *parser) { + const struct bfs_ctx *ctx = parser->ctx; + const struct bfs_expr *expr = parser->files0_expr; + + if (ctx->npaths > 0) { + bool highlight[ctx->argc]; + init_highlight(ctx, highlight); + highlight_args(ctx, expr->argv, expr->argc, highlight); + + for (size_t i = 0; i < ctx->argc; ++i) { + if (ctx->kinds[i] == BFS_PATH) { + highlight[i] = true; + } + } + + bfs_argv_error(ctx, highlight); + bfs_error(ctx, "Cannot combine %pX with explicit root paths.\n", expr); + return -1; + } + + const char *from = expr->argv[1]; + + FILE *file; + if (strcmp(from, "-") == 0) { + if (!consume_stdin(parser, expr)) { + return -1; + } + file = stdin; + } else { + file = xfopen(from, O_RDONLY | O_CLOEXEC); + } + if (!file) { + parse_expr_error(parser, expr, "%s.\n", errstr()); + return -1; + } + + while (true) { + char *path = xgetdelim(file, '\0'); + if (!path) { + if (errno) { + goto fail; + } else { + break; + } + } + + int ret = parse_root(parser, path); + free(path); + if (ret != 0) { + goto fail; + } + } + + if (file != stdin) { + fclose(file); + } + + return 0; + +fail: + if (file != stdin) { + fclose(file); + } + return -1; +} + /** * Parse the top-level expression. */ @@ -3528,12 +3611,22 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { return NULL; } + if (parser->files0_expr) { + if (parse_files0_roots(parser) != 0) { + return NULL; + } + } else if (ctx->npaths == 0) { + if (parse_root(parser, ".") != 0) { + return NULL; + } + } + if (parser->implicit_print) { - char **limit = parser->limit_arg; + const struct bfs_expr *limit = parser->limit_expr; if (limit) { - parse_argv_error(parser, parser->limit_arg, 2, - "With ${blu}%s${rs}, you must specify an action explicitly; for example, ${blu}-print${rs} ${blu}%s${rs} ${bld}%s${rs}.\n", - limit[0], limit[0], limit[1]); + parse_expr_error(parser, limit, + "With %pX, you must specify an action explicitly; for example, ${blu}-print${rs} %px.\n", + limit, limit); return NULL; } @@ -3549,16 +3642,16 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { } } - if (parser->mount_arg && parser->xdev_arg) { - parse_conflict_warning(parser, parser->mount_arg, 1, parser->xdev_arg, 1, - "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n", - parser->xdev_arg[0], parser->mount_arg[0]); + if (parser->mount_expr && parser->xdev_expr) { + parse_conflict_warning(parser, parser->mount_expr, parser->xdev_expr, + "%px is redundant in the presence of %px.\n\n", + parser->xdev_expr, parser->mount_expr); } - if (ctx->warn && parser->depth_arg && parser->prune_arg) { - parse_conflict_warning(parser, parser->depth_arg, 1, parser->prune_arg, 1, - "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n", - parser->prune_arg[0], parser->depth_arg[0]); + if (ctx->warn && parser->depth_expr && parser->prune_expr) { + parse_conflict_warning(parser, parser->depth_expr, parser->prune_expr, + "%px does not work in the presence of %px.\n", + parser->prune_expr, parser->depth_expr); if (ctx->interactive) { bfs_warning(ctx, "Do you want to continue? "); @@ -3570,13 +3663,6 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { fprintf(stderr, "\n"); } - if (parser->ok_expr && parser->files0_stdin_arg) { - parse_conflict_error(parser, parser->ok_expr->argv, parser->ok_expr->argc, parser->files0_stdin_arg, 2, - "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n", - parser->ok_expr->argv[0], parser->files0_stdin_arg[0], parser->files0_stdin_arg[1]); - return NULL; - } - return expr; } @@ -3758,6 +3844,12 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { goto fail; } + ctx->kinds = ZALLOC_ARRAY(enum bfs_kind, argc); + if (!ctx->kinds) { + perror("zalloc()"); + goto fail; + } + enum use_color use_color = COLOR_AUTO; const char *no_color = getenv("NO_COLOR"); if (no_color && *no_color) { @@ -3806,16 +3898,14 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { .stdout_tty = stdout_tty, .use_color = use_color, .implicit_print = true, - .implicit_root = true, .just_info = false, .excluding = false, .last_arg = NULL, - .depth_arg = NULL, - .prune_arg = NULL, - .mount_arg = NULL, - .xdev_arg = NULL, - .files0_stdin_arg = NULL, - .ok_expr = NULL, + .depth_expr = NULL, + .prune_expr = NULL, + .mount_expr = NULL, + .xdev_expr = NULL, + .stdin_expr = NULL, .now = ctx->now, }; @@ -3844,12 +3934,6 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { goto fail; } - if (ctx->npaths == 0 && parser.implicit_root) { - if (parse_root(&parser, ".") != 0) { - goto fail; - } - } - if ((ctx->flags & BFTW_FOLLOW_ALL) && !ctx->unique) { // We need bftw() to detect cycles unless -unique does it for us ctx->flags |= BFTW_DETECT_CYCLES; diff --git a/tests/bfs/files0_from_root.sh b/tests/bfs/files0_from_root.sh new file mode 100644 index 0000000..6ba5f00 --- /dev/null +++ b/tests/bfs/files0_from_root.sh @@ -0,0 +1,2 @@ +printf 'basic\0' >"$TEST/input" +! invoke_bfs basic -files0-from "$TEST/input" diff --git a/tests/bfstd.c b/tests/bfstd.c index a43783a..6e15e2b 100644 --- a/tests/bfstd.c +++ b/tests/bfstd.c @@ -6,35 +6,15 @@ #include "bfstd.h" #include "diag.h" +#include <errno.h> #include <langinfo.h> +#include <limits.h> +#include <stdint.h> #include <stdlib.h> #include <string.h> -/** Check the result of xdirname()/xbasename(). */ -static void check_base_dir(const char *path, const char *dir, const char *base) { - char *xdir = xdirname(path); - bfs_everify(xdir, "xdirname()"); - bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir); - free(xdir); - - char *xbase = xbasename(path); - bfs_everify(xbase, "xbasename()"); - bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base); - free(xbase); -} - -/** Check the result of wordesc(). */ -static void check_wordesc(const char *str, const char *exp, enum wesc_flags flags) { - char buf[256]; - char *end = buf + sizeof(buf); - char *esc = wordesc(buf, end, str, flags); - - if (bfs_check(esc != end)) { - bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp); - } -} - -void check_bfstd(void) { +/** asciilen() test cases. */ +static void check_asciilen(void) { bfs_check(asciilen("") == 0); bfs_check(asciilen("@") == 1); bfs_check(asciilen("@@") == 2); @@ -49,7 +29,23 @@ void check_bfstd(void) { bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7); bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8); bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9); +} + +/** Check the result of xdirname()/xbasename(). */ +static void check_base_dir(const char *path, const char *dir, const char *base) { + char *xdir = xdirname(path); + bfs_everify(xdir, "xdirname()"); + bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir); + free(xdir); + char *xbase = xbasename(path); + bfs_everify(xbase, "xbasename()"); + bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base); + free(xbase); +} + +/** xdirname()/xbasename() test cases. */ +static void check_basedirs(void) { // From man 3p basename check_base_dir("usr", ".", "usr"); check_base_dir("usr/", ".", "usr"); @@ -61,7 +57,21 @@ void check_bfstd(void) { check_base_dir("/usr/lib", "/usr", "lib"); check_base_dir("//usr//lib//", "//usr", "lib"); check_base_dir("/home//dwc//test", "/home//dwc", "test"); +} + +/** Check the result of wordesc(). */ +static void check_wordesc(const char *str, const char *exp, enum wesc_flags flags) { + char buf[256]; + char *end = buf + sizeof(buf); + char *esc = wordesc(buf, end, str, flags); + if (bfs_check(esc != end)) { + bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp); + } +} + +/** wordesc() test cases. */ +static void check_wordescs(void) { check_wordesc("", "\"\"", WESC_SHELL); check_wordesc("word", "word", WESC_SHELL); check_wordesc("two words", "\"two words\"", WESC_SHELL); @@ -80,7 +90,123 @@ void check_bfstd(void) { check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY); check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL); } +} + +/** xstrto*() test cases. */ +static void check_strtox(void) { + short s; + unsigned short us; + int i; + unsigned int ui; + long l; + unsigned long ul; + long long ll; + unsigned long long ull; + char *end; + +#define check_strtouerr(err, str, end, base) \ + do { \ + bfs_echeck(xstrtous(str, end, base, &us) != 0 && errno == err); \ + bfs_echeck(xstrtoui(str, end, base, &ui) != 0 && errno == err); \ + bfs_echeck(xstrtoul(str, end, base, &ul) != 0 && errno == err); \ + bfs_echeck(xstrtoull(str, end, base, &ull) != 0 && errno == err); \ + } while (0) + + check_strtouerr(ERANGE, "-1", NULL, 0); + check_strtouerr(ERANGE, "-0x1", NULL, 0); + check_strtouerr(EINVAL, "-", NULL, 0); + check_strtouerr(EINVAL, "-q", NULL, 0); + check_strtouerr(EINVAL, "-1q", NULL, 0); + check_strtouerr(EINVAL, "-0x", NULL, 0); + +#define check_strtoerr(err, str, end, base) \ + do { \ + bfs_echeck(xstrtos(str, end, base, &s) != 0 && errno == err); \ + bfs_echeck(xstrtoi(str, end, base, &i) != 0 && errno == err); \ + bfs_echeck(xstrtol(str, end, base, &l) != 0 && errno == err); \ + bfs_echeck(xstrtoll(str, end, base, &ll) != 0 && errno == err); \ + check_strtouerr(err, str, end, base); \ + } while (0) + + check_strtoerr(EINVAL, "", NULL, 0); + check_strtoerr(EINVAL, "", &end, 0); + check_strtoerr(EINVAL, " 1 ", &end, 0); + check_strtoerr(EINVAL, " -1", NULL, 0); + check_strtoerr(EINVAL, " 123", NULL, 0); + check_strtoerr(EINVAL, "123 ", NULL, 0); + check_strtoerr(EINVAL, "0789", NULL, 0); + check_strtoerr(EINVAL, "789A", NULL, 0); + check_strtoerr(EINVAL, "0x", NULL, 0); + check_strtoerr(EINVAL, "0x789A", NULL, 10); + check_strtoerr(EINVAL, "0x-1", NULL, 0); + +#define check_strtotype(type, min, max, fmt, fn, str, base, v, n) \ + do { \ + if ((n) >= min && (n) <= max) { \ + bfs_echeck(fn(str, NULL, base, &v) == 0); \ + bfs_check(v == (type)(n), "%s('%s') == " fmt " (!= " fmt ")", #fn, str, v, (type)(n)); \ + } else { \ + bfs_echeck(fn(str, NULL, base, &v) != 0 && errno == ERANGE); \ + } \ + } while (0) + +#define check_strtoint(str, base, n) \ + do { \ + check_strtotype( signed short, SHRT_MIN, SHRT_MAX, "%d", xstrtos, str, base, s, n); \ + check_strtotype( signed int, INT_MIN, INT_MAX, "%d", xstrtoi, str, base, i, n); \ + check_strtotype( signed long, LONG_MIN, LONG_MAX, "%ld", xstrtol, str, base, l, n); \ + check_strtotype( signed long long, LLONG_MIN, LLONG_MAX, "%lld", xstrtoll, str, base, ll, n); \ + check_strtotype(unsigned short, 0, USHRT_MAX, "%u", xstrtous, str, base, us, n); \ + check_strtotype(unsigned int, 0, UINT_MAX, "%u", xstrtoui, str, base, ui, n); \ + check_strtotype(unsigned long, 0, ULONG_MAX, "%lu", xstrtoul, str, base, ul, n); \ + check_strtotype(unsigned long long, 0, ULLONG_MAX, "%llu", xstrtoull, str, base, ull, n); \ + } while (0) + + check_strtoint("123", 0, 123); + check_strtoint("+123", 0, 123); + check_strtoint("-123", 0, -123); + + check_strtoint("0123", 0, 0123); + check_strtoint("0x789A", 0, 0x789A); + + check_strtoint("0123", 10, 123); + check_strtoint("0789", 10, 789); + + check_strtoint("123", 16, 0x123); + + check_strtoint("0x7FFF", 0, 0x7FFF); + check_strtoint("-0x8000", 0, -0x8000); + + check_strtoint("0x7FFFFFFF", 0, 0x7FFFFFFFL); + check_strtoint("-0x80000000", 0, -0x7FFFFFFFL - 1); + + check_strtoint("0x7FFFFFFFFFFFFFFF", 0, 0x7FFFFFFFFFFFFFFFLL); + check_strtoint("-0x8000000000000000", 0, -0x7FFFFFFFFFFFFFFFLL - 1); + +#define check_strtoend(str, estr, base, n) \ + do { \ + bfs_echeck(xstrtoll(str, &end, base, &ll) == 0); \ + bfs_check(ll == (n), "xstrtoll('%s') == %lld (!= %lld)", str, ll, (long long)(n)); \ + bfs_check(strcmp(end, estr) == 0, "xstrtoll('%s'): end == '%s' (!= '%s')", str, end, estr); \ + } while (0) + + check_strtoend("123 ", " ", 0, 123); + check_strtoend("0789", "89", 0, 07); + check_strtoend("789A", "A", 0, 789); + check_strtoend("0xDEFG", "G", 0, 0xDEF); +} + +/** xstrwidth() test cases. */ +static void check_strwidth(void) { bfs_check(xstrwidth("Hello world") == 11); bfs_check(xstrwidth("Hello\1world") == 10); } + +void check_bfstd(void) { + check_asciilen(); + check_basedirs(); + check_wordescs(); + check_strtox(); + check_strwidth(); +} diff --git a/tests/gnu/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh index 85eee8f..7b42772 100644 --- a/tests/gnu/files0_from_empty.sh +++ b/tests/gnu/files0_from_empty.sh @@ -1 +1 @@ -! printf "\0" | invoke_bfs -files0-from - +! printf '\0' | invoke_bfs -files0-from - diff --git a/tests/gnu/files0_from_file_file.out b/tests/gnu/files0_from_file_file.out new file mode 100644 index 0000000..fb683c7 --- /dev/null +++ b/tests/gnu/files0_from_file_file.out @@ -0,0 +1,2 @@ +basic/g +basic/g/h diff --git a/tests/gnu/files0_from_file_file.sh b/tests/gnu/files0_from_file_file.sh new file mode 100644 index 0000000..1119952 --- /dev/null +++ b/tests/gnu/files0_from_file_file.sh @@ -0,0 +1,3 @@ +printf 'basic/c\0' >"$TEST/in1" +printf 'basic/g\0' >"$TEST/in2" +bfs_diff -files0-from "$TEST/in1" -files0-from "$TEST/in2" diff --git a/tests/gnu/files0_from_ok.sh b/tests/gnu/files0_from_ok.sh deleted file mode 100644 index 8e145ce..0000000 --- a/tests/gnu/files0_from_ok.sh +++ /dev/null @@ -1 +0,0 @@ -! printf "basic\0" | invoke_bfs -files0-from - -ok echo {} \; diff --git a/tests/gnu/files0_from_stdin_ok.sh b/tests/gnu/files0_from_stdin_ok.sh new file mode 100644 index 0000000..0283c8d --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok.sh @@ -0,0 +1 @@ +! printf 'basic\0' | invoke_bfs -files0-from - -ok echo {} \; diff --git a/tests/gnu/files0_from_stdin_ok_file.out b/tests/gnu/files0_from_stdin_ok_file.out new file mode 100644 index 0000000..0f6b00d --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok_file.out @@ -0,0 +1,45 @@ + + + + + + /j + /j +! +!- +!-/e +!-/e +!/d +!/d +( +(- +(-/c +(-/c +(/b +(/b +) +)/g +)/g +* +*/m +*/m +, +,/f +,/f +- +-/a +-/a +... +.../h +.../h +/n +/n +[ +[/k +[/k +\ +\/i +\/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_stdin_ok_file.sh b/tests/gnu/files0_from_stdin_ok_file.sh new file mode 100644 index 0000000..028df0c --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok_file.sh @@ -0,0 +1,4 @@ +FILE="$TMP/$TEST.in" +cd weirdnames +invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0" +yes | bfs_diff -files0-from - -ok printf '%s\n' {} \; -files0-from "$FILE" diff --git a/tests/gnu/files0_from_stdin_stdin.out b/tests/gnu/files0_from_stdin_stdin.out new file mode 100644 index 0000000..0f6b00d --- /dev/null +++ b/tests/gnu/files0_from_stdin_stdin.out @@ -0,0 +1,45 @@ + + + + + + /j + /j +! +!- +!-/e +!-/e +!/d +!/d +( +(- +(-/c +(-/c +(/b +(/b +) +)/g +)/g +* +*/m +*/m +, +,/f +,/f +- +-/a +-/a +... +.../h +.../h +/n +/n +[ +[/k +[/k +\ +\/i +\/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_stdin_stdin.sh b/tests/gnu/files0_from_stdin_stdin.sh new file mode 100644 index 0000000..8f6368f --- /dev/null +++ b/tests/gnu/files0_from_stdin_stdin.sh @@ -0,0 +1,2 @@ +cd weirdnames +invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from - -files0-from - diff --git a/tests/gnu/follow_files0_from.out b/tests/gnu/follow_files0_from.out new file mode 100644 index 0000000..c77d546 --- /dev/null +++ b/tests/gnu/follow_files0_from.out @@ -0,0 +1,42 @@ +links +links/broken +links/broken +links/deeply +links/deeply +links/deeply/nested +links/deeply/nested +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/link +links/deeply/nested/link +links/deeply/nested/link +links/deeply/nested/link +links/file +links/file +links/hardlink +links/hardlink +links/notdir +links/notdir +links/skip +links/skip +links/skip/broken +links/skip/broken +links/skip/dir +links/skip/dir +links/skip/file +links/skip/file +links/skip/link +links/skip/link +links/symlink +links/symlink diff --git a/tests/gnu/follow_files0_from.sh b/tests/gnu/follow_files0_from.sh new file mode 100644 index 0000000..8c20f6d --- /dev/null +++ b/tests/gnu/follow_files0_from.sh @@ -0,0 +1 @@ +invoke_bfs links -print0 | bfs_diff -follow -files0-from - diff --git a/tests/gnu/ok_files0_from_stdin.sh b/tests/gnu/ok_files0_from_stdin.sh new file mode 100644 index 0000000..2c4de7b --- /dev/null +++ b/tests/gnu/ok_files0_from_stdin.sh @@ -0,0 +1 @@ +! printf 'basic\0' | invoke_bfs -ok echo {} \; -files0-from - diff --git a/tests/gnu/ok_flush.sh b/tests/gnu/ok_flush.sh index 87c7298..a5dc0d0 100644 --- a/tests/gnu/ok_flush.sh +++ b/tests/gnu/ok_flush.sh @@ -1,4 +1,4 @@ # I/O streams should be flushed before -ok prompts -yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | tr '\0' ' ' | sed 's/?.*?/?/' >"$OUT" +yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | sed 's/?.*?/?/' >"$OUT" sort_output diff_output diff --git a/tests/main.c b/tests/main.c index 4c770bd..9240e1c 100644 --- a/tests/main.c +++ b/tests/main.c @@ -222,15 +222,15 @@ int main(int argc, char *argv[]) { } tzset(); - long jobs = 0; + unsigned int jobs = 0; const char *cmd = argc > 0 ? argv[0] : "units"; int c; while (c = getopt(argc, argv, ":j:"), c != -1) { switch (c) { case 'j': - if (xstrtol(optarg, NULL, 10, &jobs) != 0 || jobs <= 0) { - fprintf(stderr, "%s: Bad job count '%s'\n", cmd, optarg); + if (xstrtoui(optarg, NULL, 10, &jobs) != 0) { + fprintf(stderr, "%s: Bad job count '%s': %s\n", cmd, optarg, errstr()); return EXIT_FAILURE; } break; @@ -243,7 +243,7 @@ int main(int argc, char *argv[]) { } } - if (jobs == 0) { + if (!jobs) { jobs = nproc(); } diff --git a/tests/ptyx.c b/tests/ptyx.c new file mode 100644 index 0000000..59292df --- /dev/null +++ b/tests/ptyx.c @@ -0,0 +1,252 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Execute a command in a pseudo-terminal. + * + * $ ptyx [-w WIDTH] [-h HEIGHT] [--] COMMAND [ARGS...] + */ + +#include "bfs.h" +#include "bfstd.h" + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <termios.h> +#include <unistd.h> + +#if __has_include(<stropts.h>) +# include <stropts.h> +#endif + +#if __sun +/** + * Push a STREAMS module, if it's not already there. + * + * See https://www.illumos.org/issues/9042. + */ +static int i_push(int fd, const char *name) { + int ret = ioctl(fd, I_FIND, name); + if (ret < 0) { + return ret; + } else if (ret == 0) { + return ioctl(fd, I_PUSH, name); + } else { + return 0; + } +} +#endif + +int main(int argc, char *argv[]) { + const char *cmd = argc > 0 ? argv[0] : "ptyx"; + +/** Report an error message and exit. */ +#define die(...) die_(__VA_ARGS__, ) + +#define die_(format, ...) \ + do { \ + fprintf(stderr, "%s: " format "%s", cmd, __VA_ARGS__ "\n"); \ + exit(EXIT_FAILURE); \ + } while (0) + +/** Report an error code and exit. */ +#define edie(...) edie_(__VA_ARGS__, ) + +#define edie_(format, ...) \ + do { \ + fprintf(stderr, "%s: " format ": %s\n", cmd, __VA_ARGS__ errstr()); \ + exit(EXIT_FAILURE); \ + } while (0) + + unsigned short width = 0; + unsigned short height = 0; + + // Parse the command line + int c; + while (c = getopt(argc, argv, "+:w:h:"), c != -1) { + switch (c) { + case 'w': + if (xstrtous(optarg, NULL, 10, &width) != 0) { + edie("Bad width '%s'", optarg); + } + break; + case 'h': + if (xstrtous(optarg, NULL, 10, &height) != 0) { + edie("Bad height '%s'", optarg); + } + break; + case ':': + die("Missing argument to -%c", optopt); + case '?': + die("Unrecognized option -%c", optopt); + } + } + + if (optind >= argc) { + die("Missing command"); + } + char **args = argv + optind; + + // Create a new pty, and set it up + int ptm = posix_openpt(O_RDWR | O_NOCTTY); + if (ptm < 0) { + edie("posix_openpt()"); + } + if (grantpt(ptm) != 0) { + edie("grantpt()"); + } + if (unlockpt(ptm) != 0) { + edie("unlockpt()"); + } + + // Get the subsidiary device path + char *name = ptsname(ptm); + if (!name) { + edie("ptsname()"); + } + + // Open the subsidiary device + int pts = open(name, O_RDWR | O_NOCTTY); + if (pts < 0) { + edie("%s", name); + } + +#if __sun + // On Solaris/illumos, a pty doesn't behave like a terminal until we + // push some STREAMS modules (see ptm(4D), ptem(4M), ldterm(4M)). + if (i_push(pts, "ptem") != 0) { + die("ioctl(I_PUSH, ptem)"); + } + if (i_push(pts, "ldterm") != 0) { + die("ioctl(I_PUSH, ldterm)"); + } +#endif + + // A new pty starts at 0x0, which is not very useful. Instead, grab the + // default size from the current controlling terminal, if possible. + if (!width || !height) { + int tty = open_cterm(O_RDONLY | O_CLOEXEC); + if (tty >= 0) { + struct winsize ws; + if (xtcgetwinsize(tty, &ws) != 0) { + edie("tcgetwinsize()"); + } + if (!width) { + width = ws.ws_col; + } + if (!height) { + height = ws.ws_row; + } + xclose(tty); + } + } + if (!width) { + width = 80; + } + if (!height) { + height = 24; + } + + // Update the pty size + struct winsize ws; + if (xtcgetwinsize(pts, &ws) != 0) { + edie("tcgetwinsize()"); + } + ws.ws_col = width; + ws.ws_row = height; + if (xtcsetwinsize(pts, &ws) != 0) { + edie("tcsetwinsize()"); + } + + // Set custom terminal attributes + struct termios attrs; + if (tcgetattr(pts, &attrs) != 0) { + edie("tcgetattr()"); + } + attrs.c_oflag &= ~OPOST; // Don't convert \n to \r\n + if (tcsetattr(pts, TCSANOW, &attrs) != 0) { + edie("tcsetattr()"); + } + + pid_t pid = fork(); + if (pid < 0) { + edie("fork()"); + } else if (pid == 0) { + // Child + close(ptm); + + // Make ourselves a session leader so we can have our own + // controlling terminal + if (setsid() < 0) { + edie("setsid()"); + } + +#ifdef TIOCSCTTY + // Set the pty as the controlling terminal + if (ioctl(pts, TIOCSCTTY, 0) != 0) { + edie("ioctl(TIOCSCTTY)"); + } +#endif + + // Redirect std{in,out,err} to the pty + if (dup2(pts, STDIN_FILENO) < 0 + || dup2(pts, STDOUT_FILENO) < 0 + || dup2(pts, STDERR_FILENO) < 0) { + edie("dup2()"); + } + if (pts > STDERR_FILENO) { + xclose(pts); + } + + // Run the requested command + execvp(args[0], args); + edie("execvp(): %s", args[0]); + } + + // Parent + xclose(pts); + + // Read output from the pty and copy it to stdout + char buf[1024]; + while (true) { + ssize_t len = read(ptm, buf, sizeof(buf)); + if (len > 0) { + if (xwrite(STDOUT_FILENO, buf, len) < 0) { + edie("write()"); + } + } else if (len == 0) { + break; + } else if (errno == EINTR) { + continue; + } else if (errno == EIO) { + // Linux reports EIO rather than EOF when pts is closed + break; + } else { + die("read()"); + } + } + + xclose(ptm); + + int wstatus; + if (xwaitpid(pid, &wstatus, 0) < 0) { + edie("waitpid()"); + } + + if (WIFEXITED(wstatus)) { + return WEXITSTATUS(wstatus); + } else if (WIFSIGNALED(wstatus)) { + int sig = WTERMSIG(wstatus); + fprintf(stderr, "%s: %s: %s\n", cmd, args[0], strsignal(sig)); + return 128 + sig; + } else { + return 128; + } +} diff --git a/tests/run.sh b/tests/run.sh index e3a4e3f..8d3a5d2 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -362,20 +362,12 @@ invoke_bfs() { fi } -if command -v unbuffer &>/dev/null; then - UNBUFFER=unbuffer -elif command -v expect_unbuffer &>/dev/null; then - UNBUFFER=expect_unbuffer -fi - # Run bfs with a pseudo-terminal attached bfs_pty() { - test -n "${UNBUFFER:-}" || skip - bfs_verbose "$@" local ret=0 - "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@" </dev/null' bash "${BFS[@]}" "$@" || ret=$? + "$PTYX" -w80 -h24 -- "${BFS[@]}" "$@" || ret=$? if ((ret > 125)); then exit $ret diff --git a/tests/util.sh b/tests/util.sh index d8b7036..b846d45 100644 --- a/tests/util.sh +++ b/tests/util.sh @@ -16,6 +16,7 @@ ROOT=$(_realpath "$(dirname -- "$TESTS")") TESTS="$ROOT/tests" BIN="$ROOT/bin" MKSOCK="$BIN/tests/mksock" +PTYX="$BIN/tests/ptyx" XTOUCH="$BIN/tests/xtouch" UNAME=$(uname) diff --git a/tests/xtouch.c b/tests/xtouch.c index 5d65a4c..f33c573 100644 --- a/tests/xtouch.c +++ b/tests/xtouch.c @@ -217,8 +217,8 @@ int main(int argc, char *argv[]) { } if (marg) { - long mode; - if (xstrtol(marg, NULL, 8, &mode) == 0 && mode >= 0 && mode < 01000) { + unsigned int mode; + if (xstrtoui(marg, NULL, 8, &mode) == 0 && mode < 01000) { args.fmode = args.dmode = mode; } else { fprintf(stderr, "%s: Invalid mode '%s'\n", cmd, marg); |