diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt index 4412377fa3d2ef..c2a5e429149417 100644 --- a/Documentation/technical/api-parse-options.txt +++ b/Documentation/technical/api-parse-options.txt @@ -8,7 +8,8 @@ Basics ------ The argument vector `argv[]` may usually contain mandatory or optional -'non-option arguments', e.g. a filename or a branch, and 'options'. +'non-option arguments', e.g. a filename or a branch, 'options', and +'subcommands'. Options are optional arguments that start with a dash and that allow to change the behavior of a command. @@ -48,6 +49,33 @@ The parse-options API allows: option, e.g. `-a -b --option -- --this-is-a-file` indicates that `--this-is-a-file` must not be processed as an option. +Subcommands are special in a couple of ways: + +* Subcommands only have long form, and they have no double dash prefix, no + negated form, and no description, and they don't take any arguments, and + can't be abbreviated. + +* There must be exactly one subcommand among the arguments, or zero if the + command has a default operation mode. + +* All arguments following the subcommand are considered to be arguments of + the subcommand, and, conversely, arguments meant for the subcommand may + not preceed the subcommand. + +Therefore, if the options array contains at least one subcommand and +`parse_options()` encounters the first dashless argument, it will either: + +* stop and return, if that dashless argument is a known subcommand, setting + `value` to the function pointer associated with that subcommand, storing + the name of the subcommand in argv[0], and leaving the rest of the + arguments unprocessed, or + +* stop and return, if it was invoked with the `PARSE_OPT_SUBCOMMAND_OPTIONAL` + flag and that dashless argument doesn't match any subcommands, leaving + `value` unchanged and the rest of the arguments unprocessed, or + +* show error and usage, and abort. + Steps to parse options ---------------------- @@ -110,6 +138,13 @@ Flags are the bitwise-or of: turns it off and allows one to add custom handlers for these options, or to just leave them unknown. +`PARSE_OPT_SUBCOMMAND_OPTIONAL`:: + Don't error out when no subcommand is specified. + +Note that `PARSE_OPT_STOP_AT_NON_OPTION` is incompatible with subcommands; +while `PARSE_OPT_KEEP_DASHDASH` and `PARSE_OPT_KEEP_UNKNOWN_OPT` can only be +used with subcommands when combined with `PARSE_OPT_SUBCOMMAND_OPTIONAL`. + Data Structure -------------- @@ -241,7 +276,11 @@ There are some macros to easily define options: can be given by the user. `int_var` is set to `enum_val` when the option is used, but an error is reported if other "operating mode" option has already set its value to the same `int_var`. + In new commands consider using subcommands instead. +`OPT_SUBCOMMAND(long, &fn_ptr, subcommand_fn)`:: + Define a subcommand. `subcommand_fn` is put into `fn_ptr` when + this subcommand is used. The last element of the array must be `OPT_END()`. diff --git a/builtin/blame.c b/builtin/blame.c index 02e39420b62bab..a9fe8cf7a68bf3 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix) break; case PARSE_OPT_HELP: case PARSE_OPT_ERROR: + case PARSE_OPT_SUBCOMMAND: exit(129); case PARSE_OPT_COMPLETE: exit(0); diff --git a/builtin/shortlog.c b/builtin/shortlog.c index 086dfee45aa8be..7a1e1fe7c0ed6d 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix) break; case PARSE_OPT_HELP: case PARSE_OPT_ERROR: + case PARSE_OPT_SUBCOMMAND: exit(129); case PARSE_OPT_COMPLETE: exit(0); diff --git a/parse-options.c b/parse-options.c index 8748f88e6f344e..a1ec932f0f9ff3 100644 --- a/parse-options.c +++ b/parse-options.c @@ -324,6 +324,8 @@ static enum parse_opt_result parse_long_opt( const char *rest, *long_name = options->long_name; enum opt_parsed flags = OPT_LONG, opt_flags = OPT_LONG; + if (options->type == OPTION_SUBCOMMAND) + continue; if (!long_name) continue; @@ -419,6 +421,19 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p, return PARSE_OPT_ERROR; } +static enum parse_opt_result parse_subcommand(const char *arg, + const struct option *options) +{ + for (; options->type != OPTION_END; options++) + if (options->type == OPTION_SUBCOMMAND && + !strcmp(options->long_name, arg)) { + *(parse_opt_subcommand_fn **)options->value = options->subcommand_fn; + return PARSE_OPT_SUBCOMMAND; + } + + return PARSE_OPT_UNKNOWN; +} + static void check_typos(const char *arg, const struct option *options) { if (strlen(arg) < 3) @@ -442,6 +457,7 @@ static void check_typos(const char *arg, const struct option *options) static void parse_options_check(const struct option *opts) { char short_opts[128]; + void *subcommand_value = NULL; memset(short_opts, '\0', sizeof(short_opts)); for (; opts->type != OPTION_END; opts++) { @@ -489,6 +505,14 @@ static void parse_options_check(const struct option *opts) "Are you using parse_options_step() directly?\n" "That case is not supported yet."); break; + case OPTION_SUBCOMMAND: + if (!opts->value || !opts->subcommand_fn) + optbug(opts, "OPTION_SUBCOMMAND needs a value and a subcommand function"); + if (!subcommand_value) + subcommand_value = opts->value; + else if (subcommand_value != opts->value) + optbug(opts, "all OPTION_SUBCOMMANDs need the same value"); + break; default: ; /* ok. (usually accepts an argument) */ } @@ -499,6 +523,14 @@ static void parse_options_check(const struct option *opts) BUG_if_bug("invalid 'struct option'"); } +static int has_subcommands(const struct option *options) +{ + for (; options->type != OPTION_END; options++) + if (options->type == OPTION_SUBCOMMAND) + return 1; + return 0; +} + static void parse_options_start_1(struct parse_opt_ctx_t *ctx, int argc, const char **argv, const char *prefix, const struct option *options, @@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx, ctx->prefix = prefix; ctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0); ctx->flags = flags; + ctx->has_subcommands = has_subcommands(options); + if (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) + BUG("Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands"); + if (ctx->has_subcommands) { + if (flags & PARSE_OPT_STOP_AT_NON_OPTION) + BUG("subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION"); + if (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) { + if (flags & PARSE_OPT_KEEP_UNKNOWN_OPT) + BUG("subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN_OPT unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL"); + if (flags & PARSE_OPT_KEEP_DASHDASH) + BUG("subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL"); + } + } if ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) && (flags & PARSE_OPT_STOP_AT_NON_OPTION) && !(flags & PARSE_OPT_ONE_SHOT)) @@ -589,6 +634,7 @@ static int show_gitcomp(const struct option *opts, int show_all) int nr_noopts = 0; for (; opts->type != OPTION_END; opts++) { + const char *prefix = "--"; const char *suffix = ""; if (!opts->long_name) @@ -598,6 +644,9 @@ static int show_gitcomp(const struct option *opts, int show_all) continue; switch (opts->type) { + case OPTION_SUBCOMMAND: + prefix = ""; + break; case OPTION_GROUP: continue; case OPTION_STRING: @@ -620,8 +669,8 @@ static int show_gitcomp(const struct option *opts, int show_all) suffix = "="; if (starts_with(opts->long_name, "no-")) nr_noopts++; - printf("%s--%s%s", opts == original_opts ? "" : " ", - opts->long_name, suffix); + printf("%s%s%s%s", opts == original_opts ? "" : " ", + prefix, opts->long_name, suffix); } show_negated_gitcomp(original_opts, show_all, -1); show_negated_gitcomp(original_opts, show_all, nr_noopts); @@ -744,10 +793,38 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, if (*arg != '-' || !arg[1]) { if (parse_nodash_opt(ctx, arg, options) == 0) continue; - if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION) - return PARSE_OPT_NON_OPTION; - ctx->out[ctx->cpidx++] = ctx->argv[0]; - continue; + if (!ctx->has_subcommands) { + if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION) + return PARSE_OPT_NON_OPTION; + ctx->out[ctx->cpidx++] = ctx->argv[0]; + continue; + } + switch (parse_subcommand(arg, options)) { + case PARSE_OPT_SUBCOMMAND: + return PARSE_OPT_SUBCOMMAND; + case PARSE_OPT_UNKNOWN: + if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) + /* + * arg is neither a short or long + * option nor a subcommand. Since + * this command has a default + * operation mode, we have to treat + * this arg and all remaining args + * as args meant to that default + * operation mode. + * So we are done parsing. + */ + return PARSE_OPT_DONE; + error(_("unknown subcommand: `%s'"), arg); + usage_with_options(usagestr, options); + case PARSE_OPT_COMPLETE: + case PARSE_OPT_HELP: + case PARSE_OPT_ERROR: + case PARSE_OPT_DONE: + case PARSE_OPT_NON_OPTION: + /* Impossible. */ + BUG("parse_subcommand() cannot return these"); + } } /* lone -h asks for help */ @@ -775,6 +852,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, goto show_usage; goto unknown; case PARSE_OPT_NON_OPTION: + case PARSE_OPT_SUBCOMMAND: case PARSE_OPT_HELP: case PARSE_OPT_COMPLETE: BUG("parse_short_opt() cannot return these"); @@ -800,6 +878,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, *(char *)ctx->argv[0] = '-'; goto unknown; case PARSE_OPT_NON_OPTION: + case PARSE_OPT_SUBCOMMAND: case PARSE_OPT_COMPLETE: case PARSE_OPT_HELP: BUG("parse_short_opt() cannot return these"); @@ -831,6 +910,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, case PARSE_OPT_HELP: goto show_usage; case PARSE_OPT_NON_OPTION: + case PARSE_OPT_SUBCOMMAND: case PARSE_OPT_COMPLETE: BUG("parse_long_opt() cannot return these"); case PARSE_OPT_DONE: @@ -840,6 +920,18 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, unknown: if (ctx->flags & PARSE_OPT_ONE_SHOT) break; + if (ctx->has_subcommands && + (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) && + (ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT)) { + /* + * Found an unknown option given to a command with + * subcommands that has a default operation mode: + * we treat this option and all remaining args as + * arguments meant to that default operation mode. + * So we are done parsing. + */ + return PARSE_OPT_DONE; + } if (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT)) return PARSE_OPT_UNKNOWN; ctx->out[ctx->cpidx++] = ctx->argv[0]; @@ -885,7 +977,14 @@ int parse_options(int argc, const char **argv, case PARSE_OPT_COMPLETE: exit(0); case PARSE_OPT_NON_OPTION: + case PARSE_OPT_SUBCOMMAND: + break; case PARSE_OPT_DONE: + if (ctx.has_subcommands && + !(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) { + error(_("need a subcommand")); + usage_with_options(usagestr, options); + } break; case PARSE_OPT_UNKNOWN: if (ctx.argv[0][1] == '-') { @@ -1010,6 +1109,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t size_t pos; int pad; + if (opts->type == OPTION_SUBCOMMAND) + continue; if (opts->type == OPTION_GROUP) { fputc('\n', outfile); need_newline = 0; diff --git a/parse-options.h b/parse-options.h index 8cbfc7e8bff1f2..b6ef86e0d15e3d 100644 --- a/parse-options.h +++ b/parse-options.h @@ -11,6 +11,7 @@ enum parse_opt_type { OPTION_GROUP, OPTION_NUMBER, OPTION_ALIAS, + OPTION_SUBCOMMAND, /* options with no arguments */ OPTION_BIT, OPTION_NEGBIT, @@ -34,6 +35,7 @@ enum parse_opt_flags { PARSE_OPT_NO_INTERNAL_HELP = 1 << 4, PARSE_OPT_ONE_SHOT = 1 << 5, PARSE_OPT_SHELL_EVAL = 1 << 6, + PARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7, }; enum parse_opt_option_flags { @@ -56,6 +58,7 @@ enum parse_opt_result { PARSE_OPT_ERROR = -1, /* must be the same as error() */ PARSE_OPT_DONE = 0, /* fixed so that "return 0" works */ PARSE_OPT_NON_OPTION, + PARSE_OPT_SUBCOMMAND, PARSE_OPT_UNKNOWN }; @@ -67,6 +70,9 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx, const struct option *opt, const char *arg, int unset); +typedef int parse_opt_subcommand_fn(int argc, const char **argv, + const char *prefix); + /* * `type`:: * holds the type of the option, you must have an OPTION_END last in your @@ -76,7 +82,8 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx, * the character to use as a short option name, '\0' if none. * * `long_name`:: - * the long option name, without the leading dashes, NULL if none. + * the long option (without the leading dashes) or subcommand name, + * NULL if none. * * `value`:: * stores pointers to the values to be filled. @@ -93,7 +100,7 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx, * * `help`:: * the short help associated to what the option does. - * Must never be NULL (except for OPTION_END). + * Must never be NULL (except for OPTION_END and OPTION_SUBCOMMAND). * OPTION_GROUP uses this pointer to store the group header. * Should be wrapped by N_() for translation. * @@ -131,6 +138,9 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx, * `ll_callback`:: * pointer to the callback to use for OPTION_LOWLEVEL_CALLBACK * + * `subcommand_fn`:: + * pointer to a function to use for OPTION_SUBCOMMAND. + * It will be put in value when the subcommand is given on the command line. */ struct option { enum parse_opt_type type; @@ -145,6 +155,7 @@ struct option { intptr_t defval; parse_opt_ll_cb *ll_callback; intptr_t extra; + parse_opt_subcommand_fn *subcommand_fn; }; #define OPT_BIT_F(s, l, v, h, b, f) { OPTION_BIT, (s), (l), (v), NULL, (h), \ @@ -206,6 +217,14 @@ struct option { #define OPT_ALIAS(s, l, source_long_name) \ { OPTION_ALIAS, (s), (l), (source_long_name) } +#define OPT_SUBCOMMAND_F(l, v, fn, f) { \ + .type = OPTION_SUBCOMMAND, \ + .long_name = (l), \ + .value = (v), \ + .flags = (f), \ + .subcommand_fn = (fn) } +#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0) + /* * parse_options() will filter out the processed options and leave the * non-option arguments in argv[]. argv0 is assumed program name and @@ -295,6 +314,7 @@ struct parse_opt_ctx_t { int argc, cpidx, total; const char *opt; enum parse_opt_flags flags; + unsigned has_subcommands; const char *prefix; const char **alias_groups; /* must be in groups of 3 elements! */ struct option *updated_options; diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 99ad6fa4f0b9fa..aa0ad45851ab1b 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -238,6 +238,9 @@ static const struct option test_flag_options[] = { OPT_BIT(0, "no-internal-help", &test_flags, "pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()", PARSE_OPT_NO_INTERNAL_HELP), + OPT_BIT(0, "subcommand-optional", &test_flags, + "pass PARSE_OPT_SUBCOMMAND_OPTIONAL to parse_options()", + PARSE_OPT_SUBCOMMAND_OPTIONAL), OPT_END() }; @@ -258,3 +261,61 @@ int cmd__parse_options_flags(int argc, const char **argv) return parse_options_flags__cmd(argc, argv, test_flags); } + +static int subcmd_one(int argc, const char **argv, const char *prefix) +{ + printf("fn: subcmd_one\n"); + print_args(argc, argv); + return 0; +} + +static int subcmd_two(int argc, const char **argv, const char *prefix) +{ + printf("fn: subcmd_two\n"); + print_args(argc, argv); + return 0; +} + +static int parse_subcommand__cmd(int argc, const char **argv, + enum parse_opt_flags test_flags) +{ + const char *usage[] = { + "<...> cmd subcmd-one", + "<...> cmd subcmd-two", + NULL + }; + parse_opt_subcommand_fn *fn = NULL; + int opt = 0; + struct option options[] = { + OPT_SUBCOMMAND("subcmd-one", &fn, subcmd_one), + OPT_SUBCOMMAND("subcmd-two", &fn, subcmd_two), + OPT_INTEGER('o', "opt", &opt, "an integer option"), + OPT_END() + }; + + if (test_flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) + fn = subcmd_one; + argc = parse_options(argc, argv, NULL, options, usage, test_flags); + + printf("opt: %d\n", opt); + + return fn(argc, argv, NULL); +} + +int cmd__parse_subcommand(int argc, const char **argv) +{ + const char *usage[] = { + "test-tool parse-subcommand [flag-options] cmd ", + NULL + }; + + argc = parse_options(argc, argv, NULL, test_flag_options, usage, + PARSE_OPT_STOP_AT_NON_OPTION); + + if (argc == 0 || strcmp(argv[0], "cmd")) { + error("'cmd' is mandatory"); + usage_with_options(usage, test_flag_options); + } + + return parse_subcommand__cmd(argc, argv, test_flags); +} diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index 6e62282b60c1bd..49b30057f60b16 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -53,6 +53,7 @@ static struct test_cmd cmds[] = { { "parse-options", cmd__parse_options }, { "parse-options-flags", cmd__parse_options_flags }, { "parse-pathspec-file", cmd__parse_pathspec_file }, + { "parse-subcommand", cmd__parse_subcommand }, { "partial-clone", cmd__partial_clone }, { "path-utils", cmd__path_utils }, { "pcre2-config", cmd__pcre2_config }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index d8e8403d707bc8..487d84da60897f 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -43,6 +43,7 @@ int cmd__pack_mtimes(int argc, const char **argv); int cmd__parse_options(int argc, const char **argv); int cmd__parse_options_flags(int argc, const char **argv); int cmd__parse_pathspec_file(int argc, const char** argv); +int cmd__parse_subcommand(int argc, const char **argv); int cmd__partial_clone(int argc, const char **argv); int cmd__path_utils(int argc, const char **argv); int cmd__pcre2_config(int argc, const char **argv); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 264b7373098c10..b19b8d3486f774 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -526,4 +526,189 @@ test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' ' test_cmp expect actual ' +test_expect_success 'subcommand - no subcommand shows error and usage' ' + test_expect_code 129 test-tool parse-subcommand cmd 2>err && + grep "^error: need a subcommand" err && + grep ^usage: err +' + +test_expect_success 'subcommand - subcommand after -- shows error and usage' ' + test_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err && + grep "^error: need a subcommand" err && + grep ^usage: err +' + +test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' ' + test_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err && + grep "^error: need a subcommand" err && + grep ^usage: err +' + +test_expect_success 'subcommand - unknown subcommand shows error and usage' ' + test_expect_code 129 test-tool parse-subcommand cmd nope 2>err && + grep "^error: unknown subcommand: \`nope$SQ" err && + grep ^usage: err +' + +test_expect_success 'subcommand - subcommands cannot be abbreviated' ' + test_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err && + grep "^error: unknown subcommand: \`subcmd-o$SQ$" err && + grep ^usage: err +' + +test_expect_success 'subcommand - no negated subcommands' ' + test_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err && + grep "^error: unknown subcommand: \`no-subcmd-one$SQ" err && + grep ^usage: err +' + +test_expect_success 'subcommand - simple' ' + test-tool parse-subcommand cmd subcmd-two >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_two + arg 00: subcmd-two + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - stop parsing at the first subcommand' ' + test-tool parse-subcommand cmd --opt=1 subcmd-two subcmd-one --opt=2 >actual && + cat >expect <<-\EOF && + opt: 1 + fn: subcmd_two + arg 00: subcmd-two + arg 01: subcmd-one + arg 02: --opt=2 + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - KEEP_ARGV0' ' + test-tool parse-subcommand --keep-argv0 cmd subcmd-two >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_two + arg 00: cmd + arg 01: subcmd-two + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given' ' + test-tool parse-subcommand --subcommand-optional cmd >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_one + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + given subcommand' ' + test-tool parse-subcommand --subcommand-optional cmd subcmd-two branch file >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_two + arg 00: subcmd-two + arg 01: branch + arg 02: file + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown dashless args' ' + test-tool parse-subcommand --subcommand-optional cmd branch file >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_one + arg 00: branch + arg 01: file + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown option' ' + test_expect_code 129 test-tool parse-subcommand --subcommand-optional cmd --subcommand-opt 2>err && + grep "^error: unknown option" err && + grep ^usage: err +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand not given + unknown option' ' + test-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_one + arg 00: --subcommand-opt + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand ignored after unknown option' ' + test-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt subcmd-two >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_one + arg 00: --subcommand-opt + arg 01: subcmd-two + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + command and subcommand options cannot be mixed' ' + test-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt branch --opt=1 >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_one + arg 00: --subcommand-opt + arg 01: branch + arg 02: --opt=1 + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT | KEEP_ARGV0' ' + test-tool parse-subcommand --subcommand-optional --keep-unknown-opt --keep-argv0 cmd --subcommand-opt branch >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_one + arg 00: cmd + arg 01: --subcommand-opt + arg 02: branch + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT | KEEP_DASHDASH' ' + test-tool parse-subcommand --subcommand-optional --keep-unknown-opt --keep-dashdash cmd -- --subcommand-opt file >actual && + cat >expect <<-\EOF && + opt: 0 + fn: subcmd_one + arg 00: -- + arg 01: --subcommand-opt + arg 02: file + EOF + test_cmp expect actual +' + +test_expect_success 'subcommand - completion helper' ' + test-tool parse-subcommand cmd --git-completion-helper >actual && + echo "subcmd-one subcmd-two --opt= --no-opt" >expect && + test_cmp expect actual +' + +test_expect_success 'subcommands are incompatible with STOP_AT_NON_OPTION' ' + test_must_fail test-tool parse-subcommand --stop-at-non-option cmd subcmd-one 2>err && + grep ^BUG err +' + +test_expect_success 'subcommands are incompatible with KEEP_UNKNOWN_OPT unless in combination with SUBCOMMAND_OPTIONAL' ' + test_must_fail test-tool parse-subcommand --keep-unknown-opt cmd subcmd-two 2>err && + grep ^BUG err +' + +test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in combination with SUBCOMMAND_OPTIONAL' ' + test_must_fail test-tool parse-subcommand --keep-dashdash cmd subcmd-two 2>err && + grep ^BUG err +' + test_done