// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD

/**
 * The command line parser.  Expressions are parsed by recursive descent, with a
 * grammar described in the comments of the parse_*() functions.  The parser
 * also accepts flags and paths at any point in the expression, by treating
 * flags like always-true options, and skipping over paths wherever they appear.
 */

#include "parse.h"

#include "alloc.h"
#include "bfs.h"
#include "bfstd.h"
#include "bftw.h"
#include "color.h"
#include "ctx.h"
#include "diag.h"
#include "dir.h"
#include "eval.h"
#include "exec.h"
#include "expr.h"
#include "fsade.h"
#include "list.h"
#include "opt.h"
#include "printf.h"
#include "pwcache.h"
#include "sanity.h"
#include "stat.h"
#include "typo.h"
#include "xregex.h"
#include "xspawn.h"
#include "xtime.h"

#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

// Strings printed by -D tree for "fake" expressions
static char *fake_and_arg = "-and";
static char *fake_hidden_arg = "-hidden";
static char *fake_or_arg = "-or";
static char *fake_print_arg = "-print";
static char *fake_true_arg = "-true";

/**
 * Color use flags.
 */
enum use_color {
	COLOR_NEVER,
	COLOR_AUTO,
	COLOR_ALWAYS,
};

/**
 * Command line parser state.
 */
struct bfs_parser {
	/** The command line being constructed. */
	struct bfs_ctx *ctx;
	/** The command line arguments being parsed. */
	char **argv;
	/** The name of this program. */
	const char *command;

	/** The current regex flags to use. */
	enum bfs_regex_type regex_type;

	/** Whether stdout is a terminal. */
	bool stdout_tty;
	/** Whether -color or -nocolor has been passed. */
	enum use_color use_color;
	/** Whether a -print action is implied. */
	bool implicit_print;
	/** Whether the expression has started. */
	bool expr_started;
	/** Whether an information option like -help or -version was passed. */
	bool just_info;
	/** Whether we are currently parsing an -exclude expression. */
	bool excluding;

	/** The last non-path argument. */
	char **last_arg;
	/** 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;
};

/**
 * Print a low-level error message during parsing.
 */
static void parse_perror(const struct bfs_parser *parser, const char *str) {
	bfs_perror(parser->ctx, str);
}

/** Initialize an empty highlighted range. */
static void init_highlight(const struct bfs_ctx *ctx, bool *args) {
	for (size_t i = 0; i < ctx->argc; ++i) {
		args[i] = false;
	}
}

/** Highlight a range of command line arguments. */
static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, bool *args) {
	size_t i = argv - ctx->argv;
	for (size_t j = 0; j < argc; ++j) {
		bfs_assert(i + j < ctx->argc);
		args[i + j] = true;
	}
}

/**
 * Print an error message during parsing.
 */
[[_printf(2, 3)]]
static void parse_error(const struct bfs_parser *parser, const char *format, ...) {
	const struct bfs_ctx *ctx = parser->ctx;

	bool highlight[ctx->argc];
	init_highlight(ctx, highlight);
	highlight_args(ctx, parser->argv, 1, highlight);
	bfs_argv_error(ctx, highlight);

	va_list args;
	va_start(args, format);
	bfs_verror(parser->ctx, format, args);
	va_end(args);
}

/**
 * Print an error about some command line arguments.
 */
[[_printf(4, 5)]]
static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) {
	const struct bfs_ctx *ctx = parser->ctx;

	bool highlight[ctx->argc];
	init_highlight(ctx, highlight);
	highlight_args(ctx, argv, argc, highlight);
	bfs_argv_error(ctx, highlight);

	va_list args;
	va_start(args, format);
	bfs_verror(ctx, format, args);
	va_end(args);
}

/**
 * Print an error about conflicting command line arguments.
 */
[[_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, expr1->argv, expr1->argc, highlight);
	highlight_args(ctx, expr2->argv, expr2->argc, highlight);
	bfs_argv_error(ctx, highlight);

	va_list args;
	va_start(args, format);
	bfs_verror(ctx, format, args);
	va_end(args);
}

/**
 * Print an error about an expression.
 */
[[_printf(3, 4)]]
static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
	const struct bfs_ctx *ctx = parser->ctx;

	bfs_expr_error(ctx, expr);

	va_list args;
	va_start(args, format);
	bfs_verror(ctx, format, args);
	va_end(args);
}

/**
 * Print a warning message during parsing.
 */
[[_printf(2, 3)]]
static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) {
	const struct bfs_ctx *ctx = parser->ctx;

	bool highlight[ctx->argc];
	init_highlight(ctx, highlight);
	highlight_args(ctx, parser->argv, 1, highlight);
	if (!bfs_argv_warning(ctx, highlight)) {
		return false;
	}

	va_list args;
	va_start(args, format);
	bool ret = bfs_vwarning(parser->ctx, format, args);
	va_end(args);
	return ret;
}

/**
 * Print a warning about conflicting command line arguments.
 */
[[_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, expr1->argv, expr1->argc, highlight);
	highlight_args(ctx, expr2->argv, expr2->argc, highlight);
	if (!bfs_argv_warning(ctx, highlight)) {
		return false;
	}

	va_list args;
	va_start(args, format);
	bool ret = bfs_vwarning(ctx, format, args);
	va_end(args);
	return ret;
}

/**
 * Print a warning about an expression.
 */
[[_printf(3, 4)]]
static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
	const struct bfs_ctx *ctx = parser->ctx;

	if (!bfs_expr_warning(ctx, expr)) {
		return false;
	}

	va_list args;
	va_start(args, format);
	bool ret = bfs_vwarning(ctx, format, args);
	va_end(args);
	return ret;
}

/**
 * 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) {
	struct bfs_expr *expr = bfs_expr_new(parser->ctx, eval_fn, argc, argv, kind);
	if (!expr) {
		parse_perror(parser, "bfs_expr_new()");
	}
	return expr;
}

/**
 * Create a new unary expression.
 */
static struct bfs_expr *new_unary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *rhs, char **argv) {
	struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv, BFS_OPERATOR);
	if (!expr) {
		return NULL;
	}

	bfs_assert(bfs_expr_is_parent(expr));
	bfs_expr_append(expr, rhs);
	return expr;
}

/**
 * Create a new binary expression.
 */
static struct bfs_expr *new_binary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *lhs, struct bfs_expr *rhs, char **argv) {
	struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv, BFS_OPERATOR);
	if (!expr) {
		return NULL;
	}

	bfs_assert(bfs_expr_is_parent(expr));
	bfs_expr_append(expr, lhs);
	bfs_expr_append(expr, rhs);
	return expr;
}

/**
 * Fill in a "-print"-type expression.
 */
static void init_print_expr(struct bfs_parser *parser, struct bfs_expr *expr) {
	expr->cfile = parser->ctx->cout;
	expr->path = NULL;
}

/**
 * Open a file for an expression.
 */
static int expr_open(struct bfs_parser *parser, struct bfs_expr *expr, const char *path) {
	struct bfs_ctx *ctx = parser->ctx;

	FILE *file = NULL;
	CFILE *cfile = NULL;

	file = xfopen(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC);
	if (!file) {
		goto fail;
	}

	cfile = cfwrap(file, parser->use_color ? ctx->colors : NULL, true);
	if (!cfile) {
		goto fail;
	}

	CFILE *dedup = bfs_ctx_dedup(ctx, cfile, path);
	if (!dedup) {
		goto fail;
	}

	if (dedup != cfile) {
		cfclose(cfile);
	}

	expr->cfile = dedup;
	expr->path = path;
	return 0;

fail:
	parse_expr_error(parser, expr, "%s.\n", errstr());
	if (cfile) {
		cfclose(cfile);
	} else if (file) {
		fclose(file);
	}
	return -1;
}

/**
 * Invoke bfs_stat() on an argument.
 */
static int stat_arg(const struct bfs_parser *parser, char **arg, struct bfs_stat *sb) {
	const struct bfs_ctx *ctx = parser->ctx;

	bool follow = ctx->flags & (BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL);
	enum bfs_stat_flags flags = follow ? BFS_STAT_TRYFOLLOW : BFS_STAT_NOFOLLOW;

	int ret = bfs_stat(AT_FDCWD, *arg, flags, sb);
	if (ret != 0) {
		parse_argv_error(parser, arg, 1, "%s.\n", errstr());
	}
	return ret;
}

/**
 * Parse the expression specified on the command line.
 */
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;
	}

	if (kind != BFS_PATH) {
		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;
}

/**
 * Parse a root path.
 */
static int parse_root(struct bfs_parser *parser, const char *path) {
	struct bfs_ctx *ctx = parser->ctx;
	const char **root = RESERVE(const char *, &ctx->paths, &ctx->npaths);
	if (!root) {
		parse_perror(parser, "RESERVE()");
		return -1;
	}

	*root = strdup(path);
	if (!*root) {
		--ctx->npaths;
		parse_perror(parser, "strdup()");
		return -1;
	}

	return 0;
}

/**
 * While parsing an expression, skip any paths and add them to ctx->paths.
 */
static int skip_paths(struct bfs_parser *parser) {
	while (true) {
		const char *arg = parser->argv[0];
		if (!arg) {
			return 0;
		}

		if (arg[0] == '-') {
			if (strcmp(arg, "--") == 0) {
				// find uses -- to separate flags from the rest
				// of the command line.  We allow mixing flags
				// and paths/predicates, so we just ignore --.
				parser_advance(parser, BFS_FLAG, 1);
				continue;
			}
			if (strcmp(arg, "-") != 0) {
				// - by itself is a file name.  Anything else
				// starting with - is a flag/predicate.
				return 0;
			}
		}

		// By POSIX, these are always options
		if (strcmp(arg, "(") == 0 || strcmp(arg, "!") == 0) {
			return 0;
		}

		if (parser->expr_started) {
			// By POSIX, these can be paths.  We only treat them as
			// such at the beginning of the command line.
			if (strcmp(arg, ")") == 0 || strcmp(arg, ",") == 0) {
				return 0;
			}
		}

		if (parser->excluding) {
			parse_warning(parser, "This path will not be excluded.  Use a test like ${blu}-name${rs} or ${blu}-path${rs}\n");
			bfs_warning(parser->ctx, "within ${red}-exclude${rs} to exclude matching files.\n\n");
		}

		if (parse_root(parser, arg) != 0) {
			return -1;
		}

		parser_advance(parser, BFS_PATH, 1);
	}
}

/** Integer parsing flags. */
enum int_flags {
	IF_BASE_MASK   = 0x03F,
	IF_INT         = 0x040,
	IF_LONG        = 0x080,
	IF_LONG_LONG   = 0x0C0,
	IF_SIZE_MASK   = 0x0C0,
	IF_UNSIGNED    = 0x100,
	IF_PARTIAL_OK  = 0x200,
	IF_QUIET       = 0x400,
};

/**
 * Parse an integer.
 */
static const char *parse_int(const struct bfs_parser *parser, char **arg, const char *str, void *result, enum int_flags flags) {
	int base = flags & IF_BASE_MASK;
	if (base == 0) {
		base = 10;
	}

	char *endptr;
	long long value;
	if (xstrtoll(str, &endptr, base, &value) != 0) {
		if (errno == ERANGE) {
			goto range;
		} else {
			goto bad;
		}
	}

	if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') {
		goto bad;
	}

	if ((flags & IF_UNSIGNED) && value < 0) {
		goto negative;
	}

	switch (flags & IF_SIZE_MASK) {
	case IF_INT:
		if (value < INT_MIN || value > INT_MAX) {
			goto range;
		}
		*(int *)result = value;
		break;

	case IF_LONG:
		if (value < LONG_MIN || value > LONG_MAX) {
			goto range;
		}
		*(long *)result = value;
		break;

	case IF_LONG_LONG:
		*(long long *)result = value;
		break;

	default:
		bfs_bug("Invalid int size");
		goto bad;
	}

	return endptr;

bad:
	if (!(flags & IF_QUIET)) {
		parse_argv_error(parser, arg, 1, "${bld}%pq${rs} is not a valid integer.\n", str);
	}
	return NULL;

negative:
	if (!(flags & IF_QUIET)) {
		parse_argv_error(parser, arg, 1, "Negative integer ${bld}%pq${rs} is not allowed here.\n", str);
	}
	return NULL;

range:
	if (!(flags & IF_QUIET)) {
		parse_argv_error(parser, arg, 1, "${bld}%pq${rs} is too large an integer.\n", str);
	}
	return NULL;
}

/**
 * Parse an integer and a comparison flag.
 */
static const char *parse_icmp(const struct bfs_parser *parser, struct bfs_expr *expr, enum int_flags flags) {
	char **arg = &expr->argv[1];
	const char *str = *arg;
	switch (str[0]) {
	case '-':
		expr->int_cmp = BFS_INT_LESS;
		++str;
		break;
	case '+':
		expr->int_cmp = BFS_INT_GREATER;
		++str;
		break;
	default:
		expr->int_cmp = BFS_INT_EQUAL;
		break;
	}

	return parse_int(parser, arg, str, &expr->num, flags | IF_LONG_LONG | IF_UNSIGNED);
}

/**
 * Check if a string could be an integer comparison.
 */
static bool looks_like_icmp(const char *str) {
	int i;

	// One +/- for the comparison flag, one for the sign
	for (i = 0; i < 2; ++i) {
		if (str[i] != '-' && str[i] != '+') {
			break;
		}
	}

	return str[i] >= '0' && str[i] <= '9';
}

/**
 * Parse a single flag.
 */
static struct bfs_expr *parse_flag(struct bfs_parser *parser, size_t argc) {
	char **argv = parser_advance(parser, BFS_FLAG, argc);
	return parse_new_expr(parser, eval_true, argc, argv, BFS_FLAG);
}

/**
 * Parse a flag that doesn't take a value.
 */
static struct bfs_expr *parse_nullary_flag(struct bfs_parser *parser) {
	return parse_flag(parser, 1);
}

/**
 * Parse a flag that takes a value.
 */
static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) {
	const char *arg = parser->argv[0];
	char flag = arg[strlen(arg) - 1];

	const char *value = parser->argv[1];
	if (!value) {
		parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag);
		return NULL;
	}

	return parse_flag(parser, 2);
}

/**
 * Parse a prefix flag like -O3, -j8, etc.
 */
static struct bfs_expr *parse_prefix_flag(struct bfs_parser *parser, char flag, bool allow_separate, const char **value) {
	const char *arg = parser->argv[0];

