Commit c6dd82d9f14e9e24a52af637c4a56218ead13eda

Edward Thomson 2020-02-23T11:54:33

cli: introduce a help command Add a framework for commands to be defined, and add our first one, "help". When `git2_cli help` is run, the `cmd_help` function will be invoked with the remaining command line arguments. This allows users to invoke `git2_cli help foo` to get information about the `foo` subcommand.

diff --git a/src/cli/README.md b/src/cli/README.md
index eefd2ff..26f11d9 100644
--- a/src/cli/README.md
+++ b/src/cli/README.md
@@ -1,3 +1,22 @@
 # cli
 
 A git-compatible command-line interface that uses libgit2.
+
+## Adding commands
+
+1. Individual commands have a `main`-like top-level entrypoint.  For example:
+
+   ```c
+   int cmd_help(int argc, char **argv)
+   ```
+
+   Although this is the same signature as `main`, commands are not built as
+   individual standalone executables, they'll be linked into the main cli.
+   (Though there may be an option for command executables to be built as
+   standalone executables in the future.)
+
+2. Commands are prototyped in `cmd.h` and added to `main.c`'s list of
+   commands (`cli_cmds[]`).  Commands should be specified with their name,
+   entrypoint and a brief description that can be printed in `git help`.
+   This is done because commands are linked into the main cli.
+
diff --git a/src/cli/cmd.c b/src/cli/cmd.c
new file mode 100644
index 0000000..2a7e71c
--- /dev/null
+++ b/src/cli/cmd.c
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "cli.h"
+#include "cmd.h"
+
+const cli_cmd_spec *cli_cmd_spec_byname(const char *name)
+{
+	const cli_cmd_spec *cmd;
+
+	for (cmd = cli_cmds; cmd->name; cmd++) {
+		if (!strcmp(cmd->name, name))
+			return cmd;
+	}
+
+	return NULL;
+}
diff --git a/src/cli/cmd.h b/src/cli/cmd.h
new file mode 100644
index 0000000..816614e
--- /dev/null
+++ b/src/cli/cmd.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#ifndef CLI_cmd_h__
+#define CLI_cmd_h__
+
+/* Command definitions */
+typedef struct {
+	const char *name;
+	int (*fn)(int argc, char **argv);
+	const char *desc;
+} cli_cmd_spec;
+
+/* Options that are common to all commands (eg --help, --git-dir) */
+extern const cli_opt_spec cli_common_opts[];
+
+/* All the commands supported by the CLI */
+extern const cli_cmd_spec cli_cmds[];
+
+/* Find a command by name */
+extern const cli_cmd_spec *cli_cmd_spec_byname(const char *name);
+
+/* Commands */
+extern int cmd_help(int argc, char **argv);
+
+#endif /* CLI_cmd_h__ */
diff --git a/src/cli/cmd_help.c b/src/cli/cmd_help.c
new file mode 100644
index 0000000..d2ff5d4
--- /dev/null
+++ b/src/cli/cmd_help.c
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include <stdio.h>
+#include <git2.h>
+#include "cli.h"
+#include "cmd.h"
+
+#define COMMAND_NAME "help"
+
+static char *command;
+static int show_help;
+
+static const cli_opt_spec opts[] = {
+	{ CLI_OPT_TYPE_SWITCH,   "help",     0, &show_help, 1,
+	  CLI_OPT_USAGE_HIDDEN,   NULL,     "display help about the help command" },
+	{ CLI_OPT_TYPE_ARG,      "command",  0, &command,   0,
+	  CLI_OPT_USAGE_DEFAULT, "command", "the command to show help for" },
+	{ 0 },
+};
+
+static int print_help(void)
+{
+	cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts);
+	printf("\n");
+
+	printf("Display help information about %s.  If a command is specified, help\n", PROGRAM_NAME);
+	printf("about that command will be shown.  Otherwise, general information about\n");
+	printf("%s will be shown, including the commands available.\n", PROGRAM_NAME);
+
+	return 0;
+}
+
+static int print_commands(void)
+{
+	const cli_cmd_spec *cmd;
+
+	cli_opt_usage_fprint(stdout, PROGRAM_NAME, NULL, cli_common_opts);
+	printf("\n");
+
+	printf("These are the %s commands available:\n\n", PROGRAM_NAME);
+
+	for (cmd = cli_cmds; cmd->name; cmd++)
+		printf("   %-8s  %s\n", cmd->name, cmd->desc);
+
+	printf("\nSee '%s help <command>' for more information on a specific command.\n", PROGRAM_NAME);
+
+	return 0;
+}
+
+int cmd_help(int argc, char **argv)
+{
+	cli_opt invalid_opt;
+
+	if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU))
+		return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt);
+
+	/* Show the meta-help */
+	if (show_help)
+		return print_help();
+
+	/* We were not asked to show help for a specific command. */
+	if (!command)
+		return print_commands();
+
+	/* If the user asks for help with the help command */
+	if (strcmp(command, "help") == 0)
+		return print_help();
+
+        fprintf(stderr, "%s: '%s' is not a %s command. See '%s help'.\n",
+            PROGRAM_NAME, command, PROGRAM_NAME, PROGRAM_NAME);
+	return CLI_EXIT_ERROR;
+}
diff --git a/src/cli/main.c b/src/cli/main.c
index 5eff56a..c68c349 100644
--- a/src/cli/main.c
+++ b/src/cli/main.c
@@ -8,19 +8,36 @@
 #include <stdio.h>
 #include <git2.h>
 #include "cli.h"
