Commit 8526cbd56b0395b9427727e81e6f3c89768337b9

Edward Thomson 2021-11-26T09:37:29

opt: use a custom function to print usage Our argument parser (https://github.com/ethomson/adopt) includes a function to print a usage message based on the allowed options. Omit this and use a cutom function that understands that we have subcommands ("checkout", "revert", etc) that each have their own options.

diff --git a/src/cli/cli.h b/src/cli/cli.h
index a27081d..222d53a 100644
--- a/src/cli/cli.h
+++ b/src/cli/cli.h
@@ -14,5 +14,6 @@
 
 #include "error.h"
 #include "opt.h"
+#include "opt_usage.h"
 
 #endif /* CLI_cli_h__ */
diff --git a/src/cli/main.c b/src/cli/main.c
index 709f6b4..5eff56a 100644
--- a/src/cli/main.c
+++ b/src/cli/main.c
@@ -34,7 +34,7 @@ int main(int argc, char **argv)
 	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, common_opts);
+			cli_opt_usage_fprint(stderr, PROGRAM_NAME, NULL, common_opts);
 			ret = CLI_EXIT_USAGE;
 			goto done;
 		}
diff --git a/src/cli/opt.c b/src/cli/opt.c
index 11faa92..72df587 100644
--- a/src/cli/opt.c
+++ b/src/cli/opt.c
@@ -10,7 +10,7 @@
  * This file was produced by using the `rename.pl` script included with
  * adopt.  The command-line specified was:
  *
- * ./rename.pl cli_opt --filename=opt --include=cli.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status
+ * ./rename.pl cli_opt --filename=opt --include=cli.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status --without-usage
  */
 
 #include <stdlib.h>
@@ -667,84 +667,3 @@ int cli_opt_status_fprint(
 	return error;
 }
 