	const char *suffix = strchr(arg, flag) + 1;
	if (*suffix) {
		*value = suffix;
		return parse_nullary_flag(parser);
	}

	suffix = parser->argv[1];
	if (allow_separate && suffix) {
		*value = suffix;
	} else {
		parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag);
		return NULL;
	}

	return parse_unary_flag(parser);
}

/**
 * Parse a single option.
 */
static struct bfs_expr *parse_option(struct bfs_parser *parser, size_t argc) {
	char **argv = parser_advance(parser, BFS_OPTION, argc);
	return parse_new_expr(parser, eval_true, argc, argv, BFS_OPTION);
}

/**
 * Parse an option that doesn't take a value.
 */
static struct bfs_expr *parse_nullary_option(struct bfs_parser *parser) {
	return parse_option(parser, 1);
}

/**
 * Parse an option that takes a value.
 */
static struct bfs_expr *parse_unary_option(struct bfs_parser *parser) {
	const char *arg = parser->argv[0];
	const char *value = parser->argv[1];
	if (!value) {
		parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
		return NULL;
	}

	return parse_option(parser, 2);
}

/**
 * Parse a single test.
 */
static struct bfs_expr *parse_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) {
	char **argv = parser_advance(parser, BFS_TEST, argc);
	return parse_new_expr(parser, eval_fn, argc, argv, BFS_TEST);
}

/**
 * Parse a test that doesn't take a value.
 */
static struct bfs_expr *parse_nullary_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
	return parse_test(parser, eval_fn, 1);
}

/**
 * Parse a test that takes a value.
 */
static struct bfs_expr *parse_unary_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
	const char *arg = parser->argv[0];
	const char *value = parser->argv[1];
	if (!value) {
		parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
		return NULL;
	}

	return parse_test(parser, eval_fn, 2);
}

/**
 * Parse a single action.
 */
static struct bfs_expr *parse_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) {
	char **argv = parser_advance(parser, BFS_ACTION, argc);

	if (parser->excluding) {
		parse_argv_error(parser, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n");
		return NULL;
	}

	if (eval_fn != eval_limit && eval_fn != eval_prune && eval_fn != eval_quit) {
		parser->implicit_print = false;
	}

	return parse_new_expr(parser, eval_fn, argc, argv, BFS_ACTION);
}

/**
 * Parse an action that takes no arguments.
 */
static struct bfs_expr *parse_nullary_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
	return parse_action(parser, eval_fn, 1);
}

/**
 * Parse an action that takes one argument.
 */
static struct bfs_expr *parse_unary_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
	const char *arg = parser->argv[0];
	const char *value = parser->argv[1];
	if (!value) {
		parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
		return NULL;
	}

	return parse_action(parser, eval_fn, 2);
}

/**
 * Parse a test expression with integer data and a comparison flag.
 */
static struct bfs_expr *parse_test_icmp(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_fn);
	if (!expr) {
		return NULL;
	}

	if (!parse_icmp(parser, expr, 0)) {
		return NULL;
	}

	return expr;
}

/**
 * Print usage information for -D.
 */
static void debug_help(CFILE *cfile) {
	cfprintf(cfile, "Supported debug flags:\n\n");

	cfprintf(cfile, "  ${bld}help${rs}:   This message.\n");
	cfprintf(cfile, "  ${bld}cost${rs}:   Show cost estimates.\n");
	cfprintf(cfile, "  ${bld}exec${rs}:   Print executed command details.\n");
	cfprintf(cfile, "  ${bld}opt${rs}:    Print optimization details.\n");
	cfprintf(cfile, "  ${bld}rates${rs}:  Print predicate success rates.\n");
	cfprintf(cfile, "  ${bld}search${rs}: Trace the filesystem traversal.\n");
	cfprintf(cfile, "  ${bld}stat${rs}:   Trace all stat() calls.\n");
	cfprintf(cfile, "  ${bld}tree${rs}:   Print the parse tree.\n");
	cfprintf(cfile, "  ${bld}all${rs}:    All debug flags at once.\n");
}

/** Check if a substring matches a debug flag. */
static bool parse_debug_flag(const char *flag, size_t len, const char *expected) {
	if (len == strlen(expected)) {
		return strncmp(flag, expected, len) == 0;
	} else {
		return false;
	}
}

/**
 * Parse -D FLAG.
 */
static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_ctx *ctx = parser->ctx;

	const char *flags;
	struct bfs_expr *expr = parse_prefix_flag(parser, 'D', true, &flags);
	if (!expr) {
		cfprintf(ctx->cerr, "\n");
		debug_help(ctx->cerr);
		return NULL;
	}

	bool unrecognized = false;

	for (const char *flag = flags, *next; flag; flag = next) {
		size_t len = strcspn(flag, ",");
		if (flag[len]) {
			next = flag + len + 1;
		} else {
			next = NULL;
		}

		if (parse_debug_flag(flag, len, "help")) {
			debug_help(ctx->cout);
			parser->just_info = true;
			return NULL;
		} else if (parse_debug_flag(flag, len, "all")) {
			ctx->debug = DEBUG_ALL;
			continue;
		}

		enum debug_flags i;
		for (i = 1; DEBUG_ALL & i; i <<= 1) {
			const char *name = debug_flag_name(i);
			if (parse_debug_flag(flag, len, name)) {
				break;
			}
		}

		if (DEBUG_ALL & i) {
			ctx->debug |= i;
		} else {
			if (parse_expr_warning(parser, expr, "Unrecognized debug flag ${bld}")) {
				fwrite(flag, 1, len, stderr);
				cfprintf(ctx->cerr, "${rs}.\n\n");
				unrecognized = true;
			}
		}
	}

	if (unrecognized) {
		debug_help(ctx->cerr);
		cfprintf(ctx->cerr, "\n");
	}

	return expr;
}

/**
 * Parse -On.
 */
static struct bfs_expr *parse_optlevel(struct bfs_parser *parser, int arg1, int arg2) {
	const char *arg;
	struct bfs_expr *expr = parse_prefix_flag(parser, 'O', false, &arg);
	if (!expr) {
		return NULL;
	}

	int *optlevel = &parser->ctx->optlevel;

	if (strcmp(arg, "fast") == 0) {
		*optlevel = 4;
	} else if (!parse_int(parser, expr->argv, arg, optlevel, IF_INT | IF_UNSIGNED)) {
		return NULL;
	}

	if (*optlevel > 4) {
		parse_expr_warning(parser, expr, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", arg);
	}

	return expr;
}

/**
 * Parse -[PHL], -follow.
 */
static struct bfs_expr *parse_follow(struct bfs_parser *parser, int flags, int option) {
	struct bfs_ctx *ctx = parser->ctx;
	ctx->flags &= ~(BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL);
	ctx->flags |= flags;
	if (option) {
		return parse_nullary_option(parser);
	} else {
		return parse_nullary_flag(parser);
	}
}

/**
 * Parse -X.
 */
static struct bfs_expr *parse_xargs_safe(struct bfs_parser *parser, int arg1, int arg2) {
	parser->ctx->xargs_safe = true;
	return parse_nullary_flag(parser);
}

/**
 * Parse -executable, -readable, -writable
 */
static struct bfs_expr *parse_access(struct bfs_parser *parser, int flag, int arg2) {
	struct bfs_expr *expr = parse_nullary_test(parser, eval_access);
	if (expr) {
		expr->num = flag;
	}
	return expr;
}

/**
 * Parse -acl.
 */
static struct bfs_expr *parse_acl(struct bfs_parser *parser, int flag, int arg2) {
#if BFS_CAN_CHECK_ACL
	return parse_nullary_test(parser, eval_acl);
#else
	parse_error(parser, "Missing platform support.\n");
	return NULL;
#endif
}

/**
 * Parse -[aBcm]?newer.
 */
static struct bfs_expr *parse_newer(struct bfs_parser *parser, int field, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
	if (!expr) {
		return NULL;
	}

	struct bfs_stat sb;
	if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
		return NULL;
	}

	expr->reftime = sb.mtime;
	expr->stat_field = field;
	return expr;
}

/**
 * Parse -[aBcm]min.
 */
static struct bfs_expr *parse_min(struct bfs_parser *parser, int field, int arg2) {
	struct bfs_expr *expr = parse_test_icmp(parser, eval_time);
	if (!expr) {
		return NULL;
	}

	expr->reftime = parser->now;
	expr->stat_field = field;
	expr->time_unit = BFS_MINUTES;
	return expr;
}

/**
 * Parse -[aBcm]time.
 */
static struct bfs_expr *parse_time(struct bfs_parser *parser, int field, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_time);
	if (!expr) {
		return NULL;
	}

	expr->reftime = parser->now;
	expr->stat_field = field;

	const char *tail = parse_icmp(parser, expr, IF_PARTIAL_OK);
	if (!tail) {
		return NULL;
	}

	if (!*tail) {
		expr->time_unit = BFS_DAYS;
		return expr;
	}

	unsigned long long time = expr->num;
	expr->num = 0;

	while (true) {
		switch (*tail) {
		case 'w':
			time *= 7;
			[[fallthrough]];
		case 'd':
			time *= 24;
			[[fallthrough]];
		case 'h':
			time *= 60;
			[[fallthrough]];
		case 'm':
			time *= 60;
			[[fallthrough]];
		case 's':
			break;
		default:
			parse_expr_error(parser, expr, "Unknown time unit ${bld}%c${rs}.\n", *tail);
			return NULL;
		}

		expr->num += time;

		if (!*++tail) {
			break;
		}

		tail = parse_int(parser, &expr->argv[1], tail, &time, IF_PARTIAL_OK | IF_LONG_LONG | IF_UNSIGNED);
		if (!tail) {
			return NULL;
		}
		if (!*tail) {
			parse_expr_error(parser, expr, "Missing time unit.\n");
			return NULL;
		}
	}

	expr->time_unit = BFS_SECONDS;
	return expr;
}

/**
 * Parse -capable.
 */
static struct bfs_expr *parse_capable(struct bfs_parser *parser, int flag, int arg2) {
#if BFS_CAN_CHECK_CAPABILITIES
	return parse_nullary_test(parser, eval_capable);
#else
	parse_error(parser, "Missing platform support.\n");
	return NULL;
#endif
}

/**
 * Parse -(no)?color.
 */
static struct bfs_expr *parse_color(struct bfs_parser *parser, int color, int arg2) {
	struct bfs_expr *expr = parse_nullary_option(parser);
	if (!expr) {
		return NULL;
	}

	struct bfs_ctx *ctx = parser->ctx;
	struct colors *colors = ctx->colors;

	if (color) {
		if (!colors) {
			parse_expr_error(parser, expr, "Error parsing $$LS_COLORS: %s.\n", xstrerror(ctx->colors_error));
			return NULL;
		}

		parser->use_color = COLOR_ALWAYS;
		ctx->cout->colors = colors;
		ctx->cerr->colors = colors;
	} else {
		parser->use_color = COLOR_NEVER;
		ctx->cout->colors = NULL;
		ctx->cerr->colors = NULL;
	}

	return expr;
}

/**
 * Common code for fnmatch() tests.
 */
static struct bfs_expr *parse_fnmatch(const struct bfs_parser *parser, struct bfs_expr *expr, bool casefold) {
	if (!expr) {
		return NULL;
	}

	expr->pattern = expr->argv[1];

	if (casefold) {
#ifdef FNM_CASEFOLD
		expr->fnm_flags = FNM_CASEFOLD;
#else
		parse_expr_error(parser, expr, "Missing platform support.\n");
		return NULL;
#endif
	} else {
		expr->fnm_flags = 0;
	}

	// POSIX says, about fnmatch():
	//
	//     If pattern ends with an unescaped <backslash>, fnmatch() shall
	//     return a non-zero value (indicating either no match or an error).
	//
	// But not all implementations obey this, so check for it ourselves.
	size_t i, len = strlen(expr->pattern);
	for (i = 0; i < len; ++i) {
		if (expr->pattern[len - i - 1] != '\\') {
			break;
		}
	}
	if (i % 2 != 0) {
		parse_expr_warning(parser, expr, "Unescaped trailing backslash.\n\n");
		expr->eval_fn = eval_false;
		return expr;
	}

	// strcmp() can be much faster than fnmatch() since it doesn't have to
	// parse the pattern, so special-case patterns with no wildcards.
	//
	//     https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_14_01
	expr->literal = strcspn(expr->pattern, "?*\\[") == len;

	return expr;
}

/**
 * Parse -context.
 */
static struct bfs_expr *parse_context(struct bfs_parser *parser, int flag, int arg2) {
#if BFS_CAN_CHECK_CONTEXT
	struct bfs_expr *expr = parse_unary_test(parser, eval_context);
	return parse_fnmatch(parser, expr, false);
#else
	parse_error(parser, "Missing platform support.\n");
	return NULL;
#endif
}

/**
 * Parse -{false,true}.
 */
static struct bfs_expr *parse_const(struct bfs_parser *parser, int value, int arg2) {
	return parse_nullary_test(parser, value ? eval_true : eval_false);
}

/**
 * Parse -daystart.
 */
static struct bfs_expr *parse_daystart(struct bfs_parser *parser, int arg1, int arg2) {
	struct tm tm;
	if (!localtime_r(&parser->now.tv_sec, &tm)) {
		parse_perror(parser, "localtime_r()");
		return NULL;
	}

	if (tm.tm_hour || tm.tm_min || tm.tm_sec || parser->now.tv_nsec) {
		++tm.tm_mday;
	}
	tm.tm_hour = 0;
	tm.tm_min = 0;
	tm.tm_sec = 0;

	time_t time;
	if (xmktime(&tm, &time) != 0) {
		parse_perror(parser, "xmktime()");
		return NULL;
	}

	parser->now.tv_sec = time;
	parser->now.tv_nsec = 0;

	return parse_nullary_option(parser);
}

/**
 * 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_expr = expr;
	return expr;
}

/**
 * Parse -d.
 */
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_expr = expr;
	return expr;
}

/**
 * Parse -depth [N].
 */
static struct bfs_expr *parse_depth_n(struct bfs_parser *parser, int arg1, int arg2) {
	const char *arg = parser->argv[1];
	if (arg && looks_like_icmp(arg)) {
		return parse_test_icmp(parser, eval_depth);
	} else {
		return parse_depth(parser, arg1, arg2);
	}
}

/**
 * Parse -{min,max}depth N.
 */
