diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f154ff7..71f26eb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - `printf` prints what it can when input hasn't been fully converted to a number, but still prints an error (#5532). - `complete -C foo` now works instead of erroring out and requiring `complete -Cfoo`. - `argparse` now defaults to showing the current function name (instead of `argparse`) in its errors, making `--name` often superfluous (#5835). +- `argparse` learned a new `--ignore-unknown` flag to keep unrecognized options, allowing multiple argparse passes to parse options (#5367). ### Interactive improvements - Major improvements in performance and functionality to the 'sorin' sample prompt (#5411). diff --git a/sphinx_doc_src/cmds/argparse.rst b/sphinx_doc_src/cmds/argparse.rst index d930a07cc..ec79b444f 100644 --- a/sphinx_doc_src/cmds/argparse.rst +++ b/sphinx_doc_src/cmds/argparse.rst @@ -33,6 +33,8 @@ The following ``argparse`` options are available. They must appear before all OP - ``-X`` or ``--max-args`` is followed by an integer that defines the maximum number of acceptable non-option arguments. The default is infinity. +- ``-i`` or ``--ignore-unknown`` ignores unknown options, keeping them and their arguments in $argv instead. + - ``-s`` or ``--stop-nonopt`` causes scanning the arguments to stop as soon as the first non-option argument is seen. Using this arg is equivalent to calling the C function ``getopt_long()`` with the short options starting with a ``+`` symbol. This is sometimes known as "POSIXLY CORRECT". If this flag is not used then arguments are reordered (i.e., permuted) so that all non-option arguments are moved after option arguments. This mode has several uses but the main one is to implement a command that has subcommands. - ``-h`` or ``--help`` displays help about using this command. diff --git a/src/builtin_argparse.cpp b/src/builtin_argparse.cpp index a8316a31c..54e129c36 100644 --- a/src/builtin_argparse.cpp +++ b/src/builtin_argparse.cpp @@ -44,6 +44,7 @@ struct option_spec_t { using option_spec_ref_t = std::unique_ptr; struct argparse_cmd_opts_t { + bool ignore_unknown = false; bool print_help = false; bool stop_nonopt = false; size_t min_args = 0; @@ -57,8 +58,9 @@ struct argparse_cmd_opts_t { std::vector> exclusive_flag_sets; }; -static const wchar_t *const short_options = L"+:hn:sx:N:X:"; +static const wchar_t *const short_options = L"+:hn:six:N:X:"; static const struct woption long_options[] = {{L"stop-nonopt", no_argument, NULL, 's'}, + {L"ignore-unknown", no_argument, NULL, 'i'}, {L"name", required_argument, NULL, 'n'}, {L"exclusive", required_argument, NULL, 'x'}, {L"help", no_argument, NULL, 'h'}, @@ -352,6 +354,10 @@ static int parse_cmd_opts(argparse_cmd_opts_t &opts, int *optind, //!OCLINT(hig opts.stop_nonopt = true; break; } + case 'i': { + opts.ignore_unknown = true; + break; + } case 'x': { // Just save the raw string here. Later, when we have all the short and long flag // definitions we'll parse these strings into a more useful data structure. @@ -536,7 +542,7 @@ static int handle_flag(parser_t &parser, const argparse_cmd_opts_t &opts, option return STATUS_CMD_OK; } -static int argparse_parse_flags(parser_t &parser, const argparse_cmd_opts_t &opts, +static int argparse_parse_flags(parser_t &parser, argparse_cmd_opts_t &opts, const wchar_t *short_options, const woption *long_options, const wchar_t *cmd, int argc, wchar_t **argv, int *optind, io_streams_t &streams) { @@ -547,16 +553,14 @@ static int argparse_parse_flags(parser_t &parser, const argparse_cmd_opts_t &opt if (opt == ':') { builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); return STATUS_INVALID_ARGS; - } - - if (opt == '?') { + } else if (opt == '?') { // It's not a recognized flag. See if it's an implicit int flag. const wchar_t *arg_contents = argv[w.woptind - 1] + 1; int retval = STATUS_CMD_OK; if (is_implicit_int(opts, arg_contents)) { retval = validate_and_store_implicit_int(parser, opts, arg_contents, w, long_idx, streams); - } else { + } else if (!opts.ignore_unknown) { streams.err.append_format(BUILTIN_ERR_UNKNOWN, cmd, argv[w.woptind - 1]); // We don't use builtin_print_error_trailer as that // says to use the cmd help, @@ -565,12 +569,25 @@ static int argparse_parse_flags(parser_t &parser, const argparse_cmd_opts_t &opt // Plus this particular error is not an error in argparse usage. streams.err.append(parser.current_line()); retval = STATUS_INVALID_ARGS; + } else { + // Any unrecognized option is put back if ignore_unknown is used. + // This allows reusing the same argv in multiple argparse calls, + // or just ignoring the error (e.g. in completions). + opts.argv.push_back(arg_contents - 1); } if (retval != STATUS_CMD_OK) return retval; long_idx = -1; continue; + } else if (opt == 1) { + // A non-option argument. + // We use `-` as the first option-string-char to disable GNU getopt's reordering, + // otherwise we'd get ignored options first and normal arguments later. + // E.g. `argparse -i -- -t tango -w` needs to keep `-t tango -w` in $argv, not `-t -w tango`. + opts.argv.push_back(argv[w.woptind - 1]); + continue; } + // It's a recognized flag. auto found = opts.options.find(opt); assert(found != opts.options.end()); @@ -591,7 +608,8 @@ static int argparse_parse_args(argparse_cmd_opts_t &opts, const wcstring_list_t parser_t &parser, io_streams_t &streams) { if (args.empty()) return STATUS_CMD_OK; - wcstring short_options = opts.stop_nonopt ? L"+:" : L":"; + // "+" means stop at nonopt, "-" means give nonoptions the option character code `1`, and don't reorder. + wcstring short_options = opts.stop_nonopt ? L"+:" : L"-"; std::vector long_options; populate_option_strings(opts, &short_options, &long_options); @@ -614,7 +632,11 @@ static int argparse_parse_args(argparse_cmd_opts_t &opts, const wcstring_list_t retval = check_for_mutually_exclusive_flags(opts, streams); if (retval != STATUS_CMD_OK) return retval; - for (int i = optind; argv[i]; i++) opts.argv.push_back(argv[i]); + if (opts.stop_nonopt) { + for (int i = optind; argv[i]; i++) { + opts.argv.push_back(argv[i]); + } + } return STATUS_CMD_OK; } diff --git a/tests/argparse.err b/tests/argparse.err index 894d07d74..a2e81c6d6 100644 --- a/tests/argparse.err +++ b/tests/argparse.err @@ -112,3 +112,6 @@ Standard input (line 353): argparse a/alpha -- --banana ^ in function 'notargparse' + +#################### +# Ignoring unknown options diff --git a/tests/argparse.in b/tests/argparse.in index ce33d1a9d..84e8ed378 100644 --- a/tests/argparse.in +++ b/tests/argparse.in @@ -179,3 +179,10 @@ end notargparse true + +logmsg Ignoring unknown options +argparse -i a=+ b=+ -- -a alpha -b bravo -t tango -a aaaa --wurst +or echo unexpected argparse return status $status >&2 +# The unknown options are removed _entirely_. +echo $argv +echo $_flag_a diff --git a/tests/argparse.out b/tests/argparse.out index b1565bb6c..68b155242 100644 --- a/tests/argparse.out +++ b/tests/argparse.out @@ -132,3 +132,8 @@ expected argparse return status 57 #################### # Errors use function name by default + +#################### +# Ignoring unknown options +-t tango --wurst +alpha aaaa