-int cli_opt_usage_fprint(
-	FILE *file,
-	const char *command,
-	const cli_opt_spec specs[])
-{
-	const cli_opt_spec *spec;
-	int choice = 0, next_choice = 0, optional = 0;
-	int error;
-
-	if ((error = fprintf(file, "usage: %s", command)) < 0)
-		goto done;
-
-	for (spec = specs; spec->type; ++spec) {
-		if (!choice)
-			optional = !(spec->usage & CLI_OPT_USAGE_REQUIRED);
-
-		next_choice = !!((spec + 1)->usage & CLI_OPT_USAGE_CHOICE);
-
-		if (spec->usage & CLI_OPT_USAGE_HIDDEN)
-			continue;
-
-		if (choice)
-			error = fprintf(file, "|");
-		else
-			error = fprintf(file, " ");
-
-		if (error < 0)
-			goto done;
-
-		if (optional && !choice && (error = fprintf(file, "[")) < 0)
-			error = fprintf(file, "[");
-		if (!optional && !choice && next_choice)
-			error = fprintf(file, "(");
-
-		if (error < 0)
-			goto done;
-
-		if (spec->type == CLI_OPT_TYPE_VALUE && spec->alias &&
-		    !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL) &&
-		    !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
-			error = fprintf(file, "-%c <%s>", spec->alias, spec->value_name);
-		else if (spec->type == CLI_OPT_TYPE_VALUE && spec->alias &&
-		         !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
-			error = fprintf(file, "-%c [<%s>]", spec->alias, spec->value_name);
-		else if (spec->type == CLI_OPT_TYPE_VALUE &&
-		         !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL))
-			error = fprintf(file, "--%s[=<%s>]", spec->name, spec->value_name);
-		else if (spec->type == CLI_OPT_TYPE_VALUE)
-			error = fprintf(file, "--%s=<%s>", spec->name, spec->value_name);
-		else if (spec->type == CLI_OPT_TYPE_ARG)
-			error = fprintf(file, "<%s>", spec->value_name);
-		else if (spec->type == CLI_OPT_TYPE_ARGS)
-			error = fprintf(file, "<%s>...", spec->value_name);
-		else if (spec->type == CLI_OPT_TYPE_LITERAL)
-			error = fprintf(file, "--");
-		else if (spec->alias && !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
-			error = fprintf(file, "-%c", spec->alias);
-		else
-			error = fprintf(file, "--%s", spec->name);
-
-		if (error < 0)
-			goto done;
-
-		if (!optional && choice && !next_choice)
-			error = fprintf(file, ")");
-		else if (optional && !next_choice)
-			error = fprintf(file, "]");
-
-		if (error < 0)
-			goto done;
-
-		choice = next_choice;
-	}
-
-	error = fprintf(file, "\n");
-
-done:
-	error = (error < 0) ? -1 : 0;
-	return error;
-}
-
diff --git a/src/cli/opt.h b/src/cli/opt.h
index f7b6b93..6c1d460 100644
--- a/src/cli/opt.h
+++ b/src/cli/opt.h
@@ -10,7 +10,7 @@
  * This file was produced by using the `rename.pl` script included with
  * adopt.  The command-line specified was:
  *
- * ./rename.pl cli_opt --filename=opt --include=cli.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status
+ * ./rename.pl cli_opt --filename=opt --include=cli.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status --without-usage
  */
 
 #ifndef CLI_opt_h__
@@ -346,17 +346,4 @@ int cli_opt_status_fprint(
 	const char *command,
 	const cli_opt *opt);
 
-/**
- * Prints usage information to the given file handle.
- *
- * @param file The file to print information to
- * @param command The name of the command to use when printing
- * @param specs The specifications allowed by the command
- * @return 0 on success, -1 on failure
- */
-int cli_opt_usage_fprint(
-	FILE *file,
-	const char *command,
-	const cli_opt_spec specs[]);
-
 #endif /* CLI_opt_h__ */
diff --git a/src/cli/opt_usage.c b/src/cli/opt_usage.c
new file mode 100644
index 0000000..6e5d600
--- /dev/null
+++ b/src/cli/opt_usage.c
@@ -0,0 +1,194 @@
+/*
+ * 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 "str.h"
+
+static int print_spec_name(git_str *out, const cli_opt_spec *spec)
+{
+	if (spec->type == CLI_OPT_TYPE_VALUE && spec->alias &&
+	    !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL) &&
+	    !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
+		return git_str_printf(out, "-%c <%s>", spec->alias, spec->value_name);
+	if (spec->type == CLI_OPT_TYPE_VALUE && spec->alias &&
+	    !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
+		return git_str_printf(out, "-%c [<%s>]", spec->alias, spec->value_name);
+	if (spec->type == CLI_OPT_TYPE_VALUE &&
+	    !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL))
+		return git_str_printf(out, "--%s[=<%s>]", spec->name, spec->value_name);
+	if (spec->type == CLI_OPT_TYPE_VALUE)
+		return git_str_printf(out, "--%s=<%s>", spec->name, spec->value_name);
+	if (spec->type == CLI_OPT_TYPE_ARG)
+		return git_str_printf(out, "<%s>", spec->value_name);
+	if (spec->type == CLI_OPT_TYPE_ARGS)
+		return git_str_printf(out, "<%s>...", spec->value_name);
+	if (spec->type == CLI_OPT_TYPE_LITERAL)
+		return git_str_printf(out, "--");
+	if (spec->alias && !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
+		return git_str_printf(out, "-%c", spec->alias);
+	if (spec->name)
+		return git_str_printf(out, "--%s", spec->name);
+
+	GIT_ASSERT(0);
+}
+
+/*
+ * This is similar to adopt's function, but modified to understand
+ * that we have a command ("git") and a "subcommand" ("checkout").
+ * It also understands a terminal's line length and wrap appropriately,
+ * using a `git_str` for storage.
+ */
+int cli_opt_usage_fprint(
+	FILE *file,
+	const char *command,
+	const char *subcommand,
+	const cli_opt_spec specs[])
+{
+	git_str usage = GIT_BUF_INIT, opt = GIT_BUF_INIT;
+	const cli_opt_spec *spec;
+	size_t i, prefixlen, linelen;
+	bool choice = false, next_choice = false, optional = false;
+	int error;
+
+	/* TODO: query actual console width. */
+	int console_width = 80;
+
+	if ((error = git_str_printf(&usage, "usage: %s", command)) < 0)
+		goto done;
+
+	if (subcommand &&
+	    (error = git_str_printf(&usage, " %s", subcommand)) < 0)
+		goto done;
+
+	linelen = git_str_len(&usage);
+	prefixlen = linelen + 1;
+
+	for (spec = specs; spec->type; ++spec) {
+		if (!choice)
+			optional = !(spec->usage & CLI_OPT_USAGE_REQUIRED);
+
+		next_choice = !!((spec + 1)->usage & CLI_OPT_USAGE_CHOICE);
+
+		if (spec->usage & CLI_OPT_USAGE_HIDDEN)
+			continue;
+
+		if (choice)
+			git_str_putc(&opt, '|');
+		else
+			git_str_clear(&opt);
+
+		if (optional && !choice)
+			git_str_putc(&opt, '[');
+		if (!optional && !choice && next_choice)
+			git_str_putc(&opt, '(');
+
+		if ((error = print_spec_name(&opt, spec)) < 0)
+			goto done;
+
+		if (!optional && choice && !next_choice)
+			git_str_putc(&opt, ')');
+		else if (optional && !next_choice)
+			git_str_putc(&opt, ']');
+
+		if ((choice = next_choice))
+			continue;
+
+		if (git_str_oom(&opt)) {
+			error = -1;
+			goto done;
+		}
+
+		if (linelen > prefixlen &&
+		    console_width > 0 &&
+		    linelen + git_str_len(&opt) + 1 > (size_t)console_width) {
+			git_str_putc(&usage, '\n');
+
+			for (i = 0; i < prefixlen; i++)
+				git_str_putc(&usage, ' ');
+
+			linelen = prefixlen;
+		} else {
+			git_str_putc(&usage, ' ');
+			linelen += git_str_len(&opt) + 1;
+		}
+
+		git_str_puts(&usage, git_str_cstr(&opt));
+
+		if (git_str_oom(&usage)) {
+			error = -1;
+			goto done;
+		}
+	}
+
+	error = fprintf(file, "%s\n", git_str_cstr(&usage));
+
+done:
+	error = (error < 0) ? -1 : 0;
+
+	git_str_dispose(&usage);
+	git_str_dispose(&opt);
+	return error;
+}
+
+int cli_opt_usage_error(
+	const char *subcommand,
+	const cli_opt_spec specs[],
+	const cli_opt *invalid_opt)
+{
+	cli_opt_status_fprint(stderr, PROGRAM_NAME, invalid_opt);
+	cli_opt_usage_fprint(stderr, PROGRAM_NAME, subcommand, specs);
+	return CLI_EXIT_USAGE;
+}
+
+int cli_opt_help_fprint(
+	FILE *file,
+	const cli_opt_spec specs[])
+{
+	git_str help = GIT_BUF_INIT;
+	const cli_opt_spec *spec;
+	int error;
+
+	/* Display required arguments first */
+	for (spec = specs; spec->type; ++spec) {
+		if (! (spec->usage & CLI_OPT_USAGE_REQUIRED) ||
+		    (spec->usage & CLI_OPT_USAGE_HIDDEN))
+			continue;
+
+		git_str_printf(&help, "    ");
+
+		if ((error = print_spec_name(&help, spec)) < 0)
+			goto done;
+
+		git_str_printf(&help, ": %s\n", spec->help);
+	}
+
+	/* Display the remaining arguments */
+	for (spec = specs; spec->type; ++spec) {
+		if ((spec->usage & CLI_OPT_USAGE_REQUIRED) ||
+		    (spec->usage & CLI_OPT_USAGE_HIDDEN))
+			continue;
+
+		git_str_printf(&help, "    ");
+
+		if ((error = print_spec_name(&help, spec)) < 0)
+			goto done;
+
+		git_str_printf(&help, ": %s\n", spec->help);
+
+	}
+
+	if (git_str_oom(&help) ||
+	    p_write(fileno(file), help.ptr, help.size) < 0)
+		error = -1;
+
+done:
+	error = (error < 0) ? -1 : 0;
+
+	git_str_dispose(&help);
+	return error;
+}
+
diff --git a/src/cli/opt_usage.h b/src/cli/opt_usage.h
new file mode 100644
index 0000000..c752494
--- /dev/null
+++ b/src/cli/opt_usage.h
@@ -0,0 +1,35 @@
+/*
+ * 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_opt_usage_h__
+#define CLI_opt_usage_h__
+
+/**
+ * Prints usage information to the given file handle.
+ *
+ * @param file The file to print information to
+ * @param command The name of the command to use when printing
+ * @param subcommand The name of the subcommand (eg "checkout") to use when printing, or NULL to skip
+ * @param specs The specifications allowed by the command
+ * @return 0 on success, -1 on failure
+ */
+int cli_opt_usage_fprint(
+	FILE *file,
+	const char *command,
+	const char *subcommand,
+	const cli_opt_spec specs[]);
+
+int cli_opt_usage_error(
+	const char *subcommand,
+	const cli_opt_spec specs[],
+	const cli_opt *invalid_opt);
+
+int cli_opt_help_fprint(
+	FILE *file,
+	const cli_opt_spec specs[]);
+
+#endif /* CLI_opt_usage_h__ */