static struct bfs_expr *parse_depth_limit(struct bfs_parser *parser, int is_min, int arg2) {
	struct bfs_expr *expr = parse_unary_option(parser);
	if (!expr) {
		return NULL;
	}

	struct bfs_ctx *ctx = parser->ctx;
	int *depth = is_min ? &ctx->mindepth : &ctx->maxdepth;
	char **arg = &expr->argv[1];
	if (!parse_int(parser, arg, *arg, depth, IF_INT | IF_UNSIGNED)) {
		return NULL;
	}

	return expr;
}

/**
 * Parse -empty.
 */
static struct bfs_expr *parse_empty(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_test(parser, eval_empty);
	if (expr) {
		// For opendir()
		expr->ephemeral_fds = 1;
	}
	return expr;
}

/** Check for unsafe relative paths in $PATH. */
static const char *unsafe_path(const struct bfs_exec *execbuf) {
	if (!(execbuf->flags & BFS_EXEC_CHDIR)) {
		// Not -execdir or -okdir
		return NULL;
	}

	const char *exe = execbuf->tmpl_argv[0];
	if (strchr(exe, '/')) {
		// No $PATH lookups for /foo or foo/bar
		return NULL;
	}

	if (strstr(exe, "{}")) {
		// Substituted paths always contain a /
		return NULL;
	}

	const char *path = getenv("PATH");
	while (path) {
		if (path[0] != '/') {
			// Relative $PATH component!
			return path;
		}

		path = strchr(path, ':');
		if (path) {
			++path;
		}
	}

	// No relative components in $PATH
	return NULL;
}

/**
 * Parse -exec(dir)?/-ok(dir)?.
 */
static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg2) {
	struct bfs_ctx *ctx = parser->ctx;

	struct bfs_exec *execbuf = bfs_exec_parse(ctx, parser->argv, flags);
	if (!execbuf) {
		return NULL;
	}

	struct bfs_expr *expr = parse_action(parser, eval_exec, execbuf->tmpl_argc + 2);
	if (!expr) {
		bfs_exec_free(execbuf);
		return NULL;
	}

	expr->exec = execbuf;

	// For pipe() in bfs_spawn()
	expr->ephemeral_fds = 2;

	const char *unsafe = unsafe_path(execbuf);
	if (unsafe) {
		size_t len = strcspn(unsafe, ":");
		char *comp = strndup(unsafe, len);
		if (comp) {
			parse_expr_error(parser, expr,
				"This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp);
			free(comp);
		} else {
			parse_perror(parser, "strndup()");
		}
		return NULL;
	}

	if (execbuf->flags & BFS_EXEC_CHDIR) {
		// To dup() the parent directory
		if (execbuf->flags & BFS_EXEC_MULTI) {
			++expr->persistent_fds;
		} else {
			++expr->ephemeral_fds;
		}
	}

	if (execbuf->flags & BFS_EXEC_CONFIRM) {
		if (!consume_stdin(parser, expr)) {
			return NULL;
		}
	} else {
		ctx->dangerous = true;
	}

	return expr;
}

/**
 * Parse -exit [STATUS].
 */
static struct bfs_expr *parse_exit(struct bfs_parser *parser, int arg1, int arg2) {
	size_t argc = 1;
	const char *value = parser->argv[1];

	int status = EXIT_SUCCESS;
	if (value && parse_int(parser, NULL, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) {
		argc = 2;
	}

	struct bfs_expr *expr = parse_action(parser, eval_exit, argc);
	if (expr) {
		expr->num = status;
	}
	return expr;
}

/**
 * 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;
	}

	return expr;
}

/**
 * Parse -files0-from PATH.
 */
static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_option(parser);
	if (!expr) {
		return NULL;
	}

	// 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;
}

/**
 * Parse -flags FLAGS.
 */
static struct bfs_expr *parse_flags(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_flags);
	if (!expr) {
		return NULL;
	}

	const char *flags = expr->argv[1];
	switch (flags[0]) {
	case '-':
		expr->flags_cmp = BFS_MODE_ALL;
		++flags;
		break;
	case '+':
		expr->flags_cmp = BFS_MODE_ANY;
		++flags;
		break;
	default:
		expr->flags_cmp = BFS_MODE_EQUAL;
		break;
	}

	if (xstrtofflags(&flags, &expr->set_flags, &expr->clear_flags) != 0) {
		if (errno == ENOTSUP) {
			parse_expr_error(parser, expr, "Missing platform support.\n");
		} else {
			parse_expr_error(parser, expr, "Invalid flags.\n");
		}
		return NULL;
	}

	return expr;
}

/**
 * Parse -fls FILE.
 */
static struct bfs_expr *parse_fls(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_action(parser, eval_fls);
	if (!expr) {
		return NULL;
	}

	if (expr_open(parser, expr, expr->argv[1]) != 0) {
		return NULL;
	}

	return expr;
}

/**
 * Parse -fprint FILE.
 */
static struct bfs_expr *parse_fprint(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_action(parser, eval_fprint);
	if (!expr) {
		return NULL;
	}

	if (expr_open(parser, expr, expr->argv[1]) != 0) {
		return NULL;
	}

	return expr;
}

/**
 * Parse -fprint0 FILE.
 */
static struct bfs_expr *parse_fprint0(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_action(parser, eval_fprint0);
	if (!expr) {
		return NULL;
	}

	if (expr_open(parser, expr, expr->argv[1]) != 0) {
		return NULL;
	}

	return expr;
}

/**
 * Parse -fprintf FILE FORMAT.
 */
static struct bfs_expr *parse_fprintf(struct bfs_parser *parser, int arg1, int arg2) {
	const char *arg = parser->argv[0];

	const char *file = parser->argv[1];
	if (!file) {
		parse_error(parser, "${blu}%s${rs} needs a file.\n", arg);
		return NULL;
	}

	const char *format = parser->argv[2];
	if (!format) {
		parse_error(parser, "${blu}%s${rs} needs a format string.\n", arg);
		return NULL;
	}

	struct bfs_expr *expr = parse_action(parser, eval_fprintf, 3);
	if (!expr) {
		return NULL;
	}

	if (expr_open(parser, expr, file) != 0) {
		return NULL;
	}

	if (bfs_printf_parse(parser->ctx, expr, format) != 0) {
		return NULL;
	}

	return expr;
}

/**
 * Parse -fstype TYPE.
 */
static struct bfs_expr *parse_fstype(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_fstype);
	if (!expr) {
		return NULL;
	}

	if (!bfs_ctx_mtab(parser->ctx)) {
		parse_expr_error(parser, expr, "Couldn't parse the mount table: %s.\n", errstr());
		return NULL;
	}

	return expr;
}

/**
 * Parse -gid/-group.
 */
static struct bfs_expr *parse_group(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_gid);
	if (!expr) {
		return NULL;
	}

	const struct group *grp = bfs_getgrnam(parser->ctx->groups, expr->argv[1]);
	if (grp) {
		expr->num = grp->gr_gid;
		expr->int_cmp = BFS_INT_EQUAL;
	} else if (looks_like_icmp(expr->argv[1])) {
		if (!parse_icmp(parser, expr, 0)) {
			return NULL;
		}
	} else if (errno) {
		parse_expr_error(parser, expr, "%s.\n", errstr());
		return NULL;
	} else {
		parse_expr_error(parser, expr, "No such group.\n");
		return NULL;
	}

	return expr;
}

/**
 * Parse -unique.
 */
static struct bfs_expr *parse_unique(struct bfs_parser *parser, int arg1, int arg2) {
	parser->ctx->unique = true;
	return parse_nullary_option(parser);
}

/**
 * Parse -used N.
 */
static struct bfs_expr *parse_used(struct bfs_parser *parser, int arg1, int arg2) {
	return parse_test_icmp(parser, eval_used);
}

/**
 * Parse -uid/-user.
 */
static struct bfs_expr *parse_user(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_uid);
	if (!expr) {
		return NULL;
	}

	const struct passwd *pwd = bfs_getpwnam(parser->ctx->users, expr->argv[1]);
	if (pwd) {
		expr->num = pwd->pw_uid;
		expr->int_cmp = BFS_INT_EQUAL;
	} else if (looks_like_icmp(expr->argv[1])) {
		if (!parse_icmp(parser, expr, 0)) {
			return NULL;
		}
	} else if (errno) {
		parse_expr_error(parser, expr, "%s.\n", errstr());
		return NULL;
	} else {
		parse_expr_error(parser, expr, "No such user.\n");
		return NULL;
	}

	return expr;
}

/**
 * Parse -hidden.
 */
static struct bfs_expr *parse_hidden(struct bfs_parser *parser, int arg1, int arg2) {
	return parse_nullary_test(parser, eval_hidden);
}

/**
 * Parse -(no)?ignore_readdir_race.
 */
static struct bfs_expr *parse_ignore_races(struct bfs_parser *parser, int ignore, int arg2) {
	parser->ctx->ignore_races = ignore;
	return parse_nullary_option(parser);
}

/**
 * Parse -inum N.
 */
static struct bfs_expr *parse_inum(struct bfs_parser *parser, int arg1, int arg2) {
	return parse_test_icmp(parser, eval_inum);
}

/**
 * Parse -j<n>.
 */
static struct bfs_expr *parse_jobs(struct bfs_parser *parser, int arg1, int arg2) {
	const char *arg;
	struct bfs_expr *expr = parse_prefix_flag(parser, 'j', false, &arg);
	if (!expr) {
		return NULL;
	}

	unsigned int n;
	if (!parse_int(parser, expr->argv, arg, &n, IF_INT | IF_UNSIGNED)) {
		return NULL;
	}

	if (n == 0) {
		parse_expr_error(parser, expr, "${bld}0${rs} is not enough threads.\n");
		return NULL;
	}

	parser->ctx->threads = n;
	return expr;
}

/**
 * Parse -limit N.
 */
static struct bfs_expr *parse_limit(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_action(parser, eval_limit);
	if (!expr) {
		return NULL;
	}

	char **arg = &expr->argv[1];
	if (!parse_int(parser, arg, *arg, &expr->num, IF_LONG_LONG)) {
		return NULL;
	}

	if (expr->num <= 0) {
		parse_expr_error(parser, expr, "The %pX must be at least ${bld}1${rs}.\n", expr);
		return NULL;
	}

	parser->limit_expr = expr;
	return expr;
}

/**
 * Parse -links N.
 */
static struct bfs_expr *parse_links(struct bfs_parser *parser, int arg1, int arg2) {
	return parse_test_icmp(parser, eval_links);
}

/**
 * Parse -ls.
 */
static struct bfs_expr *parse_ls(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_action(parser, eval_fls);
	if (!expr) {
		return NULL;
	}

	init_print_expr(parser, expr);
	return expr;
}

/**
 * Parse -mount.
 */
static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_option(parser);
	if (!expr) {
		return NULL;
	}

	parser->ctx->flags |= BFTW_SKIP_MOUNTS;
	parser->mount_expr = expr;
	return expr;
}

/**
 * Parse -i?name.
 */
static struct bfs_expr *parse_name(struct bfs_parser *parser, int casefold, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_name);
	return parse_fnmatch(parser, expr, casefold);
}

/**
 * Parse -i?path, -i?wholename.
 */
static struct bfs_expr *parse_path(struct bfs_parser *parser, int casefold, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_path);
	return parse_fnmatch(parser, expr, casefold);
}

/**
 * Parse -i?lname.
 */
static struct bfs_expr *parse_lname(struct bfs_parser *parser, int casefold, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_lname);
	return parse_fnmatch(parser, expr, casefold);
}

/** Get the bfs_stat_field for X/Y in -newerXY. */
static enum bfs_stat_field parse_newerxy_field(char c) {
	switch (c) {
	case 'a':
		return BFS_STAT_ATIME;
	case 'B':
		return BFS_STAT_BTIME;
	case 'c':
		return BFS_STAT_CTIME;
	case 'm':
		return BFS_STAT_MTIME;
	default:
		return 0;
	}
}

/** Parse an explicit reference timestamp for -newerXt and -*since. */
static int parse_reftime(const struct bfs_parser *parser, struct bfs_expr *expr) {
	if (xgetdate(expr->argv[1], &expr->reftime) == 0) {
		return 0;
	} else if (errno != EINVAL) {
		parse_expr_error(parser, expr, "%s.\n", errstr());
		return -1;
	}

	parse_expr_error(parser, expr, "Invalid timestamp.\n\n");
	fprintf(stderr, "Supported timestamp formats are ISO 8601-like, e.g.\n\n");

	struct tm tm;
	if (!localtime_r(&parser->now.tv_sec, &tm)) {
		parse_perror(parser, "localtime_r()");
		return -1;
	}

	int year = tm.tm_year + 1900;
	int month = tm.tm_mon + 1;
	fprintf(stderr, "  - %04d-%02d-%02d\n", year, month, tm.tm_mday);
	fprintf(stderr, "  - %04d-%02d-%02dT%02d:%02d:%02d\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);

#if BFS_HAS_TM_GMTOFF
	int gmtoff = tm.tm_gmtoff;
#else
	int gmtoff = -timezone;
#endif
	int tz_hour = gmtoff / 3600;
	int tz_min = (labs(gmtoff) / 60) % 60;
	fprintf(stderr, "  - %04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d\n",
		year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min);

	if (!gmtime_r(&parser->now.tv_sec, &tm)) {
		parse_perror(parser, "gmtime_r()");
		return -1;
	}

	year = tm.tm_year + 1900;
	month = tm.tm_mon + 1;
	fprintf(stderr, "  - %04d-%02d-%02dT%02d:%02d:%02dZ\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);

	return -1;
}

/**
 * Parse -newerXY.
 */
static struct bfs_expr *parse_newerxy(struct bfs_parser *parser, int arg1, int arg2) {
	const char *arg = parser->argv[0];
	if (strlen(arg) != 8) {
		parse_error(parser, "Expected ${blu}-newer${bld}XY${rs}; found ${blu}-newer${bld}%pq${rs}.\n", arg + 6);
		return NULL;
	}

	struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
	if (!expr) {
		return NULL;
	}

	expr->stat_field = parse_newerxy_field(arg[6]);
	if (!expr->stat_field) {
		parse_expr_error(parser, expr,
			"For ${blu}-newer${bld}XY${rs}, ${bld}X${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, or ${bld}B${rs}, not ${err}%c${rs}.\n",
			arg[6]);
		return NULL;
	}

	if (arg[7] == 't') {
		if (parse_reftime(parser, expr) != 0) {
			return NULL;
		}
	} else {
		enum bfs_stat_field field = parse_newerxy_field(arg[7]);
		if (!field) {
			parse_expr_error(parser, expr,
				"For ${blu}-newer${bld}XY${rs}, ${bld}Y${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, ${bld}B${rs}, or ${bld}t${rs}, not ${err}%c${rs}.\n",
				arg[7]);
			return NULL;
		}

		struct bfs_stat sb;
		if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
			return NULL;
		}

		const struct timespec *reftime = bfs_stat_time(&sb, field);
		if (!reftime) {
			parse_expr_error(parser, expr, "Couldn't get file %s.\n", bfs_stat_field_name(field));
			return NULL;
		}

		expr->reftime = *reftime;
	}

	return expr;
}

/**
 * Parse -noerror.
 */
static struct bfs_expr *parse_noerror(struct bfs_parser *parser, int arg1, int arg2) {
	parser->ctx->ignore_errors = true;
	return parse_nullary_option(parser);
}

/**
 * Parse -nogroup.
 */
static struct bfs_expr *parse_nogroup(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_test(parser, eval_nogroup);
	if (expr) {
		// Who knows how many FDs getgrgid_r() needs?
		expr->ephemeral_fds = 3;
	}
	return expr;
}

/**
 * Parse -nohidden.
 */
static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *hidden = parse_new_expr(parser, eval_hidden, 1, &fake_hidden_arg, BFS_TEST);
	if (!hidden) {
		return NULL;
	}

	bfs_expr_append(parser->ctx->exclude, hidden);
	return parse_nullary_option(parser);
}