+#include "cmd.h"
 
+static int show_help = 0;
 static int show_version = 0;
+static char *command = NULL;
+static char **args = NULL;
 
-static const cli_opt_spec common_opts[] = {
-	{ CLI_OPT_TYPE_SWITCH,  "version",   0, &show_version, 1,
-	  CLI_OPT_USAGE_DEFAULT, NULL,      "display the version" },
+const cli_opt_spec cli_common_opts[] = {
+	{ CLI_OPT_TYPE_SWITCH,    "help",      0, &show_help,    1,
+	  CLI_OPT_USAGE_DEFAULT,   NULL,      "display help information" },
+	{ CLI_OPT_TYPE_SWITCH,    "version",   0, &show_version, 1,
+	  CLI_OPT_USAGE_DEFAULT,   NULL,      "display the version" },
+	{ CLI_OPT_TYPE_ARG,       "command",   0, &command,      0,
+	  CLI_OPT_USAGE_REQUIRED, "command", "the command to run" },
+	{ CLI_OPT_TYPE_ARGS,      "args",      0, &args,         0,
+	  CLI_OPT_USAGE_DEFAULT,  "args",    "arguments for the command" },
 	{ 0 }
 };
 
+const cli_cmd_spec cli_cmds[] = {
+	{ "help", cmd_help, "Display help information" },
+	{ NULL }
+};
+
 int main(int argc, char **argv)
 {
+	const cli_cmd_spec *cmd;
 	cli_opt_parser optparser;
 	cli_opt opt;
+	int args_len = 0;
 	int ret = 0;
 
 	if (git_libgit2_init() < 0) {
@@ -28,16 +45,26 @@ int main(int argc, char **argv)
 		exit(CLI_EXIT_GIT);
 	}
 
-	cli_opt_parser_init(&optparser, common_opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU);
+	cli_opt_parser_init(&optparser, cli_common_opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU);
 
 	/* Parse the top-level (common) options and command information */
 	while (cli_opt_parser_next(&opt, &optparser)) {
 		if (!opt.spec) {
 			cli_opt_status_fprint(stderr, PROGRAM_NAME, &opt);
-			cli_opt_usage_fprint(stderr, PROGRAM_NAME, NULL, common_opts);
+			cli_opt_usage_fprint(stderr, PROGRAM_NAME, NULL, cli_common_opts);
 			ret = CLI_EXIT_USAGE;
 			goto done;
 		}
+
+		/*
+		 * When we see a command, stop parsing and capture the
+		 * remaining arguments as args for the command itself.
+		 */
+		if (command) {
+			args = &argv[optparser.idx];
+			args_len = (int)(argc - optparser.idx);
+			break;
+		}
 	}
 
 	if (show_version) {
@@ -45,6 +72,20 @@ int main(int argc, char **argv)
 		goto done;
 	}
 
+	/* If there was no command, we want to invoke "help" */
+	if (!command || show_help) {
+		cli_opt_usage_fprint(stdout, PROGRAM_NAME, NULL, cli_common_opts);
+		goto done;
+	}
+
+	if ((cmd = cli_cmd_spec_byname(command)) == NULL) {
+		ret = cli_error("'%s' is not a %s command. See '%s help'.",
+		                command, PROGRAM_NAME, PROGRAM_NAME);
+		goto done;
+	}
+
+	ret = cmd->fn(args_len, args);
+
 done:
 	git_libgit2_shutdown();
 	return ret;