git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 00/20] parse-options: handle subcommands
@ 2022-07-25 12:38 SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
                   ` (23 more replies)
  0 siblings, 24 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

Several Git commands have subcommands to implement mutually exclusive
"operation modes", and they usually parse their subcommand argument
with a bunch of if-else if statements.

Teach parse-options to handle subcommands as well, which will result
in shorter and simpler code with consistent error handling and error
messages on unknown or missing subcommand, and it will also make
possible for our Bash completion script to handle subcommands
programmatically in a follow-up series [1].

Patches 1-8 are a mix of preparatory cleanups, documentation updates, and
test coverage improvements.

Patch 9 is the most important one, which teaches parse-options to handle
subcommands.

The remaining 10-20 convert most builtin commands with subcommands one by
one to use parse-options to handle their subcommand parameters.


This patch series has two conflicts with 'seen' (but none with 'next'):

  - builtin/bisect.c: after the conversion/rename from 'bisect--helper',
    cmd_bisect() doesn't use parse-options anymore.  Take what's on 'seen'
    to resolve the conflict.
    Note that the conflicting topic should have marked cmd_bisect() with
    the NO_PARSEOPT flag in 'git.c's command list.

  - builtin/stash.c: add OPT_SUBCOMMAND entries for the two new
    subcommands 'export' and 'import' to resolve the conflict.


[1] WIP with some one-liner commit messages and missing sign-offs here and
    there:

      https://github.com/szeder/git completion-subcommands


SZEDER Gábor (20):
  git.c: update NO_PARSEOPT markings
  t3301-notes.sh: check that default operation mode doesn't take
    arguments
  t5505-remote.sh: check the behavior without a subcommand
  t0040-parse-options: test parse_options() with various
    'parse_opt_flags'
  api-parse-options.txt: fix description of OPT_CMDMODE
  parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options
  parse-options: clarify the limitations of PARSE_OPT_NODASH
  parse-options: drop leading space from '--git-completion-helper'
    output
  parse-options: add support for parsing subcommands
  builtin/bundle.c: let parse-options parse subcommands
  builtin/commit-graph.c: let parse-options parse subcommands
  builtin/gc.c: let parse-options parse 'git maintenance's subcommands
  builtin/hook.c: let parse-option parse subcommands
  builtin/multi-pack-index.c: let parse-options parse subcommands
  builtin/notes.c: let parse-options parse subcommands
  builtin/reflog.c: let parse-options parse subcommands
  builtin/remote.c: let parse-options parse subcommands
  builtin/sparse-checkout.c: let parse-options parse subcommands
  builtin/stash.c: let parse-options parse subcommands
  builtin/worktree.c: let parse-options parse subcommands

 Documentation/technical/api-parse-options.txt |  49 +++-
 builtin/archive.c                             |   2 +-
 builtin/bisect--helper.c                      |   2 +-
 builtin/blame.c                               |   1 +
 builtin/bundle.c                              |  25 +-
 builtin/commit-graph.c                        |  30 +--
 builtin/difftool.c                            |   2 +-
 builtin/env--helper.c                         |   2 +-
 builtin/fast-export.c                         |   2 +-
 builtin/gc.c                                  |  42 +--
 builtin/hook.c                                |  12 +-
 builtin/log.c                                 |   4 +-
 builtin/multi-pack-index.c                    |  51 ++--
 builtin/notes.c                               |  43 ++-
 builtin/reflog.c                              |  43 +--
 builtin/remote.c                              |  70 +++--
 builtin/revert.c                              |   2 +-
 builtin/shortlog.c                            |   1 +
 builtin/sparse-checkout.c                     |  48 ++--
 builtin/stash.c                               |  59 ++--
 builtin/worktree.c                            |  31 +--
 diff.c                                        |   2 +-
 git.c                                         |  14 +-
 parse-options.c                               | 118 +++++++-
 parse-options.h                               |  27 +-
 t/helper/test-parse-options.c                 | 129 +++++++++
 t/helper/test-serve-v2.c                      |   2 +-
 t/helper/test-tool.c                          |   2 +
 t/helper/test-tool.h                          |   2 +
 t/t0040-parse-options.sh                      | 255 ++++++++++++++++++
 t/t3301-notes.sh                              |   5 +
 t/t3903-stash.sh                              |   2 +-
 t/t5318-commit-graph.sh                       |   4 +-
 t/t5505-remote.sh                             |  29 ++
 t/t7900-maintenance.sh                        |  10 +-
 35 files changed, 812 insertions(+), 310 deletions(-)

-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply	[flat|nested] 97+ messages in thread

* [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 14:31   ` Ævar Arnfjörð Bjarmason
  2022-07-26 19:55   ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments SZEDER Gábor
                   ` (22 subsequent siblings)
  23 siblings, 2 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

Our Bash completion script can complete --options for commands using
parse-options even when that command doesn't have a dedicated
completion function, but to do so the completion script must know
which commands use parse-options and which don't.  Therefore, commands
not using parse-options are marked in 'git.c's command list with the
NO_PARSEOPT flag.

Update this list, and remove this flag from the commands that by now
use parse-options.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 git.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/git.c b/git.c
index e5d62fa5a9..c4282f194a 100644
--- a/git.c
+++ b/git.c
@@ -489,14 +489,14 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)
 static struct cmd_struct commands[] = {
 	{ "add", cmd_add, RUN_SETUP | NEED_WORK_TREE },
 	{ "am", cmd_am, RUN_SETUP | NEED_WORK_TREE },
-	{ "annotate", cmd_annotate, RUN_SETUP | NO_PARSEOPT },
+	{ "annotate", cmd_annotate, RUN_SETUP },
 	{ "apply", cmd_apply, RUN_SETUP_GENTLY },
 	{ "archive", cmd_archive, RUN_SETUP_GENTLY },
 	{ "bisect--helper", cmd_bisect__helper, RUN_SETUP },
 	{ "blame", cmd_blame, RUN_SETUP },
 	{ "branch", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },
 	{ "bugreport", cmd_bugreport, RUN_SETUP_GENTLY },
-	{ "bundle", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },
+	{ "bundle", cmd_bundle, RUN_SETUP_GENTLY },
 	{ "cat-file", cmd_cat_file, RUN_SETUP },
 	{ "check-attr", cmd_check_attr, RUN_SETUP },
 	{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
@@ -514,7 +514,7 @@ static struct cmd_struct commands[] = {
 	{ "column", cmd_column, RUN_SETUP_GENTLY },
 	{ "commit", cmd_commit, RUN_SETUP | NEED_WORK_TREE },
 	{ "commit-graph", cmd_commit_graph, RUN_SETUP },
-	{ "commit-tree", cmd_commit_tree, RUN_SETUP | NO_PARSEOPT },
+	{ "commit-tree", cmd_commit_tree, RUN_SETUP },
 	{ "config", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },
 	{ "count-objects", cmd_count_objects, RUN_SETUP },
 	{ "credential", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },
@@ -553,7 +553,7 @@ static struct cmd_struct commands[] = {
 	{ "ls-files", cmd_ls_files, RUN_SETUP },
 	{ "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
 	{ "ls-tree", cmd_ls_tree, RUN_SETUP },
-	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },
+	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY },
 	{ "mailsplit", cmd_mailsplit, NO_PARSEOPT },
 	{ "maintenance", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },
 	{ "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE },
@@ -566,7 +566,7 @@ static struct cmd_struct commands[] = {
 	{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-tree", cmd_merge_tree, RUN_SETUP },
-	{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
+	{ "mktag", cmd_mktag, RUN_SETUP },
 	{ "mktree", cmd_mktree, RUN_SETUP },
 	{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },
 	{ "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE },
@@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {
 	{ "verify-tag", cmd_verify_tag, RUN_SETUP },
 	{ "version", cmd_version },
 	{ "whatchanged", cmd_whatchanged, RUN_SETUP },
-	{ "worktree", cmd_worktree, RUN_SETUP | NO_PARSEOPT },
+	{ "worktree", cmd_worktree, RUN_SETUP },
 	{ "write-tree", cmd_write_tree, RUN_SETUP },
 };
 
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 03/20] t5505-remote.sh: check the behavior without a subcommand SZEDER Gábor
                   ` (21 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git notes' without a subcommand defaults to listing all notes and
doesn't accept any arguments.

We are about to teach parse-options to handle subcommands, and update
'git notes' to make use of that new feature.  So let's add a test to
make sure that the upcoming changes don't inadvertenly change the
behavior in this corner case.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/t3301-notes.sh | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh
index d742be8840..3288aaec7d 100755
--- a/t/t3301-notes.sh
+++ b/t/t3301-notes.sh
@@ -505,6 +505,11 @@ test_expect_success 'list notes with "git notes"' '
 	test_cmp expect actual
 '
 
+test_expect_success '"git notes" without subcommand does not take arguments' '
+	test_expect_code 129 git notes HEAD^^ 2>err &&
+	grep "^error: unknown subcommand" err
+'
+
 test_expect_success 'list specific note with "git notes list <object>"' '
 	git rev-parse refs/notes/commits:$commit_3 >expect &&
 	git notes list HEAD^^ >actual &&
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 03/20] t5505-remote.sh: check the behavior without a subcommand
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 14:37   ` Ævar Arnfjörð Bjarmason
  2022-07-25 12:38 ` [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
                   ` (20 subsequent siblings)
  23 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git remote' without a subcommand defaults to listing all remotes and
doesn't accept any arguments except the '-v|--verbose' option.

We are about to teach parse-options to handle subcommands, and update
'git remote' to make use of that new feature.  So let's add some tests
to make sure that the upcoming changes don't inadvertently change the
behavior in these cases.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/t5505-remote.sh | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 6c7370f87f..f075dd4afa 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -241,6 +241,35 @@ test_expect_success 'add invalid foreign_vcs remote' '
 	test_cmp expect actual
 '
 
+test_expect_success 'without subcommand' '
+	(
+		cd test &&
+		git remote >actual &&
+		echo origin >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'without subcommand accepts -v' '
+	cat >test/expect <<-EOF &&
+	origin	$(pwd)/one (fetch)
+	origin	$(pwd)/one (push)
+	EOF
+	(
+		cd test &&
+		git remote -v >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'without subcommand does not take arguments' '
+	(
+		cd test &&
+		test_expect_code 129 git remote origin 2>err &&
+		grep "^error: Unknown subcommand:" err
+	)
+'
+
 cat >test/expect <<EOF
 * remote origin
   Fetch URL: $(pwd)/one
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (2 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 03/20] t5505-remote.sh: check the behavior without a subcommand SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 14:38   ` Ævar Arnfjörð Bjarmason
  2022-07-25 12:38 ` [PATCH 05/20] api-parse-options.txt: fix description of OPT_CMDMODE SZEDER Gábor
                   ` (19 subsequent siblings)
  23 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

In 't0040-parse-options.sh' we thoroughly test the parsing of all
types and forms of options, but in all those tests parse_options() is
always invoked with a 0 flags parameter.

Add a few tests to demonstrate how various 'enum parse_opt_flags'
values are supposed to influence option parsing.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/helper/test-parse-options.c | 61 ++++++++++++++++++++++++++++++
 t/helper/test-tool.c          |  1 +
 t/helper/test-tool.h          |  1 +
 t/t0040-parse-options.sh      | 70 +++++++++++++++++++++++++++++++++++
 4 files changed, 133 insertions(+)

diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
index 48d3cf6692..32b906bd6a 100644
--- a/t/helper/test-parse-options.c
+++ b/t/helper/test-parse-options.c
@@ -192,3 +192,64 @@ int cmd__parse_options(int argc, const char **argv)
 
 	return ret;
 }
+
+static int parse_options_flags__cmd(int argc, const char **argv,
+				    enum parse_opt_flags test_flags)
+{
+	const char *usage[] = {
+		"<...> cmd [options]",
+		NULL
+	};
+	int opt = 0;
+	const struct option options[] = {
+		OPT_INTEGER('o', "opt", &opt, "an integer option"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, NULL, options, usage, test_flags);
+
+	printf("opt: %d\n", opt);
+	for (int i = 0; i < argc; i++)
+		printf("arg %02d: %s\n", i, argv[i]);
+
+	return 0;
+}
+
+static enum parse_opt_flags test_flags = 0;
+static const struct option test_flag_options[] = {
+	OPT_GROUP("flag-options:"),
+	OPT_BIT(0, "keep-dashdash", &test_flags,
+		"pass PARSE_OPT_KEEP_DASHDASH to parse_options()",
+		PARSE_OPT_KEEP_DASHDASH),
+	OPT_BIT(0, "stop-at-non-option", &test_flags,
+		"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()",
+		PARSE_OPT_STOP_AT_NON_OPTION),
+	OPT_BIT(0, "keep-argv0", &test_flags,
+		"pass PARSE_OPT_KEEP_ARGV0 to parse_options()",
+		PARSE_OPT_KEEP_ARGV0),
+	OPT_BIT(0, "keep-unknown", &test_flags,
+		"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()",
+		PARSE_OPT_KEEP_UNKNOWN),
+	OPT_BIT(0, "no-internal-help", &test_flags,
+		"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()",
+		PARSE_OPT_NO_INTERNAL_HELP),
+	OPT_END()
+};
+
+int cmd__parse_options_flags(int argc, const char **argv)
+{
+	const char *usage[] = {
+		"test-tool parse-options-flags [flag-options] cmd [options]",
+		NULL
+	};
+
+	argc = parse_options(argc, argv, NULL, test_flag_options, usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+
+	if (argc == 0 || strcmp(argv[0], "cmd")) {
+		error("'cmd' is mandatory");
+		usage_with_options(usage, test_flag_options);
+	}
+
+	return parse_options_flags__cmd(argc, argv, test_flags);
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 318fdbab0c..6e62282b60 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -51,6 +51,7 @@ static struct test_cmd cmds[] = {
 	{ "online-cpus", cmd__online_cpus },
 	{ "pack-mtimes", cmd__pack_mtimes },
 	{ "parse-options", cmd__parse_options },
+	{ "parse-options-flags", cmd__parse_options_flags },
 	{ "parse-pathspec-file", cmd__parse_pathspec_file },
 	{ "partial-clone", cmd__partial_clone },
 	{ "path-utils", cmd__path_utils },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index bb79927163..d8e8403d70 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -41,6 +41,7 @@ int cmd__oidtree(int argc, const char **argv);
 int cmd__online_cpus(int argc, const char **argv);
 int cmd__pack_mtimes(int argc, const char **argv);
 int cmd__parse_options(int argc, const char **argv);
+int cmd__parse_options_flags(int argc, const char **argv);
 int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__partial_clone(int argc, const char **argv);
 int cmd__path_utils(int argc, const char **argv);
diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
index ed2fb620a9..569e906579 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh
@@ -456,4 +456,74 @@ test_expect_success '--end-of-options treats remainder as args' '
 	    --end-of-options --verbose
 '
 
+test_expect_success 'KEEP_DASHDASH works' '
+	test-tool parse-options-flags --keep-dashdash cmd --opt=1 -- --opt=2 --unknown >actual &&
+	cat >expect <<-\EOF &&
+	opt: 1
+	arg 00: --
+	arg 01: --opt=2
+	arg 02: --unknown
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'KEEP_ARGV0 works' '
+	test-tool parse-options-flags --keep-argv0 cmd arg0 --opt=3 >actual &&
+	cat >expect <<-\EOF &&
+	opt: 3
+	arg 00: cmd
+	arg 01: arg0
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'STOP_AT_NON_OPTION works' '
+	test-tool parse-options-flags --stop-at-non-option cmd --opt=4 arg0 --opt=5 --unknown >actual &&
+	cat >expect <<-\EOF &&
+	opt: 4
+	arg 00: arg0
+	arg 01: --opt=5
+	arg 02: --unknown
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'KEEP_UNKNOWN works' '
+	test-tool parse-options-flags --keep-unknown cmd --unknown=1 --opt=6 -u2 >actual &&
+	cat >expect <<-\EOF &&
+	opt: 6
+	arg 00: --unknown=1
+	arg 01: -u2
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'NO_INTERNAL_HELP works for -h' '
+	test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&
+	cat err &&
+	grep "^error: unknown switch \`h'\''" err &&
+	grep "^usage: " err
+'
+
+for help_opt in help help-all
+do
+	test_expect_success "NO_INTERNAL_HELP works for --$help_opt" "
+		test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&
+		cat err &&
+		grep '^error: unknown option \`'$help_opt\' err &&
+		grep '^usage: ' err
+	"
+done
+
+test_expect_success 'KEEP_UNKNOWN | NO_INTERNAL_HELP works' '
+	test-tool parse-options-flags --keep-unknown --no-internal-help cmd -h --help --help-all >actual &&
+	cat >expect <<-\EOF &&
+	opt: 0
+	arg 00: -h
+	arg 01: --help
+	arg 02: --help-all
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 05/20] api-parse-options.txt: fix description of OPT_CMDMODE
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (3 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options SZEDER Gábor
                   ` (18 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

The description of the 'OPT_CMDMODE' macro states that "enum_val is
set to int_var when ...", but it's the other way around, 'int_var' is
set to 'enum_val'.  Fix this.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 Documentation/technical/api-parse-options.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt
index acfd5dc1d8..5a04f3daec 100644
--- a/Documentation/technical/api-parse-options.txt
+++ b/Documentation/technical/api-parse-options.txt
@@ -236,7 +236,7 @@ There are some macros to easily define options:
 `OPT_CMDMODE(short, long, &int_var, description, enum_val)`::
 	Define an "operation mode" option, only one of which in the same
 	group of "operating mode" options that share the same `int_var`
-	can be given by the user. `enum_val` is set to `int_var` when the
+	can be given by the user. `int_var` is set to `enum_val` when the
 	option is used, but an error is reported if other "operating mode"
 	option has already set its value to the same `int_var`.
 
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (4 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 05/20] api-parse-options.txt: fix description of OPT_CMDMODE SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH SZEDER Gábor
                   ` (17 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

The description of 'PARSE_OPT_KEEP_UNKNOWN' starts with "Keep unknown
arguments instead of erroring out".  This is a bit misleading, as this
flag only applies to unknown --options, while non-option arguments are
kept even without this flag.

Update the description to clarify this, and rename the flag to
PARSE_OPTIONS_KEEP_UNKNOWN_OPT to make this obvious just by looking at
the flag name.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 Documentation/technical/api-parse-options.txt | 6 ++++--
 builtin/archive.c                             | 2 +-
 builtin/bisect--helper.c                      | 2 +-
 builtin/difftool.c                            | 2 +-
 builtin/env--helper.c                         | 2 +-
 builtin/fast-export.c                         | 2 +-
 builtin/log.c                                 | 4 ++--
 builtin/reflog.c                              | 4 ++--
 builtin/revert.c                              | 2 +-
 builtin/sparse-checkout.c                     | 4 ++--
 builtin/stash.c                               | 8 ++++----
 diff.c                                        | 2 +-
 parse-options.c                               | 6 +++---
 parse-options.h                               | 2 +-
 t/helper/test-parse-options.c                 | 6 +++---
 t/helper/test-serve-v2.c                      | 2 +-
 t/t0040-parse-options.sh                      | 8 ++++----
 17 files changed, 33 insertions(+), 31 deletions(-)

diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt
index 5a04f3daec..4412377fa3 100644
--- a/Documentation/technical/api-parse-options.txt
+++ b/Documentation/technical/api-parse-options.txt
@@ -90,8 +90,8 @@ Flags are the bitwise-or of:
 	Keep the first argument, which contains the program name.  It's
 	removed from argv[] by default.
 
-`PARSE_OPT_KEEP_UNKNOWN`::
-	Keep unknown arguments instead of erroring out.  This doesn't
+`PARSE_OPT_KEEP_UNKNOWN_OPT`::
+	Keep unknown options instead of erroring out.  This doesn't
 	work for all combinations of arguments as users might expect
 	it to do.  E.g. if the first argument in `--unknown --known`
 	takes a value (which we can't know), the second one is
@@ -101,6 +101,8 @@ Flags are the bitwise-or of:
 	non-option, not as a value belonging to the unknown option,
 	the parser early.  That's why parse_options() errors out if
 	both options are set.
+	Note that non-option arguments are always kept, even without
+	this flag.
 
 `PARSE_OPT_NO_INTERNAL_HELP`::
 	By default, parse_options() handles `-h`, `--help` and
diff --git a/builtin/archive.c b/builtin/archive.c
index 7176b041b6..f094390ee0 100644
--- a/builtin/archive.c
+++ b/builtin/archive.c
@@ -75,7 +75,7 @@ static int run_remote_archiver(int argc, const char **argv,
 
 #define PARSE_OPT_KEEP_ALL ( PARSE_OPT_KEEP_DASHDASH | 	\
 			     PARSE_OPT_KEEP_ARGV0 | 	\
-			     PARSE_OPT_KEEP_UNKNOWN |	\
+			     PARSE_OPT_KEEP_UNKNOWN_OPT |	\
 			     PARSE_OPT_NO_INTERNAL_HELP	)
 
 int cmd_archive(int argc, const char **argv, const char *prefix)
diff --git a/builtin/bisect--helper.c b/builtin/bisect--helper.c
index 8a052c7111..7097750fc6 100644
--- a/builtin/bisect--helper.c
+++ b/builtin/bisect--helper.c
@@ -1324,7 +1324,7 @@ int cmd_bisect__helper(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options,
 			     git_bisect_helper_usage,
-			     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (!cmdmode)
 		usage_with_options(git_bisect_helper_usage, options);
diff --git a/builtin/difftool.c b/builtin/difftool.c
index b3c509b8de..8706f68492 100644
--- a/builtin/difftool.c
+++ b/builtin/difftool.c
@@ -716,7 +716,7 @@ int cmd_difftool(int argc, const char **argv, const char *prefix)
 	symlinks = has_symlinks;
 
 	argc = parse_options(argc, argv, prefix, builtin_difftool_options,
-			     builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN |
+			     builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
 
 	if (tool_help)
diff --git a/builtin/env--helper.c b/builtin/env--helper.c
index 27349098b0..ea04c16636 100644
--- a/builtin/env--helper.c
+++ b/builtin/env--helper.c
@@ -50,7 +50,7 @@ int cmd_env__helper(int argc, const char **argv, const char *prefix)
 	};
 
 	argc = parse_options(argc, argv, prefix, opts, env__helper_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 	if (env_default && !*env_default)
 		usage_with_options(env__helper_usage, opts);
 	if (!cmdmode)
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index e1748fb98b..bf3c20dea2 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -1221,7 +1221,7 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix)
 	revs.sources = &revision_sources;
 	revs.rewrite_parents = 1;
 	argc = parse_options(argc, argv, prefix, options, fast_export_usage,
-			PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN);
+			PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 	argc = setup_revisions(argc, argv, &revs, NULL);
 	if (argc > 1)
 		usage_with_options (fast_export_usage, options);
diff --git a/builtin/log.c b/builtin/log.c
index 88a5e98875..fb84a0d399 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -199,7 +199,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
 	mailmap = use_mailmap_config;
 	argc = parse_options(argc, argv, prefix,
 			     builtin_log_options, builtin_log_usage,
-			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
 
 	if (quiet)
@@ -1926,7 +1926,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
 	 */
 	argc = parse_options(argc, argv, prefix, builtin_format_patch_options,
 			     builtin_format_patch_usage,
-			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
 
 	/* Make sure "0000-$sub.patch" gives non-negative length for $sub */
diff --git a/builtin/reflog.c b/builtin/reflog.c
index 4dd297dce8..b8b1f4f8ea 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -223,7 +223,7 @@ static int cmd_reflog_show(int argc, const char **argv, const char *prefix)
 
 	parse_options(argc, argv, prefix, options, reflog_show_usage,
 		      PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |
-		      PARSE_OPT_KEEP_UNKNOWN);
+		      PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	return cmd_log_reflog(argc, argv, prefix);
 }
@@ -410,7 +410,7 @@ int cmd_reflog(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options, reflog_usage,
 			     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |
-			     PARSE_OPT_KEEP_UNKNOWN |
+			     PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_NO_INTERNAL_HELP);
 
 	/*
diff --git a/builtin/revert.c b/builtin/revert.c
index 2554f9099c..ee2a0807f0 100644
--- a/builtin/revert.c
+++ b/builtin/revert.c
@@ -141,7 +141,7 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)
 
 	argc = parse_options(argc, argv, NULL, options, usage_str,
 			PARSE_OPT_KEEP_ARGV0 |
-			PARSE_OPT_KEEP_UNKNOWN);
+			PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c
index f91e29b56a..a5e4b95a9d 100644
--- a/builtin/sparse-checkout.c
+++ b/builtin/sparse-checkout.c
@@ -767,7 +767,7 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix,
 			     builtin_sparse_checkout_add_options,
 			     builtin_sparse_checkout_add_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	sanitize_paths(argc, argv, prefix, add_opts.skip_checks);
 
@@ -813,7 +813,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix,
 			     builtin_sparse_checkout_set_options,
 			     builtin_sparse_checkout_set_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (update_modes(&set_opts.cone_mode, &set_opts.sparse_index))
 		return 1;
diff --git a/builtin/stash.c b/builtin/stash.c
index 30fa101460..a14e832e9f 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -782,7 +782,7 @@ static int list_stash(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options,
 			     git_stash_list_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (!ref_exists(ref_stash))
 		return 0;
@@ -873,7 +873,7 @@ static int show_stash(int argc, const char **argv, const char *prefix)
 	init_revisions(&rev, prefix);
 
 	argc = parse_options(argc, argv, prefix, options, git_stash_show_usage,
-			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
 
 	strvec_push(&revision_args, argv[0]);
@@ -979,7 +979,7 @@ static int store_stash(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options,
 			     git_stash_store_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (argc != 1) {
 		if (!quiet)
@@ -1795,7 +1795,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 	git_config(git_stash_config, NULL);
 
 	argc = parse_options(argc, argv, prefix, options, git_stash_usage,
-			     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);
 
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
diff --git a/diff.c b/diff.c
index 974626a621..dd68281ba4 100644
--- a/diff.c
+++ b/diff.c
@@ -5661,7 +5661,7 @@ int diff_opt_parse(struct diff_options *options,
 
 	ac = parse_options(ac, av, prefix, options->parseopts, NULL,
 			   PARSE_OPT_KEEP_DASHDASH |
-			   PARSE_OPT_KEEP_UNKNOWN |
+			   PARSE_OPT_KEEP_UNKNOWN_OPT |
 			   PARSE_OPT_NO_INTERNAL_HELP |
 			   PARSE_OPT_ONE_SHOT |
 			   PARSE_OPT_STOP_AT_NON_OPTION);
diff --git a/parse-options.c b/parse-options.c
index edf55d3ef5..a0a2cf98fa 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -332,7 +332,7 @@ static enum parse_opt_result parse_long_opt(
 			rest = NULL;
 		if (!rest) {
 			/* abbreviated? */
-			if (!(p->flags & PARSE_OPT_KEEP_UNKNOWN) &&
+			if (!(p->flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&
 			    !strncmp(long_name, arg, arg_end - arg)) {
 is_abbreviated:
 				if (abbrev_option &&
@@ -515,7 +515,7 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
 	ctx->prefix = prefix;
 	ctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);
 	ctx->flags = flags;
-	if ((flags & PARSE_OPT_KEEP_UNKNOWN) &&
+	if ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&
 	    (flags & PARSE_OPT_STOP_AT_NON_OPTION) &&
 	    !(flags & PARSE_OPT_ONE_SHOT))
 		BUG("STOP_AT_NON_OPTION and KEEP_UNKNOWN don't go together");
@@ -839,7 +839,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
 unknown:
 		if (ctx->flags & PARSE_OPT_ONE_SHOT)
 			break;
-		if (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN))
+		if (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT))
 			return PARSE_OPT_UNKNOWN;
 		ctx->out[ctx->cpidx++] = ctx->argv[0];
 		ctx->opt = NULL;
diff --git a/parse-options.h b/parse-options.h
index 685fccac13..591df64191 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -30,7 +30,7 @@ enum parse_opt_flags {
 	PARSE_OPT_KEEP_DASHDASH = 1 << 0,
 	PARSE_OPT_STOP_AT_NON_OPTION = 1 << 1,
 	PARSE_OPT_KEEP_ARGV0 = 1 << 2,
-	PARSE_OPT_KEEP_UNKNOWN = 1 << 3,
+	PARSE_OPT_KEEP_UNKNOWN_OPT = 1 << 3,
 	PARSE_OPT_NO_INTERNAL_HELP = 1 << 4,
 	PARSE_OPT_ONE_SHOT = 1 << 5,
 	PARSE_OPT_SHELL_EVAL = 1 << 6,
diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
index 32b906bd6a..a715c772bd 100644
--- a/t/helper/test-parse-options.c
+++ b/t/helper/test-parse-options.c
@@ -227,9 +227,9 @@ static const struct option test_flag_options[] = {
 	OPT_BIT(0, "keep-argv0", &test_flags,
 		"pass PARSE_OPT_KEEP_ARGV0 to parse_options()",
 		PARSE_OPT_KEEP_ARGV0),
-	OPT_BIT(0, "keep-unknown", &test_flags,
-		"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()",
-		PARSE_OPT_KEEP_UNKNOWN),
+	OPT_BIT(0, "keep-unknown-opt", &test_flags,
+		"pass PARSE_OPT_KEEP_UNKNOWN_OPT to parse_options()",
+		PARSE_OPT_KEEP_UNKNOWN_OPT),
 	OPT_BIT(0, "no-internal-help", &test_flags,
 		"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()",
 		PARSE_OPT_NO_INTERNAL_HELP),
diff --git a/t/helper/test-serve-v2.c b/t/helper/test-serve-v2.c
index 28e905afc3..824e5c0a95 100644
--- a/t/helper/test-serve-v2.c
+++ b/t/helper/test-serve-v2.c
@@ -24,7 +24,7 @@ int cmd__serve_v2(int argc, const char **argv)
 	/* ignore all unknown cmdline switches for now */
 	argc = parse_options(argc, argv, prefix, options, serve_usage,
 			     PARSE_OPT_KEEP_DASHDASH |
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (advertise_capabilities)
 		protocol_v2_advertise_capabilities();
diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
index 569e906579..673a01ca71 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh
@@ -488,8 +488,8 @@ test_expect_success 'STOP_AT_NON_OPTION works' '
 	test_cmp expect actual
 '
 
-test_expect_success 'KEEP_UNKNOWN works' '
-	test-tool parse-options-flags --keep-unknown cmd --unknown=1 --opt=6 -u2 >actual &&
+test_expect_success 'KEEP_UNKNOWN_OPT works' '
+	test-tool parse-options-flags --keep-unknown-opt cmd --unknown=1 --opt=6 -u2 >actual &&
 	cat >expect <<-\EOF &&
 	opt: 6
 	arg 00: --unknown=1
@@ -515,8 +515,8 @@ do
 	"
 done
 
-test_expect_success 'KEEP_UNKNOWN | NO_INTERNAL_HELP works' '
-	test-tool parse-options-flags --keep-unknown --no-internal-help cmd -h --help --help-all >actual &&
+test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '
+	test-tool parse-options-flags --keep-unknown-opt --no-internal-help cmd -h --help --help-all >actual &&
 	cat >expect <<-\EOF &&
 	opt: 0
 	arg 00: -h
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (5 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 08/20] parse-options: drop leading space from '--git-completion-helper' output SZEDER Gábor
                   ` (16 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

Update the comment documenting 'struct option' to clarify that
PARSE_OPT_NODASH can only be an argumentless short option; see
51a9949eda (parseopt: add PARSE_OPT_NODASH, 2009-05-07).

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 parse-options.h | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/parse-options.h b/parse-options.h
index 591df64191..8cbfc7e8bf 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -109,7 +109,8 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,
  *				is last on the command line. If the option is
  *				not last it will require an argument.
  *				Should not be used with PARSE_OPT_OPTARG.
- *   PARSE_OPT_NODASH: this option doesn't start with a dash.
+ *   PARSE_OPT_NODASH: this option doesn't start with a dash; can only be a
+ *		       short option and can't accept arguments.
  *   PARSE_OPT_LITERAL_ARGHELP: says that argh shouldn't be enclosed in brackets
  *				(i.e. '<argh>') in the help message.
  *				Useful for options with multiple parameters.
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 08/20] parse-options: drop leading space from '--git-completion-helper' output
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (6 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
                   ` (15 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

The output of 'git <cmd> --git-completion-helper' always starts with a
space, e.g.:

  $ git config --git-completion-helper
   --global --system --local [...]

This doesn't matter for the completion script, because field splitting
discards that space anyway.

However, later patches in this series will teach parse-options to
handle subcommands, and subcommands will be included in the completion
helper output as well.  This will make the loop printing options (and
subcommands) a tad more complex, so I wanted to test the result.  The
test would have to account for the presence of that leading space,
which bugged my OCD, so let's get rid of it.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 parse-options.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/parse-options.c b/parse-options.c
index a0a2cf98fa..8748f88e6f 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -620,7 +620,8 @@ static int show_gitcomp(const struct option *opts, int show_all)
 			suffix = "=";
 		if (starts_with(opts->long_name, "no-"))
 			nr_noopts++;
-		printf(" --%s%s", opts->long_name, suffix);
+		printf("%s--%s%s", opts == original_opts ? "" : " ",
+		       opts->long_name, suffix);
 	}
 	show_negated_gitcomp(original_opts, show_all, -1);
 	show_negated_gitcomp(original_opts, show_all, nr_noopts);
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 09/20] parse-options: add support for parsing subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (7 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 08/20] parse-options: drop leading space from '--git-completion-helper' output SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 14:43   ` Ævar Arnfjörð Bjarmason
  2022-07-25 17:37   ` Junio C Hamano
  2022-07-25 12:38 ` [PATCH 10/20] builtin/bundle.c: let parse-options parse subcommands SZEDER Gábor
                   ` (14 subsequent siblings)
  23 siblings, 2 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

Several Git commands have subcommands to implement mutually exclusive
"operation modes", and they usually parse their subcommand argument
with a bunch of if-else if statements.

Teach parse-options to handle subcommands as well, which will result
in shorter and simpler code with consistent error handling and error
messages on unknown or missing subcommand, and it will also make
possible for our Bash completion script to handle subcommands
programmatically.

The approach is guided by the following observations:

  - Most subcommands [1] are implemented in dedicated functions, and
    most of those functions [2] either have a signature matching the
    'int cmd_foo(int argc, const char **argc, const char *prefix)'
    signature of builtin commands or can be trivially converted to
    that signature, because they miss only that last prefix parameter
    or have no parameters at all.

  - Subcommand arguments only have long form, and they have no double
    dash prefix, no negated form, and no description, and they don't
    take any arguments, and can't be abbreviated.

  - There must be exactly one subcommand among the arguments, or zero
    if the command has a default operation mode.

  - All arguments following the subcommand are considered to be
    arguments of the subcommand, and, conversely, arguments meant for
    the subcommand may not preceed the subcommand.

So in the end subcommand declaration and parsing would look something
like this:

    parse_opt_subcommand_fn *fn = NULL;
    struct option builtin_commit_graph_options[] = {
        OPT_STRING(0, "object-dir", &opts.obj_dir, N_("dir"),
                   N_("the object directory to store the graph")),
        OPT_SUBCOMMAND("verify", &fn, graph_verify),
        OPT_SUBCOMMAND("write", &fn, graph_write),
        OPT_END(),
    };
    argc = parse_options(argc, argv, prefix, options,
                         builtin_commit_graph_usage, 0);
    return fn(argc, argv, prefix);

Here each OPT_SUBCOMMAND specifies the name of the subcommand and the
function implementing it, and the same pointer to 'fn' where
parse_options() will store the function associated with the given
subcommand.  There is no need to check whether 'fn' is non-NULL before
invoking it: if there were no subcommands given on the command line
then the parse_options() call would error out and show usage.  In case
a command has a default operation mode, 'fn' should be initialized to
the function implementing that mode, and parse_options() should be
invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.

Some thoughts about the implementation:

  - Arguably it is a bit weird that the same pointer to 'fn' have to
    be specified as 'value' for each OPT_SUBCOMMAND, but we need a way
    to tell parse_options() where to put the function associated with
    the given subcommand, and I didn't like the alternatives:

      - Change parse_options()'s signature by adding a pointer to
        subcommand function to be set to the function associated with
        the given subcommand, affecting all callsites, even those that
        don't have subcommands.

      - Introduce a specific parse_options_and_subcommand() variant
        with that extra funcion parameter.

  - I decided against automatically calling the subcommand function
    from within parse_options(), because:

      - There are commands that have to perform additional actions
        after option parsing but before calling the function
        implementing the specified subcommand.

      - The return code of the subcommand is usually the return code
        of the git command, but preserving the return code of the
        automatically called subcommand function would have made the
        API awkward.

  - Also add a OPT_SUBCOMMAND_F() variant to allow specifying an
    option flag: we have two subcommands that are purposefully
    excluded from completion ('git remote rm' and 'git stash save'),
    so they'll have to be specified with the PARSE_OPT_NOCOMPLETE
    flag.

  - Some of the 'parse_opt_flags' don't make sense with subcommands,
    and using them is probably just an oversight or misunderstanding.
    Therefore parse_options() will BUG() when invoked with any of the
    following flags while the options array contains at least one
    OPT_SUBCOMMAND:

      - PARSE_OPT_KEEP_DASHDASH: parse_options() stops parsing
        arguments when encountering a "--" argument, so it doesn't
        make sense to expect and keep one before a subcommand, because
        it would prevent the parsing of the subcommand.

        However, this flag is allowed in combination with the
        PARSE_OPT_SUBCOMMAND_OPTIONAL flag, because the double dash
        might be meaningful for the command's default operation mode,
        e.g. to disambiguate refs and pathspecs.

      - PARSE_OPT_STOP_AT_NON_OPTION: As its name suggests, this flag
        tells parse_options() to stop as soon as it encouners a
        non-option argument, but subcommands are by definition not
        options...  so how could they be parsed, then?!

      - PARSE_OPT_KEEP_UNKNOWN: This flag can be used to collect any
        unknown --options and then pass them to a different command or
        subsystem.  Surely if a command has subcommands, then this
        functionality should rather be delegated to one of those
        subcommands, and not performed by the command itself.

        However, this flag is allowed in combination with the
        PARSE_OPT_SUBCOMMAND_OPTIONAL flag, making possible to pass
        --options to the default operation mode.

  - If the command with subcommands has a default operation mode, then
    all arguments to the command must preceed the arguments of the
    subcommand.

    AFAICT we don't have any commands where this makes a difference,
    because in those commands either only the command accepts any
    arguments ('notes' and 'remote'), or only the default subcommand
    ('reflog' and 'stash'), but never both.

  - The 'argv' array passed to subcommand functions currently starts
    with the name of the subcommand.  Keep this behavior.  AFAICT no
    subcommand functions depend on the actual content of 'argv[0]',
    but the parse_options() call handling their options expects that
    the options start at argv[1].

  - To support handling subcommands programmatically in our Bash
    completion script, 'git cmd --git-completion-helper' will now list
    both subcommands and regular --options, if any.  This means that
    the completion script will have to separate subcommands (i.e.
    words without a double dash prefix) from --options on its own, but
    that's rather easy to do, and it's not much work either, because
    the number of subcommands a command might have is rather low, and
    those commands accept only a single --option or none at all.  An
    alternative would be to introduce a separate option that lists
    only subcommands, but then the completion script would need not
    one but two git invocations and command substitutions for commands
    with subcommands.

    Note that this change doesn't affect the behavior of our Bash
    completion script, because when completing the --option of a
    command with subcommands, e.g. for 'git notes --<TAB>', then all
    subcommands will be filtered out anyway, as none of them will
    match the word to be completed starting with that double dash
    prefix.

[1] Except 'git rerere', because many of its subcommands are
    implemented in the bodies of the if-else if statements parsing the
    command's subcommand argument.

[2] Except 'credential', 'credential-store' and 'fsmonitor--daemon',
    because some of the functions implementing their subcommands take
    special parameters.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 Documentation/technical/api-parse-options.txt |  41 +++-
 builtin/blame.c                               |   1 +
 builtin/shortlog.c                            |   1 +
 parse-options.c                               | 113 ++++++++++-
 parse-options.h                               |  22 ++-
 t/helper/test-parse-options.c                 |  68 +++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t0040-parse-options.sh                      | 185 ++++++++++++++++++
 9 files changed, 424 insertions(+), 9 deletions(-)

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


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 10/20] builtin/bundle.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (8 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 11/20] builtin/commit-graph.c: " SZEDER Gábor
                   ` (13 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git bundle' parses its subcommands with a couple of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/bundle.c | 25 +++++++------------------
 1 file changed, 7 insertions(+), 18 deletions(-)

diff --git a/builtin/bundle.c b/builtin/bundle.c
index 2adad545a2..e80efce3a4 100644
--- a/builtin/bundle.c
+++ b/builtin/bundle.c
@@ -195,30 +195,19 @@ static int cmd_bundle_unbundle(int argc, const char **argv, const char *prefix)
 
 int cmd_bundle(int argc, const char **argv, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("create", &fn, cmd_bundle_create),
+		OPT_SUBCOMMAND("verify", &fn, cmd_bundle_verify),
+		OPT_SUBCOMMAND("list-heads", &fn, cmd_bundle_list_heads),
+		OPT_SUBCOMMAND("unbundle", &fn, cmd_bundle_unbundle),
 		OPT_END()
 	};
-	int result;
 
 	argc = parse_options(argc, argv, prefix, options, builtin_bundle_usage,
-		PARSE_OPT_STOP_AT_NON_OPTION);
+			     0);
 
 	packet_trace_identity("bundle");
 
-	if (argc < 2)
-		usage_with_options(builtin_bundle_usage, options);
-
-	else if (!strcmp(argv[0], "create"))
-		result = cmd_bundle_create(argc, argv, prefix);
-	else if (!strcmp(argv[0], "verify"))
-		result = cmd_bundle_verify(argc, argv, prefix);
-	else if (!strcmp(argv[0], "list-heads"))
-		result = cmd_bundle_list_heads(argc, argv, prefix);
-	else if (!strcmp(argv[0], "unbundle"))
-		result = cmd_bundle_unbundle(argc, argv, prefix);
-	else {
-		error(_("Unknown subcommand: %s"), argv[0]);
-		usage_with_options(builtin_bundle_usage, options);
-	}
-	return result ? 1 : 0;
+	return !!fn(argc, argv, prefix);
 }
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 11/20] builtin/commit-graph.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (9 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 10/20] builtin/bundle.c: let parse-options parse subcommands SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands SZEDER Gábor
                   ` (12 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git commit-graph' parses its subcommands with an if-else if
statement.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Note that the functions implementing each subcommand only accept the
'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter
to make them match the type expected by parse-options, and thus avoid
casting function pointers.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/commit-graph.c  | 30 +++++++++++++-----------------
 t/t5318-commit-graph.sh |  4 ++--
 2 files changed, 15 insertions(+), 19 deletions(-)

diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c
index 51c4040ea6..1eb5492cbd 100644
--- a/builtin/commit-graph.c
+++ b/builtin/commit-graph.c
@@ -58,7 +58,7 @@ static struct option *add_common_options(struct option *to)
 	return parse_options_concat(common_opts, to);
 }
 
-static int graph_verify(int argc, const char **argv)
+static int graph_verify(int argc, const char **argv, const char *prefix)
 {
 	struct commit_graph *graph = NULL;
 	struct object_directory *odb = NULL;
@@ -190,7 +190,7 @@ static int git_commit_graph_write_config(const char *var, const char *value,
 	return 0;
 }
 
-static int graph_write(int argc, const char **argv)
+static int graph_write(int argc, const char **argv, const char *prefix)
 {
 	struct string_list pack_indexes = STRING_LIST_INIT_DUP;
 	struct strbuf buf = STRBUF_INIT;
@@ -307,26 +307,22 @@ static int graph_write(int argc, const char **argv)
 
 int cmd_commit_graph(int argc, const char **argv, const char *prefix)
 {
-	struct option *builtin_commit_graph_options = common_opts;
+	parse_opt_subcommand_fn *fn = NULL;
+	struct option builtin_commit_graph_options[] = {
+		OPT_SUBCOMMAND("verify", &fn, graph_verify),
+		OPT_SUBCOMMAND("write", &fn, graph_write),
+		OPT_END(),
+	};
+	struct option *options = parse_options_concat(builtin_commit_graph_options, common_opts);
 
 	git_config(git_default_config, NULL);
-	argc = parse_options(argc, argv, prefix,
-			     builtin_commit_graph_options,
-			     builtin_commit_graph_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
-	if (!argc)
-		goto usage;
 
 	read_replace_refs = 0;
 	save_commit_buffer = 0;
 
-	if (!strcmp(argv[0], "verify"))
-		return graph_verify(argc, argv);
-	else if (argc && !strcmp(argv[0], "write"))
-		return graph_write(argc, argv);
+	argc = parse_options(argc, argv, prefix, options,
+			     builtin_commit_graph_usage, 0);
+	FREE_AND_NULL(options);
 
-	error(_("unrecognized subcommand: %s"), argv[0]);
-usage:
-	usage_with_options(builtin_commit_graph_usage,
-			   builtin_commit_graph_options);
+	return fn(argc, argv, prefix);
 }
diff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh
index be0b5641ff..e85b36e825 100755
--- a/t/t5318-commit-graph.sh
+++ b/t/t5318-commit-graph.sh
@@ -12,12 +12,12 @@ test_expect_success 'usage' '
 
 test_expect_success 'usage shown without sub-command' '
 	test_expect_code 129 git commit-graph 2>err &&
-	! grep error: err
+	grep usage: err
 '
 
 test_expect_success 'usage shown with an error on unknown sub-command' '
 	cat >expect <<-\EOF &&
-	error: unrecognized subcommand: unknown
+	error: unknown subcommand: unknown
 	EOF
 	test_expect_code 129 git commit-graph unknown 2>stderr &&
 	grep error stderr >actual &&
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (10 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 11/20] builtin/commit-graph.c: " SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 13/20] builtin/hook.c: let parse-option parse subcommands SZEDER Gábor
                   ` (11 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git maintenanze' parses its subcommands with a couple of if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

This change makes 'git maintenance' consistent with other commands in
that the help text shown for '-h' goes to standard output, not error,
in the exit code and error message on unknown subcommand, and the
error message on missing subcommand.  There is a test checking these,
which is now updated accordingly.

Note that some of the functions implementing each subcommand don't
accept any parameters, so add the (unused) 'argc', '**argv' and
'*prefix' parameters to make them match the type expected by
parse-options, and thus avoid casting function pointers.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/gc.c           | 42 +++++++++++++++++++++---------------------
 git.c                  |  2 +-
 t/t7900-maintenance.sh | 10 ++++++----
 3 files changed, 28 insertions(+), 26 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index eeff2b760e..19d6b3b558 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1465,7 +1465,7 @@ static char *get_maintpath(void)
 	return strbuf_detach(&sb, NULL);
 }
 
-static int maintenance_register(void)
+static int maintenance_register(int argc, const char **argv, const char *prefix)
 {
 	int rc;
 	char *config_value;
@@ -1509,7 +1509,7 @@ static int maintenance_register(void)
 	return rc;
 }
 
-static int maintenance_unregister(void)
+static int maintenance_unregister(int argc, const char **argv, const char *prefix)
 {
 	int rc;
 	struct child_process config_unset = CHILD_PROCESS_INIT;
@@ -2505,34 +2505,34 @@ static int maintenance_start(int argc, const char **argv, const char *prefix)
 	opts.scheduler = resolve_scheduler(opts.scheduler);
 	validate_scheduler(opts.scheduler);
 
-	if (maintenance_register())
+	if (maintenance_register(0, NULL, NULL)) /* It doesn't take any args */
 		warning(_("failed to add repo to global config"));
 	return update_background_schedule(&opts, 1);
 }
 
-static int maintenance_stop(void)
+static int maintenance_stop(int argc, const char **argv, const char *prefix)
 {
 	return update_background_schedule(NULL, 0);
 }
 
-static const char builtin_maintenance_usage[] =	N_("git maintenance <subcommand> [<options>]");
+static const char * const builtin_maintenance_usage[] = {
+	N_("git maintenance <subcommand> [<options>]"),
+	NULL,
+};
 
 int cmd_maintenance(int argc, const char **argv, const char *prefix)
 {
-	if (argc < 2 ||
-	    (argc == 2 && !strcmp(argv[1], "-h")))
-		usage(builtin_maintenance_usage);
-
-	if (!strcmp(argv[1], "run"))
-		return maintenance_run(argc - 1, argv + 1, prefix);
-	if (!strcmp(argv[1], "start"))
-		return maintenance_start(argc - 1, argv + 1, prefix);
-	if (!strcmp(argv[1], "stop"))
-		return maintenance_stop();
-	if (!strcmp(argv[1], "register"))
-		return maintenance_register();
-	if (!strcmp(argv[1], "unregister"))
-		return maintenance_unregister();
-
-	die(_("invalid subcommand: %s"), argv[1]);
+	parse_opt_subcommand_fn *fn = NULL;
+	struct option builtin_maintenance_options[] = {
+		OPT_SUBCOMMAND("run", &fn, maintenance_run),
+		OPT_SUBCOMMAND("start", &fn, maintenance_start),
+		OPT_SUBCOMMAND("stop", &fn, maintenance_stop),
+		OPT_SUBCOMMAND("register", &fn, maintenance_register),
+		OPT_SUBCOMMAND("unregister", &fn, maintenance_unregister),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, builtin_maintenance_options,
+			     builtin_maintenance_usage, 0);
+	return fn(argc, argv, prefix);
 }
diff --git a/git.c b/git.c
index c4282f194a..f52a955410 100644
--- a/git.c
+++ b/git.c
@@ -555,7 +555,7 @@ static struct cmd_struct commands[] = {
 	{ "ls-tree", cmd_ls_tree, RUN_SETUP },
 	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY },
 	{ "mailsplit", cmd_mailsplit, NO_PARSEOPT },
-	{ "maintenance", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },
+	{ "maintenance", cmd_maintenance, RUN_SETUP },
 	{ "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE },
 	{ "merge-base", cmd_merge_base, RUN_SETUP },
 	{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 74aa638475..1030a36c8d 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -32,11 +32,13 @@ test_systemd_analyze_verify () {
 }
 
 test_expect_success 'help text' '
-	test_expect_code 129 git maintenance -h 2>err &&
-	test_i18ngrep "usage: git maintenance <subcommand>" err &&
-	test_expect_code 128 git maintenance barf 2>err &&
-	test_i18ngrep "invalid subcommand: barf" err &&
+	test_expect_code 129 git maintenance -h >actual &&
+	test_i18ngrep "usage: git maintenance <subcommand>" actual &&
+	test_expect_code 129 git maintenance barf 2>err &&
+	test_i18ngrep "unknown subcommand: barf" err &&
+	test_i18ngrep "usage: git maintenance" err &&
 	test_expect_code 129 git maintenance 2>err &&
+	test_i18ngrep "error: need a subcommand" err &&
 	test_i18ngrep "usage: git maintenance" err
 '
 
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 13/20] builtin/hook.c: let parse-option parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (11 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 14/20] builtin/multi-pack-index.c: let parse-options " SZEDER Gábor
                   ` (10 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git hook' parses its currently only subcommand with an if statement.
parse-options has just learned to parse subcommands, so let's use that
facility instead, with the benefits of shorter code, handling missing
or unknown subcommands, and listing subcommands for Bash completion.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/hook.c | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/builtin/hook.c b/builtin/hook.c
index 54e5c6ec93..b6530d189a 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -67,18 +67,14 @@ static int run(int argc, const char **argv, const char *prefix)
 
 int cmd_hook(int argc, const char **argv, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option builtin_hook_options[] = {
+		OPT_SUBCOMMAND("run", &fn, run),
 		OPT_END(),
 	};
 
 	argc = parse_options(argc, argv, NULL, builtin_hook_options,
-			     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
-	if (!argc)
-		goto usage;
+			     builtin_hook_usage, 0);
 
-	if (!strcmp(argv[0], "run"))
-		return run(argc, argv, prefix);
-
-usage:
-	usage_with_options(builtin_hook_usage, builtin_hook_options);
+	return fn(argc, argv, prefix);
 }
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 14/20] builtin/multi-pack-index.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (12 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 13/20] builtin/hook.c: let parse-option parse subcommands SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 15/20] builtin/notes.c: " SZEDER Gábor
                   ` (9 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git multi-pack-index' parses its subcommands with a couple of if-else
if statements.  parse-options has just learned to parse subcommands,
so let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Note that the functions implementing each subcommand only accept the
'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter
to make them match the type expected by parse-options, and thus avoid
casting function pointers.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/multi-pack-index.c | 51 ++++++++++++++++----------------------
 1 file changed, 22 insertions(+), 29 deletions(-)

diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
index 8f24d59a75..b8320d597b 100644
--- a/builtin/multi-pack-index.c
+++ b/builtin/multi-pack-index.c
@@ -104,7 +104,8 @@ static void read_packs_from_stdin(struct string_list *to)
 	strbuf_release(&buf);
 }
 
-static int cmd_multi_pack_index_write(int argc, const char **argv)
+static int cmd_multi_pack_index_write(int argc, const char **argv,
+				      const char *prefix)
 {
 	struct option *options;
 	static struct option builtin_multi_pack_index_write_options[] = {
@@ -160,7 +161,8 @@ static int cmd_multi_pack_index_write(int argc, const char **argv)
 			       opts.refs_snapshot, opts.flags);
 }
 
-static int cmd_multi_pack_index_verify(int argc, const char **argv)
+static int cmd_multi_pack_index_verify(int argc, const char **argv,
+				       const char *prefix)
 {
 	struct option *options;
 	static struct option builtin_multi_pack_index_verify_options[] = {
@@ -186,7 +188,8 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv)
 	return verify_midx_file(the_repository, opts.object_dir, opts.flags);
 }
 
-static int cmd_multi_pack_index_expire(int argc, const char **argv)
+static int cmd_multi_pack_index_expire(int argc, const char **argv,
+				       const char *prefix)
 {
 	struct option *options;
 	static struct option builtin_multi_pack_index_expire_options[] = {
@@ -212,7 +215,8 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv)
 	return expire_midx_packs(the_repository, opts.object_dir, opts.flags);
 }
 
-static int cmd_multi_pack_index_repack(int argc, const char **argv)
+static int cmd_multi_pack_index_repack(int argc, const char **argv,
+				       const char *prefix)
 {
 	struct option *options;
 	static struct option builtin_multi_pack_index_repack_options[] = {
@@ -247,7 +251,15 @@ int cmd_multi_pack_index(int argc, const char **argv,
 			 const char *prefix)
 {
 	int res;
-	struct option *builtin_multi_pack_index_options = common_opts;
+	parse_opt_subcommand_fn *fn = NULL;
+	struct option builtin_multi_pack_index_options[] = {
+		OPT_SUBCOMMAND("repack", &fn, cmd_multi_pack_index_repack),
+		OPT_SUBCOMMAND("write", &fn, cmd_multi_pack_index_write),
+		OPT_SUBCOMMAND("verify", &fn, cmd_multi_pack_index_verify),
+		OPT_SUBCOMMAND("expire", &fn, cmd_multi_pack_index_expire),
+		OPT_END(),
+	};
+	struct option *options = parse_options_concat(builtin_multi_pack_index_options, common_opts);
 
 	git_config(git_default_config, NULL);
 
@@ -256,31 +268,12 @@ int cmd_multi_pack_index(int argc, const char **argv,
 	    the_repository->objects->odb)
 		opts.object_dir = xstrdup(the_repository->objects->odb->path);
 
-	argc = parse_options(argc, argv, prefix,
-			     builtin_multi_pack_index_options,
-			     builtin_multi_pack_index_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
-
-	if (!argc)
-		goto usage;
-
-	if (!strcmp(argv[0], "repack"))
-		res = cmd_multi_pack_index_repack(argc, argv);
-	else if (!strcmp(argv[0], "write"))
-		res =  cmd_multi_pack_index_write(argc, argv);
-	else if (!strcmp(argv[0], "verify"))
-		res =  cmd_multi_pack_index_verify(argc, argv);
-	else if (!strcmp(argv[0], "expire"))
-		res =  cmd_multi_pack_index_expire(argc, argv);
-	else {
-		error(_("unrecognized subcommand: %s"), argv[0]);
-		goto usage;
-	}
+	argc = parse_options(argc, argv, prefix, options,
+			     builtin_multi_pack_index_usage, 0);
+	FREE_AND_NULL(options);
+
+	res = fn(argc, argv, prefix);
 
 	free(opts.object_dir);
 	return res;
-
-usage:
-	usage_with_options(builtin_multi_pack_index_usage,
-			   builtin_multi_pack_index_options);
 }
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 15/20] builtin/notes.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (13 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 14/20] builtin/multi-pack-index.c: let parse-options " SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 16:49   ` Junio C Hamano
  2022-07-25 12:38 ` [PATCH 16/20] builtin/reflog.c: " SZEDER Gábor
                   ` (8 subsequent siblings)
  23 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git notes' parses its subcommands with a long list of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling unknown subcommands, and listing subcommands for Bash
completion.  Make sure that the default operation mode doesn't accept
any arguments.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/notes.c | 43 +++++++++++++++++--------------------------
 1 file changed, 17 insertions(+), 26 deletions(-)

diff --git a/builtin/notes.c b/builtin/notes.c
index a3d0d15a22..42cbae4659 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -994,17 +994,31 @@ static int get_ref(int argc, const char **argv, const char *prefix)
 
 int cmd_notes(int argc, const char **argv, const char *prefix)
 {
-	int result;
 	const char *override_notes_ref = NULL;
+	parse_opt_subcommand_fn *fn = list;
 	struct option options[] = {
 		OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"),
 			   N_("use notes from <notes-ref>")),
+		OPT_SUBCOMMAND("list", &fn, list),
+		OPT_SUBCOMMAND("add", &fn, add),
+		OPT_SUBCOMMAND("copy", &fn, copy),
+		OPT_SUBCOMMAND("append", &fn, append_edit),
+		OPT_SUBCOMMAND("edit", &fn, append_edit),
+		OPT_SUBCOMMAND("show", &fn, show),
+		OPT_SUBCOMMAND("merge", &fn, merge),
+		OPT_SUBCOMMAND("remove", &fn, remove_cmd),
+		OPT_SUBCOMMAND("prune", &fn, prune),
+		OPT_SUBCOMMAND("get-ref", &fn, get_ref),
 		OPT_END()
 	};
 
 	git_config(git_default_config, NULL);
 	argc = parse_options(argc, argv, prefix, options, git_notes_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
+	if (fn == list && argc && strcmp(argv[0], "list")) {
+		error(_("unknown subcommand: %s"), argv[0]);
+		usage_with_options(git_notes_usage, options);
+	}
 
 	if (override_notes_ref) {
 		struct strbuf sb = STRBUF_INIT;
@@ -1014,28 +1028,5 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
 		strbuf_release(&sb);
 	}
 
-	if (argc < 1 || !strcmp(argv[0], "list"))
-		result = list(argc, argv, prefix);
-	else if (!strcmp(argv[0], "add"))
-		result = add(argc, argv, prefix);
-	else if (!strcmp(argv[0], "copy"))
-		result = copy(argc, argv, prefix);
-	else if (!strcmp(argv[0], "append") || !strcmp(argv[0], "edit"))
-		result = append_edit(argc, argv, prefix);
-	else if (!strcmp(argv[0], "show"))
-		result = show(argc, argv, prefix);
-	else if (!strcmp(argv[0], "merge"))
-		result = merge(argc, argv, prefix);
-	else if (!strcmp(argv[0], "remove"))
-		result = remove_cmd(argc, argv, prefix);
-	else if (!strcmp(argv[0], "prune"))
-		result = prune(argc, argv, prefix);
-	else if (!strcmp(argv[0], "get-ref"))
-		result = get_ref(argc, argv, prefix);
-	else {
-		result = error(_("unknown subcommand: %s"), argv[0]);
-		usage_with_options(git_notes_usage, options);
-	}
-
-	return result ? 1 : 0;
+	return !!fn(argc, argv, prefix);
 }
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 16/20] builtin/reflog.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (14 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 15/20] builtin/notes.c: " SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 17/20] builtin/remote.c: " SZEDER Gábor
                   ` (7 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git reflog' parses its subcommands with a couple of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
and listing subcommands for Bash completion.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/reflog.c | 41 +++++++++++------------------------------
 1 file changed, 11 insertions(+), 30 deletions(-)

diff --git a/builtin/reflog.c b/builtin/reflog.c
index b8b1f4f8ea..d3f6d903fb 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -404,40 +404,21 @@ static int cmd_reflog_exists(int argc, const char **argv, const char *prefix)
 
 int cmd_reflog(int argc, const char **argv, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("show", &fn, cmd_reflog_show),
+		OPT_SUBCOMMAND("expire", &fn, cmd_reflog_expire),
+		OPT_SUBCOMMAND("delete", &fn, cmd_reflog_delete),
+		OPT_SUBCOMMAND("exists", &fn, cmd_reflog_exists),
 		OPT_END()
 	};
 
 	argc = parse_options(argc, argv, prefix, options, reflog_usage,
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL |
 			     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |
-			     PARSE_OPT_KEEP_UNKNOWN_OPT |
-			     PARSE_OPT_NO_INTERNAL_HELP);
-
-	/*
-	 * With "git reflog" we default to showing it. !argc is
-	 * impossible with PARSE_OPT_KEEP_ARGV0.
-	 */
-	if (argc == 1)
-		goto log_reflog;
-
-	if (!strcmp(argv[1], "-h"))
-		usage_with_options(reflog_usage, options);
-	else if (*argv[1] == '-')
-		goto log_reflog;
-
-	if (!strcmp(argv[1], "show"))
-		return cmd_reflog_show(argc - 1, argv + 1, prefix);
-	else if (!strcmp(argv[1], "expire"))
-		return cmd_reflog_expire(argc - 1, argv + 1, prefix);
-	else if (!strcmp(argv[1], "delete"))
-		return cmd_reflog_delete(argc - 1, argv + 1, prefix);
-	else if (!strcmp(argv[1], "exists"))
-		return cmd_reflog_exists(argc - 1, argv + 1, prefix);
-
-	/*
-	 * Fall-through for e.g. "git reflog -1", "git reflog master",
-	 * as well as the plain "git reflog" above goto above.
-	 */
-log_reflog:
-	return cmd_log_reflog(argc, argv, prefix);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
+	if (fn)
+		return fn(argc - 1, argv + 1, prefix);
+	else
+		return cmd_log_reflog(argc, argv, prefix);
 }
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 17/20] builtin/remote.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (15 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 16/20] builtin/reflog.c: " SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 18/20] builtin/sparse-checkout.c: " SZEDER Gábor
                   ` (6 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git remote' parses its subcommands with a long list of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling unknown subcommands, and listing subcommands for Bash
completion.  Make sure that the default operation mode doesn't accept
any arguments; and while at it remove the capitalization of the error
message and adjust the test checking it accordingly.

Note that 'git remote' has both 'remove' and 'rm' subcommands, and the
former is preferred [1], so hide the latter for completion.

Note also that the functions implementing each subcommand only accept
the 'argc' and '**argv' parameters, so add a (unused) '*prefix'
parameter to make them match the type expected by parse-options, and
thus avoid casting a bunch of function pointers.

[1] e17dba8fe1 (remote: prefer subcommand name 'remove' to 'rm',
    2012-09-06)

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/remote.c  | 70 +++++++++++++++++++++--------------------------
 t/t5505-remote.sh |  2 +-
 2 files changed, 32 insertions(+), 40 deletions(-)

diff --git a/builtin/remote.c b/builtin/remote.c
index d9b8746cb3..4a6d47c03a 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -150,7 +150,7 @@ static int parse_mirror_opt(const struct option *opt, const char *arg, int not)
 	return 0;
 }
 
-static int add(int argc, const char **argv)
+static int add(int argc, const char **argv, const char *prefix)
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
@@ -680,7 +680,7 @@ static void handle_push_default(const char* old_name, const char* new_name)
 }
 
 
-static int mv(int argc, const char **argv)
+static int mv(int argc, const char **argv, const char *prefix)
 {
 	int show_progress = isatty(2);
 	struct option options[] = {
@@ -844,7 +844,7 @@ static int mv(int argc, const char **argv)
 	return 0;
 }
 
-static int rm(int argc, const char **argv)
+static int rm(int argc, const char **argv, const char *prefix)
 {
 	struct option options[] = {
 		OPT_END()
@@ -1255,7 +1255,7 @@ static int show_all(void)
 	return result;
 }
 
-static int show(int argc, const char **argv)
+static int show(int argc, const char **argv, const char *prefix)
 {
 	int no_query = 0, result = 0, query_flag = 0;
 	struct option options[] = {
@@ -1358,7 +1358,7 @@ static int show(int argc, const char **argv)
 	return result;
 }
 
-static int set_head(int argc, const char **argv)
+static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
 	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
@@ -1463,7 +1463,7 @@ static int prune_remote(const char *remote, int dry_run)
 	return result;
 }
 
-static int prune(int argc, const char **argv)
+static int prune(int argc, const char **argv, const char *prefix)
 {
 	int dry_run = 0, result = 0;
 	struct option options[] = {
@@ -1492,7 +1492,7 @@ static int get_remote_default(const char *key, const char *value, void *priv)
 	return 0;
 }
 
-static int update(int argc, const char **argv)
+static int update(int argc, const char **argv, const char *prefix)
 {
 	int i, prune = -1;
 	struct option options[] = {
@@ -1575,7 +1575,7 @@ static int set_remote_branches(const char *remotename, const char **branches,
 	return 0;
 }
 
-static int set_branches(int argc, const char **argv)
+static int set_branches(int argc, const char **argv, const char *prefix)
 {
 	int add_mode = 0;
 	struct option options[] = {
@@ -1594,7 +1594,7 @@ static int set_branches(int argc, const char **argv)
 	return set_remote_branches(argv[0], argv + 1, add_mode);
 }
 
-static int get_url(int argc, const char **argv)
+static int get_url(int argc, const char **argv, const char *prefix)
 {
 	int i, push_mode = 0, all_mode = 0;
 	const char *remotename = NULL;
@@ -1647,7 +1647,7 @@ static int get_url(int argc, const char **argv)
 	return 0;
 }
 
-static int set_url(int argc, const char **argv)
+static int set_url(int argc, const char **argv, const char *prefix)
 {
 	int i, push_mode = 0, add_mode = 0, delete_mode = 0;
 	int matches = 0, negative_matches = 0;
@@ -1739,41 +1739,33 @@ static int set_url(int argc, const char **argv)
 
 int cmd_remote(int argc, const char **argv, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
 		OPT__VERBOSE(&verbose, N_("be verbose; must be placed before a subcommand")),
+		OPT_SUBCOMMAND("add", &fn, add),
+		OPT_SUBCOMMAND("rename", &fn, mv),
+		OPT_SUBCOMMAND_F("rm", &fn, rm, PARSE_OPT_NOCOMPLETE),
+		OPT_SUBCOMMAND("remove", &fn, rm),
+		OPT_SUBCOMMAND("set-head", &fn, set_head),
+		OPT_SUBCOMMAND("set-branches", &fn, set_branches),
+		OPT_SUBCOMMAND("get-url", &fn, get_url),
+		OPT_SUBCOMMAND("set-url", &fn, set_url),
+		OPT_SUBCOMMAND("show", &fn, show),
+		OPT_SUBCOMMAND("prune", &fn, prune),
+		OPT_SUBCOMMAND("update", &fn, update),
 		OPT_END()
 	};
-	int result;
 
 	argc = parse_options(argc, argv, prefix, options, builtin_remote_usage,
-		PARSE_OPT_STOP_AT_NON_OPTION);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
 
-	if (argc < 1)
-		result = show_all();
-	else if (!strcmp(argv[0], "add"))
-		result = add(argc, argv);
-	else if (!strcmp(argv[0], "rename"))
-		result = mv(argc, argv);
-	else if (!strcmp(argv[0], "rm") || !strcmp(argv[0], "remove"))
-		result = rm(argc, argv);
-	else if (!strcmp(argv[0], "set-head"))
-		result = set_head(argc, argv);
-	else if (!strcmp(argv[0], "set-branches"))
-		result = set_branches(argc, argv);
-	else if (!strcmp(argv[0], "get-url"))
-		result = get_url(argc, argv);
-	else if (!strcmp(argv[0], "set-url"))
-		result = set_url(argc, argv);
-	else if (!strcmp(argv[0], "show"))
-		result = show(argc, argv);
-	else if (!strcmp(argv[0], "prune"))
-		result = prune(argc, argv);
-	else if (!strcmp(argv[0], "update"))
-		result = update(argc, argv);
-	else {
-		error(_("Unknown subcommand: %s"), argv[0]);
-		usage_with_options(builtin_remote_usage, options);
+	if (fn) {
+		return !!fn(argc, argv, prefix);
+	} else {
+		if (argc) {
+			error(_("unknown subcommand: %s"), argv[0]);
+			usage_with_options(builtin_remote_usage, options);
+		}
+		return !!show_all();
 	}
-
-	return result ? 1 : 0;
 }
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index f075dd4afa..0cdb563994 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -266,7 +266,7 @@ test_expect_success 'without subcommand does not take arguments' '
 	(
 		cd test &&
 		test_expect_code 129 git remote origin 2>err &&
-		grep "^error: Unknown subcommand:" err
+		grep "^error: unknown subcommand:" err
 	)
 '
 
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 18/20] builtin/sparse-checkout.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (16 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 17/20] builtin/remote.c: " SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 19/20] builtin/stash.c: " SZEDER Gábor
                   ` (5 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git sparse-checkout' parses its subcommands with a couple of if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Note that some of the functions implementing each subcommand only
accept the 'argc' and '**argv' parameters, so add a (unused) '*prefix'
parameter to make them match the type expected by parse-options, and
thus avoid casting function pointers.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/sparse-checkout.c | 44 ++++++++++++++-------------------------
 1 file changed, 16 insertions(+), 28 deletions(-)

diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c
index a5e4b95a9d..7b39a150a9 100644
--- a/builtin/sparse-checkout.c
+++ b/builtin/sparse-checkout.c
@@ -48,7 +48,7 @@ static char const * const builtin_sparse_checkout_list_usage[] = {
 	NULL
 };
 
-static int sparse_checkout_list(int argc, const char **argv)
+static int sparse_checkout_list(int argc, const char **argv, const char *prefix)
 {
 	static struct option builtin_sparse_checkout_list_options[] = {
 		OPT_END(),
@@ -431,7 +431,7 @@ static struct sparse_checkout_init_opts {
 	int sparse_index;
 } init_opts;
 
-static int sparse_checkout_init(int argc, const char **argv)
+static int sparse_checkout_init(int argc, const char **argv, const char *prefix)
 {
 	struct pattern_list pl;
 	char *sparse_filename;
@@ -843,7 +843,8 @@ static struct sparse_checkout_reapply_opts {
 	int sparse_index;
 } reapply_opts;
 
-static int sparse_checkout_reapply(int argc, const char **argv)
+static int sparse_checkout_reapply(int argc, const char **argv,
+				   const char *prefix)
 {
 	static struct option builtin_sparse_checkout_reapply_options[] = {
 		OPT_BOOL(0, "cone", &reapply_opts.cone_mode,
@@ -876,7 +877,8 @@ static char const * const builtin_sparse_checkout_disable_usage[] = {
 	NULL
 };
 
-static int sparse_checkout_disable(int argc, const char **argv)
+static int sparse_checkout_disable(int argc, const char **argv,
+				   const char *prefix)
 {
 	static struct option builtin_sparse_checkout_disable_options[] = {
 		OPT_END(),
@@ -922,39 +924,25 @@ static int sparse_checkout_disable(int argc, const char **argv)
 
 int cmd_sparse_checkout(int argc, const char **argv, const char *prefix)
 {
-	static struct option builtin_sparse_checkout_options[] = {
+	parse_opt_subcommand_fn *fn = NULL;
+	struct option builtin_sparse_checkout_options[] = {
+		OPT_SUBCOMMAND("list", &fn, sparse_checkout_list),
+		OPT_SUBCOMMAND("init", &fn, sparse_checkout_init),
+		OPT_SUBCOMMAND("set", &fn, sparse_checkout_set),
+		OPT_SUBCOMMAND("add", &fn, sparse_checkout_add),
+		OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply),
+		OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable),
 		OPT_END(),
 	};
 
-	if (argc == 2 && !strcmp(argv[1], "-h"))
-		usage_with_options(builtin_sparse_checkout_usage,
-				   builtin_sparse_checkout_options);
-
 	argc = parse_options(argc, argv, prefix,
 			     builtin_sparse_checkout_options,
-			     builtin_sparse_checkout_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
+			     builtin_sparse_checkout_usage, 0);
 
 	git_config(git_default_config, NULL);
 
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
 
-	if (argc > 0) {
-		if (!strcmp(argv[0], "list"))
-			return sparse_checkout_list(argc, argv);
-		if (!strcmp(argv[0], "init"))
-			return sparse_checkout_init(argc, argv);
-		if (!strcmp(argv[0], "set"))
-			return sparse_checkout_set(argc, argv, prefix);
-		if (!strcmp(argv[0], "add"))
-			return sparse_checkout_add(argc, argv, prefix);
-		if (!strcmp(argv[0], "reapply"))
-			return sparse_checkout_reapply(argc, argv);
-		if (!strcmp(argv[0], "disable"))
-			return sparse_checkout_disable(argc, argv);
-	}
-
-	usage_with_options(builtin_sparse_checkout_usage,
-			   builtin_sparse_checkout_options);
+	return fn(argc, argv, prefix);
 }
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 19/20] builtin/stash.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (17 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 18/20] builtin/sparse-checkout.c: " SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 12:38 ` [PATCH 20/20] builtin/worktree.c: " SZEDER Gábor
                   ` (4 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git stash' parses its subcommands with a long list of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
and listing subcommands for Bash completion.

Note that the push_stash() function implementing the 'push' subcommand
accepts an extra flag parameter to indicate whether push was assumed,
so add a wrapper function with the standard subcommand function
signature.

Note also that this change "hides" the '-h' option in 'git stash push
-h' from the parse_option() call in cmd_stash(), as it comes after the
subcommand.  Consequently, from now on it will emit the usage of the
'push' subcommand instead of the usage of 'git stash'.  We had a
failing test for this case, which can now be flipped to expect
success.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/stash.c  | 53 ++++++++++++++++++++++--------------------------
 t/t3903-stash.sh |  2 +-
 2 files changed, 25 insertions(+), 30 deletions(-)

diff --git a/builtin/stash.c b/builtin/stash.c
index a14e832e9f..1ba24c1173 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1739,6 +1739,11 @@ static int push_stash(int argc, const char **argv, const char *prefix,
 			     include_untracked, only_staged);
 }
 
+static int push_stash_unassumed(int argc, const char **argv, const char *prefix)
+{
+	return push_stash(argc, argv, prefix, 0);
+}
+
 static int save_stash(int argc, const char **argv, const char *prefix)
 {
 	int keep_index = -1;
@@ -1787,15 +1792,28 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 	pid_t pid = getpid();
 	const char *index_file;
 	struct strvec args = STRVEC_INIT;
-
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("apply", &fn, apply_stash),
+		OPT_SUBCOMMAND("clear", &fn, clear_stash),
+		OPT_SUBCOMMAND("drop", &fn, drop_stash),
+		OPT_SUBCOMMAND("pop", &fn, pop_stash),
+		OPT_SUBCOMMAND("branch", &fn, branch_stash),
+		OPT_SUBCOMMAND("list", &fn, list_stash),
+		OPT_SUBCOMMAND("show", &fn, show_stash),
+		OPT_SUBCOMMAND("store", &fn, store_stash),
+		OPT_SUBCOMMAND("create", &fn, create_stash),
+		OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
+		OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
 		OPT_END()
 	};
 
 	git_config(git_stash_config, NULL);
 
 	argc = parse_options(argc, argv, prefix, options, git_stash_usage,
-			     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL |
+			     PARSE_OPT_KEEP_UNKNOWN_OPT |
+			     PARSE_OPT_KEEP_DASHDASH);
 
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
@@ -1804,33 +1822,10 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 	strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
 		    (uintmax_t)pid);
 
-	if (!argc)
-		return !!push_stash(0, NULL, prefix, 0);
-	else if (!strcmp(argv[0], "apply"))
-		return !!apply_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "clear"))
-		return !!clear_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "drop"))
-		return !!drop_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "pop"))
-		return !!pop_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "branch"))
-		return !!branch_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "list"))
-		return !!list_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "show"))
-		return !!show_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "store"))
-		return !!store_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "create"))
-		return !!create_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "push"))
-		return !!push_stash(argc, argv, prefix, 0);
-	else if (!strcmp(argv[0], "save"))
-		return !!save_stash(argc, argv, prefix);
-	else if (*argv[0] != '-')
-		usage_msg_optf(_("unknown subcommand: %s"),
-			       git_stash_usage, options, argv[0]);
+	if (fn)
+		return !!fn(argc, argv, prefix);
+	else if (!argc)
+		return !!push_stash_unassumed(0, NULL, prefix);
 
 	/* Assume 'stash push' */
 	strvec_push(&args, "push");
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 2a4c3fd61c..376cc8f4ab 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '
 	grep -F "or: git stash show" usage
 '
 
-test_expect_failure 'usage for subcommands should emit subcommand usage' '
+test_expect_success 'usage for subcommands should emit subcommand usage' '
 	test_expect_code 129 git stash push -h >usage &&
 	grep -F "usage: git stash [push" usage
 '
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 20/20] builtin/worktree.c: let parse-options parse subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (18 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 19/20] builtin/stash.c: " SZEDER Gábor
@ 2022-07-25 12:38 ` SZEDER Gábor
  2022-07-25 13:15 ` [PATCH 00/20] parse-options: handle subcommands Derrick Stolee
                   ` (3 subsequent siblings)
  23 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 12:38 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, SZEDER Gábor

'git worktree' parses its subcommands with a long list of if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/worktree.c | 31 ++++++++++++-------------------
 1 file changed, 12 insertions(+), 19 deletions(-)

diff --git a/builtin/worktree.c b/builtin/worktree.c
index cd62eef240..c6710b2552 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -1112,31 +1112,24 @@ static int repair(int ac, const char **av, const char *prefix)
 
 int cmd_worktree(int ac, const char **av, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("add", &fn, add),
+		OPT_SUBCOMMAND("prune", &fn, prune),
+		OPT_SUBCOMMAND("list", &fn, list),
+		OPT_SUBCOMMAND("lock", &fn, lock_worktree),
+		OPT_SUBCOMMAND("unlock", &fn, unlock_worktree),
+		OPT_SUBCOMMAND("move", &fn, move_worktree),
+		OPT_SUBCOMMAND("remove", &fn, remove_worktree),
+		OPT_SUBCOMMAND("repair", &fn, repair),
 		OPT_END()
 	};
 
 	git_config(git_worktree_config, NULL);
 
-	if (ac < 2)
-		usage_with_options(worktree_usage, options);
 	if (!prefix)
 		prefix = "";
-	if (!strcmp(av[1], "add"))
-		return add(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "prune"))
-		return prune(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "list"))
-		return list(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "lock"))
-		return lock_worktree(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "unlock"))
-		return unlock_worktree(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "move"))
-		return move_worktree(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "remove"))
-		return remove_worktree(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "repair"))
-		return repair(ac - 1, av + 1, prefix);
-	usage_with_options(worktree_usage, options);
+
+	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
+	return fn(ac, av, prefix);
 }
-- 
2.37.1.633.g6a0fa73e39


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* Re: [PATCH 00/20] parse-options: handle subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (19 preceding siblings ...)
  2022-07-25 12:38 ` [PATCH 20/20] builtin/worktree.c: " SZEDER Gábor
@ 2022-07-25 13:15 ` Derrick Stolee
  2022-07-25 16:00   ` SZEDER Gábor
  2022-07-25 17:13 ` Ævar Arnfjörð Bjarmason
                   ` (2 subsequent siblings)
  23 siblings, 1 reply; 97+ messages in thread
From: Derrick Stolee @ 2022-07-25 13:15 UTC (permalink / raw)
  To: SZEDER Gábor, git; +Cc: Ævar Arnfjörð Bjarmason

On 7/25/2022 8:38 AM, SZEDER Gábor wrote:
> Several Git commands have subcommands to implement mutually exclusive
> "operation modes", and they usually parse their subcommand argument
> with a bunch of if-else if statements.
> 
> Teach parse-options to handle subcommands as well, which will result
> in shorter and simpler code with consistent error handling and error
> messages on unknown or missing subcommand, and it will also make
> possible for our Bash completion script to handle subcommands
> programmatically in a follow-up series [1].

Since this has become an increasingly common pattern, I appreciate
that you have standardized it here.

> Patches 1-8 are a mix of preparatory cleanups, documentation updates, and
> test coverage improvements.
> 
> Patch 9 is the most important one, which teaches parse-options to handle
> subcommands.
> 
> The remaining 10-20 convert most builtin commands with subcommands one by
> one to use parse-options to handle their subcommand parameters.

I focused on reading the changes to the builtins I have experience with
(commit-graph, maintenance, multi-pack-index, sparse-checkout, worktree)
and found the adaptation to the new model very clean.

The one common thing I saw was that you are updating a function pointer
that you name "fn" but it could be more informative on first reading if
it was named something like "subcommand_fn".

> This patch series has two conflicts with 'seen' (but none with 'next'):

An upcoming conflict is going to be my bundle URI topic which is going
to replace 'git fetch --bundle-uri' with 'git bundle fetch' in its next
version. I'll wait to see how Junio applies this series and I'll think
about splitting the current series into "design doc" and "implementation"
just so I can build on your work here instead of colliding.

Thanks,
-Stolee


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-07-25 12:38 ` [PATCH 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
@ 2022-07-25 14:31   ` Ævar Arnfjörð Bjarmason
  2022-08-02 17:37     ` SZEDER Gábor
  2022-07-26 19:55   ` SZEDER Gábor
  1 sibling, 1 reply; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25 14:31 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git


On Mon, Jul 25 2022, SZEDER Gábor wrote:

> Our Bash completion script can complete --options for commands using
> parse-options even when that command doesn't have a dedicated
> completion function, but to do so the completion script must know
> which commands use parse-options and which don't.  Therefore, commands
> not using parse-options are marked in 'git.c's command list with the
> NO_PARSEOPT flag.
>
> Update this list, and remove this flag from the commands that by now
> use parse-options.
>
> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> ---
>  git.c | 12 ++++++------
>  1 file changed, 6 insertions(+), 6 deletions(-)
>
> diff --git a/git.c b/git.c
> index e5d62fa5a9..c4282f194a 100644
> --- a/git.c
> +++ b/git.c
> @@ -489,14 +489,14 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)
>  static struct cmd_struct commands[] = {
>  	{ "add", cmd_add, RUN_SETUP | NEED_WORK_TREE },
>  	{ "am", cmd_am, RUN_SETUP | NEED_WORK_TREE },
> -	{ "annotate", cmd_annotate, RUN_SETUP | NO_PARSEOPT },
> +	{ "annotate", cmd_annotate, RUN_SETUP },
>  	{ "apply", cmd_apply, RUN_SETUP_GENTLY },
>  	{ "archive", cmd_archive, RUN_SETUP_GENTLY },
>  	{ "bisect--helper", cmd_bisect__helper, RUN_SETUP },
>  	{ "blame", cmd_blame, RUN_SETUP },
>  	{ "branch", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },
>  	{ "bugreport", cmd_bugreport, RUN_SETUP_GENTLY },
> -	{ "bundle", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },
> +	{ "bundle", cmd_bundle, RUN_SETUP_GENTLY },
>  	{ "cat-file", cmd_cat_file, RUN_SETUP },
>  	{ "check-attr", cmd_check_attr, RUN_SETUP },
>  	{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
> @@ -514,7 +514,7 @@ static struct cmd_struct commands[] = {
>  	{ "column", cmd_column, RUN_SETUP_GENTLY },
>  	{ "commit", cmd_commit, RUN_SETUP | NEED_WORK_TREE },
>  	{ "commit-graph", cmd_commit_graph, RUN_SETUP },
> -	{ "commit-tree", cmd_commit_tree, RUN_SETUP | NO_PARSEOPT },
> +	{ "commit-tree", cmd_commit_tree, RUN_SETUP },
>  	{ "config", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },
>  	{ "count-objects", cmd_count_objects, RUN_SETUP },
>  	{ "credential", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },
> @@ -553,7 +553,7 @@ static struct cmd_struct commands[] = {
>  	{ "ls-files", cmd_ls_files, RUN_SETUP },
>  	{ "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
>  	{ "ls-tree", cmd_ls_tree, RUN_SETUP },
> -	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },
> +	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY },
>  	{ "mailsplit", cmd_mailsplit, NO_PARSEOPT },
>  	{ "maintenance", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },
>  	{ "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE },
> @@ -566,7 +566,7 @@ static struct cmd_struct commands[] = {
>  	{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
>  	{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
>  	{ "merge-tree", cmd_merge_tree, RUN_SETUP },
> -	{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
> +	{ "mktag", cmd_mktag, RUN_SETUP },
>  	{ "mktree", cmd_mktree, RUN_SETUP },
>  	{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },
>  	{ "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE },
> @@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {
>  	{ "verify-tag", cmd_verify_tag, RUN_SETUP },
>  	{ "version", cmd_version },
>  	{ "whatchanged", cmd_whatchanged, RUN_SETUP },
> -	{ "worktree", cmd_worktree, RUN_SETUP | NO_PARSEOPT },
> +	{ "worktree", cmd_worktree, RUN_SETUP },
>  	{ "write-tree", cmd_write_tree, RUN_SETUP },
>  };

I have a superset of this change in my local tree:
https://github.com/avar/git/commit/f0076de17fd

I.e. "parseopt" here really means "handles --git-completion-helper", so
we can also address the diff family here. See the test that's associated
with the change (which you might want to grab in any case, i.e. to have
tests in this area).

But as far as this goes this looks good to me.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 03/20] t5505-remote.sh: check the behavior without a subcommand
  2022-07-25 12:38 ` [PATCH 03/20] t5505-remote.sh: check the behavior without a subcommand SZEDER Gábor
@ 2022-07-25 14:37   ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25 14:37 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git


On Mon, Jul 25 2022, SZEDER Gábor wrote:

>  t/t5505-remote.sh | 29 +++++++++++++++++++++++++++++
>  1 file changed, 29 insertions(+)
>
> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
> index 6c7370f87f..f075dd4afa 100755
> --- a/t/t5505-remote.sh
> +++ b/t/t5505-remote.sh
> @@ -241,6 +241,35 @@ test_expect_success 'add invalid foreign_vcs remote' '
>  	test_cmp expect actual
>  '
>  
> +test_expect_success 'without subcommand' '
> +	(
> +		cd test &&
> +		git remote >actual &&
> +		echo origin >expect &&
> +		test_cmp expect actual
> +	)
> +'
> +
> +test_expect_success 'without subcommand accepts -v' '
> +	cat >test/expect <<-EOF &&
> +	origin	$(pwd)/one (fetch)
> +	origin	$(pwd)/one (push)
> +	EOF
> +	(
> +		cd test &&
> +		git remote -v >actual &&
> +		test_cmp expect actual
> +	)
> +'
> +
> +test_expect_success 'without subcommand does not take arguments' '
> +	(
> +		cd test &&
> +		test_expect_code 129 git remote origin 2>err &&
> +		grep "^error: Unknown subcommand:" err
> +	)
> +'
> +

This can be shortened quite a bit by getting rid of the sub-shells:
https://github.com/avar/git/commit/e8eebd4eef6

But that version came from an earlier version of yours without those -v
tests...

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-07-25 12:38 ` [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
@ 2022-07-25 14:38   ` Ævar Arnfjörð Bjarmason
  2022-08-12 15:04     ` SZEDER Gábor
  0 siblings, 1 reply; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25 14:38 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git


On Mon, Jul 25 2022, SZEDER Gábor wrote:

> In 't0040-parse-options.sh' we thoroughly test the parsing of all
> types and forms of options, but in all those tests parse_options() is
> always invoked with a 0 flags parameter.
>
> Add a few tests to demonstrate how various 'enum parse_opt_flags'
> values are supposed to influence option parsing.
>
> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> ---
>  t/helper/test-parse-options.c | 61 ++++++++++++++++++++++++++++++
>  t/helper/test-tool.c          |  1 +
>  t/helper/test-tool.h          |  1 +
>  t/t0040-parse-options.sh      | 70 +++++++++++++++++++++++++++++++++++
>  4 files changed, 133 insertions(+)
>
> diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
> index 48d3cf6692..32b906bd6a 100644
> --- a/t/helper/test-parse-options.c
> +++ b/t/helper/test-parse-options.c
> @@ -192,3 +192,64 @@ int cmd__parse_options(int argc, const char **argv)
>  
>  	return ret;
>  }
> +
> +static int parse_options_flags__cmd(int argc, const char **argv,
> +				    enum parse_opt_flags test_flags)
> +{
> +	const char *usage[] = {
> +		"<...> cmd [options]",
> +		NULL
> +	};
> +	int opt = 0;
> +	const struct option options[] = {
> +		OPT_INTEGER('o', "opt", &opt, "an integer option"),
> +		OPT_END()
> +	};
> +
> +	argc = parse_options(argc, argv, NULL, options, usage, test_flags);
> +
> +	printf("opt: %d\n", opt);
> +	for (int i = 0; i < argc; i++)
> +		printf("arg %02d: %s\n", i, argv[i]);
> +
> +	return 0;
> +}
> +
> +static enum parse_opt_flags test_flags = 0;
> +static const struct option test_flag_options[] = {
> +	OPT_GROUP("flag-options:"),
> +	OPT_BIT(0, "keep-dashdash", &test_flags,
> +		"pass PARSE_OPT_KEEP_DASHDASH to parse_options()",
> +		PARSE_OPT_KEEP_DASHDASH),
> +	OPT_BIT(0, "stop-at-non-option", &test_flags,
> +		"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()",
> +		PARSE_OPT_STOP_AT_NON_OPTION),
> +	OPT_BIT(0, "keep-argv0", &test_flags,
> +		"pass PARSE_OPT_KEEP_ARGV0 to parse_options()",
> +		PARSE_OPT_KEEP_ARGV0),
> +	OPT_BIT(0, "keep-unknown", &test_flags,
> +		"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()",
> +		PARSE_OPT_KEEP_UNKNOWN),
> +	OPT_BIT(0, "no-internal-help", &test_flags,
> +		"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()",
> +		PARSE_OPT_NO_INTERNAL_HELP),
> +	OPT_END()
> +};
> +
> +int cmd__parse_options_flags(int argc, const char **argv)
> +{
> +	const char *usage[] = {
> +		"test-tool parse-options-flags [flag-options] cmd [options]",
> +		NULL
> +	};
> +
> +	argc = parse_options(argc, argv, NULL, test_flag_options, usage,
> +			     PARSE_OPT_STOP_AT_NON_OPTION);
> +
> +	if (argc == 0 || strcmp(argv[0], "cmd")) {
> +		error("'cmd' is mandatory");
> +		usage_with_options(usage, test_flag_options);
> +	}
> +
> +	return parse_options_flags__cmd(argc, argv, test_flags);
> +}
> diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
> index 318fdbab0c..6e62282b60 100644
> --- a/t/helper/test-tool.c
> +++ b/t/helper/test-tool.c
> @@ -51,6 +51,7 @@ static struct test_cmd cmds[] = {
>  	{ "online-cpus", cmd__online_cpus },
>  	{ "pack-mtimes", cmd__pack_mtimes },
>  	{ "parse-options", cmd__parse_options },
> +	{ "parse-options-flags", cmd__parse_options_flags },
>  	{ "parse-pathspec-file", cmd__parse_pathspec_file },
>  	{ "partial-clone", cmd__partial_clone },
>  	{ "path-utils", cmd__path_utils },

I wanted to add some new parse_options() code to
t/helper/test-parse-options.c in the past, but was stymied by its
cmd_*() going through a singular parse_options().

So just creating a new callback is a neat solution.

But wouldn't it be better to just create a new
t/helper/test-parse-options-something-.c & test file? It seems this
doesn't really share anything with the current helper & tests...

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 09/20] parse-options: add support for parsing subcommands
  2022-07-25 12:38 ` [PATCH 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
@ 2022-07-25 14:43   ` Ævar Arnfjörð Bjarmason
  2022-07-25 19:29     ` SZEDER Gábor
  2022-07-25 17:37   ` Junio C Hamano
  1 sibling, 1 reply; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25 14:43 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git


On Mon, Jul 25 2022, SZEDER Gábor wrote:

> Several Git commands have subcommands to implement mutually exclusive
> "operation modes", and they usually parse their subcommand argument
> with a bunch of if-else if statements.

I'll need do look this over in more details, just some comments on the
non-meaty parts for now:

> diff --git a/builtin/blame.c b/builtin/blame.c
> index 02e39420b6..a9fe8cf7a6 100644
> --- a/builtin/blame.c
> +++ b/builtin/blame.c
> @@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
>  			break;
>  		case PARSE_OPT_HELP:
>  		case PARSE_OPT_ERROR:
> +		case PARSE_OPT_SUBCOMMAND:
>  			exit(129);
>  		case PARSE_OPT_COMPLETE:
>  			exit(0);
> diff --git a/builtin/shortlog.c b/builtin/shortlog.c
> index 086dfee45a..7a1e1fe7c0 100644
> --- a/builtin/shortlog.c
> +++ b/builtin/shortlog.c
> @@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
>  			break;
>  		case PARSE_OPT_HELP:
>  		case PARSE_OPT_ERROR:
> +		case PARSE_OPT_SUBCOMMAND:
>  			exit(129);
>  		case PARSE_OPT_COMPLETE:
>  			exit(0);

This feels a bit like carrying forward an API wart, i.e. shouldn't we
instead BUG() if we are returning a PARSE_OPT_SUBCOMMAND from
parse_options_step() for options lists that don't have
PARSE_OPT_SUBCOMMAND in them?

I.e. is this even reachable, or just something to suppress the compiler
complaining about missing enum labels?

>  static void check_typos(const char *arg, const struct option *options)
>  {
>  	if (strlen(arg) < 3)
> @@ -442,6 +457,7 @@ static void check_typos(const char *arg, const struct option *options)
>  static void parse_options_check(const struct option *opts)
>  {
>  	char short_opts[128];
> +	void *subcommand_value = NULL;
>  
>  	memset(short_opts, '\0', sizeof(short_opts));
>  	for (; opts->type != OPTION_END; opts++) {
> @@ -489,6 +505,14 @@ static void parse_options_check(const struct option *opts)
>  			       "Are you using parse_options_step() directly?\n"
>  			       "That case is not supported yet.");
>  			break;
> +		case OPTION_SUBCOMMAND:
> +			if (!opts->value || !opts->subcommand_fn)
> +				optbug(opts, "OPTION_SUBCOMMAND needs a value and a subcommand function");
> +			if (!subcommand_value)
> +				subcommand_value = opts->value;
> +			else if (subcommand_value != opts->value)
> +				optbug(opts, "all OPTION_SUBCOMMANDs need the same value");
> +			break;
>  		default:
>  			; /* ok. (usually accepts an argument) */
>  		}

This addition looks good...

> @@ -499,6 +523,14 @@ static void parse_options_check(const struct option *opts)
>  	BUG_if_bug("invalid 'struct option'");
>  }
>  
> +static int has_subcommands(const struct option *options)
> +{
> +	for (; options->type != OPTION_END; options++)
> +		if (options->type == OPTION_SUBCOMMAND)
> +			return 1;
> +	return 0;
> +}

...but why not...

>  static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
>  				  int argc, const char **argv, const char *prefix,
>  				  const struct option *options,
> @@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
>  	ctx->prefix = prefix;
>  	ctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);
>  	ctx->flags = flags;
> +	ctx->has_subcommands = has_subcommands(options);
> +	if (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL))
> +		BUG("Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands");
> +	if (ctx->has_subcommands) {
> +		if (flags & PARSE_OPT_STOP_AT_NON_OPTION)
> +			BUG("subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION");
> +		if (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {
> +			if (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)
> +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
> +			if (flags & PARSE_OPT_KEEP_DASHDASH)
> +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
> +		}
> +	}

...move this into parse_options_check()? I.e. we'd need to loop over the
list once, but it seems like this should belong there.

We have an existing bug in-tree due to usage_with_options() not doing a
parse_options_check() (I have a local fix...), checking this sort of
thing there instead of in parse_options_start() is therefore the right
thing to do, i.e. we shoudl have a one-stop "does this options variable
look sane?".

> +				error(_("unknown subcommand: %s"), arg);

s/%s/'%s'/ while we're at it, perhaps?

> +				usage_with_options(usagestr, options);
> +			case PARSE_OPT_COMPLETE:
> +			case PARSE_OPT_HELP:
> +			case PARSE_OPT_ERROR:
> +			case PARSE_OPT_DONE:
> +			case PARSE_OPT_NON_OPTION:
> +				BUG("parse_subcommand() cannot return these");

nit: BUG("got bad %d", v) or whatever, i.e. say what we got?

> @@ -206,6 +217,11 @@ struct option {
>  #define OPT_ALIAS(s, l, source_long_name) \
>  	{ OPTION_ALIAS, (s), (l), (source_long_name) }
>  
> +#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
> +					NULL, 0, NULL, 0, NULL, 0, (fn) }
> +#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
> +					NULL, (f), NULL, 0, NULL, 0, (fn) }

Nit, I know you're carrying forward existing patterns, but since that
all pre-dated designated init perhaps we could just (untested):
	
	#define OPT_SUBCOMMAND_F(l, v, fn, f) { \
		.type = OPTION_SUBCOMMAND, \
		.long_name = (l), \
		.value = (v), \
		.ll_callback = (fn), \
	}
	#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)

Which IMO is much nicer. I have some patches somewhere to convert these
to saner patterns (I think not designated init, but the X() can be
defined in terms of X_F() like that, but since this is new we can use
designated init all the way...

> +{
> +	int i;

Nit: missing \n (usual style of variable decl);

> +		error("'cmd' is mandatory");
> +		usage_with_options(usage, test_flag_options);

nit: I think you want usage_msg_optf() or usage_msg_opt().

> +test_expect_success 'subcommand - no subcommand shows error and usage' '
> +	test_expect_code 129 test-tool parse-subcommand cmd 2>err &&
> +	grep "^error: need a subcommand" err &&
> +	grep ^usage: err
> +'
> +
> +test_expect_success 'subcommand - subcommand after -- shows error and usage' '
> +	test_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&
> +	grep "^error: need a subcommand" err &&
> +	grep ^usage: err
> +'
> +
> +test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '
> +	test_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&
> +	grep "^error: need a subcommand" err &&
> +	grep ^usage: err
> +'
> +
> +test_expect_success 'subcommand - unknown subcommand shows error and usage' '
> +	test_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&
> +	grep "^error: unknown subcommand: nope" err &&
> +	grep ^usage: err
> +'
> +
> +test_expect_success 'subcommand - subcommands cannot be abbreviated' '
> +	test_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&
> +	grep "^error: unknown subcommand: subcmd-o$" err &&
> +	grep ^usage: err
> +'
> +
> +test_expect_success 'subcommand - no negated subcommands' '
> +	test_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&
> +	grep "^error: unknown subcommand: no-subcmd-one" err &&
> +	grep ^usage: err
> +'

Creating a trivial helper for this seems worthile, then something like:

	that_helper "expected error here" -- arg u ments to test-tool parse-subcommand



> +test_expect_success 'subcommand - simple' '
> +	test-tool parse-subcommand cmd subcmd-two >actual &&
> +	cat >expect <<-\EOF &&
> +	opt: 0
> +	fn: subcmd_two
> +	arg 00: subcmd-two
> +	EOF
> +	test_cmp expect actual
> +'

Ditto, perhaps? I.e.

	new_hepler arg u ments <<-\EOF
        expected
        goes here
       	EOF

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 00/20] parse-options: handle subcommands
  2022-07-25 13:15 ` [PATCH 00/20] parse-options: handle subcommands Derrick Stolee
@ 2022-07-25 16:00   ` SZEDER Gábor
  2022-07-25 16:08     ` Derrick Stolee
  0 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 16:00 UTC (permalink / raw)
  To: Derrick Stolee; +Cc: git, Ævar Arnfjörð Bjarmason

On Mon, Jul 25, 2022 at 09:15:45AM -0400, Derrick Stolee wrote:
> I focused on reading the changes to the builtins I have experience with
> (commit-graph, maintenance, multi-pack-index, sparse-checkout, worktree)
> and found the adaptation to the new model very clean.
> 
> The one common thing I saw was that you are updating a function pointer
> that you name "fn" but it could be more informative on first reading if
> it was named something like "subcommand_fn".

I felt that redundant, because most lines mentioning that 'fn'
have something clearly subcommand-specific next to it, i.e. the type
'parse_opt_subcommand_fn' at its declaration, or the OPT_SUBCOMMAND
macro.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 00/20] parse-options: handle subcommands
  2022-07-25 16:00   ` SZEDER Gábor
@ 2022-07-25 16:08     ` Derrick Stolee
  0 siblings, 0 replies; 97+ messages in thread
From: Derrick Stolee @ 2022-07-25 16:08 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Ævar Arnfjörð Bjarmason

On 7/25/2022 12:00 PM, SZEDER Gábor wrote:
> On Mon, Jul 25, 2022 at 09:15:45AM -0400, Derrick Stolee wrote:
>> I focused on reading the changes to the builtins I have experience with
>> (commit-graph, maintenance, multi-pack-index, sparse-checkout, worktree)
>> and found the adaptation to the new model very clean.
>>
>> The one common thing I saw was that you are updating a function pointer
>> that you name "fn" but it could be more informative on first reading if
>> it was named something like "subcommand_fn".
> 
> I felt that redundant, because most lines mentioning that 'fn'
> have something clearly subcommand-specific next to it, i.e. the type
> 'parse_opt_subcommand_fn' at its declaration, or the OPT_SUBCOMMAND
> macro.
 
I guess I was just reading the final "return fn(...);" at the end
and thought it looked a bit generic. It's probably not worth
changing.

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 15/20] builtin/notes.c: let parse-options parse subcommands
  2022-07-25 12:38 ` [PATCH 15/20] builtin/notes.c: " SZEDER Gábor
@ 2022-07-25 16:49   ` Junio C Hamano
  0 siblings, 0 replies; 97+ messages in thread
From: Junio C Hamano @ 2022-07-25 16:49 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Ævar Arnfjörð Bjarmason

SZEDER Gábor <szeder.dev@gmail.com> writes:

> 'git notes' parses its subcommands with a long list of if-else if
> statements.  parse-options has just learned to parse subcommands, so
> let's use that facility instead, with the benefits of shorter code,
> handling unknown subcommands, and listing subcommands for Bash
> completion.  Make sure that the default operation mode doesn't accept
> any arguments.
>
> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> ---
>  builtin/notes.c | 43 +++++++++++++++++--------------------------
>  1 file changed, 17 insertions(+), 26 deletions(-)
>
> diff --git a/builtin/notes.c b/builtin/notes.c
> index a3d0d15a22..42cbae4659 100644
> --- a/builtin/notes.c
> +++ b/builtin/notes.c
> @@ -994,17 +994,31 @@ static int get_ref(int argc, const char **argv, const char *prefix)
>  
>  int cmd_notes(int argc, const char **argv, const char *prefix)
>  {
> -	int result;
>  	const char *override_notes_ref = NULL;
> +	parse_opt_subcommand_fn *fn = list;
>  	struct option options[] = {
>  		OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"),
>  			   N_("use notes from <notes-ref>")),
> +		OPT_SUBCOMMAND("list", &fn, list),
> +		OPT_SUBCOMMAND("add", &fn, add),
> +		OPT_SUBCOMMAND("copy", &fn, copy),
> +		OPT_SUBCOMMAND("append", &fn, append_edit),
> +		OPT_SUBCOMMAND("edit", &fn, append_edit),
> +		OPT_SUBCOMMAND("show", &fn, show),
> +		OPT_SUBCOMMAND("merge", &fn, merge),
> +		OPT_SUBCOMMAND("remove", &fn, remove_cmd),
> +		OPT_SUBCOMMAND("prune", &fn, prune),
> +		OPT_SUBCOMMAND("get-ref", &fn, get_ref),
>  		OPT_END()
>  	};

Reading the series backwards from the end, I would expect that the
above to replicate the current behaviour to allow commands to have
both "command wide" options and "per subcommand" options, e.g.

    $ git notes get-ref --ref=amlog
    error: unknown option `ref=amlog`
    usage: git notes get-ref
    $ git notes --ref=amlog get-ref
    refs/notes/amlog

Assuming that is how the new OPT_SUBCOMMAND() interacts with the
dashed options in a single "struct option []", everything I saw
so far in [10-20/20] makes sense.  [17/20] has another instance
of the above, dashed-options and subcommands mixed in an array,
to parse the option for "git remote --verbose <subcmd>" that applies
to all subcommands.

Thanks.



^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 00/20] parse-options: handle subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (20 preceding siblings ...)
  2022-07-25 13:15 ` [PATCH 00/20] parse-options: handle subcommands Derrick Stolee
@ 2022-07-25 17:13 ` Ævar Arnfjörð Bjarmason
  2022-07-25 17:56 ` Junio C Hamano
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
  23 siblings, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25 17:13 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git


On Mon, Jul 25 2022, SZEDER Gábor wrote:

> Several Git commands have subcommands to implement mutually exclusive
> "operation modes", and they usually parse their subcommand argument
> with a bunch of if-else if statements.

On this series in general, quoting from 19/20:

> Note also that this change "hides" the '-h' option in 'git stash push
> -h' from the parse_option() call in cmd_stash(), as it comes after the
> subcommand.  Consequently, from now on it will emit the usage of the
> 'push' subcommand instead of the usage of 'git stash'.  We had a
> failing test for this case, which can now be flipped to expect
> success.

This is good!

>  	/* Assume 'stash push' */
>  	strvec_push(&args, "push");
> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> index 2a4c3fd61c..376cc8f4ab 100755
> --- a/t/t3903-stash.sh
> +++ b/t/t3903-stash.sh
> @@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '
>  	grep -F "or: git stash show" usage
>  '
>  
> -test_expect_failure 'usage for subcommands should emit subcommand usage' '
> +test_expect_success 'usage for subcommands should emit subcommand usage' '
>  	test_expect_code 129 git stash push -h >usage &&
>  	grep -F "usage: git stash [push" usage
>  '

This fixes the TODO test I left in ca7990cea5a (stash: don't show "git
stash push" usage on bad "git stash" usage, 2021-12-16), it probably
makes sense to link to that in the commit message, as it shows exactly
the output you're referring to.

After our recent off-list discussion about this topic I thought it would
make sense to lead with some testing of this behavior, so I had created
this WIP:
https://github.com/git/git/compare/master...avar:git:avar-szeder/add-usage-tests

Some of that is adding tests for the behavior of commands you're
modifying in this series, many of them suffer from the same caveat as
"git stash" did before this fix, and it would be nice to test for that.

But on the other hand maybe we're confident enough with this being a
standard API that it's just tested once, and we don't need to reapeat
for every command that uses it that "git commit-graph write -h" or
whatever returns usage that's a subset of "git commit-graph -h" etc.

I left some nits here & there, and would like to have more time to
review 10/20 in particlar (as well as others).

But this series is a bit winding, in that it's doing at least these N
unrelated things:

 * Fixes to parse-options docs
 * Tests for existing parse options behavior
 * Nits here & there, e.g. 08/20
 * The meaty change in 10/20
 * For 11-20 some of it's one-to-one with existing behavior, and some of
   it's changing behavior (but all in a good way, I think!), per the
   above some of which we don't have tests for.

Personally I'm fine with just bundling this all together, just some
reading notes from going over this...



^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 09/20] parse-options: add support for parsing subcommands
  2022-07-25 12:38 ` [PATCH 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
  2022-07-25 14:43   ` Ævar Arnfjörð Bjarmason
@ 2022-07-25 17:37   ` Junio C Hamano
  1 sibling, 0 replies; 97+ messages in thread
From: Junio C Hamano @ 2022-07-25 17:37 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Ævar Arnfjörð Bjarmason

SZEDER Gábor <szeder.dev@gmail.com> writes:

> The approach is guided by the following observations:
> ...
> So in the end subcommand declaration and parsing would look something
> like this:
>
>     parse_opt_subcommand_fn *fn = NULL;
>     struct option builtin_commit_graph_options[] = {
>         OPT_STRING(0, "object-dir", &opts.obj_dir, N_("dir"),
>                    N_("the object directory to store the graph")),
>         OPT_SUBCOMMAND("verify", &fn, graph_verify),
>         OPT_SUBCOMMAND("write", &fn, graph_write),
>         OPT_END(),
>     };
>     argc = parse_options(argc, argv, prefix, options,
>                          builtin_commit_graph_usage, 0);
>     return fn(argc, argv, prefix);

All makes sense and exactly as I expected to see, after reading the
later steps [10-20/20] that make use of the facility.  Nicely designed.

> Here each OPT_SUBCOMMAND specifies the name of the subcommand and the
> function implementing it, and the same pointer to 'fn' where
> parse_options() will store the function associated with the given
> subcommand.

Puzzling.  Isn't parse_options() an read-only operation with respect
to the elements of the struct option array?  

With s/store/expect to find/, I would understand the above, though.

> There is no need to check whether 'fn' is non-NULL before
> invoking it:

Again, this is puzzling.  "There is no need to"---to whom does this
statement mean to advise?  The implementor of the new codepath IN
parse_options() to handle OPT_SUBCOMMAND()?

> if there were no subcommands given on the command line
> then the parse_options() call would error out and show usage.  

I think that you mean to say

    When one or more OPT_SUBCOMMAND elements exist in an array of
    struct option, parse_options() will error out if none of them
    triggers.

and that makes sense, but I cannot quite tell how it relates to the
previous sentence about fn possibly being NULL.

Ahh, OK.  Let's try to rephrase to see if I can make it easier to
understand.

    Here each OPT_SUBCOMMAND specifies ... with the given
    subcommand.

    After parse_options() returns, the variable 'fn' would have been
    updated to point at the function to call, if one of the
    subcommand specified by the OPT_SUBCOMMAND() is given on the
    command line.

    - When the PARSE_OPT_SUBCOMMAND_OPTIONAL flag is given to
      parse_options(), and no subcommand is given on the command
      line, the variable 'fn' is left intact.  This can be used to
      implement a command with the "default" subcommand by
      initializing the variable 'fn' to the default value.

    - Otherwise, parse_options() will error out and show usage, when
      no subcommand is given.

    - If more than one subcommands are given from the command line,
      parse_options() will stop at the first one, and treat the
      remainder of the command line as arguments to the subcommand.
    
> In case
> a command has a default operation mode, 'fn' should be initialized to
> the function implementing that mode, and parse_options() should be
> invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.

OK.

> Some thoughts about the implementation:
>
>   - Arguably it is a bit weird that the same pointer to 'fn' have to
>     be specified as 'value' for each OPT_SUBCOMMAND, but we need a way
>     to tell parse_options() where to put the function associated with
>     the given subcommand, and I didn't like the alternatives:

I do not find this so disturbing.  This is similar to CMDMODE in
spirit in that only one has to be chosen.  CMDMODE needs to go an
extra mile to ensure that by checking the current value in the
variable pointed by the pointer because the parser does not stop
immediately after getting one, but SUBCOMMAND can afford to be
simpler because the parser immediately stops once a subcommand is
found.

In a sense, OPT_SUBCOMMAND() does not have to exist as the first-class
mechanism.  With just two primitives:

 - a flag that says "after seeing this option, immediately stop
   parsing".
 - something similar to OPT_SET_INT() but works with a function pointer.

we can go 80% of the way to implement OPT_SUBCOMMAND() as a thin
wrapper (there is "one of the OPT_SET_FUNC() must be triggered" that
needs new code specific to the need, which is the other 20%).

>   - I decided against automatically calling the subcommand function
>     from within parse_options(), because:
>
>       - There are commands that have to perform additional actions
>         after option parsing but before calling the function
>         implementing the specified subcommand.
>
>       - The return code of the subcommand is usually the return code
>         of the git command, but preserving the return code of the
>         automatically called subcommand function would have made the
>         API awkward.

Yes, the above makes perfect sense.  Also I suspect that we would
find some cases where being able to use two or more variables become
handy.

> +			case PARSE_OPT_COMPLETE:
> +			case PARSE_OPT_HELP:
> +			case PARSE_OPT_ERROR:
> +			case PARSE_OPT_DONE:
> +			case PARSE_OPT_NON_OPTION:
> +				BUG("parse_subcommand() cannot return these");
> +			}

"these" without giving the violating value?  A "%d" would be cheap
addition, but I see this was copied from elsewhere.

Thanks.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 00/20] parse-options: handle subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (21 preceding siblings ...)
  2022-07-25 17:13 ` Ævar Arnfjörð Bjarmason
@ 2022-07-25 17:56 ` Junio C Hamano
  2022-07-26 15:42   ` Johannes Schindelin
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
  23 siblings, 1 reply; 97+ messages in thread
From: Junio C Hamano @ 2022-07-25 17:56 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Johannes Schindelin

SZEDER Gábor <szeder.dev@gmail.com> writes:

>   - builtin/bisect.c: after the conversion/rename from 'bisect--helper',
>     cmd_bisect() doesn't use parse-options anymore.  Take what's on 'seen'
>     to resolve the conflict.
>     Note that the conflicting topic should have marked cmd_bisect() with
>     the NO_PARSEOPT flag in 'git.c's command list.

I was wondering about this one.  Does the new "subcommand" support
help implementing the dispatching to subcommands better?  If so it
may not be a bad idea to redo the cmd_bisect() on top of this series
once it proves to be solid.

So far, I like everything I saw in this series.  Nicely done.

Thanks.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 09/20] parse-options: add support for parsing subcommands
  2022-07-25 14:43   ` Ævar Arnfjörð Bjarmason
@ 2022-07-25 19:29     ` SZEDER Gábor
  2022-07-25 19:41       ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 19:29 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Mon, Jul 25, 2022 at 04:43:30PM +0200, Ævar Arnfjörð Bjarmason wrote:
> > diff --git a/builtin/blame.c b/builtin/blame.c
> > index 02e39420b6..a9fe8cf7a6 100644
> > --- a/builtin/blame.c
> > +++ b/builtin/blame.c
> > @@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
> >  			break;
> >  		case PARSE_OPT_HELP:
> >  		case PARSE_OPT_ERROR:
> > +		case PARSE_OPT_SUBCOMMAND:
> >  			exit(129);
> >  		case PARSE_OPT_COMPLETE:
> >  			exit(0);
> > diff --git a/builtin/shortlog.c b/builtin/shortlog.c
> > index 086dfee45a..7a1e1fe7c0 100644
> > --- a/builtin/shortlog.c
> > +++ b/builtin/shortlog.c
> > @@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
> >  			break;
> >  		case PARSE_OPT_HELP:
> >  		case PARSE_OPT_ERROR:
> > +		case PARSE_OPT_SUBCOMMAND:
> >  			exit(129);
> >  		case PARSE_OPT_COMPLETE:
> >  			exit(0);
> 
> This feels a bit like carrying forward an API wart, i.e. shouldn't we
> instead BUG() if we are returning a PARSE_OPT_SUBCOMMAND from
> parse_options_step() for options lists that don't have
> PARSE_OPT_SUBCOMMAND in them?
> 
> I.e. is this even reachable, or just something to suppress the compiler
> complaining about missing enum labels?

I think it's as good as unreachable, because neither of these two
commands have subcommands.  However, without these hunks the compiler
invoked with '-Wswitch' (implied by '-Wall') does indeed complain.
 
> ...but why not...
> 
> >  static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
> >  				  int argc, const char **argv, const char *prefix,
> >  				  const struct option *options,
> > @@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
> >  	ctx->prefix = prefix;
> >  	ctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);
> >  	ctx->flags = flags;
> > +	ctx->has_subcommands = has_subcommands(options);
> > +	if (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL))
> > +		BUG("Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands");
> > +	if (ctx->has_subcommands) {
> > +		if (flags & PARSE_OPT_STOP_AT_NON_OPTION)
> > +			BUG("subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION");
> > +		if (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {
> > +			if (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)
> > +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
> > +			if (flags & PARSE_OPT_KEEP_DASHDASH)
> > +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
> > +		}
> > +	}
> 
> ...move this into parse_options_check()? I.e. we'd need to loop over the
> list once, but it seems like this should belong there.
> 
> We have an existing bug in-tree due to usage_with_options() not doing a
> parse_options_check() (I have a local fix...), checking this sort of
> thing there instead of in parse_options_start() is therefore the right
> thing to do, i.e. we shoudl have a one-stop "does this options variable
> look sane?".

The checks added in this hunk (and the existing checks in the hunk's
after-context) are not about the elements of the 'struct option'
array, like the checks in parse_options_check(), but rather about the
sensibility of parse_options()'s 'parse_opt_flags' parameter.
usage_with_options() doesn't have (and doesn't at all need) such a
parameter.

> > +				error(_("unknown subcommand: %s"), arg);
> 
> s/%s/'%s'/ while we're at it, perhaps?

Indeed, though it should be `%s', because that's what surrounds
unknown switches and options in the corresponding error messages.

> > +				usage_with_options(usagestr, options);
> > +			case PARSE_OPT_COMPLETE:
> > +			case PARSE_OPT_HELP:
> > +			case PARSE_OPT_ERROR:
> > +			case PARSE_OPT_DONE:
> > +			case PARSE_OPT_NON_OPTION:
> > +				BUG("parse_subcommand() cannot return these");
> 
> nit: BUG("got bad %d", v) or whatever, i.e. say what we got?

All these are impossible, so I don't think it matters.  This is
another case of the compiler with '-Wswitch' complaining, and follows
suit of similar switch statements after the calls to parse_short_opt()
and parse_long_opt() functions.

> > @@ -206,6 +217,11 @@ struct option {
> >  #define OPT_ALIAS(s, l, source_long_name) \
> >  	{ OPTION_ALIAS, (s), (l), (source_long_name) }
> >  
> > +#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
> > +					NULL, 0, NULL, 0, NULL, 0, (fn) }
> > +#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
> > +					NULL, (f), NULL, 0, NULL, 0, (fn) }
> 
> Nit, I know you're carrying forward existing patterns, but since that
> all pre-dated designated init perhaps we could just (untested):
> 	
> 	#define OPT_SUBCOMMAND_F(l, v, fn, f) { \
> 		.type = OPTION_SUBCOMMAND, \
> 		.long_name = (l), \
> 		.value = (v), \
> 		.ll_callback = (fn), \
> 	}
> 	#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)
> 
> Which IMO is much nicer. I have some patches somewhere to convert these
> to saner patterns (I think not designated init, but the X() can be
> defined in terms of X_F() like that, but since this is new we can use
> designated init all the way...

Oh, I love this idea!  But are we there yet?  I remember the weather
balloon about designated initializers, but I'm not sure whether we've
already made the decision to allow them.  If we do, then I'm inclined
to volunteer to clean up all those OPT_* macros in 'parse-options.h'
with designated initializers, 

> > +{
> > +	int i;
> 
> Nit: missing \n (usual style of variable decl);

Hm, speaking of newer C features, what about 'for (int i = 0; ...)'?

> > +		error("'cmd' is mandatory");
> > +		usage_with_options(usage, test_flag_options);
> 
> nit: I think you want usage_msg_optf() or usage_msg_opt().

Maybe... but I don't know what they do ;)  Though I remember removing
a couple of similar error() and usage_with_options() pairs from the
builtin commands.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 09/20] parse-options: add support for parsing subcommands
  2022-07-25 19:29     ` SZEDER Gábor
@ 2022-07-25 19:41       ` Ævar Arnfjörð Bjarmason
  2022-07-25 21:02         ` SZEDER Gábor
  2022-08-12 15:15         ` SZEDER Gábor
  0 siblings, 2 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25 19:41 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git


On Mon, Jul 25 2022, SZEDER Gábor wrote:

> On Mon, Jul 25, 2022 at 04:43:30PM +0200, Ævar Arnfjörð Bjarmason wrote:
>> > diff --git a/builtin/blame.c b/builtin/blame.c
>> > index 02e39420b6..a9fe8cf7a6 100644
>> > --- a/builtin/blame.c
>> > +++ b/builtin/blame.c
>> > @@ -920,6 +920,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
>> >  			break;
>> >  		case PARSE_OPT_HELP:
>> >  		case PARSE_OPT_ERROR:
>> > +		case PARSE_OPT_SUBCOMMAND:
>> >  			exit(129);
>> >  		case PARSE_OPT_COMPLETE:
>> >  			exit(0);
>> > diff --git a/builtin/shortlog.c b/builtin/shortlog.c
>> > index 086dfee45a..7a1e1fe7c0 100644
>> > --- a/builtin/shortlog.c
>> > +++ b/builtin/shortlog.c
>> > @@ -381,6 +381,7 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
>> >  			break;
>> >  		case PARSE_OPT_HELP:
>> >  		case PARSE_OPT_ERROR:
>> > +		case PARSE_OPT_SUBCOMMAND:
>> >  			exit(129);
>> >  		case PARSE_OPT_COMPLETE:
>> >  			exit(0);
>> 
>> This feels a bit like carrying forward an API wart, i.e. shouldn't we
>> instead BUG() if we are returning a PARSE_OPT_SUBCOMMAND from
>> parse_options_step() for options lists that don't have
>> PARSE_OPT_SUBCOMMAND in them?
>> 
>> I.e. is this even reachable, or just something to suppress the compiler
>> complaining about missing enum labels?
>
> I think it's as good as unreachable, because neither of these two
> commands have subcommands.  However, without these hunks the compiler
> invoked with '-Wswitch' (implied by '-Wall') does indeed complain.

Yeah, we should add a case, but let's just do:

	case PARSE_OPT_SUBCOMMAND:
		BUG("unreachable");

>> ...but why not...
>> 
>> >  static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
>> >  				  int argc, const char **argv, const char *prefix,
>> >  				  const struct option *options,
>> > @@ -515,6 +547,19 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
>> >  	ctx->prefix = prefix;
>> >  	ctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);
>> >  	ctx->flags = flags;
>> > +	ctx->has_subcommands = has_subcommands(options);
>> > +	if (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL))
>> > +		BUG("Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands");
>> > +	if (ctx->has_subcommands) {
>> > +		if (flags & PARSE_OPT_STOP_AT_NON_OPTION)
>> > +			BUG("subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION");
>> > +		if (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {
>> > +			if (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)
>> > +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
>> > +			if (flags & PARSE_OPT_KEEP_DASHDASH)
>> > +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
>> > +		}
>> > +	}
>> 
>> ...move this into parse_options_check()? I.e. we'd need to loop over the
>> list once, but it seems like this should belong there.
>> 
>> We have an existing bug in-tree due to usage_with_options() not doing a
>> parse_options_check() (I have a local fix...), checking this sort of
>> thing there instead of in parse_options_start() is therefore the right
>> thing to do, i.e. we shoudl have a one-stop "does this options variable
>> look sane?".
>
> The checks added in this hunk (and the existing checks in the hunk's
> after-context) are not about the elements of the 'struct option'
> array, like the checks in parse_options_check(), but rather about the
> sensibility of parse_options()'s 'parse_opt_flags' parameter.
> usage_with_options() doesn't have (and doesn't at all need) such a
> parameter.

Ah, sorry, I was just confused. FWIW it's because I split out *that*
part into another helper a while ago:
https://github.com/avar/git/commit/55dda82a409

Which might be worthhile doing/stealing heere while we're at it,
i.e. the flags checking has become quite ab ig part of
parse_options_start_1(), or just leave it for later...

>> > +				error(_("unknown subcommand: %s"), arg);
>> 
>> s/%s/'%s'/ while we're at it, perhaps?
>
> Indeed, though it should be `%s', because that's what surrounds
> unknown switches and options in the corresponding error messages.
>
>> > +				usage_with_options(usagestr, options);
>> > +			case PARSE_OPT_COMPLETE:
>> > +			case PARSE_OPT_HELP:
>> > +			case PARSE_OPT_ERROR:
>> > +			case PARSE_OPT_DONE:
>> > +			case PARSE_OPT_NON_OPTION:
>> > +				BUG("parse_subcommand() cannot return these");
>> 
>> nit: BUG("got bad %d", v) or whatever, i.e. say what we got?
>
> All these are impossible, so I don't think it matters.  This is
> another case of the compiler with '-Wswitch' complaining, and follows
> suit of similar switch statements after the calls to parse_short_opt()
> and parse_long_opt() functions.

*nod*, and this one's a BUG(), which is good...

>> > @@ -206,6 +217,11 @@ struct option {
>> >  #define OPT_ALIAS(s, l, source_long_name) \
>> >  	{ OPTION_ALIAS, (s), (l), (source_long_name) }
>> >  
>> > +#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
>> > +					NULL, 0, NULL, 0, NULL, 0, (fn) }
>> > +#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
>> > +					NULL, (f), NULL, 0, NULL, 0, (fn) }
>> 
>> Nit, I know you're carrying forward existing patterns, but since that
>> all pre-dated designated init perhaps we could just (untested):
>> 	
>> 	#define OPT_SUBCOMMAND_F(l, v, fn, f) { \
>> 		.type = OPTION_SUBCOMMAND, \
>> 		.long_name = (l), \
>> 		.value = (v), \
>> 		.ll_callback = (fn), \
>> 	}
>> 	#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)
>> 
>> Which IMO is much nicer. I have some patches somewhere to convert these
>> to saner patterns (I think not designated init, but the X() can be
>> defined in terms of X_F() like that, but since this is new we can use
>> designated init all the way...
>
> Oh, I love this idea!  But are we there yet?  I remember the weather
> balloon about designated initializers, but I'm not sure whether we've
> already made the decision to allow them.

Yes, we've got a thoroughly hard dependency on that part of C99 for a
while now, and it's OK to add new ones (especially in cases like these,
where it makes thigs easier to read).

> If we do, then I'm inclined
> to volunteer to clean up all those OPT_* macros in 'parse-options.h'
> with designated initializers, 

Sounds good, you might want to steal this & perhaps some things on the
same branch: https://github.com/avar/git/commit/a1a5e9c68c8

I didn't convert them to designated init, but some macros are "missing",
some are needlessy copy/pasted when X_F() could be defined in terms of
X() etc.

FWIW I thought it would eventually make sense to rename the members of
the struct itself, so we'd e.g. just have a "t" and "l" name, so we
could use that inline instead of the OPT_*() (we could use the long
names too, but that would probably be too verbose).

That would allow adding optional arguments, which e.g. would be handy
for things like "...and here's a list of what options this is
incompatible with".

>
>> > +{
>> > +	int i;
>> 
>> Nit: missing \n (usual style of variable decl);
>
> Hm, speaking of newer C features, what about 'for (int i = 0; ...)'?

Junio says this November, but see:
https://lore.kernel.org/git/220725.86zggxpfed.gmgdl@evledraar.gmail.com/

>> > +		error("'cmd' is mandatory");
>> > +		usage_with_options(usage, test_flag_options);
>> 
>> nit: I think you want usage_msg_optf() or usage_msg_opt().
>
> Maybe... but I don't know what they do ;)  Though I remember removing
> a couple of similar error() and usage_with_options() pairs from the
> builtin commands.

It's just helpers for "usage_with_options, except with a message, e.g.:

	$ ./git cat-file a b c
	fatal: only two arguments allowed in <type> <object> mode, not 3

	usage: git cat-file <type> <object>


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 09/20] parse-options: add support for parsing subcommands
  2022-07-25 19:41       ` Ævar Arnfjörð Bjarmason
@ 2022-07-25 21:02         ` SZEDER Gábor
  2022-08-12 15:15         ` SZEDER Gábor
  1 sibling, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-25 21:02 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Mon, Jul 25, 2022 at 09:41:52PM +0200, Ævar Arnfjörð Bjarmason wrote:
> >> > +		error("'cmd' is mandatory");
> >> > +		usage_with_options(usage, test_flag_options);
> >> 
> >> nit: I think you want usage_msg_optf() or usage_msg_opt().
> >
> > Maybe... but I don't know what they do ;)  Though I remember removing
> > a couple of similar error() and usage_with_options() pairs from the
> > builtin commands.
> 
> It's just helpers for "usage_with_options, except with a message, e.g.:
> 
> 	$ ./git cat-file a b c
> 	fatal: only two arguments allowed in <type> <object> mode, not 3
> 
> 	usage: git cat-file <type> <object>

I've looked them up in the meantime.  The problem is that they both
output the given message with a "fatal:" prefix, but option parsing
related errors are usually prefixed with "error:".


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 00/20] parse-options: handle subcommands
  2022-07-25 17:56 ` Junio C Hamano
@ 2022-07-26 15:42   ` Johannes Schindelin
  2022-07-26 18:02     ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 97+ messages in thread
From: Johannes Schindelin @ 2022-07-26 15:42 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: SZEDER Gábor, git

[-- Attachment #1: Type: text/plain, Size: 1321 bytes --]

Hi Junio,

On Mon, 25 Jul 2022, Junio C Hamano wrote:

> SZEDER Gábor <szeder.dev@gmail.com> writes:
>
> >   - builtin/bisect.c: after the conversion/rename from 'bisect--helper',
> >     cmd_bisect() doesn't use parse-options anymore.  Take what's on 'seen'
> >     to resolve the conflict.
> >     Note that the conflicting topic should have marked cmd_bisect() with
> >     the NO_PARSEOPT flag in 'git.c's command list.
>
> I was wondering about this one.  Does the new "subcommand" support
> help implementing the dispatching to subcommands better?  If so it
> may not be a bad idea to redo the cmd_bisect() on top of this series
> once it proves to be solid.

The built-in `bisect` code carries around some local state, in a somewhat
futile attempt to keep things in a state that would be more amenable to
libifying.

The `subcommand` series does not accommodate for such a local state, the
signature `typedef int parse_opt_subcommand_fn(int argc, const char
**argv, const char *prefix);` requires all pre-subcommand options to be
parsed into global (or file-local, but not function-local) variables.

This implies that moving `cmd_bisect()` on top of the `subcommand` topic
would require the `bisect` code to become less libifyable, which is an
undesirable direction.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 00/20] parse-options: handle subcommands
  2022-07-26 15:42   ` Johannes Schindelin
@ 2022-07-26 18:02     ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-26 18:02 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Junio C Hamano, SZEDER Gábor, git


On Tue, Jul 26 2022, Johannes Schindelin wrote:

> Hi Junio,
>
> On Mon, 25 Jul 2022, Junio C Hamano wrote:
>
>> SZEDER Gábor <szeder.dev@gmail.com> writes:
>>
>> >   - builtin/bisect.c: after the conversion/rename from 'bisect--helper',
>> >     cmd_bisect() doesn't use parse-options anymore.  Take what's on 'seen'
>> >     to resolve the conflict.
>> >     Note that the conflicting topic should have marked cmd_bisect() with
>> >     the NO_PARSEOPT flag in 'git.c's command list.
>>
>> I was wondering about this one.  Does the new "subcommand" support
>> help implementing the dispatching to subcommands better?  If so it
>> may not be a bad idea to redo the cmd_bisect() on top of this series
>> once it proves to be solid.
>
> The built-in `bisect` code carries around some local state, in a somewhat
> futile attempt to keep things in a state that would be more amenable to
> libifying.
>
> The `subcommand` series does not accommodate for such a local state, the
> signature `typedef int parse_opt_subcommand_fn(int argc, const char
> **argv, const char *prefix);` requires all pre-subcommand options to be
> parsed into global (or file-local, but not function-local) variables.
>
> This implies that moving `cmd_bisect()` on top of the `subcommand` topic
> would require the `bisect` code to become less libifyable, which is an
> undesirable direction.

What are you referring to here specifically?

The commands in the builtin/bisect.c take a "struct bisect_terms", but
as far as I can tell we could simply move setting that up into the
sub-command callbacks.

So cmd_bisect() only needs to parse_options() enough to figure out that
the first argument is e.g. "start", then call bisect_start(), which will
do its own parse_options() & setup of the rest.

But I could see how we'd in general have a need to carry state from the
cmd_name() to the cmd_name_subcmd(). Such a thing doesn't seem like a
big change to SZEDER's patches here, we'd support functions that take a
void *, similar to how we support two types of "callback" in the "struct
option" itself.

Or, you could have perfectly lib-ified code where your
cmd_name_subcmd(int argc, char **argv, char *prefix) would be a one-line
wrapper for another function taking an extra argument.

You'd use a global only to ferry state between the cmd_name() and that
cmd_name_subcmd(), which would be some boilerplate, but it wouldn't be
prohibitive.

When used as a library the API would probably want to take a struct, and
not an argc/argv/prefix. I don't see how having just that part of the
command callback handling needing one global would close the door on
anything else...

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-07-25 12:38 ` [PATCH 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
  2022-07-25 14:31   ` Ævar Arnfjörð Bjarmason
@ 2022-07-26 19:55   ` SZEDER Gábor
  1 sibling, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-07-26 19:55 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason

On Mon, Jul 25, 2022 at 02:38:38PM +0200, SZEDER Gábor wrote:
> Our Bash completion script can complete --options for commands using
> parse-options even when that command doesn't have a dedicated
> completion function, but to do so the completion script must know
> which commands use parse-options and which don't.  Therefore, commands
> not using parse-options are marked in 'git.c's command list with the
> NO_PARSEOPT flag.
> 
> Update this list, and remove this flag from the commands that by now
> use parse-options.
> 
> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> ---
>  git.c | 12 ++++++------
>  1 file changed, 6 insertions(+), 6 deletions(-)
> 
> diff --git a/git.c b/git.c
> index e5d62fa5a9..c4282f194a 100644
> --- a/git.c
> +++ b/git.c

> @@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {
>  	{ "verify-tag", cmd_verify_tag, RUN_SETUP },
>  	{ "version", cmd_version },
>  	{ "whatchanged", cmd_whatchanged, RUN_SETUP },
> -	{ "worktree", cmd_worktree, RUN_SETUP | NO_PARSEOPT },
> +	{ "worktree", cmd_worktree, RUN_SETUP },

This hunk is wrong, 'worktree' does not use parse_options().  I was
fooled by seeing its (empty) 'struct option' array, thinking that then
it must surely use parse_options(), but it only uses that options
array to pass it to usage_with_options().

This NO_PARSEOPT flag should be removed in the last patch of the
series, when I convert cmd_worktree() to handle its subcommands with
parse_options().

>  	{ "write-tree", cmd_write_tree, RUN_SETUP },
>  };
>  
> -- 
> 2.37.1.633.g6a0fa73e39
> 

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-07-25 14:31   ` Ævar Arnfjörð Bjarmason
@ 2022-08-02 17:37     ` SZEDER Gábor
  2022-08-02 21:00       ` Junio C Hamano
  0 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-02 17:37 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Mon, Jul 25, 2022 at 04:31:40PM +0200, Ævar Arnfjörð Bjarmason wrote:
> 
> On Mon, Jul 25 2022, SZEDER Gábor wrote:
> 
> > Our Bash completion script can complete --options for commands using
> > parse-options even when that command doesn't have a dedicated
> > completion function, but to do so the completion script must know
> > which commands use parse-options and which don't.  Therefore, commands
> > not using parse-options are marked in 'git.c's command list with the
> > NO_PARSEOPT flag.
> >
> > Update this list, and remove this flag from the commands that by now
> > use parse-options.
> >
> > Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> > ---
> >  git.c | 12 ++++++------
> >  1 file changed, 6 insertions(+), 6 deletions(-)
> >
> > diff --git a/git.c b/git.c
> > index e5d62fa5a9..c4282f194a 100644
> > --- a/git.c
> > +++ b/git.c
> > @@ -489,14 +489,14 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)
> >  static struct cmd_struct commands[] = {
> >  	{ "add", cmd_add, RUN_SETUP | NEED_WORK_TREE },
> >  	{ "am", cmd_am, RUN_SETUP | NEED_WORK_TREE },
> > -	{ "annotate", cmd_annotate, RUN_SETUP | NO_PARSEOPT },
> > +	{ "annotate", cmd_annotate, RUN_SETUP },
> >  	{ "apply", cmd_apply, RUN_SETUP_GENTLY },
> >  	{ "archive", cmd_archive, RUN_SETUP_GENTLY },
> >  	{ "bisect--helper", cmd_bisect__helper, RUN_SETUP },
> >  	{ "blame", cmd_blame, RUN_SETUP },
> >  	{ "branch", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },
> >  	{ "bugreport", cmd_bugreport, RUN_SETUP_GENTLY },
> > -	{ "bundle", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },
> > +	{ "bundle", cmd_bundle, RUN_SETUP_GENTLY },
> >  	{ "cat-file", cmd_cat_file, RUN_SETUP },
> >  	{ "check-attr", cmd_check_attr, RUN_SETUP },
> >  	{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
> > @@ -514,7 +514,7 @@ static struct cmd_struct commands[] = {
> >  	{ "column", cmd_column, RUN_SETUP_GENTLY },
> >  	{ "commit", cmd_commit, RUN_SETUP | NEED_WORK_TREE },
> >  	{ "commit-graph", cmd_commit_graph, RUN_SETUP },
> > -	{ "commit-tree", cmd_commit_tree, RUN_SETUP | NO_PARSEOPT },
> > +	{ "commit-tree", cmd_commit_tree, RUN_SETUP },
> >  	{ "config", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },
> >  	{ "count-objects", cmd_count_objects, RUN_SETUP },
> >  	{ "credential", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },
> > @@ -553,7 +553,7 @@ static struct cmd_struct commands[] = {
> >  	{ "ls-files", cmd_ls_files, RUN_SETUP },
> >  	{ "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
> >  	{ "ls-tree", cmd_ls_tree, RUN_SETUP },
> > -	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },
> > +	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY },
> >  	{ "mailsplit", cmd_mailsplit, NO_PARSEOPT },
> >  	{ "maintenance", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },
> >  	{ "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE },
> > @@ -566,7 +566,7 @@ static struct cmd_struct commands[] = {
> >  	{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
> >  	{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
> >  	{ "merge-tree", cmd_merge_tree, RUN_SETUP },
> > -	{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
> > +	{ "mktag", cmd_mktag, RUN_SETUP },
> >  	{ "mktree", cmd_mktree, RUN_SETUP },
> >  	{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },
> >  	{ "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE },
> > @@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {
> >  	{ "verify-tag", cmd_verify_tag, RUN_SETUP },
> >  	{ "version", cmd_version },
> >  	{ "whatchanged", cmd_whatchanged, RUN_SETUP },
> > -	{ "worktree", cmd_worktree, RUN_SETUP | NO_PARSEOPT },
> > +	{ "worktree", cmd_worktree, RUN_SETUP },
> >  	{ "write-tree", cmd_write_tree, RUN_SETUP },
> >  };
> 
> I have a superset of this change in my local tree:
> https://github.com/avar/git/commit/f0076de17fd
> 
> I.e. "parseopt" here really means "handles --git-completion-helper"

Indeed, with additional subtleties, like it doesn't try to read from
stdin before handling '--git-completion-helper'.

> so
> we can also address the diff family here. See the test that's associated
> with the change (which you might want to grab in any case, i.e. to have
> tests in this area).

Dunno.  I don't like this NO_PARSEOPT thing, and instead of testing it
I'm thinking about removing it altogether.  It seems to be doable, but
of course conflicts with the upcoming 'completion-subcommand' patch
series, so I'll leave it for later.

  https://github.com/szeder/git/commits/completion-poc-builtins


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-08-02 17:37     ` SZEDER Gábor
@ 2022-08-02 21:00       ` Junio C Hamano
  2022-08-03 13:11         ` Ævar Arnfjörð Bjarmason
  2022-08-03 21:34         ` SZEDER Gábor
  0 siblings, 2 replies; 97+ messages in thread
From: Junio C Hamano @ 2022-08-02 21:00 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: Ævar Arnfjörð Bjarmason, git

SZEDER Gábor <szeder.dev@gmail.com> writes:

> Dunno.  I don't like this NO_PARSEOPT thing, and instead of testing it
> I'm thinking about removing it altogether.

Sorry if this is obvious to the others, but I am confused here.

Lack of NO_PARSEOPT bit is used as a mark to say "this subcommand
takes '--git-completion-helper' option to help listing the options,
so include it in the 'git --list-cmds=parseopt' output", right?  I
do not mind removing an unused or unnecessary bit at all, but what
is your plan of getting rid of the bit?  Will we make sure everybody
supports the "--git-completion-helper" option?


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-08-02 21:00       ` Junio C Hamano
@ 2022-08-03 13:11         ` Ævar Arnfjörð Bjarmason
  2022-08-03 21:34         ` SZEDER Gábor
  1 sibling, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-03 13:11 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: SZEDER Gábor, git


On Tue, Aug 02 2022, Junio C Hamano wrote:

> SZEDER Gábor <szeder.dev@gmail.com> writes:
>
>> Dunno.  I don't like this NO_PARSEOPT thing, and instead of testing it
>> I'm thinking about removing it altogether.
>
> Sorry if this is obvious to the others, but I am confused here.
>
> Lack of NO_PARSEOPT bit is used as a mark to say "this subcommand
> takes '--git-completion-helper' option to help listing the options,
> so include it in the 'git --list-cmds=parseopt' output", right?  I
> do not mind removing an unused or unnecessary bit at all, but what
> is your plan of getting rid of the bit?  Will we make sure everybody
> supports the "--git-completion-helper" option?

We could:

 * Handle it in git-completion.bash itself, e.g. send-email supports
   --git-completion-helper but isn't a built-in, the git-completion.bash
   itself knows it can be invoked with it.

 * We could just try to run the command with --git-comletion-helper, and
   fall back if it returns 128 or 129. AFAICT the only ones we'd need to
   handle specially would be rev-list (returns 0) and fast-import (will
   hang, expecting stdin input).

The non-parse_options() supporting ones are rare enough these days (and
we've been ~quickly converting them) that perhaps an on-the-fly check
would be fine (and we cache the result, as with most/all other such
checks in git-completion.bsah).

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-08-02 21:00       ` Junio C Hamano
  2022-08-03 13:11         ` Ævar Arnfjörð Bjarmason
@ 2022-08-03 21:34         ` SZEDER Gábor
  2022-08-04  7:47           ` Ævar Arnfjörð Bjarmason
  2022-08-11 21:35           ` Junio C Hamano
  1 sibling, 2 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-03 21:34 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Ævar Arnfjörð Bjarmason, git

On Tue, Aug 02, 2022 at 02:00:09PM -0700, Junio C Hamano wrote:
> SZEDER Gábor <szeder.dev@gmail.com> writes:
> 
> > Dunno.  I don't like this NO_PARSEOPT thing, and instead of testing it
> > I'm thinking about removing it altogether.
> 
> Sorry if this is obvious to the others, but I am confused here.
> 
> Lack of NO_PARSEOPT bit is used as a mark to say "this subcommand
> takes '--git-completion-helper' option to help listing the options,
> so include it in the 'git --list-cmds=parseopt' output", right?  I
> do not mind removing an unused or unnecessary bit at all, but what
> is your plan of getting rid of the bit?  Will we make sure everybody
> supports the "--git-completion-helper" option?

A bit of clarification first:  This NO_PARSEOPT flag only matters for
git commands for which we don't have a corresponding _git_cmd()
completion function in our Bash completion script.  If there is such a
function, then either:

  - that function has a hardcoded list of --options, for commands that
    don't use parse-options, or

  - it invokes '__gitcomp_builtin cmd' to complete --options with the
    help of '--git-completion-helper'.  Note that this works even if
    said 'cmd' were marked with the NO_PARSEOPT flag, moreover,
    'cmd' doesn't even have to be a builtin, i.e. 'git send-email
    --git-completion-helper' works as Ævar pointed out.

For the many builtin (mostly plumbing) commands that do use
parse-options but don't have such a _git_cmd() completion function, we
added the generic fallback __git_complete_common() to complete their
options with '__gitcomp_builtin cmd'.  And this is where the
NO_PARSEOPT flag matters, because the completion script only ever
invokes that fallback function for commands that are not marked with
that flag.  See 9f642a7169 (completion: add --option completion for
most builtin commands, 2018-03-24) for more details.

So far so good.

Now, instead of relying on (possibly outdated) NO_PARSEOPT markings
telling us not to use that fallback function, i.e. not invoke certain
commands with '--git-completion-helper', I'm considering whether we
could rely on our git commands behaving sensibly when invoked with
'--git-completion-helper'.  I.e. a git command either supports that
option and lists all its --options, or fails fast because of the
unknown option without printing anything to stdout.  Then perhaps the
completion script doesn't even have to pay attention to this error,
because it will do the right thing as long as the

  options=$(git builtin --git-completion-helper)

command fills $options with the list of --options or leaves it empty.

AFAICT almost all builtins behave sensibly in this sense (I haven't
looked at non-builtin git commands yet), with the exception of:

  - 'git credential' and 'fast-import': these two attempt to read from
    stdin before recognizing the unknown --git-completion-helper
    option and hang.  On one hand, this hang can be easily prevented
    by redirecting stdin from /dev/null (but 'fast-import' leaves
    behind a crash file, which is not nice).  OTOH, I'm inclined to
    consider this behavior as a bug: all commands should fail fast on
    unknown --options.

  - 'git rev-parse --git-completion-helper' prints
    '--git-completion-helper' to stdout.  I forgot about this, but
    this is how this command is supposed to behave.  I guess I'll just
    have to special case this command in the completion script...

Of course we can only do this with git builtin commands, because those
are under our control and we can make sure that they behave sensibly.
We must not pass '--git-completion-helper' to any 'git-foo' command
that might be present in $PATH, because who knows what they might do.

However, I'll definitely need to think more about corner cases, e.g.
hitting 'git cmd --<TAB>' while outside a repository doesn't even
reaches parse_options() if that command requires a repository.  But
let's leave that for later and get this series in shape first.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-08-03 21:34         ` SZEDER Gábor
@ 2022-08-04  7:47           ` Ævar Arnfjörð Bjarmason
  2022-08-11 21:35           ` Junio C Hamano
  1 sibling, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-04  7:47 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: Junio C Hamano, git


On Wed, Aug 03 2022, SZEDER Gábor wrote:

> [...]
> hitting 'git cmd --<TAB>' while outside a repository doesn't even
> reaches parse_options() if that command requires a repository.  But
> let's leave that for later and get this series in shape first.

Hrm, I wonder how that interacts with the git-completion.bash caching in
a bad way, i.e. if we were to emit options in a non-repo, then the user
cd's to a repo and tab-completes...

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-08-03 21:34         ` SZEDER Gábor
  2022-08-04  7:47           ` Ævar Arnfjörð Bjarmason
@ 2022-08-11 21:35           ` Junio C Hamano
  2022-08-12 15:28             ` SZEDER Gábor
  1 sibling, 1 reply; 97+ messages in thread
From: Junio C Hamano @ 2022-08-11 21:35 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: Ævar Arnfjörð Bjarmason, git

SZEDER Gábor <szeder.dev@gmail.com> writes:

[elided here is a good discussion on how to (re)do command line
completion without NO_PARSEOPT markings]

> ...
> However, I'll definitely need to think more about corner cases, e.g.
> hitting 'git cmd --<TAB>' while outside a repository doesn't even
> reaches parse_options() if that command requires a repository.  But
> let's leave that for later and get this series in shape first.

Yes.  I thought this series was mostly ready, except for a minor
nits in the way things are explained in the proposed log message
in [09/20].  How would we want to proceed from here?

Thanks.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-07-25 14:38   ` Ævar Arnfjörð Bjarmason
@ 2022-08-12 15:04     ` SZEDER Gábor
  0 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-12 15:04 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Mon, Jul 25, 2022 at 04:38:48PM +0200, Ævar Arnfjörð Bjarmason wrote:
> 
> On Mon, Jul 25 2022, SZEDER Gábor wrote:
> 
> > In 't0040-parse-options.sh' we thoroughly test the parsing of all
> > types and forms of options, but in all those tests parse_options() is
> > always invoked with a 0 flags parameter.
> >
> > Add a few tests to demonstrate how various 'enum parse_opt_flags'
> > values are supposed to influence option parsing.
> >
> > Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> > ---
> >  t/helper/test-parse-options.c | 61 ++++++++++++++++++++++++++++++
> >  t/helper/test-tool.c          |  1 +
> >  t/helper/test-tool.h          |  1 +
> >  t/t0040-parse-options.sh      | 70 +++++++++++++++++++++++++++++++++++
> >  4 files changed, 133 insertions(+)
> >
> > diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
> > index 48d3cf6692..32b906bd6a 100644
> > --- a/t/helper/test-parse-options.c
> > +++ b/t/helper/test-parse-options.c
> > @@ -192,3 +192,64 @@ int cmd__parse_options(int argc, const char **argv)
> >  
> >  	return ret;
> >  }
> > +
> > +static int parse_options_flags__cmd(int argc, const char **argv,
> > +				    enum parse_opt_flags test_flags)
> > +{
> > +	const char *usage[] = {
> > +		"<...> cmd [options]",
> > +		NULL
> > +	};
> > +	int opt = 0;
> > +	const struct option options[] = {
> > +		OPT_INTEGER('o', "opt", &opt, "an integer option"),
> > +		OPT_END()
> > +	};
> > +
> > +	argc = parse_options(argc, argv, NULL, options, usage, test_flags);
> > +
> > +	printf("opt: %d\n", opt);
> > +	for (int i = 0; i < argc; i++)
> > +		printf("arg %02d: %s\n", i, argv[i]);
> > +
> > +	return 0;
> > +}
> > +
> > +static enum parse_opt_flags test_flags = 0;
> > +static const struct option test_flag_options[] = {
> > +	OPT_GROUP("flag-options:"),
> > +	OPT_BIT(0, "keep-dashdash", &test_flags,
> > +		"pass PARSE_OPT_KEEP_DASHDASH to parse_options()",
> > +		PARSE_OPT_KEEP_DASHDASH),
> > +	OPT_BIT(0, "stop-at-non-option", &test_flags,
> > +		"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()",
> > +		PARSE_OPT_STOP_AT_NON_OPTION),
> > +	OPT_BIT(0, "keep-argv0", &test_flags,
> > +		"pass PARSE_OPT_KEEP_ARGV0 to parse_options()",
> > +		PARSE_OPT_KEEP_ARGV0),
> > +	OPT_BIT(0, "keep-unknown", &test_flags,
> > +		"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()",
> > +		PARSE_OPT_KEEP_UNKNOWN),
> > +	OPT_BIT(0, "no-internal-help", &test_flags,
> > +		"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()",
> > +		PARSE_OPT_NO_INTERNAL_HELP),
> > +	OPT_END()
> > +};
> > +
> > +int cmd__parse_options_flags(int argc, const char **argv)
> > +{
> > +	const char *usage[] = {
> > +		"test-tool parse-options-flags [flag-options] cmd [options]",
> > +		NULL
> > +	};
> > +
> > +	argc = parse_options(argc, argv, NULL, test_flag_options, usage,
> > +			     PARSE_OPT_STOP_AT_NON_OPTION);
> > +
> > +	if (argc == 0 || strcmp(argv[0], "cmd")) {
> > +		error("'cmd' is mandatory");
> > +		usage_with_options(usage, test_flag_options);
> > +	}
> > +
> > +	return parse_options_flags__cmd(argc, argv, test_flags);
> > +}
> > diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
> > index 318fdbab0c..6e62282b60 100644
> > --- a/t/helper/test-tool.c
> > +++ b/t/helper/test-tool.c
> > @@ -51,6 +51,7 @@ static struct test_cmd cmds[] = {
> >  	{ "online-cpus", cmd__online_cpus },
> >  	{ "pack-mtimes", cmd__pack_mtimes },
> >  	{ "parse-options", cmd__parse_options },
> > +	{ "parse-options-flags", cmd__parse_options_flags },
> >  	{ "parse-pathspec-file", cmd__parse_pathspec_file },
> >  	{ "partial-clone", cmd__partial_clone },
> >  	{ "path-utils", cmd__path_utils },
> 
> I wanted to add some new parse_options() code to
> t/helper/test-parse-options.c in the past, but was stymied by its
> cmd_*() going through a singular parse_options().
> 
> So just creating a new callback is a neat solution.
> 
> But wouldn't it be better to just create a new
> t/helper/test-parse-options-something-.c & test file? It seems this
> doesn't really share anything with the current helper & tests...

Well, at least they share the concept, as they all test parse-options.
And 'parse-options-flags' and 'parse-subcommands' do share the options
array to specify the various parse_opt_flags.

A new test script for these flags and/or for subcommands would
definitely be worse, IMO.  I've found it very convenient that whenever
I updated 'parse-options.{c,h}', I only needed to run a single
'./t0040-parse-options.sh' script to check my changes.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 09/20] parse-options: add support for parsing subcommands
  2022-07-25 19:41       ` Ævar Arnfjörð Bjarmason
  2022-07-25 21:02         ` SZEDER Gábor
@ 2022-08-12 15:15         ` SZEDER Gábor
  1 sibling, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-12 15:15 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Mon, Jul 25, 2022 at 09:41:52PM +0200, Ævar Arnfjörð Bjarmason wrote:
> >> > @@ -206,6 +217,11 @@ struct option {
> >> >  #define OPT_ALIAS(s, l, source_long_name) \
> >> >  	{ OPTION_ALIAS, (s), (l), (source_long_name) }
> >> >  
> >> > +#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
> >> > +					NULL, 0, NULL, 0, NULL, 0, (fn) }
> >> > +#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
> >> > +					NULL, (f), NULL, 0, NULL, 0, (fn) }
> >> 
> >> Nit, I know you're carrying forward existing patterns, but since that
> >> all pre-dated designated init perhaps we could just (untested):
> >> 	
> >> 	#define OPT_SUBCOMMAND_F(l, v, fn, f) { \
> >> 		.type = OPTION_SUBCOMMAND, \
> >> 		.long_name = (l), \
> >> 		.value = (v), \
> >> 		.ll_callback = (fn), \
> >> 	}
> >> 	#define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0)
> >> 
> >> Which IMO is much nicer. I have some patches somewhere to convert these
> >> to saner patterns (I think not designated init, but the X() can be
> >> defined in terms of X_F() like that, but since this is new we can use
> >> designated init all the way...
> >
> > Oh, I love this idea!  But are we there yet?  I remember the weather
> > balloon about designated initializers, but I'm not sure whether we've
> > already made the decision to allow them.
> 
> Yes, we've got a thoroughly hard dependency on that part of C99 for a
> while now, and it's OK to add new ones (especially in cases like these,
> where it makes thigs easier to read).

Good.  I updated this hunk to use designated initializers as you
suggested, because all those unused 0/NULL fields in there are just...
ugly.

> > If we do, then I'm inclined
> > to volunteer to clean up all those OPT_* macros in 'parse-options.h'
> > with designated initializers,

But I'll leave this for later, because it's awfully easy to make a
mistake and assign a macro parameter to the wrong field, and I find
the resulting diff very hard to review.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-08-11 21:35           ` Junio C Hamano
@ 2022-08-12 15:28             ` SZEDER Gábor
  2022-08-12 16:46               ` Junio C Hamano
  0 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-12 15:28 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Ævar Arnfjörð Bjarmason, git

On Thu, Aug 11, 2022 at 02:35:03PM -0700, Junio C Hamano wrote:
> Yes.  I thought this series was mostly ready, except for a minor
> nits in the way things are explained in the proposed log message
> in [09/20].  How would we want to proceed from here?

Please wait for the reroll.  v2 is getting along, ever so slowly, I
think I'll be able to submit it over the weekend.  Thanks.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 01/20] git.c: update NO_PARSEOPT markings
  2022-08-12 15:28             ` SZEDER Gábor
@ 2022-08-12 16:46               ` Junio C Hamano
  0 siblings, 0 replies; 97+ messages in thread
From: Junio C Hamano @ 2022-08-12 16:46 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: Ævar Arnfjörð Bjarmason, git

SZEDER Gábor <szeder.dev@gmail.com> writes:

> On Thu, Aug 11, 2022 at 02:35:03PM -0700, Junio C Hamano wrote:
>> Yes.  I thought this series was mostly ready, except for a minor
>> nits in the way things are explained in the proposed log message
>> in [09/20].  How would we want to proceed from here?
>
> Please wait for the reroll.  v2 is getting along, ever so slowly, I
> think I'll be able to submit it over the weekend.  Thanks.

OK.  The original round looked mostly good to me overall and I was
wondering what the current status was.  Will wait.  No rush.

Thanks.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* [PATCH v2 00/20] parse-options: handle subcommands
  2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
                   ` (22 preceding siblings ...)
  2022-07-25 17:56 ` Junio C Hamano
@ 2022-08-19 16:03 ` SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
                     ` (20 more replies)
  23 siblings, 21 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

Several Git commands have subcommands to implement mutually exclusive
"operation modes", and they usually parse their subcommand argument
with a bunch of if-else if statements.

Teach parse-options to handle subcommands as well, which will result
in shorter and simpler code with consistent error handling and error
messages on unknown or missing subcommand, and it will also make
possible for our Bash completion script to handle subcommands
programmatically in a follow-up series.

Changes since v1:

  - Commit message updates.
  - Put the troublesome subcommand arg between `' in error messages.
  - Use designated initializers for OPT_SUBCOMMAND.
  - Remove the unused 'kept_unknown' flag from 'struct parse_opt_ctx_t'
    (it was used in WIP version of this patch series, that I didn't manage
    to remove completely).
  - Drop cmd_worktree()'s NO_PARSEOPT flag in the right patch.
  - Use 'git -C ...' instead of ( cd ... && ... ) in t5505's new tests.
  - Update API doc and BUG() string to mention PARSE_OPT_KEEP_UNKNOWN_OPT.
  - Minor test helper refactorings.


SZEDER Gábor (20):
  git.c: update NO_PARSEOPT markings
  t3301-notes.sh: check that default operation mode doesn't take
    arguments
  t5505-remote.sh: check the behavior without a subcommand
  t0040-parse-options: test parse_options() with various
    'parse_opt_flags'
  api-parse-options.txt: fix description of OPT_CMDMODE
  parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options
  parse-options: clarify the limitations of PARSE_OPT_NODASH
  parse-options: drop leading space from '--git-completion-helper'
    output
  parse-options: add support for parsing subcommands
  builtin/bundle.c: let parse-options parse subcommands
  builtin/commit-graph.c: let parse-options parse subcommands
  builtin/gc.c: let parse-options parse 'git maintenance's subcommands
  builtin/hook.c: let parse-options parse subcommands
  builtin/multi-pack-index.c: let parse-options parse subcommands
  builtin/notes.c: let parse-options parse subcommands
  builtin/reflog.c: let parse-options parse subcommands
  builtin/remote.c: let parse-options parse subcommands
  builtin/sparse-checkout.c: let parse-options parse subcommands
  builtin/stash.c: let parse-options parse subcommands
  builtin/worktree.c: let parse-options parse subcommands

 Documentation/technical/api-parse-options.txt |  49 +++-
 builtin/archive.c                             |   2 +-
 builtin/bisect--helper.c                      |   2 +-
 builtin/blame.c                               |   1 +
 builtin/bundle.c                              |  25 +-
 builtin/commit-graph.c                        |  30 +--
 builtin/difftool.c                            |   2 +-
 builtin/env--helper.c                         |   2 +-
 builtin/fast-export.c                         |   2 +-
 builtin/gc.c                                  |  42 +--
 builtin/hook.c                                |  12 +-
 builtin/log.c                                 |   4 +-
 builtin/multi-pack-index.c                    |  51 ++--
 builtin/notes.c                               |  43 ++-
 builtin/reflog.c                              |  43 +--
 builtin/remote.c                              |  70 +++--
 builtin/revert.c                              |   2 +-
 builtin/shortlog.c                            |   1 +
 builtin/sparse-checkout.c                     |  48 ++--
 builtin/stash.c                               |  59 ++--
 builtin/worktree.c                            |  31 +--
 diff.c                                        |   2 +-
 git.c                                         |  14 +-
 parse-options.c                               | 118 +++++++-
 parse-options.h                               |  29 +-
 t/helper/test-parse-options.c                 | 127 +++++++++
 t/helper/test-serve-v2.c                      |   2 +-
 t/helper/test-tool.c                          |   2 +
 t/helper/test-tool.h                          |   2 +
 t/t0040-parse-options.sh                      | 255 ++++++++++++++++++
 t/t3301-notes.sh                              |   5 +
 t/t3903-stash.sh                              |   2 +-
 t/t5318-commit-graph.sh                       |   4 +-
 t/t5505-remote.sh                             |  20 ++
 t/t7900-maintenance.sh                        |  10 +-
 35 files changed, 803 insertions(+), 310 deletions(-)

Range-diff:
 1:  78448310f9 !  1:  8128131f7b git.c: update NO_PARSEOPT markings
    @@ Commit message
         Update this list, and remove this flag from the commands that by now
         use parse-options.
     
    +    After this change we can TAB complete --options of the plumbing
    +    commands 'commit-tree', 'mailinfo' and 'mktag'.
    +
         Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
     
      ## git.c ##
    @@ git.c: static struct cmd_struct commands[] = {
      	{ "mktree", cmd_mktree, RUN_SETUP },
      	{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },
      	{ "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE },
    -@@ git.c: static struct cmd_struct commands[] = {
    - 	{ "verify-tag", cmd_verify_tag, RUN_SETUP },
    - 	{ "version", cmd_version },
    - 	{ "whatchanged", cmd_whatchanged, RUN_SETUP },
    --	{ "worktree", cmd_worktree, RUN_SETUP | NO_PARSEOPT },
    -+	{ "worktree", cmd_worktree, RUN_SETUP },
    - 	{ "write-tree", cmd_write_tree, RUN_SETUP },
    - };
    - 
 2:  dee73c9832 =  2:  a8895169e7 t3301-notes.sh: check that default operation mode doesn't take arguments
 3:  4ce0c7717b !  3:  ecc7ca97e7 t5505-remote.sh: check the behavior without a subcommand
    @@ t/t5505-remote.sh: test_expect_success 'add invalid foreign_vcs remote' '
      '
      
     +test_expect_success 'without subcommand' '
    -+	(
    -+		cd test &&
    -+		git remote >actual &&
    -+		echo origin >expect &&
    -+		test_cmp expect actual
    -+	)
    ++	echo origin >expect &&
    ++	git -C test remote >actual &&
    ++	test_cmp expect actual
     +'
     +
     +test_expect_success 'without subcommand accepts -v' '
    -+	cat >test/expect <<-EOF &&
    ++	cat >expect <<-EOF &&
     +	origin	$(pwd)/one (fetch)
     +	origin	$(pwd)/one (push)
     +	EOF
    -+	(
    -+		cd test &&
    -+		git remote -v >actual &&
    -+		test_cmp expect actual
    -+	)
    ++	git -C test remote -v >actual &&
    ++	test_cmp expect actual
     +'
     +
     +test_expect_success 'without subcommand does not take arguments' '
    -+	(
    -+		cd test &&
    -+		test_expect_code 129 git remote origin 2>err &&
    -+		grep "^error: Unknown subcommand:" err
    -+	)
    ++	test_expect_code 129 git -C test remote origin 2>err &&
    ++	grep "^error: Unknown subcommand:" err
     +'
     +
      cat >test/expect <<EOF
 4:  7e7bf30b32 !  4:  84c1aa3318 t0040-parse-options: test parse_options() with various 'parse_opt_flags'
    @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **arg
      	return ret;
      }
     +
    ++static void print_args(int argc, const char **argv)
    ++{
    ++	for (int i = 0; i < argc; i++)
    ++		printf("arg %02d: %s\n", i, argv[i]);
    ++}
    ++
     +static int parse_options_flags__cmd(int argc, const char **argv,
     +				    enum parse_opt_flags test_flags)
     +{
    @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **arg
     +	argc = parse_options(argc, argv, NULL, options, usage, test_flags);
     +
     +	printf("opt: %d\n", opt);
    -+	for (int i = 0; i < argc; i++)
    -+		printf("arg %02d: %s\n", i, argv[i]);
    ++	print_args(argc, argv);
     +
     +	return 0;
     +}
    @@ t/t0040-parse-options.sh: test_expect_success '--end-of-options treats remainder
     +test_expect_success 'NO_INTERNAL_HELP works for -h' '
     +	test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&
     +	cat err &&
    -+	grep "^error: unknown switch \`h'\''" err &&
    ++	grep "^error: unknown switch \`h$SQ" err &&
     +	grep "^usage: " err
     +'
     +
 5:  8515609ac2 =  5:  40b51c9992 api-parse-options.txt: fix description of OPT_CMDMODE
 6:  60885e91cf =  6:  7f561992a5 parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options
 7:  2e1d40b4f0 =  7:  27a66b0fcd parse-options: clarify the limitations of PARSE_OPT_NODASH
 8:  2e4f9259ba =  8:  722a47874a parse-options: drop leading space from '--git-completion-helper' output
 9:  caf0a395df !  9:  b65385a02d parse-options: add support for parsing subcommands
    @@ Commit message
             return fn(argc, argv, prefix);
     
         Here each OPT_SUBCOMMAND specifies the name of the subcommand and the
    -    function implementing it, and the same pointer to 'fn' where
    -    parse_options() will store the function associated with the given
    -    subcommand.  There is no need to check whether 'fn' is non-NULL before
    -    invoking it: if there were no subcommands given on the command line
    -    then the parse_options() call would error out and show usage.  In case
    -    a command has a default operation mode, 'fn' should be initialized to
    -    the function implementing that mode, and parse_options() should be
    -    invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.
    +    function implementing it, and the address of the same 'fn' subcommand
    +    function pointer.  parse_options() then processes the arguments until
    +    it finds the first argument matching one of the subcommands, sets 'fn'
    +    to the function associated with that subcommand, and returns, leaving
    +    the rest of the arguments unprocessed.  If none of the listed
    +    subcommands is found among the arguments, parse_options() will show
    +    usage and abort.
    +
    +    If a command has a default operation mode, 'fn' should be initialized
    +    to the function implementing that mode, and parse_options() should be
    +    invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.  In this case
    +    parse_options() won't error out when not finding any subcommands, but
    +    will return leaving 'fn' unchanged.  Note that if that default
    +    operation mode has any --options, then the PARSE_OPT_KEEP_UNKNOWN_OPT
    +    flag is necessary as well (otherwise parse_options() would error out
    +    upon seeing the unknown option meant to the default operation mode).
     
         Some thoughts about the implementation:
     
    -      - Arguably it is a bit weird that the same pointer to 'fn' have to
    -        be specified as 'value' for each OPT_SUBCOMMAND, but we need a way
    -        to tell parse_options() where to put the function associated with
    -        the given subcommand, and I didn't like the alternatives:
    +      - The same pointer to 'fn' must be specified as 'value' for each
    +        OPT_SUBCOMMAND, because there can be only one set of mutually
    +        exclusive subcommands; parse_options() will BUG() otherwise.
    +
    +        There are other ways to tell parse_options() where to put the
    +        function associated with the subcommand given on the command line,
    +        but I didn't like them:
     
               - Change parse_options()'s signature by adding a pointer to
                 subcommand function to be set to the function associated with
    @@ Documentation/technical/api-parse-options.txt: Flags are the bitwise-or of:
     +	Don't error out when no subcommand is specified.
     +
     +Note that `PARSE_OPT_STOP_AT_NON_OPTION` is incompatible with subcommands;
    -+while `PARSE_OPT_KEEP_DASHDASH` and `PARSE_OPT_KEEP_UNKNOWN` can only be
    ++while `PARSE_OPT_KEEP_DASHDASH` and `PARSE_OPT_KEEP_UNKNOWN_OPT` can only be
     +used with subcommands when combined with `PARSE_OPT_SUBCOMMAND_OPTIONAL`.
     +
      Data Structure
    @@ parse-options.c: static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
     +			BUG("subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION");
     +		if (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {
     +			if (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)
    -+				BUG("subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
    ++				BUG("subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN_OPT unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
     +			if (flags & PARSE_OPT_KEEP_DASHDASH)
     +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
     +		}
    @@ parse-options.c: enum parse_opt_result parse_options_step(struct parse_opt_ctx_t
     +					 * So we are done parsing.
     +					 */
     +					return PARSE_OPT_DONE;
    -+				error(_("unknown subcommand: %s"), arg);
    ++				error(_("unknown subcommand: `%s'"), arg);
     +				usage_with_options(usagestr, options);
     +			case PARSE_OPT_COMPLETE:
     +			case PARSE_OPT_HELP:
     +			case PARSE_OPT_ERROR:
     +			case PARSE_OPT_DONE:
     +			case PARSE_OPT_NON_OPTION:
    ++				/* Impossible. */
     +				BUG("parse_subcommand() cannot return these");
     +			}
      		}
    @@ parse-options.c: enum parse_opt_result parse_options_step(struct parse_opt_ctx_t
     +		}
      		if (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT))
      			return PARSE_OPT_UNKNOWN;
    -+		ctx->kept_unknown = 1;
      		ctx->out[ctx->cpidx++] = ctx->argv[0];
    - 		ctx->opt = NULL;
    - 	}
     @@ parse-options.c: int parse_options(int argc, const char **argv,
      	case PARSE_OPT_COMPLETE:
      		exit(0);
    @@ parse-options.h: struct option {
      #define OPT_ALIAS(s, l, source_long_name) \
      	{ OPTION_ALIAS, (s), (l), (source_long_name) }
      
    -+#define OPT_SUBCOMMAND(l, v, fn)      { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
    -+					NULL, 0, NULL, 0, NULL, 0, (fn) }
    -+#define OPT_SUBCOMMAND_F(l, v, fn, f) { OPTION_SUBCOMMAND, 0, (l), (v), NULL, \
    -+					NULL, (f), NULL, 0, NULL, 0, (fn) }
    ++#define OPT_SUBCOMMAND_F(l, v, fn, f) { \
    ++	.type = OPTION_SUBCOMMAND, \
    ++	.long_name = (l), \
    ++	.value = (v), \
    ++	.flags = (f), \
    ++	.subcommand_fn = (fn) }
    ++#define OPT_SUBCOMMAND(l, v, fn)    OPT_SUBCOMMAND_F((l), (v), (fn), 0)
     +
      /*
       * parse_options() will filter out the processed options and leave the
    @@ parse-options.h: struct parse_opt_ctx_t {
      	int argc, cpidx, total;
      	const char *opt;
      	enum parse_opt_flags flags;
    -+	unsigned has_subcommands:1,
    -+		 kept_unknown:1;
    ++	unsigned has_subcommands;
      	const char *prefix;
      	const char **alias_groups; /* must be in groups of 3 elements! */
      	struct option *updated_options;
    @@ t/helper/test-parse-options.c: int cmd__parse_options_flags(int argc, const char
      	return parse_options_flags__cmd(argc, argv, test_flags);
      }
     +
    -+static void print_subcommand_args(const char *fn_name, int argc,
    -+				  const char **argv)
    -+{
    -+	int i;
    -+	printf("fn: %s\n", fn_name);
    -+	for (i = 0; i < argc; i++)
    -+		printf("arg %02d: %s\n", i, argv[i]);
    -+}
    -+
     +static int subcmd_one(int argc, const char **argv, const char *prefix)
     +{
    -+	print_subcommand_args("subcmd_one", argc, argv);
    ++	printf("fn: subcmd_one\n");
    ++	print_args(argc, argv);
     +	return 0;
     +}
     +
     +static int subcmd_two(int argc, const char **argv, const char *prefix)
     +{
    -+	print_subcommand_args("subcmd_two", argc, argv);
    ++	printf("fn: subcmd_two\n");
    ++	print_args(argc, argv);
     +	return 0;
     +}
     +
    @@ t/t0040-parse-options.sh: test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HE
     +
     +test_expect_success 'subcommand - unknown subcommand shows error and usage' '
     +	test_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&
    -+	grep "^error: unknown subcommand: nope" err &&
    ++	grep "^error: unknown subcommand: \`nope$SQ" err &&
     +	grep ^usage: err
     +'
     +
     +test_expect_success 'subcommand - subcommands cannot be abbreviated' '
     +	test_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&
    -+	grep "^error: unknown subcommand: subcmd-o$" err &&
    ++	grep "^error: unknown subcommand: \`subcmd-o$SQ$" err &&
     +	grep ^usage: err
     +'
     +
     +test_expect_success 'subcommand - no negated subcommands' '
     +	test_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&
    -+	grep "^error: unknown subcommand: no-subcmd-one" err &&
    ++	grep "^error: unknown subcommand: \`no-subcmd-one$SQ" err &&
     +	grep ^usage: err
     +'
     +
10:  cac17fa063 = 10:  3526a93d90 builtin/bundle.c: let parse-options parse subcommands
11:  9cfd5875fb ! 11:  a9533e14a5 builtin/commit-graph.c: let parse-options parse subcommands
    @@ t/t5318-commit-graph.sh: test_expect_success 'usage' '
      test_expect_success 'usage shown with an error on unknown sub-command' '
      	cat >expect <<-\EOF &&
     -	error: unrecognized subcommand: unknown
    -+	error: unknown subcommand: unknown
    ++	error: unknown subcommand: `unknown'\''
      	EOF
      	test_expect_code 129 git commit-graph unknown 2>stderr &&
      	grep error stderr >actual &&
12:  cab2adf79b ! 12:  501a2bd1b5 builtin/gc.c: let parse-options parse 'git maintenance's subcommands
    @@ t/t7900-maintenance.sh: test_systemd_analyze_verify () {
     +	test_expect_code 129 git maintenance -h >actual &&
     +	test_i18ngrep "usage: git maintenance <subcommand>" actual &&
     +	test_expect_code 129 git maintenance barf 2>err &&
    -+	test_i18ngrep "unknown subcommand: barf" err &&
    ++	test_i18ngrep "unknown subcommand: \`barf'\''" err &&
     +	test_i18ngrep "usage: git maintenance" err &&
      	test_expect_code 129 git maintenance 2>err &&
     +	test_i18ngrep "error: need a subcommand" err &&
13:  6a16515cd8 ! 13:  a38a61756b builtin/hook.c: let parse-option parse subcommands
    @@ Metadata
     Author: SZEDER Gábor <szeder.dev@gmail.com>
     
      ## Commit message ##
    -    builtin/hook.c: let parse-option parse subcommands
    +    builtin/hook.c: let parse-options parse subcommands
     
         'git hook' parses its currently only subcommand with an if statement.
         parse-options has just learned to parse subcommands, so let's use that
14:  a5a7f28ced = 14:  f511717008 builtin/multi-pack-index.c: let parse-options parse subcommands
15:  0104f48ae1 = 15:  220bbf16cf builtin/notes.c: let parse-options parse subcommands
16:  250e12ddfc = 16:  d0e7329f42 builtin/reflog.c: let parse-options parse subcommands
17:  90a2968469 ! 17:  9af71fd655 builtin/remote.c: let parse-options parse subcommands
    @@ builtin/remote.c: static int set_url(int argc, const char **argv)
      }
     
      ## t/t5505-remote.sh ##
    -@@ t/t5505-remote.sh: test_expect_success 'without subcommand does not take arguments' '
    - 	(
    - 		cd test &&
    - 		test_expect_code 129 git remote origin 2>err &&
    --		grep "^error: Unknown subcommand:" err
    -+		grep "^error: unknown subcommand:" err
    - 	)
    +@@ t/t5505-remote.sh: test_expect_success 'without subcommand accepts -v' '
    + 
    + test_expect_success 'without subcommand does not take arguments' '
    + 	test_expect_code 129 git -C test remote origin 2>err &&
    +-	grep "^error: Unknown subcommand:" err
    ++	grep "^error: unknown subcommand:" err
      '
      
    + cat >test/expect <<EOF
18:  dc5c071b51 = 18:  2bd2d40bb3 builtin/sparse-checkout.c: let parse-options parse subcommands
19:  5c7cd8cbed = 19:  e8b7fdd2d8 builtin/stash.c: let parse-options parse subcommands
20:  4d884dfe62 ! 20:  6d8108292f builtin/worktree.c: let parse-options parse subcommands
    @@ builtin/worktree.c: static int repair(int ac, const char **av, const char *prefi
     +	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
     +	return fn(ac, av, prefix);
      }
    +
    + ## git.c ##
    +@@ git.c: static struct cmd_struct commands[] = {
    + 	{ "verify-tag", cmd_verify_tag, RUN_SETUP },
    + 	{ "version", cmd_version },
    + 	{ "whatchanged", cmd_whatchanged, RUN_SETUP },
    +-	{ "worktree", cmd_worktree, RUN_SETUP | NO_PARSEOPT },
    ++	{ "worktree", cmd_worktree, RUN_SETUP },
    + 	{ "write-tree", cmd_write_tree, RUN_SETUP },
    + };
    + 
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply	[flat|nested] 97+ messages in thread

* [PATCH v2 01/20] git.c: update NO_PARSEOPT markings
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
@ 2022-08-19 16:03   ` SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments SZEDER Gábor
                     ` (19 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

Our Bash completion script can complete --options for commands using
parse-options even when that command doesn't have a dedicated
completion function, but to do so the completion script must know
which commands use parse-options and which don't.  Therefore, commands
not using parse-options are marked in 'git.c's command list with the
NO_PARSEOPT flag.

Update this list, and remove this flag from the commands that by now
use parse-options.

After this change we can TAB complete --options of the plumbing
commands 'commit-tree', 'mailinfo' and 'mktag'.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 git.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/git.c b/git.c
index e5d62fa5a9..09c126c33d 100644
--- a/git.c
+++ b/git.c
@@ -489,14 +489,14 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)
 static struct cmd_struct commands[] = {
 	{ "add", cmd_add, RUN_SETUP | NEED_WORK_TREE },
 	{ "am", cmd_am, RUN_SETUP | NEED_WORK_TREE },
-	{ "annotate", cmd_annotate, RUN_SETUP | NO_PARSEOPT },
+	{ "annotate", cmd_annotate, RUN_SETUP },
 	{ "apply", cmd_apply, RUN_SETUP_GENTLY },
 	{ "archive", cmd_archive, RUN_SETUP_GENTLY },
 	{ "bisect--helper", cmd_bisect__helper, RUN_SETUP },
 	{ "blame", cmd_blame, RUN_SETUP },
 	{ "branch", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },
 	{ "bugreport", cmd_bugreport, RUN_SETUP_GENTLY },
-	{ "bundle", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },
+	{ "bundle", cmd_bundle, RUN_SETUP_GENTLY },
 	{ "cat-file", cmd_cat_file, RUN_SETUP },
 	{ "check-attr", cmd_check_attr, RUN_SETUP },
 	{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
@@ -514,7 +514,7 @@ static struct cmd_struct commands[] = {
 	{ "column", cmd_column, RUN_SETUP_GENTLY },
 	{ "commit", cmd_commit, RUN_SETUP | NEED_WORK_TREE },
 	{ "commit-graph", cmd_commit_graph, RUN_SETUP },
-	{ "commit-tree", cmd_commit_tree, RUN_SETUP | NO_PARSEOPT },
+	{ "commit-tree", cmd_commit_tree, RUN_SETUP },
 	{ "config", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },
 	{ "count-objects", cmd_count_objects, RUN_SETUP },
 	{ "credential", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },
@@ -553,7 +553,7 @@ static struct cmd_struct commands[] = {
 	{ "ls-files", cmd_ls_files, RUN_SETUP },
 	{ "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
 	{ "ls-tree", cmd_ls_tree, RUN_SETUP },
-	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },
+	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY },
 	{ "mailsplit", cmd_mailsplit, NO_PARSEOPT },
 	{ "maintenance", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },
 	{ "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE },
@@ -566,7 +566,7 @@ static struct cmd_struct commands[] = {
 	{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-tree", cmd_merge_tree, RUN_SETUP },
-	{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
+	{ "mktag", cmd_mktag, RUN_SETUP },
 	{ "mktree", cmd_mktree, RUN_SETUP },
 	{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },
 	{ "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE },
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
@ 2022-08-19 16:03   ` SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 03/20] t5505-remote.sh: check the behavior without a subcommand SZEDER Gábor
                     ` (18 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git notes' without a subcommand defaults to listing all notes and
doesn't accept any arguments.

We are about to teach parse-options to handle subcommands, and update
'git notes' to make use of that new feature.  So let's add a test to
make sure that the upcoming changes don't inadvertenly change the
behavior in this corner case.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/t3301-notes.sh | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh
index d742be8840..3288aaec7d 100755
--- a/t/t3301-notes.sh
+++ b/t/t3301-notes.sh
@@ -505,6 +505,11 @@ test_expect_success 'list notes with "git notes"' '
 	test_cmp expect actual
 '
 
+test_expect_success '"git notes" without subcommand does not take arguments' '
+	test_expect_code 129 git notes HEAD^^ 2>err &&
+	grep "^error: unknown subcommand" err
+'
+
 test_expect_success 'list specific note with "git notes list <object>"' '
 	git rev-parse refs/notes/commits:$commit_3 >expect &&
 	git notes list HEAD^^ >actual &&
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 03/20] t5505-remote.sh: check the behavior without a subcommand
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments SZEDER Gábor
@ 2022-08-19 16:03   ` SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
                     ` (17 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git remote' without a subcommand defaults to listing all remotes and
doesn't accept any arguments except the '-v|--verbose' option.

We are about to teach parse-options to handle subcommands, and update
'git remote' to make use of that new feature.  So let's add some tests
to make sure that the upcoming changes don't inadvertently change the
behavior in these cases.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/t5505-remote.sh | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 6c7370f87f..a549a21ef6 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -241,6 +241,26 @@ test_expect_success 'add invalid foreign_vcs remote' '
 	test_cmp expect actual
 '
 
+test_expect_success 'without subcommand' '
+	echo origin >expect &&
+	git -C test remote >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'without subcommand accepts -v' '
+	cat >expect <<-EOF &&
+	origin	$(pwd)/one (fetch)
+	origin	$(pwd)/one (push)
+	EOF
+	git -C test remote -v >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'without subcommand does not take arguments' '
+	test_expect_code 129 git -C test remote origin 2>err &&
+	grep "^error: Unknown subcommand:" err
+'
+
 cat >test/expect <<EOF
 * remote origin
   Fetch URL: $(pwd)/one
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (2 preceding siblings ...)
  2022-08-19 16:03   ` [PATCH v2 03/20] t5505-remote.sh: check the behavior without a subcommand SZEDER Gábor
@ 2022-08-19 16:03   ` SZEDER Gábor
  2022-08-19 17:23     ` Ævar Arnfjörð Bjarmason
  2022-08-19 18:18     ` Junio C Hamano
  2022-08-19 16:03   ` [PATCH v2 05/20] api-parse-options.txt: fix description of OPT_CMDMODE SZEDER Gábor
                     ` (16 subsequent siblings)
  20 siblings, 2 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

In 't0040-parse-options.sh' we thoroughly test the parsing of all
types and forms of options, but in all those tests parse_options() is
always invoked with a 0 flags parameter.

Add a few tests to demonstrate how various 'enum parse_opt_flags'
values are supposed to influence option parsing.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/helper/test-parse-options.c | 66 +++++++++++++++++++++++++++++++++
 t/helper/test-tool.c          |  1 +
 t/helper/test-tool.h          |  1 +
 t/t0040-parse-options.sh      | 70 +++++++++++++++++++++++++++++++++++
 4 files changed, 138 insertions(+)

diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
index 48d3cf6692..88919785d3 100644
--- a/t/helper/test-parse-options.c
+++ b/t/helper/test-parse-options.c
@@ -192,3 +192,69 @@ int cmd__parse_options(int argc, const char **argv)
 
 	return ret;
 }
+
+static void print_args(int argc, const char **argv)
+{
+	for (int i = 0; i < argc; i++)
+		printf("arg %02d: %s\n", i, argv[i]);
+}
+
+static int parse_options_flags__cmd(int argc, const char **argv,
+				    enum parse_opt_flags test_flags)
+{
+	const char *usage[] = {
+		"<...> cmd [options]",
+		NULL
+	};
+	int opt = 0;
+	const struct option options[] = {
+		OPT_INTEGER('o', "opt", &opt, "an integer option"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, NULL, options, usage, test_flags);
+
+	printf("opt: %d\n", opt);
+	print_args(argc, argv);
+
+	return 0;
+}
+
+static enum parse_opt_flags test_flags = 0;
+static const struct option test_flag_options[] = {
+	OPT_GROUP("flag-options:"),
+	OPT_BIT(0, "keep-dashdash", &test_flags,
+		"pass PARSE_OPT_KEEP_DASHDASH to parse_options()",
+		PARSE_OPT_KEEP_DASHDASH),
+	OPT_BIT(0, "stop-at-non-option", &test_flags,
+		"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()",
+		PARSE_OPT_STOP_AT_NON_OPTION),
+	OPT_BIT(0, "keep-argv0", &test_flags,
+		"pass PARSE_OPT_KEEP_ARGV0 to parse_options()",
+		PARSE_OPT_KEEP_ARGV0),
+	OPT_BIT(0, "keep-unknown", &test_flags,
+		"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()",
+		PARSE_OPT_KEEP_UNKNOWN),
+	OPT_BIT(0, "no-internal-help", &test_flags,
+		"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()",
+		PARSE_OPT_NO_INTERNAL_HELP),
+	OPT_END()
+};
+
+int cmd__parse_options_flags(int argc, const char **argv)
+{
+	const char *usage[] = {
+		"test-tool parse-options-flags [flag-options] cmd [options]",
+		NULL
+	};
+
+	argc = parse_options(argc, argv, NULL, test_flag_options, usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+
+	if (argc == 0 || strcmp(argv[0], "cmd")) {
+		error("'cmd' is mandatory");
+		usage_with_options(usage, test_flag_options);
+	}
+
+	return parse_options_flags__cmd(argc, argv, test_flags);
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 318fdbab0c..6e62282b60 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -51,6 +51,7 @@ static struct test_cmd cmds[] = {
 	{ "online-cpus", cmd__online_cpus },
 	{ "pack-mtimes", cmd__pack_mtimes },
 	{ "parse-options", cmd__parse_options },
+	{ "parse-options-flags", cmd__parse_options_flags },
 	{ "parse-pathspec-file", cmd__parse_pathspec_file },
 	{ "partial-clone", cmd__partial_clone },
 	{ "path-utils", cmd__path_utils },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index bb79927163..d8e8403d70 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -41,6 +41,7 @@ int cmd__oidtree(int argc, const char **argv);
 int cmd__online_cpus(int argc, const char **argv);
 int cmd__pack_mtimes(int argc, const char **argv);
 int cmd__parse_options(int argc, const char **argv);
+int cmd__parse_options_flags(int argc, const char **argv);
 int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__partial_clone(int argc, const char **argv);
 int cmd__path_utils(int argc, const char **argv);
diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
index ed2fb620a9..8511ce24bb 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh
@@ -456,4 +456,74 @@ test_expect_success '--end-of-options treats remainder as args' '
 	    --end-of-options --verbose
 '
 
+test_expect_success 'KEEP_DASHDASH works' '
+	test-tool parse-options-flags --keep-dashdash cmd --opt=1 -- --opt=2 --unknown >actual &&
+	cat >expect <<-\EOF &&
+	opt: 1
+	arg 00: --
+	arg 01: --opt=2
+	arg 02: --unknown
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'KEEP_ARGV0 works' '
+	test-tool parse-options-flags --keep-argv0 cmd arg0 --opt=3 >actual &&
+	cat >expect <<-\EOF &&
+	opt: 3
+	arg 00: cmd
+	arg 01: arg0
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'STOP_AT_NON_OPTION works' '
+	test-tool parse-options-flags --stop-at-non-option cmd --opt=4 arg0 --opt=5 --unknown >actual &&
+	cat >expect <<-\EOF &&
+	opt: 4
+	arg 00: arg0
+	arg 01: --opt=5
+	arg 02: --unknown
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'KEEP_UNKNOWN works' '
+	test-tool parse-options-flags --keep-unknown cmd --unknown=1 --opt=6 -u2 >actual &&
+	cat >expect <<-\EOF &&
+	opt: 6
+	arg 00: --unknown=1
+	arg 01: -u2
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'NO_INTERNAL_HELP works for -h' '
+	test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&
+	cat err &&
+	grep "^error: unknown switch \`h$SQ" err &&
+	grep "^usage: " err
+'
+
+for help_opt in help help-all
+do
+	test_expect_success "NO_INTERNAL_HELP works for --$help_opt" "
+		test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&
+		cat err &&
+		grep '^error: unknown option \`'$help_opt\' err &&
+		grep '^usage: ' err
+	"
+done
+
+test_expect_success 'KEEP_UNKNOWN | NO_INTERNAL_HELP works' '
+	test-tool parse-options-flags --keep-unknown --no-internal-help cmd -h --help --help-all >actual &&
+	cat >expect <<-\EOF &&
+	opt: 0
+	arg 00: -h
+	arg 01: --help
+	arg 02: --help-all
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 05/20] api-parse-options.txt: fix description of OPT_CMDMODE
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (3 preceding siblings ...)
  2022-08-19 16:03   ` [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
@ 2022-08-19 16:03   ` SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options SZEDER Gábor
                     ` (15 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

The description of the 'OPT_CMDMODE' macro states that "enum_val is
set to int_var when ...", but it's the other way around, 'int_var' is
set to 'enum_val'.  Fix this.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 Documentation/technical/api-parse-options.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt
index acfd5dc1d8..5a04f3daec 100644
--- a/Documentation/technical/api-parse-options.txt
+++ b/Documentation/technical/api-parse-options.txt
@@ -236,7 +236,7 @@ There are some macros to easily define options:
 `OPT_CMDMODE(short, long, &int_var, description, enum_val)`::
 	Define an "operation mode" option, only one of which in the same
 	group of "operating mode" options that share the same `int_var`
-	can be given by the user. `enum_val` is set to `int_var` when the
+	can be given by the user. `int_var` is set to `enum_val` when the
 	option is used, but an error is reported if other "operating mode"
 	option has already set its value to the same `int_var`.
 
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (4 preceding siblings ...)
  2022-08-19 16:03   ` [PATCH v2 05/20] api-parse-options.txt: fix description of OPT_CMDMODE SZEDER Gábor
@ 2022-08-19 16:03   ` SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH SZEDER Gábor
                     ` (14 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

The description of 'PARSE_OPT_KEEP_UNKNOWN' starts with "Keep unknown
arguments instead of erroring out".  This is a bit misleading, as this
flag only applies to unknown --options, while non-option arguments are
kept even without this flag.

Update the description to clarify this, and rename the flag to
PARSE_OPTIONS_KEEP_UNKNOWN_OPT to make this obvious just by looking at
the flag name.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 Documentation/technical/api-parse-options.txt | 6 ++++--
 builtin/archive.c                             | 2 +-
 builtin/bisect--helper.c                      | 2 +-
 builtin/difftool.c                            | 2 +-
 builtin/env--helper.c                         | 2 +-
 builtin/fast-export.c                         | 2 +-
 builtin/log.c                                 | 4 ++--
 builtin/reflog.c                              | 4 ++--
 builtin/revert.c                              | 2 +-
 builtin/sparse-checkout.c                     | 4 ++--
 builtin/stash.c                               | 8 ++++----
 diff.c                                        | 2 +-
 parse-options.c                               | 6 +++---
 parse-options.h                               | 2 +-
 t/helper/test-parse-options.c                 | 6 +++---
 t/helper/test-serve-v2.c                      | 2 +-
 t/t0040-parse-options.sh                      | 8 ++++----
 17 files changed, 33 insertions(+), 31 deletions(-)

diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt
index 5a04f3daec..4412377fa3 100644
--- a/Documentation/technical/api-parse-options.txt
+++ b/Documentation/technical/api-parse-options.txt
@@ -90,8 +90,8 @@ Flags are the bitwise-or of:
 	Keep the first argument, which contains the program name.  It's
 	removed from argv[] by default.
 
-`PARSE_OPT_KEEP_UNKNOWN`::
-	Keep unknown arguments instead of erroring out.  This doesn't
+`PARSE_OPT_KEEP_UNKNOWN_OPT`::
+	Keep unknown options instead of erroring out.  This doesn't
 	work for all combinations of arguments as users might expect
 	it to do.  E.g. if the first argument in `--unknown --known`
 	takes a value (which we can't know), the second one is
@@ -101,6 +101,8 @@ Flags are the bitwise-or of:
 	non-option, not as a value belonging to the unknown option,
 	the parser early.  That's why parse_options() errors out if
 	both options are set.
+	Note that non-option arguments are always kept, even without
+	this flag.
 
 `PARSE_OPT_NO_INTERNAL_HELP`::
 	By default, parse_options() handles `-h`, `--help` and
diff --git a/builtin/archive.c b/builtin/archive.c
index 7176b041b6..f094390ee0 100644
--- a/builtin/archive.c
+++ b/builtin/archive.c
@@ -75,7 +75,7 @@ static int run_remote_archiver(int argc, const char **argv,
 
 #define PARSE_OPT_KEEP_ALL ( PARSE_OPT_KEEP_DASHDASH | 	\
 			     PARSE_OPT_KEEP_ARGV0 | 	\
-			     PARSE_OPT_KEEP_UNKNOWN |	\
+			     PARSE_OPT_KEEP_UNKNOWN_OPT |	\
 			     PARSE_OPT_NO_INTERNAL_HELP	)
 
 int cmd_archive(int argc, const char **argv, const char *prefix)
diff --git a/builtin/bisect--helper.c b/builtin/bisect--helper.c
index 8a052c7111..7097750fc6 100644
--- a/builtin/bisect--helper.c
+++ b/builtin/bisect--helper.c
@@ -1324,7 +1324,7 @@ int cmd_bisect__helper(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options,
 			     git_bisect_helper_usage,
-			     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (!cmdmode)
 		usage_with_options(git_bisect_helper_usage, options);
diff --git a/builtin/difftool.c b/builtin/difftool.c
index b3c509b8de..8706f68492 100644
--- a/builtin/difftool.c
+++ b/builtin/difftool.c
@@ -716,7 +716,7 @@ int cmd_difftool(int argc, const char **argv, const char *prefix)
 	symlinks = has_symlinks;
 
 	argc = parse_options(argc, argv, prefix, builtin_difftool_options,
-			     builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN |
+			     builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
 
 	if (tool_help)
diff --git a/builtin/env--helper.c b/builtin/env--helper.c
index 27349098b0..ea04c16636 100644
--- a/builtin/env--helper.c
+++ b/builtin/env--helper.c
@@ -50,7 +50,7 @@ int cmd_env__helper(int argc, const char **argv, const char *prefix)
 	};
 
 	argc = parse_options(argc, argv, prefix, opts, env__helper_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 	if (env_default && !*env_default)
 		usage_with_options(env__helper_usage, opts);
 	if (!cmdmode)
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index e1748fb98b..bf3c20dea2 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -1221,7 +1221,7 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix)
 	revs.sources = &revision_sources;
 	revs.rewrite_parents = 1;
 	argc = parse_options(argc, argv, prefix, options, fast_export_usage,
-			PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN);
+			PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 	argc = setup_revisions(argc, argv, &revs, NULL);
 	if (argc > 1)
 		usage_with_options (fast_export_usage, options);
diff --git a/builtin/log.c b/builtin/log.c
index 88a5e98875..fb84a0d399 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -199,7 +199,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
 	mailmap = use_mailmap_config;
 	argc = parse_options(argc, argv, prefix,
 			     builtin_log_options, builtin_log_usage,
-			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
 
 	if (quiet)
@@ -1926,7 +1926,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
 	 */
 	argc = parse_options(argc, argv, prefix, builtin_format_patch_options,
 			     builtin_format_patch_usage,
-			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
 
 	/* Make sure "0000-$sub.patch" gives non-negative length for $sub */
diff --git a/builtin/reflog.c b/builtin/reflog.c
index 4dd297dce8..b8b1f4f8ea 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -223,7 +223,7 @@ static int cmd_reflog_show(int argc, const char **argv, const char *prefix)
 
 	parse_options(argc, argv, prefix, options, reflog_show_usage,
 		      PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |
-		      PARSE_OPT_KEEP_UNKNOWN);
+		      PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	return cmd_log_reflog(argc, argv, prefix);
 }
@@ -410,7 +410,7 @@ int cmd_reflog(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options, reflog_usage,
 			     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |
-			     PARSE_OPT_KEEP_UNKNOWN |
+			     PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_NO_INTERNAL_HELP);
 
 	/*
diff --git a/builtin/revert.c b/builtin/revert.c
index 2554f9099c..ee2a0807f0 100644
--- a/builtin/revert.c
+++ b/builtin/revert.c
@@ -141,7 +141,7 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)
 
 	argc = parse_options(argc, argv, NULL, options, usage_str,
 			PARSE_OPT_KEEP_ARGV0 |
-			PARSE_OPT_KEEP_UNKNOWN);
+			PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c
index f91e29b56a..a5e4b95a9d 100644
--- a/builtin/sparse-checkout.c
+++ b/builtin/sparse-checkout.c
@@ -767,7 +767,7 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix,
 			     builtin_sparse_checkout_add_options,
 			     builtin_sparse_checkout_add_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	sanitize_paths(argc, argv, prefix, add_opts.skip_checks);
 
@@ -813,7 +813,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix,
 			     builtin_sparse_checkout_set_options,
 			     builtin_sparse_checkout_set_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (update_modes(&set_opts.cone_mode, &set_opts.sparse_index))
 		return 1;
diff --git a/builtin/stash.c b/builtin/stash.c
index 30fa101460..a14e832e9f 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -782,7 +782,7 @@ static int list_stash(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options,
 			     git_stash_list_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (!ref_exists(ref_stash))
 		return 0;
@@ -873,7 +873,7 @@ static int show_stash(int argc, const char **argv, const char *prefix)
 	init_revisions(&rev, prefix);
 
 	argc = parse_options(argc, argv, prefix, options, git_stash_show_usage,
-			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
 
 	strvec_push(&revision_args, argv[0]);
@@ -979,7 +979,7 @@ static int store_stash(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options,
 			     git_stash_store_usage,
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (argc != 1) {
 		if (!quiet)
@@ -1795,7 +1795,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 	git_config(git_stash_config, NULL);
 
 	argc = parse_options(argc, argv, prefix, options, git_stash_usage,
-			     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);
 
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
diff --git a/diff.c b/diff.c
index 974626a621..dd68281ba4 100644
--- a/diff.c
+++ b/diff.c
@@ -5661,7 +5661,7 @@ int diff_opt_parse(struct diff_options *options,
 
 	ac = parse_options(ac, av, prefix, options->parseopts, NULL,
 			   PARSE_OPT_KEEP_DASHDASH |
-			   PARSE_OPT_KEEP_UNKNOWN |
+			   PARSE_OPT_KEEP_UNKNOWN_OPT |
 			   PARSE_OPT_NO_INTERNAL_HELP |
 			   PARSE_OPT_ONE_SHOT |
 			   PARSE_OPT_STOP_AT_NON_OPTION);
diff --git a/parse-options.c b/parse-options.c
index edf55d3ef5..a0a2cf98fa 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -332,7 +332,7 @@ static enum parse_opt_result parse_long_opt(
 			rest = NULL;
 		if (!rest) {
 			/* abbreviated? */
-			if (!(p->flags & PARSE_OPT_KEEP_UNKNOWN) &&
+			if (!(p->flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&
 			    !strncmp(long_name, arg, arg_end - arg)) {
 is_abbreviated:
 				if (abbrev_option &&
@@ -515,7 +515,7 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx,
 	ctx->prefix = prefix;
 	ctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0);
 	ctx->flags = flags;
-	if ((flags & PARSE_OPT_KEEP_UNKNOWN) &&
+	if ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&
 	    (flags & PARSE_OPT_STOP_AT_NON_OPTION) &&
 	    !(flags & PARSE_OPT_ONE_SHOT))
 		BUG("STOP_AT_NON_OPTION and KEEP_UNKNOWN don't go together");
@@ -839,7 +839,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
 unknown:
 		if (ctx->flags & PARSE_OPT_ONE_SHOT)
 			break;
-		if (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN))
+		if (!(ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT))
 			return PARSE_OPT_UNKNOWN;
 		ctx->out[ctx->cpidx++] = ctx->argv[0];
 		ctx->opt = NULL;
diff --git a/parse-options.h b/parse-options.h
index 685fccac13..591df64191 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -30,7 +30,7 @@ enum parse_opt_flags {
 	PARSE_OPT_KEEP_DASHDASH = 1 << 0,
 	PARSE_OPT_STOP_AT_NON_OPTION = 1 << 1,
 	PARSE_OPT_KEEP_ARGV0 = 1 << 2,
-	PARSE_OPT_KEEP_UNKNOWN = 1 << 3,
+	PARSE_OPT_KEEP_UNKNOWN_OPT = 1 << 3,
 	PARSE_OPT_NO_INTERNAL_HELP = 1 << 4,
 	PARSE_OPT_ONE_SHOT = 1 << 5,
 	PARSE_OPT_SHELL_EVAL = 1 << 6,
diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
index 88919785d3..99ad6fa4f0 100644
--- a/t/helper/test-parse-options.c
+++ b/t/helper/test-parse-options.c
@@ -232,9 +232,9 @@ static const struct option test_flag_options[] = {
 	OPT_BIT(0, "keep-argv0", &test_flags,
 		"pass PARSE_OPT_KEEP_ARGV0 to parse_options()",
 		PARSE_OPT_KEEP_ARGV0),
-	OPT_BIT(0, "keep-unknown", &test_flags,
-		"pass PARSE_OPT_KEEP_UNKNOWN to parse_options()",
-		PARSE_OPT_KEEP_UNKNOWN),
+	OPT_BIT(0, "keep-unknown-opt", &test_flags,
+		"pass PARSE_OPT_KEEP_UNKNOWN_OPT to parse_options()",
+		PARSE_OPT_KEEP_UNKNOWN_OPT),
 	OPT_BIT(0, "no-internal-help", &test_flags,
 		"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()",
 		PARSE_OPT_NO_INTERNAL_HELP),
diff --git a/t/helper/test-serve-v2.c b/t/helper/test-serve-v2.c
index 28e905afc3..824e5c0a95 100644
--- a/t/helper/test-serve-v2.c
+++ b/t/helper/test-serve-v2.c
@@ -24,7 +24,7 @@ int cmd__serve_v2(int argc, const char **argv)
 	/* ignore all unknown cmdline switches for now */
 	argc = parse_options(argc, argv, prefix, options, serve_usage,
 			     PARSE_OPT_KEEP_DASHDASH |
-			     PARSE_OPT_KEEP_UNKNOWN);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
 
 	if (advertise_capabilities)
 		protocol_v2_advertise_capabilities();
diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
index 8511ce24bb..264b737309 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh
@@ -488,8 +488,8 @@ test_expect_success 'STOP_AT_NON_OPTION works' '
 	test_cmp expect actual
 '
 
-test_expect_success 'KEEP_UNKNOWN works' '
-	test-tool parse-options-flags --keep-unknown cmd --unknown=1 --opt=6 -u2 >actual &&
+test_expect_success 'KEEP_UNKNOWN_OPT works' '
+	test-tool parse-options-flags --keep-unknown-opt cmd --unknown=1 --opt=6 -u2 >actual &&
 	cat >expect <<-\EOF &&
 	opt: 6
 	arg 00: --unknown=1
@@ -515,8 +515,8 @@ do
 	"
 done
 
-test_expect_success 'KEEP_UNKNOWN | NO_INTERNAL_HELP works' '
-	test-tool parse-options-flags --keep-unknown --no-internal-help cmd -h --help --help-all >actual &&
+test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '
+	test-tool parse-options-flags --keep-unknown-opt --no-internal-help cmd -h --help --help-all >actual &&
 	cat >expect <<-\EOF &&
 	opt: 0
 	arg 00: -h
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (5 preceding siblings ...)
  2022-08-19 16:03   ` [PATCH v2 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options SZEDER Gábor
@ 2022-08-19 16:03   ` SZEDER Gábor
  2022-08-19 16:03   ` [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output SZEDER Gábor
                     ` (13 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

Update the comment documenting 'struct option' to clarify that
PARSE_OPT_NODASH can only be an argumentless short option; see
51a9949eda (parseopt: add PARSE_OPT_NODASH, 2009-05-07).

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 parse-options.h | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/parse-options.h b/parse-options.h
index 591df64191..8cbfc7e8bf 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -109,7 +109,8 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx,
  *				is last on the command line. If the option is
  *				not last it will require an argument.
  *				Should not be used with PARSE_OPT_OPTARG.
- *   PARSE_OPT_NODASH: this option doesn't start with a dash.
+ *   PARSE_OPT_NODASH: this option doesn't start with a dash; can only be a
+ *		       short option and can't accept arguments.
  *   PARSE_OPT_LITERAL_ARGHELP: says that argh shouldn't be enclosed in brackets
  *				(i.e. '<argh>') in the help message.
  *				Useful for options with multiple parameters.
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (6 preceding siblings ...)
  2022-08-19 16:03   ` [PATCH v2 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH SZEDER Gábor
@ 2022-08-19 16:03   ` SZEDER Gábor
  2022-08-19 17:30     ` Ævar Arnfjörð Bjarmason
  2022-08-19 16:04   ` [PATCH v2 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
                     ` (12 subsequent siblings)
  20 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:03 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

The output of 'git <cmd> --git-completion-helper' always starts with a
space, e.g.:

  $ git config --git-completion-helper
   --global --system --local [...]

This doesn't matter for the completion script, because field splitting
discards that space anyway.

However, later patches in this series will teach parse-options to
handle subcommands, and subcommands will be included in the completion
helper output as well.  This will make the loop printing options (and
subcommands) a tad more complex, so I wanted to test the result.  The
test would have to account for the presence of that leading space,
which bugged my OCD, so let's get rid of it.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 parse-options.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/parse-options.c b/parse-options.c
index a0a2cf98fa..8748f88e6f 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -620,7 +620,8 @@ static int show_gitcomp(const struct option *opts, int show_all)
 			suffix = "=";
 		if (starts_with(opts->long_name, "no-"))
 			nr_noopts++;
-		printf(" --%s%s", opts->long_name, suffix);
+		printf("%s--%s%s", opts == original_opts ? "" : " ",
+		       opts->long_name, suffix);
 	}
 	show_negated_gitcomp(original_opts, show_all, -1);
 	show_negated_gitcomp(original_opts, show_all, nr_noopts);
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 09/20] parse-options: add support for parsing subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (7 preceding siblings ...)
  2022-08-19 16:03   ` [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 17:33     ` Ævar Arnfjörð Bjarmason
  2022-08-19 19:03     ` Ævar Arnfjörð Bjarmason
  2022-08-19 16:04   ` [PATCH v2 10/20] builtin/bundle.c: let parse-options parse subcommands SZEDER Gábor
                     ` (11 subsequent siblings)
  20 siblings, 2 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

Several Git commands have subcommands to implement mutually exclusive
"operation modes", and they usually parse their subcommand argument
with a bunch of if-else if statements.

Teach parse-options to handle subcommands as well, which will result
in shorter and simpler code with consistent error handling and error
messages on unknown or missing subcommand, and it will also make
possible for our Bash completion script to handle subcommands
programmatically.

The approach is guided by the following observations:

  - Most subcommands [1] are implemented in dedicated functions, and
    most of those functions [2] either have a signature matching the
    'int cmd_foo(int argc, const char **argc, const char *prefix)'
    signature of builtin commands or can be trivially converted to
    that signature, because they miss only that last prefix parameter
    or have no parameters at all.

  - Subcommand arguments only have long form, and they have no double
    dash prefix, no negated form, and no description, and they don't
    take any arguments, and can't be abbreviated.

  - There must be exactly one subcommand among the arguments, or zero
    if the command has a default operation mode.

  - All arguments following the subcommand are considered to be
    arguments of the subcommand, and, conversely, arguments meant for
    the subcommand may not preceed the subcommand.

So in the end subcommand declaration and parsing would look something
like this:

    parse_opt_subcommand_fn *fn = NULL;
    struct option builtin_commit_graph_options[] = {
        OPT_STRING(0, "object-dir", &opts.obj_dir, N_("dir"),
                   N_("the object directory to store the graph")),
        OPT_SUBCOMMAND("verify", &fn, graph_verify),
        OPT_SUBCOMMAND("write", &fn, graph_write),
        OPT_END(),
    };
    argc = parse_options(argc, argv, prefix, options,
                         builtin_commit_graph_usage, 0);
    return fn(argc, argv, prefix);

Here each OPT_SUBCOMMAND specifies the name of the subcommand and the
function implementing it, and the address of the same 'fn' subcommand
function pointer.  parse_options() then processes the arguments until
it finds the first argument matching one of the subcommands, sets 'fn'
to the function associated with that subcommand, and returns, leaving
the rest of the arguments unprocessed.  If none of the listed
subcommands is found among the arguments, parse_options() will show
usage and abort.

If a command has a default operation mode, 'fn' should be initialized
to the function implementing that mode, and parse_options() should be
invoked with the PARSE_OPT_SUBCOMMAND_OPTIONAL flag.  In this case
parse_options() won't error out when not finding any subcommands, but
will return leaving 'fn' unchanged.  Note that if that default
operation mode has any --options, then the PARSE_OPT_KEEP_UNKNOWN_OPT
flag is necessary as well (otherwise parse_options() would error out
upon seeing the unknown option meant to the default operation mode).

Some thoughts about the implementation:

  - The same pointer to 'fn' must be specified as 'value' for each
    OPT_SUBCOMMAND, because there can be only one set of mutually
    exclusive subcommands; parse_options() will BUG() otherwise.

    There are other ways to tell parse_options() where to put the
    function associated with the subcommand given on the command line,
    but I didn't like them:

      - Change parse_options()'s signature by adding a pointer to
        subcommand function to be set to the function associated with
        the given subcommand, affecting all callsites, even those that
        don't have subcommands.

      - Introduce a specific parse_options_and_subcommand() variant
        with that extra funcion parameter.

  - I decided against automatically calling the subcommand function
    from within parse_options(), because:

      - There are commands that have to perform additional actions
        after option parsing but before calling the function
        implementing the specified subcommand.

      - The return code of the subcommand is usually the return code
        of the git command, but preserving the return code of the
        automatically called subcommand function would have made the
        API awkward.

  - Also add a OPT_SUBCOMMAND_F() variant to allow specifying an
    option flag: we have two subcommands that are purposefully
    excluded from completion ('git remote rm' and 'git stash save'),
    so they'll have to be specified with the PARSE_OPT_NOCOMPLETE
    flag.

  - Some of the 'parse_opt_flags' don't make sense with subcommands,
    and using them is probably just an oversight or misunderstanding.
    Therefore parse_options() will BUG() when invoked with any of the
    following flags while the options array contains at least one
    OPT_SUBCOMMAND:

      - PARSE_OPT_KEEP_DASHDASH: parse_options() stops parsing
        arguments when encountering a "--" argument, so it doesn't
        make sense to expect and keep one before a subcommand, because
        it would prevent the parsing of the subcommand.

        However, this flag is allowed in combination with the
        PARSE_OPT_SUBCOMMAND_OPTIONAL flag, because the double dash
        might be meaningful for the command's default operation mode,
        e.g. to disambiguate refs and pathspecs.

      - PARSE_OPT_STOP_AT_NON_OPTION: As its name suggests, this flag
        tells parse_options() to stop as soon as it encouners a
        non-option argument, but subcommands are by definition not
        options...  so how could they be parsed, then?!

      - PARSE_OPT_KEEP_UNKNOWN: This flag can be used to collect any
        unknown --options and then pass them to a different command or
        subsystem.  Surely if a command has subcommands, then this
        functionality should rather be delegated to one of those
        subcommands, and not performed by the command itself.

        However, this flag is allowed in combination with the
        PARSE_OPT_SUBCOMMAND_OPTIONAL flag, making possible to pass
        --options to the default operation mode.

  - If the command with subcommands has a default operation mode, then
    all arguments to the command must preceed the arguments of the
    subcommand.

    AFAICT we don't have any commands where this makes a difference,
    because in those commands either only the command accepts any
    arguments ('notes' and 'remote'), or only the default subcommand
    ('reflog' and 'stash'), but never both.

  - The 'argv' array passed to subcommand functions currently starts
    with the name of the subcommand.  Keep this behavior.  AFAICT no
    subcommand functions depend on the actual content of 'argv[0]',
    but the parse_options() call handling their options expects that
    the options start at argv[1].

  - To support handling subcommands programmatically in our Bash
    completion script, 'git cmd --git-completion-helper' will now list
    both subcommands and regular --options, if any.  This means that
    the completion script will have to separate subcommands (i.e.
    words without a double dash prefix) from --options on its own, but
    that's rather easy to do, and it's not much work either, because
    the number of subcommands a command might have is rather low, and
    those commands accept only a single --option or none at all.  An
    alternative would be to introduce a separate option that lists
    only subcommands, but then the completion script would need not
    one but two git invocations and command substitutions for commands
    with subcommands.

    Note that this change doesn't affect the behavior of our Bash
    completion script, because when completing the --option of a
    command with subcommands, e.g. for 'git notes --<TAB>', then all
    subcommands will be filtered out anyway, as none of them will
    match the word to be completed starting with that double dash
    prefix.

[1] Except 'git rerere', because many of its subcommands are
    implemented in the bodies of the if-else if statements parsing the
    command's subcommand argument.

[2] Except 'credential', 'credential-store' and 'fsmonitor--daemon',
    because some of the functions implementing their subcommands take
    special parameters.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 Documentation/technical/api-parse-options.txt |  41 +++-
 builtin/blame.c                               |   1 +
 builtin/shortlog.c                            |   1 +
 parse-options.c                               | 113 ++++++++++-
 parse-options.h                               |  24 ++-
 t/helper/test-parse-options.c                 |  61 ++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t0040-parse-options.sh                      | 185 ++++++++++++++++++
 9 files changed, 419 insertions(+), 9 deletions(-)

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


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 10/20] builtin/bundle.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (8 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 17:50     ` Ævar Arnfjörð Bjarmason
  2022-08-19 16:04   ` [PATCH v2 11/20] builtin/commit-graph.c: " SZEDER Gábor
                     ` (10 subsequent siblings)
  20 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git bundle' parses its subcommands with a couple of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/bundle.c | 25 +++++++------------------
 1 file changed, 7 insertions(+), 18 deletions(-)

diff --git a/builtin/bundle.c b/builtin/bundle.c
index 2adad545a2..e80efce3a4 100644
--- a/builtin/bundle.c
+++ b/builtin/bundle.c
@@ -195,30 +195,19 @@ static int cmd_bundle_unbundle(int argc, const char **argv, const char *prefix)
 
 int cmd_bundle(int argc, const char **argv, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("create", &fn, cmd_bundle_create),
+		OPT_SUBCOMMAND("verify", &fn, cmd_bundle_verify),
+		OPT_SUBCOMMAND("list-heads", &fn, cmd_bundle_list_heads),
+		OPT_SUBCOMMAND("unbundle", &fn, cmd_bundle_unbundle),
 		OPT_END()
 	};
-	int result;
 
 	argc = parse_options(argc, argv, prefix, options, builtin_bundle_usage,
-		PARSE_OPT_STOP_AT_NON_OPTION);
+			     0);
 
 	packet_trace_identity("bundle");
 
-	if (argc < 2)
-		usage_with_options(builtin_bundle_usage, options);
-
-	else if (!strcmp(argv[0], "create"))
-		result = cmd_bundle_create(argc, argv, prefix);
-	else if (!strcmp(argv[0], "verify"))
-		result = cmd_bundle_verify(argc, argv, prefix);
-	else if (!strcmp(argv[0], "list-heads"))
-		result = cmd_bundle_list_heads(argc, argv, prefix);
-	else if (!strcmp(argv[0], "unbundle"))
-		result = cmd_bundle_unbundle(argc, argv, prefix);
-	else {
-		error(_("Unknown subcommand: %s"), argv[0]);
-		usage_with_options(builtin_bundle_usage, options);
-	}
-	return result ? 1 : 0;
+	return !!fn(argc, argv, prefix);
 }
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 11/20] builtin/commit-graph.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (9 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 10/20] builtin/bundle.c: let parse-options parse subcommands SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 17:53     ` Ævar Arnfjörð Bjarmason
  2022-08-19 16:04   ` [PATCH v2 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands SZEDER Gábor
                     ` (9 subsequent siblings)
  20 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git commit-graph' parses its subcommands with an if-else if
statement.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Note that the functions implementing each subcommand only accept the
'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter
to make them match the type expected by parse-options, and thus avoid
casting function pointers.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/commit-graph.c  | 30 +++++++++++++-----------------
 t/t5318-commit-graph.sh |  4 ++--
 2 files changed, 15 insertions(+), 19 deletions(-)

diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c
index 51c4040ea6..1eb5492cbd 100644
--- a/builtin/commit-graph.c
+++ b/builtin/commit-graph.c
@@ -58,7 +58,7 @@ static struct option *add_common_options(struct option *to)
 	return parse_options_concat(common_opts, to);
 }
 
-static int graph_verify(int argc, const char **argv)
+static int graph_verify(int argc, const char **argv, const char *prefix)
 {
 	struct commit_graph *graph = NULL;
 	struct object_directory *odb = NULL;
@@ -190,7 +190,7 @@ static int git_commit_graph_write_config(const char *var, const char *value,
 	return 0;
 }
 
-static int graph_write(int argc, const char **argv)
+static int graph_write(int argc, const char **argv, const char *prefix)
 {
 	struct string_list pack_indexes = STRING_LIST_INIT_DUP;
 	struct strbuf buf = STRBUF_INIT;
@@ -307,26 +307,22 @@ static int graph_write(int argc, const char **argv)
 
 int cmd_commit_graph(int argc, const char **argv, const char *prefix)
 {
-	struct option *builtin_commit_graph_options = common_opts;
+	parse_opt_subcommand_fn *fn = NULL;
+	struct option builtin_commit_graph_options[] = {
+		OPT_SUBCOMMAND("verify", &fn, graph_verify),
+		OPT_SUBCOMMAND("write", &fn, graph_write),
+		OPT_END(),
+	};
+	struct option *options = parse_options_concat(builtin_commit_graph_options, common_opts);
 
 	git_config(git_default_config, NULL);
-	argc = parse_options(argc, argv, prefix,
-			     builtin_commit_graph_options,
-			     builtin_commit_graph_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
-	if (!argc)
-		goto usage;
 
 	read_replace_refs = 0;
 	save_commit_buffer = 0;
 
-	if (!strcmp(argv[0], "verify"))
-		return graph_verify(argc, argv);
-	else if (argc && !strcmp(argv[0], "write"))
-		return graph_write(argc, argv);
+	argc = parse_options(argc, argv, prefix, options,
+			     builtin_commit_graph_usage, 0);
+	FREE_AND_NULL(options);
 
-	error(_("unrecognized subcommand: %s"), argv[0]);
-usage:
-	usage_with_options(builtin_commit_graph_usage,
-			   builtin_commit_graph_options);
+	return fn(argc, argv, prefix);
 }
diff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh
index be0b5641ff..7e040eb1ed 100755
--- a/t/t5318-commit-graph.sh
+++ b/t/t5318-commit-graph.sh
@@ -12,12 +12,12 @@ test_expect_success 'usage' '
 
 test_expect_success 'usage shown without sub-command' '
 	test_expect_code 129 git commit-graph 2>err &&
-	! grep error: err
+	grep usage: err
 '
 
 test_expect_success 'usage shown with an error on unknown sub-command' '
 	cat >expect <<-\EOF &&
-	error: unrecognized subcommand: unknown
+	error: unknown subcommand: `unknown'\''
 	EOF
 	test_expect_code 129 git commit-graph unknown 2>stderr &&
 	grep error stderr >actual &&
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (10 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 11/20] builtin/commit-graph.c: " SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 20:59     ` Junio C Hamano
  2022-08-19 16:04   ` [PATCH v2 13/20] builtin/hook.c: let parse-options parse subcommands SZEDER Gábor
                     ` (8 subsequent siblings)
  20 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git maintenanze' parses its subcommands with a couple of if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

This change makes 'git maintenance' consistent with other commands in
that the help text shown for '-h' goes to standard output, not error,
in the exit code and error message on unknown subcommand, and the
error message on missing subcommand.  There is a test checking these,
which is now updated accordingly.

Note that some of the functions implementing each subcommand don't
accept any parameters, so add the (unused) 'argc', '**argv' and
'*prefix' parameters to make them match the type expected by
parse-options, and thus avoid casting function pointers.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/gc.c           | 42 +++++++++++++++++++++---------------------
 git.c                  |  2 +-
 t/t7900-maintenance.sh | 10 ++++++----
 3 files changed, 28 insertions(+), 26 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index eeff2b760e..19d6b3b558 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1465,7 +1465,7 @@ static char *get_maintpath(void)
 	return strbuf_detach(&sb, NULL);
 }
 
-static int maintenance_register(void)
+static int maintenance_register(int argc, const char **argv, const char *prefix)
 {
 	int rc;
 	char *config_value;
@@ -1509,7 +1509,7 @@ static int maintenance_register(void)
 	return rc;
 }
 
-static int maintenance_unregister(void)
+static int maintenance_unregister(int argc, const char **argv, const char *prefix)
 {
 	int rc;
 	struct child_process config_unset = CHILD_PROCESS_INIT;
@@ -2505,34 +2505,34 @@ static int maintenance_start(int argc, const char **argv, const char *prefix)
 	opts.scheduler = resolve_scheduler(opts.scheduler);
 	validate_scheduler(opts.scheduler);
 
-	if (maintenance_register())
+	if (maintenance_register(0, NULL, NULL)) /* It doesn't take any args */
 		warning(_("failed to add repo to global config"));
 	return update_background_schedule(&opts, 1);
 }
 
-static int maintenance_stop(void)
+static int maintenance_stop(int argc, const char **argv, const char *prefix)
 {
 	return update_background_schedule(NULL, 0);
 }
 
-static const char builtin_maintenance_usage[] =	N_("git maintenance <subcommand> [<options>]");
+static const char * const builtin_maintenance_usage[] = {
+	N_("git maintenance <subcommand> [<options>]"),
+	NULL,
+};
 
 int cmd_maintenance(int argc, const char **argv, const char *prefix)
 {
-	if (argc < 2 ||
-	    (argc == 2 && !strcmp(argv[1], "-h")))
-		usage(builtin_maintenance_usage);
-
-	if (!strcmp(argv[1], "run"))
-		return maintenance_run(argc - 1, argv + 1, prefix);
-	if (!strcmp(argv[1], "start"))
-		return maintenance_start(argc - 1, argv + 1, prefix);
-	if (!strcmp(argv[1], "stop"))
-		return maintenance_stop();
-	if (!strcmp(argv[1], "register"))
-		return maintenance_register();
-	if (!strcmp(argv[1], "unregister"))
-		return maintenance_unregister();
-
-	die(_("invalid subcommand: %s"), argv[1]);
+	parse_opt_subcommand_fn *fn = NULL;
+	struct option builtin_maintenance_options[] = {
+		OPT_SUBCOMMAND("run", &fn, maintenance_run),
+		OPT_SUBCOMMAND("start", &fn, maintenance_start),
+		OPT_SUBCOMMAND("stop", &fn, maintenance_stop),
+		OPT_SUBCOMMAND("register", &fn, maintenance_register),
+		OPT_SUBCOMMAND("unregister", &fn, maintenance_unregister),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, builtin_maintenance_options,
+			     builtin_maintenance_usage, 0);
+	return fn(argc, argv, prefix);
 }
diff --git a/git.c b/git.c
index 09c126c33d..73ddf0f452 100644
--- a/git.c
+++ b/git.c
@@ -555,7 +555,7 @@ static struct cmd_struct commands[] = {
 	{ "ls-tree", cmd_ls_tree, RUN_SETUP },
 	{ "mailinfo", cmd_mailinfo, RUN_SETUP_GENTLY },
 	{ "mailsplit", cmd_mailsplit, NO_PARSEOPT },
-	{ "maintenance", cmd_maintenance, RUN_SETUP | NO_PARSEOPT },
+	{ "maintenance", cmd_maintenance, RUN_SETUP },
 	{ "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE },
 	{ "merge-base", cmd_merge_base, RUN_SETUP },
 	{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 74aa638475..852498ff06 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -32,11 +32,13 @@ test_systemd_analyze_verify () {
 }
 
 test_expect_success 'help text' '
-	test_expect_code 129 git maintenance -h 2>err &&
-	test_i18ngrep "usage: git maintenance <subcommand>" err &&
-	test_expect_code 128 git maintenance barf 2>err &&
-	test_i18ngrep "invalid subcommand: barf" err &&
+	test_expect_code 129 git maintenance -h >actual &&
+	test_i18ngrep "usage: git maintenance <subcommand>" actual &&
+	test_expect_code 129 git maintenance barf 2>err &&
+	test_i18ngrep "unknown subcommand: \`barf'\''" err &&
+	test_i18ngrep "usage: git maintenance" err &&
 	test_expect_code 129 git maintenance 2>err &&
+	test_i18ngrep "error: need a subcommand" err &&
 	test_i18ngrep "usage: git maintenance" err
 '
 
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 13/20] builtin/hook.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (11 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 16:04   ` [PATCH v2 14/20] builtin/multi-pack-index.c: " SZEDER Gábor
                     ` (7 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git hook' parses its currently only subcommand with an if statement.
parse-options has just learned to parse subcommands, so let's use that
facility instead, with the benefits of shorter code, handling missing
or unknown subcommands, and listing subcommands for Bash completion.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/hook.c | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/builtin/hook.c b/builtin/hook.c
index 54e5c6ec93..b6530d189a 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -67,18 +67,14 @@ static int run(int argc, const char **argv, const char *prefix)
 
 int cmd_hook(int argc, const char **argv, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option builtin_hook_options[] = {
+		OPT_SUBCOMMAND("run", &fn, run),
 		OPT_END(),
 	};
 
 	argc = parse_options(argc, argv, NULL, builtin_hook_options,
-			     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
-	if (!argc)
-		goto usage;
+			     builtin_hook_usage, 0);
 
-	if (!strcmp(argv[0], "run"))
-		return run(argc, argv, prefix);
-
-usage:
-	usage_with_options(builtin_hook_usage, builtin_hook_options);
+	return fn(argc, argv, prefix);
 }
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 14/20] builtin/multi-pack-index.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (12 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 13/20] builtin/hook.c: let parse-options parse subcommands SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 17:57     ` Ævar Arnfjörð Bjarmason
  2022-08-19 16:04   ` [PATCH v2 15/20] builtin/notes.c: " SZEDER Gábor
                     ` (6 subsequent siblings)
  20 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git multi-pack-index' parses its subcommands with a couple of if-else
if statements.  parse-options has just learned to parse subcommands,
so let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Note that the functions implementing each subcommand only accept the
'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter
to make them match the type expected by parse-options, and thus avoid
casting function pointers.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/multi-pack-index.c | 51 ++++++++++++++++----------------------
 1 file changed, 22 insertions(+), 29 deletions(-)

diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
index 8f24d59a75..b8320d597b 100644
--- a/builtin/multi-pack-index.c
+++ b/builtin/multi-pack-index.c
@@ -104,7 +104,8 @@ static void read_packs_from_stdin(struct string_list *to)
 	strbuf_release(&buf);
 }
 
-static int cmd_multi_pack_index_write(int argc, const char **argv)
+static int cmd_multi_pack_index_write(int argc, const char **argv,
+				      const char *prefix)
 {
 	struct option *options;
 	static struct option builtin_multi_pack_index_write_options[] = {
@@ -160,7 +161,8 @@ static int cmd_multi_pack_index_write(int argc, const char **argv)
 			       opts.refs_snapshot, opts.flags);
 }
 
-static int cmd_multi_pack_index_verify(int argc, const char **argv)
+static int cmd_multi_pack_index_verify(int argc, const char **argv,
+				       const char *prefix)
 {
 	struct option *options;
 	static struct option builtin_multi_pack_index_verify_options[] = {
@@ -186,7 +188,8 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv)
 	return verify_midx_file(the_repository, opts.object_dir, opts.flags);
 }
 
-static int cmd_multi_pack_index_expire(int argc, const char **argv)
+static int cmd_multi_pack_index_expire(int argc, const char **argv,
+				       const char *prefix)
 {
 	struct option *options;
 	static struct option builtin_multi_pack_index_expire_options[] = {
@@ -212,7 +215,8 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv)
 	return expire_midx_packs(the_repository, opts.object_dir, opts.flags);
 }
 
-static int cmd_multi_pack_index_repack(int argc, const char **argv)
+static int cmd_multi_pack_index_repack(int argc, const char **argv,
+				       const char *prefix)
 {
 	struct option *options;
 	static struct option builtin_multi_pack_index_repack_options[] = {
@@ -247,7 +251,15 @@ int cmd_multi_pack_index(int argc, const char **argv,
 			 const char *prefix)
 {
 	int res;
-	struct option *builtin_multi_pack_index_options = common_opts;
+	parse_opt_subcommand_fn *fn = NULL;
+	struct option builtin_multi_pack_index_options[] = {
+		OPT_SUBCOMMAND("repack", &fn, cmd_multi_pack_index_repack),
+		OPT_SUBCOMMAND("write", &fn, cmd_multi_pack_index_write),
+		OPT_SUBCOMMAND("verify", &fn, cmd_multi_pack_index_verify),
+		OPT_SUBCOMMAND("expire", &fn, cmd_multi_pack_index_expire),
+		OPT_END(),
+	};
+	struct option *options = parse_options_concat(builtin_multi_pack_index_options, common_opts);
 
 	git_config(git_default_config, NULL);
 
@@ -256,31 +268,12 @@ int cmd_multi_pack_index(int argc, const char **argv,
 	    the_repository->objects->odb)
 		opts.object_dir = xstrdup(the_repository->objects->odb->path);
 
-	argc = parse_options(argc, argv, prefix,
-			     builtin_multi_pack_index_options,
-			     builtin_multi_pack_index_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
-
-	if (!argc)
-		goto usage;
-
-	if (!strcmp(argv[0], "repack"))
-		res = cmd_multi_pack_index_repack(argc, argv);
-	else if (!strcmp(argv[0], "write"))
-		res =  cmd_multi_pack_index_write(argc, argv);
-	else if (!strcmp(argv[0], "verify"))
-		res =  cmd_multi_pack_index_verify(argc, argv);
-	else if (!strcmp(argv[0], "expire"))
-		res =  cmd_multi_pack_index_expire(argc, argv);
-	else {
-		error(_("unrecognized subcommand: %s"), argv[0]);
-		goto usage;
-	}
+	argc = parse_options(argc, argv, prefix, options,
+			     builtin_multi_pack_index_usage, 0);
+	FREE_AND_NULL(options);
+
+	res = fn(argc, argv, prefix);
 
 	free(opts.object_dir);
 	return res;
-
-usage:
-	usage_with_options(builtin_multi_pack_index_usage,
-			   builtin_multi_pack_index_options);
 }
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 15/20] builtin/notes.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (13 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 14/20] builtin/multi-pack-index.c: " SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 18:01     ` Ævar Arnfjörð Bjarmason
  2022-08-19 16:04   ` [PATCH v2 16/20] builtin/reflog.c: " SZEDER Gábor
                     ` (5 subsequent siblings)
  20 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git notes' parses its subcommands with a long list of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling unknown subcommands, and listing subcommands for Bash
completion.  Make sure that the default operation mode doesn't accept
any arguments.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/notes.c | 43 +++++++++++++++++--------------------------
 1 file changed, 17 insertions(+), 26 deletions(-)

diff --git a/builtin/notes.c b/builtin/notes.c
index a3d0d15a22..42cbae4659 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -994,17 +994,31 @@ static int get_ref(int argc, const char **argv, const char *prefix)
 
 int cmd_notes(int argc, const char **argv, const char *prefix)
 {
-	int result;
 	const char *override_notes_ref = NULL;
+	parse_opt_subcommand_fn *fn = list;
 	struct option options[] = {
 		OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"),
 			   N_("use notes from <notes-ref>")),
+		OPT_SUBCOMMAND("list", &fn, list),
+		OPT_SUBCOMMAND("add", &fn, add),
+		OPT_SUBCOMMAND("copy", &fn, copy),
+		OPT_SUBCOMMAND("append", &fn, append_edit),
+		OPT_SUBCOMMAND("edit", &fn, append_edit),
+		OPT_SUBCOMMAND("show", &fn, show),
+		OPT_SUBCOMMAND("merge", &fn, merge),
+		OPT_SUBCOMMAND("remove", &fn, remove_cmd),
+		OPT_SUBCOMMAND("prune", &fn, prune),
+		OPT_SUBCOMMAND("get-ref", &fn, get_ref),
 		OPT_END()
 	};
 
 	git_config(git_default_config, NULL);
 	argc = parse_options(argc, argv, prefix, options, git_notes_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
+	if (fn == list && argc && strcmp(argv[0], "list")) {
+		error(_("unknown subcommand: %s"), argv[0]);
+		usage_with_options(git_notes_usage, options);
+	}
 
 	if (override_notes_ref) {
 		struct strbuf sb = STRBUF_INIT;
@@ -1014,28 +1028,5 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
 		strbuf_release(&sb);
 	}
 
-	if (argc < 1 || !strcmp(argv[0], "list"))
-		result = list(argc, argv, prefix);
-	else if (!strcmp(argv[0], "add"))
-		result = add(argc, argv, prefix);
-	else if (!strcmp(argv[0], "copy"))
-		result = copy(argc, argv, prefix);
-	else if (!strcmp(argv[0], "append") || !strcmp(argv[0], "edit"))
-		result = append_edit(argc, argv, prefix);
-	else if (!strcmp(argv[0], "show"))
-		result = show(argc, argv, prefix);
-	else if (!strcmp(argv[0], "merge"))
-		result = merge(argc, argv, prefix);
-	else if (!strcmp(argv[0], "remove"))
-		result = remove_cmd(argc, argv, prefix);
-	else if (!strcmp(argv[0], "prune"))
-		result = prune(argc, argv, prefix);
-	else if (!strcmp(argv[0], "get-ref"))
-		result = get_ref(argc, argv, prefix);
-	else {
-		result = error(_("unknown subcommand: %s"), argv[0]);
-		usage_with_options(git_notes_usage, options);
-	}
-
-	return result ? 1 : 0;
+	return !!fn(argc, argv, prefix);
 }
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 16/20] builtin/reflog.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (14 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 15/20] builtin/notes.c: " SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 18:08     ` Ævar Arnfjörð Bjarmason
  2022-08-19 16:04   ` [PATCH v2 17/20] builtin/remote.c: " SZEDER Gábor
                     ` (4 subsequent siblings)
  20 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git reflog' parses its subcommands with a couple of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
and listing subcommands for Bash completion.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/reflog.c | 41 +++++++++++------------------------------
 1 file changed, 11 insertions(+), 30 deletions(-)

diff --git a/builtin/reflog.c b/builtin/reflog.c
index b8b1f4f8ea..d3f6d903fb 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -404,40 +404,21 @@ static int cmd_reflog_exists(int argc, const char **argv, const char *prefix)
 
 int cmd_reflog(int argc, const char **argv, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("show", &fn, cmd_reflog_show),
+		OPT_SUBCOMMAND("expire", &fn, cmd_reflog_expire),
+		OPT_SUBCOMMAND("delete", &fn, cmd_reflog_delete),
+		OPT_SUBCOMMAND("exists", &fn, cmd_reflog_exists),
 		OPT_END()
 	};
 
 	argc = parse_options(argc, argv, prefix, options, reflog_usage,
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL |
 			     PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 |
-			     PARSE_OPT_KEEP_UNKNOWN_OPT |
-			     PARSE_OPT_NO_INTERNAL_HELP);
-
-	/*
-	 * With "git reflog" we default to showing it. !argc is
-	 * impossible with PARSE_OPT_KEEP_ARGV0.
-	 */
-	if (argc == 1)
-		goto log_reflog;
-
-	if (!strcmp(argv[1], "-h"))
-		usage_with_options(reflog_usage, options);
-	else if (*argv[1] == '-')
-		goto log_reflog;
-
-	if (!strcmp(argv[1], "show"))
-		return cmd_reflog_show(argc - 1, argv + 1, prefix);
-	else if (!strcmp(argv[1], "expire"))
-		return cmd_reflog_expire(argc - 1, argv + 1, prefix);
-	else if (!strcmp(argv[1], "delete"))
-		return cmd_reflog_delete(argc - 1, argv + 1, prefix);
-	else if (!strcmp(argv[1], "exists"))
-		return cmd_reflog_exists(argc - 1, argv + 1, prefix);
-
-	/*
-	 * Fall-through for e.g. "git reflog -1", "git reflog master",
-	 * as well as the plain "git reflog" above goto above.
-	 */
-log_reflog:
-	return cmd_log_reflog(argc, argv, prefix);
+			     PARSE_OPT_KEEP_UNKNOWN_OPT);
+	if (fn)
+		return fn(argc - 1, argv + 1, prefix);
+	else
+		return cmd_log_reflog(argc, argv, prefix);
 }
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 17/20] builtin/remote.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (15 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 16/20] builtin/reflog.c: " SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 16:04   ` [PATCH v2 18/20] builtin/sparse-checkout.c: " SZEDER Gábor
                     ` (3 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git remote' parses its subcommands with a long list of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling unknown subcommands, and listing subcommands for Bash
completion.  Make sure that the default operation mode doesn't accept
any arguments; and while at it remove the capitalization of the error
message and adjust the test checking it accordingly.

Note that 'git remote' has both 'remove' and 'rm' subcommands, and the
former is preferred [1], so hide the latter for completion.

Note also that the functions implementing each subcommand only accept
the 'argc' and '**argv' parameters, so add a (unused) '*prefix'
parameter to make them match the type expected by parse-options, and
thus avoid casting a bunch of function pointers.

[1] e17dba8fe1 (remote: prefer subcommand name 'remove' to 'rm',
    2012-09-06)

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/remote.c  | 70 +++++++++++++++++++++--------------------------
 t/t5505-remote.sh |  2 +-
 2 files changed, 32 insertions(+), 40 deletions(-)

diff --git a/builtin/remote.c b/builtin/remote.c
index d9b8746cb3..4a6d47c03a 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -150,7 +150,7 @@ static int parse_mirror_opt(const struct option *opt, const char *arg, int not)
 	return 0;
 }
 
-static int add(int argc, const char **argv)
+static int add(int argc, const char **argv, const char *prefix)
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
@@ -680,7 +680,7 @@ static void handle_push_default(const char* old_name, const char* new_name)
 }
 
 
-static int mv(int argc, const char **argv)
+static int mv(int argc, const char **argv, const char *prefix)
 {
 	int show_progress = isatty(2);
 	struct option options[] = {
@@ -844,7 +844,7 @@ static int mv(int argc, const char **argv)
 	return 0;
 }
 
-static int rm(int argc, const char **argv)
+static int rm(int argc, const char **argv, const char *prefix)
 {
 	struct option options[] = {
 		OPT_END()
@@ -1255,7 +1255,7 @@ static int show_all(void)
 	return result;
 }
 
-static int show(int argc, const char **argv)
+static int show(int argc, const char **argv, const char *prefix)
 {
 	int no_query = 0, result = 0, query_flag = 0;
 	struct option options[] = {
@@ -1358,7 +1358,7 @@ static int show(int argc, const char **argv)
 	return result;
 }
 
-static int set_head(int argc, const char **argv)
+static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
 	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
@@ -1463,7 +1463,7 @@ static int prune_remote(const char *remote, int dry_run)
 	return result;
 }
 
-static int prune(int argc, const char **argv)
+static int prune(int argc, const char **argv, const char *prefix)
 {
 	int dry_run = 0, result = 0;
 	struct option options[] = {
@@ -1492,7 +1492,7 @@ static int get_remote_default(const char *key, const char *value, void *priv)
 	return 0;
 }
 
-static int update(int argc, const char **argv)
+static int update(int argc, const char **argv, const char *prefix)
 {
 	int i, prune = -1;
 	struct option options[] = {
@@ -1575,7 +1575,7 @@ static int set_remote_branches(const char *remotename, const char **branches,
 	return 0;
 }
 
-static int set_branches(int argc, const char **argv)
+static int set_branches(int argc, const char **argv, const char *prefix)
 {
 	int add_mode = 0;
 	struct option options[] = {
@@ -1594,7 +1594,7 @@ static int set_branches(int argc, const char **argv)
 	return set_remote_branches(argv[0], argv + 1, add_mode);
 }
 
-static int get_url(int argc, const char **argv)
+static int get_url(int argc, const char **argv, const char *prefix)
 {
 	int i, push_mode = 0, all_mode = 0;
 	const char *remotename = NULL;
@@ -1647,7 +1647,7 @@ static int get_url(int argc, const char **argv)
 	return 0;
 }
 
-static int set_url(int argc, const char **argv)
+static int set_url(int argc, const char **argv, const char *prefix)
 {
 	int i, push_mode = 0, add_mode = 0, delete_mode = 0;
 	int matches = 0, negative_matches = 0;
@@ -1739,41 +1739,33 @@ static int set_url(int argc, const char **argv)
 
 int cmd_remote(int argc, const char **argv, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
 		OPT__VERBOSE(&verbose, N_("be verbose; must be placed before a subcommand")),
+		OPT_SUBCOMMAND("add", &fn, add),
+		OPT_SUBCOMMAND("rename", &fn, mv),
+		OPT_SUBCOMMAND_F("rm", &fn, rm, PARSE_OPT_NOCOMPLETE),
+		OPT_SUBCOMMAND("remove", &fn, rm),
+		OPT_SUBCOMMAND("set-head", &fn, set_head),
+		OPT_SUBCOMMAND("set-branches", &fn, set_branches),
+		OPT_SUBCOMMAND("get-url", &fn, get_url),
+		OPT_SUBCOMMAND("set-url", &fn, set_url),
+		OPT_SUBCOMMAND("show", &fn, show),
+		OPT_SUBCOMMAND("prune", &fn, prune),
+		OPT_SUBCOMMAND("update", &fn, update),
 		OPT_END()
 	};
-	int result;
 
 	argc = parse_options(argc, argv, prefix, options, builtin_remote_usage,
-		PARSE_OPT_STOP_AT_NON_OPTION);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
 
-	if (argc < 1)
-		result = show_all();
-	else if (!strcmp(argv[0], "add"))
-		result = add(argc, argv);
-	else if (!strcmp(argv[0], "rename"))
-		result = mv(argc, argv);
-	else if (!strcmp(argv[0], "rm") || !strcmp(argv[0], "remove"))
-		result = rm(argc, argv);
-	else if (!strcmp(argv[0], "set-head"))
-		result = set_head(argc, argv);
-	else if (!strcmp(argv[0], "set-branches"))
-		result = set_branches(argc, argv);
-	else if (!strcmp(argv[0], "get-url"))
-		result = get_url(argc, argv);
-	else if (!strcmp(argv[0], "set-url"))
-		result = set_url(argc, argv);
-	else if (!strcmp(argv[0], "show"))
-		result = show(argc, argv);
-	else if (!strcmp(argv[0], "prune"))
-		result = prune(argc, argv);
-	else if (!strcmp(argv[0], "update"))
-		result = update(argc, argv);
-	else {
-		error(_("Unknown subcommand: %s"), argv[0]);
-		usage_with_options(builtin_remote_usage, options);
+	if (fn) {
+		return !!fn(argc, argv, prefix);
+	} else {
+		if (argc) {
+			error(_("unknown subcommand: %s"), argv[0]);
+			usage_with_options(builtin_remote_usage, options);
+		}
+		return !!show_all();
 	}
-
-	return result ? 1 : 0;
 }
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index a549a21ef6..9006196ac6 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -258,7 +258,7 @@ test_expect_success 'without subcommand accepts -v' '
 
 test_expect_success 'without subcommand does not take arguments' '
 	test_expect_code 129 git -C test remote origin 2>err &&
-	grep "^error: Unknown subcommand:" err
+	grep "^error: unknown subcommand:" err
 '
 
 cat >test/expect <<EOF
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 18/20] builtin/sparse-checkout.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (16 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 17/20] builtin/remote.c: " SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 16:04   ` [PATCH v2 19/20] builtin/stash.c: " SZEDER Gábor
                     ` (2 subsequent siblings)
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git sparse-checkout' parses its subcommands with a couple of if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Note that some of the functions implementing each subcommand only
accept the 'argc' and '**argv' parameters, so add a (unused) '*prefix'
parameter to make them match the type expected by parse-options, and
thus avoid casting function pointers.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/sparse-checkout.c | 44 ++++++++++++++-------------------------
 1 file changed, 16 insertions(+), 28 deletions(-)

diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c
index a5e4b95a9d..7b39a150a9 100644
--- a/builtin/sparse-checkout.c
+++ b/builtin/sparse-checkout.c
@@ -48,7 +48,7 @@ static char const * const builtin_sparse_checkout_list_usage[] = {
 	NULL
 };
 
-static int sparse_checkout_list(int argc, const char **argv)
+static int sparse_checkout_list(int argc, const char **argv, const char *prefix)
 {
 	static struct option builtin_sparse_checkout_list_options[] = {
 		OPT_END(),
@@ -431,7 +431,7 @@ static struct sparse_checkout_init_opts {
 	int sparse_index;
 } init_opts;
 
-static int sparse_checkout_init(int argc, const char **argv)
+static int sparse_checkout_init(int argc, const char **argv, const char *prefix)
 {
 	struct pattern_list pl;
 	char *sparse_filename;
@@ -843,7 +843,8 @@ static struct sparse_checkout_reapply_opts {
 	int sparse_index;
 } reapply_opts;
 
-static int sparse_checkout_reapply(int argc, const char **argv)
+static int sparse_checkout_reapply(int argc, const char **argv,
+				   const char *prefix)
 {
 	static struct option builtin_sparse_checkout_reapply_options[] = {
 		OPT_BOOL(0, "cone", &reapply_opts.cone_mode,
@@ -876,7 +877,8 @@ static char const * const builtin_sparse_checkout_disable_usage[] = {
 	NULL
 };
 
-static int sparse_checkout_disable(int argc, const char **argv)
+static int sparse_checkout_disable(int argc, const char **argv,
+				   const char *prefix)
 {
 	static struct option builtin_sparse_checkout_disable_options[] = {
 		OPT_END(),
@@ -922,39 +924,25 @@ static int sparse_checkout_disable(int argc, const char **argv)
 
 int cmd_sparse_checkout(int argc, const char **argv, const char *prefix)
 {
-	static struct option builtin_sparse_checkout_options[] = {
+	parse_opt_subcommand_fn *fn = NULL;
+	struct option builtin_sparse_checkout_options[] = {
+		OPT_SUBCOMMAND("list", &fn, sparse_checkout_list),
+		OPT_SUBCOMMAND("init", &fn, sparse_checkout_init),
+		OPT_SUBCOMMAND("set", &fn, sparse_checkout_set),
+		OPT_SUBCOMMAND("add", &fn, sparse_checkout_add),
+		OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply),
+		OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable),
 		OPT_END(),
 	};
 
-	if (argc == 2 && !strcmp(argv[1], "-h"))
-		usage_with_options(builtin_sparse_checkout_usage,
-				   builtin_sparse_checkout_options);
-
 	argc = parse_options(argc, argv, prefix,
 			     builtin_sparse_checkout_options,
-			     builtin_sparse_checkout_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
+			     builtin_sparse_checkout_usage, 0);
 
 	git_config(git_default_config, NULL);
 
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
 
-	if (argc > 0) {
-		if (!strcmp(argv[0], "list"))
-			return sparse_checkout_list(argc, argv);
-		if (!strcmp(argv[0], "init"))
-			return sparse_checkout_init(argc, argv);
-		if (!strcmp(argv[0], "set"))
-			return sparse_checkout_set(argc, argv, prefix);
-		if (!strcmp(argv[0], "add"))
-			return sparse_checkout_add(argc, argv, prefix);
-		if (!strcmp(argv[0], "reapply"))
-			return sparse_checkout_reapply(argc, argv);
-		if (!strcmp(argv[0], "disable"))
-			return sparse_checkout_disable(argc, argv);
-	}
-
-	usage_with_options(builtin_sparse_checkout_usage,
-			   builtin_sparse_checkout_options);
+	return fn(argc, argv, prefix);
 }
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 19/20] builtin/stash.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (17 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 18/20] builtin/sparse-checkout.c: " SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-08-19 19:06     ` Ævar Arnfjörð Bjarmason
  2022-08-19 16:04   ` [PATCH v2 20/20] builtin/worktree.c: " SZEDER Gábor
  2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
  20 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git stash' parses its subcommands with a long list of if-else if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
and listing subcommands for Bash completion.

Note that the push_stash() function implementing the 'push' subcommand
accepts an extra flag parameter to indicate whether push was assumed,
so add a wrapper function with the standard subcommand function
signature.

Note also that this change "hides" the '-h' option in 'git stash push
-h' from the parse_option() call in cmd_stash(), as it comes after the
subcommand.  Consequently, from now on it will emit the usage of the
'push' subcommand instead of the usage of 'git stash'.  We had a
failing test for this case, which can now be flipped to expect
success.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/stash.c  | 53 ++++++++++++++++++++++--------------------------
 t/t3903-stash.sh |  2 +-
 2 files changed, 25 insertions(+), 30 deletions(-)

diff --git a/builtin/stash.c b/builtin/stash.c
index a14e832e9f..1ba24c1173 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1739,6 +1739,11 @@ static int push_stash(int argc, const char **argv, const char *prefix,
 			     include_untracked, only_staged);
 }
 
+static int push_stash_unassumed(int argc, const char **argv, const char *prefix)
+{
+	return push_stash(argc, argv, prefix, 0);
+}
+
 static int save_stash(int argc, const char **argv, const char *prefix)
 {
 	int keep_index = -1;
@@ -1787,15 +1792,28 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 	pid_t pid = getpid();
 	const char *index_file;
 	struct strvec args = STRVEC_INIT;
-
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("apply", &fn, apply_stash),
+		OPT_SUBCOMMAND("clear", &fn, clear_stash),
+		OPT_SUBCOMMAND("drop", &fn, drop_stash),
+		OPT_SUBCOMMAND("pop", &fn, pop_stash),
+		OPT_SUBCOMMAND("branch", &fn, branch_stash),
+		OPT_SUBCOMMAND("list", &fn, list_stash),
+		OPT_SUBCOMMAND("show", &fn, show_stash),
+		OPT_SUBCOMMAND("store", &fn, store_stash),
+		OPT_SUBCOMMAND("create", &fn, create_stash),
+		OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
+		OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
 		OPT_END()
 	};
 
 	git_config(git_stash_config, NULL);
 
 	argc = parse_options(argc, argv, prefix, options, git_stash_usage,
-			     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL |
+			     PARSE_OPT_KEEP_UNKNOWN_OPT |
+			     PARSE_OPT_KEEP_DASHDASH);
 
 	prepare_repo_settings(the_repository);
 	the_repository->settings.command_requires_full_index = 0;
@@ -1804,33 +1822,10 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 	strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
 		    (uintmax_t)pid);
 
-	if (!argc)
-		return !!push_stash(0, NULL, prefix, 0);
-	else if (!strcmp(argv[0], "apply"))
-		return !!apply_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "clear"))
-		return !!clear_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "drop"))
-		return !!drop_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "pop"))
-		return !!pop_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "branch"))
-		return !!branch_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "list"))
-		return !!list_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "show"))
-		return !!show_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "store"))
-		return !!store_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "create"))
-		return !!create_stash(argc, argv, prefix);
-	else if (!strcmp(argv[0], "push"))
-		return !!push_stash(argc, argv, prefix, 0);
-	else if (!strcmp(argv[0], "save"))
-		return !!save_stash(argc, argv, prefix);
-	else if (*argv[0] != '-')
-		usage_msg_optf(_("unknown subcommand: %s"),
-			       git_stash_usage, options, argv[0]);
+	if (fn)
+		return !!fn(argc, argv, prefix);
+	else if (!argc)
+		return !!push_stash_unassumed(0, NULL, prefix);
 
 	/* Assume 'stash push' */
 	strvec_push(&args, "push");
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 2a4c3fd61c..376cc8f4ab 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '
 	grep -F "or: git stash show" usage
 '
 
-test_expect_failure 'usage for subcommands should emit subcommand usage' '
+test_expect_success 'usage for subcommands should emit subcommand usage' '
 	test_expect_code 129 git stash push -h >usage &&
 	grep -F "usage: git stash [push" usage
 '
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH v2 20/20] builtin/worktree.c: let parse-options parse subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (18 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 19/20] builtin/stash.c: " SZEDER Gábor
@ 2022-08-19 16:04   ` SZEDER Gábor
  2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
  20 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 16:04 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git worktree' parses its subcommands with a long list of if
statements.  parse-options has just learned to parse subcommands, so
let's use that facility instead, with the benefits of shorter code,
handling missing or unknown subcommands, and listing subcommands for
Bash completion.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/worktree.c | 31 ++++++++++++-------------------
 git.c              |  2 +-
 2 files changed, 13 insertions(+), 20 deletions(-)

diff --git a/builtin/worktree.c b/builtin/worktree.c
index cd62eef240..c6710b2552 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -1112,31 +1112,24 @@ static int repair(int ac, const char **av, const char *prefix)
 
 int cmd_worktree(int ac, const char **av, const char *prefix)
 {
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("add", &fn, add),
+		OPT_SUBCOMMAND("prune", &fn, prune),
+		OPT_SUBCOMMAND("list", &fn, list),
+		OPT_SUBCOMMAND("lock", &fn, lock_worktree),
+		OPT_SUBCOMMAND("unlock", &fn, unlock_worktree),
+		OPT_SUBCOMMAND("move", &fn, move_worktree),
+		OPT_SUBCOMMAND("remove", &fn, remove_worktree),
+		OPT_SUBCOMMAND("repair", &fn, repair),
 		OPT_END()
 	};
 
 	git_config(git_worktree_config, NULL);
 
-	if (ac < 2)
-		usage_with_options(worktree_usage, options);
 	if (!prefix)
 		prefix = "";
-	if (!strcmp(av[1], "add"))
-		return add(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "prune"))
-		return prune(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "list"))
-		return list(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "lock"))
-		return lock_worktree(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "unlock"))
-		return unlock_worktree(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "move"))
-		return move_worktree(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "remove"))
-		return remove_worktree(ac - 1, av + 1, prefix);
-	if (!strcmp(av[1], "repair"))
-		return repair(ac - 1, av + 1, prefix);
-	usage_with_options(worktree_usage, options);
+
+	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
+	return fn(ac, av, prefix);
 }
diff --git a/git.c b/git.c
index 73ddf0f452..f52a955410 100644
--- a/git.c
+++ b/git.c
@@ -627,7 +627,7 @@ static struct cmd_struct commands[] = {
 	{ "verify-tag", cmd_verify_tag, RUN_SETUP },
 	{ "version", cmd_version },
 	{ "whatchanged", cmd_whatchanged, RUN_SETUP },
-	{ "worktree", cmd_worktree, RUN_SETUP | NO_PARSEOPT },
+	{ "worktree", cmd_worktree, RUN_SETUP },
 	{ "write-tree", cmd_write_tree, RUN_SETUP },
 };
 
-- 
2.37.2.817.g36f84ce71d


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-08-19 16:03   ` [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
@ 2022-08-19 17:23     ` Ævar Arnfjörð Bjarmason
  2022-08-20 11:14       ` SZEDER Gábor
  2022-08-19 18:18     ` Junio C Hamano
  1 sibling, 1 reply; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 17:23 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> +	for (int i = 0; i < argc; i++)

See
https://lore.kernel.org/git/CABPp-BHvQwct2WRRYGyzm=YVkjmwBqoe1DUtCicuQW=jrQ2hdA@mail.gmail.com/;
but maybe nothing to to be done here...

> +	if (argc == 0 || strcmp(argv[0], "cmd")) {

Nit: !argc
> +		error("'cmd' is mandatory");
> +		usage_with_options(usage, test_flag_options);

I think you want usage_msg_opt() instead.

> +test_expect_success 'NO_INTERNAL_HELP works for -h' '
> +	test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&
> +	cat err &&

Stray "cat", presumably in error..

> +	grep "^error: unknown switch \`h$SQ" err &&
> +	grep "^usage: " err
> +'
> +
> +for help_opt in help help-all
> +do
> +	test_expect_success "NO_INTERNAL_HELP works for --$help_opt" "
> +		test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&
> +		cat err &&

ditto.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output
  2022-08-19 16:03   ` [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output SZEDER Gábor
@ 2022-08-19 17:30     ` Ævar Arnfjörð Bjarmason
  2022-08-19 18:35       ` SZEDER Gábor
  0 siblings, 1 reply; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 17:30 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> subcommands) a tad more complex, so I wanted to test the result.  The
> test would have to account for the presence of that leading space,
> which bugged my OCD, so let's get rid of it.

On the subject of OCD it...

> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> ---
>  parse-options.c | 3 ++-
>  1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/parse-options.c b/parse-options.c
> index a0a2cf98fa..8748f88e6f 100644
> --- a/parse-options.c
> +++ b/parse-options.c
> @@ -620,7 +620,8 @@ static int show_gitcomp(const struct option *opts, int show_all)
>  			suffix = "=";
>  		if (starts_with(opts->long_name, "no-"))
>  			nr_noopts++;
> -		printf(" --%s%s", opts->long_name, suffix);
> +		printf("%s--%s%s", opts == original_opts ? "" : " ",
> +		       opts->long_name, suffix);
>  	}
>  	show_negated_gitcomp(original_opts, show_all, -1);
>  	show_negated_gitcomp(original_opts, show_all, nr_noopts);

...bugs me a bit that we have a "suffix" variable, but not a "prefix"
for this, maybe this? We could also make the prefix a "const char
*const" to indicate that we don't modify it in the "for" body:

diff --git a/parse-options.c b/parse-options.c
index edf55d3ef5d..3000121e5c0 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -589,6 +589,7 @@ static int show_gitcomp(const struct option *opts, int show_all)
 	int nr_noopts = 0;
 
 	for (; opts->type != OPTION_END; opts++) {
+		const char *prefix = opts == original_opts ? "" : " ";
 		const char *suffix = "";
 
 		if (!opts->long_name)
@@ -620,7 +621,7 @@ static int show_gitcomp(const struct option *opts, int show_all)
 			suffix = "=";
 		if (starts_with(opts->long_name, "no-"))
 			nr_noopts++;
-		printf(" --%s%s", opts->long_name, suffix);
+		printf("%s--%s%s", opts->long_name, prefix, suffix);
 	}
 	show_negated_gitcomp(original_opts, show_all, -1);
 	show_negated_gitcomp(original_opts, show_all, nr_noopts);

^ permalink raw reply related	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 09/20] parse-options: add support for parsing subcommands
  2022-08-19 16:04   ` [PATCH v2 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
@ 2022-08-19 17:33     ` Ævar Arnfjörð Bjarmason
  2022-08-19 19:03     ` Ævar Arnfjörð Bjarmason
  1 sibling, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 17:33 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> +static enum parse_opt_result parse_subcommand(const char *arg,
> +					      const struct option *options)
> +{
> +	for (; options->type != OPTION_END; options++)
> +		if (options->type == OPTION_SUBCOMMAND &&
> +		    !strcmp(options->long_name, arg)) {
> +			*(parse_opt_subcommand_fn **)options->value = options->subcommand_fn;

Nit: Maybe do the cast by assigning to an intermediate variable? Would
make this less dense, and since we already have {}-braces...

> +			return PARSE_OPT_SUBCOMMAND;
> +		}
> +
> +	return PARSE_OPT_UNKNOWN;
> +}
> +
>  static void check_typos(const char *arg, const struct option *options)
>  {
>  	if (strlen(arg) < 3)
> @@ -442,6 +457,7 @@ static void check_typos(const char *arg, const struct option *options)
>  static void parse_options_check(const struct option *opts)
>  {
>  	char short_opts[128];
> +	void *subcommand_value = NULL;
>  
>  	memset(short_opts, '\0', sizeof(short_opts));
>  	for (; opts->type != OPTION_END; opts++) {
> @@ -489,6 +505,14 @@ static void parse_options_check(const struct option *opts)
>  			       "Are you using parse_options_step() directly?\n"
>  			       "That case is not supported yet.");
>  			break;
> +		case OPTION_SUBCOMMAND:
> +			if (!opts->value || !opts->subcommand_fn)
> +				optbug(opts, "OPTION_SUBCOMMAND needs a value and a subcommand function");

Better split into two IMO:

	if (!opts->value)
		optbug(opts, "OPTION_SUBCOMMAND needs a value");
	if (!opts->subcommand_fn)
		optbug(opts, "OPTION_SUBCOMMAND needs a subcommand_fn");

Then if we have extra checks we don't need to keep adding to a dense ||
and amend an existing string.

> +static int has_subcommands(const struct option *options)
> +{
> +	for (; options->type != OPTION_END; options++)
> +		if (options->type == OPTION_SUBCOMMAND)
> +			return 1;
> +	return 0;
> +}

I wondered if parse_options_check() couldn't take a "int
*has_subcommands", since we already loop through the options to check it
as part of the checks there (but we'd need to re-arrange them).

Not for optimization, just wondering if it would make the code flow
clearer, maybe not.

> +	if (ctx->has_subcommands) {
> +		if (flags & PARSE_OPT_STOP_AT_NON_OPTION)
> +			BUG("subcommands are incompatible with PARSE_OPT_STOP_AT_NON_OPTION");
> +		if (!(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {
> +			if (flags & PARSE_OPT_KEEP_UNKNOWN_OPT)
> +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_UNKNOWN_OPT unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");
> +			if (flags & PARSE_OPT_KEEP_DASHDASH)
> +				BUG("subcommands are incompatible with PARSE_OPT_KEEP_DASHDASH unless in combination with PARSE_OPT_SUBCOMMAND_OPTIONAL");

MM, very long lines. Maybe something like:

	BUG("%s flag cannot be combined with %s, unless %s is also provided", ...);

> +		}
> +	}
>  	if ((flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&
>  	    (flags & PARSE_OPT_STOP_AT_NON_OPTION) &&
>  	    !(flags & PARSE_OPT_ONE_SHOT))
> @@ -589,6 +634,7 @@ static int show_gitcomp(const struct option *opts, int show_all)
>  	int nr_noopts = 0;
>  
>  	for (; opts->type != OPTION_END; opts++) {
> +		const char *prefix = "--";

Ah, here we have a prefix variable, but it's more of an "infix"... :)

> +					return PARSE_OPT_DONE;
> +				error(_("unknown subcommand: `%s'"), arg);
> +				usage_with_options(usagestr, options);

ditto earlier patch comment about usage_msg_opt() (also applies below)

> +			case PARSE_OPT_NON_OPTION:
> +				/* Impossible. */

We could just skip this comment as...
> +				BUG("parse_subcommand() cannot return these");

...this makes it clear.

>  	case PARSE_OPT_DONE:
> +		if (ctx.has_subcommands &&
> +		    !(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {
> +			error(_("need a subcommand"));
> +			usage_with_options(usagestr, options);

ditto.

> +typedef int parse_opt_subcommand_fn(int argc, const char **argv,
> +				    const char *prefix);

We usually define function typedefs like:

	typedef int (*parse_opt_subcommand_fn)(...);

But I see that...

>  	enum parse_opt_type type;
> @@ -145,6 +155,7 @@ struct option {
>  	intptr_t defval;
>  	parse_opt_ll_cb *ll_callback;
>  	intptr_t extra;
> +	parse_opt_subcommand_fn *subcommand_fn;

You're doing this consistently with parse_opt_ll_cb etc., fair enough.

> +#define OPT_SUBCOMMAND_F(l, v, fn, f) { \
> +	.type = OPTION_SUBCOMMAND, \
> +	.long_name = (l), \
> +	.value = (v), \
> +	.flags = (f), \

Nice to see this form

> +	.subcommand_fn = (fn) }

Almost all other such decls end with:

		.foo = bar, \
	}

I.e. we put the closing "}" at the start of the line.

> +#define OPT_SUBCOMMAND(l, v, fn)    OPT_SUBCOMMAND_F((l), (v), (fn), 0)

I think this would be more readable as:

	#define OPT_SUBCOMMAND(l, v, fn) \
		OPT_SUBCOMMAND_F((l), (v), (fn), 0)

Which both aligns nicely, and isn't using a \t in the middle of the line
(which in any case we'll end up aligning with nothing else, as the names
are all different lengths, if other things continue using this pattern.
>  	enum parse_opt_flags flags;
> +	unsigned has_subcommands;

Maybe make it: "unsigned has_subcommands:1"?

> +	printf("fn: subcmd_one\n");

s/printf/puts/g here

> +	print_args(argc, argv);
> +	return 0;
> +}
> +
> +static int subcmd_two(int argc, const char **argv, const char *prefix)
> +{
> +	printf("fn: subcmd_two\n");

ditto.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 10/20] builtin/bundle.c: let parse-options parse subcommands
  2022-08-19 16:04   ` [PATCH v2 10/20] builtin/bundle.c: let parse-options parse subcommands SZEDER Gábor
@ 2022-08-19 17:50     ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 17:50 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:


Nit: I wouldn't mind keeping this variable:

>  	};
> -	int result;
>  
>  	argc = parse_options(argc, argv, prefix, options, builtin_bundle_usage,
> -		PARSE_OPT_STOP_AT_NON_OPTION);
> +			     0);
>  
>  	packet_trace_identity("bundle");
>  
> -	if (argc < 2)
> -		usage_with_options(builtin_bundle_usage, options);
> -
> -	else if (!strcmp(argv[0], "create"))
> -		result = cmd_bundle_create(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "verify"))
> -		result = cmd_bundle_verify(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "list-heads"))
> -		result = cmd_bundle_list_heads(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "unbundle"))
> -		result = cmd_bundle_unbundle(argc, argv, prefix);
> -	else {
> -		error(_("Unknown subcommand: %s"), argv[0]);
> -		usage_with_options(builtin_bundle_usage, options);
> -	}

Then just doing:

	result = fn(argc, argv, prefix);

Which would eliminate the need to change this:

> -	return result ? 1 : 0;
> +	return !!fn(argc, argv, prefix);
>  }

I wondered about why !! v.s. 0/1 for a second or so, but realized you
were just golf-ing an existing pattern.

FWIW I *think* if we're changing this we could just make it "return
fn()", as the functions themselves seem to return 0/1 or a !!'d
variable.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 11/20] builtin/commit-graph.c: let parse-options parse subcommands
  2022-08-19 16:04   ` [PATCH v2 11/20] builtin/commit-graph.c: " SZEDER Gábor
@ 2022-08-19 17:53     ` Ævar Arnfjörð Bjarmason
  2022-08-19 17:56       ` Ævar Arnfjörð Bjarmason
  2022-08-19 18:22       ` SZEDER Gábor
  0 siblings, 2 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 17:53 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> 'git commit-graph' parses its subcommands with an if-else if
> statement.  parse-options has just learned to parse subcommands, so
> let's use that facility instead, with the benefits of shorter code,
> handling missing or unknown subcommands, and listing subcommands for
> Bash completion.
>
> Note that the functions implementing each subcommand only accept the
> 'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter
> to make them match the type expected by parse-options, and thus avoid
> casting function pointers.
>
> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> ---
>  builtin/commit-graph.c  | 30 +++++++++++++-----------------
>  t/t5318-commit-graph.sh |  4 ++--
>  2 files changed, 15 insertions(+), 19 deletions(-)
>
> diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c
> index 51c4040ea6..1eb5492cbd 100644
> --- a/builtin/commit-graph.c
> +++ b/builtin/commit-graph.c
> @@ -58,7 +58,7 @@ static struct option *add_common_options(struct option *to)
>  	return parse_options_concat(common_opts, to);
>  }
>  
> -static int graph_verify(int argc, const char **argv)
> +static int graph_verify(int argc, const char **argv, const char *prefix)
>  {
>  	struct commit_graph *graph = NULL;
>  	struct object_directory *odb = NULL;
> @@ -190,7 +190,7 @@ static int git_commit_graph_write_config(const char *var, const char *value,
>  	return 0;
>  }
>  
> -static int graph_write(int argc, const char **argv)
> +static int graph_write(int argc, const char **argv, const char *prefix)
>  {
>  	struct string_list pack_indexes = STRING_LIST_INIT_DUP;
>  	struct strbuf buf = STRBUF_INIT;
> @@ -307,26 +307,22 @@ static int graph_write(int argc, const char **argv)
>  
>  int cmd_commit_graph(int argc, const char **argv, const char *prefix)
>  {
> -	struct option *builtin_commit_graph_options = common_opts;
> +	parse_opt_subcommand_fn *fn = NULL;
> +	struct option builtin_commit_graph_options[] = {
> +		OPT_SUBCOMMAND("verify", &fn, graph_verify),
> +		OPT_SUBCOMMAND("write", &fn, graph_write),
> +		OPT_END(),
> +	};
> +	struct option *options = parse_options_concat(builtin_commit_graph_options, common_opts);

This looks like it'll leak if...

>  
>  	git_config(git_default_config, NULL);
> -	argc = parse_options(argc, argv, prefix,
> -			     builtin_commit_graph_options,
> -			     builtin_commit_graph_usage,
> -			     PARSE_OPT_STOP_AT_NON_OPTION);
> -	if (!argc)
> -		goto usage;

We take this goto..

> +	argc = parse_options(argc, argv, prefix, options,
> +			     builtin_commit_graph_usage, 0);
> +	FREE_AND_NULL(options);

Why FREE_AND_NULL() over free()?
>  
> -	error(_("unrecognized subcommand: %s"), argv[0]);
> -usage:
> -	usage_with_options(builtin_commit_graph_usage,
> -			   builtin_commit_graph_options);
> +	return fn(argc, argv, prefix);

The leak seems easily solved, let's just do the free() at the end?

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 11/20] builtin/commit-graph.c: let parse-options parse subcommands
  2022-08-19 17:53     ` Ævar Arnfjörð Bjarmason
@ 2022-08-19 17:56       ` Ævar Arnfjörð Bjarmason
  2022-08-19 18:22       ` SZEDER Gábor
  1 sibling, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 17:56 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, Ævar Arnfjörð Bjarmason wrote:

> On Fri, Aug 19 2022, SZEDER Gábor wrote:
>
>> 'git commit-graph' parses its subcommands with an if-else if
>> statement.  parse-options has just learned to parse subcommands, so
>> let's use that facility instead, with the benefits of shorter code,
>> handling missing or unknown subcommands, and listing subcommands for
>> Bash completion.
>>
>> Note that the functions implementing each subcommand only accept the
>> 'argc' and '**argv' parameters, so add a (unused) '*prefix' parameter
>> to make them match the type expected by parse-options, and thus avoid
>> casting function pointers.
>>
>> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
>> ---
>>  builtin/commit-graph.c  | 30 +++++++++++++-----------------
>>  t/t5318-commit-graph.sh |  4 ++--
>>  2 files changed, 15 insertions(+), 19 deletions(-)
>>
>> diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c
>> index 51c4040ea6..1eb5492cbd 100644
>> --- a/builtin/commit-graph.c
>> +++ b/builtin/commit-graph.c
>> @@ -58,7 +58,7 @@ static struct option *add_common_options(struct option *to)
>>  	return parse_options_concat(common_opts, to);
>>  }
>>  
>> -static int graph_verify(int argc, const char **argv)
>> +static int graph_verify(int argc, const char **argv, const char *prefix)
>>  {
>>  	struct commit_graph *graph = NULL;
>>  	struct object_directory *odb = NULL;
>> @@ -190,7 +190,7 @@ static int git_commit_graph_write_config(const char *var, const char *value,
>>  	return 0;
>>  }
>>  
>> -static int graph_write(int argc, const char **argv)
>> +static int graph_write(int argc, const char **argv, const char *prefix)
>>  {
>>  	struct string_list pack_indexes = STRING_LIST_INIT_DUP;
>>  	struct strbuf buf = STRBUF_INIT;
>> @@ -307,26 +307,22 @@ static int graph_write(int argc, const char **argv)
>>  
>>  int cmd_commit_graph(int argc, const char **argv, const char *prefix)
>>  {
>> -	struct option *builtin_commit_graph_options = common_opts;
>> +	parse_opt_subcommand_fn *fn = NULL;
>> +	struct option builtin_commit_graph_options[] = {
>> +		OPT_SUBCOMMAND("verify", &fn, graph_verify),
>> +		OPT_SUBCOMMAND("write", &fn, graph_write),
>> +		OPT_END(),
>> +	};
>> +	struct option *options = parse_options_concat(builtin_commit_graph_options, common_opts);
>
> This looks like it'll leak if...
>
>>  
>>  	git_config(git_default_config, NULL);
>> -	argc = parse_options(argc, argv, prefix,
>> -			     builtin_commit_graph_options,
>> -			     builtin_commit_graph_usage,
>> -			     PARSE_OPT_STOP_AT_NON_OPTION);
>> -	if (!argc)
>> -		goto usage;
>
> We take this goto..

Sorry, I'm being an idiot and misreading this, the "goto" isn't here
anymore, du'h!

>> +	argc = parse_options(argc, argv, prefix, options,
>> +			     builtin_commit_graph_usage, 0);
>> +	FREE_AND_NULL(options);
>
> Why FREE_AND_NULL() over free()?

But this question still stands :)

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 14/20] builtin/multi-pack-index.c: let parse-options parse subcommands
  2022-08-19 16:04   ` [PATCH v2 14/20] builtin/multi-pack-index.c: " SZEDER Gábor
@ 2022-08-19 17:57     ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 17:57 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> +	argc = parse_options(argc, argv, prefix, options,
> +			     builtin_multi_pack_index_usage, 0);
> +	FREE_AND_NULL(options);

Ditto s/FREE_AND_NULL/free/g per a previous patch's comment.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 15/20] builtin/notes.c: let parse-options parse subcommands
  2022-08-19 16:04   ` [PATCH v2 15/20] builtin/notes.c: " SZEDER Gábor
@ 2022-08-19 18:01     ` Ævar Arnfjörð Bjarmason
  2022-08-21 17:56       ` SZEDER Gábor
  0 siblings, 1 reply; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 18:01 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> -	int result;
>  	const char *override_notes_ref = NULL;
> +	parse_opt_subcommand_fn *fn = list;
>  	struct option options[] = {
>  		OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"),
>  			   N_("use notes from <notes-ref>")),
> +		OPT_SUBCOMMAND("list", &fn, list),
> +		OPT_SUBCOMMAND("add", &fn, add),
> +		OPT_SUBCOMMAND("copy", &fn, copy),
> +		OPT_SUBCOMMAND("append", &fn, append_edit),
> +		OPT_SUBCOMMAND("edit", &fn, append_edit),
> +		OPT_SUBCOMMAND("show", &fn, show),
> +		OPT_SUBCOMMAND("merge", &fn, merge),
> +		OPT_SUBCOMMAND("remove", &fn, remove_cmd),
> +		OPT_SUBCOMMAND("prune", &fn, prune),
> +		OPT_SUBCOMMAND("get-ref", &fn, get_ref),
>  		OPT_END()
>  	};
>  
>  	git_config(git_default_config, NULL);
>  	argc = parse_options(argc, argv, prefix, options, git_notes_usage,
> -			     PARSE_OPT_STOP_AT_NON_OPTION);
> +			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
> +	if (fn == list && argc && strcmp(argv[0], "list")) {
> +		error(_("unknown subcommand: %s"), argv[0]);
> +		usage_with_options(git_notes_usage, options);
> +	}

I wanted to ask why the API can't smartly handle this, but your "Found
an unknown option given to a command with" comment in an earlier patch
answered it.

I think something in this direction would be a bit more readble/obvious,
as it avoids hardcoding "list":
	
	diff --git a/builtin/notes.c b/builtin/notes.c
	index 42cbae46598..43d59b1a98e 100644
	--- a/builtin/notes.c
	+++ b/builtin/notes.c
	@@ -995,7 +995,7 @@ static int get_ref(int argc, const char **argv, const char *prefix)
	 int cmd_notes(int argc, const char **argv, const char *prefix)
	 {
	 	const char *override_notes_ref = NULL;
	-	parse_opt_subcommand_fn *fn = list;
	+	parse_opt_subcommand_fn *fn = NULL;
	 	struct option options[] = {
	 		OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"),
	 			   N_("use notes from <notes-ref>")),
	@@ -1015,10 +1015,11 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
	 	git_config(git_default_config, NULL);
	 	argc = parse_options(argc, argv, prefix, options, git_notes_usage,
	 			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
	-	if (fn == list && argc && strcmp(argv[0], "list")) {
	-		error(_("unknown subcommand: %s"), argv[0]);
	-		usage_with_options(git_notes_usage, options);
	-	}
	+	if (!fn && argc)
	+		usage_msg_optf(_("unknown subcommand: %s"),
	+			       git_notes_usage, options, argv[0]);
	+	else if (!fn)
	+		fn = list;
	 
	 	if (override_notes_ref) {
	 		struct strbuf sb = STRBUF_INIT;

I.e. we rely on the API setting it to non-NULL if it finds a subcommand,
otherwise we just set it to "list" after checking whether we have excess
arguments.

> [...]
> -	else if (!strcmp(argv[0], "get-ref"))
> -		result = get_ref(argc, argv, prefix);
> -	else {
> -		result = error(_("unknown subcommand: %s"), argv[0]);
> -		usage_with_options(git_notes_usage, options);
> -	}
> -
> -	return result ? 1 : 0;
> +	return !!fn(argc, argv, prefix);
>  }

In any case this is a lot nicer, ditto previous comment about maybe
skipping the refactoring of this end code, but I'm also fine with
keeping it.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 16/20] builtin/reflog.c: let parse-options parse subcommands
  2022-08-19 16:04   ` [PATCH v2 16/20] builtin/reflog.c: " SZEDER Gábor
@ 2022-08-19 18:08     ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 18:08 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> +	parse_opt_subcommand_fn *fn = NULL;

Re the comment on notes.c this is a bit like that pattern...

> -log_reflog:
> -	return cmd_log_reflog(argc, argv, prefix);
> +			     PARSE_OPT_KEEP_UNKNOWN_OPT);
> +	if (fn)
> +		return fn(argc - 1, argv + 1, prefix);
> +	else
> +		return cmd_log_reflog(argc, argv, prefix);
>  }

Maybe more obvious (untested):

	if (!fn) {
		argc--;
		argv++;
		fn = cmd_log_reflog;
	}
	return fn(argc, argv, prefix);


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-08-19 16:03   ` [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
  2022-08-19 17:23     ` Ævar Arnfjörð Bjarmason
@ 2022-08-19 18:18     ` Junio C Hamano
  2022-08-20 10:31       ` SZEDER Gábor
  1 sibling, 1 reply; 97+ messages in thread
From: Junio C Hamano @ 2022-08-19 18:18 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Ævar Arnfjörð Bjarmason

SZEDER Gábor <szeder.dev@gmail.com> writes:

> +static void print_args(int argc, const char **argv)
> +{
> +	for (int i = 0; i < argc; i++)
> +		printf("arg %02d: %s\n", i, argv[i]);
> +}

It is not November 2022 yet (cf. Documentation/CodingGuidelines).

Curious why "%02d", not "%d", or autoscale for cases where argc is
larger than 99, but I'll let it pass (iow no need to reroll only to
"fix" it).

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 11/20] builtin/commit-graph.c: let parse-options parse subcommands
  2022-08-19 17:53     ` Ævar Arnfjörð Bjarmason
  2022-08-19 17:56       ` Ævar Arnfjörð Bjarmason
@ 2022-08-19 18:22       ` SZEDER Gábor
  1 sibling, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 18:22 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Junio C Hamano

On Fri, Aug 19, 2022 at 07:53:08PM +0200, Ævar Arnfjörð Bjarmason wrote:
> > +	argc = parse_options(argc, argv, prefix, options,
> > +			     builtin_commit_graph_usage, 0);
> > +	FREE_AND_NULL(options);
> 
> Why FREE_AND_NULL() over free()?

Heh, it's interesting that you should ask that ;)

I followed the existing pattern for now, just like you did in
84e4484f12 (commit-graph: use parse_options_concat(), 2021-08-23),
where you made graph_verify() and graph_write() use FREE_AND_NULL() to
release their concatenated options arrays.

But yeah, a plain free() should work just fine in these cases.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output
  2022-08-19 17:30     ` Ævar Arnfjörð Bjarmason
@ 2022-08-19 18:35       ` SZEDER Gábor
  0 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-19 18:35 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Junio C Hamano

On Fri, Aug 19, 2022 at 07:30:13PM +0200, Ævar Arnfjörð Bjarmason wrote:
> 
> On Fri, Aug 19 2022, SZEDER Gábor wrote:
> 
> > subcommands) a tad more complex, so I wanted to test the result.  The
> > test would have to account for the presence of that leading space,
> > which bugged my OCD, so let's get rid of it.
> 
> On the subject of OCD it...
> 
> > Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> > ---
> >  parse-options.c | 3 ++-
> >  1 file changed, 2 insertions(+), 1 deletion(-)
> >
> > diff --git a/parse-options.c b/parse-options.c
> > index a0a2cf98fa..8748f88e6f 100644
> > --- a/parse-options.c
> > +++ b/parse-options.c
> > @@ -620,7 +620,8 @@ static int show_gitcomp(const struct option *opts, int show_all)
> >  			suffix = "=";
> >  		if (starts_with(opts->long_name, "no-"))
> >  			nr_noopts++;
> > -		printf(" --%s%s", opts->long_name, suffix);
> > +		printf("%s--%s%s", opts == original_opts ? "" : " ",
> > +		       opts->long_name, suffix);
> >  	}
> >  	show_negated_gitcomp(original_opts, show_all, -1);
> >  	show_negated_gitcomp(original_opts, show_all, nr_noopts);
> 
> ...bugs me a bit that we have a "suffix" variable, but not a "prefix"
> for this

That space acts as a separator, so calling it prefix would be
misleading.


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 09/20] parse-options: add support for parsing subcommands
  2022-08-19 16:04   ` [PATCH v2 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
  2022-08-19 17:33     ` Ævar Arnfjörð Bjarmason
@ 2022-08-19 19:03     ` Ævar Arnfjörð Bjarmason
  1 sibling, 0 replies; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 19:03 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> @@ -885,7 +977,14 @@ int parse_options(int argc, const char **argv,
>  	case PARSE_OPT_COMPLETE:
>  		exit(0);
>  	case PARSE_OPT_NON_OPTION:
> +	case PARSE_OPT_SUBCOMMAND:
> +		break;

This amounts to the same behavior, because...

>  	case PARSE_OPT_DONE:
> +		if (ctx.has_subcommands &&
> +		    !(flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) {
> +			error(_("need a subcommand"));
> +			usage_with_options(usagestr, options);
> +		}
>  		break;

...AFAICT we won't have PARSE_OPT_NON_OPTION *and* ctx.has_subcommands,
but it's really confusing that before we'd fall through from
PARSE_OPT_NON_OPTION to PARSE_OPT_DONE, but now we don't.'


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 19/20] builtin/stash.c: let parse-options parse subcommands
  2022-08-19 16:04   ` [PATCH v2 19/20] builtin/stash.c: " SZEDER Gábor
@ 2022-08-19 19:06     ` Ævar Arnfjörð Bjarmason
  2022-08-20 10:27       ` SZEDER Gábor
  0 siblings, 1 reply; 97+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-19 19:06 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Junio C Hamano


On Fri, Aug 19 2022, SZEDER Gábor wrote:

> 'git stash' parses its subcommands with a long list of if-else if
> statements.  parse-options has just learned to parse subcommands, so
> let's use that facility instead, with the benefits of shorter code,
> and listing subcommands for Bash completion.
>
> Note that the push_stash() function implementing the 'push' subcommand
> accepts an extra flag parameter to indicate whether push was assumed,
> so add a wrapper function with the standard subcommand function
> signature.
>
> Note also that this change "hides" the '-h' option in 'git stash push
> -h' from the parse_option() call in cmd_stash(), as it comes after the
> subcommand.  Consequently, from now on it will emit the usage of the
> 'push' subcommand instead of the usage of 'git stash'.  We had a
> failing test for this case, which can now be flipped to expect
> success.
>
> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> ---
>  builtin/stash.c  | 53 ++++++++++++++++++++++--------------------------
>  t/t3903-stash.sh |  2 +-
>  2 files changed, 25 insertions(+), 30 deletions(-)
>
> diff --git a/builtin/stash.c b/builtin/stash.c
> index a14e832e9f..1ba24c1173 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
> @@ -1739,6 +1739,11 @@ static int push_stash(int argc, const char **argv, const char *prefix,
>  			     include_untracked, only_staged);
>  }
>  
> +static int push_stash_unassumed(int argc, const char **argv, const char *prefix)
> +{
> +	return push_stash(argc, argv, prefix, 0);
> +}
> +
>  static int save_stash(int argc, const char **argv, const char *prefix)
>  {
>  	int keep_index = -1;
> @@ -1787,15 +1792,28 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
>  	pid_t pid = getpid();
>  	const char *index_file;
>  	struct strvec args = STRVEC_INIT;
> -
> +	parse_opt_subcommand_fn *fn = NULL;
>  	struct option options[] = {
> +		OPT_SUBCOMMAND("apply", &fn, apply_stash),
> +		OPT_SUBCOMMAND("clear", &fn, clear_stash),
> +		OPT_SUBCOMMAND("drop", &fn, drop_stash),
> +		OPT_SUBCOMMAND("pop", &fn, pop_stash),
> +		OPT_SUBCOMMAND("branch", &fn, branch_stash),
> +		OPT_SUBCOMMAND("list", &fn, list_stash),
> +		OPT_SUBCOMMAND("show", &fn, show_stash),
> +		OPT_SUBCOMMAND("store", &fn, store_stash),
> +		OPT_SUBCOMMAND("create", &fn, create_stash),
> +		OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
> +		OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
>  		OPT_END()
>  	};
>  
>  	git_config(git_stash_config, NULL);
>  
>  	argc = parse_options(argc, argv, prefix, options, git_stash_usage,
> -			     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);
> +			     PARSE_OPT_SUBCOMMAND_OPTIONAL |
> +			     PARSE_OPT_KEEP_UNKNOWN_OPT |
> +			     PARSE_OPT_KEEP_DASHDASH);
>  
>  	prepare_repo_settings(the_repository);
>  	the_repository->settings.command_requires_full_index = 0;
> @@ -1804,33 +1822,10 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
>  	strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
>  		    (uintmax_t)pid);
>  
> -	if (!argc)
> -		return !!push_stash(0, NULL, prefix, 0);
> -	else if (!strcmp(argv[0], "apply"))
> -		return !!apply_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "clear"))
> -		return !!clear_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "drop"))
> -		return !!drop_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "pop"))
> -		return !!pop_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "branch"))
> -		return !!branch_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "list"))
> -		return !!list_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "show"))
> -		return !!show_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "store"))
> -		return !!store_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "create"))
> -		return !!create_stash(argc, argv, prefix);
> -	else if (!strcmp(argv[0], "push"))
> -		return !!push_stash(argc, argv, prefix, 0);
> -	else if (!strcmp(argv[0], "save"))
> -		return !!save_stash(argc, argv, prefix);
> -	else if (*argv[0] != '-')
> -		usage_msg_optf(_("unknown subcommand: %s"),
> -			       git_stash_usage, options, argv[0]);
> +	if (fn)
> +		return !!fn(argc, argv, prefix);
> +	else if (!argc)
> +		return !!push_stash_unassumed(0, NULL, prefix);
>  
>  	/* Assume 'stash push' */
>  	strvec_push(&args, "push");
> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> index 2a4c3fd61c..376cc8f4ab 100755
> --- a/t/t3903-stash.sh
> +++ b/t/t3903-stash.sh
> @@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '
>  	grep -F "or: git stash show" usage
>  '
>  
> -test_expect_failure 'usage for subcommands should emit subcommand usage' '
> +test_expect_success 'usage for subcommands should emit subcommand usage' '
>  	test_expect_code 129 git stash push -h >usage &&
>  	grep -F "usage: git stash [push" usage
>  '

This isn't on you, but I found the control flow here really confusing. I
wonder if we could this cleanup before/squashed in. I may have missed
some cases, but it passes all tests.

I.e. the whole business of pushing this "did we assume?" around goes
away if we simply pass the command-line as-is to push_stash(), and ask
it to determine this.

diff --git a/builtin/stash.c b/builtin/stash.c
index 1ba24c11737..a588389d66b 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1665,10 +1665,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
 	return ret;
 }
 
-static int push_stash(int argc, const char **argv, const char *prefix,
-		      int push_assumed)
+static int push_stash(int argc, const char **argv, const char *prefix)
 {
-	int force_assume = 0;
 	int keep_index = -1;
 	int only_staged = 0;
 	int patch_mode = 0;
@@ -1696,20 +1694,22 @@ static int push_stash(int argc, const char **argv, const char *prefix,
 		OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),
 		OPT_END()
 	};
+	int push_assumed = argc == 1 || strcmp(argv[1], "push");
 
-	if (argc) {
-		force_assume = !strcmp(argv[0], "-p");
-		argc = parse_options(argc, argv, prefix, options,
-				     push_assumed ? git_stash_usage :
-				     git_stash_push_usage,
-				     PARSE_OPT_KEEP_DASHDASH);
+	if (!push_assumed) {
+		argc--;
+		argv++;
 	}
 
+	argc = parse_options(argc, argv, prefix, options,
+			     push_assumed ? git_stash_usage :
+			     git_stash_push_usage,
+			     PARSE_OPT_KEEP_DASHDASH);
 	if (argc) {
 		if (!strcmp(argv[0], "--")) {
 			argc--;
 			argv++;
-		} else if (push_assumed && !force_assume) {
+		} else if (push_assumed) {
 			die("subcommand wasn't specified; 'push' can't be assumed due to unexpected token '%s'",
 			    argv[0]);
 		}
@@ -1739,11 +1739,6 @@ static int push_stash(int argc, const char **argv, const char *prefix,
 			     include_untracked, only_staged);
 }
 
-static int push_stash_unassumed(int argc, const char **argv, const char *prefix)
-{
-	return push_stash(argc, argv, prefix, 0);
-}
-
 static int save_stash(int argc, const char **argv, const char *prefix)
 {
 	int keep_index = -1;
@@ -1791,8 +1786,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 {
 	pid_t pid = getpid();
 	const char *index_file;
-	struct strvec args = STRVEC_INIT;
-	parse_opt_subcommand_fn *fn = NULL;
+	parse_opt_subcommand_fn *fn = push_stash;
 	struct option options[] = {
 		OPT_SUBCOMMAND("apply", &fn, apply_stash),
 		OPT_SUBCOMMAND("clear", &fn, clear_stash),
@@ -1803,7 +1797,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 		OPT_SUBCOMMAND("show", &fn, show_stash),
 		OPT_SUBCOMMAND("store", &fn, store_stash),
 		OPT_SUBCOMMAND("create", &fn, create_stash),
-		OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
+		OPT_SUBCOMMAND("push", &fn, push_stash),
 		OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
 		OPT_END()
 	};
@@ -1811,6 +1805,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 	git_config(git_stash_config, NULL);
 
 	argc = parse_options(argc, argv, prefix, options, git_stash_usage,
+			     PARSE_OPT_KEEP_ARGV0 |
 			     PARSE_OPT_SUBCOMMAND_OPTIONAL |
 			     PARSE_OPT_KEEP_UNKNOWN_OPT |
 			     PARSE_OPT_KEEP_DASHDASH);
@@ -1821,14 +1816,11 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
 	index_file = get_index_file();
 	strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
 		    (uintmax_t)pid);
+	
+	if (fn != push_stash) {
+		argc--;
+		argv++;
+	}
 
-	if (fn)
-		return !!fn(argc, argv, prefix);
-	else if (!argc)
-		return !!push_stash_unassumed(0, NULL, prefix);
-
-	/* Assume 'stash push' */
-	strvec_push(&args, "push");
-	strvec_pushv(&args, argv);
-	return !!push_stash(args.nr, args.v, prefix, 1);
+	return !!fn(argc, argv, prefix);
 }

^ permalink raw reply related	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands
  2022-08-19 16:04   ` [PATCH v2 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands SZEDER Gábor
@ 2022-08-19 20:59     ` Junio C Hamano
  0 siblings, 0 replies; 97+ messages in thread
From: Junio C Hamano @ 2022-08-19 20:59 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Ævar Arnfjörð Bjarmason

SZEDER Gábor <szeder.dev@gmail.com> writes:

> 'git maintenanze' parses its subcommands with a couple of if

nanze?

> statements.  parse-options has just learned to parse subcommands, so
> let's use that facility instead, with the benefits of shorter code,
> handling missing or unknown subcommands, and listing subcommands for
> Bash completion.
>
> This change makes 'git maintenance' consistent with other commands in
> that the help text shown for '-h' goes to standard output, not error,
> in the exit code and error message on unknown subcommand, and the
> error message on missing subcommand.  There is a test checking these,
> which is now updated accordingly.
>
> Note that some of the functions implementing each subcommand don't
> accept any parameters, so add the (unused) 'argc', '**argv' and
> '*prefix' parameters to make them match the type expected by
> parse-options, and thus avoid casting function pointers.

OK.  When the recently posted "unused annotation" and merges down
together with this topic, we would want to add UNUSED() annotation
to them so that we can get closer to be able to compile with -Wunused
warning enabled, but we do not have to worry about it yet.

> +	struct option builtin_maintenance_options[] = {
> +		OPT_SUBCOMMAND("run", &fn, maintenance_run),
> +		OPT_SUBCOMMAND("start", &fn, maintenance_start),
> +		OPT_SUBCOMMAND("stop", &fn, maintenance_stop),
> +		OPT_SUBCOMMAND("register", &fn, maintenance_register),
> +		OPT_SUBCOMMAND("unregister", &fn, maintenance_unregister),
> +		OPT_END(),
> +	};
> +
> +	argc = parse_options(argc, argv, prefix, builtin_maintenance_options,
> +			     builtin_maintenance_usage, 0);
> +	return fn(argc, argv, prefix);

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 19/20] builtin/stash.c: let parse-options parse subcommands
  2022-08-19 19:06     ` Ævar Arnfjörð Bjarmason
@ 2022-08-20 10:27       ` SZEDER Gábor
  0 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-20 10:27 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Junio C Hamano

On Fri, Aug 19, 2022 at 09:06:32PM +0200, Ævar Arnfjörð Bjarmason wrote:
> 
> On Fri, Aug 19 2022, SZEDER Gábor wrote:
> 
> > 'git stash' parses its subcommands with a long list of if-else if
> > statements.  parse-options has just learned to parse subcommands, so
> > let's use that facility instead, with the benefits of shorter code,
> > and listing subcommands for Bash completion.
> >
> > Note that the push_stash() function implementing the 'push' subcommand
> > accepts an extra flag parameter to indicate whether push was assumed,
> > so add a wrapper function with the standard subcommand function
> > signature.
> >
> > Note also that this change "hides" the '-h' option in 'git stash push
> > -h' from the parse_option() call in cmd_stash(), as it comes after the
> > subcommand.  Consequently, from now on it will emit the usage of the
> > 'push' subcommand instead of the usage of 'git stash'.  We had a
> > failing test for this case, which can now be flipped to expect
> > success.
> >
> > Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> > ---
> >  builtin/stash.c  | 53 ++++++++++++++++++++++--------------------------
> >  t/t3903-stash.sh |  2 +-
> >  2 files changed, 25 insertions(+), 30 deletions(-)
> >
> > diff --git a/builtin/stash.c b/builtin/stash.c
> > index a14e832e9f..1ba24c1173 100644
> > --- a/builtin/stash.c
> > +++ b/builtin/stash.c
> > @@ -1739,6 +1739,11 @@ static int push_stash(int argc, const char **argv, const char *prefix,
> >  			     include_untracked, only_staged);
> >  }
> >  
> > +static int push_stash_unassumed(int argc, const char **argv, const char *prefix)
> > +{
> > +	return push_stash(argc, argv, prefix, 0);
> > +}
> > +
> >  static int save_stash(int argc, const char **argv, const char *prefix)
> >  {
> >  	int keep_index = -1;
> > @@ -1787,15 +1792,28 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
> >  	pid_t pid = getpid();
> >  	const char *index_file;
> >  	struct strvec args = STRVEC_INIT;
> > -
> > +	parse_opt_subcommand_fn *fn = NULL;
> >  	struct option options[] = {
> > +		OPT_SUBCOMMAND("apply", &fn, apply_stash),
> > +		OPT_SUBCOMMAND("clear", &fn, clear_stash),
> > +		OPT_SUBCOMMAND("drop", &fn, drop_stash),
> > +		OPT_SUBCOMMAND("pop", &fn, pop_stash),
> > +		OPT_SUBCOMMAND("branch", &fn, branch_stash),
> > +		OPT_SUBCOMMAND("list", &fn, list_stash),
> > +		OPT_SUBCOMMAND("show", &fn, show_stash),
> > +		OPT_SUBCOMMAND("store", &fn, store_stash),
> > +		OPT_SUBCOMMAND("create", &fn, create_stash),
> > +		OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
> > +		OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
> >  		OPT_END()
> >  	};
> >  
> >  	git_config(git_stash_config, NULL);
> >  
> >  	argc = parse_options(argc, argv, prefix, options, git_stash_usage,
> > -			     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_DASHDASH);
> > +			     PARSE_OPT_SUBCOMMAND_OPTIONAL |
> > +			     PARSE_OPT_KEEP_UNKNOWN_OPT |
> > +			     PARSE_OPT_KEEP_DASHDASH);
> >  
> >  	prepare_repo_settings(the_repository);
> >  	the_repository->settings.command_requires_full_index = 0;
> > @@ -1804,33 +1822,10 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
> >  	strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
> >  		    (uintmax_t)pid);
> >  
> > -	if (!argc)
> > -		return !!push_stash(0, NULL, prefix, 0);
> > -	else if (!strcmp(argv[0], "apply"))
> > -		return !!apply_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "clear"))
> > -		return !!clear_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "drop"))
> > -		return !!drop_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "pop"))
> > -		return !!pop_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "branch"))
> > -		return !!branch_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "list"))
> > -		return !!list_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "show"))
> > -		return !!show_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "store"))
> > -		return !!store_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "create"))
> > -		return !!create_stash(argc, argv, prefix);
> > -	else if (!strcmp(argv[0], "push"))
> > -		return !!push_stash(argc, argv, prefix, 0);
> > -	else if (!strcmp(argv[0], "save"))
> > -		return !!save_stash(argc, argv, prefix);
> > -	else if (*argv[0] != '-')
> > -		usage_msg_optf(_("unknown subcommand: %s"),
> > -			       git_stash_usage, options, argv[0]);
> > +	if (fn)
> > +		return !!fn(argc, argv, prefix);
> > +	else if (!argc)
> > +		return !!push_stash_unassumed(0, NULL, prefix);
> >  
> >  	/* Assume 'stash push' */
> >  	strvec_push(&args, "push");
> > diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> > index 2a4c3fd61c..376cc8f4ab 100755
> > --- a/t/t3903-stash.sh
> > +++ b/t/t3903-stash.sh
> > @@ -25,7 +25,7 @@ test_expect_success 'usage on main command -h emits a summary of subcommands' '
> >  	grep -F "or: git stash show" usage
> >  '
> >  
> > -test_expect_failure 'usage for subcommands should emit subcommand usage' '
> > +test_expect_success 'usage for subcommands should emit subcommand usage' '
> >  	test_expect_code 129 git stash push -h >usage &&
> >  	grep -F "usage: git stash [push" usage
> >  '
> 
> This isn't on you, but I found the control flow here really confusing. I
> wonder if we could this cleanup before/squashed in. I may have missed
> some cases, but it passes all tests.

I think it would be better to leave this for later.  I did notice that
something is fishy around this 'force_assume' flag (argv[0] can never
possibly be "-p") added in 8c3713cede (stash: eliminate crude option
parsing, 2020-02-17), and even after your patch some of that fishiness
remains (e.g. argv[0] can't possibly be "--" either).  Unfortunately,
the commit message doesn't explains what was intended, and I couldn't
spend time on figuring it out, either.


> I.e. the whole business of pushing this "did we assume?" around goes
> away if we simply pass the command-line as-is to push_stash(), and ask
> it to determine this.
> 
> diff --git a/builtin/stash.c b/builtin/stash.c
> index 1ba24c11737..a588389d66b 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
> @@ -1665,10 +1665,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
>  	return ret;
>  }
>  
> -static int push_stash(int argc, const char **argv, const char *prefix,
> -		      int push_assumed)
> +static int push_stash(int argc, const char **argv, const char *prefix)
>  {
> -	int force_assume = 0;
>  	int keep_index = -1;
>  	int only_staged = 0;
>  	int patch_mode = 0;
> @@ -1696,20 +1694,22 @@ static int push_stash(int argc, const char **argv, const char *prefix,
>  		OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),
>  		OPT_END()
>  	};
> +	int push_assumed = argc == 1 || strcmp(argv[1], "push");
>  
> -	if (argc) {
> -		force_assume = !strcmp(argv[0], "-p");
> -		argc = parse_options(argc, argv, prefix, options,
> -				     push_assumed ? git_stash_usage :
> -				     git_stash_push_usage,
> -				     PARSE_OPT_KEEP_DASHDASH);
> +	if (!push_assumed) {
> +		argc--;
> +		argv++;
>  	}
>  
> +	argc = parse_options(argc, argv, prefix, options,
> +			     push_assumed ? git_stash_usage :
> +			     git_stash_push_usage,
> +			     PARSE_OPT_KEEP_DASHDASH);
>  	if (argc) {
>  		if (!strcmp(argv[0], "--")) {
>  			argc--;
>  			argv++;
> -		} else if (push_assumed && !force_assume) {
> +		} else if (push_assumed) {
>  			die("subcommand wasn't specified; 'push' can't be assumed due to unexpected token '%s'",
>  			    argv[0]);
>  		}
> @@ -1739,11 +1739,6 @@ static int push_stash(int argc, const char **argv, const char *prefix,
>  			     include_untracked, only_staged);
>  }
>  
> -static int push_stash_unassumed(int argc, const char **argv, const char *prefix)
> -{
> -	return push_stash(argc, argv, prefix, 0);
> -}
> -
>  static int save_stash(int argc, const char **argv, const char *prefix)
>  {
>  	int keep_index = -1;
> @@ -1791,8 +1786,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
>  {
>  	pid_t pid = getpid();
>  	const char *index_file;
> -	struct strvec args = STRVEC_INIT;
> -	parse_opt_subcommand_fn *fn = NULL;
> +	parse_opt_subcommand_fn *fn = push_stash;
>  	struct option options[] = {
>  		OPT_SUBCOMMAND("apply", &fn, apply_stash),
>  		OPT_SUBCOMMAND("clear", &fn, clear_stash),
> @@ -1803,7 +1797,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
>  		OPT_SUBCOMMAND("show", &fn, show_stash),
>  		OPT_SUBCOMMAND("store", &fn, store_stash),
>  		OPT_SUBCOMMAND("create", &fn, create_stash),
> -		OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
> +		OPT_SUBCOMMAND("push", &fn, push_stash),
>  		OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
>  		OPT_END()
>  	};
> @@ -1811,6 +1805,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
>  	git_config(git_stash_config, NULL);
>  
>  	argc = parse_options(argc, argv, prefix, options, git_stash_usage,
> +			     PARSE_OPT_KEEP_ARGV0 |
>  			     PARSE_OPT_SUBCOMMAND_OPTIONAL |
>  			     PARSE_OPT_KEEP_UNKNOWN_OPT |
>  			     PARSE_OPT_KEEP_DASHDASH);
> @@ -1821,14 +1816,11 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
>  	index_file = get_index_file();
>  	strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
>  		    (uintmax_t)pid);
> +	
> +	if (fn != push_stash) {
> +		argc--;
> +		argv++;
> +	}
>  
> -	if (fn)
> -		return !!fn(argc, argv, prefix);
> -	else if (!argc)
> -		return !!push_stash_unassumed(0, NULL, prefix);
> -
> -	/* Assume 'stash push' */
> -	strvec_push(&args, "push");
> -	strvec_pushv(&args, argv);
> -	return !!push_stash(args.nr, args.v, prefix, 1);
> +	return !!fn(argc, argv, prefix);
>  }

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-08-19 18:18     ` Junio C Hamano
@ 2022-08-20 10:31       ` SZEDER Gábor
  2022-08-20 21:27         ` Junio C Hamano
  0 siblings, 1 reply; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-20 10:31 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Ævar Arnfjörð Bjarmason

On Fri, Aug 19, 2022 at 11:18:20AM -0700, Junio C Hamano wrote:
> SZEDER Gábor <szeder.dev@gmail.com> writes:
> 
> > +static void print_args(int argc, const char **argv)
> > +{
> > +	for (int i = 0; i < argc; i++)
> > +		printf("arg %02d: %s\n", i, argv[i]);
> > +}
> 
> It is not November 2022 yet (cf. Documentation/CodingGuidelines).

Oh, I've misunderstood Ævar's remarks about this in the previous
round, and thought it's fair game.

> Curious why "%02d", not "%d", or autoscale for cases where argc is
> larger than 99, but I'll let it pass (iow no need to reroll only to
> "fix" it).

It doesn't matter for these tests, but 'test-tool parse-options' uses
the same format to print args:

  $ ./t/helper/test-tool parse-options foo bar baz |tail -n3
  arg 00: foo
  arg 01: bar
  arg 02: baz


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-08-19 17:23     ` Ævar Arnfjörð Bjarmason
@ 2022-08-20 11:14       ` SZEDER Gábor
  0 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-20 11:14 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Junio C Hamano

On Fri, Aug 19, 2022 at 07:23:44PM +0200, Ævar Arnfjörð Bjarmason wrote:
> > +		error("'cmd' is mandatory");
> > +		usage_with_options(usage, test_flag_options);
> 
> I think you want usage_msg_opt() instead.

As I responded to a similar remark in the previous round,
parse-options uses the "error:" prefix in its error messages:

  $ ./t/helper/test-tool parse-options -U
  error: unknown switch `U'
  $ ./t/helper/test-tool parse-options --unknown
  error: unknown option `unknown'
  $ ./t/helper/test-tool parse-options -i
  error: switch `i' requires a value
  $ ./t/helper/test-tool parse-options --int
  error: option `integer' requires a value
  $ ./t/helper/test-tool parse-options -i foo
  error: switch `i' expects a numerical value
  $ ./t/helper/test-tool parse-options --int foo
  error: option `integer' expects a numerical value
  $ ./t/helper/test-tool parse-options --quiet=42
  error: option `quiet' takes no value
  $ ./t/helper/test-tool parse-options --mode1 --mode2
  error: option `mode2' is incompatible with --mode1

Subcommand-related error messages should be consistent with these, and
use the "error:" prefix as well.  Unfortunately, both usage_msg_opt()
and usage_msg_optf() use the "fatal:" prefix instead, so I will not
use those functions anywhere in this patch series.


> > +test_expect_success 'NO_INTERNAL_HELP works for -h' '
> > +	test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&
> > +	cat err &&
> 
> Stray "cat", presumably in error..

Leftover debugging :(


^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags'
  2022-08-20 10:31       ` SZEDER Gábor
@ 2022-08-20 21:27         ` Junio C Hamano
  0 siblings, 0 replies; 97+ messages in thread
From: Junio C Hamano @ 2022-08-20 21:27 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Ævar Arnfjörð Bjarmason

SZEDER Gábor <szeder.dev@gmail.com> writes:

> On Fri, Aug 19, 2022 at 11:18:20AM -0700, Junio C Hamano wrote:
>> SZEDER Gábor <szeder.dev@gmail.com> writes:
>> 
>> > +static void print_args(int argc, const char **argv)
>> > +{
>> > +	for (int i = 0; i < argc; i++)
>> > +		printf("arg %02d: %s\n", i, argv[i]);
>> > +}
>> 
>> It is not November 2022 yet (cf. Documentation/CodingGuidelines).
>
> Oh, I've misunderstood Ævar's remarks about this in the previous
> round, and thought it's fair game.

If we make it a "fair game", when we find a platform that has
problems with the syntax, we will have to find them and fix up many
places.  At least the number of the ones we let in by mistake are
small and known, it may be still be manageable.

It is how to be conservative.

> It doesn't matter for these tests, but 'test-tool parse-options' uses
> the same format to print args:

As long as 99 is enough for us, I do not very much care.  I just
noticed an attempt to align that does not do a thorough job at it,
and found it strange, that's all.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH v2 15/20] builtin/notes.c: let parse-options parse subcommands
  2022-08-19 18:01     ` Ævar Arnfjörð Bjarmason
@ 2022-08-21 17:56       ` SZEDER Gábor
  0 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-08-21 17:56 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Junio C Hamano

On Fri, Aug 19, 2022 at 08:01:55PM +0200, Ævar Arnfjörð Bjarmason wrote:
> 
> On Fri, Aug 19 2022, SZEDER Gábor wrote:
> 
> > -	int result;
> >  	const char *override_notes_ref = NULL;
> > +	parse_opt_subcommand_fn *fn = list;
> >  	struct option options[] = {
> >  		OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"),
> >  			   N_("use notes from <notes-ref>")),
> > +		OPT_SUBCOMMAND("list", &fn, list),
> > +		OPT_SUBCOMMAND("add", &fn, add),
> > +		OPT_SUBCOMMAND("copy", &fn, copy),
> > +		OPT_SUBCOMMAND("append", &fn, append_edit),
> > +		OPT_SUBCOMMAND("edit", &fn, append_edit),
> > +		OPT_SUBCOMMAND("show", &fn, show),
> > +		OPT_SUBCOMMAND("merge", &fn, merge),
> > +		OPT_SUBCOMMAND("remove", &fn, remove_cmd),
> > +		OPT_SUBCOMMAND("prune", &fn, prune),
> > +		OPT_SUBCOMMAND("get-ref", &fn, get_ref),
> >  		OPT_END()
> >  	};
> >  
> >  	git_config(git_default_config, NULL);
> >  	argc = parse_options(argc, argv, prefix, options, git_notes_usage,
> > -			     PARSE_OPT_STOP_AT_NON_OPTION);
> > +			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
> > +	if (fn == list && argc && strcmp(argv[0], "list")) {
> > +		error(_("unknown subcommand: %s"), argv[0]);

This should have been `%s' here, and in cmd_remote() as well.

> > +		usage_with_options(git_notes_usage, options);
> > +	}
> 
> I wanted to ask why the API can't smartly handle this, but your "Found
> an unknown option given to a command with" comment in an earlier patch
> answered it.

It's not about unknown options but rather about (non-option) arguments.

'git notes list' doesn't accept any --options, and since this 'list'
is the default operation mode, parse_options() is invoked without the
PARSE_OPT_KEEP_UNKNOWN_OPT flag, so 'git notes --foo' errors out even
without any of the extra checks in the above hunk.

However, while 'git notes list' does accept non-option arguments
(objects or refs), 'git notes' does not.  Alas, currently there is no
way to tell parse_options() to error out upon finding a (non-option
and non-subcommand) argument, it always keeps them in 'argv'; that's
why we need these additional checks.

Now, while we could add such a flag, of course, it would not be
limited to this one particular use case, so when the error is
triggered inside parse_options() I doubt that we could have this
specific "unknown subcommand" error message.


> I think something in this direction would be a bit more readble/obvious,
> as it avoids hardcoding "list":
> 	
> 	diff --git a/builtin/notes.c b/builtin/notes.c
> 	index 42cbae46598..43d59b1a98e 100644
> 	--- a/builtin/notes.c
> 	+++ b/builtin/notes.c
> 	@@ -995,7 +995,7 @@ static int get_ref(int argc, const char **argv, const char *prefix)
> 	 int cmd_notes(int argc, const char **argv, const char *prefix)
> 	 {
> 	 	const char *override_notes_ref = NULL;
> 	-	parse_opt_subcommand_fn *fn = list;
> 	+	parse_opt_subcommand_fn *fn = NULL;
> 	 	struct option options[] = {
> 	 		OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"),
> 	 			   N_("use notes from <notes-ref>")),
> 	@@ -1015,10 +1015,11 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
> 	 	git_config(git_default_config, NULL);
> 	 	argc = parse_options(argc, argv, prefix, options, git_notes_usage,
> 	 			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
> 	-	if (fn == list && argc && strcmp(argv[0], "list")) {
> 	-		error(_("unknown subcommand: %s"), argv[0]);
> 	-		usage_with_options(git_notes_usage, options);
> 	-	}
> 	+	if (!fn && argc)
> 	+		usage_msg_optf(_("unknown subcommand: %s"),
> 	+			       git_notes_usage, options, argv[0]);
> 	+	else if (!fn)
> 	+		fn = list;
> 	 
> 	 	if (override_notes_ref) {
> 	 		struct strbuf sb = STRBUF_INIT;
> 
> I.e. we rely on the API setting it to non-NULL if it finds a subcommand,
> otherwise we just set it to "list" after checking whether we have excess
> arguments.

Oh, that does look nicer indeed.

> > [...]
> > -	else if (!strcmp(argv[0], "get-ref"))
> > -		result = get_ref(argc, argv, prefix);
> > -	else {
> > -		result = error(_("unknown subcommand: %s"), argv[0]);
> > -		usage_with_options(git_notes_usage, options);
> > -	}
> > -
> > -	return result ? 1 : 0;
> > +	return !!fn(argc, argv, prefix);
> >  }
> 
> In any case this is a lot nicer, ditto previous comment about maybe
> skipping the refactoring of this end code, but I'm also fine with
> keeping it.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* [PATCH 0/5] parse-options: minor cleanups for handling subcommands
  2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
                     ` (19 preceding siblings ...)
  2022-08-19 16:04   ` [PATCH v2 20/20] builtin/worktree.c: " SZEDER Gábor
@ 2022-09-05 18:50   ` SZEDER Gábor
  2022-09-05 18:50     ` [PATCH 1/5] t0040-parse-options: remove leftover debugging SZEDER Gábor
                       ` (5 more replies)
  20 siblings, 6 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-09-05 18:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

I still wanted to make a few minor cleanups after v2 of
'sg/parse-options-subcommand', but while I got distracted with some
Coccinelle stuff that topic got merged to 'next', and it's now in
'master'.  Oh, well.

So here are those cleanups on top of that topic (or on top of 'master').

The changes really are minor: the first four patches shouldn't alter
behavior in any way, and even the last one touches only two error
messages.

SZEDER Gábor (5):
  t0040-parse-options: remove leftover debugging
  test-parse-options.c: don't use for loop initial declaration
  test-parse-options.c: fix style of comparison with zero
  notes: simplify default operation mode arguments check
  notes, remote: show unknown subcommands between `'

 builtin/notes.c               | 11 +++++++----
 builtin/remote.c              |  2 +-
 t/helper/test-parse-options.c |  7 ++++---
 t/t0040-parse-options.sh      |  2 --
 4 files changed, 12 insertions(+), 10 deletions(-)

-- 
2.37.3.989.g4c3dfb3304


^ permalink raw reply	[flat|nested] 97+ messages in thread

* [PATCH 1/5] t0040-parse-options: remove leftover debugging
  2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
@ 2022-09-05 18:50     ` SZEDER Gábor
  2022-09-05 18:50     ` [PATCH 2/5] test-parse-options.c: don't use for loop initial declaration SZEDER Gábor
                       ` (4 subsequent siblings)
  5 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-09-05 18:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/t0040-parse-options.sh | 2 --
 1 file changed, 2 deletions(-)

diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
index b19b8d3486..5cc62306e3 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh
@@ -500,7 +500,6 @@ test_expect_success 'KEEP_UNKNOWN_OPT works' '
 
 test_expect_success 'NO_INTERNAL_HELP works for -h' '
 	test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&
-	cat err &&
 	grep "^error: unknown switch \`h$SQ" err &&
 	grep "^usage: " err
 '
@@ -509,7 +508,6 @@ for help_opt in help help-all
 do
 	test_expect_success "NO_INTERNAL_HELP works for --$help_opt" "
 		test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&
-		cat err &&
 		grep '^error: unknown option \`'$help_opt\' err &&
 		grep '^usage: ' err
 	"
-- 
2.37.3.989.g4c3dfb3304


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 2/5] test-parse-options.c: don't use for loop initial declaration
  2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
  2022-09-05 18:50     ` [PATCH 1/5] t0040-parse-options: remove leftover debugging SZEDER Gábor
@ 2022-09-05 18:50     ` SZEDER Gábor
  2022-09-05 18:50     ` [PATCH 3/5] test-parse-options.c: fix style of comparison with zero SZEDER Gábor
                       ` (3 subsequent siblings)
  5 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-09-05 18:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

We would like to eventually use for loop initial declarations in our
codebase, but we are not there yet.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/helper/test-parse-options.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
index aa0ad45851..9fe8ce66cb 100644
--- a/t/helper/test-parse-options.c
+++ b/t/helper/test-parse-options.c
@@ -195,7 +195,8 @@ int cmd__parse_options(int argc, const char **argv)
 
 static void print_args(int argc, const char **argv)
 {
-	for (int i = 0; i < argc; i++)
+	int i;
+	for (i = 0; i < argc; i++)
 		printf("arg %02d: %s\n", i, argv[i]);
 }
 
-- 
2.37.3.989.g4c3dfb3304


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 3/5] test-parse-options.c: fix style of comparison with zero
  2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
  2022-09-05 18:50     ` [PATCH 1/5] t0040-parse-options: remove leftover debugging SZEDER Gábor
  2022-09-05 18:50     ` [PATCH 2/5] test-parse-options.c: don't use for loop initial declaration SZEDER Gábor
@ 2022-09-05 18:50     ` SZEDER Gábor
  2022-09-05 18:50     ` [PATCH 4/5] notes: simplify default operation mode arguments check SZEDER Gábor
                       ` (2 subsequent siblings)
  5 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-09-05 18:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

The preferred style is '!argc' instead of 'argc == 0'.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/helper/test-parse-options.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
index 9fe8ce66cb..506835521a 100644
--- a/t/helper/test-parse-options.c
+++ b/t/helper/test-parse-options.c
@@ -255,7 +255,7 @@ int cmd__parse_options_flags(int argc, const char **argv)
 	argc = parse_options(argc, argv, NULL, test_flag_options, usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
 
-	if (argc == 0 || strcmp(argv[0], "cmd")) {
+	if (!argc || strcmp(argv[0], "cmd")) {
 		error("'cmd' is mandatory");
 		usage_with_options(usage, test_flag_options);
 	}
@@ -313,7 +313,7 @@ int cmd__parse_subcommand(int argc, const char **argv)
 	argc = parse_options(argc, argv, NULL, test_flag_options, usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
 
-	if (argc == 0 || strcmp(argv[0], "cmd")) {
+	if (!argc || strcmp(argv[0], "cmd")) {
 		error("'cmd' is mandatory");
 		usage_with_options(usage, test_flag_options);
 	}
-- 
2.37.3.989.g4c3dfb3304


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 4/5] notes: simplify default operation mode arguments check
  2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
                       ` (2 preceding siblings ...)
  2022-09-05 18:50     ` [PATCH 3/5] test-parse-options.c: fix style of comparison with zero SZEDER Gábor
@ 2022-09-05 18:50     ` SZEDER Gábor
  2022-09-05 18:50     ` [PATCH 5/5] notes, remote: show unknown subcommands between `' SZEDER Gábor
  2022-09-07 19:12     ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands Junio C Hamano
  5 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-09-05 18:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

'git notes' has a default operation mode, but when invoked without a
subcommand it doesn't accept any arguments (although the 'list'
subcommand implementing the default operation mode does accept
arguments).  The condition checking this ended up a bit awkward, so
let's make it clearer.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/notes.c | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/builtin/notes.c b/builtin/notes.c
index 42cbae4659..60410af572 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -995,7 +995,7 @@ static int get_ref(int argc, const char **argv, const char *prefix)
 int cmd_notes(int argc, const char **argv, const char *prefix)
 {
 	const char *override_notes_ref = NULL;
-	parse_opt_subcommand_fn *fn = list;
+	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
 		OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"),
 			   N_("use notes from <notes-ref>")),
@@ -1015,9 +1015,12 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
 	git_config(git_default_config, NULL);
 	argc = parse_options(argc, argv, prefix, options, git_notes_usage,
 			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
-	if (fn == list && argc && strcmp(argv[0], "list")) {
-		error(_("unknown subcommand: %s"), argv[0]);
-		usage_with_options(git_notes_usage, options);
+	if (!fn) {
+		if (argc) {
+			error(_("unknown subcommand: %s"), argv[0]);
+			usage_with_options(git_notes_usage, options);
+		}
+		fn = list;
 	}
 
 	if (override_notes_ref) {
-- 
2.37.3.989.g4c3dfb3304


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* [PATCH 5/5] notes, remote: show unknown subcommands between `'
  2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
                       ` (3 preceding siblings ...)
  2022-09-05 18:50     ` [PATCH 4/5] notes: simplify default operation mode arguments check SZEDER Gábor
@ 2022-09-05 18:50     ` SZEDER Gábor
  2022-09-07 19:12     ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands Junio C Hamano
  5 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-09-05 18:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason,
	SZEDER Gábor

Update the "unknown subcommand" error message in 'git notes' and 'git
remote' to wrap the offending argument between `', to make it
consistent with the "unknown switch/option/subcommand" error messages
in parse-options.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 builtin/notes.c  | 2 +-
 builtin/remote.c | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/builtin/notes.c b/builtin/notes.c
index 60410af572..be51f69225 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -1017,7 +1017,7 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
 			     PARSE_OPT_SUBCOMMAND_OPTIONAL);
 	if (!fn) {
 		if (argc) {
-			error(_("unknown subcommand: %s"), argv[0]);
+			error(_("unknown subcommand: `%s'"), argv[0]);
 			usage_with_options(git_notes_usage, options);
 		}
 		fn = list;
diff --git a/builtin/remote.c b/builtin/remote.c
index 272c7b8d9e..07117e4c9a 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1768,7 +1768,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix)
 		return !!fn(argc, argv, prefix);
 	} else {
 		if (argc) {
-			error(_("unknown subcommand: %s"), argv[0]);
+			error(_("unknown subcommand: `%s'"), argv[0]);
 			usage_with_options(builtin_remote_usage, options);
 		}
 		return !!show_all();
-- 
2.37.3.989.g4c3dfb3304


^ permalink raw reply related	[flat|nested] 97+ messages in thread

* Re: [PATCH 0/5] parse-options: minor cleanups for handling subcommands
  2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
                       ` (4 preceding siblings ...)
  2022-09-05 18:50     ` [PATCH 5/5] notes, remote: show unknown subcommands between `' SZEDER Gábor
@ 2022-09-07 19:12     ` Junio C Hamano
  2022-09-07 21:22       ` SZEDER Gábor
  5 siblings, 1 reply; 97+ messages in thread
From: Junio C Hamano @ 2022-09-07 19:12 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: git, Ævar Arnfjörð Bjarmason

SZEDER Gábor <szeder.dev@gmail.com> writes:

> The changes really are minor: the first four patches shouldn't alter
> behavior in any way, and even the last one touches only two error
> messages.

All of them make sense.  I especially liked the simplification of
"defaults to list" one.  "git notes list list foo" might change
behaviour but probably in a better direction.

Will queue.

Thanks.

^ permalink raw reply	[flat|nested] 97+ messages in thread

* Re: [PATCH 0/5] parse-options: minor cleanups for handling subcommands
  2022-09-07 19:12     ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands Junio C Hamano
@ 2022-09-07 21:22       ` SZEDER Gábor
  0 siblings, 0 replies; 97+ messages in thread
From: SZEDER Gábor @ 2022-09-07 21:22 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Ævar Arnfjörð Bjarmason

On Wed, Sep 07, 2022 at 12:12:21PM -0700, Junio C Hamano wrote:
> SZEDER Gábor <szeder.dev@gmail.com> writes:
> 
> > The changes really are minor: the first four patches shouldn't alter
> > behavior in any way, and even the last one touches only two error
> > messages.
> 
> All of them make sense.  I especially liked the simplification of
> "defaults to list" one.  "git notes list list foo" might change
> behaviour but probably in a better direction.

v2.37, current master (or 'sg/parse-options-subcommand'), and this
series behave the same when running that command: control is
dispatched to the function implementing the 'list' subcommand, which
then complains about "too many arguments", as it accepts only a one
argument.


^ permalink raw reply	[flat|nested] 97+ messages in thread

end of thread, other threads:[~2022-09-07 21:22 UTC | newest]

Thread overview: 97+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-07-25 12:38 [PATCH 00/20] parse-options: handle subcommands SZEDER Gábor
2022-07-25 12:38 ` [PATCH 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
2022-07-25 14:31   ` Ævar Arnfjörð Bjarmason
2022-08-02 17:37     ` SZEDER Gábor
2022-08-02 21:00       ` Junio C Hamano
2022-08-03 13:11         ` Ævar Arnfjörð Bjarmason
2022-08-03 21:34         ` SZEDER Gábor
2022-08-04  7:47           ` Ævar Arnfjörð Bjarmason
2022-08-11 21:35           ` Junio C Hamano
2022-08-12 15:28             ` SZEDER Gábor
2022-08-12 16:46               ` Junio C Hamano
2022-07-26 19:55   ` SZEDER Gábor
2022-07-25 12:38 ` [PATCH 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments SZEDER Gábor
2022-07-25 12:38 ` [PATCH 03/20] t5505-remote.sh: check the behavior without a subcommand SZEDER Gábor
2022-07-25 14:37   ` Ævar Arnfjörð Bjarmason
2022-07-25 12:38 ` [PATCH 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
2022-07-25 14:38   ` Ævar Arnfjörð Bjarmason
2022-08-12 15:04     ` SZEDER Gábor
2022-07-25 12:38 ` [PATCH 05/20] api-parse-options.txt: fix description of OPT_CMDMODE SZEDER Gábor
2022-07-25 12:38 ` [PATCH 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options SZEDER Gábor
2022-07-25 12:38 ` [PATCH 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH SZEDER Gábor
2022-07-25 12:38 ` [PATCH 08/20] parse-options: drop leading space from '--git-completion-helper' output SZEDER Gábor
2022-07-25 12:38 ` [PATCH 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
2022-07-25 14:43   ` Ævar Arnfjörð Bjarmason
2022-07-25 19:29     ` SZEDER Gábor
2022-07-25 19:41       ` Ævar Arnfjörð Bjarmason
2022-07-25 21:02         ` SZEDER Gábor
2022-08-12 15:15         ` SZEDER Gábor
2022-07-25 17:37   ` Junio C Hamano
2022-07-25 12:38 ` [PATCH 10/20] builtin/bundle.c: let parse-options parse subcommands SZEDER Gábor
2022-07-25 12:38 ` [PATCH 11/20] builtin/commit-graph.c: " SZEDER Gábor
2022-07-25 12:38 ` [PATCH 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands SZEDER Gábor
2022-07-25 12:38 ` [PATCH 13/20] builtin/hook.c: let parse-option parse subcommands SZEDER Gábor
2022-07-25 12:38 ` [PATCH 14/20] builtin/multi-pack-index.c: let parse-options " SZEDER Gábor
2022-07-25 12:38 ` [PATCH 15/20] builtin/notes.c: " SZEDER Gábor
2022-07-25 16:49   ` Junio C Hamano
2022-07-25 12:38 ` [PATCH 16/20] builtin/reflog.c: " SZEDER Gábor
2022-07-25 12:38 ` [PATCH 17/20] builtin/remote.c: " SZEDER Gábor
2022-07-25 12:38 ` [PATCH 18/20] builtin/sparse-checkout.c: " SZEDER Gábor
2022-07-25 12:38 ` [PATCH 19/20] builtin/stash.c: " SZEDER Gábor
2022-07-25 12:38 ` [PATCH 20/20] builtin/worktree.c: " SZEDER Gábor
2022-07-25 13:15 ` [PATCH 00/20] parse-options: handle subcommands Derrick Stolee
2022-07-25 16:00   ` SZEDER Gábor
2022-07-25 16:08     ` Derrick Stolee
2022-07-25 17:13 ` Ævar Arnfjörð Bjarmason
2022-07-25 17:56 ` Junio C Hamano
2022-07-26 15:42   ` Johannes Schindelin
2022-07-26 18:02     ` Ævar Arnfjörð Bjarmason
2022-08-19 16:03 ` [PATCH v2 " SZEDER Gábor
2022-08-19 16:03   ` [PATCH v2 01/20] git.c: update NO_PARSEOPT markings SZEDER Gábor
2022-08-19 16:03   ` [PATCH v2 02/20] t3301-notes.sh: check that default operation mode doesn't take arguments SZEDER Gábor
2022-08-19 16:03   ` [PATCH v2 03/20] t5505-remote.sh: check the behavior without a subcommand SZEDER Gábor
2022-08-19 16:03   ` [PATCH v2 04/20] t0040-parse-options: test parse_options() with various 'parse_opt_flags' SZEDER Gábor
2022-08-19 17:23     ` Ævar Arnfjörð Bjarmason
2022-08-20 11:14       ` SZEDER Gábor
2022-08-19 18:18     ` Junio C Hamano
2022-08-20 10:31       ` SZEDER Gábor
2022-08-20 21:27         ` Junio C Hamano
2022-08-19 16:03   ` [PATCH v2 05/20] api-parse-options.txt: fix description of OPT_CMDMODE SZEDER Gábor
2022-08-19 16:03   ` [PATCH v2 06/20] parse-options: PARSE_OPT_KEEP_UNKNOWN only applies to --options SZEDER Gábor
2022-08-19 16:03   ` [PATCH v2 07/20] parse-options: clarify the limitations of PARSE_OPT_NODASH SZEDER Gábor
2022-08-19 16:03   ` [PATCH v2 08/20] parse-options: drop leading space from '--git-completion-helper' output SZEDER Gábor
2022-08-19 17:30     ` Ævar Arnfjörð Bjarmason
2022-08-19 18:35       ` SZEDER Gábor
2022-08-19 16:04   ` [PATCH v2 09/20] parse-options: add support for parsing subcommands SZEDER Gábor
2022-08-19 17:33     ` Ævar Arnfjörð Bjarmason
2022-08-19 19:03     ` Ævar Arnfjörð Bjarmason
2022-08-19 16:04   ` [PATCH v2 10/20] builtin/bundle.c: let parse-options parse subcommands SZEDER Gábor
2022-08-19 17:50     ` Ævar Arnfjörð Bjarmason
2022-08-19 16:04   ` [PATCH v2 11/20] builtin/commit-graph.c: " SZEDER Gábor
2022-08-19 17:53     ` Ævar Arnfjörð Bjarmason
2022-08-19 17:56       ` Ævar Arnfjörð Bjarmason
2022-08-19 18:22       ` SZEDER Gábor
2022-08-19 16:04   ` [PATCH v2 12/20] builtin/gc.c: let parse-options parse 'git maintenance's subcommands SZEDER Gábor
2022-08-19 20:59     ` Junio C Hamano
2022-08-19 16:04   ` [PATCH v2 13/20] builtin/hook.c: let parse-options parse subcommands SZEDER Gábor
2022-08-19 16:04   ` [PATCH v2 14/20] builtin/multi-pack-index.c: " SZEDER Gábor
2022-08-19 17:57     ` Ævar Arnfjörð Bjarmason
2022-08-19 16:04   ` [PATCH v2 15/20] builtin/notes.c: " SZEDER Gábor
2022-08-19 18:01     ` Ævar Arnfjörð Bjarmason
2022-08-21 17:56       ` SZEDER Gábor
2022-08-19 16:04   ` [PATCH v2 16/20] builtin/reflog.c: " SZEDER Gábor
2022-08-19 18:08     ` Ævar Arnfjörð Bjarmason
2022-08-19 16:04   ` [PATCH v2 17/20] builtin/remote.c: " SZEDER Gábor
2022-08-19 16:04   ` [PATCH v2 18/20] builtin/sparse-checkout.c: " SZEDER Gábor
2022-08-19 16:04   ` [PATCH v2 19/20] builtin/stash.c: " SZEDER Gábor
2022-08-19 19:06     ` Ævar Arnfjörð Bjarmason
2022-08-20 10:27       ` SZEDER Gábor
2022-08-19 16:04   ` [PATCH v2 20/20] builtin/worktree.c: " SZEDER Gábor
2022-09-05 18:50   ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands SZEDER Gábor
2022-09-05 18:50     ` [PATCH 1/5] t0040-parse-options: remove leftover debugging SZEDER Gábor
2022-09-05 18:50     ` [PATCH 2/5] test-parse-options.c: don't use for loop initial declaration SZEDER Gábor
2022-09-05 18:50     ` [PATCH 3/5] test-parse-options.c: fix style of comparison with zero SZEDER Gábor
2022-09-05 18:50     ` [PATCH 4/5] notes: simplify default operation mode arguments check SZEDER Gábor
2022-09-05 18:50     ` [PATCH 5/5] notes, remote: show unknown subcommands between `' SZEDER Gábor
2022-09-07 19:12     ` [PATCH 0/5] parse-options: minor cleanups for handling subcommands Junio C Hamano
2022-09-07 21:22       ` SZEDER Gábor

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).