/**
 * Parse -noleaf.
 */
static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) {
	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;
}

/**
 * Parse -nouser.
 */
static struct bfs_expr *parse_nouser(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_test(parser, eval_nouser);
	if (expr) {
		// Who knows how many FDs getpwuid_r() needs?
		expr->ephemeral_fds = 3;
	}
	return expr;
}

/**
 * Parse a permission mode like chmod(1).
 */
static int parse_mode(const struct bfs_parser *parser, const char *mode, struct bfs_expr *expr) {
	if (mode[0] >= '0' && mode[0] <= '9') {
		unsigned int parsed;
		if (!parse_int(parser, NULL, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) {
			goto fail;
		}
		if (parsed > 07777) {
			goto fail;
		}

		expr->file_mode = parsed;
		expr->dir_mode = parsed;
		return 0;
	}

	mode_t umask = parser->ctx->umask;

	expr->file_mode = 0;
	expr->dir_mode = 0;

	// Parse the same grammar as chmod(1), which looks like this:
	//
	// MODE : CLAUSE ["," CLAUSE]*
	//
	// CLAUSE : WHO* ACTION+
	//
	// WHO : "u" | "g" | "o" | "a"
	//
	// ACTION : OP PERM*
	//        | OP PERMCOPY
	//
	// OP : "+" | "-" | "="
	//
	// PERM : "r" | "w" | "x" | "X" | "s" | "t"
	//
	// PERMCOPY : "u" | "g" | "o"

	// State machine state
	enum {
		MODE_CLAUSE,
		MODE_WHO,
		MODE_ACTION,
		MODE_ACTION_APPLY,
		MODE_OP,
		MODE_PERM,
	} state = MODE_CLAUSE;

	enum {
		MODE_PLUS,
		MODE_MINUS,
		MODE_EQUALS,
	} op uninit(MODE_EQUALS);

	mode_t who uninit(0);
	mode_t mask uninit(0);
	mode_t file_change uninit(0);
	mode_t dir_change uninit(0);

	const char *i = mode;
	while (true) {
		switch (state) {
		case MODE_CLAUSE:
			who = 0;
			mask = 0777;
			state = MODE_WHO;
			[[fallthrough]];

		case MODE_WHO:
			switch (*i) {
			case 'u':
				who |= 0700;
				break;
			case 'g':
				who |= 0070;
				break;
			case 'o':
				who |= 0007;
				break;
			case 'a':
				who |= 0777;
				break;
			default:
				state = MODE_ACTION;
				continue;
			}
			break;

		case MODE_ACTION_APPLY:
			switch (op) {
			case MODE_EQUALS:
				expr->file_mode &= ~who;
				expr->dir_mode &= ~who;
				[[fallthrough]];
			case MODE_PLUS:
				expr->file_mode |= file_change;
				expr->dir_mode |= dir_change;
				break;
			case MODE_MINUS:
				expr->file_mode &= ~file_change;
				expr->dir_mode &= ~dir_change;
				break;
			}
			[[fallthrough]];

		case MODE_ACTION:
			if (who == 0) {
				who = 0777;
				mask = who & ~umask;
			} else {
				mask = who;
			}

			switch (*i) {
			case '+':
				op = MODE_PLUS;
				state = MODE_OP;
				break;
			case '-':
				op = MODE_MINUS;
				state = MODE_OP;
				break;
			case '=':
				op = MODE_EQUALS;
				state = MODE_OP;
				break;

			case ',':
				if (state == MODE_ACTION_APPLY) {
					state = MODE_CLAUSE;
				} else {
					goto fail;
				}
				break;

			case '\0':
				if (state == MODE_ACTION_APPLY) {
					goto done;
				} else {
					goto fail;
				}

			default:
				goto fail;
			}
			break;

		case MODE_OP:
			switch (*i) {
			case 'u':
				file_change = (expr->file_mode >> 6) & 07;
				dir_change = (expr->dir_mode >> 6) & 07;
				break;
			case 'g':
				file_change = (expr->file_mode >> 3) & 07;
				dir_change = (expr->dir_mode >> 3) & 07;
				break;
			case 'o':
				file_change = expr->file_mode & 07;
				dir_change = expr->dir_mode & 07;
				break;

			default:
				file_change = 0;
				dir_change = 0;
				state = MODE_PERM;
				continue;
			}

			file_change |= (file_change << 6) | (file_change << 3);
			file_change &= mask;
			dir_change |= (dir_change << 6) | (dir_change << 3);
			dir_change &= mask;
			state = MODE_ACTION_APPLY;
			break;

		case MODE_PERM:
			switch (*i) {
			case 'r':
				file_change |= mask & 0444;
				dir_change |= mask & 0444;
				break;
			case 'w':
				file_change |= mask & 0222;
				dir_change |= mask & 0222;
				break;
			case 'x':
				file_change |= mask & 0111;
				[[fallthrough]];
			case 'X':
				dir_change |= mask & 0111;
				break;
			case 's':
				if (who & 0700) {
					file_change |= S_ISUID;
					dir_change |= S_ISUID;
				}
				if (who & 0070) {
					file_change |= S_ISGID;
					dir_change |= S_ISGID;
				}
				break;
			case 't':
				if (who & 0007) {
					file_change |= S_ISVTX;
					dir_change |= S_ISVTX;
				}
				break;
			default:
				state = MODE_ACTION_APPLY;
				continue;
			}
			break;
		}

		++i;
	}

done:
	return 0;

fail:
	parse_expr_error(parser, expr, "Invalid mode.\n");
	return -1;
}

/**
 * Parse -perm MODE.
 */
static struct bfs_expr *parse_perm(struct bfs_parser *parser, int field, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_perm);
	if (!expr) {
		return NULL;
	}

	const char *mode = expr->argv[1];
	switch (mode[0]) {
	case '-':
		expr->mode_cmp = BFS_MODE_ALL;
		++mode;
		break;
	case '/':
		expr->mode_cmp = BFS_MODE_ANY;
		++mode;
		break;
	case '+':
		if (mode[1] >= '0' && mode[1] <= '9') {
			expr->mode_cmp = BFS_MODE_ANY;
			++mode;
			break;
		}
		[[fallthrough]];
	default:
		expr->mode_cmp = BFS_MODE_EQUAL;
		break;
	}

	if (parse_mode(parser, mode, expr) != 0) {
		return NULL;
	}

	return expr;
}

/**
 * Parse -print.
 */
static struct bfs_expr *parse_print(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_action(parser, eval_fprint);
	if (expr) {
		init_print_expr(parser, expr);
	}
	return expr;
}

/**
 * Parse -print0.
 */
static struct bfs_expr *parse_print0(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_action(parser, eval_fprint0);
	if (expr) {
		init_print_expr(parser, expr);
	}
	return expr;
}

/**
 * Parse -printf FORMAT.
 */
static struct bfs_expr *parse_printf(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_action(parser, eval_fprintf);
	if (!expr) {
		return NULL;
	}

	init_print_expr(parser, expr);

	if (bfs_printf_parse(parser->ctx, expr, expr->argv[1]) != 0) {
		return NULL;
	}

	return expr;
}

/**
 * Parse -printx.
 */
static struct bfs_expr *parse_printx(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_action(parser, eval_fprintx);
	if (expr) {
		init_print_expr(parser, expr);
	}
	return expr;
}

/**
 * Parse -prune.
 */
static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_nullary_action(parser, eval_prune);
	if (!expr) {
		return NULL;
	}

	parser->prune_expr = expr;
	return expr;
}

/**
 * Parse -quit.
 */
static struct bfs_expr *parse_quit(struct bfs_parser *parser, int arg1, int arg2) {
	return parse_nullary_action(parser, eval_quit);
}

/**
 * Parse -i?regex.
 */
static struct bfs_expr *parse_regex(struct bfs_parser *parser, int flags, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_regex);
	if (!expr) {
		return NULL;
	}

	if (bfs_regcomp(&expr->regex, expr->argv[1], parser->regex_type, flags) != 0) {
		if (expr->regex) {
			char *str = bfs_regerror(expr->regex);
			if (str) {
				parse_expr_error(parser, expr, "%s.\n", str);
				free(str);
			} else {
				parse_perror(parser, "bfs_regerror()");
			}
		} else {
			parse_perror(parser, "bfs_regcomp()");
		}

		return NULL;
	}

	return expr;
}

/**
 * Parse -E.
 */
static struct bfs_expr *parse_regex_extended(struct bfs_parser *parser, int arg1, int arg2) {
	parser->regex_type = BFS_REGEX_POSIX_EXTENDED;
	return parse_nullary_flag(parser);
}

/**
 * Parse -regextype TYPE.
 */
static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_ctx *ctx = parser->ctx;
	CFILE *cfile = ctx->cerr;

	struct bfs_expr *expr = parse_unary_option(parser);
	if (!expr) {
		cfprintf(cfile, "\n");
		goto list_types;
	}

	// See https://www.gnu.org/software/gnulib/manual/html_node/Predefined-Syntaxes.html
	const char *type = expr->argv[1];
	if (strcmp(type, "posix-basic") == 0
	    || strcmp(type, "posix-minimal-basic") == 0
	    || strcmp(type, "ed") == 0
	    || strcmp(type, "sed") == 0) {
		parser->regex_type = BFS_REGEX_POSIX_BASIC;
	} else if (strcmp(type, "posix-extended") == 0) {
		parser->regex_type = BFS_REGEX_POSIX_EXTENDED;
#if BFS_WITH_ONIGURUMA
	} else if (strcmp(type, "awk") == 0
	           || strcmp(type, "posix-awk") == 0) {
		parser->regex_type = BFS_REGEX_AWK;
	} else if (strcmp(type, "gnu-awk") == 0) {
		parser->regex_type = BFS_REGEX_GNU_AWK;
	} else if (strcmp(type, "emacs") == 0) {
		parser->regex_type = BFS_REGEX_EMACS;
	} else if (strcmp(type, "grep") == 0) {
		parser->regex_type = BFS_REGEX_GREP;
	} else if (strcmp(type, "egrep") == 0
	           || strcmp(type, "posix-egrep") == 0) {
		parser->regex_type = BFS_REGEX_EGREP;
	} else if (strcmp(type, "findutils-default") == 0) {
		parser->regex_type = BFS_REGEX_GNU_FIND;
#endif
	} else if (strcmp(type, "help") == 0) {
		parser->just_info = true;
		cfile = ctx->cout;
		goto list_types;
	} else {
		parse_expr_error(parser, expr, "Unsupported regex type.\n\n");
		goto list_types;
	}

	return expr;

list_types:
	cfprintf(cfile, "Supported types are:\n\n");
	cfprintf(cfile, "        ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n");
	cfprintf(cfile, "                 ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n");
	cfprintf(cfile, "                ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n\n");

	cfprintf(cfile, "     ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n\n");

#if BFS_WITH_ONIGURUMA
	cfprintf(cfile, "        [${bld}posix-${rs}]${bld}awk${rs}: Like ${grn}awk${rs}\n");
	cfprintf(cfile, "            ${bld}gnu-awk${rs}: Like GNU ${grn}awk${rs}\n\n");

	cfprintf(cfile, "              ${bld}emacs${rs}: Like ${grn}emacs${rs}\n\n");

	cfprintf(cfile, "               ${bld}grep${rs}: Like ${grn}grep${rs}\n");
	cfprintf(cfile, "      [${bld}posix-${rs}]${bld}egrep${rs}: Like ${grn}grep${rs} ${cyn}-E${rs}\n\n");

	cfprintf(cfile, "  ${bld}findutils-default${rs}: Like GNU ${grn}find${rs}\n");
#endif
	return NULL;
}

/**
 * Parse -s.
 */
static struct bfs_expr *parse_s(struct bfs_parser *parser, int arg1, int arg2) {
	parser->ctx->flags |= BFTW_SORT;
	return parse_nullary_flag(parser);
}

/**
 * Parse -samefile FILE.
 */
static struct bfs_expr *parse_samefile(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_samefile);
	if (!expr) {
		return NULL;
	}

	struct bfs_stat sb;
	if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
		return NULL;
	}

	expr->dev = sb.dev;
	expr->ino = sb.ino;
	return expr;
}

/**
 * Parse -S STRATEGY.
 */
