{"thread":{"id":"58218","subject":"[PATCH 00/20] parse-options: handle subcommands","startedAt":"2022-07-25T12:39:08Z","lastAt":"2022-09-07T21:22:20Z","messageCount":97,"participants":["SZEDER Gábor","Derrick Stolee","Ævar Arnfjörð Bjarmason","Junio C Hamano","Johannes Schindelin"],"isPatch":true,"patchVersion":1,"patchTotal":20},"messages":[{"id":"459862","messageId":"20220725123857.2773963-1-szeder.dev@gmail.com","threadId":"58218","inReplyTo":null,"subject":"[PATCH 00/20] parse-options: handle subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:37Z","receivedAt":"2022-07-25T12:39:08Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Several Git commands have subcommands to implement mutually exclusive\n\"operation modes\", and they usually parse their subcommand argument\nwith a bunch of if-else if statements.\n\nTeach parse-options to handle subcommands as well, which will result\nin shorter and simpler code with consistent error handling and error\nmessages on unknown or missing subcommand, and it will also make\npossible for our Bash completion script to handle subcommands\nprogrammatically in a follow-up series [1].\n\nPatches 1-8 are a mix of preparatory cleanups, documentation updates, and\ntest coverage improvements.\n\nPatch 9 is the most important one, which teaches parse-options to handle\nsubcommands.\n\nThe remaining 10-20 convert most builtin commands with subcommands one by\none to use parse-options to handle their subcommand parameters.\n\n\nThis patch series has two conflicts with 'seen' (but none with 'next'):\n\n  - builtin/bisect.c: after the conversion/rename from 'bisect--helper',\n    cmd_bisect() doesn't use parse-options anymore.  Take what's on 'seen'\n    to resolve the conflict.\n    Note that the conflicting topic should have marked cmd_bisect() with\n    the NO_PARSEOPT flag in 'git.c's command list.\n\n  - builtin/stash.c: add OPT_SUBCOMMAND entries for the two new\n    subcommands 'export' and 'import' to resolve the conflict.\n\n\n[1] WIP with some one-liner commit messages and missing sign-offs here and\n    there:\n\n      https://github.com/szeder/git completion-subcommands\n\n\nSZEDER Gábor (20):\n  git.c: update NO_PARSEOPT markings\n  t3301-notes.sh: check that default operation mode doesn't take\n    arguments\n  t5505-remote.sh: check the behavior without a subcommand\n  t0040-parse-options: test parse_options() with various\n    'parse_opt_flags'\n  api-parse-options.txt: fix description of OPT_CMDMODE\n  parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options\n  parse-options: clarify the limitations of PARSE_OPT_NODASH\n  parse-options: drop leading space from '--git-completion-helper'\n    output\n  parse-options: add support for parsing subcommands\n  builtin/bundle.c: let parse-options parse subcommands\n  builtin/commit-graph.c: let parse-options parse subcommands\n  builtin/gc.c: let parse-options parse 'git maintenance's subcommands\n  builtin/hook.c: let parse-option parse subcommands\n  builtin/multi-pack-index.c: let parse-options parse subcommands\n  builtin/notes.c: let parse-options parse subcommands\n  builtin/reflog.c: let parse-options parse subcommands\n  builtin/remote.c: let parse-options parse subcommands\n  builtin/sparse-checkout.c: let parse-options parse subcommands\n  builtin/stash.c: let parse-options parse subcommands\n  builtin/worktree.c: let parse-options parse subcommands\n\n Documentation/technical/api-parse-options.txt |  49 +++-\n builtin/archive.c                             |   2 +-\n builtin/bisect--helper.c                      |   2 +-\n builtin/blame.c                               |   1 +\n builtin/bundle.c                              |  25 +-\n builtin/commit-graph.c                        |  30 +--\n builtin/difftool.c                            |   2 +-\n builtin/env--helper.c                         |   2 +-\n builtin/fast-export.c                         |   2 +-\n builtin/gc.c                                  |  42 +--\n builtin/hook.c                                |  12 +-\n builtin/log.c                                 |   4 +-\n builtin/multi-pack-index.c                    |  51 ++--\n builtin/notes.c                               |  43 ++-\n builtin/reflog.c                              |  43 +--\n builtin/remote.c                              |  70 +++--\n builtin/revert.c                              |   2 +-\n builtin/shortlog.c                            |   1 +\n builtin/sparse-checkout.c                     |  48 ++--\n builtin/stash.c                               |  59 ++--\n builtin/worktree.c                            |  31 +--\n diff.c                                        |   2 +-\n git.c                                         |  14 +-\n parse-options.c                               | 118 +++++++-\n parse-options.h                               |  27 +-\n t/helper/test-parse-options.c                 | 129 +++++++++\n t/helper/test-serve-v2.c                      |   2 +-\n t/helper/test-tool.c                          |   2 +\n t/helper/test-tool.h                          |   2 +\n t/t0040-parse-options.sh                      | 255 ++++++++++++++++++\n t/t3301-notes.sh                              |   5 +\n t/t3903-stash.sh                              |   2 +-\n t/t5318-commit-graph.sh                       |   4 +-\n t/t5505-remote.sh                             |  29 ++\n t/t7900-maintenance.sh                        |  10 +-\n 35 files changed, 812 insertions(+), 310 deletions(-)\n\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459863","messageId":"20220725123857.2773963-2-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:38Z","receivedAt":"2022-07-25T12:39:12Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Our Bash completion script can complete --options for commands using\nparse-options even when that command doesn't have a dedicated\ncompletion function, but to do so the completion script must know\nwhich commands use parse-options and which don't.  Therefore, commands\nnot using parse-options are marked in 'git.c's command list with the\nNO_PARSEOPT flag.\n\nUpdate this list, and remove this flag from the commands that by now\nuse parse-options.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n git.c | 12 ++++++------\n 1 file changed, 6 insertions(+), 6 deletions(-)\n\ndiff --git a/git.c b/git.c\nindex e5d62fa5a9..c4282f194a 100644\n--- a/git.c\n+++ b/git.c\n@@ -489,14 +489,14 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)\n static struct cmd_struct commands[] = {\n \t{ \"add\", cmd_add, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"am\", cmd_am, RUN_SETUP | NEED_WORK_TREE },\n-\t{ \"annotate\", cmd_annotate, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"annotate\", cmd_annotate, RUN_SETUP },\n \t{ \"apply\", cmd_apply, RUN_SETUP_GENTLY },\n \t{ \"archive\", cmd_archive, RUN_SETUP_GENTLY },\n \t{ \"bisect--helper\", cmd_bisect__helper, RUN_SETUP },\n \t{ \"blame\", cmd_blame, RUN_SETUP },\n \t{ \"branch\", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },\n \t{ \"bugreport\", cmd_bugreport, RUN_SETUP_GENTLY },\n-\t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },\n+\t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY },\n \t{ \"cat-file\", cmd_cat_file, RUN_SETUP },\n \t{ \"check-attr\", cmd_check_attr, RUN_SETUP },\n \t{ \"check-ignore\", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },\n@@ -514,7 +514,7 @@ static struct cmd_struct commands[] = {\n \t{ \"column\", cmd_column, RUN_SETUP_GENTLY },\n \t{ \"commit\", cmd_commit, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"commit-graph\", cmd_commit_graph, RUN_SETUP },\n-\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP },\n \t{ \"config\", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },\n \t{ \"count-objects\", cmd_count_objects, RUN_SETUP },\n \t{ \"credential\", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },\n@@ -553,7 +553,7 @@ static struct cmd_struct commands[] = {\n \t{ \"ls-files\", cmd_ls_files, RUN_SETUP },\n \t{ \"ls-remote\", cmd_ls_remote, RUN_SETUP_GENTLY },\n \t{ \"ls-tree\", cmd_ls_tree, RUN_SETUP },\n-\t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },\n+\t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY },\n \t{ \"mailsplit\", cmd_mailsplit, NO_PARSEOPT },\n \t{ \"maintenance\", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },\n \t{ \"merge\", cmd_merge, RUN_SETUP | NEED_WORK_TREE },\n@@ -566,7 +566,7 @@ static struct cmd_struct commands[] = {\n \t{ \"merge-recursive-theirs\", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },\n \t{ \"merge-subtree\", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },\n \t{ \"merge-tree\", cmd_merge_tree, RUN_SETUP },\n-\t{ \"mktag\", cmd_mktag, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"mktag\", cmd_mktag, RUN_SETUP },\n \t{ \"mktree\", cmd_mktree, RUN_SETUP },\n \t{ \"multi-pack-index\", cmd_multi_pack_index, RUN_SETUP },\n \t{ \"mv\", cmd_mv, RUN_SETUP | NEED_WORK_TREE },\n@@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {\n \t{ \"verify-tag\", cmd_verify_tag, RUN_SETUP },\n \t{ \"version\", cmd_version },\n \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP },\n-\t{ \"worktree\", cmd_worktree, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"worktree\", cmd_worktree, RUN_SETUP },\n \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n };\n \n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459864","messageId":"20220725123857.2773963-3-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:39Z","receivedAt":"2022-07-25T12:39:17Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git notes' without a subcommand defaults to listing all notes and\ndoesn't accept any arguments.\n\nWe are about to teach parse-options to handle subcommands, and update\n'git notes' to make use of that new feature.  So let's add a test to\nmake sure that the upcoming changes don't inadvertenly change the\nbehavior in this corner case.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/t3301-notes.sh | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/t/t3301-notes.sh b/t/t3301-notes.sh\nindex d742be8840..3288aaec7d 100755\n--- a/t/t3301-notes.sh\n+++ b/t/t3301-notes.sh\n@@ -505,6 +505,11 @@ test_expect_success 'list notes with \"git notes\"' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success '\"git notes\" without subcommand does not take arguments' '\n+\ttest_expect_code 129 git notes HEAD^^ 2>err &&\n+\tgrep \"^error: unknown subcommand\" err\n+'\n+\n test_expect_success 'list specific note with \"git notes list <object>\"' '\n \tgit rev-parse refs/notes/commits:$commit_3 >expect &&\n \tgit notes list HEAD^^ >actual &&\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459865","messageId":"20220725123857.2773963-4-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 03/20] t5505-remote.sh: check the behavior without a subcommand","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:40Z","receivedAt":"2022-07-25T12:39:19Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git remote' without a subcommand defaults to listing all remotes and\ndoesn't accept any arguments except the '-v|--verbose' option.\n\nWe are about to teach parse-options to handle subcommands, and update\n'git remote' to make use of that new feature.  So let's add some tests\nto make sure that the upcoming changes don't inadvertently change the\nbehavior in these cases.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/t5505-remote.sh | 29 +++++++++++++++++++++++++++++\n 1 file changed, 29 insertions(+)\n\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 6c7370f87f..f075dd4afa 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -241,6 +241,35 @@ test_expect_success 'add invalid foreign_vcs remote' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'without subcommand' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote >actual &&\n+\t\techo origin >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'without subcommand accepts -v' '\n+\tcat >test/expect <<-EOF &&\n+\torigin\t$(pwd)/one (fetch)\n+\torigin\t$(pwd)/one (push)\n+\tEOF\n+\t(\n+\t\tcd test &&\n+\t\tgit remote -v >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'without subcommand does not take arguments' '\n+\t(\n+\t\tcd test &&\n+\t\ttest_expect_code 129 git remote origin 2>err &&\n+\t\tgrep \"^error: Unknown subcommand:\" err\n+\t)\n+'\n+\n cat >test/expect <<EOF\n * remote origin\n   Fetch URL: $(pwd)/one\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459866","messageId":"20220725123857.2773963-5-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:41Z","receivedAt":"2022-07-25T12:39:21Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"In 't0040-parse-options.sh' we thoroughly test the parsing of all\ntypes and forms of options, but in all those tests parse_options() is\nalways invoked with a 0 flags parameter.\n\nAdd a few tests to demonstrate how various 'enum parse_opt_flags'\nvalues are supposed to influence option parsing.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/helper/test-parse-options.c | 61 ++++++++++++++++++++++++++++++\n t/helper/test-tool.c          |  1 +\n t/helper/test-tool.h          |  1 +\n t/t0040-parse-options.sh      | 70 +++++++++++++++++++++++++++++++++++\n 4 files changed, 133 insertions(+)\n\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 48d3cf6692..32b906bd6a 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -192,3 +192,64 @@ int cmd__parse_options(int argc, const char **argv)\n \n \treturn ret;\n }\n+\n+static int parse_options_flags__cmd(int argc, const char **argv,\n+\t\t\t\t    enum parse_opt_flags test_flags)\n+{\n+\tconst char *usage[] = {\n+\t\t\"<...> cmd [options]\",\n+\t\tNULL\n+\t};\n+\tint opt = 0;\n+\tconst struct option options[] = {\n+\t\tOPT_INTEGER('o', \"opt\", &opt, \"an integer option\"),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, options, usage, test_flags);\n+\n+\tprintf(\"opt: %d\\n\", opt);\n+\tfor (int i = 0; i < argc; i++)\n+\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n+\n+\treturn 0;\n+}\n+\n+static enum parse_opt_flags test_flags = 0;\n+static const struct option test_flag_options[] = {\n+\tOPT_GROUP(\"flag-options:\"),\n+\tOPT_BIT(0, \"keep-dashdash\", &test_flags,\n+\t\t\"pass PARSE_OPT_KEEP_DASHDASH to parse_options()\",\n+\t\tPARSE_OPT_KEEP_DASHDASH),\n+\tOPT_BIT(0, \"stop-at-non-option\", &test_flags,\n+\t\t\"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()\",\n+\t\tPARSE_OPT_STOP_AT_NON_OPTION),\n+\tOPT_BIT(0, \"keep-argv0\", &test_flags,\n+\t\t\"pass PARSE_OPT_KEEP_ARGV0 to parse_options()\",\n+\t\tPARSE_OPT_KEEP_ARGV0),\n+\tOPT_BIT(0, \"keep-unknown\", &test_flags,\n+\t\t\"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()\",\n+\t\tPARSE_OPT_KEEP_UNKNOWN),\n+\tOPT_BIT(0, \"no-internal-help\", &test_flags,\n+\t\t\"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()\",\n+\t\tPARSE_OPT_NO_INTERNAL_HELP),\n+\tOPT_END()\n+};\n+\n+int cmd__parse_options_flags(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool parse-options-flags [flag-options] cmd [options]\",\n+\t\tNULL\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, test_flag_options, usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n+\t\terror(\"'cmd' is mandatory\");\n+\t\tusage_with_options(usage, test_flag_options);\n+\t}\n+\n+\treturn parse_options_flags__cmd(argc, argv, test_flags);\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex 318fdbab0c..6e62282b60 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -51,6 +51,7 @@ static struct test_cmd cmds[] = {\n \t{ \"online-cpus\", cmd__online_cpus },\n \t{ \"pack-mtimes\", cmd__pack_mtimes },\n \t{ \"parse-options\", cmd__parse_options },\n+\t{ \"parse-options-flags\", cmd__parse_options_flags },\n \t{ \"parse-pathspec-file\", cmd__parse_pathspec_file },\n \t{ \"partial-clone\", cmd__partial_clone },\n \t{ \"path-utils\", cmd__path_utils },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex bb79927163..d8e8403d70 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -41,6 +41,7 @@ int cmd__oidtree(int argc, const char **argv);\n int cmd__online_cpus(int argc, const char **argv);\n int cmd__pack_mtimes(int argc, const char **argv);\n int cmd__parse_options(int argc, const char **argv);\n+int cmd__parse_options_flags(int argc, const char **argv);\n int cmd__parse_pathspec_file(int argc, const char** argv);\n int cmd__partial_clone(int argc, const char **argv);\n int cmd__path_utils(int argc, const char **argv);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ed2fb620a9..569e906579 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -456,4 +456,74 @@ test_expect_success '--end-of-options treats remainder as args' '\n \t    --end-of-options --verbose\n '\n \n+test_expect_success 'KEEP_DASHDASH works' '\n+\ttest-tool parse-options-flags --keep-dashdash cmd --opt=1 -- --opt=2 --unknown >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 1\n+\targ 00: --\n+\targ 01: --opt=2\n+\targ 02: --unknown\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'KEEP_ARGV0 works' '\n+\ttest-tool parse-options-flags --keep-argv0 cmd arg0 --opt=3 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 3\n+\targ 00: cmd\n+\targ 01: arg0\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'STOP_AT_NON_OPTION works' '\n+\ttest-tool parse-options-flags --stop-at-non-option cmd --opt=4 arg0 --opt=5 --unknown >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 4\n+\targ 00: arg0\n+\targ 01: --opt=5\n+\targ 02: --unknown\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'KEEP_UNKNOWN works' '\n+\ttest-tool parse-options-flags --keep-unknown cmd --unknown=1 --opt=6 -u2 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 6\n+\targ 00: --unknown=1\n+\targ 01: -u2\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'NO_INTERNAL_HELP works for -h' '\n+\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n+\tcat err &&\n+\tgrep \"^error: unknown switch \\`h'\\''\" err &&\n+\tgrep \"^usage: \" err\n+'\n+\n+for help_opt in help help-all\n+do\n+\ttest_expect_success \"NO_INTERNAL_HELP works for --$help_opt\" \"\n+\t\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&\n+\t\tcat err &&\n+\t\tgrep '^error: unknown option \\`'$help_opt\\' err &&\n+\t\tgrep '^usage: ' err\n+\t\"\n+done\n+\n+test_expect_success 'KEEP_UNKNOWN | NO_INTERNAL_HELP works' '\n+\ttest-tool parse-options-flags --keep-unknown --no-internal-help cmd -h --help --help-all >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\targ 00: -h\n+\targ 01: --help\n+\targ 02: --help-all\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459867","messageId":"20220725123857.2773963-6-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 05/20] api-parse-options.txt: fix description of OPT_CMDMODE","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:42Z","receivedAt":"2022-07-25T12:39:23Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The description of the 'OPT_CMDMODE' macro states that \"enum_val is\nset to int_var when ...\", but it's the other way around, 'int_var' is\nset to 'enum_val'.  Fix this.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n Documentation/technical/api-parse-options.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex acfd5dc1d8..5a04f3daec 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -236,7 +236,7 @@ There are some macros to easily define options:\n `OPT_CMDMODE(short, long, &int_var, description, enum_val)`::\n \tDefine an \"operation mode\" option, only one of which in the same\n \tgroup of \"operating mode\" options that share the same `int_var`\n-\tcan be given by the user. `enum_val` is set to `int_var` when the\n+\tcan be given by the user. `int_var` is set to `enum_val` when the\n \toption is used, but an error is reported if other \"operating mode\"\n \toption has already set its value to the same `int_var`.\n \n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459868","messageId":"20220725123857.2773963-7-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:43Z","receivedAt":"2022-07-25T12:39:25Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The description of 'PARSE_OPT_KEEP_UNKNOWN' starts with \"Keep unknown\narguments instead of erroring out\".  This is a bit misleading, as this\nflag only applies to unknown --options, while non-option arguments are\nkept even without this flag.\n\nUpdate the description to clarify this, and rename the flag to\nPARSE_OPTIONS_KEEP_UNKNOWN_OPT to make this obvious just by looking at\nthe flag name.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n Documentation/technical/api-parse-options.txt | 6 ++++--\n builtin/archive.c                             | 2 +-\n builtin/bisect--helper.c                      | 2 +-\n builtin/difftool.c                            | 2 +-\n builtin/env--helper.c                         | 2 +-\n builtin/fast-export.c                         | 2 +-\n builtin/log.c                                 | 4 ++--\n builtin/reflog.c                              | 4 ++--\n builtin/revert.c                              | 2 +-\n builtin/sparse-checkout.c                     | 4 ++--\n builtin/stash.c                               | 8 ++++----\n diff.c                                        | 2 +-\n parse-options.c                               | 6 +++---\n parse-options.h                               | 2 +-\n t/helper/test-parse-options.c                 | 6 +++---\n t/helper/test-serve-v2.c                      | 2 +-\n t/t0040-parse-options.sh                      | 8 ++++----\n 17 files changed, 33 insertions(+), 31 deletions(-)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 5a04f3daec..4412377fa3 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -90,8 +90,8 @@ Flags are the bitwise-or of:\n \tKeep the first argument, which contains the program name.  It's\n \tremoved from argv[] by default.\n \n-`PARSE_OPT_KEEP_UNKNOWN`::\n-\tKeep unknown arguments instead of erroring out.  This doesn't\n+`PARSE_OPT_KEEP_UNKNOWN_OPT`::\n+\tKeep unknown options instead of erroring out.  This doesn't\n \twork for all combinations of arguments as users might expect\n \tit to do.  E.g. if the first argument in `--unknown --known`\n \ttakes a value (which we can't know), the second one is\n@@ -101,6 +101,8 @@ Flags are the bitwise-or of:\n \tnon-option, not as a value belonging to the unknown option,\n \tthe parser early.  That's why parse_options() errors out if\n \tboth options are set.\n+\tNote that non-option arguments are always kept, even without\n+\tthis flag.\n \n `PARSE_OPT_NO_INTERNAL_HELP`::\n \tBy default, parse_options() handles `-h`, `--help` and\ndiff --git a/builtin/archive.c b/builtin/archive.c\nindex 7176b041b6..f094390ee0 100644\n--- a/builtin/archive.c\n+++ b/builtin/archive.c\n@@ -75,7 +75,7 @@ static int run_remote_archiver(int argc, const char **argv,\n \n #define PARSE_OPT_KEEP_ALL ( PARSE_OPT_KEEP_DASHDASH | \t\\\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | \t\\\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN |\t\\\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\t\\\n \t\t\t     PARSE_OPT_NO_INTERNAL_HELP\t)\n \n int cmd_archive(int argc, const char **argv, const char *prefix)\ndiff --git a/builtin/bisect--helper.c b/builtin/bisect--helper.c\nindex 8a052c7111..7097750fc6 100644\n--- a/builtin/bisect--helper.c\n+++ b/builtin/bisect--helper.c\n@@ -1324,7 +1324,7 @@ int cmd_bisect__helper(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options,\n \t\t\t     git_bisect_helper_usage,\n-\t\t\t     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (!cmdmode)\n \t\tusage_with_options(git_bisect_helper_usage, options);\ndiff --git a/builtin/difftool.c b/builtin/difftool.c\nindex b3c509b8de..8706f68492 100644\n--- a/builtin/difftool.c\n+++ b/builtin/difftool.c\n@@ -716,7 +716,7 @@ int cmd_difftool(int argc, const char **argv, const char *prefix)\n \tsymlinks = has_symlinks;\n \n \targc = parse_options(argc, argv, prefix, builtin_difftool_options,\n-\t\t\t     builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \tif (tool_help)\ndiff --git a/builtin/env--helper.c b/builtin/env--helper.c\nindex 27349098b0..ea04c16636 100644\n--- a/builtin/env--helper.c\n+++ b/builtin/env--helper.c\n@@ -50,7 +50,7 @@ int cmd_env__helper(int argc, const char **argv, const char *prefix)\n \t};\n \n \targc = parse_options(argc, argv, prefix, opts, env__helper_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \tif (env_default && !*env_default)\n \t\tusage_with_options(env__helper_usage, opts);\n \tif (!cmdmode)\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex e1748fb98b..bf3c20dea2 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -1221,7 +1221,7 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix)\n \trevs.sources = &revision_sources;\n \trevs.rewrite_parents = 1;\n \targc = parse_options(argc, argv, prefix, options, fast_export_usage,\n-\t\t\tPARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\tPARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \targc = setup_revisions(argc, argv, &revs, NULL);\n \tif (argc > 1)\n \t\tusage_with_options (fast_export_usage, options);\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 88a5e98875..fb84a0d399 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -199,7 +199,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \tmailmap = use_mailmap_config;\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_log_options, builtin_log_usage,\n-\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \tif (quiet)\n@@ -1926,7 +1926,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t */\n \targc = parse_options(argc, argv, prefix, builtin_format_patch_options,\n \t\t\t     builtin_format_patch_usage,\n-\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \t/* Make sure \"0000-$sub.patch\" gives non-negative length for $sub */\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 4dd297dce8..b8b1f4f8ea 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -223,7 +223,7 @@ static int cmd_reflog_show(int argc, const char **argv, const char *prefix)\n \n \tparse_options(argc, argv, prefix, options, reflog_show_usage,\n \t\t      PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |\n-\t\t      PARSE_OPT_KEEP_UNKNOWN);\n+\t\t      PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \treturn cmd_log_reflog(argc, argv, prefix);\n }\n@@ -410,7 +410,7 @@ int cmd_reflog(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options, reflog_usage,\n \t\t\t     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_NO_INTERNAL_HELP);\n \n \t/*\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex 2554f9099c..ee2a0807f0 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -141,7 +141,7 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)\n \n \targc = parse_options(argc, argv, NULL, options, usage_str,\n \t\t\tPARSE_OPT_KEEP_ARGV0 |\n-\t\t\tPARSE_OPT_KEEP_UNKNOWN);\n+\t\t\tPARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.command_requires_full_index = 0;\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex f91e29b56a..a5e4b95a9d 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -767,7 +767,7 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_sparse_checkout_add_options,\n \t\t\t     builtin_sparse_checkout_add_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tsanitize_paths(argc, argv, prefix, add_opts.skip_checks);\n \n@@ -813,7 +813,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_sparse_checkout_set_options,\n \t\t\t     builtin_sparse_checkout_set_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (update_modes(&set_opts.cone_mode, &set_opts.sparse_index))\n \t\treturn 1;\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 30fa101460..a14e832e9f 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -782,7 +782,7 @@ static int list_stash(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options,\n \t\t\t     git_stash_list_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (!ref_exists(ref_stash))\n \t\treturn 0;\n@@ -873,7 +873,7 @@ static int show_stash(int argc, const char **argv, const char *prefix)\n \tinit_revisions(&rev, prefix);\n \n \targc = parse_options(argc, argv, prefix, options, git_stash_show_usage,\n-\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \tstrvec_push(&revision_args, argv[0]);\n@@ -979,7 +979,7 @@ static int store_stash(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options,\n \t\t\t     git_stash_store_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (argc != 1) {\n \t\tif (!quiet)\n@@ -1795,7 +1795,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \tgit_config(git_stash_config, NULL);\n \n \targc = parse_options(argc, argv, prefix, options, git_stash_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.command_requires_full_index = 0;\ndiff --git a/diff.c b/diff.c\nindex 974626a621..dd68281ba4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -5661,7 +5661,7 @@ int diff_opt_parse(struct diff_options *options,\n \n \tac = parse_options(ac, av, prefix, options->parseopts, NULL,\n \t\t\t   PARSE_OPT_KEEP_DASHDASH |\n-\t\t\t   PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t   PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t   PARSE_OPT_NO_INTERNAL_HELP |\n \t\t\t   PARSE_OPT_ONE_SHOT |\n \t\t\t   PARSE_OPT_STOP_AT_NON_OPTION);\ndiff --git a/parse-options.c b/parse-options.c\nindex edf55d3ef5..a0a2cf98fa 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -332,7 +332,7 @@ static enum parse_opt_result parse_long_opt(\n \t\t\trest = NULL;\n \t\tif (!rest) {\n \t\t\t/* abbreviated? */\n-\t\t\tif (!(p->flags & PARSE_OPT_KEEP_UNKNOWN) &&\n+\t\t\tif (!(p->flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&\n \t\t\t    !strncmp(long_name, arg, arg_end - arg)) {\n is_abbreviated:\n \t\t\t\tif (abbrev_option &&\n@@ -515,7 +515,7 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n \tctx->prefix = prefix;\n \tctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);\n \tctx->flags = flags;\n-\tif ((flags & PARSE_OPT_KEEP_UNKNOWN) &&\n+\tif ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&\n \t    (flags & PARSE_OPT_STOP_AT_NON_OPTION) &&\n \t    !(flags & PARSE_OPT_ONE_SHOT))\n \t\tBUG(\"STOP_AT_NON_OPTION and KEEP_UNKNOWN don't go together\");\n@@ -839,7 +839,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n unknown:\n \t\tif (ctx->flags & PARSE_OPT_ONE_SHOT)\n \t\t\tbreak;\n-\t\tif (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN))\n+\t\tif (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT))\n \t\t\treturn PARSE_OPT_UNKNOWN;\n \t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n \t\tctx->opt = NULL;\ndiff --git a/parse-options.h b/parse-options.h\nindex 685fccac13..591df64191 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -30,7 +30,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_KEEP_DASHDASH = 1 << 0,\n \tPARSE_OPT_STOP_AT_NON_OPTION = 1 << 1,\n \tPARSE_OPT_KEEP_ARGV0 = 1 << 2,\n-\tPARSE_OPT_KEEP_UNKNOWN = 1 << 3,\n+\tPARSE_OPT_KEEP_UNKNOWN_OPT = 1 << 3,\n \tPARSE_OPT_NO_INTERNAL_HELP = 1 << 4,\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 32b906bd6a..a715c772bd 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -227,9 +227,9 @@ static const struct option test_flag_options[] = {\n \tOPT_BIT(0, \"keep-argv0\", &test_flags,\n \t\t\"pass PARSE_OPT_KEEP_ARGV0 to parse_options()\",\n \t\tPARSE_OPT_KEEP_ARGV0),\n-\tOPT_BIT(0, \"keep-unknown\", &test_flags,\n-\t\t\"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()\",\n-\t\tPARSE_OPT_KEEP_UNKNOWN),\n+\tOPT_BIT(0, \"keep-unknown-opt\", &test_flags,\n+\t\t\"pass PARSE_OPT_KEEP_UNKNOWN_OPT to parse_options()\",\n+\t\tPARSE_OPT_KEEP_UNKNOWN_OPT),\n \tOPT_BIT(0, \"no-internal-help\", &test_flags,\n \t\t\"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()\",\n \t\tPARSE_OPT_NO_INTERNAL_HELP),\ndiff --git a/t/helper/test-serve-v2.c b/t/helper/test-serve-v2.c\nindex 28e905afc3..824e5c0a95 100644\n--- a/t/helper/test-serve-v2.c\n+++ b/t/helper/test-serve-v2.c\n@@ -24,7 +24,7 @@ int cmd__serve_v2(int argc, const char **argv)\n \t/* ignore all unknown cmdline switches for now */\n \targc = parse_options(argc, argv, prefix, options, serve_usage,\n \t\t\t     PARSE_OPT_KEEP_DASHDASH |\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (advertise_capabilities)\n \t\tprotocol_v2_advertise_capabilities();\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 569e906579..673a01ca71 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -488,8 +488,8 @@ test_expect_success 'STOP_AT_NON_OPTION works' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'KEEP_UNKNOWN works' '\n-\ttest-tool parse-options-flags --keep-unknown cmd --unknown=1 --opt=6 -u2 >actual &&\n+test_expect_success 'KEEP_UNKNOWN_OPT works' '\n+\ttest-tool parse-options-flags --keep-unknown-opt cmd --unknown=1 --opt=6 -u2 >actual &&\n \tcat >expect <<-\\EOF &&\n \topt: 6\n \targ 00: --unknown=1\n@@ -515,8 +515,8 @@ do\n \t\"\n done\n \n-test_expect_success 'KEEP_UNKNOWN | NO_INTERNAL_HELP works' '\n-\ttest-tool parse-options-flags --keep-unknown --no-internal-help cmd -h --help --help-all >actual &&\n+test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '\n+\ttest-tool parse-options-flags --keep-unknown-opt --no-internal-help cmd -h --help --help-all >actual &&\n \tcat >expect <<-\\EOF &&\n \topt: 0\n \targ 00: -h\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459869","messageId":"20220725123857.2773963-8-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:44Z","receivedAt":"2022-07-25T12:39:28Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Update the comment documenting 'struct option' to clarify that\nPARSE_OPT_NODASH can only be an argumentless short option; see\n51a9949eda (parseopt: add PARSE_OPT_NODASH, 2009-05-07).\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n parse-options.h | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/parse-options.h b/parse-options.h\nindex 591df64191..8cbfc7e8bf 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -109,7 +109,8 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n  *\t\t\t\tis last on the command line. If the option is\n  *\t\t\t\tnot last it will require an argument.\n  *\t\t\t\tShould not be used with PARSE_OPT_OPTARG.\n- *   PARSE_OPT_NODASH: this option doesn't start with a dash.\n+ *   PARSE_OPT_NODASH: this option doesn't start with a dash; can only be a\n+ *\t\t       short option and can't accept arguments.\n  *   PARSE_OPT_LITERAL_ARGHELP: says that argh shouldn't be enclosed in brackets\n  *\t\t\t\t(i.e. '<argh>') in the help message.\n  *\t\t\t\tUseful for options with multiple parameters.\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459870","messageId":"20220725123857.2773963-9-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 08/20] parse-options: drop leading space from '--git-completion-helper' output","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:45Z","receivedAt":"2022-07-25T12:39:36Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The output of 'git <cmd> --git-completion-helper' always starts with a\nspace, e.g.:\n\n  $ git config --git-completion-helper\n   --global --system --local [...]\n\nThis doesn't matter for the completion script, because field splitting\ndiscards that space anyway.\n\nHowever, later patches in this series will teach parse-options to\nhandle subcommands, and subcommands will be included in the completion\nhelper output as well.  This will make the loop printing options (and\nsubcommands) a tad more complex, so I wanted to test the result.  The\ntest would have to account for the presence of that leading space,\nwhich bugged my OCD, so let's get rid of it.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n parse-options.c | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex a0a2cf98fa..8748f88e6f 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -620,7 +620,8 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \t\t\tsuffix = \"=\";\n \t\tif (starts_with(opts->long_name, \"no-\"))\n \t\t\tnr_noopts++;\n-\t\tprintf(\" --%s%s\", opts->long_name, suffix);\n+\t\tprintf(\"%s--%s%s\", opts == original_opts ? \"\" : \" \",\n+\t\t       opts->long_name, suffix);\n \t}\n \tshow_negated_gitcomp(original_opts, show_all, -1);\n \tshow_negated_gitcomp(original_opts, show_all, nr_noopts);\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459871","messageId":"20220725123857.2773963-11-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 10/20] builtin/bundle.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:47Z","receivedAt":"2022-07-25T12:39:43Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git bundle' parses its subcommands with a couple of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/bundle.c | 25 +++++++------------------\n 1 file changed, 7 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/bundle.c b/builtin/bundle.c\nindex 2adad545a2..e80efce3a4 100644\n--- a/builtin/bundle.c\n+++ b/builtin/bundle.c\n@@ -195,30 +195,19 @@ static int cmd_bundle_unbundle(int argc, const char **argv, const char *prefix)\n \n int cmd_bundle(int argc, const char **argv, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_bundle_create),\n+\t\tOPT_SUBCOMMAND(\"verify\", &fn, cmd_bundle_verify),\n+\t\tOPT_SUBCOMMAND(\"list-heads\", &fn, cmd_bundle_list_heads),\n+\t\tOPT_SUBCOMMAND(\"unbundle\", &fn, cmd_bundle_unbundle),\n \t\tOPT_END()\n \t};\n-\tint result;\n \n \targc = parse_options(argc, argv, prefix, options, builtin_bundle_usage,\n-\t\tPARSE_OPT_STOP_AT_NON_OPTION);\n+\t\t\t     0);\n \n \tpacket_trace_identity(\"bundle\");\n \n-\tif (argc < 2)\n-\t\tusage_with_options(builtin_bundle_usage, options);\n-\n-\telse if (!strcmp(argv[0], \"create\"))\n-\t\tresult = cmd_bundle_create(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"verify\"))\n-\t\tresult = cmd_bundle_verify(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"list-heads\"))\n-\t\tresult = cmd_bundle_list_heads(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"unbundle\"))\n-\t\tresult = cmd_bundle_unbundle(argc, argv, prefix);\n-\telse {\n-\t\terror(_(\"Unknown subcommand: %s\"), argv[0]);\n-\t\tusage_with_options(builtin_bundle_usage, options);\n-\t}\n-\treturn result ? 1 : 0;\n+\treturn !!fn(argc, argv, prefix);\n }\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459872","messageId":"20220725123857.2773963-10-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 09/20] parse-options: add support for parsing subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:46Z","receivedAt":"2022-07-25T12:39:44Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Several Git commands have subcommands to implement mutually exclusive\n\"operation modes\", and they usually parse their subcommand argument\nwith a bunch of if-else if statements.\n\nTeach parse-options to handle subcommands as well, which will result\nin shorter and simpler code with consistent error handling and error\nmessages on unknown or missing subcommand, and it will also make\npossible for our Bash completion script to handle subcommands\nprogrammatically.\n\nThe approach is guided by the following observations:\n\n  - Most subcommands [1] are implemented in dedicated functions, and\n    most of those functions [2] either have a signature matching the\n    'int cmd_foo(int argc, const char **argc, const char *prefix)'\n    signature of builtin commands or can be trivially converted to\n    that signature, because they miss only that last prefix parameter\n    or have no parameters at all.\n\n  - Subcommand arguments only have long form, and they have no double\n    dash prefix, no negated form, and no description, and they don't\n    take any arguments, and can't be abbreviated.\n\n  - There must be exactly one subcommand among the arguments, or zero\n    if the command has a default operation mode.\n\n  - All arguments following the subcommand are considered to be\n    arguments of the subcommand, and, conversely, arguments meant for\n    the subcommand may not preceed the subcommand.\n\nSo in the end subcommand declaration and parsing would look something\nlike this:\n\n    parse_opt_subcommand_fn *fn = NULL;\n    struct option builtin_commit_graph_options[] = {\n        OPT_STRING(0, \"object-dir\", &opts.obj_dir, N_(\"dir\"),\n                   N_(\"the object directory to store the graph\")),\n        OPT_SUBCOMMAND(\"verify\", &fn, graph_verify),\n        OPT_SUBCOMMAND(\"write\", &fn, graph_write),\n        OPT_END(),\n    };\n    argc = parse_options(argc, argv, prefix, options,\n                         builtin_commit_graph_usage, 0);\n    return fn(argc, argv, prefix);\n\nHere each OPT_SUBCOMMAND specifies the name of the subcommand and the\nfunction implementing it, and the same pointer to 'fn' where\nparse_options() will store the function associated with the given\nsubcommand.  There is no need to check whether 'fn' is non-NULL before\ninvoking it: if there were no subcommands given on the command line\nthen the parse_options() call would error out and show usage.  In case\na command has a default operation mode, 'fn' should be initialized to\nthe function implementing that mode, and parse_options() should be\ninvoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.\n\nSome thoughts about the implementation:\n\n  - Arguably it is a bit weird that the same pointer to 'fn' have to\n    be specified as 'value' for each OPT_SUBCOMMAND, but we need a way\n    to tell parse_options() where to put the function associated with\n    the given subcommand, and I didn't like the alternatives:\n\n      - Change parse_options()'s signature by adding a pointer to\n        subcommand function to be set to the function associated with\n        the given subcommand, affecting all callsites, even those that\n        don't have subcommands.\n\n      - Introduce a specific parse_options_and_subcommand() variant\n        with that extra funcion parameter.\n\n  - I decided against automatically calling the subcommand function\n    from within parse_options(), because:\n\n      - There are commands that have to perform additional actions\n        after option parsing but before calling the function\n        implementing the specified subcommand.\n\n      - The return code of the subcommand is usually the return code\n        of the git command, but preserving the return code of the\n        automatically called subcommand function would have made the\n        API awkward.\n\n  - Also add a OPT_SUBCOMMAND_F() variant to allow specifying an\n    option flag: we have two subcommands that are purposefully\n    excluded from completion ('git remote rm' and 'git stash save'),\n    so they'll have to be specified with the PARSE_OPT_NOCOMPLETE\n    flag.\n\n  - Some of the 'parse_opt_flags' don't make sense with subcommands,\n    and using them is probably just an oversight or misunderstanding.\n    Therefore parse_options() will BUG() when invoked with any of the\n    following flags while the options array contains at least one\n    OPT_SUBCOMMAND:\n\n      - PARSE_OPT_KEEP_DASHDASH: parse_options() stops parsing\n        arguments when encountering a \"--\" argument, so it doesn't\n        make sense to expect and keep one before a subcommand, because\n        it would prevent the parsing of the subcommand.\n\n        However, this flag is allowed in combination with the\n        PARSE_OPT_SUBCOMMAND_OPTIONAL flag, because the double dash\n        might be meaningful for the command's default operation mode,\n        e.g. to disambiguate refs and pathspecs.\n\n      - PARSE_OPT_STOP_AT_NON_OPTION: As its name suggests, this flag\n        tells parse_options() to stop as soon as it encouners a\n        non-option argument, but subcommands are by definition not\n        options...  so how could they be parsed, then?!\n\n      - PARSE_OPT_KEEP_UNKNOWN: This flag can be used to collect any\n        unknown --options and then pass them to a different command or\n        subsystem.  Surely if a command has subcommands, then this\n        functionality should rather be delegated to one of those\n        subcommands, and not performed by the command itself.\n\n        However, this flag is allowed in combination with the\n        PARSE_OPT_SUBCOMMAND_OPTIONAL flag, making possible to pass\n        --options to the default operation mode.\n\n  - If the command with subcommands has a default operation mode, then\n    all arguments to the command must preceed the arguments of the\n    subcommand.\n\n    AFAICT we don't have any commands where this makes a difference,\n    because in those commands either only the command accepts any\n    arguments ('notes' and 'remote'), or only the default subcommand\n    ('reflog' and 'stash'), but never both.\n\n  - The 'argv' array passed to subcommand functions currently starts\n    with the name of the subcommand.  Keep this behavior.  AFAICT no\n    subcommand functions depend on the actual content of 'argv[0]',\n    but the parse_options() call handling their options expects that\n    the options start at argv[1].\n\n  - To support handling subcommands programmatically in our Bash\n    completion script, 'git cmd --git-completion-helper' will now list\n    both subcommands and regular --options, if any.  This means that\n    the completion script will have to separate subcommands (i.e.\n    words without a double dash prefix) from --options on its own, but\n    that's rather easy to do, and it's not much work either, because\n    the number of subcommands a command might have is rather low, and\n    those commands accept only a single --option or none at all.  An\n    alternative would be to introduce a separate option that lists\n    only subcommands, but then the completion script would need not\n    one but two git invocations and command substitutions for commands\n    with subcommands.\n\n    Note that this change doesn't affect the behavior of our Bash\n    completion script, because when completing the --option of a\n    command with subcommands, e.g. for 'git notes --<TAB>', then all\n    subcommands will be filtered out anyway, as none of them will\n    match the word to be completed starting with that double dash\n    prefix.\n\n[1] Except 'git rerere', because many of its subcommands are\n    implemented in the bodies of the if-else if statements parsing the\n    command's subcommand argument.\n\n[2] Except 'credential', 'credential-store' and 'fsmonitor--daemon',\n    because some of the functions implementing their subcommands take\n    special parameters.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n Documentation/technical/api-parse-options.txt |  41 +++-\n builtin/blame.c                               |   1 +\n builtin/shortlog.c                            |   1 +\n parse-options.c                               | 113 ++++++++++-\n parse-options.h                               |  22 ++-\n t/helper/test-parse-options.c                 |  68 +++++++\n t/helper/test-tool.c                          |   1 +\n t/helper/test-tool.h                          |   1 +\n t/t0040-parse-options.sh                      | 185 ++++++++++++++++++\n 9 files changed, 424 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 4412377fa3..845111d6a8 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -8,7 +8,8 @@ Basics\n ------\n \n The argument vector `argv[]` may usually contain mandatory or optional\n-'non-option arguments', e.g. a filename or a branch, and 'options'.\n+'non-option arguments', e.g. a filename or a branch, 'options', and\n+'subcommands'.\n Options are optional arguments that start with a dash and\n that allow to change the behavior of a command.\n \n@@ -48,6 +49,33 @@ The parse-options API allows:\n   option, e.g. `-a -b --option -- --this-is-a-file` indicates that\n   `--this-is-a-file` must not be processed as an option.\n \n+Subcommands are special in a couple of ways:\n+\n+* Subcommands only have long form, and they have no double dash prefix, no\n+  negated form, and no description, and they don't take any arguments, and\n+  can't be abbreviated.\n+\n+* There must be exactly one subcommand among the arguments, or zero if the\n+  command has a default operation mode.\n+\n+* All arguments following the subcommand are considered to be arguments of\n+  the subcommand, and, conversely, arguments meant for the subcommand may\n+  not preceed the subcommand.\n+\n+Therefore, if the options array contains at least one subcommand and\n+`parse_options()` encounters the first dashless argument, it will either:\n+\n+* stop and return, if that dashless argument is a known subcommand, setting\n+  `value` to the function pointer associated with that subcommand, storing\n+  the name of the subcommand in argv[0], and leaving the rest of the\n+  arguments unprocessed, or\n+\n+* stop and return, if it was invoked with the `PARSE_OPT_SUBCOMMAND_OPTIONAL`\n+  flag and that dashless argument doesn't match any subcommands, leaving\n+  `value` unchanged and the rest of the arguments unprocessed, or\n+\n+* show error and usage, and abort.\n+\n Steps to parse options\n ----------------------\n \n@@ -110,6 +138,13 @@ Flags are the bitwise-or of:\n \tturns it off and allows one to add custom handlers for these\n \toptions, or to just leave them unknown.\n \n+`PARSE_OPT_SUBCOMMAND_OPTIONAL`::\n+\tDon't error out when no subcommand is specified.\n+\n+Note that `PARSE_OPT_STOP_AT_NON_OPTION` is incompatible with subcommands;\n+while `PARSE_OPT_KEEP_DASHDASH` and `PARSE_OPT_KEEP_UNKNOWN` can only be\n+used with subcommands when combined with `PARSE_OPT_SUBCOMMAND_OPTIONAL`.\n+\n Data Structure\n --------------\n \n@@ -241,7 +276,11 @@ There are some macros to easily define options:\n \tcan be given by the user. `int_var` is set to `enum_val` when the\n \toption is used, but an error is reported if other \"operating mode\"\n \toption has already set its value to the same `int_var`.\n+\tIn new commands consider using subcommands instead.\n \n+`OPT_SUBCOMMAND(long, &fn_ptr, subcommand_fn)`::\n+\tDefine a subcommand.  `subcommand_fn` is put into `fn_ptr` when\n+\tthis subcommand is used.\n \n The last element of the array must be `OPT_END()`.\n \ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 02e39420b6..a9fe8cf7a6 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n \t\tcase PARSE_OPT_ERROR:\n+\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\texit(129);\n \t\tcase PARSE_OPT_COMPLETE:\n \t\t\texit(0);\ndiff --git a/builtin/shortlog.c b/builtin/shortlog.c\nindex 086dfee45a..7a1e1fe7c0 100644\n--- a/builtin/shortlog.c\n+++ b/builtin/shortlog.c\n@@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n \t\tcase PARSE_OPT_ERROR:\n+\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\texit(129);\n \t\tcase PARSE_OPT_COMPLETE:\n \t\t\texit(0);\ndiff --git a/parse-options.c b/parse-options.c\nindex 8748f88e6f..7c4c805d51 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -324,6 +324,8 @@ static enum parse_opt_result parse_long_opt(\n \t\tconst char *rest, *long_name = options->long_name;\n \t\tenum opt_parsed flags = OPT_LONG, opt_flags = OPT_LONG;\n \n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\tcontinue;\n \t\tif (!long_name)\n \t\t\tcontinue;\n \n@@ -419,6 +421,19 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,\n \treturn PARSE_OPT_ERROR;\n }\n \n+static enum parse_opt_result parse_subcommand(const char *arg,\n+\t\t\t\t\t      const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++)\n+\t\tif (options->type == OPTION_SUBCOMMAND &&\n+\t\t    !strcmp(options->long_name, arg)) {\n+\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n+\t\t\treturn PARSE_OPT_SUBCOMMAND;\n+\t\t}\n+\n+\treturn PARSE_OPT_UNKNOWN;\n+}\n+\n static void check_typos(const char *arg, const struct option *options)\n {\n \tif (strlen(arg) < 3)\n@@ -442,6 +457,7 @@ static void check_typos(const char *arg, const struct option *options)\n static void parse_options_check(const struct option *opts)\n {\n \tchar short_opts[128];\n+\tvoid *subcommand_value = NULL;\n \n \tmemset(short_opts, '\\0', sizeof(short_opts));\n \tfor (; opts->type != OPTION_END; opts++) {\n@@ -489,6 +505,14 @@ static void parse_options_check(const struct option *opts)\n \t\t\t       \"Are you using parse_options_step() directly?\\n\"\n \t\t\t       \"That case is not supported yet.\");\n \t\t\tbreak;\n+\t\tcase OPTION_SUBCOMMAND:\n+\t\t\tif (!opts->value || !opts->subcommand_fn)\n+\t\t\t\toptbug(opts, \"OPTION_SUBCOMMAND needs a value and a subcommand function\");\n+\t\t\tif (!subcommand_value)\n+\t\t\t\tsubcommand_value = opts->value;\n+\t\t\telse if (subcommand_value != opts->value)\n+\t\t\t\toptbug(opts, \"all OPTION_SUBCOMMANDs need the same value\");\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\t; /* ok. (usually accepts an argument) */\n \t\t}\n@@ -499,6 +523,14 @@ static void parse_options_check(const struct option *opts)\n \tBUG_if_bug(\"invalid 'struct option'\");\n }\n \n+static int has_subcommands(const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++)\n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n \t\t\t\t  int argc, const char **argv, const char *prefix,\n \t\t\t\t  const struct option *options,\n@@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n \tctx->prefix = prefix;\n \tctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);\n \tctx->flags = flags;\n+\tctx->has_subcommands = has_subcommands(options);\n+\tif (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL))\n+\t\tBUG(\"Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands\");\n+\tif (ctx->has_subcommands) {\n+\t\tif (flags & PARSE_OPT_STOP_AT_NON_OPTION)\n+\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION\");\n+\t\tif (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n+\t\t\tif (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)\n+\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n+\t\t\tif (flags & PARSE_OPT_KEEP_DASHDASH)\n+\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n+\t\t}\n+\t}\n \tif ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&\n \t    (flags & PARSE_OPT_STOP_AT_NON_OPTION) &&\n \t    !(flags & PARSE_OPT_ONE_SHOT))\n@@ -589,6 +634,7 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \tint nr_noopts = 0;\n \n \tfor (; opts->type != OPTION_END; opts++) {\n+\t\tconst char *prefix = \"--\";\n \t\tconst char *suffix = \"\";\n \n \t\tif (!opts->long_name)\n@@ -598,6 +644,9 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \t\t\tcontinue;\n \n \t\tswitch (opts->type) {\n+\t\tcase OPTION_SUBCOMMAND:\n+\t\t\tprefix = \"\";\n+\t\t\tbreak;\n \t\tcase OPTION_GROUP:\n \t\t\tcontinue;\n \t\tcase OPTION_STRING:\n@@ -620,8 +669,8 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \t\t\tsuffix = \"=\";\n \t\tif (starts_with(opts->long_name, \"no-\"))\n \t\t\tnr_noopts++;\n-\t\tprintf(\"%s--%s%s\", opts == original_opts ? \"\" : \" \",\n-\t\t       opts->long_name, suffix);\n+\t\tprintf(\"%s%s%s%s\", opts == original_opts ? \"\" : \" \",\n+\t\t       prefix, opts->long_name, suffix);\n \t}\n \tshow_negated_gitcomp(original_opts, show_all, -1);\n \tshow_negated_gitcomp(original_opts, show_all, nr_noopts);\n@@ -744,10 +793,37 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tif (*arg != '-' || !arg[1]) {\n \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n \t\t\t\tcontinue;\n-\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n-\t\t\t\treturn PARSE_OPT_NON_OPTION;\n-\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n-\t\t\tcontinue;\n+\t\t\tif (!ctx->has_subcommands) {\n+\t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n+\t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n+\t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tswitch (parse_subcommand(arg, options)) {\n+\t\t\tcase PARSE_OPT_SUBCOMMAND:\n+\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n+\t\t\tcase PARSE_OPT_UNKNOWN:\n+\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\t\t\t\t/*\n+\t\t\t\t\t * arg is neither a short or long\n+\t\t\t\t\t * option nor a subcommand.  Since\n+\t\t\t\t\t * this command has a default\n+\t\t\t\t\t * operation mode, we have to treat\n+\t\t\t\t\t * this arg and all remaining args\n+\t\t\t\t\t * as args meant to that default\n+\t\t\t\t\t * operation mode.\n+\t\t\t\t\t * So we are done parsing.\n+\t\t\t\t\t */\n+\t\t\t\t\treturn PARSE_OPT_DONE;\n+\t\t\t\terror(_(\"unknown subcommand: %s\"), arg);\n+\t\t\t\tusage_with_options(usagestr, options);\n+\t\t\tcase PARSE_OPT_COMPLETE:\n+\t\t\tcase PARSE_OPT_HELP:\n+\t\t\tcase PARSE_OPT_ERROR:\n+\t\t\tcase PARSE_OPT_DONE:\n+\t\t\tcase PARSE_OPT_NON_OPTION:\n+\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n+\t\t\t}\n \t\t}\n \n \t\t/* lone -h asks for help */\n@@ -775,6 +851,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\tgoto show_usage;\n \t\t\t\tgoto unknown;\n \t\t\tcase PARSE_OPT_NON_OPTION:\n+\t\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\tcase PARSE_OPT_HELP:\n \t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\t\tBUG(\"parse_short_opt() cannot return these\");\n@@ -800,6 +877,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t*(char *)ctx->argv[0] = '-';\n \t\t\t\t\tgoto unknown;\n \t\t\t\tcase PARSE_OPT_NON_OPTION:\n+\t\t\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\t\tcase PARSE_OPT_HELP:\n \t\t\t\t\tBUG(\"parse_short_opt() cannot return these\");\n@@ -831,6 +909,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tcase PARSE_OPT_HELP:\n \t\t\tgoto show_usage;\n \t\tcase PARSE_OPT_NON_OPTION:\n+\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\tcase PARSE_OPT_COMPLETE:\n \t\t\tBUG(\"parse_long_opt() cannot return these\");\n \t\tcase PARSE_OPT_DONE:\n@@ -840,8 +919,21 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n unknown:\n \t\tif (ctx->flags & PARSE_OPT_ONE_SHOT)\n \t\t\tbreak;\n+\t\tif (ctx->has_subcommands &&\n+\t\t    (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) &&\n+\t\t    (ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT)) {\n+\t\t\t/*\n+\t\t\t * Found an unknown option given to a command with\n+\t\t\t * subcommands that has a default operation mode:\n+\t\t\t * we treat this option and all remaining args as\n+\t\t\t * arguments meant to that default operation mode.\n+\t\t\t * So we are done parsing.\n+\t\t\t */\n+\t\t\treturn PARSE_OPT_DONE;\n+\t\t}\n \t\tif (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT))\n \t\t\treturn PARSE_OPT_UNKNOWN;\n+\t\tctx->kept_unknown = 1;\n \t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n \t\tctx->opt = NULL;\n \t}\n@@ -885,7 +977,14 @@ int parse_options(int argc, const char **argv,\n \tcase PARSE_OPT_COMPLETE:\n \t\texit(0);\n \tcase PARSE_OPT_NON_OPTION:\n+\tcase PARSE_OPT_SUBCOMMAND:\n+\t\tbreak;\n \tcase PARSE_OPT_DONE:\n+\t\tif (ctx.has_subcommands &&\n+\t\t    !(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n+\t\t\terror(_(\"need a subcommand\"));\n+\t\t\tusage_with_options(usagestr, options);\n+\t\t}\n \t\tbreak;\n \tcase PARSE_OPT_UNKNOWN:\n \t\tif (ctx.argv[0][1] == '-') {\n@@ -1010,6 +1109,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \t\tsize_t pos;\n \t\tint pad;\n \n+\t\tif (opts->type == OPTION_SUBCOMMAND)\n+\t\t\tcontinue;\n \t\tif (opts->type == OPTION_GROUP) {\n \t\t\tfputc('\\n', outfile);\n \t\t\tneed_newline = 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex 8cbfc7e8bf..b089b6fa03 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -11,6 +11,7 @@ enum parse_opt_type {\n \tOPTION_GROUP,\n \tOPTION_NUMBER,\n \tOPTION_ALIAS,\n+\tOPTION_SUBCOMMAND,\n \t/* options with no arguments */\n \tOPTION_BIT,\n \tOPTION_NEGBIT,\n@@ -34,6 +35,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_NO_INTERNAL_HELP = 1 << 4,\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\n+\tPARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,\n };\n \n enum parse_opt_option_flags {\n@@ -56,6 +58,7 @@ enum parse_opt_result {\n \tPARSE_OPT_ERROR = -1,\t/* must be the same as error() */\n \tPARSE_OPT_DONE = 0,\t/* fixed so that \"return 0\" works */\n \tPARSE_OPT_NON_OPTION,\n+\tPARSE_OPT_SUBCOMMAND,\n \tPARSE_OPT_UNKNOWN\n };\n \n@@ -67,6 +70,9 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t      const struct option *opt,\n \t\t\t\t\t      const char *arg, int unset);\n \n+typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n+\t\t\t\t    const char *prefix);\n+\n /*\n  * `type`::\n  *   holds the type of the option, you must have an OPTION_END last in your\n@@ -76,7 +82,8 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n  *   the character to use as a short option name, '\\0' if none.\n  *\n  * `long_name`::\n- *   the long option name, without the leading dashes, NULL if none.\n+ *   the long option (without the leading dashes) or subcommand name,\n+ *   NULL if none.\n  *\n  * `value`::\n  *   stores pointers to the values to be filled.\n@@ -93,7 +100,7 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n  *\n  * `help`::\n  *   the short help associated to what the option does.\n- *   Must never be NULL (except for OPTION_END).\n+ *   Must never be NULL (except for OPTION_END and OPTION_SUBCOMMAND).\n  *   OPTION_GROUP uses this pointer to store the group header.\n  *   Should be wrapped by N_() for translation.\n  *\n@@ -131,6 +138,9 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n  * `ll_callback`::\n  *   pointer to the callback to use for OPTION_LOWLEVEL_CALLBACK\n  *\n+ * `subcommand_fn`::\n+ *   pointer to a function to use for OPTION_SUBCOMMAND.\n+ *   It will be put in value when the subcommand is given on the command line.\n  */\n struct option {\n \tenum parse_opt_type type;\n@@ -145,6 +155,7 @@ struct option {\n \tintptr_t defval;\n \tparse_opt_ll_cb *ll_callback;\n \tintptr_t extra;\n+\tparse_opt_subcommand_fn *subcommand_fn;\n };\n \n #define OPT_BIT_F(s, l, v, h, b, f) { OPTION_BIT, (s), (l), (v), NULL, (h), \\\n@@ -206,6 +217,11 @@ struct option {\n #define OPT_ALIAS(s, l, source_long_name) \\\n \t{ OPTION_ALIAS, (s), (l), (source_long_name) }\n \n+#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n+\t\t\t\t\tNULL, 0, NULL, 0, NULL, 0, (fn) }\n+#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n+\t\t\t\t\tNULL, (f), NULL, 0, NULL, 0, (fn) }\n+\n /*\n  * parse_options() will filter out the processed options and leave the\n  * non-option arguments in argv[]. argv0 is assumed program name and\n@@ -295,6 +311,8 @@ struct parse_opt_ctx_t {\n \tint argc, cpidx, total;\n \tconst char *opt;\n \tenum parse_opt_flags flags;\n+\tunsigned has_subcommands:1,\n+\t\t kept_unknown:1;\n \tconst char *prefix;\n \tconst char **alias_groups; /* must be in groups of 3 elements! */\n \tstruct option *updated_options;\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex a715c772bd..07a4e185ef 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -233,6 +233,9 @@ static const struct option test_flag_options[] = {\n \tOPT_BIT(0, \"no-internal-help\", &test_flags,\n \t\t\"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()\",\n \t\tPARSE_OPT_NO_INTERNAL_HELP),\n+\tOPT_BIT(0, \"subcommand-optional\", &test_flags,\n+\t\t\"pass PARSE_OPT_SUBCOMMAND_OPTIONAL to parse_options()\",\n+\t\tPARSE_OPT_SUBCOMMAND_OPTIONAL),\n \tOPT_END()\n };\n \n@@ -253,3 +256,68 @@ int cmd__parse_options_flags(int argc, const char **argv)\n \n \treturn parse_options_flags__cmd(argc, argv, test_flags);\n }\n+\n+static void print_subcommand_args(const char *fn_name, int argc,\n+\t\t\t\t  const char **argv)\n+{\n+\tint i;\n+\tprintf(\"fn: %s\\n\", fn_name);\n+\tfor (i = 0; i < argc; i++)\n+\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n+}\n+\n+static int subcmd_one(int argc, const char **argv, const char *prefix)\n+{\n+\tprint_subcommand_args(\"subcmd_one\", argc, argv);\n+\treturn 0;\n+}\n+\n+static int subcmd_two(int argc, const char **argv, const char *prefix)\n+{\n+\tprint_subcommand_args(\"subcmd_two\", argc, argv);\n+\treturn 0;\n+}\n+\n+static int parse_subcommand__cmd(int argc, const char **argv,\n+\t\t\t\t enum parse_opt_flags test_flags)\n+{\n+\tconst char *usage[] = {\n+\t\t\"<...> cmd subcmd-one\",\n+\t\t\"<...> cmd subcmd-two\",\n+\t\tNULL\n+\t};\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tint opt = 0;\n+\tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"subcmd-one\", &fn, subcmd_one),\n+\t\tOPT_SUBCOMMAND(\"subcmd-two\", &fn, subcmd_two),\n+\t\tOPT_INTEGER('o', \"opt\", &opt, \"an integer option\"),\n+\t\tOPT_END()\n+\t};\n+\n+\tif (test_flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\tfn = subcmd_one;\n+\targc = parse_options(argc, argv, NULL, options, usage, test_flags);\n+\n+\tprintf(\"opt: %d\\n\", opt);\n+\n+\treturn fn(argc, argv, NULL);\n+}\n+\n+int cmd__parse_subcommand(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool parse-subcommand [flag-options] cmd <subcommand>\",\n+\t\tNULL\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, test_flag_options, usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n+\t\terror(\"'cmd' is mandatory\");\n+\t\tusage_with_options(usage, test_flag_options);\n+\t}\n+\n+\treturn parse_subcommand__cmd(argc, argv, test_flags);\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex 6e62282b60..49b30057f6 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -53,6 +53,7 @@ static struct test_cmd cmds[] = {\n \t{ \"parse-options\", cmd__parse_options },\n \t{ \"parse-options-flags\", cmd__parse_options_flags },\n \t{ \"parse-pathspec-file\", cmd__parse_pathspec_file },\n+\t{ \"parse-subcommand\", cmd__parse_subcommand },\n \t{ \"partial-clone\", cmd__partial_clone },\n \t{ \"path-utils\", cmd__path_utils },\n \t{ \"pcre2-config\", cmd__pcre2_config },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex d8e8403d70..487d84da60 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -43,6 +43,7 @@ int cmd__pack_mtimes(int argc, const char **argv);\n int cmd__parse_options(int argc, const char **argv);\n int cmd__parse_options_flags(int argc, const char **argv);\n int cmd__parse_pathspec_file(int argc, const char** argv);\n+int cmd__parse_subcommand(int argc, const char **argv);\n int cmd__partial_clone(int argc, const char **argv);\n int cmd__path_utils(int argc, const char **argv);\n int cmd__pcre2_config(int argc, const char **argv);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 673a01ca71..a5f9ef363f 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -526,4 +526,189 @@ test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'subcommand - no subcommand shows error and usage' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd 2>err &&\n+\tgrep \"^error: need a subcommand\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - subcommand after -- shows error and usage' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&\n+\tgrep \"^error: need a subcommand\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&\n+\tgrep \"^error: need a subcommand\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&\n+\tgrep \"^error: unknown subcommand: nope\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n+\tgrep \"^error: unknown subcommand: subcmd-o$\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - no negated subcommands' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&\n+\tgrep \"^error: unknown subcommand: no-subcmd-one\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - simple' '\n+\ttest-tool parse-subcommand cmd subcmd-two >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_two\n+\targ 00: subcmd-two\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - stop parsing at the first subcommand' '\n+\ttest-tool parse-subcommand cmd --opt=1 subcmd-two subcmd-one --opt=2 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 1\n+\tfn: subcmd_two\n+\targ 00: subcmd-two\n+\targ 01: subcmd-one\n+\targ 02: --opt=2\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - KEEP_ARGV0' '\n+\ttest-tool parse-subcommand --keep-argv0 cmd subcmd-two >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_two\n+\targ 00: cmd\n+\targ 01: subcmd-two\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given' '\n+\ttest-tool parse-subcommand --subcommand-optional cmd >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + given subcommand' '\n+\ttest-tool parse-subcommand --subcommand-optional cmd subcmd-two branch file >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_two\n+\targ 00: subcmd-two\n+\targ 01: branch\n+\targ 02: file\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown dashless args' '\n+\ttest-tool parse-subcommand --subcommand-optional cmd branch file >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: branch\n+\targ 01: file\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown option' '\n+\ttest_expect_code 129 test-tool parse-subcommand --subcommand-optional cmd --subcommand-opt 2>err &&\n+\tgrep \"^error: unknown option\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand not given + unknown option' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: --subcommand-opt\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand ignored after unknown option' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt subcmd-two >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: --subcommand-opt\n+\targ 01: subcmd-two\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + command and subcommand options cannot be mixed' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt branch --opt=1 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: --subcommand-opt\n+\targ 01: branch\n+\targ 02: --opt=1\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT | KEEP_ARGV0' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt --keep-argv0 cmd --subcommand-opt branch >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: cmd\n+\targ 01: --subcommand-opt\n+\targ 02: branch\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT | KEEP_DASHDASH' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt --keep-dashdash cmd -- --subcommand-opt file >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: --\n+\targ 01: --subcommand-opt\n+\targ 02: file\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - completion helper' '\n+\ttest-tool parse-subcommand cmd --git-completion-helper >actual &&\n+\techo \"subcmd-one subcmd-two --opt= --no-opt\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommands are incompatible with STOP_AT_NON_OPTION' '\n+\ttest_must_fail test-tool parse-subcommand --stop-at-non-option cmd subcmd-one 2>err &&\n+\tgrep ^BUG err\n+'\n+\n+test_expect_success 'subcommands are incompatible with KEEP_UNKNOWN_OPT unless in combination with SUBCOMMAND_OPTIONAL' '\n+\ttest_must_fail test-tool parse-subcommand --keep-unknown-opt cmd subcmd-two 2>err &&\n+\tgrep ^BUG err\n+'\n+\n+test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in combination with SUBCOMMAND_OPTIONAL' '\n+\ttest_must_fail test-tool parse-subcommand --keep-dashdash cmd subcmd-two 2>err &&\n+\tgrep ^BUG err\n+'\n+\n test_done\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459873","messageId":"20220725123857.2773963-12-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 11/20] builtin/commit-graph.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:48Z","receivedAt":"2022-07-25T12:39:47Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git commit-graph' parses its subcommands with an if-else if\nstatement.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nNote that the functions implementing each subcommand only accept the\n'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter\nto make them match the type expected by parse-options, and thus avoid\ncasting function pointers.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/commit-graph.c  | 30 +++++++++++++-----------------\n t/t5318-commit-graph.sh |  4 ++--\n 2 files changed, 15 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/commit-graph.c b/builtin/commit-graph.c\nindex 51c4040ea6..1eb5492cbd 100644\n--- a/builtin/commit-graph.c\n+++ b/builtin/commit-graph.c\n@@ -58,7 +58,7 @@ static struct option *add_common_options(struct option *to)\n \treturn parse_options_concat(common_opts, to);\n }\n \n-static int graph_verify(int argc, const char **argv)\n+static int graph_verify(int argc, const char **argv, const char *prefix)\n {\n \tstruct commit_graph *graph = NULL;\n \tstruct object_directory *odb = NULL;\n@@ -190,7 +190,7 @@ static int git_commit_graph_write_config(const char *var, const char *value,\n \treturn 0;\n }\n \n-static int graph_write(int argc, const char **argv)\n+static int graph_write(int argc, const char **argv, const char *prefix)\n {\n \tstruct string_list pack_indexes = STRING_LIST_INIT_DUP;\n \tstruct strbuf buf = STRBUF_INIT;\n@@ -307,26 +307,22 @@ static int graph_write(int argc, const char **argv)\n \n int cmd_commit_graph(int argc, const char **argv, const char *prefix)\n {\n-\tstruct option *builtin_commit_graph_options = common_opts;\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option builtin_commit_graph_options[] = {\n+\t\tOPT_SUBCOMMAND(\"verify\", &fn, graph_verify),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, graph_write),\n+\t\tOPT_END(),\n+\t};\n+\tstruct option *options = parse_options_concat(builtin_commit_graph_options, common_opts);\n \n \tgit_config(git_default_config, NULL);\n-\targc = parse_options(argc, argv, prefix,\n-\t\t\t     builtin_commit_graph_options,\n-\t\t\t     builtin_commit_graph_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n-\tif (!argc)\n-\t\tgoto usage;\n \n \tread_replace_refs = 0;\n \tsave_commit_buffer = 0;\n \n-\tif (!strcmp(argv[0], \"verify\"))\n-\t\treturn graph_verify(argc, argv);\n-\telse if (argc && !strcmp(argv[0], \"write\"))\n-\t\treturn graph_write(argc, argv);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_commit_graph_usage, 0);\n+\tFREE_AND_NULL(options);\n \n-\terror(_(\"unrecognized subcommand: %s\"), argv[0]);\n-usage:\n-\tusage_with_options(builtin_commit_graph_usage,\n-\t\t\t   builtin_commit_graph_options);\n+\treturn fn(argc, argv, prefix);\n }\ndiff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh\nindex be0b5641ff..e85b36e825 100755\n--- a/t/t5318-commit-graph.sh\n+++ b/t/t5318-commit-graph.sh\n@@ -12,12 +12,12 @@ test_expect_success 'usage' '\n \n test_expect_success 'usage shown without sub-command' '\n \ttest_expect_code 129 git commit-graph 2>err &&\n-\t! grep error: err\n+\tgrep usage: err\n '\n \n test_expect_success 'usage shown with an error on unknown sub-command' '\n \tcat >expect <<-\\EOF &&\n-\terror: unrecognized subcommand: unknown\n+\terror: unknown subcommand: unknown\n \tEOF\n \ttest_expect_code 129 git commit-graph unknown 2>stderr &&\n \tgrep error stderr >actual &&\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459874","messageId":"20220725123857.2773963-13-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:49Z","receivedAt":"2022-07-25T12:39:47Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git maintenanze' parses its subcommands with a couple of if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nThis change makes 'git maintenance' consistent with other commands in\nthat the help text shown for '-h' goes to standard output, not error,\nin the exit code and error message on unknown subcommand, and the\nerror message on missing subcommand.  There is a test checking these,\nwhich is now updated accordingly.\n\nNote that some of the functions implementing each subcommand don't\naccept any parameters, so add the (unused) 'argc', '**argv' and\n'*prefix' parameters to make them match the type expected by\nparse-options, and thus avoid casting function pointers.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/gc.c           | 42 +++++++++++++++++++++---------------------\n git.c                  |  2 +-\n t/t7900-maintenance.sh | 10 ++++++----\n 3 files changed, 28 insertions(+), 26 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex eeff2b760e..19d6b3b558 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1465,7 +1465,7 @@ static char *get_maintpath(void)\n \treturn strbuf_detach(&sb, NULL);\n }\n \n-static int maintenance_register(void)\n+static int maintenance_register(int argc, const char **argv, const char *prefix)\n {\n \tint rc;\n \tchar *config_value;\n@@ -1509,7 +1509,7 @@ static int maintenance_register(void)\n \treturn rc;\n }\n \n-static int maintenance_unregister(void)\n+static int maintenance_unregister(int argc, const char **argv, const char *prefix)\n {\n \tint rc;\n \tstruct child_process config_unset = CHILD_PROCESS_INIT;\n@@ -2505,34 +2505,34 @@ static int maintenance_start(int argc, const char **argv, const char *prefix)\n \topts.scheduler = resolve_scheduler(opts.scheduler);\n \tvalidate_scheduler(opts.scheduler);\n \n-\tif (maintenance_register())\n+\tif (maintenance_register(0, NULL, NULL)) /* It doesn't take any args */\n \t\twarning(_(\"failed to add repo to global config\"));\n \treturn update_background_schedule(&opts, 1);\n }\n \n-static int maintenance_stop(void)\n+static int maintenance_stop(int argc, const char **argv, const char *prefix)\n {\n \treturn update_background_schedule(NULL, 0);\n }\n \n-static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n+static const char * const builtin_maintenance_usage[] = {\n+\tN_(\"git maintenance <subcommand> [<options>]\"),\n+\tNULL,\n+};\n \n int cmd_maintenance(int argc, const char **argv, const char *prefix)\n {\n-\tif (argc < 2 ||\n-\t    (argc == 2 && !strcmp(argv[1], \"-h\")))\n-\t\tusage(builtin_maintenance_usage);\n-\n-\tif (!strcmp(argv[1], \"run\"))\n-\t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n-\tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n-\tif (!strcmp(argv[1], \"stop\"))\n-\t\treturn maintenance_stop();\n-\tif (!strcmp(argv[1], \"register\"))\n-\t\treturn maintenance_register();\n-\tif (!strcmp(argv[1], \"unregister\"))\n-\t\treturn maintenance_unregister();\n-\n-\tdie(_(\"invalid subcommand: %s\"), argv[1]);\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option builtin_maintenance_options[] = {\n+\t\tOPT_SUBCOMMAND(\"run\", &fn, maintenance_run),\n+\t\tOPT_SUBCOMMAND(\"start\", &fn, maintenance_start),\n+\t\tOPT_SUBCOMMAND(\"stop\", &fn, maintenance_stop),\n+\t\tOPT_SUBCOMMAND(\"register\", &fn, maintenance_register),\n+\t\tOPT_SUBCOMMAND(\"unregister\", &fn, maintenance_unregister),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, builtin_maintenance_options,\n+\t\t\t     builtin_maintenance_usage, 0);\n+\treturn fn(argc, argv, prefix);\n }\ndiff --git a/git.c b/git.c\nindex c4282f194a..f52a955410 100644\n--- a/git.c\n+++ b/git.c\n@@ -555,7 +555,7 @@ static struct cmd_struct commands[] = {\n \t{ \"ls-tree\", cmd_ls_tree, RUN_SETUP },\n \t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY },\n \t{ \"mailsplit\", cmd_mailsplit, NO_PARSEOPT },\n-\t{ \"maintenance\", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"maintenance\", cmd_maintenance, RUN_SETUP },\n \t{ \"merge\", cmd_merge, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"merge-base\", cmd_merge_base, RUN_SETUP },\n \t{ \"merge-file\", cmd_merge_file, RUN_SETUP_GENTLY },\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 74aa638475..1030a36c8d 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -32,11 +32,13 @@ test_systemd_analyze_verify () {\n }\n \n test_expect_success 'help text' '\n-\ttest_expect_code 129 git maintenance -h 2>err &&\n-\ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n-\ttest_expect_code 128 git maintenance barf 2>err &&\n-\ttest_i18ngrep \"invalid subcommand: barf\" err &&\n+\ttest_expect_code 129 git maintenance -h >actual &&\n+\ttest_i18ngrep \"usage: git maintenance <subcommand>\" actual &&\n+\ttest_expect_code 129 git maintenance barf 2>err &&\n+\ttest_i18ngrep \"unknown subcommand: barf\" err &&\n+\ttest_i18ngrep \"usage: git maintenance\" err &&\n \ttest_expect_code 129 git maintenance 2>err &&\n+\ttest_i18ngrep \"error: need a subcommand\" err &&\n \ttest_i18ngrep \"usage: git maintenance\" err\n '\n \n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459875","messageId":"20220725123857.2773963-14-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 13/20] builtin/hook.c: let parse-option parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:50Z","receivedAt":"2022-07-25T12:39:49Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git hook' parses its currently only subcommand with an if statement.\nparse-options has just learned to parse subcommands, so let's use that\nfacility instead, with the benefits of shorter code, handling missing\nor unknown subcommands, and listing subcommands for Bash completion.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/hook.c | 12 ++++--------\n 1 file changed, 4 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 54e5c6ec93..b6530d189a 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -67,18 +67,14 @@ static int run(int argc, const char **argv, const char *prefix)\n \n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option builtin_hook_options[] = {\n+\t\tOPT_SUBCOMMAND(\"run\", &fn, run),\n \t\tOPT_END(),\n \t};\n \n \targc = parse_options(argc, argv, NULL, builtin_hook_options,\n-\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n-\tif (!argc)\n-\t\tgoto usage;\n+\t\t\t     builtin_hook_usage, 0);\n \n-\tif (!strcmp(argv[0], \"run\"))\n-\t\treturn run(argc, argv, prefix);\n-\n-usage:\n-\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+\treturn fn(argc, argv, prefix);\n }\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459876","messageId":"20220725123857.2773963-16-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 15/20] builtin/notes.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:52Z","receivedAt":"2022-07-25T12:39:52Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git notes' parses its subcommands with a long list of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling unknown subcommands, and listing subcommands for Bash\ncompletion.  Make sure that the default operation mode doesn't accept\nany arguments.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/notes.c | 43 +++++++++++++++++--------------------------\n 1 file changed, 17 insertions(+), 26 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex a3d0d15a22..42cbae4659 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -994,17 +994,31 @@ static int get_ref(int argc, const char **argv, const char *prefix)\n \n int cmd_notes(int argc, const char **argv, const char *prefix)\n {\n-\tint result;\n \tconst char *override_notes_ref = NULL;\n+\tparse_opt_subcommand_fn *fn = list;\n \tstruct option options[] = {\n \t\tOPT_STRING(0, \"ref\", &override_notes_ref, N_(\"notes-ref\"),\n \t\t\t   N_(\"use notes from <notes-ref>\")),\n+\t\tOPT_SUBCOMMAND(\"list\", &fn, list),\n+\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n+\t\tOPT_SUBCOMMAND(\"copy\", &fn, copy),\n+\t\tOPT_SUBCOMMAND(\"append\", &fn, append_edit),\n+\t\tOPT_SUBCOMMAND(\"edit\", &fn, append_edit),\n+\t\tOPT_SUBCOMMAND(\"show\", &fn, show),\n+\t\tOPT_SUBCOMMAND(\"merge\", &fn, merge),\n+\t\tOPT_SUBCOMMAND(\"remove\", &fn, remove_cmd),\n+\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n+\t\tOPT_SUBCOMMAND(\"get-ref\", &fn, get_ref),\n \t\tOPT_END()\n \t};\n \n \tgit_config(git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n+\tif (fn == list && argc && strcmp(argv[0], \"list\")) {\n+\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n+\t\tusage_with_options(git_notes_usage, options);\n+\t}\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\n@@ -1014,28 +1028,5 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n \t\tstrbuf_release(&sb);\n \t}\n \n-\tif (argc < 1 || !strcmp(argv[0], \"list\"))\n-\t\tresult = list(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"add\"))\n-\t\tresult = add(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"copy\"))\n-\t\tresult = copy(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"append\") || !strcmp(argv[0], \"edit\"))\n-\t\tresult = append_edit(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"show\"))\n-\t\tresult = show(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"merge\"))\n-\t\tresult = merge(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"remove\"))\n-\t\tresult = remove_cmd(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"prune\"))\n-\t\tresult = prune(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"get-ref\"))\n-\t\tresult = get_ref(argc, argv, prefix);\n-\telse {\n-\t\tresult = error(_(\"unknown subcommand: %s\"), argv[0]);\n-\t\tusage_with_options(git_notes_usage, options);\n-\t}\n-\n-\treturn result ? 1 : 0;\n+\treturn !!fn(argc, argv, prefix);\n }\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459877","messageId":"20220725123857.2773963-15-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 14/20] builtin/multi-pack-index.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:51Z","receivedAt":"2022-07-25T12:39:57Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git multi-pack-index' parses its subcommands with a couple of if-else\nif statements.  parse-options has just learned to parse subcommands,\nso let's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nNote that the functions implementing each subcommand only accept the\n'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter\nto make them match the type expected by parse-options, and thus avoid\ncasting function pointers.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/multi-pack-index.c | 51 ++++++++++++++++----------------------\n 1 file changed, 22 insertions(+), 29 deletions(-)\n\ndiff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c\nindex 8f24d59a75..b8320d597b 100644\n--- a/builtin/multi-pack-index.c\n+++ b/builtin/multi-pack-index.c\n@@ -104,7 +104,8 @@ static void read_packs_from_stdin(struct string_list *to)\n \tstrbuf_release(&buf);\n }\n \n-static int cmd_multi_pack_index_write(int argc, const char **argv)\n+static int cmd_multi_pack_index_write(int argc, const char **argv,\n+\t\t\t\t      const char *prefix)\n {\n \tstruct option *options;\n \tstatic struct option builtin_multi_pack_index_write_options[] = {\n@@ -160,7 +161,8 @@ static int cmd_multi_pack_index_write(int argc, const char **argv)\n \t\t\t       opts.refs_snapshot, opts.flags);\n }\n \n-static int cmd_multi_pack_index_verify(int argc, const char **argv)\n+static int cmd_multi_pack_index_verify(int argc, const char **argv,\n+\t\t\t\t       const char *prefix)\n {\n \tstruct option *options;\n \tstatic struct option builtin_multi_pack_index_verify_options[] = {\n@@ -186,7 +188,8 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv)\n \treturn verify_midx_file(the_repository, opts.object_dir, opts.flags);\n }\n \n-static int cmd_multi_pack_index_expire(int argc, const char **argv)\n+static int cmd_multi_pack_index_expire(int argc, const char **argv,\n+\t\t\t\t       const char *prefix)\n {\n \tstruct option *options;\n \tstatic struct option builtin_multi_pack_index_expire_options[] = {\n@@ -212,7 +215,8 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv)\n \treturn expire_midx_packs(the_repository, opts.object_dir, opts.flags);\n }\n \n-static int cmd_multi_pack_index_repack(int argc, const char **argv)\n+static int cmd_multi_pack_index_repack(int argc, const char **argv,\n+\t\t\t\t       const char *prefix)\n {\n \tstruct option *options;\n \tstatic struct option builtin_multi_pack_index_repack_options[] = {\n@@ -247,7 +251,15 @@ int cmd_multi_pack_index(int argc, const char **argv,\n \t\t\t const char *prefix)\n {\n \tint res;\n-\tstruct option *builtin_multi_pack_index_options = common_opts;\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option builtin_multi_pack_index_options[] = {\n+\t\tOPT_SUBCOMMAND(\"repack\", &fn, cmd_multi_pack_index_repack),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_multi_pack_index_write),\n+\t\tOPT_SUBCOMMAND(\"verify\", &fn, cmd_multi_pack_index_verify),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_multi_pack_index_expire),\n+\t\tOPT_END(),\n+\t};\n+\tstruct option *options = parse_options_concat(builtin_multi_pack_index_options, common_opts);\n \n \tgit_config(git_default_config, NULL);\n \n@@ -256,31 +268,12 @@ int cmd_multi_pack_index(int argc, const char **argv,\n \t    the_repository->objects->odb)\n \t\topts.object_dir = xstrdup(the_repository->objects->odb->path);\n \n-\targc = parse_options(argc, argv, prefix,\n-\t\t\t     builtin_multi_pack_index_options,\n-\t\t\t     builtin_multi_pack_index_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n-\n-\tif (!argc)\n-\t\tgoto usage;\n-\n-\tif (!strcmp(argv[0], \"repack\"))\n-\t\tres = cmd_multi_pack_index_repack(argc, argv);\n-\telse if (!strcmp(argv[0], \"write\"))\n-\t\tres =  cmd_multi_pack_index_write(argc, argv);\n-\telse if (!strcmp(argv[0], \"verify\"))\n-\t\tres =  cmd_multi_pack_index_verify(argc, argv);\n-\telse if (!strcmp(argv[0], \"expire\"))\n-\t\tres =  cmd_multi_pack_index_expire(argc, argv);\n-\telse {\n-\t\terror(_(\"unrecognized subcommand: %s\"), argv[0]);\n-\t\tgoto usage;\n-\t}\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_multi_pack_index_usage, 0);\n+\tFREE_AND_NULL(options);\n+\n+\tres = fn(argc, argv, prefix);\n \n \tfree(opts.object_dir);\n \treturn res;\n-\n-usage:\n-\tusage_with_options(builtin_multi_pack_index_usage,\n-\t\t\t   builtin_multi_pack_index_options);\n }\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459878","messageId":"20220725123857.2773963-17-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 16/20] builtin/reflog.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:53Z","receivedAt":"2022-07-25T12:39:58Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git reflog' parses its subcommands with a couple of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nand listing subcommands for Bash completion.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/reflog.c | 41 +++++++++++------------------------------\n 1 file changed, 11 insertions(+), 30 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex b8b1f4f8ea..d3f6d903fb 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -404,40 +404,21 @@ static int cmd_reflog_exists(int argc, const char **argv, const char *prefix)\n \n int cmd_reflog(int argc, const char **argv, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n+\t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n \t\tOPT_END()\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, reflog_usage,\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n-\t\t\t     PARSE_OPT_NO_INTERNAL_HELP);\n-\n-\t/*\n-\t * With \"git reflog\" we default to showing it. !argc is\n-\t * impossible with PARSE_OPT_KEEP_ARGV0.\n-\t */\n-\tif (argc == 1)\n-\t\tgoto log_reflog;\n-\n-\tif (!strcmp(argv[1], \"-h\"))\n-\t\tusage_with_options(reflog_usage, options);\n-\telse if (*argv[1] == '-')\n-\t\tgoto log_reflog;\n-\n-\tif (!strcmp(argv[1], \"show\"))\n-\t\treturn cmd_reflog_show(argc - 1, argv + 1, prefix);\n-\telse if (!strcmp(argv[1], \"expire\"))\n-\t\treturn cmd_reflog_expire(argc - 1, argv + 1, prefix);\n-\telse if (!strcmp(argv[1], \"delete\"))\n-\t\treturn cmd_reflog_delete(argc - 1, argv + 1, prefix);\n-\telse if (!strcmp(argv[1], \"exists\"))\n-\t\treturn cmd_reflog_exists(argc - 1, argv + 1, prefix);\n-\n-\t/*\n-\t * Fall-through for e.g. \"git reflog -1\", \"git reflog master\",\n-\t * as well as the plain \"git reflog\" above goto above.\n-\t */\n-log_reflog:\n-\treturn cmd_log_reflog(argc, argv, prefix);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\tif (fn)\n+\t\treturn fn(argc - 1, argv + 1, prefix);\n+\telse\n+\t\treturn cmd_log_reflog(argc, argv, prefix);\n }\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459879","messageId":"20220725123857.2773963-18-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 17/20] builtin/remote.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:54Z","receivedAt":"2022-07-25T12:40:15Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git remote' parses its subcommands with a long list of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling unknown subcommands, and listing subcommands for Bash\ncompletion.  Make sure that the default operation mode doesn't accept\nany arguments; and while at it remove the capitalization of the error\nmessage and adjust the test checking it accordingly.\n\nNote that 'git remote' has both 'remove' and 'rm' subcommands, and the\nformer is preferred [1], so hide the latter for completion.\n\nNote also that the functions implementing each subcommand only accept\nthe 'argc' and '**argv' parameters, so add a (unused) '*prefix'\nparameter to make them match the type expected by parse-options, and\nthus avoid casting a bunch of function pointers.\n\n[1] e17dba8fe1 (remote: prefer subcommand name 'remove' to 'rm',\n    2012-09-06)\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/remote.c  | 70 +++++++++++++++++++++--------------------------\n t/t5505-remote.sh |  2 +-\n 2 files changed, 32 insertions(+), 40 deletions(-)\n\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex d9b8746cb3..4a6d47c03a 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -150,7 +150,7 @@ static int parse_mirror_opt(const struct option *opt, const char *arg, int not)\n \treturn 0;\n }\n \n-static int add(int argc, const char **argv)\n+static int add(int argc, const char **argv, const char *prefix)\n {\n \tint fetch = 0, fetch_tags = TAGS_DEFAULT;\n \tunsigned mirror = MIRROR_NONE;\n@@ -680,7 +680,7 @@ static void handle_push_default(const char* old_name, const char* new_name)\n }\n \n \n-static int mv(int argc, const char **argv)\n+static int mv(int argc, const char **argv, const char *prefix)\n {\n \tint show_progress = isatty(2);\n \tstruct option options[] = {\n@@ -844,7 +844,7 @@ static int mv(int argc, const char **argv)\n \treturn 0;\n }\n \n-static int rm(int argc, const char **argv)\n+static int rm(int argc, const char **argv, const char *prefix)\n {\n \tstruct option options[] = {\n \t\tOPT_END()\n@@ -1255,7 +1255,7 @@ static int show_all(void)\n \treturn result;\n }\n \n-static int show(int argc, const char **argv)\n+static int show(int argc, const char **argv, const char *prefix)\n {\n \tint no_query = 0, result = 0, query_flag = 0;\n \tstruct option options[] = {\n@@ -1358,7 +1358,7 @@ static int show(int argc, const char **argv)\n \treturn result;\n }\n \n-static int set_head(int argc, const char **argv)\n+static int set_head(int argc, const char **argv, const char *prefix)\n {\n \tint i, opt_a = 0, opt_d = 0, result = 0;\n \tstruct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;\n@@ -1463,7 +1463,7 @@ static int prune_remote(const char *remote, int dry_run)\n \treturn result;\n }\n \n-static int prune(int argc, const char **argv)\n+static int prune(int argc, const char **argv, const char *prefix)\n {\n \tint dry_run = 0, result = 0;\n \tstruct option options[] = {\n@@ -1492,7 +1492,7 @@ static int get_remote_default(const char *key, const char *value, void *priv)\n \treturn 0;\n }\n \n-static int update(int argc, const char **argv)\n+static int update(int argc, const char **argv, const char *prefix)\n {\n \tint i, prune = -1;\n \tstruct option options[] = {\n@@ -1575,7 +1575,7 @@ static int set_remote_branches(const char *remotename, const char **branches,\n \treturn 0;\n }\n \n-static int set_branches(int argc, const char **argv)\n+static int set_branches(int argc, const char **argv, const char *prefix)\n {\n \tint add_mode = 0;\n \tstruct option options[] = {\n@@ -1594,7 +1594,7 @@ static int set_branches(int argc, const char **argv)\n \treturn set_remote_branches(argv[0], argv + 1, add_mode);\n }\n \n-static int get_url(int argc, const char **argv)\n+static int get_url(int argc, const char **argv, const char *prefix)\n {\n \tint i, push_mode = 0, all_mode = 0;\n \tconst char *remotename = NULL;\n@@ -1647,7 +1647,7 @@ static int get_url(int argc, const char **argv)\n \treturn 0;\n }\n \n-static int set_url(int argc, const char **argv)\n+static int set_url(int argc, const char **argv, const char *prefix)\n {\n \tint i, push_mode = 0, add_mode = 0, delete_mode = 0;\n \tint matches = 0, negative_matches = 0;\n@@ -1739,41 +1739,33 @@ static int set_url(int argc, const char **argv)\n \n int cmd_remote(int argc, const char **argv, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT__VERBOSE(&verbose, N_(\"be verbose; must be placed before a subcommand\")),\n+\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n+\t\tOPT_SUBCOMMAND(\"rename\", &fn, mv),\n+\t\tOPT_SUBCOMMAND_F(\"rm\", &fn, rm, PARSE_OPT_NOCOMPLETE),\n+\t\tOPT_SUBCOMMAND(\"remove\", &fn, rm),\n+\t\tOPT_SUBCOMMAND(\"set-head\", &fn, set_head),\n+\t\tOPT_SUBCOMMAND(\"set-branches\", &fn, set_branches),\n+\t\tOPT_SUBCOMMAND(\"get-url\", &fn, get_url),\n+\t\tOPT_SUBCOMMAND(\"set-url\", &fn, set_url),\n+\t\tOPT_SUBCOMMAND(\"show\", &fn, show),\n+\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n+\t\tOPT_SUBCOMMAND(\"update\", &fn, update),\n \t\tOPT_END()\n \t};\n-\tint result;\n \n \targc = parse_options(argc, argv, prefix, options, builtin_remote_usage,\n-\t\tPARSE_OPT_STOP_AT_NON_OPTION);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n \n-\tif (argc < 1)\n-\t\tresult = show_all();\n-\telse if (!strcmp(argv[0], \"add\"))\n-\t\tresult = add(argc, argv);\n-\telse if (!strcmp(argv[0], \"rename\"))\n-\t\tresult = mv(argc, argv);\n-\telse if (!strcmp(argv[0], \"rm\") || !strcmp(argv[0], \"remove\"))\n-\t\tresult = rm(argc, argv);\n-\telse if (!strcmp(argv[0], \"set-head\"))\n-\t\tresult = set_head(argc, argv);\n-\telse if (!strcmp(argv[0], \"set-branches\"))\n-\t\tresult = set_branches(argc, argv);\n-\telse if (!strcmp(argv[0], \"get-url\"))\n-\t\tresult = get_url(argc, argv);\n-\telse if (!strcmp(argv[0], \"set-url\"))\n-\t\tresult = set_url(argc, argv);\n-\telse if (!strcmp(argv[0], \"show\"))\n-\t\tresult = show(argc, argv);\n-\telse if (!strcmp(argv[0], \"prune\"))\n-\t\tresult = prune(argc, argv);\n-\telse if (!strcmp(argv[0], \"update\"))\n-\t\tresult = update(argc, argv);\n-\telse {\n-\t\terror(_(\"Unknown subcommand: %s\"), argv[0]);\n-\t\tusage_with_options(builtin_remote_usage, options);\n+\tif (fn) {\n+\t\treturn !!fn(argc, argv, prefix);\n+\t} else {\n+\t\tif (argc) {\n+\t\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n+\t\t\tusage_with_options(builtin_remote_usage, options);\n+\t\t}\n+\t\treturn !!show_all();\n \t}\n-\n-\treturn result ? 1 : 0;\n }\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex f075dd4afa..0cdb563994 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -266,7 +266,7 @@ test_expect_success 'without subcommand does not take arguments' '\n \t(\n \t\tcd test &&\n \t\ttest_expect_code 129 git remote origin 2>err &&\n-\t\tgrep \"^error: Unknown subcommand:\" err\n+\t\tgrep \"^error: unknown subcommand:\" err\n \t)\n '\n \n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459880","messageId":"20220725123857.2773963-19-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 18/20] builtin/sparse-checkout.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:55Z","receivedAt":"2022-07-25T12:40:18Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git sparse-checkout' parses its subcommands with a couple of if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nNote that some of the functions implementing each subcommand only\naccept the 'argc' and '**argv' parameters, so add a (unused) '*prefix'\nparameter to make them match the type expected by parse-options, and\nthus avoid casting function pointers.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/sparse-checkout.c | 44 ++++++++++++++-------------------------\n 1 file changed, 16 insertions(+), 28 deletions(-)\n\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex a5e4b95a9d..7b39a150a9 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -48,7 +48,7 @@ static char const * const builtin_sparse_checkout_list_usage[] = {\n \tNULL\n };\n \n-static int sparse_checkout_list(int argc, const char **argv)\n+static int sparse_checkout_list(int argc, const char **argv, const char *prefix)\n {\n \tstatic struct option builtin_sparse_checkout_list_options[] = {\n \t\tOPT_END(),\n@@ -431,7 +431,7 @@ static struct sparse_checkout_init_opts {\n \tint sparse_index;\n } init_opts;\n \n-static int sparse_checkout_init(int argc, const char **argv)\n+static int sparse_checkout_init(int argc, const char **argv, const char *prefix)\n {\n \tstruct pattern_list pl;\n \tchar *sparse_filename;\n@@ -843,7 +843,8 @@ static struct sparse_checkout_reapply_opts {\n \tint sparse_index;\n } reapply_opts;\n \n-static int sparse_checkout_reapply(int argc, const char **argv)\n+static int sparse_checkout_reapply(int argc, const char **argv,\n+\t\t\t\t   const char *prefix)\n {\n \tstatic struct option builtin_sparse_checkout_reapply_options[] = {\n \t\tOPT_BOOL(0, \"cone\", &reapply_opts.cone_mode,\n@@ -876,7 +877,8 @@ static char const * const builtin_sparse_checkout_disable_usage[] = {\n \tNULL\n };\n \n-static int sparse_checkout_disable(int argc, const char **argv)\n+static int sparse_checkout_disable(int argc, const char **argv,\n+\t\t\t\t   const char *prefix)\n {\n \tstatic struct option builtin_sparse_checkout_disable_options[] = {\n \t\tOPT_END(),\n@@ -922,39 +924,25 @@ static int sparse_checkout_disable(int argc, const char **argv)\n \n int cmd_sparse_checkout(int argc, const char **argv, const char *prefix)\n {\n-\tstatic struct option builtin_sparse_checkout_options[] = {\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option builtin_sparse_checkout_options[] = {\n+\t\tOPT_SUBCOMMAND(\"list\", &fn, sparse_checkout_list),\n+\t\tOPT_SUBCOMMAND(\"init\", &fn, sparse_checkout_init),\n+\t\tOPT_SUBCOMMAND(\"set\", &fn, sparse_checkout_set),\n+\t\tOPT_SUBCOMMAND(\"add\", &fn, sparse_checkout_add),\n+\t\tOPT_SUBCOMMAND(\"reapply\", &fn, sparse_checkout_reapply),\n+\t\tOPT_SUBCOMMAND(\"disable\", &fn, sparse_checkout_disable),\n \t\tOPT_END(),\n \t};\n \n-\tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n-\t\tusage_with_options(builtin_sparse_checkout_usage,\n-\t\t\t\t   builtin_sparse_checkout_options);\n-\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_sparse_checkout_options,\n-\t\t\t     builtin_sparse_checkout_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\t\t\t     builtin_sparse_checkout_usage, 0);\n \n \tgit_config(git_default_config, NULL);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.command_requires_full_index = 0;\n \n-\tif (argc > 0) {\n-\t\tif (!strcmp(argv[0], \"list\"))\n-\t\t\treturn sparse_checkout_list(argc, argv);\n-\t\tif (!strcmp(argv[0], \"init\"))\n-\t\t\treturn sparse_checkout_init(argc, argv);\n-\t\tif (!strcmp(argv[0], \"set\"))\n-\t\t\treturn sparse_checkout_set(argc, argv, prefix);\n-\t\tif (!strcmp(argv[0], \"add\"))\n-\t\t\treturn sparse_checkout_add(argc, argv, prefix);\n-\t\tif (!strcmp(argv[0], \"reapply\"))\n-\t\t\treturn sparse_checkout_reapply(argc, argv);\n-\t\tif (!strcmp(argv[0], \"disable\"))\n-\t\t\treturn sparse_checkout_disable(argc, argv);\n-\t}\n-\n-\tusage_with_options(builtin_sparse_checkout_usage,\n-\t\t\t   builtin_sparse_checkout_options);\n+\treturn fn(argc, argv, prefix);\n }\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459881","messageId":"20220725123857.2773963-20-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 19/20] builtin/stash.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:56Z","receivedAt":"2022-07-25T12:40:21Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git stash' parses its subcommands with a long list of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nand listing subcommands for Bash completion.\n\nNote that the push_stash() function implementing the 'push' subcommand\naccepts an extra flag parameter to indicate whether push was assumed,\nso add a wrapper function with the standard subcommand function\nsignature.\n\nNote also that this change \"hides\" the '-h' option in 'git stash push\n-h' from the parse_option() call in cmd_stash(), as it comes after the\nsubcommand.  Consequently, from now on it will emit the usage of the\n'push' subcommand instead of the usage of 'git stash'.  We had a\nfailing test for this case, which can now be flipped to expect\nsuccess.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/stash.c  | 53 ++++++++++++++++++++++--------------------------\n t/t3903-stash.sh |  2 +-\n 2 files changed, 25 insertions(+), 30 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex a14e832e9f..1ba24c1173 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1739,6 +1739,11 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n \t\t\t     include_untracked, only_staged);\n }\n \n+static int push_stash_unassumed(int argc, const char **argv, const char *prefix)\n+{\n+\treturn push_stash(argc, argv, prefix, 0);\n+}\n+\n static int save_stash(int argc, const char **argv, const char *prefix)\n {\n \tint keep_index = -1;\n@@ -1787,15 +1792,28 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \tpid_t pid = getpid();\n \tconst char *index_file;\n \tstruct strvec args = STRVEC_INIT;\n-\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"apply\", &fn, apply_stash),\n+\t\tOPT_SUBCOMMAND(\"clear\", &fn, clear_stash),\n+\t\tOPT_SUBCOMMAND(\"drop\", &fn, drop_stash),\n+\t\tOPT_SUBCOMMAND(\"pop\", &fn, pop_stash),\n+\t\tOPT_SUBCOMMAND(\"branch\", &fn, branch_stash),\n+\t\tOPT_SUBCOMMAND(\"list\", &fn, list_stash),\n+\t\tOPT_SUBCOMMAND(\"show\", &fn, show_stash),\n+\t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n+\t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n+\t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n+\t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\n \n \tgit_config(git_stash_config, NULL);\n \n \targc = parse_options(argc, argv, prefix, options, git_stash_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.command_requires_full_index = 0;\n@@ -1804,33 +1822,10 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n \t\t    (uintmax_t)pid);\n \n-\tif (!argc)\n-\t\treturn !!push_stash(0, NULL, prefix, 0);\n-\telse if (!strcmp(argv[0], \"apply\"))\n-\t\treturn !!apply_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"clear\"))\n-\t\treturn !!clear_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"drop\"))\n-\t\treturn !!drop_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"pop\"))\n-\t\treturn !!pop_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"branch\"))\n-\t\treturn !!branch_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"list\"))\n-\t\treturn !!list_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"show\"))\n-\t\treturn !!show_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"store\"))\n-\t\treturn !!store_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"create\"))\n-\t\treturn !!create_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"push\"))\n-\t\treturn !!push_stash(argc, argv, prefix, 0);\n-\telse if (!strcmp(argv[0], \"save\"))\n-\t\treturn !!save_stash(argc, argv, prefix);\n-\telse if (*argv[0] != '-')\n-\t\tusage_msg_optf(_(\"unknown subcommand: %s\"),\n-\t\t\t       git_stash_usage, options, argv[0]);\n+\tif (fn)\n+\t\treturn !!fn(argc, argv, prefix);\n+\telse if (!argc)\n+\t\treturn !!push_stash_unassumed(0, NULL, prefix);\n \n \t/* Assume 'stash push' */\n \tstrvec_push(&args, \"push\");\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex 2a4c3fd61c..376cc8f4ab 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '\n \tgrep -F \"or: git stash show\" usage\n '\n \n-test_expect_failure 'usage for subcommands should emit subcommand usage' '\n+test_expect_success 'usage for subcommands should emit subcommand usage' '\n \ttest_expect_code 129 git stash push -h >usage &&\n \tgrep -F \"usage: git stash [push\" usage\n '\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459882","messageId":"20220725123857.2773963-21-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH 20/20] builtin/worktree.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T12:38:57Z","receivedAt":"2022-07-25T12:40:24Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git worktree' parses its subcommands with a long list of if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/worktree.c | 31 ++++++++++++-------------------\n 1 file changed, 12 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex cd62eef240..c6710b2552 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -1112,31 +1112,24 @@ static int repair(int ac, const char **av, const char *prefix)\n \n int cmd_worktree(int ac, const char **av, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n+\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n+\t\tOPT_SUBCOMMAND(\"list\", &fn, list),\n+\t\tOPT_SUBCOMMAND(\"lock\", &fn, lock_worktree),\n+\t\tOPT_SUBCOMMAND(\"unlock\", &fn, unlock_worktree),\n+\t\tOPT_SUBCOMMAND(\"move\", &fn, move_worktree),\n+\t\tOPT_SUBCOMMAND(\"remove\", &fn, remove_worktree),\n+\t\tOPT_SUBCOMMAND(\"repair\", &fn, repair),\n \t\tOPT_END()\n \t};\n \n \tgit_config(git_worktree_config, NULL);\n \n-\tif (ac < 2)\n-\t\tusage_with_options(worktree_usage, options);\n \tif (!prefix)\n \t\tprefix = \"\";\n-\tif (!strcmp(av[1], \"add\"))\n-\t\treturn add(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"prune\"))\n-\t\treturn prune(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"list\"))\n-\t\treturn list(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"lock\"))\n-\t\treturn lock_worktree(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"unlock\"))\n-\t\treturn unlock_worktree(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"move\"))\n-\t\treturn move_worktree(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"remove\"))\n-\t\treturn remove_worktree(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"repair\"))\n-\t\treturn repair(ac - 1, av + 1, prefix);\n-\tusage_with_options(worktree_usage, options);\n+\n+\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n+\treturn fn(ac, av, prefix);\n }\n-- \n2.37.1.633.g6a0fa73e39\n\n"},{"id":"459885","messageId":"16acb1ce-92eb-d7ec-d5a2-3ef08cda9b69@github.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"Re: [PATCH 00/20] parse-options: handle subcommands","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-07-25T13:15:45Z","receivedAt":"2022-07-25T13:16:02Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 7/25/2022 8:38 AM, SZEDER Gábor wrote:\n> Several Git commands have subcommands to implement mutually exclusive\n> \"operation modes\", and they usually parse their subcommand argument\n> with a bunch of if-else if statements.\n> \n> Teach parse-options to handle subcommands as well, which will result\n> in shorter and simpler code with consistent error handling and error\n> messages on unknown or missing subcommand, and it will also make\n> possible for our Bash completion script to handle subcommands\n> programmatically in a follow-up series [1].\n\nSince this has become an increasingly common pattern, I appreciate\nthat you have standardized it here.\n\n> Patches 1-8 are a mix of preparatory cleanups, documentation updates, and\n> test coverage improvements.\n> \n> Patch 9 is the most important one, which teaches parse-options to handle\n> subcommands.\n> \n> The remaining 10-20 convert most builtin commands with subcommands one by\n> one to use parse-options to handle their subcommand parameters.\n\nI focused on reading the changes to the builtins I have experience with\n(commit-graph, maintenance, multi-pack-index, sparse-checkout, worktree)\nand found the adaptation to the new model very clean.\n\nThe one common thing I saw was that you are updating a function pointer\nthat you name \"fn\" but it could be more informative on first reading if\nit was named something like \"subcommand_fn\".\n\n> This patch series has two conflicts with 'seen' (but none with 'next'):\n\nAn upcoming conflict is going to be my bundle URI topic which is going\nto replace 'git fetch --bundle-uri' with 'git bundle fetch' in its next\nversion. I'll wait to see how Junio applies this series and I'll think\nabout splitting the current series into \"design doc\" and \"implementation\"\njust so I can build on your work here instead of colliding.\n\nThanks,\n-Stolee\n\n"},{"id":"459889","messageId":"220725.86mtcxp8ib.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-2-szeder.dev@gmail.com","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-25T14:31:40Z","receivedAt":"2022-07-25T14:33:27Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jul 25 2022, SZEDER Gábor wrote:\n\n> Our Bash completion script can complete --options for commands using\n> parse-options even when that command doesn't have a dedicated\n> completion function, but to do so the completion script must know\n> which commands use parse-options and which don't.  Therefore, commands\n> not using parse-options are marked in 'git.c's command list with the\n> NO_PARSEOPT flag.\n>\n> Update this list, and remove this flag from the commands that by now\n> use parse-options.\n>\n> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> ---\n>  git.c | 12 ++++++------\n>  1 file changed, 6 insertions(+), 6 deletions(-)\n>\n> diff --git a/git.c b/git.c\n> index e5d62fa5a9..c4282f194a 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -489,14 +489,14 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)\n>  static struct cmd_struct commands[] = {\n>  \t{ \"add\", cmd_add, RUN_SETUP | NEED_WORK_TREE },\n>  \t{ \"am\", cmd_am, RUN_SETUP | NEED_WORK_TREE },\n> -\t{ \"annotate\", cmd_annotate, RUN_SETUP | NO_PARSEOPT },\n> +\t{ \"annotate\", cmd_annotate, RUN_SETUP },\n>  \t{ \"apply\", cmd_apply, RUN_SETUP_GENTLY },\n>  \t{ \"archive\", cmd_archive, RUN_SETUP_GENTLY },\n>  \t{ \"bisect--helper\", cmd_bisect__helper, RUN_SETUP },\n>  \t{ \"blame\", cmd_blame, RUN_SETUP },\n>  \t{ \"branch\", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },\n>  \t{ \"bugreport\", cmd_bugreport, RUN_SETUP_GENTLY },\n> -\t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },\n> +\t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY },\n>  \t{ \"cat-file\", cmd_cat_file, RUN_SETUP },\n>  \t{ \"check-attr\", cmd_check_attr, RUN_SETUP },\n>  \t{ \"check-ignore\", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },\n> @@ -514,7 +514,7 @@ static struct cmd_struct commands[] = {\n>  \t{ \"column\", cmd_column, RUN_SETUP_GENTLY },\n>  \t{ \"commit\", cmd_commit, RUN_SETUP | NEED_WORK_TREE },\n>  \t{ \"commit-graph\", cmd_commit_graph, RUN_SETUP },\n> -\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP | NO_PARSEOPT },\n> +\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP },\n>  \t{ \"config\", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },\n>  \t{ \"count-objects\", cmd_count_objects, RUN_SETUP },\n>  \t{ \"credential\", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },\n> @@ -553,7 +553,7 @@ static struct cmd_struct commands[] = {\n>  \t{ \"ls-files\", cmd_ls_files, RUN_SETUP },\n>  \t{ \"ls-remote\", cmd_ls_remote, RUN_SETUP_GENTLY },\n>  \t{ \"ls-tree\", cmd_ls_tree, RUN_SETUP },\n> -\t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },\n> +\t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY },\n>  \t{ \"mailsplit\", cmd_mailsplit, NO_PARSEOPT },\n>  \t{ \"maintenance\", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },\n>  \t{ \"merge\", cmd_merge, RUN_SETUP | NEED_WORK_TREE },\n> @@ -566,7 +566,7 @@ static struct cmd_struct commands[] = {\n>  \t{ \"merge-recursive-theirs\", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },\n>  \t{ \"merge-subtree\", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },\n>  \t{ \"merge-tree\", cmd_merge_tree, RUN_SETUP },\n> -\t{ \"mktag\", cmd_mktag, RUN_SETUP | NO_PARSEOPT },\n> +\t{ \"mktag\", cmd_mktag, RUN_SETUP },\n>  \t{ \"mktree\", cmd_mktree, RUN_SETUP },\n>  \t{ \"multi-pack-index\", cmd_multi_pack_index, RUN_SETUP },\n>  \t{ \"mv\", cmd_mv, RUN_SETUP | NEED_WORK_TREE },\n> @@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {\n>  \t{ \"verify-tag\", cmd_verify_tag, RUN_SETUP },\n>  \t{ \"version\", cmd_version },\n>  \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP },\n> -\t{ \"worktree\", cmd_worktree, RUN_SETUP | NO_PARSEOPT },\n> +\t{ \"worktree\", cmd_worktree, RUN_SETUP },\n>  \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n>  };\n\nI have a superset of this change in my local tree:\nhttps://github.com/avar/git/commit/f0076de17fd\n\nI.e. \"parseopt\" here really means \"handles --git-completion-helper\", so\nwe can also address the diff family here. See the test that's associated\nwith the change (which you might want to grab in any case, i.e. to have\ntests in this area).\n\nBut as far as this goes this looks good to me.\n\n"},{"id":"459890","messageId":"220725.86ilnlp89v.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-4-szeder.dev@gmail.com","subject":"Re: [PATCH 03/20] t5505-remote.sh: check the behavior without a subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-25T14:37:20Z","receivedAt":"2022-07-25T14:38:26Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jul 25 2022, SZEDER Gábor wrote:\n\n>  t/t5505-remote.sh | 29 +++++++++++++++++++++++++++++\n>  1 file changed, 29 insertions(+)\n>\n> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\n> index 6c7370f87f..f075dd4afa 100755\n> --- a/t/t5505-remote.sh\n> +++ b/t/t5505-remote.sh\n> @@ -241,6 +241,35 @@ test_expect_success 'add invalid foreign_vcs remote' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'without subcommand' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit remote >actual &&\n> +\t\techo origin >expect &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +'\n> +\n> +test_expect_success 'without subcommand accepts -v' '\n> +\tcat >test/expect <<-EOF &&\n> +\torigin\t$(pwd)/one (fetch)\n> +\torigin\t$(pwd)/one (push)\n> +\tEOF\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit remote -v >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +'\n> +\n> +test_expect_success 'without subcommand does not take arguments' '\n> +\t(\n> +\t\tcd test &&\n> +\t\ttest_expect_code 129 git remote origin 2>err &&\n> +\t\tgrep \"^error: Unknown subcommand:\" err\n> +\t)\n> +'\n> +\n\nThis can be shortened quite a bit by getting rid of the sub-shells:\nhttps://github.com/avar/git/commit/e8eebd4eef6\n\nBut that version came from an earlier version of yours without those -v\ntests...\n"},{"id":"459891","messageId":"220725.86edy9p85i.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-5-szeder.dev@gmail.com","subject":"Re: [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-25T14:38:48Z","receivedAt":"2022-07-25T14:41:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jul 25 2022, SZEDER Gábor wrote:\n\n> In 't0040-parse-options.sh' we thoroughly test the parsing of all\n> types and forms of options, but in all those tests parse_options() is\n> always invoked with a 0 flags parameter.\n>\n> Add a few tests to demonstrate how various 'enum parse_opt_flags'\n> values are supposed to influence option parsing.\n>\n> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> ---\n>  t/helper/test-parse-options.c | 61 ++++++++++++++++++++++++++++++\n>  t/helper/test-tool.c          |  1 +\n>  t/helper/test-tool.h          |  1 +\n>  t/t0040-parse-options.sh      | 70 +++++++++++++++++++++++++++++++++++\n>  4 files changed, 133 insertions(+)\n>\n> diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\n> index 48d3cf6692..32b906bd6a 100644\n> --- a/t/helper/test-parse-options.c\n> +++ b/t/helper/test-parse-options.c\n> @@ -192,3 +192,64 @@ int cmd__parse_options(int argc, const char **argv)\n>  \n>  \treturn ret;\n>  }\n> +\n> +static int parse_options_flags__cmd(int argc, const char **argv,\n> +\t\t\t\t    enum parse_opt_flags test_flags)\n> +{\n> +\tconst char *usage[] = {\n> +\t\t\"<...> cmd [options]\",\n> +\t\tNULL\n> +\t};\n> +\tint opt = 0;\n> +\tconst struct option options[] = {\n> +\t\tOPT_INTEGER('o', \"opt\", &opt, \"an integer option\"),\n> +\t\tOPT_END()\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, NULL, options, usage, test_flags);\n> +\n> +\tprintf(\"opt: %d\\n\", opt);\n> +\tfor (int i = 0; i < argc; i++)\n> +\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n> +\n> +\treturn 0;\n> +}\n> +\n> +static enum parse_opt_flags test_flags = 0;\n> +static const struct option test_flag_options[] = {\n> +\tOPT_GROUP(\"flag-options:\"),\n> +\tOPT_BIT(0, \"keep-dashdash\", &test_flags,\n> +\t\t\"pass PARSE_OPT_KEEP_DASHDASH to parse_options()\",\n> +\t\tPARSE_OPT_KEEP_DASHDASH),\n> +\tOPT_BIT(0, \"stop-at-non-option\", &test_flags,\n> +\t\t\"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()\",\n> +\t\tPARSE_OPT_STOP_AT_NON_OPTION),\n> +\tOPT_BIT(0, \"keep-argv0\", &test_flags,\n> +\t\t\"pass PARSE_OPT_KEEP_ARGV0 to parse_options()\",\n> +\t\tPARSE_OPT_KEEP_ARGV0),\n> +\tOPT_BIT(0, \"keep-unknown\", &test_flags,\n> +\t\t\"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()\",\n> +\t\tPARSE_OPT_KEEP_UNKNOWN),\n> +\tOPT_BIT(0, \"no-internal-help\", &test_flags,\n> +\t\t\"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()\",\n> +\t\tPARSE_OPT_NO_INTERNAL_HELP),\n> +\tOPT_END()\n> +};\n> +\n> +int cmd__parse_options_flags(int argc, const char **argv)\n> +{\n> +\tconst char *usage[] = {\n> +\t\t\"test-tool parse-options-flags [flag-options] cmd [options]\",\n> +\t\tNULL\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, NULL, test_flag_options, usage,\n> +\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n> +\n> +\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n> +\t\terror(\"'cmd' is mandatory\");\n> +\t\tusage_with_options(usage, test_flag_options);\n> +\t}\n> +\n> +\treturn parse_options_flags__cmd(argc, argv, test_flags);\n> +}\n> diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\n> index 318fdbab0c..6e62282b60 100644\n> --- a/t/helper/test-tool.c\n> +++ b/t/helper/test-tool.c\n> @@ -51,6 +51,7 @@ static struct test_cmd cmds[] = {\n>  \t{ \"online-cpus\", cmd__online_cpus },\n>  \t{ \"pack-mtimes\", cmd__pack_mtimes },\n>  \t{ \"parse-options\", cmd__parse_options },\n> +\t{ \"parse-options-flags\", cmd__parse_options_flags },\n>  \t{ \"parse-pathspec-file\", cmd__parse_pathspec_file },\n>  \t{ \"partial-clone\", cmd__partial_clone },\n>  \t{ \"path-utils\", cmd__path_utils },\n\nI wanted to add some new parse_options() code to\nt/helper/test-parse-options.c in the past, but was stymied by its\ncmd_*() going through a singular parse_options().\n\nSo just creating a new callback is a neat solution.\n\nBut wouldn't it be better to just create a new\nt/helper/test-parse-options-something-.c & test file? It seems this\ndoesn't really share anything with the current helper & tests...\n"},{"id":"459892","messageId":"220725.86a68xp7az.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-10-szeder.dev@gmail.com","subject":"Re: [PATCH 09/20] parse-options: add support for parsing subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-25T14:43:30Z","receivedAt":"2022-07-25T14:59:22Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jul 25 2022, SZEDER Gábor wrote:\n\n> Several Git commands have subcommands to implement mutually exclusive\n> \"operation modes\", and they usually parse their subcommand argument\n> with a bunch of if-else if statements.\n\nI'll need do look this over in more details, just some comments on the\nnon-meaty parts for now:\n\n> diff --git a/builtin/blame.c b/builtin/blame.c\n> index 02e39420b6..a9fe8cf7a6 100644\n> --- a/builtin/blame.c\n> +++ b/builtin/blame.c\n> @@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n>  \t\t\tbreak;\n>  \t\tcase PARSE_OPT_HELP:\n>  \t\tcase PARSE_OPT_ERROR:\n> +\t\tcase PARSE_OPT_SUBCOMMAND:\n>  \t\t\texit(129);\n>  \t\tcase PARSE_OPT_COMPLETE:\n>  \t\t\texit(0);\n> diff --git a/builtin/shortlog.c b/builtin/shortlog.c\n> index 086dfee45a..7a1e1fe7c0 100644\n> --- a/builtin/shortlog.c\n> +++ b/builtin/shortlog.c\n> @@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)\n>  \t\t\tbreak;\n>  \t\tcase PARSE_OPT_HELP:\n>  \t\tcase PARSE_OPT_ERROR:\n> +\t\tcase PARSE_OPT_SUBCOMMAND:\n>  \t\t\texit(129);\n>  \t\tcase PARSE_OPT_COMPLETE:\n>  \t\t\texit(0);\n\nThis feels a bit like carrying forward an API wart, i.e. shouldn't we\ninstead BUG() if we are returning a PARSE_OPT_SUBCOMMAND from\nparse_options_step() for options lists that don't have\nPARSE_OPT_SUBCOMMAND in them?\n\nI.e. is this even reachable, or just something to suppress the compiler\ncomplaining about missing enum labels?\n\n>  static void check_typos(const char *arg, const struct option *options)\n>  {\n>  \tif (strlen(arg) < 3)\n> @@ -442,6 +457,7 @@ static void check_typos(const char *arg, const struct option *options)\n>  static void parse_options_check(const struct option *opts)\n>  {\n>  \tchar short_opts[128];\n> +\tvoid *subcommand_value = NULL;\n>  \n>  \tmemset(short_opts, '\\0', sizeof(short_opts));\n>  \tfor (; opts->type != OPTION_END; opts++) {\n> @@ -489,6 +505,14 @@ static void parse_options_check(const struct option *opts)\n>  \t\t\t       \"Are you using parse_options_step() directly?\\n\"\n>  \t\t\t       \"That case is not supported yet.\");\n>  \t\t\tbreak;\n> +\t\tcase OPTION_SUBCOMMAND:\n> +\t\t\tif (!opts->value || !opts->subcommand_fn)\n> +\t\t\t\toptbug(opts, \"OPTION_SUBCOMMAND needs a value and a subcommand function\");\n> +\t\t\tif (!subcommand_value)\n> +\t\t\t\tsubcommand_value = opts->value;\n> +\t\t\telse if (subcommand_value != opts->value)\n> +\t\t\t\toptbug(opts, \"all OPTION_SUBCOMMANDs need the same value\");\n> +\t\t\tbreak;\n>  \t\tdefault:\n>  \t\t\t; /* ok. (usually accepts an argument) */\n>  \t\t}\n\nThis addition looks good...\n\n> @@ -499,6 +523,14 @@ static void parse_options_check(const struct option *opts)\n>  \tBUG_if_bug(\"invalid 'struct option'\");\n>  }\n>  \n> +static int has_subcommands(const struct option *options)\n> +{\n> +\tfor (; options->type != OPTION_END; options++)\n> +\t\tif (options->type == OPTION_SUBCOMMAND)\n> +\t\t\treturn 1;\n> +\treturn 0;\n> +}\n\n...but why not...\n\n>  static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n>  \t\t\t\t  int argc, const char **argv, const char *prefix,\n>  \t\t\t\t  const struct option *options,\n> @@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n>  \tctx->prefix = prefix;\n>  \tctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);\n>  \tctx->flags = flags;\n> +\tctx->has_subcommands = has_subcommands(options);\n> +\tif (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL))\n> +\t\tBUG(\"Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands\");\n> +\tif (ctx->has_subcommands) {\n> +\t\tif (flags & PARSE_OPT_STOP_AT_NON_OPTION)\n> +\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION\");\n> +\t\tif (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n> +\t\t\tif (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)\n> +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n> +\t\t\tif (flags & PARSE_OPT_KEEP_DASHDASH)\n> +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n> +\t\t}\n> +\t}\n\n...move this into parse_options_check()? I.e. we'd need to loop over the\nlist once, but it seems like this should belong there.\n\nWe have an existing bug in-tree due to usage_with_options() not doing a\nparse_options_check() (I have a local fix...), checking this sort of\nthing there instead of in parse_options_start() is therefore the right\nthing to do, i.e. we shoudl have a one-stop \"does this options variable\nlook sane?\".\n\n> +\t\t\t\terror(_(\"unknown subcommand: %s\"), arg);\n\ns/%s/'%s'/ while we're at it, perhaps?\n\n> +\t\t\t\tusage_with_options(usagestr, options);\n> +\t\t\tcase PARSE_OPT_COMPLETE:\n> +\t\t\tcase PARSE_OPT_HELP:\n> +\t\t\tcase PARSE_OPT_ERROR:\n> +\t\t\tcase PARSE_OPT_DONE:\n> +\t\t\tcase PARSE_OPT_NON_OPTION:\n> +\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n\nnit: BUG(\"got bad %d\", v) or whatever, i.e. say what we got?\n\n> @@ -206,6 +217,11 @@ struct option {\n>  #define OPT_ALIAS(s, l, source_long_name) \\\n>  \t{ OPTION_ALIAS, (s), (l), (source_long_name) }\n>  \n> +#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n> +\t\t\t\t\tNULL, 0, NULL, 0, NULL, 0, (fn) }\n> +#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n> +\t\t\t\t\tNULL, (f), NULL, 0, NULL, 0, (fn) }\n\nNit, I know you're carrying forward existing patterns, but since that\nall pre-dated designated init perhaps we could just (untested):\n\t\n\t#define OPT_SUBCOMMAND_F(l, v, fn, f) { \\\n\t\t.type = OPTION_SUBCOMMAND, \\\n\t\t.long_name = (l), \\\n\t\t.value = (v), \\\n\t\t.ll_callback = (fn), \\\n\t}\n\t#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)\n\nWhich IMO is much nicer. I have some patches somewhere to convert these\nto saner patterns (I think not designated init, but the X() can be\ndefined in terms of X_F() like that, but since this is new we can use\ndesignated init all the way...\n\n> +{\n> +\tint i;\n\nNit: missing \\n (usual style of variable decl);\n\n> +\t\terror(\"'cmd' is mandatory\");\n> +\t\tusage_with_options(usage, test_flag_options);\n\nnit: I think you want usage_msg_optf() or usage_msg_opt().\n\n> +test_expect_success 'subcommand - no subcommand shows error and usage' '\n> +\ttest_expect_code 129 test-tool parse-subcommand cmd 2>err &&\n> +\tgrep \"^error: need a subcommand\" err &&\n> +\tgrep ^usage: err\n> +'\n> +\n> +test_expect_success 'subcommand - subcommand after -- shows error and usage' '\n> +\ttest_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&\n> +\tgrep \"^error: need a subcommand\" err &&\n> +\tgrep ^usage: err\n> +'\n> +\n> +test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '\n> +\ttest_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&\n> +\tgrep \"^error: need a subcommand\" err &&\n> +\tgrep ^usage: err\n> +'\n> +\n> +test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n> +\ttest_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&\n> +\tgrep \"^error: unknown subcommand: nope\" err &&\n> +\tgrep ^usage: err\n> +'\n> +\n> +test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n> +\ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n> +\tgrep \"^error: unknown subcommand: subcmd-o$\" err &&\n> +\tgrep ^usage: err\n> +'\n> +\n> +test_expect_success 'subcommand - no negated subcommands' '\n> +\ttest_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&\n> +\tgrep \"^error: unknown subcommand: no-subcmd-one\" err &&\n> +\tgrep ^usage: err\n> +'\n\nCreating a trivial helper for this seems worthile, then something like:\n\n\tthat_helper \"expected error here\" -- arg u ments to test-tool parse-subcommand\n\n\n\n> +test_expect_success 'subcommand - simple' '\n> +\ttest-tool parse-subcommand cmd subcmd-two >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\topt: 0\n> +\tfn: subcmd_two\n> +\targ 00: subcmd-two\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nDitto, perhaps? I.e.\n\n\tnew_hepler arg u ments <<-\\EOF\n        expected\n        goes here\n       \tEOF\n"},{"id":"459894","messageId":"20220725160040.GA2543@szeder.dev","threadId":"58218","inReplyTo":"16acb1ce-92eb-d7ec-d5a2-3ef08cda9b69@github.com","subject":"Re: [PATCH 00/20] parse-options: handle subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T16:00:40Z","receivedAt":"2022-07-25T16:00:46Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Jul 25, 2022 at 09:15:45AM -0400, Derrick Stolee wrote:\n> I focused on reading the changes to the builtins I have experience with\n> (commit-graph, maintenance, multi-pack-index, sparse-checkout, worktree)\n> and found the adaptation to the new model very clean.\n> \n> The one common thing I saw was that you are updating a function pointer\n> that you name \"fn\" but it could be more informative on first reading if\n> it was named something like \"subcommand_fn\".\n\nI felt that redundant, because most lines mentioning that 'fn'\nhave something clearly subcommand-specific next to it, i.e. the type\n'parse_opt_subcommand_fn' at its declaration, or the OPT_SUBCOMMAND\nmacro.\n\n"},{"id":"459895","messageId":"4e28ea5a-03d3-990c-34ad-5d5a91d7b888@github.com","threadId":"58218","inReplyTo":"20220725160040.GA2543@szeder.dev","subject":"Re: [PATCH 00/20] parse-options: handle subcommands","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-07-25T16:08:16Z","receivedAt":"2022-07-25T16:08:22Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 7/25/2022 12:00 PM, SZEDER Gábor wrote:\n> On Mon, Jul 25, 2022 at 09:15:45AM -0400, Derrick Stolee wrote:\n>> I focused on reading the changes to the builtins I have experience with\n>> (commit-graph, maintenance, multi-pack-index, sparse-checkout, worktree)\n>> and found the adaptation to the new model very clean.\n>>\n>> The one common thing I saw was that you are updating a function pointer\n>> that you name \"fn\" but it could be more informative on first reading if\n>> it was named something like \"subcommand_fn\".\n> \n> I felt that redundant, because most lines mentioning that 'fn'\n> have something clearly subcommand-specific next to it, i.e. the type\n> 'parse_opt_subcommand_fn' at its declaration, or the OPT_SUBCOMMAND\n> macro.\n \nI guess I was just reading the final \"return fn(...);\" at the end\nand thought it looked a bit generic. It's probably not worth\nchanging.\n\nThanks,\n-Stolee\n"},{"id":"459896","messageId":"xmqq7d416sts.fsf@gitster.g","threadId":"58218","inReplyTo":"20220725123857.2773963-16-szeder.dev@gmail.com","subject":"Re: [PATCH 15/20] builtin/notes.c: let parse-options parse subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-07-25T16:49:19Z","receivedAt":"2022-07-25T16:49:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> 'git notes' parses its subcommands with a long list of if-else if\n> statements.  parse-options has just learned to parse subcommands, so\n> let's use that facility instead, with the benefits of shorter code,\n> handling unknown subcommands, and listing subcommands for Bash\n> completion.  Make sure that the default operation mode doesn't accept\n> any arguments.\n>\n> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> ---\n>  builtin/notes.c | 43 +++++++++++++++++--------------------------\n>  1 file changed, 17 insertions(+), 26 deletions(-)\n>\n> diff --git a/builtin/notes.c b/builtin/notes.c\n> index a3d0d15a22..42cbae4659 100644\n> --- a/builtin/notes.c\n> +++ b/builtin/notes.c\n> @@ -994,17 +994,31 @@ static int get_ref(int argc, const char **argv, const char *prefix)\n>  \n>  int cmd_notes(int argc, const char **argv, const char *prefix)\n>  {\n> -\tint result;\n>  \tconst char *override_notes_ref = NULL;\n> +\tparse_opt_subcommand_fn *fn = list;\n>  \tstruct option options[] = {\n>  \t\tOPT_STRING(0, \"ref\", &override_notes_ref, N_(\"notes-ref\"),\n>  \t\t\t   N_(\"use notes from <notes-ref>\")),\n> +\t\tOPT_SUBCOMMAND(\"list\", &fn, list),\n> +\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n> +\t\tOPT_SUBCOMMAND(\"copy\", &fn, copy),\n> +\t\tOPT_SUBCOMMAND(\"append\", &fn, append_edit),\n> +\t\tOPT_SUBCOMMAND(\"edit\", &fn, append_edit),\n> +\t\tOPT_SUBCOMMAND(\"show\", &fn, show),\n> +\t\tOPT_SUBCOMMAND(\"merge\", &fn, merge),\n> +\t\tOPT_SUBCOMMAND(\"remove\", &fn, remove_cmd),\n> +\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n> +\t\tOPT_SUBCOMMAND(\"get-ref\", &fn, get_ref),\n>  \t\tOPT_END()\n>  \t};\n\nReading the series backwards from the end, I would expect that the\nabove to replicate the current behaviour to allow commands to have\nboth \"command wide\" options and \"per subcommand\" options, e.g.\n\n    $ git notes get-ref --ref=amlog\n    error: unknown option `ref=amlog`\n    usage: git notes get-ref\n    $ git notes --ref=amlog get-ref\n    refs/notes/amlog\n\nAssuming that is how the new OPT_SUBCOMMAND() interacts with the\ndashed options in a single \"struct option []\", everything I saw\nso far in [10-20/20] makes sense.  [17/20] has another instance\nof the above, dashed-options and subcommands mixed in an array,\nto parse the option for \"git remote --verbose <subcmd>\" that applies\nto all subcommands.\n\nThanks.\n\n\n"},{"id":"459898","messageId":"220725.865yjlp0yi.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"Re: [PATCH 00/20] parse-options: handle subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-25T17:13:13Z","receivedAt":"2022-07-25T17:16:28Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jul 25 2022, SZEDER Gábor wrote:\n\n> Several Git commands have subcommands to implement mutually exclusive\n> \"operation modes\", and they usually parse their subcommand argument\n> with a bunch of if-else if statements.\n\nOn this series in general, quoting from 19/20:\n\n> Note also that this change \"hides\" the '-h' option in 'git stash push\n> -h' from the parse_option() call in cmd_stash(), as it comes after the\n> subcommand.  Consequently, from now on it will emit the usage of the\n> 'push' subcommand instead of the usage of 'git stash'.  We had a\n> failing test for this case, which can now be flipped to expect\n> success.\n\nThis is good!\n\n>  \t/* Assume 'stash push' */\n>  \tstrvec_push(&args, \"push\");\n> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\n> index 2a4c3fd61c..376cc8f4ab 100755\n> --- a/t/t3903-stash.sh\n> +++ b/t/t3903-stash.sh\n> @@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '\n>  \tgrep -F \"or: git stash show\" usage\n>  '\n>  \n> -test_expect_failure 'usage for subcommands should emit subcommand usage' '\n> +test_expect_success 'usage for subcommands should emit subcommand usage' '\n>  \ttest_expect_code 129 git stash push -h >usage &&\n>  \tgrep -F \"usage: git stash [push\" usage\n>  '\n\nThis fixes the TODO test I left in ca7990cea5a (stash: don't show \"git\nstash push\" usage on bad \"git stash\" usage, 2021-12-16), it probably\nmakes sense to link to that in the commit message, as it shows exactly\nthe output you're referring to.\n\nAfter our recent off-list discussion about this topic I thought it would\nmake sense to lead with some testing of this behavior, so I had created\nthis WIP:\nhttps://github.com/git/git/compare/master...avar:git:avar-szeder/add-usage-tests\n\nSome of that is adding tests for the behavior of commands you're\nmodifying in this series, many of them suffer from the same caveat as\n\"git stash\" did before this fix, and it would be nice to test for that.\n\nBut on the other hand maybe we're confident enough with this being a\nstandard API that it's just tested once, and we don't need to reapeat\nfor every command that uses it that \"git commit-graph write -h\" or\nwhatever returns usage that's a subset of \"git commit-graph -h\" etc.\n\nI left some nits here & there, and would like to have more time to\nreview 10/20 in particlar (as well as others).\n\nBut this series is a bit winding, in that it's doing at least these N\nunrelated things:\n\n * Fixes to parse-options docs\n * Tests for existing parse options behavior\n * Nits here & there, e.g. 08/20\n * The meaty change in 10/20\n * For 11-20 some of it's one-to-one with existing behavior, and some of\n   it's changing behavior (but all in a good way, I think!), per the\n   above some of which we don't have tests for.\n\nPersonally I'm fine with just bundling this all together, just some\nreading notes from going over this...\n\n\n"},{"id":"459899","messageId":"xmqqa68x5c1i.fsf@gitster.g","threadId":"58218","inReplyTo":"20220725123857.2773963-10-szeder.dev@gmail.com","subject":"Re: [PATCH 09/20] parse-options: add support for parsing subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-07-25T17:37:13Z","receivedAt":"2022-07-25T17:37:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> The approach is guided by the following observations:\n> ...\n> So in the end subcommand declaration and parsing would look something\n> like this:\n>\n>     parse_opt_subcommand_fn *fn = NULL;\n>     struct option builtin_commit_graph_options[] = {\n>         OPT_STRING(0, \"object-dir\", &opts.obj_dir, N_(\"dir\"),\n>                    N_(\"the object directory to store the graph\")),\n>         OPT_SUBCOMMAND(\"verify\", &fn, graph_verify),\n>         OPT_SUBCOMMAND(\"write\", &fn, graph_write),\n>         OPT_END(),\n>     };\n>     argc = parse_options(argc, argv, prefix, options,\n>                          builtin_commit_graph_usage, 0);\n>     return fn(argc, argv, prefix);\n\nAll makes sense and exactly as I expected to see, after reading the\nlater steps [10-20/20] that make use of the facility.  Nicely designed.\n\n> Here each OPT_SUBCOMMAND specifies the name of the subcommand and the\n> function implementing it, and the same pointer to 'fn' where\n> parse_options() will store the function associated with the given\n> subcommand.\n\nPuzzling.  Isn't parse_options() an read-only operation with respect\nto the elements of the struct option array?  \n\nWith s/store/expect to find/, I would understand the above, though.\n\n> There is no need to check whether 'fn' is non-NULL before\n> invoking it:\n\nAgain, this is puzzling.  \"There is no need to\"---to whom does this\nstatement mean to advise?  The implementor of the new codepath IN\nparse_options() to handle OPT_SUBCOMMAND()?\n\n> if there were no subcommands given on the command line\n> then the parse_options() call would error out and show usage.  \n\nI think that you mean to say\n\n    When one or more OPT_SUBCOMMAND elements exist in an array of\n    struct option, parse_options() will error out if none of them\n    triggers.\n\nand that makes sense, but I cannot quite tell how it relates to the\nprevious sentence about fn possibly being NULL.\n\nAhh, OK.  Let's try to rephrase to see if I can make it easier to\nunderstand.\n\n    Here each OPT_SUBCOMMAND specifies ... with the given\n    subcommand.\n\n    After parse_options() returns, the variable 'fn' would have been\n    updated to point at the function to call, if one of the\n    subcommand specified by the OPT_SUBCOMMAND() is given on the\n    command line.\n\n    - When the PARSE_OPT_SUBCOMMAND_OPTIONAL flag is given to\n      parse_options(), and no subcommand is given on the command\n      line, the variable 'fn' is left intact.  This can be used to\n      implement a command with the \"default\" subcommand by\n      initializing the variable 'fn' to the default value.\n\n    - Otherwise, parse_options() will error out and show usage, when\n      no subcommand is given.\n\n    - If more than one subcommands are given from the command line,\n      parse_options() will stop at the first one, and treat the\n      remainder of the command line as arguments to the subcommand.\n    \n> In case\n> a command has a default operation mode, 'fn' should be initialized to\n> the function implementing that mode, and parse_options() should be\n> invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.\n\nOK.\n\n> Some thoughts about the implementation:\n>\n>   - Arguably it is a bit weird that the same pointer to 'fn' have to\n>     be specified as 'value' for each OPT_SUBCOMMAND, but we need a way\n>     to tell parse_options() where to put the function associated with\n>     the given subcommand, and I didn't like the alternatives:\n\nI do not find this so disturbing.  This is similar to CMDMODE in\nspirit in that only one has to be chosen.  CMDMODE needs to go an\nextra mile to ensure that by checking the current value in the\nvariable pointed by the pointer because the parser does not stop\nimmediately after getting one, but SUBCOMMAND can afford to be\nsimpler because the parser immediately stops once a subcommand is\nfound.\n\nIn a sense, OPT_SUBCOMMAND() does not have to exist as the first-class\nmechanism.  With just two primitives:\n\n - a flag that says \"after seeing this option, immediately stop\n   parsing\".\n - something similar to OPT_SET_INT() but works with a function pointer.\n\nwe can go 80% of the way to implement OPT_SUBCOMMAND() as a thin\nwrapper (there is \"one of the OPT_SET_FUNC() must be triggered\" that\nneeds new code specific to the need, which is the other 20%).\n\n>   - I decided against automatically calling the subcommand function\n>     from within parse_options(), because:\n>\n>       - There are commands that have to perform additional actions\n>         after option parsing but before calling the function\n>         implementing the specified subcommand.\n>\n>       - The return code of the subcommand is usually the return code\n>         of the git command, but preserving the return code of the\n>         automatically called subcommand function would have made the\n>         API awkward.\n\nYes, the above makes perfect sense.  Also I suspect that we would\nfind some cases where being able to use two or more variables become\nhandy.\n\n> +\t\t\tcase PARSE_OPT_COMPLETE:\n> +\t\t\tcase PARSE_OPT_HELP:\n> +\t\t\tcase PARSE_OPT_ERROR:\n> +\t\t\tcase PARSE_OPT_DONE:\n> +\t\t\tcase PARSE_OPT_NON_OPTION:\n> +\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n> +\t\t\t}\n\n\"these\" without giving the violating value?  A \"%d\" would be cheap\naddition, but I see this was copied from elsewhere.\n\nThanks.\n"},{"id":"459900","messageId":"xmqq5yjl5b4y.fsf@gitster.g","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"Re: [PATCH 00/20] parse-options: handle subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-07-25T17:56:45Z","receivedAt":"2022-07-25T17:56:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n>   - builtin/bisect.c: after the conversion/rename from 'bisect--helper',\n>     cmd_bisect() doesn't use parse-options anymore.  Take what's on 'seen'\n>     to resolve the conflict.\n>     Note that the conflicting topic should have marked cmd_bisect() with\n>     the NO_PARSEOPT flag in 'git.c's command list.\n\nI was wondering about this one.  Does the new \"subcommand\" support\nhelp implementing the dispatching to subcommands better?  If so it\nmay not be a bad idea to redo the cmd_bisect() on top of this series\nonce it proves to be solid.\n\nSo far, I like everything I saw in this series.  Nicely done.\n\nThanks.\n"},{"id":"459908","messageId":"20220725192947.GB2543@szeder.dev","threadId":"58218","inReplyTo":"220725.86a68xp7az.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 09/20] parse-options: add support for parsing subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T19:29:47Z","receivedAt":"2022-07-25T19:29:54Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Jul 25, 2022 at 04:43:30PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > diff --git a/builtin/blame.c b/builtin/blame.c\n> > index 02e39420b6..a9fe8cf7a6 100644\n> > --- a/builtin/blame.c\n> > +++ b/builtin/blame.c\n> > @@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n> >  \t\t\tbreak;\n> >  \t\tcase PARSE_OPT_HELP:\n> >  \t\tcase PARSE_OPT_ERROR:\n> > +\t\tcase PARSE_OPT_SUBCOMMAND:\n> >  \t\t\texit(129);\n> >  \t\tcase PARSE_OPT_COMPLETE:\n> >  \t\t\texit(0);\n> > diff --git a/builtin/shortlog.c b/builtin/shortlog.c\n> > index 086dfee45a..7a1e1fe7c0 100644\n> > --- a/builtin/shortlog.c\n> > +++ b/builtin/shortlog.c\n> > @@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)\n> >  \t\t\tbreak;\n> >  \t\tcase PARSE_OPT_HELP:\n> >  \t\tcase PARSE_OPT_ERROR:\n> > +\t\tcase PARSE_OPT_SUBCOMMAND:\n> >  \t\t\texit(129);\n> >  \t\tcase PARSE_OPT_COMPLETE:\n> >  \t\t\texit(0);\n> \n> This feels a bit like carrying forward an API wart, i.e. shouldn't we\n> instead BUG() if we are returning a PARSE_OPT_SUBCOMMAND from\n> parse_options_step() for options lists that don't have\n> PARSE_OPT_SUBCOMMAND in them?\n> \n> I.e. is this even reachable, or just something to suppress the compiler\n> complaining about missing enum labels?\n\nI think it's as good as unreachable, because neither of these two\ncommands have subcommands.  However, without these hunks the compiler\ninvoked with '-Wswitch' (implied by '-Wall') does indeed complain.\n \n> ...but why not...\n> \n> >  static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n> >  \t\t\t\t  int argc, const char **argv, const char *prefix,\n> >  \t\t\t\t  const struct option *options,\n> > @@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n> >  \tctx->prefix = prefix;\n> >  \tctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);\n> >  \tctx->flags = flags;\n> > +\tctx->has_subcommands = has_subcommands(options);\n> > +\tif (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL))\n> > +\t\tBUG(\"Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands\");\n> > +\tif (ctx->has_subcommands) {\n> > +\t\tif (flags & PARSE_OPT_STOP_AT_NON_OPTION)\n> > +\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION\");\n> > +\t\tif (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n> > +\t\t\tif (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)\n> > +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n> > +\t\t\tif (flags & PARSE_OPT_KEEP_DASHDASH)\n> > +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n> > +\t\t}\n> > +\t}\n> \n> ...move this into parse_options_check()? I.e. we'd need to loop over the\n> list once, but it seems like this should belong there.\n> \n> We have an existing bug in-tree due to usage_with_options() not doing a\n> parse_options_check() (I have a local fix...), checking this sort of\n> thing there instead of in parse_options_start() is therefore the right\n> thing to do, i.e. we shoudl have a one-stop \"does this options variable\n> look sane?\".\n\nThe checks added in this hunk (and the existing checks in the hunk's\nafter-context) are not about the elements of the 'struct option'\narray, like the checks in parse_options_check(), but rather about the\nsensibility of parse_options()'s 'parse_opt_flags' parameter.\nusage_with_options() doesn't have (and doesn't at all need) such a\nparameter.\n\n> > +\t\t\t\terror(_(\"unknown subcommand: %s\"), arg);\n> \n> s/%s/'%s'/ while we're at it, perhaps?\n\nIndeed, though it should be `%s', because that's what surrounds\nunknown switches and options in the corresponding error messages.\n\n> > +\t\t\t\tusage_with_options(usagestr, options);\n> > +\t\t\tcase PARSE_OPT_COMPLETE:\n> > +\t\t\tcase PARSE_OPT_HELP:\n> > +\t\t\tcase PARSE_OPT_ERROR:\n> > +\t\t\tcase PARSE_OPT_DONE:\n> > +\t\t\tcase PARSE_OPT_NON_OPTION:\n> > +\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n> \n> nit: BUG(\"got bad %d\", v) or whatever, i.e. say what we got?\n\nAll these are impossible, so I don't think it matters.  This is\nanother case of the compiler with '-Wswitch' complaining, and follows\nsuit of similar switch statements after the calls to parse_short_opt()\nand parse_long_opt() functions.\n\n> > @@ -206,6 +217,11 @@ struct option {\n> >  #define OPT_ALIAS(s, l, source_long_name) \\\n> >  \t{ OPTION_ALIAS, (s), (l), (source_long_name) }\n> >  \n> > +#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n> > +\t\t\t\t\tNULL, 0, NULL, 0, NULL, 0, (fn) }\n> > +#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n> > +\t\t\t\t\tNULL, (f), NULL, 0, NULL, 0, (fn) }\n> \n> Nit, I know you're carrying forward existing patterns, but since that\n> all pre-dated designated init perhaps we could just (untested):\n> \t\n> \t#define OPT_SUBCOMMAND_F(l, v, fn, f) { \\\n> \t\t.type = OPTION_SUBCOMMAND, \\\n> \t\t.long_name = (l), \\\n> \t\t.value = (v), \\\n> \t\t.ll_callback = (fn), \\\n> \t}\n> \t#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)\n> \n> Which IMO is much nicer. I have some patches somewhere to convert these\n> to saner patterns (I think not designated init, but the X() can be\n> defined in terms of X_F() like that, but since this is new we can use\n> designated init all the way...\n\nOh, I love this idea!  But are we there yet?  I remember the weather\nballoon about designated initializers, but I'm not sure whether we've\nalready made the decision to allow them.  If we do, then I'm inclined\nto volunteer to clean up all those OPT_* macros in 'parse-options.h'\nwith designated initializers, \n\n> > +{\n> > +\tint i;\n> \n> Nit: missing \\n (usual style of variable decl);\n\nHm, speaking of newer C features, what about 'for (int i = 0; ...)'?\n\n> > +\t\terror(\"'cmd' is mandatory\");\n> > +\t\tusage_with_options(usage, test_flag_options);\n> \n> nit: I think you want usage_msg_optf() or usage_msg_opt().\n\nMaybe... but I don't know what they do ;)  Though I remember removing\na couple of similar error() and usage_with_options() pairs from the\nbuiltin commands.\n\n"},{"id":"459909","messageId":"220725.86wnc1nf6c.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220725192947.GB2543@szeder.dev","subject":"Re: [PATCH 09/20] parse-options: add support for parsing subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-25T19:41:52Z","receivedAt":"2022-07-25T19:52:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jul 25 2022, SZEDER Gábor wrote:\n\n> On Mon, Jul 25, 2022 at 04:43:30PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> > diff --git a/builtin/blame.c b/builtin/blame.c\n>> > index 02e39420b6..a9fe8cf7a6 100644\n>> > --- a/builtin/blame.c\n>> > +++ b/builtin/blame.c\n>> > @@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n>> >  \t\t\tbreak;\n>> >  \t\tcase PARSE_OPT_HELP:\n>> >  \t\tcase PARSE_OPT_ERROR:\n>> > +\t\tcase PARSE_OPT_SUBCOMMAND:\n>> >  \t\t\texit(129);\n>> >  \t\tcase PARSE_OPT_COMPLETE:\n>> >  \t\t\texit(0);\n>> > diff --git a/builtin/shortlog.c b/builtin/shortlog.c\n>> > index 086dfee45a..7a1e1fe7c0 100644\n>> > --- a/builtin/shortlog.c\n>> > +++ b/builtin/shortlog.c\n>> > @@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)\n>> >  \t\t\tbreak;\n>> >  \t\tcase PARSE_OPT_HELP:\n>> >  \t\tcase PARSE_OPT_ERROR:\n>> > +\t\tcase PARSE_OPT_SUBCOMMAND:\n>> >  \t\t\texit(129);\n>> >  \t\tcase PARSE_OPT_COMPLETE:\n>> >  \t\t\texit(0);\n>> \n>> This feels a bit like carrying forward an API wart, i.e. shouldn't we\n>> instead BUG() if we are returning a PARSE_OPT_SUBCOMMAND from\n>> parse_options_step() for options lists that don't have\n>> PARSE_OPT_SUBCOMMAND in them?\n>> \n>> I.e. is this even reachable, or just something to suppress the compiler\n>> complaining about missing enum labels?\n>\n> I think it's as good as unreachable, because neither of these two\n> commands have subcommands.  However, without these hunks the compiler\n> invoked with '-Wswitch' (implied by '-Wall') does indeed complain.\n\nYeah, we should add a case, but let's just do:\n\n\tcase PARSE_OPT_SUBCOMMAND:\n\t\tBUG(\"unreachable\");\n\n>> ...but why not...\n>> \n>> >  static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n>> >  \t\t\t\t  int argc, const char **argv, const char *prefix,\n>> >  \t\t\t\t  const struct option *options,\n>> > @@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n>> >  \tctx->prefix = prefix;\n>> >  \tctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);\n>> >  \tctx->flags = flags;\n>> > +\tctx->has_subcommands = has_subcommands(options);\n>> > +\tif (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL))\n>> > +\t\tBUG(\"Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands\");\n>> > +\tif (ctx->has_subcommands) {\n>> > +\t\tif (flags & PARSE_OPT_STOP_AT_NON_OPTION)\n>> > +\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION\");\n>> > +\t\tif (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n>> > +\t\t\tif (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)\n>> > +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n>> > +\t\t\tif (flags & PARSE_OPT_KEEP_DASHDASH)\n>> > +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n>> > +\t\t}\n>> > +\t}\n>> \n>> ...move this into parse_options_check()? I.e. we'd need to loop over the\n>> list once, but it seems like this should belong there.\n>> \n>> We have an existing bug in-tree due to usage_with_options() not doing a\n>> parse_options_check() (I have a local fix...), checking this sort of\n>> thing there instead of in parse_options_start() is therefore the right\n>> thing to do, i.e. we shoudl have a one-stop \"does this options variable\n>> look sane?\".\n>\n> The checks added in this hunk (and the existing checks in the hunk's\n> after-context) are not about the elements of the 'struct option'\n> array, like the checks in parse_options_check(), but rather about the\n> sensibility of parse_options()'s 'parse_opt_flags' parameter.\n> usage_with_options() doesn't have (and doesn't at all need) such a\n> parameter.\n\nAh, sorry, I was just confused. FWIW it's because I split out *that*\npart into another helper a while ago:\nhttps://github.com/avar/git/commit/55dda82a409\n\nWhich might be worthhile doing/stealing heere while we're at it,\ni.e. the flags checking has become quite ab ig part of\nparse_options_start_1(), or just leave it for later...\n\n>> > +\t\t\t\terror(_(\"unknown subcommand: %s\"), arg);\n>> \n>> s/%s/'%s'/ while we're at it, perhaps?\n>\n> Indeed, though it should be `%s', because that's what surrounds\n> unknown switches and options in the corresponding error messages.\n>\n>> > +\t\t\t\tusage_with_options(usagestr, options);\n>> > +\t\t\tcase PARSE_OPT_COMPLETE:\n>> > +\t\t\tcase PARSE_OPT_HELP:\n>> > +\t\t\tcase PARSE_OPT_ERROR:\n>> > +\t\t\tcase PARSE_OPT_DONE:\n>> > +\t\t\tcase PARSE_OPT_NON_OPTION:\n>> > +\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n>> \n>> nit: BUG(\"got bad %d\", v) or whatever, i.e. say what we got?\n>\n> All these are impossible, so I don't think it matters.  This is\n> another case of the compiler with '-Wswitch' complaining, and follows\n> suit of similar switch statements after the calls to parse_short_opt()\n> and parse_long_opt() functions.\n\n*nod*, and this one's a BUG(), which is good...\n\n>> > @@ -206,6 +217,11 @@ struct option {\n>> >  #define OPT_ALIAS(s, l, source_long_name) \\\n>> >  \t{ OPTION_ALIAS, (s), (l), (source_long_name) }\n>> >  \n>> > +#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n>> > +\t\t\t\t\tNULL, 0, NULL, 0, NULL, 0, (fn) }\n>> > +#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n>> > +\t\t\t\t\tNULL, (f), NULL, 0, NULL, 0, (fn) }\n>> \n>> Nit, I know you're carrying forward existing patterns, but since that\n>> all pre-dated designated init perhaps we could just (untested):\n>> \t\n>> \t#define OPT_SUBCOMMAND_F(l, v, fn, f) { \\\n>> \t\t.type = OPTION_SUBCOMMAND, \\\n>> \t\t.long_name = (l), \\\n>> \t\t.value = (v), \\\n>> \t\t.ll_callback = (fn), \\\n>> \t}\n>> \t#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)\n>> \n>> Which IMO is much nicer. I have some patches somewhere to convert these\n>> to saner patterns (I think not designated init, but the X() can be\n>> defined in terms of X_F() like that, but since this is new we can use\n>> designated init all the way...\n>\n> Oh, I love this idea!  But are we there yet?  I remember the weather\n> balloon about designated initializers, but I'm not sure whether we've\n> already made the decision to allow them.\n\nYes, we've got a thoroughly hard dependency on that part of C99 for a\nwhile now, and it's OK to add new ones (especially in cases like these,\nwhere it makes thigs easier to read).\n\n> If we do, then I'm inclined\n> to volunteer to clean up all those OPT_* macros in 'parse-options.h'\n> with designated initializers, \n\nSounds good, you might want to steal this & perhaps some things on the\nsame branch: https://github.com/avar/git/commit/a1a5e9c68c8\n\nI didn't convert them to designated init, but some macros are \"missing\",\nsome are needlessy copy/pasted when X_F() could be defined in terms of\nX() etc.\n\nFWIW I thought it would eventually make sense to rename the members of\nthe struct itself, so we'd e.g. just have a \"t\" and \"l\" name, so we\ncould use that inline instead of the OPT_*() (we could use the long\nnames too, but that would probably be too verbose).\n\nThat would allow adding optional arguments, which e.g. would be handy\nfor things like \"...and here's a list of what options this is\nincompatible with\".\n\n>\n>> > +{\n>> > +\tint i;\n>> \n>> Nit: missing \\n (usual style of variable decl);\n>\n> Hm, speaking of newer C features, what about 'for (int i = 0; ...)'?\n\nJunio says this November, but see:\nhttps://lore.kernel.org/git/220725.86zggxpfed.gmgdl@evledraar.gmail.com/\n\n>> > +\t\terror(\"'cmd' is mandatory\");\n>> > +\t\tusage_with_options(usage, test_flag_options);\n>> \n>> nit: I think you want usage_msg_optf() or usage_msg_opt().\n>\n> Maybe... but I don't know what they do ;)  Though I remember removing\n> a couple of similar error() and usage_with_options() pairs from the\n> builtin commands.\n\nIt's just helpers for \"usage_with_options, except with a message, e.g.:\n\n\t$ ./git cat-file a b c\n\tfatal: only two arguments allowed in <type> <object> mode, not 3\n\n\tusage: git cat-file <type> <object>\n\n"},{"id":"459925","messageId":"20220725210236.GC2543@szeder.dev","threadId":"58218","inReplyTo":"220725.86wnc1nf6c.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 09/20] parse-options: add support for parsing subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-25T21:02:36Z","receivedAt":"2022-07-25T21:02:45Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Jul 25, 2022 at 09:41:52PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> >> > +\t\terror(\"'cmd' is mandatory\");\n> >> > +\t\tusage_with_options(usage, test_flag_options);\n> >> \n> >> nit: I think you want usage_msg_optf() or usage_msg_opt().\n> >\n> > Maybe... but I don't know what they do ;)  Though I remember removing\n> > a couple of similar error() and usage_with_options() pairs from the\n> > builtin commands.\n> \n> It's just helpers for \"usage_with_options, except with a message, e.g.:\n> \n> \t$ ./git cat-file a b c\n> \tfatal: only two arguments allowed in <type> <object> mode, not 3\n> \n> \tusage: git cat-file <type> <object>\n\nI've looked them up in the meantime.  The problem is that they both\noutput the given message with a \"fatal:\" prefix, but option parsing\nrelated errors are usually prefixed with \"error:\".\n\n"},{"id":"459981","messageId":"1p04q351-9938-r0r7-snr6-9s8237s27459@tzk.qr","threadId":"58218","inReplyTo":"xmqq5yjl5b4y.fsf@gitster.g","subject":"Re: [PATCH 00/20] parse-options: handle subcommands","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2022-07-26T15:42:01Z","receivedAt":"2022-07-26T15:42:10Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Mon, 25 Jul 2022, Junio C Hamano wrote:\n\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n>\n> >   - builtin/bisect.c: after the conversion/rename from 'bisect--helper',\n> >     cmd_bisect() doesn't use parse-options anymore.  Take what's on 'seen'\n> >     to resolve the conflict.\n> >     Note that the conflicting topic should have marked cmd_bisect() with\n> >     the NO_PARSEOPT flag in 'git.c's command list.\n>\n> I was wondering about this one.  Does the new \"subcommand\" support\n> help implementing the dispatching to subcommands better?  If so it\n> may not be a bad idea to redo the cmd_bisect() on top of this series\n> once it proves to be solid.\n\nThe built-in `bisect` code carries around some local state, in a somewhat\nfutile attempt to keep things in a state that would be more amenable to\nlibifying.\n\nThe `subcommand` series does not accommodate for such a local state, the\nsignature `typedef int parse_opt_subcommand_fn(int argc, const char\n**argv, const char *prefix);` requires all pre-subcommand options to be\nparsed into global (or file-local, but not function-local) variables.\n\nThis implies that moving `cmd_bisect()` on top of the `subcommand` topic\nwould require the `bisect` code to become less libifyable, which is an\nundesirable direction.\n\nCiao,\nDscho\n"},{"id":"459991","messageId":"220726.86h733n3sd.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"1p04q351-9938-r0r7-snr6-9s8237s27459@tzk.qr","subject":"Re: [PATCH 00/20] parse-options: handle subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-26T18:02:32Z","receivedAt":"2022-07-26T18:10:34Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Jul 26 2022, Johannes Schindelin wrote:\n\n> Hi Junio,\n>\n> On Mon, 25 Jul 2022, Junio C Hamano wrote:\n>\n>> SZEDER Gábor <szeder.dev@gmail.com> writes:\n>>\n>> >   - builtin/bisect.c: after the conversion/rename from 'bisect--helper',\n>> >     cmd_bisect() doesn't use parse-options anymore.  Take what's on 'seen'\n>> >     to resolve the conflict.\n>> >     Note that the conflicting topic should have marked cmd_bisect() with\n>> >     the NO_PARSEOPT flag in 'git.c's command list.\n>>\n>> I was wondering about this one.  Does the new \"subcommand\" support\n>> help implementing the dispatching to subcommands better?  If so it\n>> may not be a bad idea to redo the cmd_bisect() on top of this series\n>> once it proves to be solid.\n>\n> The built-in `bisect` code carries around some local state, in a somewhat\n> futile attempt to keep things in a state that would be more amenable to\n> libifying.\n>\n> The `subcommand` series does not accommodate for such a local state, the\n> signature `typedef int parse_opt_subcommand_fn(int argc, const char\n> **argv, const char *prefix);` requires all pre-subcommand options to be\n> parsed into global (or file-local, but not function-local) variables.\n>\n> This implies that moving `cmd_bisect()` on top of the `subcommand` topic\n> would require the `bisect` code to become less libifyable, which is an\n> undesirable direction.\n\nWhat are you referring to here specifically?\n\nThe commands in the builtin/bisect.c take a \"struct bisect_terms\", but\nas far as I can tell we could simply move setting that up into the\nsub-command callbacks.\n\nSo cmd_bisect() only needs to parse_options() enough to figure out that\nthe first argument is e.g. \"start\", then call bisect_start(), which will\ndo its own parse_options() & setup of the rest.\n\nBut I could see how we'd in general have a need to carry state from the\ncmd_name() to the cmd_name_subcmd(). Such a thing doesn't seem like a\nbig change to SZEDER's patches here, we'd support functions that take a\nvoid *, similar to how we support two types of \"callback\" in the \"struct\noption\" itself.\n\nOr, you could have perfectly lib-ified code where your\ncmd_name_subcmd(int argc, char **argv, char *prefix) would be a one-line\nwrapper for another function taking an extra argument.\n\nYou'd use a global only to ferry state between the cmd_name() and that\ncmd_name_subcmd(), which would be some boilerplate, but it wouldn't be\nprohibitive.\n\nWhen used as a library the API would probably want to take a struct, and\nnot an argc/argv/prefix. I don't see how having just that part of the\ncommand callback handling needing one global would close the door on\nanything else...\n"},{"id":"460000","messageId":"20220726195516.GA2264@szeder.dev","threadId":"58218","inReplyTo":"20220725123857.2773963-2-szeder.dev@gmail.com","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-07-26T19:55:16Z","receivedAt":"2022-07-26T19:55:28Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Jul 25, 2022 at 02:38:38PM +0200, SZEDER Gábor wrote:\n> Our Bash completion script can complete --options for commands using\n> parse-options even when that command doesn't have a dedicated\n> completion function, but to do so the completion script must know\n> which commands use parse-options and which don't.  Therefore, commands\n> not using parse-options are marked in 'git.c's command list with the\n> NO_PARSEOPT flag.\n> \n> Update this list, and remove this flag from the commands that by now\n> use parse-options.\n> \n> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> ---\n>  git.c | 12 ++++++------\n>  1 file changed, 6 insertions(+), 6 deletions(-)\n> \n> diff --git a/git.c b/git.c\n> index e5d62fa5a9..c4282f194a 100644\n> --- a/git.c\n> +++ b/git.c\n\n> @@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {\n>  \t{ \"verify-tag\", cmd_verify_tag, RUN_SETUP },\n>  \t{ \"version\", cmd_version },\n>  \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP },\n> -\t{ \"worktree\", cmd_worktree, RUN_SETUP | NO_PARSEOPT },\n> +\t{ \"worktree\", cmd_worktree, RUN_SETUP },\n\nThis hunk is wrong, 'worktree' does not use parse_options().  I was\nfooled by seeing its (empty) 'struct option' array, thinking that then\nit must surely use parse_options(), but it only uses that options\narray to pass it to usage_with_options().\n\nThis NO_PARSEOPT flag should be removed in the last patch of the\nseries, when I convert cmd_worktree() to handle its subcommands with\nparse_options().\n\n>  \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n>  };\n>  \n> -- \n> 2.37.1.633.g6a0fa73e39\n> \n"},{"id":"460498","messageId":"20220802173754.GA10040@szeder.dev","threadId":"58218","inReplyTo":"220725.86mtcxp8ib.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-02T17:37:54Z","receivedAt":"2022-08-02T17:38:02Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Jul 25, 2022 at 04:31:40PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Mon, Jul 25 2022, SZEDER Gábor wrote:\n> \n> > Our Bash completion script can complete --options for commands using\n> > parse-options even when that command doesn't have a dedicated\n> > completion function, but to do so the completion script must know\n> > which commands use parse-options and which don't.  Therefore, commands\n> > not using parse-options are marked in 'git.c's command list with the\n> > NO_PARSEOPT flag.\n> >\n> > Update this list, and remove this flag from the commands that by now\n> > use parse-options.\n> >\n> > Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> > ---\n> >  git.c | 12 ++++++------\n> >  1 file changed, 6 insertions(+), 6 deletions(-)\n> >\n> > diff --git a/git.c b/git.c\n> > index e5d62fa5a9..c4282f194a 100644\n> > --- a/git.c\n> > +++ b/git.c\n> > @@ -489,14 +489,14 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)\n> >  static struct cmd_struct commands[] = {\n> >  \t{ \"add\", cmd_add, RUN_SETUP | NEED_WORK_TREE },\n> >  \t{ \"am\", cmd_am, RUN_SETUP | NEED_WORK_TREE },\n> > -\t{ \"annotate\", cmd_annotate, RUN_SETUP | NO_PARSEOPT },\n> > +\t{ \"annotate\", cmd_annotate, RUN_SETUP },\n> >  \t{ \"apply\", cmd_apply, RUN_SETUP_GENTLY },\n> >  \t{ \"archive\", cmd_archive, RUN_SETUP_GENTLY },\n> >  \t{ \"bisect--helper\", cmd_bisect__helper, RUN_SETUP },\n> >  \t{ \"blame\", cmd_blame, RUN_SETUP },\n> >  \t{ \"branch\", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },\n> >  \t{ \"bugreport\", cmd_bugreport, RUN_SETUP_GENTLY },\n> > -\t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },\n> > +\t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY },\n> >  \t{ \"cat-file\", cmd_cat_file, RUN_SETUP },\n> >  \t{ \"check-attr\", cmd_check_attr, RUN_SETUP },\n> >  \t{ \"check-ignore\", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },\n> > @@ -514,7 +514,7 @@ static struct cmd_struct commands[] = {\n> >  \t{ \"column\", cmd_column, RUN_SETUP_GENTLY },\n> >  \t{ \"commit\", cmd_commit, RUN_SETUP | NEED_WORK_TREE },\n> >  \t{ \"commit-graph\", cmd_commit_graph, RUN_SETUP },\n> > -\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP | NO_PARSEOPT },\n> > +\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP },\n> >  \t{ \"config\", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },\n> >  \t{ \"count-objects\", cmd_count_objects, RUN_SETUP },\n> >  \t{ \"credential\", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },\n> > @@ -553,7 +553,7 @@ static struct cmd_struct commands[] = {\n> >  \t{ \"ls-files\", cmd_ls_files, RUN_SETUP },\n> >  \t{ \"ls-remote\", cmd_ls_remote, RUN_SETUP_GENTLY },\n> >  \t{ \"ls-tree\", cmd_ls_tree, RUN_SETUP },\n> > -\t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },\n> > +\t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY },\n> >  \t{ \"mailsplit\", cmd_mailsplit, NO_PARSEOPT },\n> >  \t{ \"maintenance\", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },\n> >  \t{ \"merge\", cmd_merge, RUN_SETUP | NEED_WORK_TREE },\n> > @@ -566,7 +566,7 @@ static struct cmd_struct commands[] = {\n> >  \t{ \"merge-recursive-theirs\", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },\n> >  \t{ \"merge-subtree\", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },\n> >  \t{ \"merge-tree\", cmd_merge_tree, RUN_SETUP },\n> > -\t{ \"mktag\", cmd_mktag, RUN_SETUP | NO_PARSEOPT },\n> > +\t{ \"mktag\", cmd_mktag, RUN_SETUP },\n> >  \t{ \"mktree\", cmd_mktree, RUN_SETUP },\n> >  \t{ \"multi-pack-index\", cmd_multi_pack_index, RUN_SETUP },\n> >  \t{ \"mv\", cmd_mv, RUN_SETUP | NEED_WORK_TREE },\n> > @@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {\n> >  \t{ \"verify-tag\", cmd_verify_tag, RUN_SETUP },\n> >  \t{ \"version\", cmd_version },\n> >  \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP },\n> > -\t{ \"worktree\", cmd_worktree, RUN_SETUP | NO_PARSEOPT },\n> > +\t{ \"worktree\", cmd_worktree, RUN_SETUP },\n> >  \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n> >  };\n> \n> I have a superset of this change in my local tree:\n> https://github.com/avar/git/commit/f0076de17fd\n> \n> I.e. \"parseopt\" here really means \"handles --git-completion-helper\"\n\nIndeed, with additional subtleties, like it doesn't try to read from\nstdin before handling '--git-completion-helper'.\n\n> so\n> we can also address the diff family here. See the test that's associated\n> with the change (which you might want to grab in any case, i.e. to have\n> tests in this area).\n\nDunno.  I don't like this NO_PARSEOPT thing, and instead of testing it\nI'm thinking about removing it altogether.  It seems to be doable, but\nof course conflicts with the upcoming 'completion-subcommand' patch\nseries, so I'll leave it for later.\n\n  https://github.com/szeder/git/commits/completion-poc-builtins\n\n"},{"id":"460503","messageId":"xmqqsfmefjja.fsf@gitster.g","threadId":"58218","inReplyTo":"20220802173754.GA10040@szeder.dev","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-08-02T21:00:09Z","receivedAt":"2022-08-02T21:00:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> Dunno.  I don't like this NO_PARSEOPT thing, and instead of testing it\n> I'm thinking about removing it altogether.\n\nSorry if this is obvious to the others, but I am confused here.\n\nLack of NO_PARSEOPT bit is used as a mark to say \"this subcommand\ntakes '--git-completion-helper' option to help listing the options,\nso include it in the 'git --list-cmds=parseopt' output\", right?  I\ndo not mind removing an unused or unnecessary bit at all, but what\nis your plan of getting rid of the bit?  Will we make sure everybody\nsupports the \"--git-completion-helper\" option?\n\n"},{"id":"460545","messageId":"220803.86czdhfos0.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"xmqqsfmefjja.fsf@gitster.g","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-03T13:11:13Z","receivedAt":"2022-08-03T13:19:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Aug 02 2022, Junio C Hamano wrote:\n\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n>\n>> Dunno.  I don't like this NO_PARSEOPT thing, and instead of testing it\n>> I'm thinking about removing it altogether.\n>\n> Sorry if this is obvious to the others, but I am confused here.\n>\n> Lack of NO_PARSEOPT bit is used as a mark to say \"this subcommand\n> takes '--git-completion-helper' option to help listing the options,\n> so include it in the 'git --list-cmds=parseopt' output\", right?  I\n> do not mind removing an unused or unnecessary bit at all, but what\n> is your plan of getting rid of the bit?  Will we make sure everybody\n> supports the \"--git-completion-helper\" option?\n\nWe could:\n\n * Handle it in git-completion.bash itself, e.g. send-email supports\n   --git-completion-helper but isn't a built-in, the git-completion.bash\n   itself knows it can be invoked with it.\n\n * We could just try to run the command with --git-comletion-helper, and\n   fall back if it returns 128 or 129. AFAICT the only ones we'd need to\n   handle specially would be rev-list (returns 0) and fast-import (will\n   hang, expecting stdin input).\n\nThe non-parse_options() supporting ones are rare enough these days (and\nwe've been ~quickly converting them) that perhaps an on-the-fly check\nwould be fine (and we cache the result, as with most/all other such\nchecks in git-completion.bsah).\n"},{"id":"460575","messageId":"20220803213430.GB10040@szeder.dev","threadId":"58218","inReplyTo":"xmqqsfmefjja.fsf@gitster.g","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-03T21:34:30Z","receivedAt":"2022-08-03T21:34:48Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Tue, Aug 02, 2022 at 02:00:09PM -0700, Junio C Hamano wrote:\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n> \n> > Dunno.  I don't like this NO_PARSEOPT thing, and instead of testing it\n> > I'm thinking about removing it altogether.\n> \n> Sorry if this is obvious to the others, but I am confused here.\n> \n> Lack of NO_PARSEOPT bit is used as a mark to say \"this subcommand\n> takes '--git-completion-helper' option to help listing the options,\n> so include it in the 'git --list-cmds=parseopt' output\", right?  I\n> do not mind removing an unused or unnecessary bit at all, but what\n> is your plan of getting rid of the bit?  Will we make sure everybody\n> supports the \"--git-completion-helper\" option?\n\nA bit of clarification first:  This NO_PARSEOPT flag only matters for\ngit commands for which we don't have a corresponding _git_cmd()\ncompletion function in our Bash completion script.  If there is such a\nfunction, then either:\n\n  - that function has a hardcoded list of --options, for commands that\n    don't use parse-options, or\n\n  - it invokes '__gitcomp_builtin cmd' to complete --options with the\n    help of '--git-completion-helper'.  Note that this works even if\n    said 'cmd' were marked with the NO_PARSEOPT flag, moreover,\n    'cmd' doesn't even have to be a builtin, i.e. 'git send-email\n    --git-completion-helper' works as Ævar pointed out.\n\nFor the many builtin (mostly plumbing) commands that do use\nparse-options but don't have such a _git_cmd() completion function, we\nadded the generic fallback __git_complete_common() to complete their\noptions with '__gitcomp_builtin cmd'.  And this is where the\nNO_PARSEOPT flag matters, because the completion script only ever\ninvokes that fallback function for commands that are not marked with\nthat flag.  See 9f642a7169 (completion: add --option completion for\nmost builtin commands, 2018-03-24) for more details.\n\nSo far so good.\n\nNow, instead of relying on (possibly outdated) NO_PARSEOPT markings\ntelling us not to use that fallback function, i.e. not invoke certain\ncommands with '--git-completion-helper', I'm considering whether we\ncould rely on our git commands behaving sensibly when invoked with\n'--git-completion-helper'.  I.e. a git command either supports that\noption and lists all its --options, or fails fast because of the\nunknown option without printing anything to stdout.  Then perhaps the\ncompletion script doesn't even have to pay attention to this error,\nbecause it will do the right thing as long as the\n\n  options=$(git builtin --git-completion-helper)\n\ncommand fills $options with the list of --options or leaves it empty.\n\nAFAICT almost all builtins behave sensibly in this sense (I haven't\nlooked at non-builtin git commands yet), with the exception of:\n\n  - 'git credential' and 'fast-import': these two attempt to read from\n    stdin before recognizing the unknown --git-completion-helper\n    option and hang.  On one hand, this hang can be easily prevented\n    by redirecting stdin from /dev/null (but 'fast-import' leaves\n    behind a crash file, which is not nice).  OTOH, I'm inclined to\n    consider this behavior as a bug: all commands should fail fast on\n    unknown --options.\n\n  - 'git rev-parse --git-completion-helper' prints\n    '--git-completion-helper' to stdout.  I forgot about this, but\n    this is how this command is supposed to behave.  I guess I'll just\n    have to special case this command in the completion script...\n\nOf course we can only do this with git builtin commands, because those\nare under our control and we can make sure that they behave sensibly.\nWe must not pass '--git-completion-helper' to any 'git-foo' command\nthat might be present in $PATH, because who knows what they might do.\n\nHowever, I'll definitely need to think more about corner cases, e.g.\nhitting 'git cmd --<TAB>' while outside a repository doesn't even\nreaches parse_options() if that command requires a repository.  But\nlet's leave that for later and get this series in shape first.\n\n"},{"id":"460605","messageId":"220804.86edxwe9fm.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220803213430.GB10040@szeder.dev","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-04T07:47:33Z","receivedAt":"2022-08-04T07:48:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 03 2022, SZEDER Gábor wrote:\n\n> [...]\n> hitting 'git cmd --<TAB>' while outside a repository doesn't even\n> reaches parse_options() if that command requires a repository.  But\n> let's leave that for later and get this series in shape first.\n\nHrm, I wonder how that interacts with the git-completion.bash caching in\na bad way, i.e. if we were to emit options in a non-repo, then the user\ncd's to a repo and tab-completes...\n"},{"id":"461101","messageId":"xmqq8rnuea60.fsf@gitster.g","threadId":"58218","inReplyTo":"20220803213430.GB10040@szeder.dev","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-08-11T21:35:03Z","receivedAt":"2022-08-11T21:35:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n[elided here is a good discussion on how to (re)do command line\ncompletion without NO_PARSEOPT markings]\n\n> ...\n> However, I'll definitely need to think more about corner cases, e.g.\n> hitting 'git cmd --<TAB>' while outside a repository doesn't even\n> reaches parse_options() if that command requires a repository.  But\n> let's leave that for later and get this series in shape first.\n\nYes.  I thought this series was mostly ready, except for a minor\nnits in the way things are explained in the proposed log message\nin [09/20].  How would we want to proceed from here?\n\nThanks.\n\n"},{"id":"461118","messageId":"20220812150420.GA3790@szeder.dev","threadId":"58218","inReplyTo":"220725.86edy9p85i.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-12T15:04:20Z","receivedAt":"2022-08-12T15:04:27Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Jul 25, 2022 at 04:38:48PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Mon, Jul 25 2022, SZEDER Gábor wrote:\n> \n> > In 't0040-parse-options.sh' we thoroughly test the parsing of all\n> > types and forms of options, but in all those tests parse_options() is\n> > always invoked with a 0 flags parameter.\n> >\n> > Add a few tests to demonstrate how various 'enum parse_opt_flags'\n> > values are supposed to influence option parsing.\n> >\n> > Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> > ---\n> >  t/helper/test-parse-options.c | 61 ++++++++++++++++++++++++++++++\n> >  t/helper/test-tool.c          |  1 +\n> >  t/helper/test-tool.h          |  1 +\n> >  t/t0040-parse-options.sh      | 70 +++++++++++++++++++++++++++++++++++\n> >  4 files changed, 133 insertions(+)\n> >\n> > diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\n> > index 48d3cf6692..32b906bd6a 100644\n> > --- a/t/helper/test-parse-options.c\n> > +++ b/t/helper/test-parse-options.c\n> > @@ -192,3 +192,64 @@ int cmd__parse_options(int argc, const char **argv)\n> >  \n> >  \treturn ret;\n> >  }\n> > +\n> > +static int parse_options_flags__cmd(int argc, const char **argv,\n> > +\t\t\t\t    enum parse_opt_flags test_flags)\n> > +{\n> > +\tconst char *usage[] = {\n> > +\t\t\"<...> cmd [options]\",\n> > +\t\tNULL\n> > +\t};\n> > +\tint opt = 0;\n> > +\tconst struct option options[] = {\n> > +\t\tOPT_INTEGER('o', \"opt\", &opt, \"an integer option\"),\n> > +\t\tOPT_END()\n> > +\t};\n> > +\n> > +\targc = parse_options(argc, argv, NULL, options, usage, test_flags);\n> > +\n> > +\tprintf(\"opt: %d\\n\", opt);\n> > +\tfor (int i = 0; i < argc; i++)\n> > +\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +static enum parse_opt_flags test_flags = 0;\n> > +static const struct option test_flag_options[] = {\n> > +\tOPT_GROUP(\"flag-options:\"),\n> > +\tOPT_BIT(0, \"keep-dashdash\", &test_flags,\n> > +\t\t\"pass PARSE_OPT_KEEP_DASHDASH to parse_options()\",\n> > +\t\tPARSE_OPT_KEEP_DASHDASH),\n> > +\tOPT_BIT(0, \"stop-at-non-option\", &test_flags,\n> > +\t\t\"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()\",\n> > +\t\tPARSE_OPT_STOP_AT_NON_OPTION),\n> > +\tOPT_BIT(0, \"keep-argv0\", &test_flags,\n> > +\t\t\"pass PARSE_OPT_KEEP_ARGV0 to parse_options()\",\n> > +\t\tPARSE_OPT_KEEP_ARGV0),\n> > +\tOPT_BIT(0, \"keep-unknown\", &test_flags,\n> > +\t\t\"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()\",\n> > +\t\tPARSE_OPT_KEEP_UNKNOWN),\n> > +\tOPT_BIT(0, \"no-internal-help\", &test_flags,\n> > +\t\t\"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()\",\n> > +\t\tPARSE_OPT_NO_INTERNAL_HELP),\n> > +\tOPT_END()\n> > +};\n> > +\n> > +int cmd__parse_options_flags(int argc, const char **argv)\n> > +{\n> > +\tconst char *usage[] = {\n> > +\t\t\"test-tool parse-options-flags [flag-options] cmd [options]\",\n> > +\t\tNULL\n> > +\t};\n> > +\n> > +\targc = parse_options(argc, argv, NULL, test_flag_options, usage,\n> > +\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n> > +\n> > +\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n> > +\t\terror(\"'cmd' is mandatory\");\n> > +\t\tusage_with_options(usage, test_flag_options);\n> > +\t}\n> > +\n> > +\treturn parse_options_flags__cmd(argc, argv, test_flags);\n> > +}\n> > diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\n> > index 318fdbab0c..6e62282b60 100644\n> > --- a/t/helper/test-tool.c\n> > +++ b/t/helper/test-tool.c\n> > @@ -51,6 +51,7 @@ static struct test_cmd cmds[] = {\n> >  \t{ \"online-cpus\", cmd__online_cpus },\n> >  \t{ \"pack-mtimes\", cmd__pack_mtimes },\n> >  \t{ \"parse-options\", cmd__parse_options },\n> > +\t{ \"parse-options-flags\", cmd__parse_options_flags },\n> >  \t{ \"parse-pathspec-file\", cmd__parse_pathspec_file },\n> >  \t{ \"partial-clone\", cmd__partial_clone },\n> >  \t{ \"path-utils\", cmd__path_utils },\n> \n> I wanted to add some new parse_options() code to\n> t/helper/test-parse-options.c in the past, but was stymied by its\n> cmd_*() going through a singular parse_options().\n> \n> So just creating a new callback is a neat solution.\n> \n> But wouldn't it be better to just create a new\n> t/helper/test-parse-options-something-.c & test file? It seems this\n> doesn't really share anything with the current helper & tests...\n\nWell, at least they share the concept, as they all test parse-options.\nAnd 'parse-options-flags' and 'parse-subcommands' do share the options\narray to specify the various parse_opt_flags.\n\nA new test script for these flags and/or for subcommands would\ndefinitely be worse, IMO.  I've found it very convenient that whenever\nI updated 'parse-options.{c,h}', I only needed to run a single\n'./t0040-parse-options.sh' script to check my changes.\n\n"},{"id":"461119","messageId":"20220812151513.GB3790@szeder.dev","threadId":"58218","inReplyTo":"220725.86wnc1nf6c.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 09/20] parse-options: add support for parsing subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-12T15:15:13Z","receivedAt":"2022-08-12T15:16:25Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Jul 25, 2022 at 09:41:52PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> >> > @@ -206,6 +217,11 @@ struct option {\n> >> >  #define OPT_ALIAS(s, l, source_long_name) \\\n> >> >  \t{ OPTION_ALIAS, (s), (l), (source_long_name) }\n> >> >  \n> >> > +#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n> >> > +\t\t\t\t\tNULL, 0, NULL, 0, NULL, 0, (fn) }\n> >> > +#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n> >> > +\t\t\t\t\tNULL, (f), NULL, 0, NULL, 0, (fn) }\n> >> \n> >> Nit, I know you're carrying forward existing patterns, but since that\n> >> all pre-dated designated init perhaps we could just (untested):\n> >> \t\n> >> \t#define OPT_SUBCOMMAND_F(l, v, fn, f) { \\\n> >> \t\t.type = OPTION_SUBCOMMAND, \\\n> >> \t\t.long_name = (l), \\\n> >> \t\t.value = (v), \\\n> >> \t\t.ll_callback = (fn), \\\n> >> \t}\n> >> \t#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)\n> >> \n> >> Which IMO is much nicer. I have some patches somewhere to convert these\n> >> to saner patterns (I think not designated init, but the X() can be\n> >> defined in terms of X_F() like that, but since this is new we can use\n> >> designated init all the way...\n> >\n> > Oh, I love this idea!  But are we there yet?  I remember the weather\n> > balloon about designated initializers, but I'm not sure whether we've\n> > already made the decision to allow them.\n> \n> Yes, we've got a thoroughly hard dependency on that part of C99 for a\n> while now, and it's OK to add new ones (especially in cases like these,\n> where it makes thigs easier to read).\n\nGood.  I updated this hunk to use designated initializers as you\nsuggested, because all those unused 0/NULL fields in there are just...\nugly.\n\n> > If we do, then I'm inclined\n> > to volunteer to clean up all those OPT_* macros in 'parse-options.h'\n> > with designated initializers,\n\nBut I'll leave this for later, because it's awfully easy to make a\nmistake and assign a macro parameter to the wrong field, and I find\nthe resulting diff very hard to review.\n\n"},{"id":"461120","messageId":"20220812152837.GC3790@szeder.dev","threadId":"58218","inReplyTo":"xmqq8rnuea60.fsf@gitster.g","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-12T15:28:37Z","receivedAt":"2022-08-12T15:28:44Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Thu, Aug 11, 2022 at 02:35:03PM -0700, Junio C Hamano wrote:\n> Yes.  I thought this series was mostly ready, except for a minor\n> nits in the way things are explained in the proposed log message\n> in [09/20].  How would we want to proceed from here?\n\nPlease wait for the reroll.  v2 is getting along, ever so slowly, I\nthink I'll be able to submit it over the weekend.  Thanks.\n\n"},{"id":"461125","messageId":"xmqqsfm1csux.fsf@gitster.g","threadId":"58218","inReplyTo":"20220812152837.GC3790@szeder.dev","subject":"Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-08-12T16:46:30Z","receivedAt":"2022-08-12T16:46:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> On Thu, Aug 11, 2022 at 02:35:03PM -0700, Junio C Hamano wrote:\n>> Yes.  I thought this series was mostly ready, except for a minor\n>> nits in the way things are explained in the proposed log message\n>> in [09/20].  How would we want to proceed from here?\n>\n> Please wait for the reroll.  v2 is getting along, ever so slowly, I\n> think I'll be able to submit it over the weekend.  Thanks.\n\nOK.  The original round looked mostly good to me overall and I was\nwondering what the current status was.  Will wait.  No rush.\n\nThanks.\n"},{"id":"461558","messageId":"20220819160411.1791200-10-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 09/20] parse-options: add support for parsing subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:00Z","receivedAt":"2022-08-19T16:32:40Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Several Git commands have subcommands to implement mutually exclusive\n\"operation modes\", and they usually parse their subcommand argument\nwith a bunch of if-else if statements.\n\nTeach parse-options to handle subcommands as well, which will result\nin shorter and simpler code with consistent error handling and error\nmessages on unknown or missing subcommand, and it will also make\npossible for our Bash completion script to handle subcommands\nprogrammatically.\n\nThe approach is guided by the following observations:\n\n  - Most subcommands [1] are implemented in dedicated functions, and\n    most of those functions [2] either have a signature matching the\n    'int cmd_foo(int argc, const char **argc, const char *prefix)'\n    signature of builtin commands or can be trivially converted to\n    that signature, because they miss only that last prefix parameter\n    or have no parameters at all.\n\n  - Subcommand arguments only have long form, and they have no double\n    dash prefix, no negated form, and no description, and they don't\n    take any arguments, and can't be abbreviated.\n\n  - There must be exactly one subcommand among the arguments, or zero\n    if the command has a default operation mode.\n\n  - All arguments following the subcommand are considered to be\n    arguments of the subcommand, and, conversely, arguments meant for\n    the subcommand may not preceed the subcommand.\n\nSo in the end subcommand declaration and parsing would look something\nlike this:\n\n    parse_opt_subcommand_fn *fn = NULL;\n    struct option builtin_commit_graph_options[] = {\n        OPT_STRING(0, \"object-dir\", &opts.obj_dir, N_(\"dir\"),\n                   N_(\"the object directory to store the graph\")),\n        OPT_SUBCOMMAND(\"verify\", &fn, graph_verify),\n        OPT_SUBCOMMAND(\"write\", &fn, graph_write),\n        OPT_END(),\n    };\n    argc = parse_options(argc, argv, prefix, options,\n                         builtin_commit_graph_usage, 0);\n    return fn(argc, argv, prefix);\n\nHere each OPT_SUBCOMMAND specifies the name of the subcommand and the\nfunction implementing it, and the address of the same 'fn' subcommand\nfunction pointer.  parse_options() then processes the arguments until\nit finds the first argument matching one of the subcommands, sets 'fn'\nto the function associated with that subcommand, and returns, leaving\nthe rest of the arguments unprocessed.  If none of the listed\nsubcommands is found among the arguments, parse_options() will show\nusage and abort.\n\nIf a command has a default operation mode, 'fn' should be initialized\nto the function implementing that mode, and parse_options() should be\ninvoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.  In this case\nparse_options() won't error out when not finding any subcommands, but\nwill return leaving 'fn' unchanged.  Note that if that default\noperation mode has any --options, then the PARSE_OPT_KEEP_UNKNOWN_OPT\nflag is necessary as well (otherwise parse_options() would error out\nupon seeing the unknown option meant to the default operation mode).\n\nSome thoughts about the implementation:\n\n  - The same pointer to 'fn' must be specified as 'value' for each\n    OPT_SUBCOMMAND, because there can be only one set of mutually\n    exclusive subcommands; parse_options() will BUG() otherwise.\n\n    There are other ways to tell parse_options() where to put the\n    function associated with the subcommand given on the command line,\n    but I didn't like them:\n\n      - Change parse_options()'s signature by adding a pointer to\n        subcommand function to be set to the function associated with\n        the given subcommand, affecting all callsites, even those that\n        don't have subcommands.\n\n      - Introduce a specific parse_options_and_subcommand() variant\n        with that extra funcion parameter.\n\n  - I decided against automatically calling the subcommand function\n    from within parse_options(), because:\n\n      - There are commands that have to perform additional actions\n        after option parsing but before calling the function\n        implementing the specified subcommand.\n\n      - The return code of the subcommand is usually the return code\n        of the git command, but preserving the return code of the\n        automatically called subcommand function would have made the\n        API awkward.\n\n  - Also add a OPT_SUBCOMMAND_F() variant to allow specifying an\n    option flag: we have two subcommands that are purposefully\n    excluded from completion ('git remote rm' and 'git stash save'),\n    so they'll have to be specified with the PARSE_OPT_NOCOMPLETE\n    flag.\n\n  - Some of the 'parse_opt_flags' don't make sense with subcommands,\n    and using them is probably just an oversight or misunderstanding.\n    Therefore parse_options() will BUG() when invoked with any of the\n    following flags while the options array contains at least one\n    OPT_SUBCOMMAND:\n\n      - PARSE_OPT_KEEP_DASHDASH: parse_options() stops parsing\n        arguments when encountering a \"--\" argument, so it doesn't\n        make sense to expect and keep one before a subcommand, because\n        it would prevent the parsing of the subcommand.\n\n        However, this flag is allowed in combination with the\n        PARSE_OPT_SUBCOMMAND_OPTIONAL flag, because the double dash\n        might be meaningful for the command's default operation mode,\n        e.g. to disambiguate refs and pathspecs.\n\n      - PARSE_OPT_STOP_AT_NON_OPTION: As its name suggests, this flag\n        tells parse_options() to stop as soon as it encouners a\n        non-option argument, but subcommands are by definition not\n        options...  so how could they be parsed, then?!\n\n      - PARSE_OPT_KEEP_UNKNOWN: This flag can be used to collect any\n        unknown --options and then pass them to a different command or\n        subsystem.  Surely if a command has subcommands, then this\n        functionality should rather be delegated to one of those\n        subcommands, and not performed by the command itself.\n\n        However, this flag is allowed in combination with the\n        PARSE_OPT_SUBCOMMAND_OPTIONAL flag, making possible to pass\n        --options to the default operation mode.\n\n  - If the command with subcommands has a default operation mode, then\n    all arguments to the command must preceed the arguments of the\n    subcommand.\n\n    AFAICT we don't have any commands where this makes a difference,\n    because in those commands either only the command accepts any\n    arguments ('notes' and 'remote'), or only the default subcommand\n    ('reflog' and 'stash'), but never both.\n\n  - The 'argv' array passed to subcommand functions currently starts\n    with the name of the subcommand.  Keep this behavior.  AFAICT no\n    subcommand functions depend on the actual content of 'argv[0]',\n    but the parse_options() call handling their options expects that\n    the options start at argv[1].\n\n  - To support handling subcommands programmatically in our Bash\n    completion script, 'git cmd --git-completion-helper' will now list\n    both subcommands and regular --options, if any.  This means that\n    the completion script will have to separate subcommands (i.e.\n    words without a double dash prefix) from --options on its own, but\n    that's rather easy to do, and it's not much work either, because\n    the number of subcommands a command might have is rather low, and\n    those commands accept only a single --option or none at all.  An\n    alternative would be to introduce a separate option that lists\n    only subcommands, but then the completion script would need not\n    one but two git invocations and command substitutions for commands\n    with subcommands.\n\n    Note that this change doesn't affect the behavior of our Bash\n    completion script, because when completing the --option of a\n    command with subcommands, e.g. for 'git notes --<TAB>', then all\n    subcommands will be filtered out anyway, as none of them will\n    match the word to be completed starting with that double dash\n    prefix.\n\n[1] Except 'git rerere', because many of its subcommands are\n    implemented in the bodies of the if-else if statements parsing the\n    command's subcommand argument.\n\n[2] Except 'credential', 'credential-store' and 'fsmonitor--daemon',\n    because some of the functions implementing their subcommands take\n    special parameters.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n Documentation/technical/api-parse-options.txt |  41 +++-\n builtin/blame.c                               |   1 +\n builtin/shortlog.c                            |   1 +\n parse-options.c                               | 113 ++++++++++-\n parse-options.h                               |  24 ++-\n t/helper/test-parse-options.c                 |  61 ++++++\n t/helper/test-tool.c                          |   1 +\n t/helper/test-tool.h                          |   1 +\n t/t0040-parse-options.sh                      | 185 ++++++++++++++++++\n 9 files changed, 419 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 4412377fa3..c2a5e42914 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -8,7 +8,8 @@ Basics\n ------\n \n The argument vector `argv[]` may usually contain mandatory or optional\n-'non-option arguments', e.g. a filename or a branch, and 'options'.\n+'non-option arguments', e.g. a filename or a branch, 'options', and\n+'subcommands'.\n Options are optional arguments that start with a dash and\n that allow to change the behavior of a command.\n \n@@ -48,6 +49,33 @@ The parse-options API allows:\n   option, e.g. `-a -b --option -- --this-is-a-file` indicates that\n   `--this-is-a-file` must not be processed as an option.\n \n+Subcommands are special in a couple of ways:\n+\n+* Subcommands only have long form, and they have no double dash prefix, no\n+  negated form, and no description, and they don't take any arguments, and\n+  can't be abbreviated.\n+\n+* There must be exactly one subcommand among the arguments, or zero if the\n+  command has a default operation mode.\n+\n+* All arguments following the subcommand are considered to be arguments of\n+  the subcommand, and, conversely, arguments meant for the subcommand may\n+  not preceed the subcommand.\n+\n+Therefore, if the options array contains at least one subcommand and\n+`parse_options()` encounters the first dashless argument, it will either:\n+\n+* stop and return, if that dashless argument is a known subcommand, setting\n+  `value` to the function pointer associated with that subcommand, storing\n+  the name of the subcommand in argv[0], and leaving the rest of the\n+  arguments unprocessed, or\n+\n+* stop and return, if it was invoked with the `PARSE_OPT_SUBCOMMAND_OPTIONAL`\n+  flag and that dashless argument doesn't match any subcommands, leaving\n+  `value` unchanged and the rest of the arguments unprocessed, or\n+\n+* show error and usage, and abort.\n+\n Steps to parse options\n ----------------------\n \n@@ -110,6 +138,13 @@ Flags are the bitwise-or of:\n \tturns it off and allows one to add custom handlers for these\n \toptions, or to just leave them unknown.\n \n+`PARSE_OPT_SUBCOMMAND_OPTIONAL`::\n+\tDon't error out when no subcommand is specified.\n+\n+Note that `PARSE_OPT_STOP_AT_NON_OPTION` is incompatible with subcommands;\n+while `PARSE_OPT_KEEP_DASHDASH` and `PARSE_OPT_KEEP_UNKNOWN_OPT` can only be\n+used with subcommands when combined with `PARSE_OPT_SUBCOMMAND_OPTIONAL`.\n+\n Data Structure\n --------------\n \n@@ -241,7 +276,11 @@ There are some macros to easily define options:\n \tcan be given by the user. `int_var` is set to `enum_val` when the\n \toption is used, but an error is reported if other \"operating mode\"\n \toption has already set its value to the same `int_var`.\n+\tIn new commands consider using subcommands instead.\n \n+`OPT_SUBCOMMAND(long, &fn_ptr, subcommand_fn)`::\n+\tDefine a subcommand.  `subcommand_fn` is put into `fn_ptr` when\n+\tthis subcommand is used.\n \n The last element of the array must be `OPT_END()`.\n \ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 02e39420b6..a9fe8cf7a6 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n \t\tcase PARSE_OPT_ERROR:\n+\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\texit(129);\n \t\tcase PARSE_OPT_COMPLETE:\n \t\t\texit(0);\ndiff --git a/builtin/shortlog.c b/builtin/shortlog.c\nindex 086dfee45a..7a1e1fe7c0 100644\n--- a/builtin/shortlog.c\n+++ b/builtin/shortlog.c\n@@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n \t\tcase PARSE_OPT_ERROR:\n+\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\texit(129);\n \t\tcase PARSE_OPT_COMPLETE:\n \t\t\texit(0);\ndiff --git a/parse-options.c b/parse-options.c\nindex 8748f88e6f..a1ec932f0f 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -324,6 +324,8 @@ static enum parse_opt_result parse_long_opt(\n \t\tconst char *rest, *long_name = options->long_name;\n \t\tenum opt_parsed flags = OPT_LONG, opt_flags = OPT_LONG;\n \n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\tcontinue;\n \t\tif (!long_name)\n \t\t\tcontinue;\n \n@@ -419,6 +421,19 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p,\n \treturn PARSE_OPT_ERROR;\n }\n \n+static enum parse_opt_result parse_subcommand(const char *arg,\n+\t\t\t\t\t      const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++)\n+\t\tif (options->type == OPTION_SUBCOMMAND &&\n+\t\t    !strcmp(options->long_name, arg)) {\n+\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n+\t\t\treturn PARSE_OPT_SUBCOMMAND;\n+\t\t}\n+\n+\treturn PARSE_OPT_UNKNOWN;\n+}\n+\n static void check_typos(const char *arg, const struct option *options)\n {\n \tif (strlen(arg) < 3)\n@@ -442,6 +457,7 @@ static void check_typos(const char *arg, const struct option *options)\n static void parse_options_check(const struct option *opts)\n {\n \tchar short_opts[128];\n+\tvoid *subcommand_value = NULL;\n \n \tmemset(short_opts, '\\0', sizeof(short_opts));\n \tfor (; opts->type != OPTION_END; opts++) {\n@@ -489,6 +505,14 @@ static void parse_options_check(const struct option *opts)\n \t\t\t       \"Are you using parse_options_step() directly?\\n\"\n \t\t\t       \"That case is not supported yet.\");\n \t\t\tbreak;\n+\t\tcase OPTION_SUBCOMMAND:\n+\t\t\tif (!opts->value || !opts->subcommand_fn)\n+\t\t\t\toptbug(opts, \"OPTION_SUBCOMMAND needs a value and a subcommand function\");\n+\t\t\tif (!subcommand_value)\n+\t\t\t\tsubcommand_value = opts->value;\n+\t\t\telse if (subcommand_value != opts->value)\n+\t\t\t\toptbug(opts, \"all OPTION_SUBCOMMANDs need the same value\");\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\t; /* ok. (usually accepts an argument) */\n \t\t}\n@@ -499,6 +523,14 @@ static void parse_options_check(const struct option *opts)\n \tBUG_if_bug(\"invalid 'struct option'\");\n }\n \n+static int has_subcommands(const struct option *options)\n+{\n+\tfor (; options->type != OPTION_END; options++)\n+\t\tif (options->type == OPTION_SUBCOMMAND)\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n \t\t\t\t  int argc, const char **argv, const char *prefix,\n \t\t\t\t  const struct option *options,\n@@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n \tctx->prefix = prefix;\n \tctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);\n \tctx->flags = flags;\n+\tctx->has_subcommands = has_subcommands(options);\n+\tif (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL))\n+\t\tBUG(\"Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands\");\n+\tif (ctx->has_subcommands) {\n+\t\tif (flags & PARSE_OPT_STOP_AT_NON_OPTION)\n+\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION\");\n+\t\tif (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n+\t\t\tif (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)\n+\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN_OPT unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n+\t\t\tif (flags & PARSE_OPT_KEEP_DASHDASH)\n+\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n+\t\t}\n+\t}\n \tif ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&\n \t    (flags & PARSE_OPT_STOP_AT_NON_OPTION) &&\n \t    !(flags & PARSE_OPT_ONE_SHOT))\n@@ -589,6 +634,7 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \tint nr_noopts = 0;\n \n \tfor (; opts->type != OPTION_END; opts++) {\n+\t\tconst char *prefix = \"--\";\n \t\tconst char *suffix = \"\";\n \n \t\tif (!opts->long_name)\n@@ -598,6 +644,9 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \t\t\tcontinue;\n \n \t\tswitch (opts->type) {\n+\t\tcase OPTION_SUBCOMMAND:\n+\t\t\tprefix = \"\";\n+\t\t\tbreak;\n \t\tcase OPTION_GROUP:\n \t\t\tcontinue;\n \t\tcase OPTION_STRING:\n@@ -620,8 +669,8 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \t\t\tsuffix = \"=\";\n \t\tif (starts_with(opts->long_name, \"no-\"))\n \t\t\tnr_noopts++;\n-\t\tprintf(\"%s--%s%s\", opts == original_opts ? \"\" : \" \",\n-\t\t       opts->long_name, suffix);\n+\t\tprintf(\"%s%s%s%s\", opts == original_opts ? \"\" : \" \",\n+\t\t       prefix, opts->long_name, suffix);\n \t}\n \tshow_negated_gitcomp(original_opts, show_all, -1);\n \tshow_negated_gitcomp(original_opts, show_all, nr_noopts);\n@@ -744,10 +793,38 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tif (*arg != '-' || !arg[1]) {\n \t\t\tif (parse_nodash_opt(ctx, arg, options) == 0)\n \t\t\t\tcontinue;\n-\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n-\t\t\t\treturn PARSE_OPT_NON_OPTION;\n-\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n-\t\t\tcontinue;\n+\t\t\tif (!ctx->has_subcommands) {\n+\t\t\t\tif (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)\n+\t\t\t\t\treturn PARSE_OPT_NON_OPTION;\n+\t\t\t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tswitch (parse_subcommand(arg, options)) {\n+\t\t\tcase PARSE_OPT_SUBCOMMAND:\n+\t\t\t\treturn PARSE_OPT_SUBCOMMAND;\n+\t\t\tcase PARSE_OPT_UNKNOWN:\n+\t\t\t\tif (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\t\t\t\t/*\n+\t\t\t\t\t * arg is neither a short or long\n+\t\t\t\t\t * option nor a subcommand.  Since\n+\t\t\t\t\t * this command has a default\n+\t\t\t\t\t * operation mode, we have to treat\n+\t\t\t\t\t * this arg and all remaining args\n+\t\t\t\t\t * as args meant to that default\n+\t\t\t\t\t * operation mode.\n+\t\t\t\t\t * So we are done parsing.\n+\t\t\t\t\t */\n+\t\t\t\t\treturn PARSE_OPT_DONE;\n+\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n+\t\t\t\tusage_with_options(usagestr, options);\n+\t\t\tcase PARSE_OPT_COMPLETE:\n+\t\t\tcase PARSE_OPT_HELP:\n+\t\t\tcase PARSE_OPT_ERROR:\n+\t\t\tcase PARSE_OPT_DONE:\n+\t\t\tcase PARSE_OPT_NON_OPTION:\n+\t\t\t\t/* Impossible. */\n+\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n+\t\t\t}\n \t\t}\n \n \t\t/* lone -h asks for help */\n@@ -775,6 +852,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\tgoto show_usage;\n \t\t\t\tgoto unknown;\n \t\t\tcase PARSE_OPT_NON_OPTION:\n+\t\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\tcase PARSE_OPT_HELP:\n \t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\t\tBUG(\"parse_short_opt() cannot return these\");\n@@ -800,6 +878,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t*(char *)ctx->argv[0] = '-';\n \t\t\t\t\tgoto unknown;\n \t\t\t\tcase PARSE_OPT_NON_OPTION:\n+\t\t\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\t\tcase PARSE_OPT_HELP:\n \t\t\t\t\tBUG(\"parse_short_opt() cannot return these\");\n@@ -831,6 +910,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tcase PARSE_OPT_HELP:\n \t\t\tgoto show_usage;\n \t\tcase PARSE_OPT_NON_OPTION:\n+\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\tcase PARSE_OPT_COMPLETE:\n \t\t\tBUG(\"parse_long_opt() cannot return these\");\n \t\tcase PARSE_OPT_DONE:\n@@ -840,6 +920,18 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n unknown:\n \t\tif (ctx->flags & PARSE_OPT_ONE_SHOT)\n \t\t\tbreak;\n+\t\tif (ctx->has_subcommands &&\n+\t\t    (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL) &&\n+\t\t    (ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT)) {\n+\t\t\t/*\n+\t\t\t * Found an unknown option given to a command with\n+\t\t\t * subcommands that has a default operation mode:\n+\t\t\t * we treat this option and all remaining args as\n+\t\t\t * arguments meant to that default operation mode.\n+\t\t\t * So we are done parsing.\n+\t\t\t */\n+\t\t\treturn PARSE_OPT_DONE;\n+\t\t}\n \t\tif (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT))\n \t\t\treturn PARSE_OPT_UNKNOWN;\n \t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n@@ -885,7 +977,14 @@ int parse_options(int argc, const char **argv,\n \tcase PARSE_OPT_COMPLETE:\n \t\texit(0);\n \tcase PARSE_OPT_NON_OPTION:\n+\tcase PARSE_OPT_SUBCOMMAND:\n+\t\tbreak;\n \tcase PARSE_OPT_DONE:\n+\t\tif (ctx.has_subcommands &&\n+\t\t    !(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n+\t\t\terror(_(\"need a subcommand\"));\n+\t\t\tusage_with_options(usagestr, options);\n+\t\t}\n \t\tbreak;\n \tcase PARSE_OPT_UNKNOWN:\n \t\tif (ctx.argv[0][1] == '-') {\n@@ -1010,6 +1109,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \t\tsize_t pos;\n \t\tint pad;\n \n+\t\tif (opts->type == OPTION_SUBCOMMAND)\n+\t\t\tcontinue;\n \t\tif (opts->type == OPTION_GROUP) {\n \t\t\tfputc('\\n', outfile);\n \t\t\tneed_newline = 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex 8cbfc7e8bf..b6ef86e0d1 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -11,6 +11,7 @@ enum parse_opt_type {\n \tOPTION_GROUP,\n \tOPTION_NUMBER,\n \tOPTION_ALIAS,\n+\tOPTION_SUBCOMMAND,\n \t/* options with no arguments */\n \tOPTION_BIT,\n \tOPTION_NEGBIT,\n@@ -34,6 +35,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_NO_INTERNAL_HELP = 1 << 4,\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\n+\tPARSE_OPT_SUBCOMMAND_OPTIONAL = 1 << 7,\n };\n \n enum parse_opt_option_flags {\n@@ -56,6 +58,7 @@ enum parse_opt_result {\n \tPARSE_OPT_ERROR = -1,\t/* must be the same as error() */\n \tPARSE_OPT_DONE = 0,\t/* fixed so that \"return 0\" works */\n \tPARSE_OPT_NON_OPTION,\n+\tPARSE_OPT_SUBCOMMAND,\n \tPARSE_OPT_UNKNOWN\n };\n \n@@ -67,6 +70,9 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t      const struct option *opt,\n \t\t\t\t\t      const char *arg, int unset);\n \n+typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n+\t\t\t\t    const char *prefix);\n+\n /*\n  * `type`::\n  *   holds the type of the option, you must have an OPTION_END last in your\n@@ -76,7 +82,8 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n  *   the character to use as a short option name, '\\0' if none.\n  *\n  * `long_name`::\n- *   the long option name, without the leading dashes, NULL if none.\n+ *   the long option (without the leading dashes) or subcommand name,\n+ *   NULL if none.\n  *\n  * `value`::\n  *   stores pointers to the values to be filled.\n@@ -93,7 +100,7 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n  *\n  * `help`::\n  *   the short help associated to what the option does.\n- *   Must never be NULL (except for OPTION_END).\n+ *   Must never be NULL (except for OPTION_END and OPTION_SUBCOMMAND).\n  *   OPTION_GROUP uses this pointer to store the group header.\n  *   Should be wrapped by N_() for translation.\n  *\n@@ -131,6 +138,9 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n  * `ll_callback`::\n  *   pointer to the callback to use for OPTION_LOWLEVEL_CALLBACK\n  *\n+ * `subcommand_fn`::\n+ *   pointer to a function to use for OPTION_SUBCOMMAND.\n+ *   It will be put in value when the subcommand is given on the command line.\n  */\n struct option {\n \tenum parse_opt_type type;\n@@ -145,6 +155,7 @@ struct option {\n \tintptr_t defval;\n \tparse_opt_ll_cb *ll_callback;\n \tintptr_t extra;\n+\tparse_opt_subcommand_fn *subcommand_fn;\n };\n \n #define OPT_BIT_F(s, l, v, h, b, f) { OPTION_BIT, (s), (l), (v), NULL, (h), \\\n@@ -206,6 +217,14 @@ struct option {\n #define OPT_ALIAS(s, l, source_long_name) \\\n \t{ OPTION_ALIAS, (s), (l), (source_long_name) }\n \n+#define OPT_SUBCOMMAND_F(l, v, fn, f) { \\\n+\t.type = OPTION_SUBCOMMAND, \\\n+\t.long_name = (l), \\\n+\t.value = (v), \\\n+\t.flags = (f), \\\n+\t.subcommand_fn = (fn) }\n+#define OPT_SUBCOMMAND(l, v, fn)    OPT_SUBCOMMAND_F((l), (v), (fn), 0)\n+\n /*\n  * parse_options() will filter out the processed options and leave the\n  * non-option arguments in argv[]. argv0 is assumed program name and\n@@ -295,6 +314,7 @@ struct parse_opt_ctx_t {\n \tint argc, cpidx, total;\n \tconst char *opt;\n \tenum parse_opt_flags flags;\n+\tunsigned has_subcommands;\n \tconst char *prefix;\n \tconst char **alias_groups; /* must be in groups of 3 elements! */\n \tstruct option *updated_options;\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 99ad6fa4f0..aa0ad45851 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -238,6 +238,9 @@ static const struct option test_flag_options[] = {\n \tOPT_BIT(0, \"no-internal-help\", &test_flags,\n \t\t\"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()\",\n \t\tPARSE_OPT_NO_INTERNAL_HELP),\n+\tOPT_BIT(0, \"subcommand-optional\", &test_flags,\n+\t\t\"pass PARSE_OPT_SUBCOMMAND_OPTIONAL to parse_options()\",\n+\t\tPARSE_OPT_SUBCOMMAND_OPTIONAL),\n \tOPT_END()\n };\n \n@@ -258,3 +261,61 @@ int cmd__parse_options_flags(int argc, const char **argv)\n \n \treturn parse_options_flags__cmd(argc, argv, test_flags);\n }\n+\n+static int subcmd_one(int argc, const char **argv, const char *prefix)\n+{\n+\tprintf(\"fn: subcmd_one\\n\");\n+\tprint_args(argc, argv);\n+\treturn 0;\n+}\n+\n+static int subcmd_two(int argc, const char **argv, const char *prefix)\n+{\n+\tprintf(\"fn: subcmd_two\\n\");\n+\tprint_args(argc, argv);\n+\treturn 0;\n+}\n+\n+static int parse_subcommand__cmd(int argc, const char **argv,\n+\t\t\t\t enum parse_opt_flags test_flags)\n+{\n+\tconst char *usage[] = {\n+\t\t\"<...> cmd subcmd-one\",\n+\t\t\"<...> cmd subcmd-two\",\n+\t\tNULL\n+\t};\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tint opt = 0;\n+\tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"subcmd-one\", &fn, subcmd_one),\n+\t\tOPT_SUBCOMMAND(\"subcmd-two\", &fn, subcmd_two),\n+\t\tOPT_INTEGER('o', \"opt\", &opt, \"an integer option\"),\n+\t\tOPT_END()\n+\t};\n+\n+\tif (test_flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)\n+\t\tfn = subcmd_one;\n+\targc = parse_options(argc, argv, NULL, options, usage, test_flags);\n+\n+\tprintf(\"opt: %d\\n\", opt);\n+\n+\treturn fn(argc, argv, NULL);\n+}\n+\n+int cmd__parse_subcommand(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool parse-subcommand [flag-options] cmd <subcommand>\",\n+\t\tNULL\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, test_flag_options, usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n+\t\terror(\"'cmd' is mandatory\");\n+\t\tusage_with_options(usage, test_flag_options);\n+\t}\n+\n+\treturn parse_subcommand__cmd(argc, argv, test_flags);\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex 6e62282b60..49b30057f6 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -53,6 +53,7 @@ static struct test_cmd cmds[] = {\n \t{ \"parse-options\", cmd__parse_options },\n \t{ \"parse-options-flags\", cmd__parse_options_flags },\n \t{ \"parse-pathspec-file\", cmd__parse_pathspec_file },\n+\t{ \"parse-subcommand\", cmd__parse_subcommand },\n \t{ \"partial-clone\", cmd__partial_clone },\n \t{ \"path-utils\", cmd__path_utils },\n \t{ \"pcre2-config\", cmd__pcre2_config },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex d8e8403d70..487d84da60 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -43,6 +43,7 @@ int cmd__pack_mtimes(int argc, const char **argv);\n int cmd__parse_options(int argc, const char **argv);\n int cmd__parse_options_flags(int argc, const char **argv);\n int cmd__parse_pathspec_file(int argc, const char** argv);\n+int cmd__parse_subcommand(int argc, const char **argv);\n int cmd__partial_clone(int argc, const char **argv);\n int cmd__path_utils(int argc, const char **argv);\n int cmd__pcre2_config(int argc, const char **argv);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 264b737309..b19b8d3486 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -526,4 +526,189 @@ test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'subcommand - no subcommand shows error and usage' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd 2>err &&\n+\tgrep \"^error: need a subcommand\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - subcommand after -- shows error and usage' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&\n+\tgrep \"^error: need a subcommand\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&\n+\tgrep \"^error: need a subcommand\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&\n+\tgrep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n+\tgrep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - no negated subcommands' '\n+\ttest_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&\n+\tgrep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - simple' '\n+\ttest-tool parse-subcommand cmd subcmd-two >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_two\n+\targ 00: subcmd-two\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - stop parsing at the first subcommand' '\n+\ttest-tool parse-subcommand cmd --opt=1 subcmd-two subcmd-one --opt=2 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 1\n+\tfn: subcmd_two\n+\targ 00: subcmd-two\n+\targ 01: subcmd-one\n+\targ 02: --opt=2\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - KEEP_ARGV0' '\n+\ttest-tool parse-subcommand --keep-argv0 cmd subcmd-two >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_two\n+\targ 00: cmd\n+\targ 01: subcmd-two\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given' '\n+\ttest-tool parse-subcommand --subcommand-optional cmd >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + given subcommand' '\n+\ttest-tool parse-subcommand --subcommand-optional cmd subcmd-two branch file >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_two\n+\targ 00: subcmd-two\n+\targ 01: branch\n+\targ 02: file\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown dashless args' '\n+\ttest-tool parse-subcommand --subcommand-optional cmd branch file >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: branch\n+\targ 01: file\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown option' '\n+\ttest_expect_code 129 test-tool parse-subcommand --subcommand-optional cmd --subcommand-opt 2>err &&\n+\tgrep \"^error: unknown option\" err &&\n+\tgrep ^usage: err\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand not given + unknown option' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: --subcommand-opt\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand ignored after unknown option' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt subcmd-two >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: --subcommand-opt\n+\targ 01: subcmd-two\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + command and subcommand options cannot be mixed' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt cmd --subcommand-opt branch --opt=1 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: --subcommand-opt\n+\targ 01: branch\n+\targ 02: --opt=1\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT | KEEP_ARGV0' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt --keep-argv0 cmd --subcommand-opt branch >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: cmd\n+\targ 01: --subcommand-opt\n+\targ 02: branch\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT | KEEP_DASHDASH' '\n+\ttest-tool parse-subcommand --subcommand-optional --keep-unknown-opt --keep-dashdash cmd -- --subcommand-opt file >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\tfn: subcmd_one\n+\targ 00: --\n+\targ 01: --subcommand-opt\n+\targ 02: file\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommand - completion helper' '\n+\ttest-tool parse-subcommand cmd --git-completion-helper >actual &&\n+\techo \"subcmd-one subcmd-two --opt= --no-opt\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'subcommands are incompatible with STOP_AT_NON_OPTION' '\n+\ttest_must_fail test-tool parse-subcommand --stop-at-non-option cmd subcmd-one 2>err &&\n+\tgrep ^BUG err\n+'\n+\n+test_expect_success 'subcommands are incompatible with KEEP_UNKNOWN_OPT unless in combination with SUBCOMMAND_OPTIONAL' '\n+\ttest_must_fail test-tool parse-subcommand --keep-unknown-opt cmd subcmd-two 2>err &&\n+\tgrep ^BUG err\n+'\n+\n+test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in combination with SUBCOMMAND_OPTIONAL' '\n+\ttest_must_fail test-tool parse-subcommand --keep-dashdash cmd subcmd-two 2>err &&\n+\tgrep ^BUG err\n+'\n+\n test_done\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461559","messageId":"20220819160411.1791200-16-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 15/20] builtin/notes.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:06Z","receivedAt":"2022-08-19T16:32:42Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git notes' parses its subcommands with a long list of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling unknown subcommands, and listing subcommands for Bash\ncompletion.  Make sure that the default operation mode doesn't accept\nany arguments.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/notes.c | 43 +++++++++++++++++--------------------------\n 1 file changed, 17 insertions(+), 26 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex a3d0d15a22..42cbae4659 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -994,17 +994,31 @@ static int get_ref(int argc, const char **argv, const char *prefix)\n \n int cmd_notes(int argc, const char **argv, const char *prefix)\n {\n-\tint result;\n \tconst char *override_notes_ref = NULL;\n+\tparse_opt_subcommand_fn *fn = list;\n \tstruct option options[] = {\n \t\tOPT_STRING(0, \"ref\", &override_notes_ref, N_(\"notes-ref\"),\n \t\t\t   N_(\"use notes from <notes-ref>\")),\n+\t\tOPT_SUBCOMMAND(\"list\", &fn, list),\n+\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n+\t\tOPT_SUBCOMMAND(\"copy\", &fn, copy),\n+\t\tOPT_SUBCOMMAND(\"append\", &fn, append_edit),\n+\t\tOPT_SUBCOMMAND(\"edit\", &fn, append_edit),\n+\t\tOPT_SUBCOMMAND(\"show\", &fn, show),\n+\t\tOPT_SUBCOMMAND(\"merge\", &fn, merge),\n+\t\tOPT_SUBCOMMAND(\"remove\", &fn, remove_cmd),\n+\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n+\t\tOPT_SUBCOMMAND(\"get-ref\", &fn, get_ref),\n \t\tOPT_END()\n \t};\n \n \tgit_config(git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n+\tif (fn == list && argc && strcmp(argv[0], \"list\")) {\n+\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n+\t\tusage_with_options(git_notes_usage, options);\n+\t}\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\n@@ -1014,28 +1028,5 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n \t\tstrbuf_release(&sb);\n \t}\n \n-\tif (argc < 1 || !strcmp(argv[0], \"list\"))\n-\t\tresult = list(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"add\"))\n-\t\tresult = add(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"copy\"))\n-\t\tresult = copy(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"append\") || !strcmp(argv[0], \"edit\"))\n-\t\tresult = append_edit(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"show\"))\n-\t\tresult = show(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"merge\"))\n-\t\tresult = merge(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"remove\"))\n-\t\tresult = remove_cmd(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"prune\"))\n-\t\tresult = prune(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"get-ref\"))\n-\t\tresult = get_ref(argc, argv, prefix);\n-\telse {\n-\t\tresult = error(_(\"unknown subcommand: %s\"), argv[0]);\n-\t\tusage_with_options(git_notes_usage, options);\n-\t}\n-\n-\treturn result ? 1 : 0;\n+\treturn !!fn(argc, argv, prefix);\n }\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461560","messageId":"20220819160411.1791200-18-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 17/20] builtin/remote.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:08Z","receivedAt":"2022-08-19T16:32:43Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git remote' parses its subcommands with a long list of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling unknown subcommands, and listing subcommands for Bash\ncompletion.  Make sure that the default operation mode doesn't accept\nany arguments; and while at it remove the capitalization of the error\nmessage and adjust the test checking it accordingly.\n\nNote that 'git remote' has both 'remove' and 'rm' subcommands, and the\nformer is preferred [1], so hide the latter for completion.\n\nNote also that the functions implementing each subcommand only accept\nthe 'argc' and '**argv' parameters, so add a (unused) '*prefix'\nparameter to make them match the type expected by parse-options, and\nthus avoid casting a bunch of function pointers.\n\n[1] e17dba8fe1 (remote: prefer subcommand name 'remove' to 'rm',\n    2012-09-06)\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/remote.c  | 70 +++++++++++++++++++++--------------------------\n t/t5505-remote.sh |  2 +-\n 2 files changed, 32 insertions(+), 40 deletions(-)\n\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex d9b8746cb3..4a6d47c03a 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -150,7 +150,7 @@ static int parse_mirror_opt(const struct option *opt, const char *arg, int not)\n \treturn 0;\n }\n \n-static int add(int argc, const char **argv)\n+static int add(int argc, const char **argv, const char *prefix)\n {\n \tint fetch = 0, fetch_tags = TAGS_DEFAULT;\n \tunsigned mirror = MIRROR_NONE;\n@@ -680,7 +680,7 @@ static void handle_push_default(const char* old_name, const char* new_name)\n }\n \n \n-static int mv(int argc, const char **argv)\n+static int mv(int argc, const char **argv, const char *prefix)\n {\n \tint show_progress = isatty(2);\n \tstruct option options[] = {\n@@ -844,7 +844,7 @@ static int mv(int argc, const char **argv)\n \treturn 0;\n }\n \n-static int rm(int argc, const char **argv)\n+static int rm(int argc, const char **argv, const char *prefix)\n {\n \tstruct option options[] = {\n \t\tOPT_END()\n@@ -1255,7 +1255,7 @@ static int show_all(void)\n \treturn result;\n }\n \n-static int show(int argc, const char **argv)\n+static int show(int argc, const char **argv, const char *prefix)\n {\n \tint no_query = 0, result = 0, query_flag = 0;\n \tstruct option options[] = {\n@@ -1358,7 +1358,7 @@ static int show(int argc, const char **argv)\n \treturn result;\n }\n \n-static int set_head(int argc, const char **argv)\n+static int set_head(int argc, const char **argv, const char *prefix)\n {\n \tint i, opt_a = 0, opt_d = 0, result = 0;\n \tstruct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;\n@@ -1463,7 +1463,7 @@ static int prune_remote(const char *remote, int dry_run)\n \treturn result;\n }\n \n-static int prune(int argc, const char **argv)\n+static int prune(int argc, const char **argv, const char *prefix)\n {\n \tint dry_run = 0, result = 0;\n \tstruct option options[] = {\n@@ -1492,7 +1492,7 @@ static int get_remote_default(const char *key, const char *value, void *priv)\n \treturn 0;\n }\n \n-static int update(int argc, const char **argv)\n+static int update(int argc, const char **argv, const char *prefix)\n {\n \tint i, prune = -1;\n \tstruct option options[] = {\n@@ -1575,7 +1575,7 @@ static int set_remote_branches(const char *remotename, const char **branches,\n \treturn 0;\n }\n \n-static int set_branches(int argc, const char **argv)\n+static int set_branches(int argc, const char **argv, const char *prefix)\n {\n \tint add_mode = 0;\n \tstruct option options[] = {\n@@ -1594,7 +1594,7 @@ static int set_branches(int argc, const char **argv)\n \treturn set_remote_branches(argv[0], argv + 1, add_mode);\n }\n \n-static int get_url(int argc, const char **argv)\n+static int get_url(int argc, const char **argv, const char *prefix)\n {\n \tint i, push_mode = 0, all_mode = 0;\n \tconst char *remotename = NULL;\n@@ -1647,7 +1647,7 @@ static int get_url(int argc, const char **argv)\n \treturn 0;\n }\n \n-static int set_url(int argc, const char **argv)\n+static int set_url(int argc, const char **argv, const char *prefix)\n {\n \tint i, push_mode = 0, add_mode = 0, delete_mode = 0;\n \tint matches = 0, negative_matches = 0;\n@@ -1739,41 +1739,33 @@ static int set_url(int argc, const char **argv)\n \n int cmd_remote(int argc, const char **argv, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT__VERBOSE(&verbose, N_(\"be verbose; must be placed before a subcommand\")),\n+\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n+\t\tOPT_SUBCOMMAND(\"rename\", &fn, mv),\n+\t\tOPT_SUBCOMMAND_F(\"rm\", &fn, rm, PARSE_OPT_NOCOMPLETE),\n+\t\tOPT_SUBCOMMAND(\"remove\", &fn, rm),\n+\t\tOPT_SUBCOMMAND(\"set-head\", &fn, set_head),\n+\t\tOPT_SUBCOMMAND(\"set-branches\", &fn, set_branches),\n+\t\tOPT_SUBCOMMAND(\"get-url\", &fn, get_url),\n+\t\tOPT_SUBCOMMAND(\"set-url\", &fn, set_url),\n+\t\tOPT_SUBCOMMAND(\"show\", &fn, show),\n+\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n+\t\tOPT_SUBCOMMAND(\"update\", &fn, update),\n \t\tOPT_END()\n \t};\n-\tint result;\n \n \targc = parse_options(argc, argv, prefix, options, builtin_remote_usage,\n-\t\tPARSE_OPT_STOP_AT_NON_OPTION);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n \n-\tif (argc < 1)\n-\t\tresult = show_all();\n-\telse if (!strcmp(argv[0], \"add\"))\n-\t\tresult = add(argc, argv);\n-\telse if (!strcmp(argv[0], \"rename\"))\n-\t\tresult = mv(argc, argv);\n-\telse if (!strcmp(argv[0], \"rm\") || !strcmp(argv[0], \"remove\"))\n-\t\tresult = rm(argc, argv);\n-\telse if (!strcmp(argv[0], \"set-head\"))\n-\t\tresult = set_head(argc, argv);\n-\telse if (!strcmp(argv[0], \"set-branches\"))\n-\t\tresult = set_branches(argc, argv);\n-\telse if (!strcmp(argv[0], \"get-url\"))\n-\t\tresult = get_url(argc, argv);\n-\telse if (!strcmp(argv[0], \"set-url\"))\n-\t\tresult = set_url(argc, argv);\n-\telse if (!strcmp(argv[0], \"show\"))\n-\t\tresult = show(argc, argv);\n-\telse if (!strcmp(argv[0], \"prune\"))\n-\t\tresult = prune(argc, argv);\n-\telse if (!strcmp(argv[0], \"update\"))\n-\t\tresult = update(argc, argv);\n-\telse {\n-\t\terror(_(\"Unknown subcommand: %s\"), argv[0]);\n-\t\tusage_with_options(builtin_remote_usage, options);\n+\tif (fn) {\n+\t\treturn !!fn(argc, argv, prefix);\n+\t} else {\n+\t\tif (argc) {\n+\t\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n+\t\t\tusage_with_options(builtin_remote_usage, options);\n+\t\t}\n+\t\treturn !!show_all();\n \t}\n-\n-\treturn result ? 1 : 0;\n }\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex a549a21ef6..9006196ac6 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -258,7 +258,7 @@ test_expect_success 'without subcommand accepts -v' '\n \n test_expect_success 'without subcommand does not take arguments' '\n \ttest_expect_code 129 git -C test remote origin 2>err &&\n-\tgrep \"^error: Unknown subcommand:\" err\n+\tgrep \"^error: unknown subcommand:\" err\n '\n \n cat >test/expect <<EOF\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461561","messageId":"20220819160411.1791200-3-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:53Z","receivedAt":"2022-08-19T16:32:46Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git notes' without a subcommand defaults to listing all notes and\ndoesn't accept any arguments.\n\nWe are about to teach parse-options to handle subcommands, and update\n'git notes' to make use of that new feature.  So let's add a test to\nmake sure that the upcoming changes don't inadvertenly change the\nbehavior in this corner case.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/t3301-notes.sh | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/t/t3301-notes.sh b/t/t3301-notes.sh\nindex d742be8840..3288aaec7d 100755\n--- a/t/t3301-notes.sh\n+++ b/t/t3301-notes.sh\n@@ -505,6 +505,11 @@ test_expect_success 'list notes with \"git notes\"' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success '\"git notes\" without subcommand does not take arguments' '\n+\ttest_expect_code 129 git notes HEAD^^ 2>err &&\n+\tgrep \"^error: unknown subcommand\" err\n+'\n+\n test_expect_success 'list specific note with \"git notes list <object>\"' '\n \tgit rev-parse refs/notes/commits:$commit_3 >expect &&\n \tgit notes list HEAD^^ >actual &&\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461562","messageId":"20220819160411.1791200-14-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 13/20] builtin/hook.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:04Z","receivedAt":"2022-08-19T16:32:48Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git hook' parses its currently only subcommand with an if statement.\nparse-options has just learned to parse subcommands, so let's use that\nfacility instead, with the benefits of shorter code, handling missing\nor unknown subcommands, and listing subcommands for Bash completion.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/hook.c | 12 ++++--------\n 1 file changed, 4 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 54e5c6ec93..b6530d189a 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -67,18 +67,14 @@ static int run(int argc, const char **argv, const char *prefix)\n \n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option builtin_hook_options[] = {\n+\t\tOPT_SUBCOMMAND(\"run\", &fn, run),\n \t\tOPT_END(),\n \t};\n \n \targc = parse_options(argc, argv, NULL, builtin_hook_options,\n-\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n-\tif (!argc)\n-\t\tgoto usage;\n+\t\t\t     builtin_hook_usage, 0);\n \n-\tif (!strcmp(argv[0], \"run\"))\n-\t\treturn run(argc, argv, prefix);\n-\n-usage:\n-\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+\treturn fn(argc, argv, prefix);\n }\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461563","messageId":"20220819160411.1791200-11-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 10/20] builtin/bundle.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:01Z","receivedAt":"2022-08-19T16:32:52Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git bundle' parses its subcommands with a couple of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/bundle.c | 25 +++++++------------------\n 1 file changed, 7 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/bundle.c b/builtin/bundle.c\nindex 2adad545a2..e80efce3a4 100644\n--- a/builtin/bundle.c\n+++ b/builtin/bundle.c\n@@ -195,30 +195,19 @@ static int cmd_bundle_unbundle(int argc, const char **argv, const char *prefix)\n \n int cmd_bundle(int argc, const char **argv, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_bundle_create),\n+\t\tOPT_SUBCOMMAND(\"verify\", &fn, cmd_bundle_verify),\n+\t\tOPT_SUBCOMMAND(\"list-heads\", &fn, cmd_bundle_list_heads),\n+\t\tOPT_SUBCOMMAND(\"unbundle\", &fn, cmd_bundle_unbundle),\n \t\tOPT_END()\n \t};\n-\tint result;\n \n \targc = parse_options(argc, argv, prefix, options, builtin_bundle_usage,\n-\t\tPARSE_OPT_STOP_AT_NON_OPTION);\n+\t\t\t     0);\n \n \tpacket_trace_identity(\"bundle\");\n \n-\tif (argc < 2)\n-\t\tusage_with_options(builtin_bundle_usage, options);\n-\n-\telse if (!strcmp(argv[0], \"create\"))\n-\t\tresult = cmd_bundle_create(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"verify\"))\n-\t\tresult = cmd_bundle_verify(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"list-heads\"))\n-\t\tresult = cmd_bundle_list_heads(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"unbundle\"))\n-\t\tresult = cmd_bundle_unbundle(argc, argv, prefix);\n-\telse {\n-\t\terror(_(\"Unknown subcommand: %s\"), argv[0]);\n-\t\tusage_with_options(builtin_bundle_usage, options);\n-\t}\n-\treturn result ? 1 : 0;\n+\treturn !!fn(argc, argv, prefix);\n }\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461564","messageId":"20220819160411.1791200-13-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:03Z","receivedAt":"2022-08-19T16:32:54Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git maintenanze' parses its subcommands with a couple of if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nThis change makes 'git maintenance' consistent with other commands in\nthat the help text shown for '-h' goes to standard output, not error,\nin the exit code and error message on unknown subcommand, and the\nerror message on missing subcommand.  There is a test checking these,\nwhich is now updated accordingly.\n\nNote that some of the functions implementing each subcommand don't\naccept any parameters, so add the (unused) 'argc', '**argv' and\n'*prefix' parameters to make them match the type expected by\nparse-options, and thus avoid casting function pointers.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/gc.c           | 42 +++++++++++++++++++++---------------------\n git.c                  |  2 +-\n t/t7900-maintenance.sh | 10 ++++++----\n 3 files changed, 28 insertions(+), 26 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex eeff2b760e..19d6b3b558 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1465,7 +1465,7 @@ static char *get_maintpath(void)\n \treturn strbuf_detach(&sb, NULL);\n }\n \n-static int maintenance_register(void)\n+static int maintenance_register(int argc, const char **argv, const char *prefix)\n {\n \tint rc;\n \tchar *config_value;\n@@ -1509,7 +1509,7 @@ static int maintenance_register(void)\n \treturn rc;\n }\n \n-static int maintenance_unregister(void)\n+static int maintenance_unregister(int argc, const char **argv, const char *prefix)\n {\n \tint rc;\n \tstruct child_process config_unset = CHILD_PROCESS_INIT;\n@@ -2505,34 +2505,34 @@ static int maintenance_start(int argc, const char **argv, const char *prefix)\n \topts.scheduler = resolve_scheduler(opts.scheduler);\n \tvalidate_scheduler(opts.scheduler);\n \n-\tif (maintenance_register())\n+\tif (maintenance_register(0, NULL, NULL)) /* It doesn't take any args */\n \t\twarning(_(\"failed to add repo to global config\"));\n \treturn update_background_schedule(&opts, 1);\n }\n \n-static int maintenance_stop(void)\n+static int maintenance_stop(int argc, const char **argv, const char *prefix)\n {\n \treturn update_background_schedule(NULL, 0);\n }\n \n-static const char builtin_maintenance_usage[] =\tN_(\"git maintenance <subcommand> [<options>]\");\n+static const char * const builtin_maintenance_usage[] = {\n+\tN_(\"git maintenance <subcommand> [<options>]\"),\n+\tNULL,\n+};\n \n int cmd_maintenance(int argc, const char **argv, const char *prefix)\n {\n-\tif (argc < 2 ||\n-\t    (argc == 2 && !strcmp(argv[1], \"-h\")))\n-\t\tusage(builtin_maintenance_usage);\n-\n-\tif (!strcmp(argv[1], \"run\"))\n-\t\treturn maintenance_run(argc - 1, argv + 1, prefix);\n-\tif (!strcmp(argv[1], \"start\"))\n-\t\treturn maintenance_start(argc - 1, argv + 1, prefix);\n-\tif (!strcmp(argv[1], \"stop\"))\n-\t\treturn maintenance_stop();\n-\tif (!strcmp(argv[1], \"register\"))\n-\t\treturn maintenance_register();\n-\tif (!strcmp(argv[1], \"unregister\"))\n-\t\treturn maintenance_unregister();\n-\n-\tdie(_(\"invalid subcommand: %s\"), argv[1]);\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option builtin_maintenance_options[] = {\n+\t\tOPT_SUBCOMMAND(\"run\", &fn, maintenance_run),\n+\t\tOPT_SUBCOMMAND(\"start\", &fn, maintenance_start),\n+\t\tOPT_SUBCOMMAND(\"stop\", &fn, maintenance_stop),\n+\t\tOPT_SUBCOMMAND(\"register\", &fn, maintenance_register),\n+\t\tOPT_SUBCOMMAND(\"unregister\", &fn, maintenance_unregister),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, builtin_maintenance_options,\n+\t\t\t     builtin_maintenance_usage, 0);\n+\treturn fn(argc, argv, prefix);\n }\ndiff --git a/git.c b/git.c\nindex 09c126c33d..73ddf0f452 100644\n--- a/git.c\n+++ b/git.c\n@@ -555,7 +555,7 @@ static struct cmd_struct commands[] = {\n \t{ \"ls-tree\", cmd_ls_tree, RUN_SETUP },\n \t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY },\n \t{ \"mailsplit\", cmd_mailsplit, NO_PARSEOPT },\n-\t{ \"maintenance\", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"maintenance\", cmd_maintenance, RUN_SETUP },\n \t{ \"merge\", cmd_merge, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"merge-base\", cmd_merge_base, RUN_SETUP },\n \t{ \"merge-file\", cmd_merge_file, RUN_SETUP_GENTLY },\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 74aa638475..852498ff06 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -32,11 +32,13 @@ test_systemd_analyze_verify () {\n }\n \n test_expect_success 'help text' '\n-\ttest_expect_code 129 git maintenance -h 2>err &&\n-\ttest_i18ngrep \"usage: git maintenance <subcommand>\" err &&\n-\ttest_expect_code 128 git maintenance barf 2>err &&\n-\ttest_i18ngrep \"invalid subcommand: barf\" err &&\n+\ttest_expect_code 129 git maintenance -h >actual &&\n+\ttest_i18ngrep \"usage: git maintenance <subcommand>\" actual &&\n+\ttest_expect_code 129 git maintenance barf 2>err &&\n+\ttest_i18ngrep \"unknown subcommand: \\`barf'\\''\" err &&\n+\ttest_i18ngrep \"usage: git maintenance\" err &&\n \ttest_expect_code 129 git maintenance 2>err &&\n+\ttest_i18ngrep \"error: need a subcommand\" err &&\n \ttest_i18ngrep \"usage: git maintenance\" err\n '\n \n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461565","messageId":"20220819160411.1791200-17-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 16/20] builtin/reflog.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:07Z","receivedAt":"2022-08-19T16:32:57Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git reflog' parses its subcommands with a couple of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nand listing subcommands for Bash completion.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/reflog.c | 41 +++++++++++------------------------------\n 1 file changed, 11 insertions(+), 30 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex b8b1f4f8ea..d3f6d903fb 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -404,40 +404,21 @@ static int cmd_reflog_exists(int argc, const char **argv, const char *prefix)\n \n int cmd_reflog(int argc, const char **argv, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n+\t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n \t\tOPT_END()\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, reflog_usage,\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n-\t\t\t     PARSE_OPT_NO_INTERNAL_HELP);\n-\n-\t/*\n-\t * With \"git reflog\" we default to showing it. !argc is\n-\t * impossible with PARSE_OPT_KEEP_ARGV0.\n-\t */\n-\tif (argc == 1)\n-\t\tgoto log_reflog;\n-\n-\tif (!strcmp(argv[1], \"-h\"))\n-\t\tusage_with_options(reflog_usage, options);\n-\telse if (*argv[1] == '-')\n-\t\tgoto log_reflog;\n-\n-\tif (!strcmp(argv[1], \"show\"))\n-\t\treturn cmd_reflog_show(argc - 1, argv + 1, prefix);\n-\telse if (!strcmp(argv[1], \"expire\"))\n-\t\treturn cmd_reflog_expire(argc - 1, argv + 1, prefix);\n-\telse if (!strcmp(argv[1], \"delete\"))\n-\t\treturn cmd_reflog_delete(argc - 1, argv + 1, prefix);\n-\telse if (!strcmp(argv[1], \"exists\"))\n-\t\treturn cmd_reflog_exists(argc - 1, argv + 1, prefix);\n-\n-\t/*\n-\t * Fall-through for e.g. \"git reflog -1\", \"git reflog master\",\n-\t * as well as the plain \"git reflog\" above goto above.\n-\t */\n-log_reflog:\n-\treturn cmd_log_reflog(argc, argv, prefix);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\tif (fn)\n+\t\treturn fn(argc - 1, argv + 1, prefix);\n+\telse\n+\t\treturn cmd_log_reflog(argc, argv, prefix);\n }\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461566","messageId":"20220819160411.1791200-12-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 11/20] builtin/commit-graph.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:02Z","receivedAt":"2022-08-19T16:33:00Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git commit-graph' parses its subcommands with an if-else if\nstatement.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nNote that the functions implementing each subcommand only accept the\n'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter\nto make them match the type expected by parse-options, and thus avoid\ncasting function pointers.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/commit-graph.c  | 30 +++++++++++++-----------------\n t/t5318-commit-graph.sh |  4 ++--\n 2 files changed, 15 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/commit-graph.c b/builtin/commit-graph.c\nindex 51c4040ea6..1eb5492cbd 100644\n--- a/builtin/commit-graph.c\n+++ b/builtin/commit-graph.c\n@@ -58,7 +58,7 @@ static struct option *add_common_options(struct option *to)\n \treturn parse_options_concat(common_opts, to);\n }\n \n-static int graph_verify(int argc, const char **argv)\n+static int graph_verify(int argc, const char **argv, const char *prefix)\n {\n \tstruct commit_graph *graph = NULL;\n \tstruct object_directory *odb = NULL;\n@@ -190,7 +190,7 @@ static int git_commit_graph_write_config(const char *var, const char *value,\n \treturn 0;\n }\n \n-static int graph_write(int argc, const char **argv)\n+static int graph_write(int argc, const char **argv, const char *prefix)\n {\n \tstruct string_list pack_indexes = STRING_LIST_INIT_DUP;\n \tstruct strbuf buf = STRBUF_INIT;\n@@ -307,26 +307,22 @@ static int graph_write(int argc, const char **argv)\n \n int cmd_commit_graph(int argc, const char **argv, const char *prefix)\n {\n-\tstruct option *builtin_commit_graph_options = common_opts;\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option builtin_commit_graph_options[] = {\n+\t\tOPT_SUBCOMMAND(\"verify\", &fn, graph_verify),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, graph_write),\n+\t\tOPT_END(),\n+\t};\n+\tstruct option *options = parse_options_concat(builtin_commit_graph_options, common_opts);\n \n \tgit_config(git_default_config, NULL);\n-\targc = parse_options(argc, argv, prefix,\n-\t\t\t     builtin_commit_graph_options,\n-\t\t\t     builtin_commit_graph_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n-\tif (!argc)\n-\t\tgoto usage;\n \n \tread_replace_refs = 0;\n \tsave_commit_buffer = 0;\n \n-\tif (!strcmp(argv[0], \"verify\"))\n-\t\treturn graph_verify(argc, argv);\n-\telse if (argc && !strcmp(argv[0], \"write\"))\n-\t\treturn graph_write(argc, argv);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_commit_graph_usage, 0);\n+\tFREE_AND_NULL(options);\n \n-\terror(_(\"unrecognized subcommand: %s\"), argv[0]);\n-usage:\n-\tusage_with_options(builtin_commit_graph_usage,\n-\t\t\t   builtin_commit_graph_options);\n+\treturn fn(argc, argv, prefix);\n }\ndiff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh\nindex be0b5641ff..7e040eb1ed 100755\n--- a/t/t5318-commit-graph.sh\n+++ b/t/t5318-commit-graph.sh\n@@ -12,12 +12,12 @@ test_expect_success 'usage' '\n \n test_expect_success 'usage shown without sub-command' '\n \ttest_expect_code 129 git commit-graph 2>err &&\n-\t! grep error: err\n+\tgrep usage: err\n '\n \n test_expect_success 'usage shown with an error on unknown sub-command' '\n \tcat >expect <<-\\EOF &&\n-\terror: unrecognized subcommand: unknown\n+\terror: unknown subcommand: `unknown'\\''\n \tEOF\n \ttest_expect_code 129 git commit-graph unknown 2>stderr &&\n \tgrep error stderr >actual &&\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461567","messageId":"20220819160411.1791200-19-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 18/20] builtin/sparse-checkout.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:09Z","receivedAt":"2022-08-19T16:33:01Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git sparse-checkout' parses its subcommands with a couple of if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nNote that some of the functions implementing each subcommand only\naccept the 'argc' and '**argv' parameters, so add a (unused) '*prefix'\nparameter to make them match the type expected by parse-options, and\nthus avoid casting function pointers.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/sparse-checkout.c | 44 ++++++++++++++-------------------------\n 1 file changed, 16 insertions(+), 28 deletions(-)\n\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex a5e4b95a9d..7b39a150a9 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -48,7 +48,7 @@ static char const * const builtin_sparse_checkout_list_usage[] = {\n \tNULL\n };\n \n-static int sparse_checkout_list(int argc, const char **argv)\n+static int sparse_checkout_list(int argc, const char **argv, const char *prefix)\n {\n \tstatic struct option builtin_sparse_checkout_list_options[] = {\n \t\tOPT_END(),\n@@ -431,7 +431,7 @@ static struct sparse_checkout_init_opts {\n \tint sparse_index;\n } init_opts;\n \n-static int sparse_checkout_init(int argc, const char **argv)\n+static int sparse_checkout_init(int argc, const char **argv, const char *prefix)\n {\n \tstruct pattern_list pl;\n \tchar *sparse_filename;\n@@ -843,7 +843,8 @@ static struct sparse_checkout_reapply_opts {\n \tint sparse_index;\n } reapply_opts;\n \n-static int sparse_checkout_reapply(int argc, const char **argv)\n+static int sparse_checkout_reapply(int argc, const char **argv,\n+\t\t\t\t   const char *prefix)\n {\n \tstatic struct option builtin_sparse_checkout_reapply_options[] = {\n \t\tOPT_BOOL(0, \"cone\", &reapply_opts.cone_mode,\n@@ -876,7 +877,8 @@ static char const * const builtin_sparse_checkout_disable_usage[] = {\n \tNULL\n };\n \n-static int sparse_checkout_disable(int argc, const char **argv)\n+static int sparse_checkout_disable(int argc, const char **argv,\n+\t\t\t\t   const char *prefix)\n {\n \tstatic struct option builtin_sparse_checkout_disable_options[] = {\n \t\tOPT_END(),\n@@ -922,39 +924,25 @@ static int sparse_checkout_disable(int argc, const char **argv)\n \n int cmd_sparse_checkout(int argc, const char **argv, const char *prefix)\n {\n-\tstatic struct option builtin_sparse_checkout_options[] = {\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option builtin_sparse_checkout_options[] = {\n+\t\tOPT_SUBCOMMAND(\"list\", &fn, sparse_checkout_list),\n+\t\tOPT_SUBCOMMAND(\"init\", &fn, sparse_checkout_init),\n+\t\tOPT_SUBCOMMAND(\"set\", &fn, sparse_checkout_set),\n+\t\tOPT_SUBCOMMAND(\"add\", &fn, sparse_checkout_add),\n+\t\tOPT_SUBCOMMAND(\"reapply\", &fn, sparse_checkout_reapply),\n+\t\tOPT_SUBCOMMAND(\"disable\", &fn, sparse_checkout_disable),\n \t\tOPT_END(),\n \t};\n \n-\tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n-\t\tusage_with_options(builtin_sparse_checkout_usage,\n-\t\t\t\t   builtin_sparse_checkout_options);\n-\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_sparse_checkout_options,\n-\t\t\t     builtin_sparse_checkout_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\t\t\t     builtin_sparse_checkout_usage, 0);\n \n \tgit_config(git_default_config, NULL);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.command_requires_full_index = 0;\n \n-\tif (argc > 0) {\n-\t\tif (!strcmp(argv[0], \"list\"))\n-\t\t\treturn sparse_checkout_list(argc, argv);\n-\t\tif (!strcmp(argv[0], \"init\"))\n-\t\t\treturn sparse_checkout_init(argc, argv);\n-\t\tif (!strcmp(argv[0], \"set\"))\n-\t\t\treturn sparse_checkout_set(argc, argv, prefix);\n-\t\tif (!strcmp(argv[0], \"add\"))\n-\t\t\treturn sparse_checkout_add(argc, argv, prefix);\n-\t\tif (!strcmp(argv[0], \"reapply\"))\n-\t\t\treturn sparse_checkout_reapply(argc, argv);\n-\t\tif (!strcmp(argv[0], \"disable\"))\n-\t\t\treturn sparse_checkout_disable(argc, argv);\n-\t}\n-\n-\tusage_with_options(builtin_sparse_checkout_usage,\n-\t\t\t   builtin_sparse_checkout_options);\n+\treturn fn(argc, argv, prefix);\n }\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461568","messageId":"20220819160411.1791200-2-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 01/20] git.c: update NO_PARSEOPT markings","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:52Z","receivedAt":"2022-08-19T16:33:06Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Our Bash completion script can complete --options for commands using\nparse-options even when that command doesn't have a dedicated\ncompletion function, but to do so the completion script must know\nwhich commands use parse-options and which don't.  Therefore, commands\nnot using parse-options are marked in 'git.c's command list with the\nNO_PARSEOPT flag.\n\nUpdate this list, and remove this flag from the commands that by now\nuse parse-options.\n\nAfter this change we can TAB complete --options of the plumbing\ncommands 'commit-tree', 'mailinfo' and 'mktag'.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n git.c | 10 +++++-----\n 1 file changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/git.c b/git.c\nindex e5d62fa5a9..09c126c33d 100644\n--- a/git.c\n+++ b/git.c\n@@ -489,14 +489,14 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)\n static struct cmd_struct commands[] = {\n \t{ \"add\", cmd_add, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"am\", cmd_am, RUN_SETUP | NEED_WORK_TREE },\n-\t{ \"annotate\", cmd_annotate, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"annotate\", cmd_annotate, RUN_SETUP },\n \t{ \"apply\", cmd_apply, RUN_SETUP_GENTLY },\n \t{ \"archive\", cmd_archive, RUN_SETUP_GENTLY },\n \t{ \"bisect--helper\", cmd_bisect__helper, RUN_SETUP },\n \t{ \"blame\", cmd_blame, RUN_SETUP },\n \t{ \"branch\", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },\n \t{ \"bugreport\", cmd_bugreport, RUN_SETUP_GENTLY },\n-\t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },\n+\t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY },\n \t{ \"cat-file\", cmd_cat_file, RUN_SETUP },\n \t{ \"check-attr\", cmd_check_attr, RUN_SETUP },\n \t{ \"check-ignore\", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },\n@@ -514,7 +514,7 @@ static struct cmd_struct commands[] = {\n \t{ \"column\", cmd_column, RUN_SETUP_GENTLY },\n \t{ \"commit\", cmd_commit, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"commit-graph\", cmd_commit_graph, RUN_SETUP },\n-\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP },\n \t{ \"config\", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },\n \t{ \"count-objects\", cmd_count_objects, RUN_SETUP },\n \t{ \"credential\", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },\n@@ -553,7 +553,7 @@ static struct cmd_struct commands[] = {\n \t{ \"ls-files\", cmd_ls_files, RUN_SETUP },\n \t{ \"ls-remote\", cmd_ls_remote, RUN_SETUP_GENTLY },\n \t{ \"ls-tree\", cmd_ls_tree, RUN_SETUP },\n-\t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },\n+\t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY },\n \t{ \"mailsplit\", cmd_mailsplit, NO_PARSEOPT },\n \t{ \"maintenance\", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },\n \t{ \"merge\", cmd_merge, RUN_SETUP | NEED_WORK_TREE },\n@@ -566,7 +566,7 @@ static struct cmd_struct commands[] = {\n \t{ \"merge-recursive-theirs\", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },\n \t{ \"merge-subtree\", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },\n \t{ \"merge-tree\", cmd_merge_tree, RUN_SETUP },\n-\t{ \"mktag\", cmd_mktag, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"mktag\", cmd_mktag, RUN_SETUP },\n \t{ \"mktree\", cmd_mktree, RUN_SETUP },\n \t{ \"multi-pack-index\", cmd_multi_pack_index, RUN_SETUP },\n \t{ \"mv\", cmd_mv, RUN_SETUP | NEED_WORK_TREE },\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461569","messageId":"20220819160411.1791200-15-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 14/20] builtin/multi-pack-index.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:05Z","receivedAt":"2022-08-19T16:33:13Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git multi-pack-index' parses its subcommands with a couple of if-else\nif statements.  parse-options has just learned to parse subcommands,\nso let's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nNote that the functions implementing each subcommand only accept the\n'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter\nto make them match the type expected by parse-options, and thus avoid\ncasting function pointers.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/multi-pack-index.c | 51 ++++++++++++++++----------------------\n 1 file changed, 22 insertions(+), 29 deletions(-)\n\ndiff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c\nindex 8f24d59a75..b8320d597b 100644\n--- a/builtin/multi-pack-index.c\n+++ b/builtin/multi-pack-index.c\n@@ -104,7 +104,8 @@ static void read_packs_from_stdin(struct string_list *to)\n \tstrbuf_release(&buf);\n }\n \n-static int cmd_multi_pack_index_write(int argc, const char **argv)\n+static int cmd_multi_pack_index_write(int argc, const char **argv,\n+\t\t\t\t      const char *prefix)\n {\n \tstruct option *options;\n \tstatic struct option builtin_multi_pack_index_write_options[] = {\n@@ -160,7 +161,8 @@ static int cmd_multi_pack_index_write(int argc, const char **argv)\n \t\t\t       opts.refs_snapshot, opts.flags);\n }\n \n-static int cmd_multi_pack_index_verify(int argc, const char **argv)\n+static int cmd_multi_pack_index_verify(int argc, const char **argv,\n+\t\t\t\t       const char *prefix)\n {\n \tstruct option *options;\n \tstatic struct option builtin_multi_pack_index_verify_options[] = {\n@@ -186,7 +188,8 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv)\n \treturn verify_midx_file(the_repository, opts.object_dir, opts.flags);\n }\n \n-static int cmd_multi_pack_index_expire(int argc, const char **argv)\n+static int cmd_multi_pack_index_expire(int argc, const char **argv,\n+\t\t\t\t       const char *prefix)\n {\n \tstruct option *options;\n \tstatic struct option builtin_multi_pack_index_expire_options[] = {\n@@ -212,7 +215,8 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv)\n \treturn expire_midx_packs(the_repository, opts.object_dir, opts.flags);\n }\n \n-static int cmd_multi_pack_index_repack(int argc, const char **argv)\n+static int cmd_multi_pack_index_repack(int argc, const char **argv,\n+\t\t\t\t       const char *prefix)\n {\n \tstruct option *options;\n \tstatic struct option builtin_multi_pack_index_repack_options[] = {\n@@ -247,7 +251,15 @@ int cmd_multi_pack_index(int argc, const char **argv,\n \t\t\t const char *prefix)\n {\n \tint res;\n-\tstruct option *builtin_multi_pack_index_options = common_opts;\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option builtin_multi_pack_index_options[] = {\n+\t\tOPT_SUBCOMMAND(\"repack\", &fn, cmd_multi_pack_index_repack),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_multi_pack_index_write),\n+\t\tOPT_SUBCOMMAND(\"verify\", &fn, cmd_multi_pack_index_verify),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_multi_pack_index_expire),\n+\t\tOPT_END(),\n+\t};\n+\tstruct option *options = parse_options_concat(builtin_multi_pack_index_options, common_opts);\n \n \tgit_config(git_default_config, NULL);\n \n@@ -256,31 +268,12 @@ int cmd_multi_pack_index(int argc, const char **argv,\n \t    the_repository->objects->odb)\n \t\topts.object_dir = xstrdup(the_repository->objects->odb->path);\n \n-\targc = parse_options(argc, argv, prefix,\n-\t\t\t     builtin_multi_pack_index_options,\n-\t\t\t     builtin_multi_pack_index_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n-\n-\tif (!argc)\n-\t\tgoto usage;\n-\n-\tif (!strcmp(argv[0], \"repack\"))\n-\t\tres = cmd_multi_pack_index_repack(argc, argv);\n-\telse if (!strcmp(argv[0], \"write\"))\n-\t\tres =  cmd_multi_pack_index_write(argc, argv);\n-\telse if (!strcmp(argv[0], \"verify\"))\n-\t\tres =  cmd_multi_pack_index_verify(argc, argv);\n-\telse if (!strcmp(argv[0], \"expire\"))\n-\t\tres =  cmd_multi_pack_index_expire(argc, argv);\n-\telse {\n-\t\terror(_(\"unrecognized subcommand: %s\"), argv[0]);\n-\t\tgoto usage;\n-\t}\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     builtin_multi_pack_index_usage, 0);\n+\tFREE_AND_NULL(options);\n+\n+\tres = fn(argc, argv, prefix);\n \n \tfree(opts.object_dir);\n \treturn res;\n-\n-usage:\n-\tusage_with_options(builtin_multi_pack_index_usage,\n-\t\t\t   builtin_multi_pack_index_options);\n }\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461570","messageId":"20220819160411.1791200-5-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:55Z","receivedAt":"2022-08-19T16:33:16Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"In 't0040-parse-options.sh' we thoroughly test the parsing of all\ntypes and forms of options, but in all those tests parse_options() is\nalways invoked with a 0 flags parameter.\n\nAdd a few tests to demonstrate how various 'enum parse_opt_flags'\nvalues are supposed to influence option parsing.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/helper/test-parse-options.c | 66 +++++++++++++++++++++++++++++++++\n t/helper/test-tool.c          |  1 +\n t/helper/test-tool.h          |  1 +\n t/t0040-parse-options.sh      | 70 +++++++++++++++++++++++++++++++++++\n 4 files changed, 138 insertions(+)\n\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 48d3cf6692..88919785d3 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -192,3 +192,69 @@ int cmd__parse_options(int argc, const char **argv)\n \n \treturn ret;\n }\n+\n+static void print_args(int argc, const char **argv)\n+{\n+\tfor (int i = 0; i < argc; i++)\n+\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n+}\n+\n+static int parse_options_flags__cmd(int argc, const char **argv,\n+\t\t\t\t    enum parse_opt_flags test_flags)\n+{\n+\tconst char *usage[] = {\n+\t\t\"<...> cmd [options]\",\n+\t\tNULL\n+\t};\n+\tint opt = 0;\n+\tconst struct option options[] = {\n+\t\tOPT_INTEGER('o', \"opt\", &opt, \"an integer option\"),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, options, usage, test_flags);\n+\n+\tprintf(\"opt: %d\\n\", opt);\n+\tprint_args(argc, argv);\n+\n+\treturn 0;\n+}\n+\n+static enum parse_opt_flags test_flags = 0;\n+static const struct option test_flag_options[] = {\n+\tOPT_GROUP(\"flag-options:\"),\n+\tOPT_BIT(0, \"keep-dashdash\", &test_flags,\n+\t\t\"pass PARSE_OPT_KEEP_DASHDASH to parse_options()\",\n+\t\tPARSE_OPT_KEEP_DASHDASH),\n+\tOPT_BIT(0, \"stop-at-non-option\", &test_flags,\n+\t\t\"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()\",\n+\t\tPARSE_OPT_STOP_AT_NON_OPTION),\n+\tOPT_BIT(0, \"keep-argv0\", &test_flags,\n+\t\t\"pass PARSE_OPT_KEEP_ARGV0 to parse_options()\",\n+\t\tPARSE_OPT_KEEP_ARGV0),\n+\tOPT_BIT(0, \"keep-unknown\", &test_flags,\n+\t\t\"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()\",\n+\t\tPARSE_OPT_KEEP_UNKNOWN),\n+\tOPT_BIT(0, \"no-internal-help\", &test_flags,\n+\t\t\"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()\",\n+\t\tPARSE_OPT_NO_INTERNAL_HELP),\n+\tOPT_END()\n+};\n+\n+int cmd__parse_options_flags(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool parse-options-flags [flag-options] cmd [options]\",\n+\t\tNULL\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, test_flag_options, usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n+\t\terror(\"'cmd' is mandatory\");\n+\t\tusage_with_options(usage, test_flag_options);\n+\t}\n+\n+\treturn parse_options_flags__cmd(argc, argv, test_flags);\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex 318fdbab0c..6e62282b60 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -51,6 +51,7 @@ static struct test_cmd cmds[] = {\n \t{ \"online-cpus\", cmd__online_cpus },\n \t{ \"pack-mtimes\", cmd__pack_mtimes },\n \t{ \"parse-options\", cmd__parse_options },\n+\t{ \"parse-options-flags\", cmd__parse_options_flags },\n \t{ \"parse-pathspec-file\", cmd__parse_pathspec_file },\n \t{ \"partial-clone\", cmd__partial_clone },\n \t{ \"path-utils\", cmd__path_utils },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex bb79927163..d8e8403d70 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -41,6 +41,7 @@ int cmd__oidtree(int argc, const char **argv);\n int cmd__online_cpus(int argc, const char **argv);\n int cmd__pack_mtimes(int argc, const char **argv);\n int cmd__parse_options(int argc, const char **argv);\n+int cmd__parse_options_flags(int argc, const char **argv);\n int cmd__parse_pathspec_file(int argc, const char** argv);\n int cmd__partial_clone(int argc, const char **argv);\n int cmd__path_utils(int argc, const char **argv);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ed2fb620a9..8511ce24bb 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -456,4 +456,74 @@ test_expect_success '--end-of-options treats remainder as args' '\n \t    --end-of-options --verbose\n '\n \n+test_expect_success 'KEEP_DASHDASH works' '\n+\ttest-tool parse-options-flags --keep-dashdash cmd --opt=1 -- --opt=2 --unknown >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 1\n+\targ 00: --\n+\targ 01: --opt=2\n+\targ 02: --unknown\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'KEEP_ARGV0 works' '\n+\ttest-tool parse-options-flags --keep-argv0 cmd arg0 --opt=3 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 3\n+\targ 00: cmd\n+\targ 01: arg0\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'STOP_AT_NON_OPTION works' '\n+\ttest-tool parse-options-flags --stop-at-non-option cmd --opt=4 arg0 --opt=5 --unknown >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 4\n+\targ 00: arg0\n+\targ 01: --opt=5\n+\targ 02: --unknown\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'KEEP_UNKNOWN works' '\n+\ttest-tool parse-options-flags --keep-unknown cmd --unknown=1 --opt=6 -u2 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 6\n+\targ 00: --unknown=1\n+\targ 01: -u2\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'NO_INTERNAL_HELP works for -h' '\n+\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n+\tcat err &&\n+\tgrep \"^error: unknown switch \\`h$SQ\" err &&\n+\tgrep \"^usage: \" err\n+'\n+\n+for help_opt in help help-all\n+do\n+\ttest_expect_success \"NO_INTERNAL_HELP works for --$help_opt\" \"\n+\t\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&\n+\t\tcat err &&\n+\t\tgrep '^error: unknown option \\`'$help_opt\\' err &&\n+\t\tgrep '^usage: ' err\n+\t\"\n+done\n+\n+test_expect_success 'KEEP_UNKNOWN | NO_INTERNAL_HELP works' '\n+\ttest-tool parse-options-flags --keep-unknown --no-internal-help cmd -h --help --help-all >actual &&\n+\tcat >expect <<-\\EOF &&\n+\topt: 0\n+\targ 00: -h\n+\targ 01: --help\n+\targ 02: --help-all\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461571","messageId":"20220819160411.1791200-9-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:59Z","receivedAt":"2022-08-19T16:33:18Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The output of 'git <cmd> --git-completion-helper' always starts with a\nspace, e.g.:\n\n  $ git config --git-completion-helper\n   --global --system --local [...]\n\nThis doesn't matter for the completion script, because field splitting\ndiscards that space anyway.\n\nHowever, later patches in this series will teach parse-options to\nhandle subcommands, and subcommands will be included in the completion\nhelper output as well.  This will make the loop printing options (and\nsubcommands) a tad more complex, so I wanted to test the result.  The\ntest would have to account for the presence of that leading space,\nwhich bugged my OCD, so let's get rid of it.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n parse-options.c | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex a0a2cf98fa..8748f88e6f 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -620,7 +620,8 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \t\t\tsuffix = \"=\";\n \t\tif (starts_with(opts->long_name, \"no-\"))\n \t\t\tnr_noopts++;\n-\t\tprintf(\" --%s%s\", opts->long_name, suffix);\n+\t\tprintf(\"%s--%s%s\", opts == original_opts ? \"\" : \" \",\n+\t\t       opts->long_name, suffix);\n \t}\n \tshow_negated_gitcomp(original_opts, show_all, -1);\n \tshow_negated_gitcomp(original_opts, show_all, nr_noopts);\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461572","messageId":"20220819160411.1791200-1-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220725123857.2773963-1-szeder.dev@gmail.com","subject":"[PATCH v2 00/20] parse-options: handle subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:51Z","receivedAt":"2022-08-19T16:33:20Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Several Git commands have subcommands to implement mutually exclusive\n\"operation modes\", and they usually parse their subcommand argument\nwith a bunch of if-else if statements.\n\nTeach parse-options to handle subcommands as well, which will result\nin shorter and simpler code with consistent error handling and error\nmessages on unknown or missing subcommand, and it will also make\npossible for our Bash completion script to handle subcommands\nprogrammatically in a follow-up series.\n\nChanges since v1:\n\n  - Commit message updates.\n  - Put the troublesome subcommand arg between `' in error messages.\n  - Use designated initializers for OPT_SUBCOMMAND.\n  - Remove the unused 'kept_unknown' flag from 'struct parse_opt_ctx_t'\n    (it was used in WIP version of this patch series, that I didn't manage\n    to remove completely).\n  - Drop cmd_worktree()'s NO_PARSEOPT flag in the right patch.\n  - Use 'git -C ...' instead of ( cd ... && ... ) in t5505's new tests.\n  - Update API doc and BUG() string to mention PARSE_OPT_KEEP_UNKNOWN_OPT.\n  - Minor test helper refactorings.\n\n\nSZEDER Gábor (20):\n  git.c: update NO_PARSEOPT markings\n  t3301-notes.sh: check that default operation mode doesn't take\n    arguments\n  t5505-remote.sh: check the behavior without a subcommand\n  t0040-parse-options: test parse_options() with various\n    'parse_opt_flags'\n  api-parse-options.txt: fix description of OPT_CMDMODE\n  parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options\n  parse-options: clarify the limitations of PARSE_OPT_NODASH\n  parse-options: drop leading space from '--git-completion-helper'\n    output\n  parse-options: add support for parsing subcommands\n  builtin/bundle.c: let parse-options parse subcommands\n  builtin/commit-graph.c: let parse-options parse subcommands\n  builtin/gc.c: let parse-options parse 'git maintenance's subcommands\n  builtin/hook.c: let parse-options parse subcommands\n  builtin/multi-pack-index.c: let parse-options parse subcommands\n  builtin/notes.c: let parse-options parse subcommands\n  builtin/reflog.c: let parse-options parse subcommands\n  builtin/remote.c: let parse-options parse subcommands\n  builtin/sparse-checkout.c: let parse-options parse subcommands\n  builtin/stash.c: let parse-options parse subcommands\n  builtin/worktree.c: let parse-options parse subcommands\n\n Documentation/technical/api-parse-options.txt |  49 +++-\n builtin/archive.c                             |   2 +-\n builtin/bisect--helper.c                      |   2 +-\n builtin/blame.c                               |   1 +\n builtin/bundle.c                              |  25 +-\n builtin/commit-graph.c                        |  30 +--\n builtin/difftool.c                            |   2 +-\n builtin/env--helper.c                         |   2 +-\n builtin/fast-export.c                         |   2 +-\n builtin/gc.c                                  |  42 +--\n builtin/hook.c                                |  12 +-\n builtin/log.c                                 |   4 +-\n builtin/multi-pack-index.c                    |  51 ++--\n builtin/notes.c                               |  43 ++-\n builtin/reflog.c                              |  43 +--\n builtin/remote.c                              |  70 +++--\n builtin/revert.c                              |   2 +-\n builtin/shortlog.c                            |   1 +\n builtin/sparse-checkout.c                     |  48 ++--\n builtin/stash.c                               |  59 ++--\n builtin/worktree.c                            |  31 +--\n diff.c                                        |   2 +-\n git.c                                         |  14 +-\n parse-options.c                               | 118 +++++++-\n parse-options.h                               |  29 +-\n t/helper/test-parse-options.c                 | 127 +++++++++\n t/helper/test-serve-v2.c                      |   2 +-\n t/helper/test-tool.c                          |   2 +\n t/helper/test-tool.h                          |   2 +\n t/t0040-parse-options.sh                      | 255 ++++++++++++++++++\n t/t3301-notes.sh                              |   5 +\n t/t3903-stash.sh                              |   2 +-\n t/t5318-commit-graph.sh                       |   4 +-\n t/t5505-remote.sh                             |  20 ++\n t/t7900-maintenance.sh                        |  10 +-\n 35 files changed, 803 insertions(+), 310 deletions(-)\n\nRange-diff:\n 1:  78448310f9 !  1:  8128131f7b git.c: update NO_PARSEOPT markings\n    @@ Commit message\n         Update this list, and remove this flag from the commands that by now\n         use parse-options.\n     \n    +    After this change we can TAB complete --options of the plumbing\n    +    commands 'commit-tree', 'mailinfo' and 'mktag'.\n    +\n         Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n     \n      ## git.c ##\n    @@ git.c: static struct cmd_struct commands[] = {\n      \t{ \"mktree\", cmd_mktree, RUN_SETUP },\n      \t{ \"multi-pack-index\", cmd_multi_pack_index, RUN_SETUP },\n      \t{ \"mv\", cmd_mv, RUN_SETUP | NEED_WORK_TREE },\n    -@@ git.c: static struct cmd_struct commands[] = {\n    - \t{ \"verify-tag\", cmd_verify_tag, RUN_SETUP },\n    - \t{ \"version\", cmd_version },\n    - \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP },\n    --\t{ \"worktree\", cmd_worktree, RUN_SETUP | NO_PARSEOPT },\n    -+\t{ \"worktree\", cmd_worktree, RUN_SETUP },\n    - \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n    - };\n    - \n 2:  dee73c9832 =  2:  a8895169e7 t3301-notes.sh: check that default operation mode doesn't take arguments\n 3:  4ce0c7717b !  3:  ecc7ca97e7 t5505-remote.sh: check the behavior without a subcommand\n    @@ t/t5505-remote.sh: test_expect_success 'add invalid foreign_vcs remote' '\n      '\n      \n     +test_expect_success 'without subcommand' '\n    -+\t(\n    -+\t\tcd test &&\n    -+\t\tgit remote >actual &&\n    -+\t\techo origin >expect &&\n    -+\t\ttest_cmp expect actual\n    -+\t)\n    ++\techo origin >expect &&\n    ++\tgit -C test remote >actual &&\n    ++\ttest_cmp expect actual\n     +'\n     +\n     +test_expect_success 'without subcommand accepts -v' '\n    -+\tcat >test/expect <<-EOF &&\n    ++\tcat >expect <<-EOF &&\n     +\torigin\t$(pwd)/one (fetch)\n     +\torigin\t$(pwd)/one (push)\n     +\tEOF\n    -+\t(\n    -+\t\tcd test &&\n    -+\t\tgit remote -v >actual &&\n    -+\t\ttest_cmp expect actual\n    -+\t)\n    ++\tgit -C test remote -v >actual &&\n    ++\ttest_cmp expect actual\n     +'\n     +\n     +test_expect_success 'without subcommand does not take arguments' '\n    -+\t(\n    -+\t\tcd test &&\n    -+\t\ttest_expect_code 129 git remote origin 2>err &&\n    -+\t\tgrep \"^error: Unknown subcommand:\" err\n    -+\t)\n    ++\ttest_expect_code 129 git -C test remote origin 2>err &&\n    ++\tgrep \"^error: Unknown subcommand:\" err\n     +'\n     +\n      cat >test/expect <<EOF\n 4:  7e7bf30b32 !  4:  84c1aa3318 t0040-parse-options: test parse_options() with various 'parse_opt_flags'\n    @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **arg\n      \treturn ret;\n      }\n     +\n    ++static void print_args(int argc, const char **argv)\n    ++{\n    ++\tfor (int i = 0; i < argc; i++)\n    ++\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n    ++}\n    ++\n     +static int parse_options_flags__cmd(int argc, const char **argv,\n     +\t\t\t\t    enum parse_opt_flags test_flags)\n     +{\n    @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **arg\n     +\targc = parse_options(argc, argv, NULL, options, usage, test_flags);\n     +\n     +\tprintf(\"opt: %d\\n\", opt);\n    -+\tfor (int i = 0; i < argc; i++)\n    -+\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n    ++\tprint_args(argc, argv);\n     +\n     +\treturn 0;\n     +}\n    @@ t/t0040-parse-options.sh: test_expect_success '--end-of-options treats remainder\n     +test_expect_success 'NO_INTERNAL_HELP works for -h' '\n     +\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n     +\tcat err &&\n    -+\tgrep \"^error: unknown switch \\`h'\\''\" err &&\n    ++\tgrep \"^error: unknown switch \\`h$SQ\" err &&\n     +\tgrep \"^usage: \" err\n     +'\n     +\n 5:  8515609ac2 =  5:  40b51c9992 api-parse-options.txt: fix description of OPT_CMDMODE\n 6:  60885e91cf =  6:  7f561992a5 parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options\n 7:  2e1d40b4f0 =  7:  27a66b0fcd parse-options: clarify the limitations of PARSE_OPT_NODASH\n 8:  2e4f9259ba =  8:  722a47874a parse-options: drop leading space from '--git-completion-helper' output\n 9:  caf0a395df !  9:  b65385a02d parse-options: add support for parsing subcommands\n    @@ Commit message\n             return fn(argc, argv, prefix);\n     \n         Here each OPT_SUBCOMMAND specifies the name of the subcommand and the\n    -    function implementing it, and the same pointer to 'fn' where\n    -    parse_options() will store the function associated with the given\n    -    subcommand.  There is no need to check whether 'fn' is non-NULL before\n    -    invoking it: if there were no subcommands given on the command line\n    -    then the parse_options() call would error out and show usage.  In case\n    -    a command has a default operation mode, 'fn' should be initialized to\n    -    the function implementing that mode, and parse_options() should be\n    -    invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.\n    +    function implementing it, and the address of the same 'fn' subcommand\n    +    function pointer.  parse_options() then processes the arguments until\n    +    it finds the first argument matching one of the subcommands, sets 'fn'\n    +    to the function associated with that subcommand, and returns, leaving\n    +    the rest of the arguments unprocessed.  If none of the listed\n    +    subcommands is found among the arguments, parse_options() will show\n    +    usage and abort.\n    +\n    +    If a command has a default operation mode, 'fn' should be initialized\n    +    to the function implementing that mode, and parse_options() should be\n    +    invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.  In this case\n    +    parse_options() won't error out when not finding any subcommands, but\n    +    will return leaving 'fn' unchanged.  Note that if that default\n    +    operation mode has any --options, then the PARSE_OPT_KEEP_UNKNOWN_OPT\n    +    flag is necessary as well (otherwise parse_options() would error out\n    +    upon seeing the unknown option meant to the default operation mode).\n     \n         Some thoughts about the implementation:\n     \n    -      - Arguably it is a bit weird that the same pointer to 'fn' have to\n    -        be specified as 'value' for each OPT_SUBCOMMAND, but we need a way\n    -        to tell parse_options() where to put the function associated with\n    -        the given subcommand, and I didn't like the alternatives:\n    +      - The same pointer to 'fn' must be specified as 'value' for each\n    +        OPT_SUBCOMMAND, because there can be only one set of mutually\n    +        exclusive subcommands; parse_options() will BUG() otherwise.\n    +\n    +        There are other ways to tell parse_options() where to put the\n    +        function associated with the subcommand given on the command line,\n    +        but I didn't like them:\n     \n               - Change parse_options()'s signature by adding a pointer to\n                 subcommand function to be set to the function associated with\n    @@ Documentation/technical/api-parse-options.txt: Flags are the bitwise-or of:\n     +\tDon't error out when no subcommand is specified.\n     +\n     +Note that `PARSE_OPT_STOP_AT_NON_OPTION` is incompatible with subcommands;\n    -+while `PARSE_OPT_KEEP_DASHDASH` and `PARSE_OPT_KEEP_UNKNOWN` can only be\n    ++while `PARSE_OPT_KEEP_DASHDASH` and `PARSE_OPT_KEEP_UNKNOWN_OPT` can only be\n     +used with subcommands when combined with `PARSE_OPT_SUBCOMMAND_OPTIONAL`.\n     +\n      Data Structure\n    @@ parse-options.c: static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n     +\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION\");\n     +\t\tif (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n     +\t\t\tif (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)\n    -+\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n    ++\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN_OPT unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n     +\t\t\tif (flags & PARSE_OPT_KEEP_DASHDASH)\n     +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n     +\t\t}\n    @@ parse-options.c: enum parse_opt_result parse_options_step(struct parse_opt_ctx_t\n     +\t\t\t\t\t * So we are done parsing.\n     +\t\t\t\t\t */\n     +\t\t\t\t\treturn PARSE_OPT_DONE;\n    -+\t\t\t\terror(_(\"unknown subcommand: %s\"), arg);\n    ++\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n     +\t\t\t\tusage_with_options(usagestr, options);\n     +\t\t\tcase PARSE_OPT_COMPLETE:\n     +\t\t\tcase PARSE_OPT_HELP:\n     +\t\t\tcase PARSE_OPT_ERROR:\n     +\t\t\tcase PARSE_OPT_DONE:\n     +\t\t\tcase PARSE_OPT_NON_OPTION:\n    ++\t\t\t\t/* Impossible. */\n     +\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n     +\t\t\t}\n      \t\t}\n    @@ parse-options.c: enum parse_opt_result parse_options_step(struct parse_opt_ctx_t\n     +\t\t}\n      \t\tif (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT))\n      \t\t\treturn PARSE_OPT_UNKNOWN;\n    -+\t\tctx->kept_unknown = 1;\n      \t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n    - \t\tctx->opt = NULL;\n    - \t}\n     @@ parse-options.c: int parse_options(int argc, const char **argv,\n      \tcase PARSE_OPT_COMPLETE:\n      \t\texit(0);\n    @@ parse-options.h: struct option {\n      #define OPT_ALIAS(s, l, source_long_name) \\\n      \t{ OPTION_ALIAS, (s), (l), (source_long_name) }\n      \n    -+#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n    -+\t\t\t\t\tNULL, 0, NULL, 0, NULL, 0, (fn) }\n    -+#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \\\n    -+\t\t\t\t\tNULL, (f), NULL, 0, NULL, 0, (fn) }\n    ++#define OPT_SUBCOMMAND_F(l, v, fn, f) { \\\n    ++\t.type = OPTION_SUBCOMMAND, \\\n    ++\t.long_name = (l), \\\n    ++\t.value = (v), \\\n    ++\t.flags = (f), \\\n    ++\t.subcommand_fn = (fn) }\n    ++#define OPT_SUBCOMMAND(l, v, fn)    OPT_SUBCOMMAND_F((l), (v), (fn), 0)\n     +\n      /*\n       * parse_options() will filter out the processed options and leave the\n    @@ parse-options.h: struct parse_opt_ctx_t {\n      \tint argc, cpidx, total;\n      \tconst char *opt;\n      \tenum parse_opt_flags flags;\n    -+\tunsigned has_subcommands:1,\n    -+\t\t kept_unknown:1;\n    ++\tunsigned has_subcommands;\n      \tconst char *prefix;\n      \tconst char **alias_groups; /* must be in groups of 3 elements! */\n      \tstruct option *updated_options;\n    @@ t/helper/test-parse-options.c: int cmd__parse_options_flags(int argc, const char\n      \treturn parse_options_flags__cmd(argc, argv, test_flags);\n      }\n     +\n    -+static void print_subcommand_args(const char *fn_name, int argc,\n    -+\t\t\t\t  const char **argv)\n    -+{\n    -+\tint i;\n    -+\tprintf(\"fn: %s\\n\", fn_name);\n    -+\tfor (i = 0; i < argc; i++)\n    -+\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n    -+}\n    -+\n     +static int subcmd_one(int argc, const char **argv, const char *prefix)\n     +{\n    -+\tprint_subcommand_args(\"subcmd_one\", argc, argv);\n    ++\tprintf(\"fn: subcmd_one\\n\");\n    ++\tprint_args(argc, argv);\n     +\treturn 0;\n     +}\n     +\n     +static int subcmd_two(int argc, const char **argv, const char *prefix)\n     +{\n    -+\tprint_subcommand_args(\"subcmd_two\", argc, argv);\n    ++\tprintf(\"fn: subcmd_two\\n\");\n    ++\tprint_args(argc, argv);\n     +\treturn 0;\n     +}\n     +\n    @@ t/t0040-parse-options.sh: test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HE\n     +\n     +test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n     +\ttest_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&\n    -+\tgrep \"^error: unknown subcommand: nope\" err &&\n    ++\tgrep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n     +\tgrep ^usage: err\n     +'\n     +\n     +test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n     +\ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n    -+\tgrep \"^error: unknown subcommand: subcmd-o$\" err &&\n    ++\tgrep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n     +\tgrep ^usage: err\n     +'\n     +\n     +test_expect_success 'subcommand - no negated subcommands' '\n     +\ttest_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&\n    -+\tgrep \"^error: unknown subcommand: no-subcmd-one\" err &&\n    ++\tgrep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n     +\tgrep ^usage: err\n     +'\n     +\n10:  cac17fa063 = 10:  3526a93d90 builtin/bundle.c: let parse-options parse subcommands\n11:  9cfd5875fb ! 11:  a9533e14a5 builtin/commit-graph.c: let parse-options parse subcommands\n    @@ t/t5318-commit-graph.sh: test_expect_success 'usage' '\n      test_expect_success 'usage shown with an error on unknown sub-command' '\n      \tcat >expect <<-\\EOF &&\n     -\terror: unrecognized subcommand: unknown\n    -+\terror: unknown subcommand: unknown\n    ++\terror: unknown subcommand: `unknown'\\''\n      \tEOF\n      \ttest_expect_code 129 git commit-graph unknown 2>stderr &&\n      \tgrep error stderr >actual &&\n12:  cab2adf79b ! 12:  501a2bd1b5 builtin/gc.c: let parse-options parse 'git maintenance's subcommands\n    @@ t/t7900-maintenance.sh: test_systemd_analyze_verify () {\n     +\ttest_expect_code 129 git maintenance -h >actual &&\n     +\ttest_i18ngrep \"usage: git maintenance <subcommand>\" actual &&\n     +\ttest_expect_code 129 git maintenance barf 2>err &&\n    -+\ttest_i18ngrep \"unknown subcommand: barf\" err &&\n    ++\ttest_i18ngrep \"unknown subcommand: \\`barf'\\''\" err &&\n     +\ttest_i18ngrep \"usage: git maintenance\" err &&\n      \ttest_expect_code 129 git maintenance 2>err &&\n     +\ttest_i18ngrep \"error: need a subcommand\" err &&\n13:  6a16515cd8 ! 13:  a38a61756b builtin/hook.c: let parse-option parse subcommands\n    @@ Metadata\n     Author: SZEDER Gábor <szeder.dev@gmail.com>\n     \n      ## Commit message ##\n    -    builtin/hook.c: let parse-option parse subcommands\n    +    builtin/hook.c: let parse-options parse subcommands\n     \n         'git hook' parses its currently only subcommand with an if statement.\n         parse-options has just learned to parse subcommands, so let's use that\n14:  a5a7f28ced = 14:  f511717008 builtin/multi-pack-index.c: let parse-options parse subcommands\n15:  0104f48ae1 = 15:  220bbf16cf builtin/notes.c: let parse-options parse subcommands\n16:  250e12ddfc = 16:  d0e7329f42 builtin/reflog.c: let parse-options parse subcommands\n17:  90a2968469 ! 17:  9af71fd655 builtin/remote.c: let parse-options parse subcommands\n    @@ builtin/remote.c: static int set_url(int argc, const char **argv)\n      }\n     \n      ## t/t5505-remote.sh ##\n    -@@ t/t5505-remote.sh: test_expect_success 'without subcommand does not take arguments' '\n    - \t(\n    - \t\tcd test &&\n    - \t\ttest_expect_code 129 git remote origin 2>err &&\n    --\t\tgrep \"^error: Unknown subcommand:\" err\n    -+\t\tgrep \"^error: unknown subcommand:\" err\n    - \t)\n    +@@ t/t5505-remote.sh: test_expect_success 'without subcommand accepts -v' '\n    + \n    + test_expect_success 'without subcommand does not take arguments' '\n    + \ttest_expect_code 129 git -C test remote origin 2>err &&\n    +-\tgrep \"^error: Unknown subcommand:\" err\n    ++\tgrep \"^error: unknown subcommand:\" err\n      '\n      \n    + cat >test/expect <<EOF\n18:  dc5c071b51 = 18:  2bd2d40bb3 builtin/sparse-checkout.c: let parse-options parse subcommands\n19:  5c7cd8cbed = 19:  e8b7fdd2d8 builtin/stash.c: let parse-options parse subcommands\n20:  4d884dfe62 ! 20:  6d8108292f builtin/worktree.c: let parse-options parse subcommands\n    @@ builtin/worktree.c: static int repair(int ac, const char **av, const char *prefi\n     +\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n     +\treturn fn(ac, av, prefix);\n      }\n    +\n    + ## git.c ##\n    +@@ git.c: static struct cmd_struct commands[] = {\n    + \t{ \"verify-tag\", cmd_verify_tag, RUN_SETUP },\n    + \t{ \"version\", cmd_version },\n    + \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP },\n    +-\t{ \"worktree\", cmd_worktree, RUN_SETUP | NO_PARSEOPT },\n    ++\t{ \"worktree\", cmd_worktree, RUN_SETUP },\n    + \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n    + };\n    + \n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461573","messageId":"20220819160411.1791200-20-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 19/20] builtin/stash.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:10Z","receivedAt":"2022-08-19T16:33:27Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git stash' parses its subcommands with a long list of if-else if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nand listing subcommands for Bash completion.\n\nNote that the push_stash() function implementing the 'push' subcommand\naccepts an extra flag parameter to indicate whether push was assumed,\nso add a wrapper function with the standard subcommand function\nsignature.\n\nNote also that this change \"hides\" the '-h' option in 'git stash push\n-h' from the parse_option() call in cmd_stash(), as it comes after the\nsubcommand.  Consequently, from now on it will emit the usage of the\n'push' subcommand instead of the usage of 'git stash'.  We had a\nfailing test for this case, which can now be flipped to expect\nsuccess.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/stash.c  | 53 ++++++++++++++++++++++--------------------------\n t/t3903-stash.sh |  2 +-\n 2 files changed, 25 insertions(+), 30 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex a14e832e9f..1ba24c1173 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1739,6 +1739,11 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n \t\t\t     include_untracked, only_staged);\n }\n \n+static int push_stash_unassumed(int argc, const char **argv, const char *prefix)\n+{\n+\treturn push_stash(argc, argv, prefix, 0);\n+}\n+\n static int save_stash(int argc, const char **argv, const char *prefix)\n {\n \tint keep_index = -1;\n@@ -1787,15 +1792,28 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \tpid_t pid = getpid();\n \tconst char *index_file;\n \tstruct strvec args = STRVEC_INIT;\n-\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"apply\", &fn, apply_stash),\n+\t\tOPT_SUBCOMMAND(\"clear\", &fn, clear_stash),\n+\t\tOPT_SUBCOMMAND(\"drop\", &fn, drop_stash),\n+\t\tOPT_SUBCOMMAND(\"pop\", &fn, pop_stash),\n+\t\tOPT_SUBCOMMAND(\"branch\", &fn, branch_stash),\n+\t\tOPT_SUBCOMMAND(\"list\", &fn, list_stash),\n+\t\tOPT_SUBCOMMAND(\"show\", &fn, show_stash),\n+\t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n+\t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n+\t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n+\t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\n \n \tgit_config(git_stash_config, NULL);\n \n \targc = parse_options(argc, argv, prefix, options, git_stash_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.command_requires_full_index = 0;\n@@ -1804,33 +1822,10 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n \t\t    (uintmax_t)pid);\n \n-\tif (!argc)\n-\t\treturn !!push_stash(0, NULL, prefix, 0);\n-\telse if (!strcmp(argv[0], \"apply\"))\n-\t\treturn !!apply_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"clear\"))\n-\t\treturn !!clear_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"drop\"))\n-\t\treturn !!drop_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"pop\"))\n-\t\treturn !!pop_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"branch\"))\n-\t\treturn !!branch_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"list\"))\n-\t\treturn !!list_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"show\"))\n-\t\treturn !!show_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"store\"))\n-\t\treturn !!store_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"create\"))\n-\t\treturn !!create_stash(argc, argv, prefix);\n-\telse if (!strcmp(argv[0], \"push\"))\n-\t\treturn !!push_stash(argc, argv, prefix, 0);\n-\telse if (!strcmp(argv[0], \"save\"))\n-\t\treturn !!save_stash(argc, argv, prefix);\n-\telse if (*argv[0] != '-')\n-\t\tusage_msg_optf(_(\"unknown subcommand: %s\"),\n-\t\t\t       git_stash_usage, options, argv[0]);\n+\tif (fn)\n+\t\treturn !!fn(argc, argv, prefix);\n+\telse if (!argc)\n+\t\treturn !!push_stash_unassumed(0, NULL, prefix);\n \n \t/* Assume 'stash push' */\n \tstrvec_push(&args, \"push\");\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex 2a4c3fd61c..376cc8f4ab 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '\n \tgrep -F \"or: git stash show\" usage\n '\n \n-test_expect_failure 'usage for subcommands should emit subcommand usage' '\n+test_expect_success 'usage for subcommands should emit subcommand usage' '\n \ttest_expect_code 129 git stash push -h >usage &&\n \tgrep -F \"usage: git stash [push\" usage\n '\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461574","messageId":"20220819160411.1791200-7-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:57Z","receivedAt":"2022-08-19T16:33:30Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The description of 'PARSE_OPT_KEEP_UNKNOWN' starts with \"Keep unknown\narguments instead of erroring out\".  This is a bit misleading, as this\nflag only applies to unknown --options, while non-option arguments are\nkept even without this flag.\n\nUpdate the description to clarify this, and rename the flag to\nPARSE_OPTIONS_KEEP_UNKNOWN_OPT to make this obvious just by looking at\nthe flag name.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n Documentation/technical/api-parse-options.txt | 6 ++++--\n builtin/archive.c                             | 2 +-\n builtin/bisect--helper.c                      | 2 +-\n builtin/difftool.c                            | 2 +-\n builtin/env--helper.c                         | 2 +-\n builtin/fast-export.c                         | 2 +-\n builtin/log.c                                 | 4 ++--\n builtin/reflog.c                              | 4 ++--\n builtin/revert.c                              | 2 +-\n builtin/sparse-checkout.c                     | 4 ++--\n builtin/stash.c                               | 8 ++++----\n diff.c                                        | 2 +-\n parse-options.c                               | 6 +++---\n parse-options.h                               | 2 +-\n t/helper/test-parse-options.c                 | 6 +++---\n t/helper/test-serve-v2.c                      | 2 +-\n t/t0040-parse-options.sh                      | 8 ++++----\n 17 files changed, 33 insertions(+), 31 deletions(-)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 5a04f3daec..4412377fa3 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -90,8 +90,8 @@ Flags are the bitwise-or of:\n \tKeep the first argument, which contains the program name.  It's\n \tremoved from argv[] by default.\n \n-`PARSE_OPT_KEEP_UNKNOWN`::\n-\tKeep unknown arguments instead of erroring out.  This doesn't\n+`PARSE_OPT_KEEP_UNKNOWN_OPT`::\n+\tKeep unknown options instead of erroring out.  This doesn't\n \twork for all combinations of arguments as users might expect\n \tit to do.  E.g. if the first argument in `--unknown --known`\n \ttakes a value (which we can't know), the second one is\n@@ -101,6 +101,8 @@ Flags are the bitwise-or of:\n \tnon-option, not as a value belonging to the unknown option,\n \tthe parser early.  That's why parse_options() errors out if\n \tboth options are set.\n+\tNote that non-option arguments are always kept, even without\n+\tthis flag.\n \n `PARSE_OPT_NO_INTERNAL_HELP`::\n \tBy default, parse_options() handles `-h`, `--help` and\ndiff --git a/builtin/archive.c b/builtin/archive.c\nindex 7176b041b6..f094390ee0 100644\n--- a/builtin/archive.c\n+++ b/builtin/archive.c\n@@ -75,7 +75,7 @@ static int run_remote_archiver(int argc, const char **argv,\n \n #define PARSE_OPT_KEEP_ALL ( PARSE_OPT_KEEP_DASHDASH | \t\\\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | \t\\\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN |\t\\\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\t\\\n \t\t\t     PARSE_OPT_NO_INTERNAL_HELP\t)\n \n int cmd_archive(int argc, const char **argv, const char *prefix)\ndiff --git a/builtin/bisect--helper.c b/builtin/bisect--helper.c\nindex 8a052c7111..7097750fc6 100644\n--- a/builtin/bisect--helper.c\n+++ b/builtin/bisect--helper.c\n@@ -1324,7 +1324,7 @@ int cmd_bisect__helper(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options,\n \t\t\t     git_bisect_helper_usage,\n-\t\t\t     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (!cmdmode)\n \t\tusage_with_options(git_bisect_helper_usage, options);\ndiff --git a/builtin/difftool.c b/builtin/difftool.c\nindex b3c509b8de..8706f68492 100644\n--- a/builtin/difftool.c\n+++ b/builtin/difftool.c\n@@ -716,7 +716,7 @@ int cmd_difftool(int argc, const char **argv, const char *prefix)\n \tsymlinks = has_symlinks;\n \n \targc = parse_options(argc, argv, prefix, builtin_difftool_options,\n-\t\t\t     builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \tif (tool_help)\ndiff --git a/builtin/env--helper.c b/builtin/env--helper.c\nindex 27349098b0..ea04c16636 100644\n--- a/builtin/env--helper.c\n+++ b/builtin/env--helper.c\n@@ -50,7 +50,7 @@ int cmd_env__helper(int argc, const char **argv, const char *prefix)\n \t};\n \n \targc = parse_options(argc, argv, prefix, opts, env__helper_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \tif (env_default && !*env_default)\n \t\tusage_with_options(env__helper_usage, opts);\n \tif (!cmdmode)\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex e1748fb98b..bf3c20dea2 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -1221,7 +1221,7 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix)\n \trevs.sources = &revision_sources;\n \trevs.rewrite_parents = 1;\n \targc = parse_options(argc, argv, prefix, options, fast_export_usage,\n-\t\t\tPARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\tPARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \targc = setup_revisions(argc, argv, &revs, NULL);\n \tif (argc > 1)\n \t\tusage_with_options (fast_export_usage, options);\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 88a5e98875..fb84a0d399 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -199,7 +199,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \tmailmap = use_mailmap_config;\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_log_options, builtin_log_usage,\n-\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \tif (quiet)\n@@ -1926,7 +1926,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t */\n \targc = parse_options(argc, argv, prefix, builtin_format_patch_options,\n \t\t\t     builtin_format_patch_usage,\n-\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \t/* Make sure \"0000-$sub.patch\" gives non-negative length for $sub */\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 4dd297dce8..b8b1f4f8ea 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -223,7 +223,7 @@ static int cmd_reflog_show(int argc, const char **argv, const char *prefix)\n \n \tparse_options(argc, argv, prefix, options, reflog_show_usage,\n \t\t      PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |\n-\t\t      PARSE_OPT_KEEP_UNKNOWN);\n+\t\t      PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \treturn cmd_log_reflog(argc, argv, prefix);\n }\n@@ -410,7 +410,7 @@ int cmd_reflog(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options, reflog_usage,\n \t\t\t     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_NO_INTERNAL_HELP);\n \n \t/*\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex 2554f9099c..ee2a0807f0 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -141,7 +141,7 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)\n \n \targc = parse_options(argc, argv, NULL, options, usage_str,\n \t\t\tPARSE_OPT_KEEP_ARGV0 |\n-\t\t\tPARSE_OPT_KEEP_UNKNOWN);\n+\t\t\tPARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.command_requires_full_index = 0;\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex f91e29b56a..a5e4b95a9d 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -767,7 +767,7 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_sparse_checkout_add_options,\n \t\t\t     builtin_sparse_checkout_add_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tsanitize_paths(argc, argv, prefix, add_opts.skip_checks);\n \n@@ -813,7 +813,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_sparse_checkout_set_options,\n \t\t\t     builtin_sparse_checkout_set_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (update_modes(&set_opts.cone_mode, &set_opts.sparse_index))\n \t\treturn 1;\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 30fa101460..a14e832e9f 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -782,7 +782,7 @@ static int list_stash(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options,\n \t\t\t     git_stash_list_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (!ref_exists(ref_stash))\n \t\treturn 0;\n@@ -873,7 +873,7 @@ static int show_stash(int argc, const char **argv, const char *prefix)\n \tinit_revisions(&rev, prefix);\n \n \targc = parse_options(argc, argv, prefix, options, git_stash_show_usage,\n-\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n \tstrvec_push(&revision_args, argv[0]);\n@@ -979,7 +979,7 @@ static int store_stash(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options,\n \t\t\t     git_stash_store_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (argc != 1) {\n \t\tif (!quiet)\n@@ -1795,7 +1795,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \tgit_config(git_stash_config, NULL);\n \n \targc = parse_options(argc, argv, prefix, options, git_stash_usage,\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.command_requires_full_index = 0;\ndiff --git a/diff.c b/diff.c\nindex 974626a621..dd68281ba4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -5661,7 +5661,7 @@ int diff_opt_parse(struct diff_options *options,\n \n \tac = parse_options(ac, av, prefix, options->parseopts, NULL,\n \t\t\t   PARSE_OPT_KEEP_DASHDASH |\n-\t\t\t   PARSE_OPT_KEEP_UNKNOWN |\n+\t\t\t   PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t   PARSE_OPT_NO_INTERNAL_HELP |\n \t\t\t   PARSE_OPT_ONE_SHOT |\n \t\t\t   PARSE_OPT_STOP_AT_NON_OPTION);\ndiff --git a/parse-options.c b/parse-options.c\nindex edf55d3ef5..a0a2cf98fa 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -332,7 +332,7 @@ static enum parse_opt_result parse_long_opt(\n \t\t\trest = NULL;\n \t\tif (!rest) {\n \t\t\t/* abbreviated? */\n-\t\t\tif (!(p->flags & PARSE_OPT_KEEP_UNKNOWN) &&\n+\t\t\tif (!(p->flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&\n \t\t\t    !strncmp(long_name, arg, arg_end - arg)) {\n is_abbreviated:\n \t\t\t\tif (abbrev_option &&\n@@ -515,7 +515,7 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,\n \tctx->prefix = prefix;\n \tctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);\n \tctx->flags = flags;\n-\tif ((flags & PARSE_OPT_KEEP_UNKNOWN) &&\n+\tif ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&\n \t    (flags & PARSE_OPT_STOP_AT_NON_OPTION) &&\n \t    !(flags & PARSE_OPT_ONE_SHOT))\n \t\tBUG(\"STOP_AT_NON_OPTION and KEEP_UNKNOWN don't go together\");\n@@ -839,7 +839,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n unknown:\n \t\tif (ctx->flags & PARSE_OPT_ONE_SHOT)\n \t\t\tbreak;\n-\t\tif (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN))\n+\t\tif (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT))\n \t\t\treturn PARSE_OPT_UNKNOWN;\n \t\tctx->out[ctx->cpidx++] = ctx->argv[0];\n \t\tctx->opt = NULL;\ndiff --git a/parse-options.h b/parse-options.h\nindex 685fccac13..591df64191 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -30,7 +30,7 @@ enum parse_opt_flags {\n \tPARSE_OPT_KEEP_DASHDASH = 1 << 0,\n \tPARSE_OPT_STOP_AT_NON_OPTION = 1 << 1,\n \tPARSE_OPT_KEEP_ARGV0 = 1 << 2,\n-\tPARSE_OPT_KEEP_UNKNOWN = 1 << 3,\n+\tPARSE_OPT_KEEP_UNKNOWN_OPT = 1 << 3,\n \tPARSE_OPT_NO_INTERNAL_HELP = 1 << 4,\n \tPARSE_OPT_ONE_SHOT = 1 << 5,\n \tPARSE_OPT_SHELL_EVAL = 1 << 6,\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 88919785d3..99ad6fa4f0 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -232,9 +232,9 @@ static const struct option test_flag_options[] = {\n \tOPT_BIT(0, \"keep-argv0\", &test_flags,\n \t\t\"pass PARSE_OPT_KEEP_ARGV0 to parse_options()\",\n \t\tPARSE_OPT_KEEP_ARGV0),\n-\tOPT_BIT(0, \"keep-unknown\", &test_flags,\n-\t\t\"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()\",\n-\t\tPARSE_OPT_KEEP_UNKNOWN),\n+\tOPT_BIT(0, \"keep-unknown-opt\", &test_flags,\n+\t\t\"pass PARSE_OPT_KEEP_UNKNOWN_OPT to parse_options()\",\n+\t\tPARSE_OPT_KEEP_UNKNOWN_OPT),\n \tOPT_BIT(0, \"no-internal-help\", &test_flags,\n \t\t\"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()\",\n \t\tPARSE_OPT_NO_INTERNAL_HELP),\ndiff --git a/t/helper/test-serve-v2.c b/t/helper/test-serve-v2.c\nindex 28e905afc3..824e5c0a95 100644\n--- a/t/helper/test-serve-v2.c\n+++ b/t/helper/test-serve-v2.c\n@@ -24,7 +24,7 @@ int cmd__serve_v2(int argc, const char **argv)\n \t/* ignore all unknown cmdline switches for now */\n \targc = parse_options(argc, argv, prefix, options, serve_usage,\n \t\t\t     PARSE_OPT_KEEP_DASHDASH |\n-\t\t\t     PARSE_OPT_KEEP_UNKNOWN);\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n \tif (advertise_capabilities)\n \t\tprotocol_v2_advertise_capabilities();\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 8511ce24bb..264b737309 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -488,8 +488,8 @@ test_expect_success 'STOP_AT_NON_OPTION works' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'KEEP_UNKNOWN works' '\n-\ttest-tool parse-options-flags --keep-unknown cmd --unknown=1 --opt=6 -u2 >actual &&\n+test_expect_success 'KEEP_UNKNOWN_OPT works' '\n+\ttest-tool parse-options-flags --keep-unknown-opt cmd --unknown=1 --opt=6 -u2 >actual &&\n \tcat >expect <<-\\EOF &&\n \topt: 6\n \targ 00: --unknown=1\n@@ -515,8 +515,8 @@ do\n \t\"\n done\n \n-test_expect_success 'KEEP_UNKNOWN | NO_INTERNAL_HELP works' '\n-\ttest-tool parse-options-flags --keep-unknown --no-internal-help cmd -h --help --help-all >actual &&\n+test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '\n+\ttest-tool parse-options-flags --keep-unknown-opt --no-internal-help cmd -h --help --help-all >actual &&\n \tcat >expect <<-\\EOF &&\n \topt: 0\n \targ 00: -h\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461575","messageId":"20220819160411.1791200-6-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 05/20] api-parse-options.txt: fix description of OPT_CMDMODE","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:56Z","receivedAt":"2022-08-19T16:33:32Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The description of the 'OPT_CMDMODE' macro states that \"enum_val is\nset to int_var when ...\", but it's the other way around, 'int_var' is\nset to 'enum_val'.  Fix this.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n Documentation/technical/api-parse-options.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex acfd5dc1d8..5a04f3daec 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -236,7 +236,7 @@ There are some macros to easily define options:\n `OPT_CMDMODE(short, long, &int_var, description, enum_val)`::\n \tDefine an \"operation mode\" option, only one of which in the same\n \tgroup of \"operating mode\" options that share the same `int_var`\n-\tcan be given by the user. `enum_val` is set to `int_var` when the\n+\tcan be given by the user. `int_var` is set to `enum_val` when the\n \toption is used, but an error is reported if other \"operating mode\"\n \toption has already set its value to the same `int_var`.\n \n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461576","messageId":"20220819160411.1791200-21-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 20/20] builtin/worktree.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:04:11Z","receivedAt":"2022-08-19T16:33:36Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git worktree' parses its subcommands with a long list of if\nstatements.  parse-options has just learned to parse subcommands, so\nlet's use that facility instead, with the benefits of shorter code,\nhandling missing or unknown subcommands, and listing subcommands for\nBash completion.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/worktree.c | 31 ++++++++++++-------------------\n git.c              |  2 +-\n 2 files changed, 13 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex cd62eef240..c6710b2552 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -1112,31 +1112,24 @@ static int repair(int ac, const char **av, const char *prefix)\n \n int cmd_worktree(int ac, const char **av, const char *prefix)\n {\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n+\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n+\t\tOPT_SUBCOMMAND(\"list\", &fn, list),\n+\t\tOPT_SUBCOMMAND(\"lock\", &fn, lock_worktree),\n+\t\tOPT_SUBCOMMAND(\"unlock\", &fn, unlock_worktree),\n+\t\tOPT_SUBCOMMAND(\"move\", &fn, move_worktree),\n+\t\tOPT_SUBCOMMAND(\"remove\", &fn, remove_worktree),\n+\t\tOPT_SUBCOMMAND(\"repair\", &fn, repair),\n \t\tOPT_END()\n \t};\n \n \tgit_config(git_worktree_config, NULL);\n \n-\tif (ac < 2)\n-\t\tusage_with_options(worktree_usage, options);\n \tif (!prefix)\n \t\tprefix = \"\";\n-\tif (!strcmp(av[1], \"add\"))\n-\t\treturn add(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"prune\"))\n-\t\treturn prune(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"list\"))\n-\t\treturn list(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"lock\"))\n-\t\treturn lock_worktree(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"unlock\"))\n-\t\treturn unlock_worktree(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"move\"))\n-\t\treturn move_worktree(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"remove\"))\n-\t\treturn remove_worktree(ac - 1, av + 1, prefix);\n-\tif (!strcmp(av[1], \"repair\"))\n-\t\treturn repair(ac - 1, av + 1, prefix);\n-\tusage_with_options(worktree_usage, options);\n+\n+\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n+\treturn fn(ac, av, prefix);\n }\ndiff --git a/git.c b/git.c\nindex 73ddf0f452..f52a955410 100644\n--- a/git.c\n+++ b/git.c\n@@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {\n \t{ \"verify-tag\", cmd_verify_tag, RUN_SETUP },\n \t{ \"version\", cmd_version },\n \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP },\n-\t{ \"worktree\", cmd_worktree, RUN_SETUP | NO_PARSEOPT },\n+\t{ \"worktree\", cmd_worktree, RUN_SETUP },\n \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n };\n \n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461577","messageId":"20220819160411.1791200-8-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:58Z","receivedAt":"2022-08-19T16:33:37Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Update the comment documenting 'struct option' to clarify that\nPARSE_OPT_NODASH can only be an argumentless short option; see\n51a9949eda (parseopt: add PARSE_OPT_NODASH, 2009-05-07).\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n parse-options.h | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/parse-options.h b/parse-options.h\nindex 591df64191..8cbfc7e8bf 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -109,7 +109,8 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,\n  *\t\t\t\tis last on the command line. If the option is\n  *\t\t\t\tnot last it will require an argument.\n  *\t\t\t\tShould not be used with PARSE_OPT_OPTARG.\n- *   PARSE_OPT_NODASH: this option doesn't start with a dash.\n+ *   PARSE_OPT_NODASH: this option doesn't start with a dash; can only be a\n+ *\t\t       short option and can't accept arguments.\n  *   PARSE_OPT_LITERAL_ARGHELP: says that argh shouldn't be enclosed in brackets\n  *\t\t\t\t(i.e. '<argh>') in the help message.\n  *\t\t\t\tUseful for options with multiple parameters.\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461578","messageId":"20220819160411.1791200-4-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH v2 03/20] t5505-remote.sh: check the behavior without a subcommand","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T16:03:54Z","receivedAt":"2022-08-19T16:33:41Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git remote' without a subcommand defaults to listing all remotes and\ndoesn't accept any arguments except the '-v|--verbose' option.\n\nWe are about to teach parse-options to handle subcommands, and update\n'git remote' to make use of that new feature.  So let's add some tests\nto make sure that the upcoming changes don't inadvertently change the\nbehavior in these cases.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/t5505-remote.sh | 20 ++++++++++++++++++++\n 1 file changed, 20 insertions(+)\n\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 6c7370f87f..a549a21ef6 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -241,6 +241,26 @@ test_expect_success 'add invalid foreign_vcs remote' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'without subcommand' '\n+\techo origin >expect &&\n+\tgit -C test remote >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'without subcommand accepts -v' '\n+\tcat >expect <<-EOF &&\n+\torigin\t$(pwd)/one (fetch)\n+\torigin\t$(pwd)/one (push)\n+\tEOF\n+\tgit -C test remote -v >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'without subcommand does not take arguments' '\n+\ttest_expect_code 129 git -C test remote origin 2>err &&\n+\tgrep \"^error: Unknown subcommand:\" err\n+'\n+\n cat >test/expect <<EOF\n * remote origin\n   Fetch URL: $(pwd)/one\n-- \n2.37.2.817.g36f84ce71d\n\n"},{"id":"461583","messageId":"220819.86k0746t7v.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-5-szeder.dev@gmail.com","subject":"Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T17:23:44Z","receivedAt":"2022-08-19T17:52:33Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> +\tfor (int i = 0; i < argc; i++)\n\nSee\nhttps://lore.kernel.org/git/CABPp-BHvQwct2WRRYGyzm=YVkjmwBqoe1DUtCicuQW=jrQ2hdA@mail.gmail.com/;\nbut maybe nothing to to be done here...\n\n> +\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n\nNit: !argc\n> +\t\terror(\"'cmd' is mandatory\");\n> +\t\tusage_with_options(usage, test_flag_options);\n\nI think you want usage_msg_opt() instead.\n\n> +test_expect_success 'NO_INTERNAL_HELP works for -h' '\n> +\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n> +\tcat err &&\n\nStray \"cat\", presumably in error..\n\n> +\tgrep \"^error: unknown switch \\`h$SQ\" err &&\n> +\tgrep \"^usage: \" err\n> +'\n> +\n> +for help_opt in help help-all\n> +do\n> +\ttest_expect_success \"NO_INTERNAL_HELP works for --$help_opt\" \"\n> +\t\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&\n> +\t\tcat err &&\n\nditto.\n\n"},{"id":"461584","messageId":"220819.86fshs6sy6.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-9-szeder.dev@gmail.com","subject":"Re: [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T17:30:13Z","receivedAt":"2022-08-19T17:54:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> subcommands) a tad more complex, so I wanted to test the result.  The\n> test would have to account for the presence of that leading space,\n> which bugged my OCD, so let's get rid of it.\n\nOn the subject of OCD it...\n\n> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> ---\n>  parse-options.c | 3 ++-\n>  1 file changed, 2 insertions(+), 1 deletion(-)\n>\n> diff --git a/parse-options.c b/parse-options.c\n> index a0a2cf98fa..8748f88e6f 100644\n> --- a/parse-options.c\n> +++ b/parse-options.c\n> @@ -620,7 +620,8 @@ static int show_gitcomp(const struct option *opts, int show_all)\n>  \t\t\tsuffix = \"=\";\n>  \t\tif (starts_with(opts->long_name, \"no-\"))\n>  \t\t\tnr_noopts++;\n> -\t\tprintf(\" --%s%s\", opts->long_name, suffix);\n> +\t\tprintf(\"%s--%s%s\", opts == original_opts ? \"\" : \" \",\n> +\t\t       opts->long_name, suffix);\n>  \t}\n>  \tshow_negated_gitcomp(original_opts, show_all, -1);\n>  \tshow_negated_gitcomp(original_opts, show_all, nr_noopts);\n\n...bugs me a bit that we have a \"suffix\" variable, but not a \"prefix\"\nfor this, maybe this? We could also make the prefix a \"const char\n*const\" to indicate that we don't modify it in the \"for\" body:\n\ndiff --git a/parse-options.c b/parse-options.c\nindex edf55d3ef5d..3000121e5c0 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -589,6 +589,7 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \tint nr_noopts = 0;\n \n \tfor (; opts->type != OPTION_END; opts++) {\n+\t\tconst char *prefix = opts == original_opts ? \"\" : \" \";\n \t\tconst char *suffix = \"\";\n \n \t\tif (!opts->long_name)\n@@ -620,7 +621,7 @@ static int show_gitcomp(const struct option *opts, int show_all)\n \t\t\tsuffix = \"=\";\n \t\tif (starts_with(opts->long_name, \"no-\"))\n \t\t\tnr_noopts++;\n-\t\tprintf(\" --%s%s\", opts->long_name, suffix);\n+\t\tprintf(\"%s--%s%s\", opts->long_name, prefix, suffix);\n \t}\n \tshow_negated_gitcomp(original_opts, show_all, -1);\n \tshow_negated_gitcomp(original_opts, show_all, nr_noopts);\n"},{"id":"461587","messageId":"220819.86bksg6s5l.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-10-szeder.dev@gmail.com","subject":"Re: [PATCH v2 09/20] parse-options: add support for parsing subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T17:33:46Z","receivedAt":"2022-08-19T18:02:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> +static enum parse_opt_result parse_subcommand(const char *arg,\n> +\t\t\t\t\t      const struct option *options)\n> +{\n> +\tfor (; options->type != OPTION_END; options++)\n> +\t\tif (options->type == OPTION_SUBCOMMAND &&\n> +\t\t    !strcmp(options->long_name, arg)) {\n> +\t\t\t*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;\n\nNit: Maybe do the cast by assigning to an intermediate variable? Would\nmake this less dense, and since we already have {}-braces...\n\n> +\t\t\treturn PARSE_OPT_SUBCOMMAND;\n> +\t\t}\n> +\n> +\treturn PARSE_OPT_UNKNOWN;\n> +}\n> +\n>  static void check_typos(const char *arg, const struct option *options)\n>  {\n>  \tif (strlen(arg) < 3)\n> @@ -442,6 +457,7 @@ static void check_typos(const char *arg, const struct option *options)\n>  static void parse_options_check(const struct option *opts)\n>  {\n>  \tchar short_opts[128];\n> +\tvoid *subcommand_value = NULL;\n>  \n>  \tmemset(short_opts, '\\0', sizeof(short_opts));\n>  \tfor (; opts->type != OPTION_END; opts++) {\n> @@ -489,6 +505,14 @@ static void parse_options_check(const struct option *opts)\n>  \t\t\t       \"Are you using parse_options_step() directly?\\n\"\n>  \t\t\t       \"That case is not supported yet.\");\n>  \t\t\tbreak;\n> +\t\tcase OPTION_SUBCOMMAND:\n> +\t\t\tif (!opts->value || !opts->subcommand_fn)\n> +\t\t\t\toptbug(opts, \"OPTION_SUBCOMMAND needs a value and a subcommand function\");\n\nBetter split into two IMO:\n\n\tif (!opts->value)\n\t\toptbug(opts, \"OPTION_SUBCOMMAND needs a value\");\n\tif (!opts->subcommand_fn)\n\t\toptbug(opts, \"OPTION_SUBCOMMAND needs a subcommand_fn\");\n\nThen if we have extra checks we don't need to keep adding to a dense ||\nand amend an existing string.\n\n> +static int has_subcommands(const struct option *options)\n> +{\n> +\tfor (; options->type != OPTION_END; options++)\n> +\t\tif (options->type == OPTION_SUBCOMMAND)\n> +\t\t\treturn 1;\n> +\treturn 0;\n> +}\n\nI wondered if parse_options_check() couldn't take a \"int\n*has_subcommands\", since we already loop through the options to check it\nas part of the checks there (but we'd need to re-arrange them).\n\nNot for optimization, just wondering if it would make the code flow\nclearer, maybe not.\n\n> +\tif (ctx->has_subcommands) {\n> +\t\tif (flags & PARSE_OPT_STOP_AT_NON_OPTION)\n> +\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION\");\n> +\t\tif (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n> +\t\t\tif (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)\n> +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN_OPT unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n> +\t\t\tif (flags & PARSE_OPT_KEEP_DASHDASH)\n> +\t\t\t\tBUG(\"subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL\");\n\nMM, very long lines. Maybe something like:\n\n\tBUG(\"%s flag cannot be combined with %s, unless %s is also provided\", ...);\n\n> +\t\t}\n> +\t}\n>  \tif ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&\n>  \t    (flags & PARSE_OPT_STOP_AT_NON_OPTION) &&\n>  \t    !(flags & PARSE_OPT_ONE_SHOT))\n> @@ -589,6 +634,7 @@ static int show_gitcomp(const struct option *opts, int show_all)\n>  \tint nr_noopts = 0;\n>  \n>  \tfor (; opts->type != OPTION_END; opts++) {\n> +\t\tconst char *prefix = \"--\";\n\nAh, here we have a prefix variable, but it's more of an \"infix\"... :)\n\n> +\t\t\t\t\treturn PARSE_OPT_DONE;\n> +\t\t\t\terror(_(\"unknown subcommand: `%s'\"), arg);\n> +\t\t\t\tusage_with_options(usagestr, options);\n\nditto earlier patch comment about usage_msg_opt() (also applies below)\n\n> +\t\t\tcase PARSE_OPT_NON_OPTION:\n> +\t\t\t\t/* Impossible. */\n\nWe could just skip this comment as...\n> +\t\t\t\tBUG(\"parse_subcommand() cannot return these\");\n\n...this makes it clear.\n\n>  \tcase PARSE_OPT_DONE:\n> +\t\tif (ctx.has_subcommands &&\n> +\t\t    !(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n> +\t\t\terror(_(\"need a subcommand\"));\n> +\t\t\tusage_with_options(usagestr, options);\n\nditto.\n\n> +typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n> +\t\t\t\t    const char *prefix);\n\nWe usually define function typedefs like:\n\n\ttypedef int (*parse_opt_subcommand_fn)(...);\n\nBut I see that...\n\n>  \tenum parse_opt_type type;\n> @@ -145,6 +155,7 @@ struct option {\n>  \tintptr_t defval;\n>  \tparse_opt_ll_cb *ll_callback;\n>  \tintptr_t extra;\n> +\tparse_opt_subcommand_fn *subcommand_fn;\n\nYou're doing this consistently with parse_opt_ll_cb etc., fair enough.\n\n> +#define OPT_SUBCOMMAND_F(l, v, fn, f) { \\\n> +\t.type = OPTION_SUBCOMMAND, \\\n> +\t.long_name = (l), \\\n> +\t.value = (v), \\\n> +\t.flags = (f), \\\n\nNice to see this form\n\n> +\t.subcommand_fn = (fn) }\n\nAlmost all other such decls end with:\n\n\t\t.foo = bar, \\\n\t}\n\nI.e. we put the closing \"}\" at the start of the line.\n\n> +#define OPT_SUBCOMMAND(l, v, fn)    OPT_SUBCOMMAND_F((l), (v), (fn), 0)\n\nI think this would be more readable as:\n\n\t#define OPT_SUBCOMMAND(l, v, fn) \\\n\t\tOPT_SUBCOMMAND_F((l), (v), (fn), 0)\n\nWhich both aligns nicely, and isn't using a \\t in the middle of the line\n(which in any case we'll end up aligning with nothing else, as the names\nare all different lengths, if other things continue using this pattern.\n>  \tenum parse_opt_flags flags;\n> +\tunsigned has_subcommands;\n\nMaybe make it: \"unsigned has_subcommands:1\"?\n\n> +\tprintf(\"fn: subcmd_one\\n\");\n\ns/printf/puts/g here\n\n> +\tprint_args(argc, argv);\n> +\treturn 0;\n> +}\n> +\n> +static int subcmd_two(int argc, const char **argv, const char *prefix)\n> +{\n> +\tprintf(\"fn: subcmd_two\\n\");\n\nditto.\n\n"},{"id":"461588","messageId":"220819.867d346rzo.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-11-szeder.dev@gmail.com","subject":"Re: [PATCH v2 10/20] builtin/bundle.c: let parse-options parse subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T17:50:03Z","receivedAt":"2022-08-19T18:05:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n\nNit: I wouldn't mind keeping this variable:\n\n>  \t};\n> -\tint result;\n>  \n>  \targc = parse_options(argc, argv, prefix, options, builtin_bundle_usage,\n> -\t\tPARSE_OPT_STOP_AT_NON_OPTION);\n> +\t\t\t     0);\n>  \n>  \tpacket_trace_identity(\"bundle\");\n>  \n> -\tif (argc < 2)\n> -\t\tusage_with_options(builtin_bundle_usage, options);\n> -\n> -\telse if (!strcmp(argv[0], \"create\"))\n> -\t\tresult = cmd_bundle_create(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"verify\"))\n> -\t\tresult = cmd_bundle_verify(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"list-heads\"))\n> -\t\tresult = cmd_bundle_list_heads(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"unbundle\"))\n> -\t\tresult = cmd_bundle_unbundle(argc, argv, prefix);\n> -\telse {\n> -\t\terror(_(\"Unknown subcommand: %s\"), argv[0]);\n> -\t\tusage_with_options(builtin_bundle_usage, options);\n> -\t}\n\nThen just doing:\n\n\tresult = fn(argc, argv, prefix);\n\nWhich would eliminate the need to change this:\n\n> -\treturn result ? 1 : 0;\n> +\treturn !!fn(argc, argv, prefix);\n>  }\n\nI wondered about why !! v.s. 0/1 for a second or so, but realized you\nwere just golf-ing an existing pattern.\n\nFWIW I *think* if we're changing this we could just make it \"return\nfn()\", as the functions themselves seem to return 0/1 or a !!'d\nvariable.\n"},{"id":"461590","messageId":"220819.8635ds6run.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-12-szeder.dev@gmail.com","subject":"Re: [PATCH v2 11/20] builtin/commit-graph.c: let parse-options parse subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T17:53:08Z","receivedAt":"2022-08-19T18:08:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> 'git commit-graph' parses its subcommands with an if-else if\n> statement.  parse-options has just learned to parse subcommands, so\n> let's use that facility instead, with the benefits of shorter code,\n> handling missing or unknown subcommands, and listing subcommands for\n> Bash completion.\n>\n> Note that the functions implementing each subcommand only accept the\n> 'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter\n> to make them match the type expected by parse-options, and thus avoid\n> casting function pointers.\n>\n> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> ---\n>  builtin/commit-graph.c  | 30 +++++++++++++-----------------\n>  t/t5318-commit-graph.sh |  4 ++--\n>  2 files changed, 15 insertions(+), 19 deletions(-)\n>\n> diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c\n> index 51c4040ea6..1eb5492cbd 100644\n> --- a/builtin/commit-graph.c\n> +++ b/builtin/commit-graph.c\n> @@ -58,7 +58,7 @@ static struct option *add_common_options(struct option *to)\n>  \treturn parse_options_concat(common_opts, to);\n>  }\n>  \n> -static int graph_verify(int argc, const char **argv)\n> +static int graph_verify(int argc, const char **argv, const char *prefix)\n>  {\n>  \tstruct commit_graph *graph = NULL;\n>  \tstruct object_directory *odb = NULL;\n> @@ -190,7 +190,7 @@ static int git_commit_graph_write_config(const char *var, const char *value,\n>  \treturn 0;\n>  }\n>  \n> -static int graph_write(int argc, const char **argv)\n> +static int graph_write(int argc, const char **argv, const char *prefix)\n>  {\n>  \tstruct string_list pack_indexes = STRING_LIST_INIT_DUP;\n>  \tstruct strbuf buf = STRBUF_INIT;\n> @@ -307,26 +307,22 @@ static int graph_write(int argc, const char **argv)\n>  \n>  int cmd_commit_graph(int argc, const char **argv, const char *prefix)\n>  {\n> -\tstruct option *builtin_commit_graph_options = common_opts;\n> +\tparse_opt_subcommand_fn *fn = NULL;\n> +\tstruct option builtin_commit_graph_options[] = {\n> +\t\tOPT_SUBCOMMAND(\"verify\", &fn, graph_verify),\n> +\t\tOPT_SUBCOMMAND(\"write\", &fn, graph_write),\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct option *options = parse_options_concat(builtin_commit_graph_options, common_opts);\n\nThis looks like it'll leak if...\n\n>  \n>  \tgit_config(git_default_config, NULL);\n> -\targc = parse_options(argc, argv, prefix,\n> -\t\t\t     builtin_commit_graph_options,\n> -\t\t\t     builtin_commit_graph_usage,\n> -\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n> -\tif (!argc)\n> -\t\tgoto usage;\n\nWe take this goto..\n\n> +\targc = parse_options(argc, argv, prefix, options,\n> +\t\t\t     builtin_commit_graph_usage, 0);\n> +\tFREE_AND_NULL(options);\n\nWhy FREE_AND_NULL() over free()?\n>  \n> -\terror(_(\"unrecognized subcommand: %s\"), argv[0]);\n> -usage:\n> -\tusage_with_options(builtin_commit_graph_usage,\n> -\t\t\t   builtin_commit_graph_options);\n> +\treturn fn(argc, argv, prefix);\n\nThe leak seems easily solved, let's just do the free() at the end?\n"},{"id":"461591","messageId":"220819.86y1vk5d73.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"220819.8635ds6run.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 11/20] builtin/commit-graph.c: let parse-options parse subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T17:56:30Z","receivedAt":"2022-08-19T18:08:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, Ævar Arnfjörð Bjarmason wrote:\n\n> On Fri, Aug 19 2022, SZEDER Gábor wrote:\n>\n>> 'git commit-graph' parses its subcommands with an if-else if\n>> statement.  parse-options has just learned to parse subcommands, so\n>> let's use that facility instead, with the benefits of shorter code,\n>> handling missing or unknown subcommands, and listing subcommands for\n>> Bash completion.\n>>\n>> Note that the functions implementing each subcommand only accept the\n>> 'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter\n>> to make them match the type expected by parse-options, and thus avoid\n>> casting function pointers.\n>>\n>> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n>> ---\n>>  builtin/commit-graph.c  | 30 +++++++++++++-----------------\n>>  t/t5318-commit-graph.sh |  4 ++--\n>>  2 files changed, 15 insertions(+), 19 deletions(-)\n>>\n>> diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c\n>> index 51c4040ea6..1eb5492cbd 100644\n>> --- a/builtin/commit-graph.c\n>> +++ b/builtin/commit-graph.c\n>> @@ -58,7 +58,7 @@ static struct option *add_common_options(struct option *to)\n>>  \treturn parse_options_concat(common_opts, to);\n>>  }\n>>  \n>> -static int graph_verify(int argc, const char **argv)\n>> +static int graph_verify(int argc, const char **argv, const char *prefix)\n>>  {\n>>  \tstruct commit_graph *graph = NULL;\n>>  \tstruct object_directory *odb = NULL;\n>> @@ -190,7 +190,7 @@ static int git_commit_graph_write_config(const char *var, const char *value,\n>>  \treturn 0;\n>>  }\n>>  \n>> -static int graph_write(int argc, const char **argv)\n>> +static int graph_write(int argc, const char **argv, const char *prefix)\n>>  {\n>>  \tstruct string_list pack_indexes = STRING_LIST_INIT_DUP;\n>>  \tstruct strbuf buf = STRBUF_INIT;\n>> @@ -307,26 +307,22 @@ static int graph_write(int argc, const char **argv)\n>>  \n>>  int cmd_commit_graph(int argc, const char **argv, const char *prefix)\n>>  {\n>> -\tstruct option *builtin_commit_graph_options = common_opts;\n>> +\tparse_opt_subcommand_fn *fn = NULL;\n>> +\tstruct option builtin_commit_graph_options[] = {\n>> +\t\tOPT_SUBCOMMAND(\"verify\", &fn, graph_verify),\n>> +\t\tOPT_SUBCOMMAND(\"write\", &fn, graph_write),\n>> +\t\tOPT_END(),\n>> +\t};\n>> +\tstruct option *options = parse_options_concat(builtin_commit_graph_options, common_opts);\n>\n> This looks like it'll leak if...\n>\n>>  \n>>  \tgit_config(git_default_config, NULL);\n>> -\targc = parse_options(argc, argv, prefix,\n>> -\t\t\t     builtin_commit_graph_options,\n>> -\t\t\t     builtin_commit_graph_usage,\n>> -\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n>> -\tif (!argc)\n>> -\t\tgoto usage;\n>\n> We take this goto..\n\nSorry, I'm being an idiot and misreading this, the \"goto\" isn't here\nanymore, du'h!\n\n>> +\targc = parse_options(argc, argv, prefix, options,\n>> +\t\t\t     builtin_commit_graph_usage, 0);\n>> +\tFREE_AND_NULL(options);\n>\n> Why FREE_AND_NULL() over free()?\n\nBut this question still stands :)\n"},{"id":"461593","messageId":"220819.86tu685d5t.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-15-szeder.dev@gmail.com","subject":"Re: [PATCH v2 14/20] builtin/multi-pack-index.c: let parse-options parse subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T17:57:23Z","receivedAt":"2022-08-19T18:09:26Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> +\targc = parse_options(argc, argv, prefix, options,\n> +\t\t\t     builtin_multi_pack_index_usage, 0);\n> +\tFREE_AND_NULL(options);\n\nDitto s/FREE_AND_NULL/free/g per a previous patch's comment.\n"},{"id":"461594","messageId":"220819.86pmgw5cpk.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-16-szeder.dev@gmail.com","subject":"Re: [PATCH v2 15/20] builtin/notes.c: let parse-options parse subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T18:01:55Z","receivedAt":"2022-08-19T18:14:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> -\tint result;\n>  \tconst char *override_notes_ref = NULL;\n> +\tparse_opt_subcommand_fn *fn = list;\n>  \tstruct option options[] = {\n>  \t\tOPT_STRING(0, \"ref\", &override_notes_ref, N_(\"notes-ref\"),\n>  \t\t\t   N_(\"use notes from <notes-ref>\")),\n> +\t\tOPT_SUBCOMMAND(\"list\", &fn, list),\n> +\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n> +\t\tOPT_SUBCOMMAND(\"copy\", &fn, copy),\n> +\t\tOPT_SUBCOMMAND(\"append\", &fn, append_edit),\n> +\t\tOPT_SUBCOMMAND(\"edit\", &fn, append_edit),\n> +\t\tOPT_SUBCOMMAND(\"show\", &fn, show),\n> +\t\tOPT_SUBCOMMAND(\"merge\", &fn, merge),\n> +\t\tOPT_SUBCOMMAND(\"remove\", &fn, remove_cmd),\n> +\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n> +\t\tOPT_SUBCOMMAND(\"get-ref\", &fn, get_ref),\n>  \t\tOPT_END()\n>  \t};\n>  \n>  \tgit_config(git_default_config, NULL);\n>  \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n> -\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n> +\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n> +\tif (fn == list && argc && strcmp(argv[0], \"list\")) {\n> +\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n> +\t\tusage_with_options(git_notes_usage, options);\n> +\t}\n\nI wanted to ask why the API can't smartly handle this, but your \"Found\nan unknown option given to a command with\" comment in an earlier patch\nanswered it.\n\nI think something in this direction would be a bit more readble/obvious,\nas it avoids hardcoding \"list\":\n\t\n\tdiff --git a/builtin/notes.c b/builtin/notes.c\n\tindex 42cbae46598..43d59b1a98e 100644\n\t--- a/builtin/notes.c\n\t+++ b/builtin/notes.c\n\t@@ -995,7 +995,7 @@ static int get_ref(int argc, const char **argv, const char *prefix)\n\t int cmd_notes(int argc, const char **argv, const char *prefix)\n\t {\n\t \tconst char *override_notes_ref = NULL;\n\t-\tparse_opt_subcommand_fn *fn = list;\n\t+\tparse_opt_subcommand_fn *fn = NULL;\n\t \tstruct option options[] = {\n\t \t\tOPT_STRING(0, \"ref\", &override_notes_ref, N_(\"notes-ref\"),\n\t \t\t\t   N_(\"use notes from <notes-ref>\")),\n\t@@ -1015,10 +1015,11 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n\t \tgit_config(git_default_config, NULL);\n\t \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n\t \t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n\t-\tif (fn == list && argc && strcmp(argv[0], \"list\")) {\n\t-\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n\t-\t\tusage_with_options(git_notes_usage, options);\n\t-\t}\n\t+\tif (!fn && argc)\n\t+\t\tusage_msg_optf(_(\"unknown subcommand: %s\"),\n\t+\t\t\t       git_notes_usage, options, argv[0]);\n\t+\telse if (!fn)\n\t+\t\tfn = list;\n\t \n\t \tif (override_notes_ref) {\n\t \t\tstruct strbuf sb = STRBUF_INIT;\n\nI.e. we rely on the API setting it to non-NULL if it finds a subcommand,\notherwise we just set it to \"list\" after checking whether we have excess\narguments.\n\n> [...]\n> -\telse if (!strcmp(argv[0], \"get-ref\"))\n> -\t\tresult = get_ref(argc, argv, prefix);\n> -\telse {\n> -\t\tresult = error(_(\"unknown subcommand: %s\"), argv[0]);\n> -\t\tusage_with_options(git_notes_usage, options);\n> -\t}\n> -\n> -\treturn result ? 1 : 0;\n> +\treturn !!fn(argc, argv, prefix);\n>  }\n\nIn any case this is a lot nicer, ditto previous comment about maybe\nskipping the refactoring of this end code, but I'm also fine with\nkeeping it.\n"},{"id":"461595","messageId":"220819.86lerk5ckq.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-17-szeder.dev@gmail.com","subject":"Re: [PATCH v2 16/20] builtin/reflog.c: let parse-options parse subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T18:08:22Z","receivedAt":"2022-08-19T18:15:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> +\tparse_opt_subcommand_fn *fn = NULL;\n\nRe the comment on notes.c this is a bit like that pattern...\n\n> -log_reflog:\n> -\treturn cmd_log_reflog(argc, argv, prefix);\n> +\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT);\n> +\tif (fn)\n> +\t\treturn fn(argc - 1, argv + 1, prefix);\n> +\telse\n> +\t\treturn cmd_log_reflog(argc, argv, prefix);\n>  }\n\nMaybe more obvious (untested):\n\n\tif (!fn) {\n\t\targc--;\n\t\targv++;\n\t\tfn = cmd_log_reflog;\n\t}\n\treturn fn(argc, argv, prefix);\n\n"},{"id":"461597","messageId":"xmqqczcwksgj.fsf@gitster.g","threadId":"58218","inReplyTo":"20220819160411.1791200-5-szeder.dev@gmail.com","subject":"Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-08-19T18:18:20Z","receivedAt":"2022-08-19T18:18:41Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> +static void print_args(int argc, const char **argv)\n> +{\n> +\tfor (int i = 0; i < argc; i++)\n> +\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n> +}\n\nIt is not November 2022 yet (cf. Documentation/CodingGuidelines).\n\nCurious why \"%02d\", not \"%d\", or autoscale for cases where argc is\nlarger than 99, but I'll let it pass (iow no need to reroll only to\n\"fix\" it).\n"},{"id":"461598","messageId":"20220819182204.GA3373722@szeder.dev","threadId":"58218","inReplyTo":"220819.8635ds6run.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 11/20] builtin/commit-graph.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T18:22:04Z","receivedAt":"2022-08-19T18:22:10Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Aug 19, 2022 at 07:53:08PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > +\targc = parse_options(argc, argv, prefix, options,\n> > +\t\t\t     builtin_commit_graph_usage, 0);\n> > +\tFREE_AND_NULL(options);\n> \n> Why FREE_AND_NULL() over free()?\n\nHeh, it's interesting that you should ask that ;)\n\nI followed the existing pattern for now, just like you did in\n84e4484f12 (commit-graph: use parse_options_concat(), 2021-08-23),\nwhere you made graph_verify() and graph_write() use FREE_AND_NULL() to\nrelease their concatenated options arrays.\n\nBut yeah, a plain free() should work just fine in these cases.\n"},{"id":"461600","messageId":"20220819183508.GB3373722@szeder.dev","threadId":"58218","inReplyTo":"220819.86fshs6sy6.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-19T18:35:08Z","receivedAt":"2022-08-19T18:35:15Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Aug 19, 2022 at 07:30:13PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Fri, Aug 19 2022, SZEDER Gábor wrote:\n> \n> > subcommands) a tad more complex, so I wanted to test the result.  The\n> > test would have to account for the presence of that leading space,\n> > which bugged my OCD, so let's get rid of it.\n> \n> On the subject of OCD it...\n> \n> > Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> > ---\n> >  parse-options.c | 3 ++-\n> >  1 file changed, 2 insertions(+), 1 deletion(-)\n> >\n> > diff --git a/parse-options.c b/parse-options.c\n> > index a0a2cf98fa..8748f88e6f 100644\n> > --- a/parse-options.c\n> > +++ b/parse-options.c\n> > @@ -620,7 +620,8 @@ static int show_gitcomp(const struct option *opts, int show_all)\n> >  \t\t\tsuffix = \"=\";\n> >  \t\tif (starts_with(opts->long_name, \"no-\"))\n> >  \t\t\tnr_noopts++;\n> > -\t\tprintf(\" --%s%s\", opts->long_name, suffix);\n> > +\t\tprintf(\"%s--%s%s\", opts == original_opts ? \"\" : \" \",\n> > +\t\t       opts->long_name, suffix);\n> >  \t}\n> >  \tshow_negated_gitcomp(original_opts, show_all, -1);\n> >  \tshow_negated_gitcomp(original_opts, show_all, nr_noopts);\n> \n> ...bugs me a bit that we have a \"suffix\" variable, but not a \"prefix\"\n> for this\n\nThat space acts as a separator, so calling it prefix would be\nmisleading.\n\n"},{"id":"461607","messageId":"220819.86h7285a1o.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-10-szeder.dev@gmail.com","subject":"Re: [PATCH v2 09/20] parse-options: add support for parsing subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T19:03:27Z","receivedAt":"2022-08-19T19:05:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> @@ -885,7 +977,14 @@ int parse_options(int argc, const char **argv,\n>  \tcase PARSE_OPT_COMPLETE:\n>  \t\texit(0);\n>  \tcase PARSE_OPT_NON_OPTION:\n> +\tcase PARSE_OPT_SUBCOMMAND:\n> +\t\tbreak;\n\nThis amounts to the same behavior, because...\n\n>  \tcase PARSE_OPT_DONE:\n> +\t\tif (ctx.has_subcommands &&\n> +\t\t    !(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {\n> +\t\t\terror(_(\"need a subcommand\"));\n> +\t\t\tusage_with_options(usagestr, options);\n> +\t\t}\n>  \t\tbreak;\n\n...AFAICT we won't have PARSE_OPT_NON_OPTION *and* ctx.has_subcommands,\nbut it's really confusing that before we'd fall through from\nPARSE_OPT_NON_OPTION to PARSE_OPT_DONE, but now we don't.'\n\n"},{"id":"461610","messageId":"220819.86czcw59w6.gmgdl@evledraar.gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-20-szeder.dev@gmail.com","subject":"Re: [PATCH v2 19/20] builtin/stash.c: let parse-options parse subcommands","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-08-19T19:06:32Z","receivedAt":"2022-08-19T19:08:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Aug 19 2022, SZEDER Gábor wrote:\n\n> 'git stash' parses its subcommands with a long list of if-else if\n> statements.  parse-options has just learned to parse subcommands, so\n> let's use that facility instead, with the benefits of shorter code,\n> and listing subcommands for Bash completion.\n>\n> Note that the push_stash() function implementing the 'push' subcommand\n> accepts an extra flag parameter to indicate whether push was assumed,\n> so add a wrapper function with the standard subcommand function\n> signature.\n>\n> Note also that this change \"hides\" the '-h' option in 'git stash push\n> -h' from the parse_option() call in cmd_stash(), as it comes after the\n> subcommand.  Consequently, from now on it will emit the usage of the\n> 'push' subcommand instead of the usage of 'git stash'.  We had a\n> failing test for this case, which can now be flipped to expect\n> success.\n>\n> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> ---\n>  builtin/stash.c  | 53 ++++++++++++++++++++++--------------------------\n>  t/t3903-stash.sh |  2 +-\n>  2 files changed, 25 insertions(+), 30 deletions(-)\n>\n> diff --git a/builtin/stash.c b/builtin/stash.c\n> index a14e832e9f..1ba24c1173 100644\n> --- a/builtin/stash.c\n> +++ b/builtin/stash.c\n> @@ -1739,6 +1739,11 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n>  \t\t\t     include_untracked, only_staged);\n>  }\n>  \n> +static int push_stash_unassumed(int argc, const char **argv, const char *prefix)\n> +{\n> +\treturn push_stash(argc, argv, prefix, 0);\n> +}\n> +\n>  static int save_stash(int argc, const char **argv, const char *prefix)\n>  {\n>  \tint keep_index = -1;\n> @@ -1787,15 +1792,28 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n>  \tpid_t pid = getpid();\n>  \tconst char *index_file;\n>  \tstruct strvec args = STRVEC_INIT;\n> -\n> +\tparse_opt_subcommand_fn *fn = NULL;\n>  \tstruct option options[] = {\n> +\t\tOPT_SUBCOMMAND(\"apply\", &fn, apply_stash),\n> +\t\tOPT_SUBCOMMAND(\"clear\", &fn, clear_stash),\n> +\t\tOPT_SUBCOMMAND(\"drop\", &fn, drop_stash),\n> +\t\tOPT_SUBCOMMAND(\"pop\", &fn, pop_stash),\n> +\t\tOPT_SUBCOMMAND(\"branch\", &fn, branch_stash),\n> +\t\tOPT_SUBCOMMAND(\"list\", &fn, list_stash),\n> +\t\tOPT_SUBCOMMAND(\"show\", &fn, show_stash),\n> +\t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n> +\t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n> +\t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n> +\t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n>  \t\tOPT_END()\n>  \t};\n>  \n>  \tgit_config(git_stash_config, NULL);\n>  \n>  \targc = parse_options(argc, argv, prefix, options, git_stash_usage,\n> -\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);\n> +\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n> +\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n> +\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n>  \n>  \tprepare_repo_settings(the_repository);\n>  \tthe_repository->settings.command_requires_full_index = 0;\n> @@ -1804,33 +1822,10 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n>  \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n>  \t\t    (uintmax_t)pid);\n>  \n> -\tif (!argc)\n> -\t\treturn !!push_stash(0, NULL, prefix, 0);\n> -\telse if (!strcmp(argv[0], \"apply\"))\n> -\t\treturn !!apply_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"clear\"))\n> -\t\treturn !!clear_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"drop\"))\n> -\t\treturn !!drop_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"pop\"))\n> -\t\treturn !!pop_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"branch\"))\n> -\t\treturn !!branch_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"list\"))\n> -\t\treturn !!list_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"show\"))\n> -\t\treturn !!show_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"store\"))\n> -\t\treturn !!store_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"create\"))\n> -\t\treturn !!create_stash(argc, argv, prefix);\n> -\telse if (!strcmp(argv[0], \"push\"))\n> -\t\treturn !!push_stash(argc, argv, prefix, 0);\n> -\telse if (!strcmp(argv[0], \"save\"))\n> -\t\treturn !!save_stash(argc, argv, prefix);\n> -\telse if (*argv[0] != '-')\n> -\t\tusage_msg_optf(_(\"unknown subcommand: %s\"),\n> -\t\t\t       git_stash_usage, options, argv[0]);\n> +\tif (fn)\n> +\t\treturn !!fn(argc, argv, prefix);\n> +\telse if (!argc)\n> +\t\treturn !!push_stash_unassumed(0, NULL, prefix);\n>  \n>  \t/* Assume 'stash push' */\n>  \tstrvec_push(&args, \"push\");\n> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\n> index 2a4c3fd61c..376cc8f4ab 100755\n> --- a/t/t3903-stash.sh\n> +++ b/t/t3903-stash.sh\n> @@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '\n>  \tgrep -F \"or: git stash show\" usage\n>  '\n>  \n> -test_expect_failure 'usage for subcommands should emit subcommand usage' '\n> +test_expect_success 'usage for subcommands should emit subcommand usage' '\n>  \ttest_expect_code 129 git stash push -h >usage &&\n>  \tgrep -F \"usage: git stash [push\" usage\n>  '\n\nThis isn't on you, but I found the control flow here really confusing. I\nwonder if we could this cleanup before/squashed in. I may have missed\nsome cases, but it passes all tests.\n\nI.e. the whole business of pushing this \"did we assume?\" around goes\naway if we simply pass the command-line as-is to push_stash(), and ask\nit to determine this.\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 1ba24c11737..a588389d66b 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1665,10 +1665,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n \treturn ret;\n }\n \n-static int push_stash(int argc, const char **argv, const char *prefix,\n-\t\t      int push_assumed)\n+static int push_stash(int argc, const char **argv, const char *prefix)\n {\n-\tint force_assume = 0;\n \tint keep_index = -1;\n \tint only_staged = 0;\n \tint patch_mode = 0;\n@@ -1696,20 +1694,22 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n \t\tOPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),\n \t\tOPT_END()\n \t};\n+\tint push_assumed = argc == 1 || strcmp(argv[1], \"push\");\n \n-\tif (argc) {\n-\t\tforce_assume = !strcmp(argv[0], \"-p\");\n-\t\targc = parse_options(argc, argv, prefix, options,\n-\t\t\t\t     push_assumed ? git_stash_usage :\n-\t\t\t\t     git_stash_push_usage,\n-\t\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\tif (!push_assumed) {\n+\t\targc--;\n+\t\targv++;\n \t}\n \n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     push_assumed ? git_stash_usage :\n+\t\t\t     git_stash_push_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \tif (argc) {\n \t\tif (!strcmp(argv[0], \"--\")) {\n \t\t\targc--;\n \t\t\targv++;\n-\t\t} else if (push_assumed && !force_assume) {\n+\t\t} else if (push_assumed) {\n \t\t\tdie(\"subcommand wasn't specified; 'push' can't be assumed due to unexpected token '%s'\",\n \t\t\t    argv[0]);\n \t\t}\n@@ -1739,11 +1739,6 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n \t\t\t     include_untracked, only_staged);\n }\n \n-static int push_stash_unassumed(int argc, const char **argv, const char *prefix)\n-{\n-\treturn push_stash(argc, argv, prefix, 0);\n-}\n-\n static int save_stash(int argc, const char **argv, const char *prefix)\n {\n \tint keep_index = -1;\n@@ -1791,8 +1786,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n {\n \tpid_t pid = getpid();\n \tconst char *index_file;\n-\tstruct strvec args = STRVEC_INIT;\n-\tparse_opt_subcommand_fn *fn = NULL;\n+\tparse_opt_subcommand_fn *fn = push_stash;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"apply\", &fn, apply_stash),\n \t\tOPT_SUBCOMMAND(\"clear\", &fn, clear_stash),\n@@ -1803,7 +1797,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \t\tOPT_SUBCOMMAND(\"show\", &fn, show_stash),\n \t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n-\t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n+\t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\n@@ -1811,6 +1805,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \tgit_config(git_stash_config, NULL);\n \n \targc = parse_options(argc, argv, prefix, options, git_stash_usage,\n+\t\t\t     PARSE_OPT_KEEP_ARGV0 |\n \t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n \t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n@@ -1821,14 +1816,11 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n \tindex_file = get_index_file();\n \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n \t\t    (uintmax_t)pid);\n+\t\n+\tif (fn != push_stash) {\n+\t\targc--;\n+\t\targv++;\n+\t}\n \n-\tif (fn)\n-\t\treturn !!fn(argc, argv, prefix);\n-\telse if (!argc)\n-\t\treturn !!push_stash_unassumed(0, NULL, prefix);\n-\n-\t/* Assume 'stash push' */\n-\tstrvec_push(&args, \"push\");\n-\tstrvec_pushv(&args, argv);\n-\treturn !!push_stash(args.nr, args.v, prefix, 1);\n+\treturn !!fn(argc, argv, prefix);\n }\n"},{"id":"461616","messageId":"xmqq35dskl0p.fsf@gitster.g","threadId":"58218","inReplyTo":"20220819160411.1791200-13-szeder.dev@gmail.com","subject":"Re: [PATCH v2 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-08-19T20:59:02Z","receivedAt":"2022-08-19T20:59:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> 'git maintenanze' parses its subcommands with a couple of if\n\nnanze?\n\n> statements.  parse-options has just learned to parse subcommands, so\n> let's use that facility instead, with the benefits of shorter code,\n> handling missing or unknown subcommands, and listing subcommands for\n> Bash completion.\n>\n> This change makes 'git maintenance' consistent with other commands in\n> that the help text shown for '-h' goes to standard output, not error,\n> in the exit code and error message on unknown subcommand, and the\n> error message on missing subcommand.  There is a test checking these,\n> which is now updated accordingly.\n>\n> Note that some of the functions implementing each subcommand don't\n> accept any parameters, so add the (unused) 'argc', '**argv' and\n> '*prefix' parameters to make them match the type expected by\n> parse-options, and thus avoid casting function pointers.\n\nOK.  When the recently posted \"unused annotation\" and merges down\ntogether with this topic, we would want to add UNUSED() annotation\nto them so that we can get closer to be able to compile with -Wunused\nwarning enabled, but we do not have to worry about it yet.\n\n> +\tstruct option builtin_maintenance_options[] = {\n> +\t\tOPT_SUBCOMMAND(\"run\", &fn, maintenance_run),\n> +\t\tOPT_SUBCOMMAND(\"start\", &fn, maintenance_start),\n> +\t\tOPT_SUBCOMMAND(\"stop\", &fn, maintenance_stop),\n> +\t\tOPT_SUBCOMMAND(\"register\", &fn, maintenance_register),\n> +\t\tOPT_SUBCOMMAND(\"unregister\", &fn, maintenance_unregister),\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, builtin_maintenance_options,\n> +\t\t\t     builtin_maintenance_usage, 0);\n> +\treturn fn(argc, argv, prefix);\n"},{"id":"461655","messageId":"20220820102731.GC3373722@szeder.dev","threadId":"58218","inReplyTo":"220819.86czcw59w6.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 19/20] builtin/stash.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-20T10:27:31Z","receivedAt":"2022-08-20T10:27:40Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Aug 19, 2022 at 09:06:32PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Fri, Aug 19 2022, SZEDER Gábor wrote:\n> \n> > 'git stash' parses its subcommands with a long list of if-else if\n> > statements.  parse-options has just learned to parse subcommands, so\n> > let's use that facility instead, with the benefits of shorter code,\n> > and listing subcommands for Bash completion.\n> >\n> > Note that the push_stash() function implementing the 'push' subcommand\n> > accepts an extra flag parameter to indicate whether push was assumed,\n> > so add a wrapper function with the standard subcommand function\n> > signature.\n> >\n> > Note also that this change \"hides\" the '-h' option in 'git stash push\n> > -h' from the parse_option() call in cmd_stash(), as it comes after the\n> > subcommand.  Consequently, from now on it will emit the usage of the\n> > 'push' subcommand instead of the usage of 'git stash'.  We had a\n> > failing test for this case, which can now be flipped to expect\n> > success.\n> >\n> > Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n> > ---\n> >  builtin/stash.c  | 53 ++++++++++++++++++++++--------------------------\n> >  t/t3903-stash.sh |  2 +-\n> >  2 files changed, 25 insertions(+), 30 deletions(-)\n> >\n> > diff --git a/builtin/stash.c b/builtin/stash.c\n> > index a14e832e9f..1ba24c1173 100644\n> > --- a/builtin/stash.c\n> > +++ b/builtin/stash.c\n> > @@ -1739,6 +1739,11 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n> >  \t\t\t     include_untracked, only_staged);\n> >  }\n> >  \n> > +static int push_stash_unassumed(int argc, const char **argv, const char *prefix)\n> > +{\n> > +\treturn push_stash(argc, argv, prefix, 0);\n> > +}\n> > +\n> >  static int save_stash(int argc, const char **argv, const char *prefix)\n> >  {\n> >  \tint keep_index = -1;\n> > @@ -1787,15 +1792,28 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n> >  \tpid_t pid = getpid();\n> >  \tconst char *index_file;\n> >  \tstruct strvec args = STRVEC_INIT;\n> > -\n> > +\tparse_opt_subcommand_fn *fn = NULL;\n> >  \tstruct option options[] = {\n> > +\t\tOPT_SUBCOMMAND(\"apply\", &fn, apply_stash),\n> > +\t\tOPT_SUBCOMMAND(\"clear\", &fn, clear_stash),\n> > +\t\tOPT_SUBCOMMAND(\"drop\", &fn, drop_stash),\n> > +\t\tOPT_SUBCOMMAND(\"pop\", &fn, pop_stash),\n> > +\t\tOPT_SUBCOMMAND(\"branch\", &fn, branch_stash),\n> > +\t\tOPT_SUBCOMMAND(\"list\", &fn, list_stash),\n> > +\t\tOPT_SUBCOMMAND(\"show\", &fn, show_stash),\n> > +\t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n> > +\t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n> > +\t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n> > +\t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n> >  \t\tOPT_END()\n> >  \t};\n> >  \n> >  \tgit_config(git_stash_config, NULL);\n> >  \n> >  \targc = parse_options(argc, argv, prefix, options, git_stash_usage,\n> > -\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);\n> > +\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n> > +\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n> > +\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n> >  \n> >  \tprepare_repo_settings(the_repository);\n> >  \tthe_repository->settings.command_requires_full_index = 0;\n> > @@ -1804,33 +1822,10 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n> >  \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n> >  \t\t    (uintmax_t)pid);\n> >  \n> > -\tif (!argc)\n> > -\t\treturn !!push_stash(0, NULL, prefix, 0);\n> > -\telse if (!strcmp(argv[0], \"apply\"))\n> > -\t\treturn !!apply_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"clear\"))\n> > -\t\treturn !!clear_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"drop\"))\n> > -\t\treturn !!drop_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"pop\"))\n> > -\t\treturn !!pop_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"branch\"))\n> > -\t\treturn !!branch_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"list\"))\n> > -\t\treturn !!list_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"show\"))\n> > -\t\treturn !!show_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"store\"))\n> > -\t\treturn !!store_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"create\"))\n> > -\t\treturn !!create_stash(argc, argv, prefix);\n> > -\telse if (!strcmp(argv[0], \"push\"))\n> > -\t\treturn !!push_stash(argc, argv, prefix, 0);\n> > -\telse if (!strcmp(argv[0], \"save\"))\n> > -\t\treturn !!save_stash(argc, argv, prefix);\n> > -\telse if (*argv[0] != '-')\n> > -\t\tusage_msg_optf(_(\"unknown subcommand: %s\"),\n> > -\t\t\t       git_stash_usage, options, argv[0]);\n> > +\tif (fn)\n> > +\t\treturn !!fn(argc, argv, prefix);\n> > +\telse if (!argc)\n> > +\t\treturn !!push_stash_unassumed(0, NULL, prefix);\n> >  \n> >  \t/* Assume 'stash push' */\n> >  \tstrvec_push(&args, \"push\");\n> > diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\n> > index 2a4c3fd61c..376cc8f4ab 100755\n> > --- a/t/t3903-stash.sh\n> > +++ b/t/t3903-stash.sh\n> > @@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '\n> >  \tgrep -F \"or: git stash show\" usage\n> >  '\n> >  \n> > -test_expect_failure 'usage for subcommands should emit subcommand usage' '\n> > +test_expect_success 'usage for subcommands should emit subcommand usage' '\n> >  \ttest_expect_code 129 git stash push -h >usage &&\n> >  \tgrep -F \"usage: git stash [push\" usage\n> >  '\n> \n> This isn't on you, but I found the control flow here really confusing. I\n> wonder if we could this cleanup before/squashed in. I may have missed\n> some cases, but it passes all tests.\n\nI think it would be better to leave this for later.  I did notice that\nsomething is fishy around this 'force_assume' flag (argv[0] can never\npossibly be \"-p\") added in 8c3713cede (stash: eliminate crude option\nparsing, 2020-02-17), and even after your patch some of that fishiness\nremains (e.g. argv[0] can't possibly be \"--\" either).  Unfortunately,\nthe commit message doesn't explains what was intended, and I couldn't\nspend time on figuring it out, either.\n\n\n> I.e. the whole business of pushing this \"did we assume?\" around goes\n> away if we simply pass the command-line as-is to push_stash(), and ask\n> it to determine this.\n> \n> diff --git a/builtin/stash.c b/builtin/stash.c\n> index 1ba24c11737..a588389d66b 100644\n> --- a/builtin/stash.c\n> +++ b/builtin/stash.c\n> @@ -1665,10 +1665,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n>  \treturn ret;\n>  }\n>  \n> -static int push_stash(int argc, const char **argv, const char *prefix,\n> -\t\t      int push_assumed)\n> +static int push_stash(int argc, const char **argv, const char *prefix)\n>  {\n> -\tint force_assume = 0;\n>  \tint keep_index = -1;\n>  \tint only_staged = 0;\n>  \tint patch_mode = 0;\n> @@ -1696,20 +1694,22 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n>  \t\tOPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),\n>  \t\tOPT_END()\n>  \t};\n> +\tint push_assumed = argc == 1 || strcmp(argv[1], \"push\");\n>  \n> -\tif (argc) {\n> -\t\tforce_assume = !strcmp(argv[0], \"-p\");\n> -\t\targc = parse_options(argc, argv, prefix, options,\n> -\t\t\t\t     push_assumed ? git_stash_usage :\n> -\t\t\t\t     git_stash_push_usage,\n> -\t\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n> +\tif (!push_assumed) {\n> +\t\targc--;\n> +\t\targv++;\n>  \t}\n>  \n> +\targc = parse_options(argc, argv, prefix, options,\n> +\t\t\t     push_assumed ? git_stash_usage :\n> +\t\t\t     git_stash_push_usage,\n> +\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n>  \tif (argc) {\n>  \t\tif (!strcmp(argv[0], \"--\")) {\n>  \t\t\targc--;\n>  \t\t\targv++;\n> -\t\t} else if (push_assumed && !force_assume) {\n> +\t\t} else if (push_assumed) {\n>  \t\t\tdie(\"subcommand wasn't specified; 'push' can't be assumed due to unexpected token '%s'\",\n>  \t\t\t    argv[0]);\n>  \t\t}\n> @@ -1739,11 +1739,6 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n>  \t\t\t     include_untracked, only_staged);\n>  }\n>  \n> -static int push_stash_unassumed(int argc, const char **argv, const char *prefix)\n> -{\n> -\treturn push_stash(argc, argv, prefix, 0);\n> -}\n> -\n>  static int save_stash(int argc, const char **argv, const char *prefix)\n>  {\n>  \tint keep_index = -1;\n> @@ -1791,8 +1786,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n>  {\n>  \tpid_t pid = getpid();\n>  \tconst char *index_file;\n> -\tstruct strvec args = STRVEC_INIT;\n> -\tparse_opt_subcommand_fn *fn = NULL;\n> +\tparse_opt_subcommand_fn *fn = push_stash;\n>  \tstruct option options[] = {\n>  \t\tOPT_SUBCOMMAND(\"apply\", &fn, apply_stash),\n>  \t\tOPT_SUBCOMMAND(\"clear\", &fn, clear_stash),\n> @@ -1803,7 +1797,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n>  \t\tOPT_SUBCOMMAND(\"show\", &fn, show_stash),\n>  \t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n>  \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n> -\t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n> +\t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash),\n>  \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n>  \t\tOPT_END()\n>  \t};\n> @@ -1811,6 +1805,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n>  \tgit_config(git_stash_config, NULL);\n>  \n>  \targc = parse_options(argc, argv, prefix, options, git_stash_usage,\n> +\t\t\t     PARSE_OPT_KEEP_ARGV0 |\n>  \t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL |\n>  \t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT |\n>  \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n> @@ -1821,14 +1816,11 @@ int cmd_stash(int argc, const char **argv, const char *prefix)\n>  \tindex_file = get_index_file();\n>  \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n>  \t\t    (uintmax_t)pid);\n> +\t\n> +\tif (fn != push_stash) {\n> +\t\targc--;\n> +\t\targv++;\n> +\t}\n>  \n> -\tif (fn)\n> -\t\treturn !!fn(argc, argv, prefix);\n> -\telse if (!argc)\n> -\t\treturn !!push_stash_unassumed(0, NULL, prefix);\n> -\n> -\t/* Assume 'stash push' */\n> -\tstrvec_push(&args, \"push\");\n> -\tstrvec_pushv(&args, argv);\n> -\treturn !!push_stash(args.nr, args.v, prefix, 1);\n> +\treturn !!fn(argc, argv, prefix);\n>  }\n"},{"id":"461656","messageId":"20220820103128.GD3373722@szeder.dev","threadId":"58218","inReplyTo":"xmqqczcwksgj.fsf@gitster.g","subject":"Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-20T10:31:28Z","receivedAt":"2022-08-20T10:31:33Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Aug 19, 2022 at 11:18:20AM -0700, Junio C Hamano wrote:\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n> \n> > +static void print_args(int argc, const char **argv)\n> > +{\n> > +\tfor (int i = 0; i < argc; i++)\n> > +\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n> > +}\n> \n> It is not November 2022 yet (cf. Documentation/CodingGuidelines).\n\nOh, I've misunderstood Ævar's remarks about this in the previous\nround, and thought it's fair game.\n\n> Curious why \"%02d\", not \"%d\", or autoscale for cases where argc is\n> larger than 99, but I'll let it pass (iow no need to reroll only to\n> \"fix\" it).\n\nIt doesn't matter for these tests, but 'test-tool parse-options' uses\nthe same format to print args:\n\n  $ ./t/helper/test-tool parse-options foo bar baz |tail -n3\n  arg 00: foo\n  arg 01: bar\n  arg 02: baz\n\n"},{"id":"461657","messageId":"20220820111422.GE3373722@szeder.dev","threadId":"58218","inReplyTo":"220819.86k0746t7v.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-20T11:14:22Z","receivedAt":"2022-08-20T11:14:29Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Aug 19, 2022 at 07:23:44PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > +\t\terror(\"'cmd' is mandatory\");\n> > +\t\tusage_with_options(usage, test_flag_options);\n> \n> I think you want usage_msg_opt() instead.\n\nAs I responded to a similar remark in the previous round,\nparse-options uses the \"error:\" prefix in its error messages:\n\n  $ ./t/helper/test-tool parse-options -U\n  error: unknown switch `U'\n  $ ./t/helper/test-tool parse-options --unknown\n  error: unknown option `unknown'\n  $ ./t/helper/test-tool parse-options -i\n  error: switch `i' requires a value\n  $ ./t/helper/test-tool parse-options --int\n  error: option `integer' requires a value\n  $ ./t/helper/test-tool parse-options -i foo\n  error: switch `i' expects a numerical value\n  $ ./t/helper/test-tool parse-options --int foo\n  error: option `integer' expects a numerical value\n  $ ./t/helper/test-tool parse-options --quiet=42\n  error: option `quiet' takes no value\n  $ ./t/helper/test-tool parse-options --mode1 --mode2\n  error: option `mode2' is incompatible with --mode1\n\nSubcommand-related error messages should be consistent with these, and\nuse the \"error:\" prefix as well.  Unfortunately, both usage_msg_opt()\nand usage_msg_optf() use the \"fatal:\" prefix instead, so I will not\nuse those functions anywhere in this patch series.\n\n\n> > +test_expect_success 'NO_INTERNAL_HELP works for -h' '\n> > +\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n> > +\tcat err &&\n> \n> Stray \"cat\", presumably in error..\n\nLeftover debugging :(\n\n"},{"id":"461664","messageId":"xmqqr11ahagf.fsf@gitster.g","threadId":"58218","inReplyTo":"20220820103128.GD3373722@szeder.dev","subject":"Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-08-20T21:27:44Z","receivedAt":"2022-08-20T21:27:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> On Fri, Aug 19, 2022 at 11:18:20AM -0700, Junio C Hamano wrote:\n>> SZEDER Gábor <szeder.dev@gmail.com> writes:\n>> \n>> > +static void print_args(int argc, const char **argv)\n>> > +{\n>> > +\tfor (int i = 0; i < argc; i++)\n>> > +\t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n>> > +}\n>> \n>> It is not November 2022 yet (cf. Documentation/CodingGuidelines).\n>\n> Oh, I've misunderstood Ævar's remarks about this in the previous\n> round, and thought it's fair game.\n\nIf we make it a \"fair game\", when we find a platform that has\nproblems with the syntax, we will have to find them and fix up many\nplaces.  At least the number of the ones we let in by mistake are\nsmall and known, it may be still be manageable.\n\nIt is how to be conservative.\n\n> It doesn't matter for these tests, but 'test-tool parse-options' uses\n> the same format to print args:\n\nAs long as 99 is enough for us, I do not very much care.  I just\nnoticed an attempt to align that does not do a thorough job at it,\nand found it strange, that's all.\n"},{"id":"461738","messageId":"20220821175609.GF3373722@szeder.dev","threadId":"58218","inReplyTo":"220819.86pmgw5cpk.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 15/20] builtin/notes.c: let parse-options parse subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-08-21T17:56:09Z","receivedAt":"2022-08-21T17:56:17Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Aug 19, 2022 at 08:01:55PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Fri, Aug 19 2022, SZEDER Gábor wrote:\n> \n> > -\tint result;\n> >  \tconst char *override_notes_ref = NULL;\n> > +\tparse_opt_subcommand_fn *fn = list;\n> >  \tstruct option options[] = {\n> >  \t\tOPT_STRING(0, \"ref\", &override_notes_ref, N_(\"notes-ref\"),\n> >  \t\t\t   N_(\"use notes from <notes-ref>\")),\n> > +\t\tOPT_SUBCOMMAND(\"list\", &fn, list),\n> > +\t\tOPT_SUBCOMMAND(\"add\", &fn, add),\n> > +\t\tOPT_SUBCOMMAND(\"copy\", &fn, copy),\n> > +\t\tOPT_SUBCOMMAND(\"append\", &fn, append_edit),\n> > +\t\tOPT_SUBCOMMAND(\"edit\", &fn, append_edit),\n> > +\t\tOPT_SUBCOMMAND(\"show\", &fn, show),\n> > +\t\tOPT_SUBCOMMAND(\"merge\", &fn, merge),\n> > +\t\tOPT_SUBCOMMAND(\"remove\", &fn, remove_cmd),\n> > +\t\tOPT_SUBCOMMAND(\"prune\", &fn, prune),\n> > +\t\tOPT_SUBCOMMAND(\"get-ref\", &fn, get_ref),\n> >  \t\tOPT_END()\n> >  \t};\n> >  \n> >  \tgit_config(git_default_config, NULL);\n> >  \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n> > -\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n> > +\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n> > +\tif (fn == list && argc && strcmp(argv[0], \"list\")) {\n> > +\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n\nThis should have been `%s' here, and in cmd_remote() as well.\n\n> > +\t\tusage_with_options(git_notes_usage, options);\n> > +\t}\n> \n> I wanted to ask why the API can't smartly handle this, but your \"Found\n> an unknown option given to a command with\" comment in an earlier patch\n> answered it.\n\nIt's not about unknown options but rather about (non-option) arguments.\n\n'git notes list' doesn't accept any --options, and since this 'list'\nis the default operation mode, parse_options() is invoked without the\nPARSE_OPT_KEEP_UNKNOWN_OPT flag, so 'git notes --foo' errors out even\nwithout any of the extra checks in the above hunk.\n\nHowever, while 'git notes list' does accept non-option arguments\n(objects or refs), 'git notes' does not.  Alas, currently there is no\nway to tell parse_options() to error out upon finding a (non-option\nand non-subcommand) argument, it always keeps them in 'argv'; that's\nwhy we need these additional checks.\n\nNow, while we could add such a flag, of course, it would not be\nlimited to this one particular use case, so when the error is\ntriggered inside parse_options() I doubt that we could have this\nspecific \"unknown subcommand\" error message.\n\n\n> I think something in this direction would be a bit more readble/obvious,\n> as it avoids hardcoding \"list\":\n> \t\n> \tdiff --git a/builtin/notes.c b/builtin/notes.c\n> \tindex 42cbae46598..43d59b1a98e 100644\n> \t--- a/builtin/notes.c\n> \t+++ b/builtin/notes.c\n> \t@@ -995,7 +995,7 @@ static int get_ref(int argc, const char **argv, const char *prefix)\n> \t int cmd_notes(int argc, const char **argv, const char *prefix)\n> \t {\n> \t \tconst char *override_notes_ref = NULL;\n> \t-\tparse_opt_subcommand_fn *fn = list;\n> \t+\tparse_opt_subcommand_fn *fn = NULL;\n> \t \tstruct option options[] = {\n> \t \t\tOPT_STRING(0, \"ref\", &override_notes_ref, N_(\"notes-ref\"),\n> \t \t\t\t   N_(\"use notes from <notes-ref>\")),\n> \t@@ -1015,10 +1015,11 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n> \t \tgit_config(git_default_config, NULL);\n> \t \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n> \t \t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n> \t-\tif (fn == list && argc && strcmp(argv[0], \"list\")) {\n> \t-\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n> \t-\t\tusage_with_options(git_notes_usage, options);\n> \t-\t}\n> \t+\tif (!fn && argc)\n> \t+\t\tusage_msg_optf(_(\"unknown subcommand: %s\"),\n> \t+\t\t\t       git_notes_usage, options, argv[0]);\n> \t+\telse if (!fn)\n> \t+\t\tfn = list;\n> \t \n> \t \tif (override_notes_ref) {\n> \t \t\tstruct strbuf sb = STRBUF_INIT;\n> \n> I.e. we rely on the API setting it to non-NULL if it finds a subcommand,\n> otherwise we just set it to \"list\" after checking whether we have excess\n> arguments.\n\nOh, that does look nicer indeed.\n\n> > [...]\n> > -\telse if (!strcmp(argv[0], \"get-ref\"))\n> > -\t\tresult = get_ref(argc, argv, prefix);\n> > -\telse {\n> > -\t\tresult = error(_(\"unknown subcommand: %s\"), argv[0]);\n> > -\t\tusage_with_options(git_notes_usage, options);\n> > -\t}\n> > -\n> > -\treturn result ? 1 : 0;\n> > +\treturn !!fn(argc, argv, prefix);\n> >  }\n> \n> In any case this is a lot nicer, ditto previous comment about maybe\n> skipping the refactoring of this end code, but I'm also fine with\n> keeping it.\n"},{"id":"462637","messageId":"20220905185007.9042-1-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220819160411.1791200-1-szeder.dev@gmail.com","subject":"[PATCH 0/5] parse-options: minor cleanups for handling subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-09-05T18:50:02Z","receivedAt":"2022-09-05T18:50:20Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"I still wanted to make a few minor cleanups after v2 of\n'sg/parse-options-subcommand', but while I got distracted with some\nCoccinelle stuff that topic got merged to 'next', and it's now in\n'master'.  Oh, well.\n\nSo here are those cleanups on top of that topic (or on top of 'master').\n\nThe changes really are minor: the first four patches shouldn't alter\nbehavior in any way, and even the last one touches only two error\nmessages.\n\nSZEDER Gábor (5):\n  t0040-parse-options: remove leftover debugging\n  test-parse-options.c: don't use for loop initial declaration\n  test-parse-options.c: fix style of comparison with zero\n  notes: simplify default operation mode arguments check\n  notes, remote: show unknown subcommands between `'\n\n builtin/notes.c               | 11 +++++++----\n builtin/remote.c              |  2 +-\n t/helper/test-parse-options.c |  7 ++++---\n t/t0040-parse-options.sh      |  2 --\n 4 files changed, 12 insertions(+), 10 deletions(-)\n\n-- \n2.37.3.989.g4c3dfb3304\n\n"},{"id":"462638","messageId":"20220905185007.9042-3-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220905185007.9042-1-szeder.dev@gmail.com","subject":"[PATCH 2/5] test-parse-options.c: don't use for loop initial declaration","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-09-05T18:50:04Z","receivedAt":"2022-09-05T18:50:21Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"We would like to eventually use for loop initial declarations in our\ncodebase, but we are not there yet.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/helper/test-parse-options.c | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex aa0ad45851..9fe8ce66cb 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -195,7 +195,8 @@ int cmd__parse_options(int argc, const char **argv)\n \n static void print_args(int argc, const char **argv)\n {\n-\tfor (int i = 0; i < argc; i++)\n+\tint i;\n+\tfor (i = 0; i < argc; i++)\n \t\tprintf(\"arg %02d: %s\\n\", i, argv[i]);\n }\n \n-- \n2.37.3.989.g4c3dfb3304\n\n"},{"id":"462639","messageId":"20220905185007.9042-2-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220905185007.9042-1-szeder.dev@gmail.com","subject":"[PATCH 1/5] t0040-parse-options: remove leftover debugging","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-09-05T18:50:03Z","receivedAt":"2022-09-05T18:50:22Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/t0040-parse-options.sh | 2 --\n 1 file changed, 2 deletions(-)\n\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex b19b8d3486..5cc62306e3 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -500,7 +500,6 @@ test_expect_success 'KEEP_UNKNOWN_OPT works' '\n \n test_expect_success 'NO_INTERNAL_HELP works for -h' '\n \ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n-\tcat err &&\n \tgrep \"^error: unknown switch \\`h$SQ\" err &&\n \tgrep \"^usage: \" err\n '\n@@ -509,7 +508,6 @@ for help_opt in help help-all\n do\n \ttest_expect_success \"NO_INTERNAL_HELP works for --$help_opt\" \"\n \t\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&\n-\t\tcat err &&\n \t\tgrep '^error: unknown option \\`'$help_opt\\' err &&\n \t\tgrep '^usage: ' err\n \t\"\n-- \n2.37.3.989.g4c3dfb3304\n\n"},{"id":"462640","messageId":"20220905185007.9042-4-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220905185007.9042-1-szeder.dev@gmail.com","subject":"[PATCH 3/5] test-parse-options.c: fix style of comparison with zero","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-09-05T18:50:05Z","receivedAt":"2022-09-05T18:50:25Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The preferred style is '!argc' instead of 'argc == 0'.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/helper/test-parse-options.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 9fe8ce66cb..506835521a 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -255,7 +255,7 @@ int cmd__parse_options_flags(int argc, const char **argv)\n \targc = parse_options(argc, argv, NULL, test_flag_options, usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n+\tif (!argc || strcmp(argv[0], \"cmd\")) {\n \t\terror(\"'cmd' is mandatory\");\n \t\tusage_with_options(usage, test_flag_options);\n \t}\n@@ -313,7 +313,7 @@ int cmd__parse_subcommand(int argc, const char **argv)\n \targc = parse_options(argc, argv, NULL, test_flag_options, usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (argc == 0 || strcmp(argv[0], \"cmd\")) {\n+\tif (!argc || strcmp(argv[0], \"cmd\")) {\n \t\terror(\"'cmd' is mandatory\");\n \t\tusage_with_options(usage, test_flag_options);\n \t}\n-- \n2.37.3.989.g4c3dfb3304\n\n"},{"id":"462641","messageId":"20220905185007.9042-5-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220905185007.9042-1-szeder.dev@gmail.com","subject":"[PATCH 4/5] notes: simplify default operation mode arguments check","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-09-05T18:50:06Z","receivedAt":"2022-09-05T18:50:26Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"'git notes' has a default operation mode, but when invoked without a\nsubcommand it doesn't accept any arguments (although the 'list'\nsubcommand implementing the default operation mode does accept\narguments).  The condition checking this ended up a bit awkward, so\nlet's make it clearer.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/notes.c | 11 +++++++----\n 1 file changed, 7 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 42cbae4659..60410af572 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -995,7 +995,7 @@ static int get_ref(int argc, const char **argv, const char *prefix)\n int cmd_notes(int argc, const char **argv, const char *prefix)\n {\n \tconst char *override_notes_ref = NULL;\n-\tparse_opt_subcommand_fn *fn = list;\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_STRING(0, \"ref\", &override_notes_ref, N_(\"notes-ref\"),\n \t\t\t   N_(\"use notes from <notes-ref>\")),\n@@ -1015,9 +1015,12 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n \tgit_config(git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, git_notes_usage,\n \t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n-\tif (fn == list && argc && strcmp(argv[0], \"list\")) {\n-\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n-\t\tusage_with_options(git_notes_usage, options);\n+\tif (!fn) {\n+\t\tif (argc) {\n+\t\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n+\t\t\tusage_with_options(git_notes_usage, options);\n+\t\t}\n+\t\tfn = list;\n \t}\n \n \tif (override_notes_ref) {\n-- \n2.37.3.989.g4c3dfb3304\n\n"},{"id":"462642","messageId":"20220905185007.9042-6-szeder.dev@gmail.com","threadId":"58218","inReplyTo":"20220905185007.9042-1-szeder.dev@gmail.com","subject":"[PATCH 5/5] notes, remote: show unknown subcommands between `'","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-09-05T18:50:07Z","receivedAt":"2022-09-05T18:50:31Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Update the \"unknown subcommand\" error message in 'git notes' and 'git\nremote' to wrap the offending argument between `', to make it\nconsistent with the \"unknown switch/option/subcommand\" error messages\nin parse-options.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n builtin/notes.c  | 2 +-\n builtin/remote.c | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 60410af572..be51f69225 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -1017,7 +1017,7 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n \t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL);\n \tif (!fn) {\n \t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n+\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n \t\t\tusage_with_options(git_notes_usage, options);\n \t\t}\n \t\tfn = list;\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex 272c7b8d9e..07117e4c9a 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -1768,7 +1768,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix)\n \t\treturn !!fn(argc, argv, prefix);\n \t} else {\n \t\tif (argc) {\n-\t\t\terror(_(\"unknown subcommand: %s\"), argv[0]);\n+\t\t\terror(_(\"unknown subcommand: `%s'\"), argv[0]);\n \t\t\tusage_with_options(builtin_remote_usage, options);\n \t\t}\n \t\treturn !!show_all();\n-- \n2.37.3.989.g4c3dfb3304\n\n"},{"id":"462748","messageId":"xmqq5yhzuhgq.fsf@gitster.g","threadId":"58218","inReplyTo":"20220905185007.9042-1-szeder.dev@gmail.com","subject":"Re: [PATCH 0/5] parse-options: minor cleanups for handling subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-09-07T19:12:21Z","receivedAt":"2022-09-07T19:12:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> The changes really are minor: the first four patches shouldn't alter\n> behavior in any way, and even the last one touches only two error\n> messages.\n\nAll of them make sense.  I especially liked the simplification of\n\"defaults to list\" one.  \"git notes list list foo\" might change\nbehaviour but probably in a better direction.\n\nWill queue.\n\nThanks.\n"},{"id":"462764","messageId":"20220907212213.GA1795@szeder.dev","threadId":"58218","inReplyTo":"xmqq5yhzuhgq.fsf@gitster.g","subject":"Re: [PATCH 0/5] parse-options: minor cleanups for handling subcommands","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-09-07T21:22:13Z","receivedAt":"2022-09-07T21:22:20Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Wed, Sep 07, 2022 at 12:12:21PM -0700, Junio C Hamano wrote:\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n> \n> > The changes really are minor: the first four patches shouldn't alter\n> > behavior in any way, and even the last one touches only two error\n> > messages.\n> \n> All of them make sense.  I especially liked the simplification of\n> \"defaults to list\" one.  \"git notes list list foo\" might change\n> behaviour but probably in a better direction.\n\nv2.37, current master (or 'sg/parse-options-subcommand'), and this\nseries behave the same when running that command: control is\ndispatched to the function implementing the 'list' subcommand, which\nthen complains about \"too many arguments\", as it accepts only a one\nargument.\n\n"}]}