static struct bfs_expr *parse_search_strategy(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_ctx *ctx = parser->ctx;
	CFILE *cfile = ctx->cerr;

	const char *arg;
	struct bfs_expr *expr = parse_prefix_flag(parser, 'S', true, &arg);
	if (!expr) {
		cfprintf(cfile, "\n");
		goto list_strategies;
	}

	if (strcmp(arg, "bfs") == 0) {
		ctx->strategy = BFTW_BFS;
	} else if (strcmp(arg, "dfs") == 0) {
		ctx->strategy = BFTW_DFS;
	} else if (strcmp(arg, "ids") == 0) {
		ctx->strategy = BFTW_IDS;
	} else if (strcmp(arg, "eds") == 0) {
		ctx->strategy = BFTW_EDS;
	} else if (strcmp(arg, "help") == 0) {
		parser->just_info = true;
		cfile = ctx->cout;
		goto list_strategies;
	} else {
		parse_expr_error(parser, expr, "Unrecognized search strategy.\n\n");
		goto list_strategies;
	}

	return expr;

list_strategies:
	cfprintf(cfile, "Supported search strategies:\n\n");
	cfprintf(cfile, "  ${bld}bfs${rs}: breadth-first search\n");
	cfprintf(cfile, "  ${bld}dfs${rs}: depth-first search\n");
	cfprintf(cfile, "  ${bld}ids${rs}: iterative deepening search\n");
	cfprintf(cfile, "  ${bld}eds${rs}: exponential deepening search\n");
	return NULL;
}

/**
 * Parse -[aBcm]?since.
 */
static struct bfs_expr *parse_since(struct bfs_parser *parser, int field, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
	if (!expr) {
		return NULL;
	}

	if (parse_reftime(parser, expr) != 0) {
		return NULL;
	}

	expr->stat_field = field;
	return expr;
}

/**
 * Parse -size N[cwbkMGTP]?.
 */
static struct bfs_expr *parse_size(struct bfs_parser *parser, int arg1, int arg2) {
	struct bfs_expr *expr = parse_unary_test(parser, eval_size);
	if (!expr) {
		return NULL;
	}

	const char *unit = parse_icmp(parser, expr, IF_PARTIAL_OK);
	if (!unit) {
		return NULL;
	}

	if (strlen(unit) > 1) {
		goto bad_unit;
	}

	switch (*unit) {
	case '\0':
	case 'b':
		expr->size_unit = BFS_BLOCKS;
		break;
	case 'c':
		expr->size_unit = BFS_BYTES;
		break;
	case 'w':
		expr->size_unit = BFS_WORDS;
		break;
	case 'k':
		expr->size_unit = BFS_KB;
		break;
	case 'M':
		expr->size_unit = BFS_MB;
		break;
	case 'G':
		expr->size_unit = BFS_GB;
		break;
	case 'T':
		expr->size_unit = BFS_TB;
		break;
	case 'P':
		expr->size_unit = BFS_PB;
		break;

	default:
	bad_unit:
		parse_expr_error(parser, expr, "Expected a size unit (one of ${bld}cwbkMGTP${rs}); found ${err}%pq${rs}.\n", unit);
		return NULL;
	}

	return expr;
}

/**
 * Parse -sparse.
 */
static struct bfs_expr *parse_sparse(struct bfs_parser *parser, int arg1, int arg2) {
	return parse_nullary_test(parser, eval_sparse);
}

/**
 * Parse -status.
 */
static struct bfs_expr *parse_status(struct bfs_parser *parser, int arg1, int arg2) {
	parser->ctx->status = true;
	return parse_nullary_option(parser);
}

/**
 * Parse -x?type [bcdpflsD].
 */
static struct bfs_expr *parse_type(struct bfs_parser *parser, int x, int arg2) {
	struct bfs_ctx *ctx = parser->ctx;

	bfs_eval_fn *eval = x ? eval_xtype : eval_type;
	struct bfs_expr *expr = parse_unary_test(parser, eval);
	if (!expr) {
		return NULL;
	}

	expr->num = 0;

	const char *c = expr->argv[1];
	while (true) {
		switch (*c) {
		case 'b':
			expr->num |= 1 << BFS_BLK;
			break;
		case 'c':
			expr->num |= 1 << BFS_CHR;
			break;
		case 'd':
			expr->num |= 1 << BFS_DIR;
			break;
		case 'D':
			expr->num |= 1 << BFS_DOOR;
			break;
		case 'p':
			expr->num |= 1 << BFS_FIFO;
			break;
		case 'f':
			expr->num |= 1 << BFS_REG;
			break;
		case 'l':
			expr->num |= 1 << BFS_LNK;
			break;
		case 's':
			expr->num |= 1 << BFS_SOCK;
			break;
		case 'w':
			expr->num |= 1 << BFS_WHT;
			ctx->flags |= BFTW_WHITEOUTS;
			break;

		case '\0':
			parse_expr_error(parser, expr, "Expected a type flag.\n");
			return NULL;

		default:
			parse_expr_error(parser, expr, "Unknown type flag ${err}%c${rs}; expected one of [${bld}bcdpflsD${rs}].\n", *c);
			return NULL;
		}

		++c;
		if (*c == '\0') {
			break;
		} else if (*c == ',') {
			++c;
			continue;
		} else {
			parse_expr_error(parser, expr, "Types must be comma-separated.\n");
			return NULL;
		}
	}

	return expr;
}

/**
 * Parse -(no)?warn.
 */
static struct bfs_expr *parse_warn(struct bfs_parser *parser, int warn, int arg2) {
	parser->ctx->warn = warn;
	return parse_nullary_option(parser);
}

/**
 * Parse -xattr.
 */
static struct bfs_expr *parse_xattr(struct bfs_parser *parser, int arg1, int arg2) {
#if BFS_CAN_CHECK_XATTRS
	return parse_nullary_test(parser, eval_xattr);
#else
	parse_error(parser, "Missing platform support.\n");
	return NULL;
#endif
}

/**
 * Parse -xattrname.
 */
static struct bfs_expr *parse_xattrname(struct bfs_parser *parser, int arg1, int arg2) {
#if BFS_CAN_CHECK_XATTRS
	return parse_unary_test(parser, eval_xattrname);
#else
	parse_error(parser, "Missing platform support.\n");
	return NULL;
#endif
}

/**
 * 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_expr = expr;
	return expr;
}

/**
 * Launch a pager for the help output.
 */
static CFILE *launch_pager(pid_t *pid, CFILE *cout) {
	char *pager = getenv("PAGER");

	char *exe;
	if (pager && pager[0]) {
		exe = bfs_spawn_resolve(pager);
	} else {
		exe = bfs_spawn_resolve("less");
		if (!exe) {
			exe = bfs_spawn_resolve("more");
		}
	}
	if (!exe) {
		goto fail;
	}

	int pipefd[2];
	if (pipe(pipefd) != 0) {
		goto fail_exe;
	}

	FILE *file = fdopen(pipefd[1], "w");
	if (!file) {
		goto fail_pipe;
	}
	pipefd[1] = -1;

	CFILE *ret = cfwrap(file, NULL, true);
	if (!ret) {
		goto fail_file;
	}
	file = NULL;

	struct bfs_spawn ctx;
	if (bfs_spawn_init(&ctx) != 0) {
		goto fail_ret;
	}

	if (bfs_spawn_addclose(&ctx, fileno(ret->file)) != 0) {
		goto fail_ctx;
	}
	if (bfs_spawn_adddup2(&ctx, pipefd[0], STDIN_FILENO) != 0) {
		goto fail_ctx;
	}
	if (bfs_spawn_addclose(&ctx, pipefd[0]) != 0) {
		goto fail_ctx;
	}

	char *argv[] = {
		exe,
		NULL,
		NULL,
	};

	const char *cmd = exe + xbaseoff(exe);
	if (strcmp(cmd, "less") == 0) {
		// We know less supports colors, other pagers may not
		ret->colors = cout->colors;
		argv[1] = "-FKRX";
	}

	*pid = bfs_spawn(exe, &ctx, argv, NULL);
	if (*pid < 0) {
		goto fail_ctx;
	}

	xclose(pipefd[0]);
	bfs_spawn_destroy(&ctx);
	free(exe);
	return ret;

fail_ctx:
	bfs_spawn_destroy(&ctx);
fail_ret:
	cfclose(ret);
fail_file:
	if (file) {
		fclose(file);
	}
fail_pipe:
	if (pipefd[1] >= 0) {
		xclose(pipefd[1]);
	}
	if (pipefd[0] >= 0) {
		xclose(pipefd[0]);
	}
fail_exe:
	free(exe);
fail:
	return cout;
}

/**
 * "Parse" -help.
 */
static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2) {
	CFILE *cout = parser->ctx->cout;

	pid_t pager = -1;
	if (parser->stdout_tty) {
		cout = launch_pager(&pager, cout);
	}

	cfprintf(cout, "Usage: ${ex}%s${rs} [${cyn}flags${rs}...] [${mag}paths${rs}...] [${blu}expression${rs}...]\n\n",
		parser->command);

	cfprintf(cout, "${ex}%s${rs} is compatible with ${ex}find${rs}, with some extensions. "
	               "${cyn}Flags${rs} (${cyn}-H${rs}/${cyn}-L${rs}/${cyn}-P${rs} etc.), ${mag}paths${rs},\n"
	               "and ${blu}expressions${rs} may be freely mixed in any order.\n\n",
		BFS_COMMAND);

	cfprintf(cout, "${bld}Flags:${rs}\n\n");

	cfprintf(cout, "  ${cyn}-H${rs}\n");
	cfprintf(cout, "      Follow symbolic links on the command line, but not while searching\n");
	cfprintf(cout, "  ${cyn}-L${rs}\n");
	cfprintf(cout, "      Follow all symbolic links\n");
	cfprintf(cout, "  ${cyn}-P${rs}\n");
	cfprintf(cout, "      Never follow symbolic links (the default)\n");

	cfprintf(cout, "  ${cyn}-E${rs}\n");
	cfprintf(cout, "      Use extended regular expressions (same as ${blu}-regextype${rs} ${bld}posix-extended${rs})\n");
	cfprintf(cout, "  ${cyn}-X${rs}\n");
	cfprintf(cout, "      Filter out files with non-${ex}xargs${rs}-safe names\n");
	cfprintf(cout, "  ${cyn}-d${rs}\n");
	cfprintf(cout, "      Search in post-order (same as ${blu}-depth${rs})\n");
	cfprintf(cout, "  ${cyn}-s${rs}\n");
	cfprintf(cout, "      Visit directory entries in sorted order\n");
	cfprintf(cout, "  ${cyn}-x${rs}\n");
	cfprintf(cout, "      Don't descend into other mount points (same as ${blu}-xdev${rs})\n");

	cfprintf(cout, "  ${cyn}-f${rs} ${mag}PATH${rs}\n");
	cfprintf(cout, "      Treat ${mag}PATH${rs} as a path to search (useful if begins with a dash)\n");
	cfprintf(cout, "  ${cyn}-D${rs} ${bld}FLAG${rs}\n");
	cfprintf(cout, "      Turn on a debugging flag (see ${cyn}-D${rs} ${bld}help${rs})\n");
	cfprintf(cout, "  ${cyn}-O${bld}N${rs}\n");
	cfprintf(cout, "      Enable optimization level ${bld}N${rs} (default: ${bld}3${rs})\n");
	cfprintf(cout, "  ${cyn}-S${rs} ${bld}bfs${rs}|${bld}dfs${rs}|${bld}ids${rs}|${bld}eds${rs}\n");
	cfprintf(cout, "      Use ${bld}b${rs}readth-${bld}f${rs}irst/${bld}d${rs}epth-${bld}f${rs}irst/${bld}i${rs}terative/${bld}e${rs}xponential ${bld}d${rs}eepening ${bld}s${rs}earch\n");
	cfprintf(cout, "      (default: ${cyn}-S${rs} ${bld}bfs${rs})\n");
	cfprintf(cout, "  ${cyn}-j${bld}N${rs}\n");
	cfprintf(cout, "      Search with ${bld}N${rs} threads in parallel (default: number of CPUs, up to ${bld}8${rs})\n\n");

	cfprintf(cout, "${bld}Operators:${rs}\n\n");

	cfprintf(cout, "  ${red}(${rs} ${blu}expression${rs} ${red})${rs}\n\n");

	cfprintf(cout, "  ${red}!${rs} ${blu}expression${rs}\n");
	cfprintf(cout, "  ${red}-not${rs} ${blu}expression${rs}\n\n");

	cfprintf(cout, "  ${blu}expression${rs} ${blu}expression${rs}\n");
	cfprintf(cout, "  ${blu}expression${rs} ${red}-a${rs} ${blu}expression${rs}\n");
	cfprintf(cout, "  ${blu}expression${rs} ${red}-and${rs} ${blu}expression${rs}\n\n");

	cfprintf(cout, "  ${blu}expression${rs} ${red}-o${rs} ${blu}expression${rs}\n");
	cfprintf(cout, "  ${blu}expression${rs} ${red}-or${rs} ${blu}expression${rs}\n\n");

	cfprintf(cout, "  ${blu}expression${rs} ${red},${rs} ${blu}expression${rs}\n\n");

	cfprintf(cout, "${bld}Special forms:${rs}\n\n");

	cfprintf(cout, "  ${red}-exclude${rs} ${blu}expression${rs}\n");
	cfprintf(cout, "      Exclude all paths matching the ${blu}expression${rs} from the search.\n\n");

	cfprintf(cout, "${bld}Options:${rs}\n\n");

	cfprintf(cout, "  ${blu}-color${rs}\n");
	cfprintf(cout, "  ${blu}-nocolor${rs}\n");
	cfprintf(cout, "      Turn colors on or off (default: ${blu}-color${rs} if outputting to a terminal,\n");
	cfprintf(cout, "      ${blu}-nocolor${rs} otherwise)\n");
	cfprintf(cout, "  ${blu}-daystart${rs}\n");
	cfprintf(cout, "      Measure times relative to the start of today\n");
	cfprintf(cout, "  ${blu}-depth${rs}\n");
	cfprintf(cout, "      Search in post-order (descendents first)\n");
	cfprintf(cout, "  ${blu}-files0-from${rs} ${bld}FILE${rs}\n");
	cfprintf(cout, "      Search the NUL ('\\0')-separated paths from ${bld}FILE${rs} (${bld}-${rs} for standard input).\n");
	cfprintf(cout, "  ${blu}-follow${rs}\n");
	cfprintf(cout, "      Follow all symbolic links (same as ${cyn}-L${rs})\n");
	cfprintf(cout, "  ${blu}-ignore_readdir_race${rs}\n");
	cfprintf(cout, "  ${blu}-noignore_readdir_race${rs}\n");
	cfprintf(cout, "      Whether to report an error if ${ex}%s${rs} detects that the file tree is modified\n",
		BFS_COMMAND);
	cfprintf(cout, "      during the search (default: ${blu}-noignore_readdir_race${rs})\n");
	cfprintf(cout, "  ${blu}-maxdepth${rs} ${bld}N${rs}\n");
	cfprintf(cout, "  ${blu}-mindepth${rs} ${bld}N${rs}\n");
	cfprintf(cout, "      Ignore files deeper/shallower than ${bld}N${rs}\n");
	cfprintf(cout, "  ${blu}-mount${rs}\n");
	cfprintf(cout, "      Exclude mount points entirely from the results\n");
	cfprintf(cout, "  ${blu}-noerror${rs}\n");
	cfprintf(cout, "      Ignore any errors that occur during traversal\n");
	cfprintf(cout, "  ${blu}-nohidden${rs}\n");
	cfprintf(cout, "      Exclude hidden files\n");
	cfprintf(cout, "  ${blu}-noleaf${rs}\n");
	cfprintf(cout, "      Ignored; for compatibility with GNU find\n");
	cfprintf(cout, "  ${blu}-regextype${rs} ${bld}TYPE${rs}\n");
	cfprintf(cout, "      Use ${bld}TYPE${rs}-flavored regexes (default: ${bld}posix-basic${rs}; see ${blu}-regextype${rs} ${bld}help${rs})\n");
	cfprintf(cout, "  ${blu}-status${rs}\n");
	cfprintf(cout, "      Display a status bar while searching\n");
	cfprintf(cout, "  ${blu}-unique${rs}\n");
	cfprintf(cout, "      Skip any files that have already been seen\n");
	cfprintf(cout, "  ${blu}-warn${rs}\n");
	cfprintf(cout, "  ${blu}-nowarn${rs}\n");
	cfprintf(cout, "      Turn on or off warnings about the command line\n");
	cfprintf(cout, "  ${blu}-xdev${rs}\n");
	cfprintf(cout, "      Don't descend into other mount points\n\n");

	cfprintf(cout, "${bld}Tests:${rs}\n\n");

#if BFS_CAN_CHECK_ACL
	cfprintf(cout, "  ${blu}-acl${rs}\n");
	cfprintf(cout, "      Find files with a non-trivial Access Control List\n");
#endif
	cfprintf(cout, "  ${blu}-${rs}[${blu}aBcm${rs}]${blu}min${rs} ${bld}[-+]N${rs}\n");
	cfprintf(cout, "      Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} minutes ago\n");
	cfprintf(cout, "  ${blu}-${rs}[${blu}aBcm${rs}]${blu}newer${rs} ${bld}FILE${rs}\n");
	cfprintf(cout, "      Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified more recently than ${bld}FILE${rs} was\n"
	               "      modified\n");
	cfprintf(cout, "  ${blu}-${rs}[${blu}aBcm${rs}]${blu}since${rs} ${bld}TIME${rs}\n");
	cfprintf(cout, "      Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified more recently than ${bld}TIME${rs}\n");
	cfprintf(cout, "  ${blu}-${rs}[${blu}aBcm${rs}]${blu}time${rs} ${bld}[-+]N${rs}\n");
	cfprintf(cout, "      Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} days ago\n");
#if BFS_CAN_CHECK_CAPABILITIES
	cfprintf(cout, "  ${blu}-capable${rs}\n");
	cfprintf(cout, "      Find files with POSIX.1e capabilities set\n");
#endif
#if BFS_CAN_CHECK_CONTEXT
	cfprintf(cout, "  ${blu}-context${rs} ${bld}GLOB${rs}\n");
	cfprintf(cout, "      Find files with SELinux context matching a glob pattern\n");
#endif
	cfprintf(cout, "  ${blu}-depth${rs} ${bld}[-+]N${rs}\n");
	cfprintf(cout, "      Find files with depth ${bld}N${rs}\n");
	cfprintf(cout, "  ${blu}-empty${rs}\n");
	cfprintf(cout, "      Find empty files/directories\n");
	cfprintf(cout, "  ${blu}-executable${rs}\n");
	cfprintf(cout, "  ${blu}-readable${rs}\n");
	cfprintf(cout, "  ${blu}-writable${rs}\n");
	cfprintf(cout, "      Find files the current user can execute/read/write\n");
	cfprintf(cout, "  ${blu}-false${rs}\n");
	cfprintf(cout, "  ${blu}-true${rs}\n");
	cfprintf(cout, "      Always false/true\n");
	cfprintf(cout, "  ${blu}-fstype${rs} ${bld}TYPE${rs}\n");
	cfprintf(cout, "      Find files on file systems with the given ${bld}TYPE${rs}\n");
	cfprintf(cout, "  ${blu}-gid${rs} ${bld}[-+]N${rs}\n");
	cfprintf(cout, "  ${blu}-uid${rs} ${bld}[-+]N${rs}\n");
	cfprintf(cout, "      Find files owned by group/user ID ${bld}N${rs}\n");
	cfprintf(cout, "  ${blu}-group${rs} ${bld}NAME${rs}\n");
	cfprintf(cout, "  ${blu}-user${rs}  ${bld}NAME${rs}\n");
	cfprintf(cout, "      Find files owned by the group/user ${bld}NAME${rs}\n");
	cfprintf(cout, "  ${blu}-hidden${rs}\n");
	cfprintf(cout, "      Find hidden files\n");
#ifdef FNM_CASEFOLD
	cfprintf(cout, "  ${blu}-ilname${rs} ${bld}GLOB${rs}\n");
	cfprintf(cout, "  ${blu}-iname${rs}  ${bld}GLOB${rs}\n");
	cfprintf(cout, "  ${blu}-ipath${rs}  ${bld}GLOB${rs}\n");
	cfprintf(cout, "  ${blu}-iregex${rs} ${bld}REGEX${rs}\n");
	cfprintf(cout, "  ${blu}-iwholename${rs} ${bld}GLOB${rs}\n");
	cfprintf(cout, "      Case-insensitive versions of ${blu}-lname${rs}/${blu}-name${rs}/${blu}-path${rs}"
	               "/${blu}-regex${rs}/${blu}-wholename${rs}\n");
#endif
	cfprintf(cout, "  ${blu}-inum${rs} ${bld}[-+]N${rs}\n");
	cfprintf(cout, "      Find files with inode number ${bld}N${rs}\n");
	cfprintf(cout, "  ${blu}-links${rs} ${bld}[-+]N${rs}\n");
	cfprintf(cout, "      Find files with ${bld}N${rs} hard links\n");
	cfprintf(cout, "  ${blu}-lname${rs} ${bld}GLOB${rs}\n");
	cfprintf(cout, "      Find symbolic links whose target matches the ${bld}GLOB${rs}\n");
	cfprintf(cout, "  ${blu}-name${rs} ${bld}GLOB${rs}\n");
	cfprintf(cout, "      Find files whose name matches the ${bld}GLOB${rs}\n");
	cfprintf(cout, "  ${blu}-newer${rs} ${bld}FILE${rs}\n");
	cfprintf(cout, "      Find files newer than ${bld}FILE${rs}\n");
	cfprintf(cout, "  ${blu}-newer${bld}XY${rs} ${bld}REFERENCE${rs}\n");
	cfprintf(cout, "      Find files whose ${bld}X${rs} time is newer than the ${bld}Y${rs} time of"
	               " ${bld}REFERENCE${rs}.  ${bld}X${rs} and ${bld}Y${rs}\n");
	cfprintf(cout, "      can be any of [${bld}aBcm${rs}].  ${bld}Y${rs} may also be ${bld}t${rs} to parse ${bld}REFERENCE${rs} an explicit\n");
	cfprintf(cout, "      timestamp.\n");
	cfprintf(cout, "  ${blu}-nogroup${rs}\n");
	cfprintf(cout, "  ${blu}-nouser${rs}\n");
	cfprintf(cout, "      Find files owned by nonexistent groups/users\n");
	cfprintf(cout, "  ${blu}-path${rs} ${bld}GLOB${rs}\n");
	cfprintf(cout, "  ${blu}-wholename${rs} ${bld}GLOB${rs}\n");
	cfprintf(cout, "      Find files whose entire path matches the ${bld}GLOB${rs}\n");
	cfprintf(cout, "  ${blu}-perm${rs} ${bld}[-]MODE${rs}\n");
	cfprintf(cout, "      Find files with a matching mode\n");
	cfprintf(cout, "  ${blu}-regex${rs} ${bld}REGEX${rs}\n");
	cfprintf(cout, "      Find files whose entire path matches the regular expression ${bld}REGEX${rs}\n");
	cfprintf(cout, "  ${blu}-samefile${rs} ${bld}FILE${rs}\n");
	cfprintf(cout, "      Find hard links to ${bld}FILE${rs}\n");
	cfprintf(cout, "  ${blu}-since${rs} ${bld}TIME${rs}\n");
	cfprintf(cout, "      Find files modified since ${bld}TIME${rs}\n");
	cfprintf(cout, "  ${blu}-size${rs} ${bld}[-+]N[cwbkMGTP]${rs}\n");
	cfprintf(cout, "      Find files with the given size, in 1-byte ${bld}c${rs}haracters, 2-byte ${bld}w${rs}ords,\n");
	cfprintf(cout, "      512-byte ${bld}b${rs}locks (default), or ${bld}k${rs}iB/${bld}M${rs}iB/${bld}G${rs}iB/${bld}T${rs}iB/${bld}P${rs}iB\n");
	cfprintf(cout, "  ${blu}-sparse${rs}\n");
	cfprintf(cout, "      Find files that occupy fewer disk blocks than expected\n");
	cfprintf(cout, "  ${blu}-type${rs} ${bld}[bcdlpfswD]${rs}\n");
	cfprintf(cout, "      Find files of the given type\n");
	cfprintf(cout, "  ${blu}-used${rs} ${bld}[-+]N${rs}\n");
	cfprintf(cout, "      Find files last accessed ${bld}N${rs} days after they were changed\n");
#if BFS_CAN_CHECK_XATTRS
	cfprintf(cout, "  ${blu}-xattr${rs}\n");
	cfprintf(cout, "      Find files with extended attributes\n");
	cfprintf(cout, "  ${blu}-xattrname${rs} ${bld}NAME${rs}\n");
	cfprintf(cout, "      Find files with the extended attribute ${bld}NAME${rs}\n");
#endif
	cfprintf(cout, "  ${blu}-xtype${rs} ${bld}[bcdlpfswD]${rs}\n");
	cfprintf(cout, "      Find files of the given type, following links when ${blu}-type${rs} would not, and\n");
	cfprintf(cout, "      vice versa\n\n");

	cfprintf(cout, "${bld}Actions:${rs}\n\n");

	cfprintf(cout, "  ${blu}-delete${rs}\n");
	cfprintf(cout, "  ${blu}-rm${rs}\n");
	cfprintf(cout, "      Delete any found files (implies ${blu}-depth${rs})\n");
	cfprintf(cout, "  ${blu}-exec${rs} ${bld}command ... {} ;${rs}\n");
	cfprintf(cout, "      Execute a command\n");
	cfprintf(cout, "  ${blu}-exec${rs} ${bld}command ... {} +${rs}\n");
	cfprintf(cout, "      Execute a command with multiple files at once\n");
	cfprintf(cout, "  ${blu}-ok${rs} ${bld}command ... {} ;${rs}\n");
	cfprintf(cout, "      Prompt the user whether to execute a command\n");
	cfprintf(cout, "  ${blu}-execdir${rs} ${bld}command ... {} ;${rs}\n");
	cfprintf(cout, "  ${blu}-execdir${rs} ${bld}command ... {} +${rs}\n");
	cfprintf(cout, "  ${blu}-okdir${rs} ${bld}command ... {} ;${rs}\n");
	cfprintf(cout, "      Like ${blu}-exec${rs}/${blu}-ok${rs}, but run the command in the same directory as the found\n");
	cfprintf(cout, "      file(s)\n");
	cfprintf(cout, "  ${blu}-exit${rs} [${bld}STATUS${rs}]\n");
	cfprintf(cout, "      Exit immediately with the given status (%d if unspecified)\n", EXIT_SUCCESS);
	cfprintf(cout, "  ${blu}-fls${rs} ${bld}FILE${rs}\n");
	cfprintf(cout, "  ${blu}-fprint${rs} ${bld}FILE${rs}\n");
	cfprintf(cout, "  ${blu}-fprint0${rs} ${bld}FILE${rs}\n");
	cfprintf(cout, "  ${blu}-fprintf${rs} ${bld}FILE${rs} ${bld}FORMAT${rs}\n");
	cfprintf(cout, "      Like ${blu}-ls${rs}/${blu}-print${rs}/${blu}-print0${rs}/${blu}-printf${rs}, but write to ${bld}FILE${rs} instead of standard\n"
	               "      output\n");
	cfprintf(cout, "  ${blu}-limit${rs} ${bld}N${rs}\n");
	cfprintf(cout, "      Quit after this action is evaluated ${bld}N${rs} times\n");
	cfprintf(cout, "  ${blu}-ls${rs}\n");
	cfprintf(cout, "      List files like ${ex}ls${rs} ${bld}-dils${rs}\n");
	cfprintf(cout, "  ${blu}-print${rs}\n");
	cfprintf(cout, "      Print the path to the found file\n");
	cfprintf(cout, "  ${blu}-print0${rs}\n");
	cfprintf(cout, "      Like ${blu}-print${rs}, but use the null character ('\\0') as a separator rather than\n");
	cfprintf(cout, "      newlines\n");
	cfprintf(cout, "  ${blu}-printf${rs} ${bld}FORMAT${rs}\n");
	cfprintf(cout, "      Print according to a format string (see ${ex}man${rs} ${bld}find${rs}).  The additional format\n");
	cfprintf(cout, "      directives %%w and %%W${bld}k${rs} for printing file birth times are supported.\n");
	cfprintf(cout, "  ${blu}-printx${rs}\n");
	cfprintf(cout, "      Like ${blu}-print${rs}, but escape whitespace and quotation characters, to make the\n");
	cfprintf(cout, "      output safe for ${ex}xargs${rs}.  Consider using ${blu}-print0${rs} and ${ex}xargs${rs} ${bld}-0${rs} instead.\n");
	cfprintf(cout, "  ${blu}-prune${rs}\n");
	cfprintf(cout, "      Don't descend into this directory\n");
	cfprintf(cout, "  ${blu}-quit${rs}\n");
	cfprintf(cout, "      Quit immediately\n");
	cfprintf(cout, "  ${blu}-version${rs}\n");
	cfprintf(cout, "      Print version information\n");
	cfprintf(cout, "  ${blu}-help${rs}\n");
	cfprintf(cout, "      Print this help message\n\n");

	cfprintf(cout, "%s\n", BFS_HOMEPAGE);

	if (pager > 0) {
		cfclose(cout);
		xwaitpid(pager, NULL, 0);
	}

	parser->just_info = true;
	return NULL;
}

/** Print the bfs "logo". */
static void print_logo(CFILE *cout) {
	if (!cout->colors) {
		goto boring;
	}

	size_t vwidth = xstrwidth(bfs_version);
	dchar *spaces = dstrepeat(" ", vwidth);
	dchar *lines = dstrepeat("─", vwidth);
	if (!spaces || !lines) {
		dstrfree(lines);
		dstrfree(spaces);
		goto boring;
	}

	// We do ----\r<emoji> rather than <emoji>--- so we don't have to assume
	// anything about the width of the emoji
	cfprintf(cout, "╭─────%s╮\r📂\n", lines);
	cfprintf(cout, "├${ex}b${rs}   %s │\n", spaces);
	cfprintf(cout, "╰├${ex}f${rs}  ${bld}%s${rs} │\n", bfs_version);
	cfprintf(cout, " ╰├${ex}s${rs} %s │\n", spaces);
	cfprintf(cout, "  ╰──%s─╯\n\n", lines);

	dstrfree(lines);
	dstrfree(spaces);
	return;

boring:
	printf("%s %s\n\n", BFS_COMMAND, bfs_version);
}

/**
 * "Parse" -version.
 */
static struct bfs_expr *parse_version(struct bfs_parser *parser, int arg1, int arg2) {
	print_logo(parser->ctx->cout);

	printf("Copyright © Tavian Barnes and the bfs contributors\n");
	printf("No rights reserved (https://opensource.org/license/0BSD)\n\n");

	printf("CONFFLAGS := %s\n", bfs_confflags);
	printf("CC        := %s\n", bfs_cc);
	printf("CPPFLAGS  := %s\n", bfs_cppflags);
	printf("CFLAGS    := %s\n", bfs_cflags);
	printf("LDFLAGS   := %s\n", bfs_ldflags);
	printf("LDLIBS    := %s\n", bfs_ldlibs);

	printf("\n%s\n", BFS_HOMEPAGE);

	parser->just_info = true;
	return NULL;
}

/** Parser callback function type. */
typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2);

/**
 * An entry in the parse table for primary expressions.
 */
struct table_entry {
	char *arg;
	enum bfs_kind kind;
	parse_fn *parse;
	int arg1;
	int arg2;
	bool prefix;
	bool needs_arg;
};

/**
 * The parse table for primary expressions.
 */
static const struct table_entry parse_table[] = {
	{"--", BFS_FLAG},
	{"--help", BFS_ACTION, parse_help},
	{"--version", BFS_ACTION, parse_version},
	{"-Bmin", BFS_TEST, parse_min, BFS_STAT_BTIME},
	{"-Bnewer", BFS_TEST, parse_newer, BFS_STAT_BTIME},
	{"-Bsince", BFS_TEST, parse_since, BFS_STAT_BTIME},
	{"-Btime", BFS_TEST, parse_time, BFS_STAT_BTIME},
	{"-D", BFS_FLAG, parse_debug, .prefix = true},
	{"-E", BFS_FLAG, parse_regex_extended},
	{"-H", BFS_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false},
	{"-L", BFS_FLAG, parse_follow, BFTW_FOLLOW_ALL, false},
	{"-O", BFS_FLAG, parse_optlevel, .prefix = true},
	{"-P", BFS_FLAG, parse_follow, 0, false},
	{"-S", BFS_FLAG, parse_search_strategy, .prefix = true},
	{"-X", BFS_FLAG, parse_xargs_safe},
	{"-a", BFS_OPERATOR},
	{"-acl", BFS_TEST, parse_acl},
	{"-amin", BFS_TEST, parse_min, BFS_STAT_ATIME},
	{"-and", BFS_OPERATOR},
	{"-anewer", BFS_TEST, parse_newer, BFS_STAT_ATIME},
	{"-asince", BFS_TEST, parse_since, BFS_STAT_ATIME},
	{"-atime", BFS_TEST, parse_time, BFS_STAT_ATIME},
	{"-capable", BFS_TEST, parse_capable},
	{"-cmin", BFS_TEST, parse_min, BFS_STAT_CTIME},
	{"-cnewer", BFS_TEST, parse_newer, BFS_STAT_CTIME},
	{"-color", BFS_OPTION, parse_color, true},
	{"-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, true},
	{"-daystart", BFS_OPTION, parse_daystart},
	{"-delete", BFS_ACTION, parse_delete},
	{"-depth", BFS_OPTION, parse_depth_n, false},
	{"-empty", BFS_TEST, parse_empty},
	{"-exclude", BFS_OPERATOR},
	{"-exec", BFS_ACTION, parse_exec, 0},
	{"-execdir", BFS_ACTION, parse_exec, BFS_EXEC_CHDIR},
	{"-executable", BFS_TEST, parse_access, X_OK},
	{"-exit", BFS_ACTION, parse_exit},
	{"-f", BFS_FLAG, parse_f, .needs_arg = true},
	{"-false", BFS_TEST, parse_const, false},
	{"-files0-from", BFS_OPTION, parse_files0_from},
	{"-flags", BFS_TEST, parse_flags},
	{"-fls", BFS_ACTION, parse_fls},
	{"-follow", BFS_OPTION, parse_follow, BFTW_FOLLOW_ALL, true},
	{"-fprint", BFS_ACTION, parse_fprint},
	{"-fprint0", BFS_ACTION, parse_fprint0},
	{"-fprintf", BFS_ACTION, parse_fprintf},
	{"-fstype", BFS_TEST, parse_fstype},
	{"-gid", BFS_TEST, parse_group},
	{"-group", BFS_TEST, parse_group},
	{"-help", BFS_ACTION, parse_help},
	{"-hidden", BFS_TEST, parse_hidden},
	{"-ignore_readdir_race", BFS_OPTION, parse_ignore_races, true},
	{"-ilname", BFS_TEST, parse_lname, true},
	{"-iname", BFS_TEST, parse_name, true},
	{"-inum", BFS_TEST, parse_inum},
	{"-ipath", BFS_TEST, parse_path, true},
	{"-iregex", BFS_TEST, parse_regex, BFS_REGEX_ICASE},
	{"-iwholename", BFS_TEST, parse_path, true},
	{"-j", BFS_FLAG, parse_jobs, .prefix = true},
	{"-limit", BFS_ACTION, parse_limit},
	{"-links", BFS_TEST, parse_links},
	{"-lname", BFS_TEST, parse_lname, false},
	{"-ls", BFS_ACTION, parse_ls},
	{"-maxdepth", BFS_OPTION, parse_depth_limit, false},
	{"-mindepth", BFS_OPTION, parse_depth_limit, true},
	{"-mmin", BFS_TEST, parse_min, BFS_STAT_MTIME},
	{"-mnewer", BFS_TEST, parse_newer, BFS_STAT_MTIME},
	{"-mount", BFS_OPTION, parse_mount},
	{"-msince", BFS_TEST, parse_since, BFS_STAT_MTIME},
	{"-mtime", BFS_TEST, parse_time, BFS_STAT_MTIME},
	{"-name", BFS_TEST, parse_name, false},
	{"-newer", BFS_TEST, parse_newer, BFS_STAT_MTIME},
	{"-newer", BFS_TEST, parse_newerxy, .prefix = true},
	{"-nocolor", BFS_OPTION, parse_color, false},
	{"-noerror", BFS_OPTION, parse_noerror},
	{"-nogroup", BFS_TEST, parse_nogroup},
	{"-nohidden", BFS_TEST, parse_nohidden},
	{"-noignore_readdir_race", BFS_OPTION, parse_ignore_races, false},
	{"-noleaf", BFS_OPTION, parse_noleaf},
	{"-not", BFS_OPERATOR},
	{"-nouser", BFS_TEST, parse_nouser},
	{"-nowarn", BFS_OPTION, parse_warn, false},
	{"-o", BFS_OPERATOR},
	{"-ok", BFS_ACTION, parse_exec, BFS_EXEC_CONFIRM},
	{"-okdir", BFS_ACTION, parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR},
	{"-or", BFS_OPERATOR},
	{"-path", BFS_TEST, parse_path, false},
	{"-perm", BFS_TEST, parse_perm},
	{"-print", BFS_ACTION, parse_print},
	{"-print0", BFS_ACTION, parse_print0},
	{"-printf", BFS_ACTION, parse_printf},
	{"-printx", BFS_ACTION, parse_printx},
	{"-prune", BFS_ACTION, parse_prune},
	{"-quit", BFS_ACTION, parse_quit},
	{"-readable", BFS_TEST, parse_access, R_OK},
	{"-regex", BFS_TEST, parse_regex, 0},
	{"-regextype", BFS_OPTION, parse_regextype},
	{"-rm", BFS_ACTION, parse_delete},
	{"-s", BFS_FLAG, parse_s},
	{"-samefile", BFS_TEST, parse_samefile},
	{"-since", BFS_TEST, parse_since, BFS_STAT_MTIME},
	{"-size", BFS_TEST, parse_size},
	{"-sparse", BFS_TEST, parse_sparse},
	{"-status", BFS_OPTION, parse_status},
	{"-true", BFS_TEST, parse_const, true},
	{"-type", BFS_TEST, parse_type, false},
	{"-uid", BFS_TEST, parse_user},
	{"-unique", BFS_OPTION, parse_unique},
	{"-used", BFS_TEST, parse_used},
	{"-user", BFS_TEST, parse_user},
	{"-version", BFS_ACTION, parse_version},
	{"-warn", BFS_OPTION, parse_warn, true},
	{"-wholename", BFS_TEST, parse_path, false},
	{"-writable", BFS_TEST, parse_access, W_OK},
	{"-x", BFS_FLAG, parse_xdev},
	{"-xattr", BFS_TEST, parse_xattr},
	{"-xattrname", BFS_TEST, parse_xattrname},
	{"-xdev", BFS_OPTION, parse_xdev},
	{"-xtype", BFS_TEST, parse_type, true},
	{0},
};

/** Look up an argument in the parse table. */
static const struct table_entry *table_lookup(const char *arg) {
	for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
		bool match;
		if (entry->prefix) {
			match = strncmp(arg, entry->arg, strlen(entry->arg)) == 0;
		} else {
			match = strcmp(arg, entry->arg) == 0;
		}
		if (match) {
			return entry;
		}
	}

	return NULL;
}

/** Look up a single-character flag in the parse table. */
static const struct table_entry *flag_lookup(char flag) {
	for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
		enum bfs_kind kind = entry->kind;
		if (kind == BFS_FLAG && entry->arg[1] == flag && !entry->arg[2]) {
			return entry;
		}
	}

	return NULL;
}

/** Check for a multi-flag argument like -LEXO2. */
static bool is_flag_group(const char *arg) {
	// We enforce that at least one flag in a flag group must be a capital
	// letter, to avoid ambiguity with primary expressions
	bool has_upper = false;

	// Flags that take an argument must appear last
	bool needs_arg = false;

	for (size_t i = 1; arg[i]; ++i) {
		char c = arg[i];
		if (c >= 'A' && c <= 'Z') {
			has_upper = true;
		}

		if (needs_arg) {
			return false;
		}

		const struct table_entry *entry = flag_lookup(c);
		if (!entry || !entry->parse) {
			return false;
		}

		if (entry->prefix) {
			// The rest is the flag's argument
			break;
		}

		needs_arg |= entry->needs_arg;
	}

	return has_upper;
}

/** Parse a multi-flag argument. */
static struct bfs_expr *parse_flag_group(struct bfs_parser *parser) {
	struct bfs_expr *expr = NULL;

	char **start = parser->argv;
	char **end = start;
	const char *arg = start[0];

	for (size_t i = 1; arg[i]; ++i) {
		parser->argv = start;

		const struct table_entry *entry = flag_lookup(arg[i]);
		expr = entry->parse(parser, entry->arg1, entry->arg2);

		if (parser->argv > end) {
			end = parser->argv;
		}

		if (!expr || entry->prefix) {
			break;
		}
	}

	if (expr) {
		bfs_assert(parser->argv == end, "Didn't eat enough tokens");
	}

	return expr;
}

/** Search for a fuzzy match in the parse table. */
static const struct table_entry *table_lookup_fuzzy(const char *arg) {
	const struct table_entry *best = NULL;
	int best_dist = INT_MAX;

	for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
		int dist = typo_distance(arg, entry->arg);
		if (!best || dist < best_dist) {
			best = entry;
			best_dist = dist;
		}
	}

	return best;
}

/**
 * PRIMARY : OPTION
 *         | TEST
 *         | ACTION
 */
static struct bfs_expr *parse_primary(struct bfs_parser *parser) {
	struct bfs_ctx *ctx = parser->ctx;

	// Paths are already skipped at this point
	const char *arg = parser->argv[0];

	if (arg[0] != '-') {
		goto unexpected;
	}

	const struct table_entry *match = table_lookup(arg);
	if (match) {
		if (match->parse) {
			goto matched;
		} else {
			goto unexpected;
		}
	}

	if (is_flag_group(arg)) {
		return parse_flag_group(parser);
	}

	match = table_lookup_fuzzy(arg);

	CFILE *cerr = ctx->cerr;
	parse_error(parser, "Unknown argument; did you mean ");
	switch (match->kind) {
	case BFS_FLAG:
		cfprintf(cerr, "${cyn}%s${rs}?", match->arg);
		break;
	case BFS_OPERATOR:
		cfprintf(cerr, "${red}%s${rs}?", match->arg);
		break;
	default:
		cfprintf(cerr, "${blu}%s${rs}?", match->arg);
		break;
	}

	if (!ctx->interactive || !match->parse) {
		fprintf(stderr, "\n");
		goto unmatched;
	}

	fprintf(stderr, " ");
	if (ynprompt() <= 0) {
		goto unmatched;
	}

	fprintf(stderr, "\n");
	parser->argv[0] = match->arg;

matched:
	return match->parse(parser, match->arg1, match->arg2);

unmatched:
	return NULL;

unexpected:
	parse_error(parser, "Expected a predicate.\n");
	return NULL;
}

/**
 * FACTOR : "(" EXPR ")"
 *        | "!" FACTOR | "-not" FACTOR
 *        | "-exclude" FACTOR
 *        | PRIMARY
 */
static struct bfs_expr *parse_factor(struct bfs_parser *parser) {
	if (skip_paths(parser) != 0) {
		return NULL;
	}

	const char *arg = parser->argv[0];
	if (!arg) {
		parse_argv_error(parser, parser->last_arg, 1, "Expression terminated prematurely here.\n");
		return NULL;
	}

	if (strcmp(arg, "(") == 0) {
		parser_advance(parser, BFS_OPERATOR, 1);

		struct bfs_expr *expr = parse_expr(parser);
		if (!expr) {
			return NULL;
		}

		if (skip_paths(parser) != 0) {
			return NULL;
		}

		arg = parser->argv[0];
		if (!arg || strcmp(arg, ")") != 0) {
			parse_argv_error(parser, parser->last_arg, 1, "Expected a ${red})${rs}.\n");
			return NULL;
		}

		parser_advance(parser, BFS_OPERATOR, 1);
		return expr;
	} else if (strcmp(arg, "-exclude") == 0) {
		if (parser->excluding) {
			parse_error(parser, "${err}%s${rs} is not supported within ${red}-exclude${rs}.\n", arg);
			return NULL;
		}

		char **argv = parser_advance(parser, BFS_OPERATOR, 1);
		parser->excluding = true;

		struct bfs_expr *factor = parse_factor(parser);
		if (!factor) {
			return NULL;
		}

		parser->excluding = false;

		bfs_expr_append(parser->ctx->exclude, factor);
		return parse_new_expr(parser, eval_true, parser->argv - argv, argv, BFS_OPERATOR);
	} else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) {
		char **argv = parser_advance(parser, BFS_OPERATOR, 1);

		struct bfs_expr *factor = parse_factor(parser);
		if (!factor) {
			return NULL;
		}

		return new_unary_expr(parser, eval_not, factor, argv);
	} else {
		return parse_primary(parser);
	}
}

/**
 * TERM : FACTOR
 *      | TERM FACTOR
 *      | TERM "-a" FACTOR
 *      | TERM "-and" FACTOR
 */
static struct bfs_expr *parse_term(struct bfs_parser *parser) {
	struct bfs_expr *term = parse_factor(parser);

	while (term) {
		if (skip_paths(parser) != 0) {
			return NULL;
		}

		const char *arg = parser->argv[0];
		if (!arg) {
			break;
		}

		if (strcmp(arg, "-o") == 0 || strcmp(arg, "-or") == 0
		    || strcmp(arg, ",") == 0
		    || strcmp(arg, ")") == 0) {
			break;
		}

		char **argv = &fake_and_arg;
		if (strcmp(arg, "-a") == 0 || strcmp(arg, "-and") == 0) {
			argv = parser_advance(parser, BFS_OPERATOR, 1);
		}

		struct bfs_expr *lhs = term;
		struct bfs_expr *rhs = parse_factor(parser);
		if (!rhs) {
			return NULL;
		}

		term = new_binary_expr(parser, eval_and, lhs, rhs, argv);
	}

	return term;
}

/**
 * CLAUSE : TERM
 *        | CLAUSE "-o" TERM
 *        | CLAUSE "-or" TERM
 */
static struct bfs_expr *parse_clause(struct bfs_parser *parser) {
	struct bfs_expr *clause = parse_term(parser);

	while (clause) {
		if (skip_paths(parser) != 0) {
			return NULL;
		}

		const char *arg = parser->argv[0];
		if (!arg) {
			break;
		}

		if (strcmp(arg, "-o") != 0 && strcmp(arg, "-or") != 0) {
			break;
		}

		char **argv = parser_advance(parser, BFS_OPERATOR, 1);

		struct bfs_expr *lhs = clause;
		struct bfs_expr *rhs = parse_term(parser);
		if (!rhs) {
			return NULL;
		}

		clause = new_binary_expr(parser, eval_or, lhs, rhs, argv);
	}

	return clause;
}

/**
 * EXPR : CLAUSE
 *      | EXPR "," CLAUSE
 */
static struct bfs_expr *parse_expr(struct bfs_parser *parser) {
	struct bfs_expr *expr = parse_clause(parser);

	while (expr) {
		if (skip_paths(parser) != 0) {
			return NULL;
		}

		const char *arg = parser->argv[0];
		if (!arg) {
			break;
		}

		if (strcmp(arg, ",") != 0) {
			break;
		}

		char **argv = parser_advance(parser, BFS_OPERATOR, 1);

		struct bfs_expr *lhs = expr;
		struct bfs_expr *rhs = parse_clause(parser);
		if (!rhs) {
			return NULL;
		}

		expr = new_binary_expr(parser, eval_comma, lhs, rhs, argv);
	}

	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.
 */
static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) {
	struct bfs_ctx *ctx = parser->ctx;

	if (skip_paths(parser) != 0) {
		return NULL;
	}

	struct bfs_expr *expr;
	if (parser->argv[0]) {
		expr = parse_expr(parser);
	} else {
		expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg, BFS_TEST);
	}
	if (!expr) {
		return NULL;
	}

	if (parser->argv[0]) {
		parse_error(parser, "Unexpected argument.\n");
		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) {
		const struct bfs_expr *limit = parser->limit_expr;
		if (limit) {
			parse_expr_error(parser, limit,
				"With %pX, you must specify an action explicitly; for example, ${blu}-print${rs} %px.\n",
				limit, limit);
			return NULL;
		}

		struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg, BFS_ACTION);
		if (!print) {
			return NULL;
		}
		init_print_expr(parser, print);

		expr = new_binary_expr(parser, eval_and, expr, print, &fake_and_arg);
		if (!expr) {
			return NULL;
		}
	}

	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_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? ");
			if (ynprompt() <= 0) {
				return NULL;
			}
		}

		fprintf(stderr, "\n");
	}

	return expr;
}

static const char *bftw_strategy_name(enum bftw_strategy strategy) {
	switch (strategy) {
	case BFTW_BFS:
		return "bfs";
	case BFTW_DFS:
		return "dfs";
	case BFTW_IDS:
		return "ids";
	case BFTW_EDS:
		return "eds";
	}

	bfs_bug("Invalid strategy");
	return "???";
}

static void dump_expr_multiline(const struct bfs_ctx *ctx, enum debug_flags flag, const struct bfs_expr *expr, int indent, int rparens) {
	bfs_debug_prefix(ctx, flag);

	for (int i = 0; i < indent; ++i) {
		cfprintf(ctx->cerr, "  ");
	}

	bool close = true;

	if (bfs_expr_is_parent(expr)) {
		if (SLIST_EMPTY(&expr->children)) {
			cfprintf(ctx->cerr, "(${red}%s${rs}", expr->argv[0]);
			++rparens;
		} else {
			cfprintf(ctx->cerr, "(${red}%s${rs}\n", expr->argv[0]);
			for_expr (child, expr) {
				int parens = child->next ? 0 : rparens + 1;
				dump_expr_multiline(ctx, flag, child, indent + 1, parens);
			}
			close = false;
		}
	} else {
		if (flag == DEBUG_RATES) {
			cfprintf(ctx->cerr, "%pE", expr);
		} else {
			cfprintf(ctx->cerr, "%pe", expr);
		}
	}

	if (close) {
		for (int i = 0; i < rparens; ++i) {
			cfprintf(ctx->cerr, ")");
		}
		cfprintf(ctx->cerr, "\n");
	}
}

void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) {
	if (!bfs_debug_prefix(ctx, flag)) {
		return;
	}

	CFILE *cerr = ctx->cerr;

	cfprintf(cerr, "${ex}%s${rs}", ctx->argv[0]);

	if (ctx->flags & BFTW_FOLLOW_ALL) {
		cfprintf(cerr, " ${cyn}-L${rs}");
	} else if (ctx->flags & BFTW_FOLLOW_ROOTS) {
		cfprintf(cerr, " ${cyn}-H${rs}");
	} else {
		cfprintf(cerr, " ${cyn}-P${rs}");
	}

	if (ctx->xargs_safe) {
		cfprintf(cerr, " ${cyn}-X${rs}");
	}

	if (ctx->flags & BFTW_SORT) {
		cfprintf(cerr, " ${cyn}-s${rs}");
	}

	cfprintf(cerr, " ${cyn}-j${bld}%d${rs}", ctx->threads);

	if (ctx->optlevel != 3) {
		cfprintf(cerr, " ${cyn}-O${bld}%d${rs}", ctx->optlevel);
	}

	cfprintf(cerr, " ${cyn}-S${rs} ${bld}%s${rs}", bftw_strategy_name(ctx->strategy));

	enum debug_flags debug = ctx->debug;
	if (debug == DEBUG_ALL) {
		cfprintf(cerr, " ${cyn}-D${rs} ${bld}all${rs}");
	} else if (debug) {
		cfprintf(cerr, " ${cyn}-D${rs} ");
		for (enum debug_flags i = 1; DEBUG_ALL & i; i <<= 1) {
			if (debug & i) {
				cfprintf(cerr, "${bld}%s${rs}", debug_flag_name(i));
				debug ^= i;
				if (debug) {
					cfprintf(cerr, ",");
				}
			}
		}
	}

	for (size_t i = 0; i < ctx->npaths; ++i) {
		const char *path = ctx->paths[i];
		char c = path[0];
		if (c == '-' || c == '(' || c == ')' || c == '!' || c == ',') {
			cfprintf(cerr, " ${cyn}-f${rs}");
		}
		cfprintf(cerr, " ${mag}%pq${rs}", path);
	}

	if (ctx->cout->colors) {
		cfprintf(cerr, " ${blu}-color${rs}");
	} else {
		cfprintf(cerr, " ${blu}-nocolor${rs}");
	}
	if (ctx->flags & BFTW_POST_ORDER) {
		cfprintf(cerr, " ${blu}-depth${rs}");
	}
	if (ctx->ignore_races) {
		cfprintf(cerr, " ${blu}-ignore_readdir_race${rs}");
	}
	if (ctx->mindepth != 0) {
		cfprintf(cerr, " ${blu}-mindepth${rs} ${bld}%d${rs}", ctx->mindepth);
	}
	if (ctx->maxdepth != INT_MAX) {
		cfprintf(cerr, " ${blu}-maxdepth${rs} ${bld}%d${rs}", ctx->maxdepth);
	}
	if (ctx->flags & BFTW_SKIP_MOUNTS) {
		cfprintf(cerr, " ${blu}-mount${rs}");
	}
	if (ctx->status) {
		cfprintf(cerr, " ${blu}-status${rs}");
	}
	if (ctx->unique) {
		cfprintf(cerr, " ${blu}-unique${rs}");
	}
	if ((ctx->flags & (BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) == BFTW_PRUNE_MOUNTS) {
		cfprintf(cerr, " ${blu}-xdev${rs}");
	}

	fputs("\n", stderr);

	bfs_debug(ctx, flag, "(${red}-exclude${rs}\n");
	dump_expr_multiline(ctx, flag, ctx->exclude, 1, 1);

	dump_expr_multiline(ctx, flag, ctx->expr, 0, 0);
}

/**
 * Dump the estimated costs.
 */
static void dump_costs(const struct bfs_ctx *ctx) {
	const struct bfs_expr *expr = ctx->expr;
	bfs_debug(ctx, DEBUG_COST, "       Cost: ~${ylw}%g${rs}\n", expr->cost);
	bfs_debug(ctx, DEBUG_COST, "Probability: ~${ylw}%g%%${rs}\n", 100.0 * expr->probability);
}

struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
	struct bfs_ctx *ctx = bfs_ctx_new();
	if (!ctx) {
		perror("bfs_ctx_new()");
		goto fail;
	}

	static char *default_argv[] = {BFS_COMMAND, NULL};
	if (argc < 1) {
		argc = 1;
		argv = default_argv;
	}

	ctx->argc = argc;
	ctx->argv = xmemdup(argv, sizeof_array(char *, argc + 1));
	if (!ctx->argv) {
		perror("xmemdup()");
		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) {
		// https://no-color.org/
		use_color = COLOR_NEVER;
	}

	ctx->colors = parse_colors();
	if (!ctx->colors) {
		ctx->colors_error = errno;
	}

	ctx->cerr = cfwrap(stderr, use_color ? ctx->colors : NULL, false);
	if (!ctx->cerr) {
		perror("cfwrap()");
		goto fail;
	}

	ctx->cout = cfwrap(stdout, use_color ? ctx->colors : NULL, false);
	if (!ctx->cout) {
		bfs_perror(ctx, "cfwrap()");
		goto fail;
	}

	if (!bfs_ctx_dedup(ctx, ctx->cout, NULL) || !bfs_ctx_dedup(ctx, ctx->cerr, NULL)) {
		bfs_perror(ctx, "bfs_ctx_dedup()");
		goto fail;
	}

	bool stdin_tty = isatty(STDIN_FILENO);
	bool stdout_tty = isatty(STDOUT_FILENO);
	bool stderr_tty = isatty(STDERR_FILENO);

	if (getenv("POSIXLY_CORRECT")) {
		ctx->posixly_correct = true;
	} else {
		ctx->warn = stdin_tty;
	}
	ctx->interactive = stdin_tty && stderr_tty;

	struct bfs_parser parser = {
		.ctx = ctx,
		.argv = ctx->argv + 1,
		.command = ctx->argv[0],
		.regex_type = BFS_REGEX_POSIX_BASIC,
		.stdout_tty = stdout_tty,
		.use_color = use_color,
		.implicit_print = true,
		.just_info = false,
		.excluding = false,
		.last_arg = NULL,
		.depth_expr = NULL,
		.prune_expr = NULL,
		.mount_expr = NULL,
		.xdev_expr = NULL,
		.stdin_expr = NULL,
		.now = ctx->now,
	};

	ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg, BFS_OPERATOR);
	if (!ctx->exclude) {
		goto fail;
	}

	ctx->expr = parse_whole_expr(&parser);
	if (!ctx->expr) {
		if (parser.just_info) {
			goto done;
		} else {
			goto fail;
		}
	}

	if (parser.use_color == COLOR_AUTO && !ctx->colors) {
		bfs_warning(ctx, "Error parsing $$LS_COLORS: %s.\n\n", xstrerror(ctx->colors_error));
	}

	if (bfs_optimize(ctx) != 0) {
		if (errno != 0) {
			bfs_perror(ctx, "bfs_optimize()");
		}
		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;
	}

	bfs_ctx_dump(ctx, DEBUG_TREE);
	dump_costs(ctx);

done:
	return ctx;

fail:
	bfs_ctx_free(ctx);
	return NULL;
}
