verbs: add "program header" and generate all help from data
We add a CommandDescription that contains basic info about the program:
- names (the main name and deprecated aliases)
- abstract, man page list, footer
This gets stored as a fake verb entry through VERB_COMMAND macro.
We can then generate the full help for the program from the verbs
and option arrays.
The formatting of man page links is changed from the form used
previously: the section is printed as ".N" instead of "(N)". This
for has the following advantages:
- the form with parenthesis looks like a function call, so e.g. close(1)
is both a valid call and a man page link.
- the new form can be automatically selected with a double click in the
typical terminal configurations, while '(' would usually terminate the
selection area.
- parentheses are special in shell syntax, so even the user manages to
successfully select and paste them, they still have to add quoting or
escaping on the command line.
- the new form is shorter
- it looks better
This commit is contained in:
@@ -85,6 +85,7 @@ typedef struct MachineCredentialContext MachineCredentialContext;
|
||||
typedef struct MountOptions MountOptions;
|
||||
typedef struct MStack MStack;
|
||||
typedef struct OpenFile OpenFile;
|
||||
typedef struct Option Option;
|
||||
typedef struct Pkcs11EncryptedKey Pkcs11EncryptedKey;
|
||||
typedef struct QmpClient QmpClient;
|
||||
typedef struct QmpSlot QmpSlot;
|
||||
@@ -94,4 +95,5 @@ typedef struct Tpm2Handle Tpm2Handle;
|
||||
typedef struct Tpm2PCRValue Tpm2PCRValue;
|
||||
typedef struct UnitInfo UnitInfo;
|
||||
typedef struct UserRecord UserRecord;
|
||||
typedef struct Verb Verb;
|
||||
typedef struct VeritySettings VeritySettings;
|
||||
|
||||
+93
-2
@@ -470,6 +470,24 @@ char* option_get_synopsis(const Option *opt, const char *joiner, bool show_metav
|
||||
option_arg_optional(opt) ? "]" : "");
|
||||
}
|
||||
|
||||
const Option* options_find_namespace(
|
||||
const Option options[],
|
||||
const Option options_end[],
|
||||
const char *namespace) {
|
||||
|
||||
if (!namespace)
|
||||
/* The first part is the default unnamed namespace, so
|
||||
* if the namespace was not specified, we are in it. */
|
||||
return options;
|
||||
|
||||
for (const Option *opt = options; opt < options_end; opt++)
|
||||
if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER) &&
|
||||
streq(namespace, ASSERT_PTR(opt->long_code)))
|
||||
return opt + 1;
|
||||
|
||||
return NULL; /* not found :/ */
|
||||
}
|
||||
|
||||
int _option_parser_get_help_table_full(
|
||||
const Option options[],
|
||||
const Option options_end[],
|
||||
@@ -478,6 +496,8 @@ int _option_parser_get_help_table_full(
|
||||
Table **ret) {
|
||||
int r;
|
||||
|
||||
// TODO: drop this function and all its wrappers
|
||||
|
||||
assert(ret);
|
||||
|
||||
_cleanup_(table_unrefp) Table *table = table_new("names", "help");
|
||||
@@ -492,7 +512,8 @@ int _option_parser_get_help_table_full(
|
||||
* <group>? The first part is the default group, so if the group was
|
||||
* not specified, we are in it. */
|
||||
|
||||
for (const Option *opt = options; opt < options_end; opt++) {
|
||||
const Option *opt;
|
||||
for (opt = options; opt < options_end; opt++) {
|
||||
bool ns_marker = FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER);
|
||||
if (!in_ns) {
|
||||
in_ns = ns_marker && streq(namespace, opt->long_code);
|
||||
@@ -543,5 +564,75 @@ int _option_parser_get_help_table_full(
|
||||
|
||||
table_set_header(table, false);
|
||||
*ret = TAKE_PTR(table);
|
||||
return 0;
|
||||
assert(opt - options < INT_MAX);
|
||||
return opt - options;
|
||||
}
|
||||
|
||||
int options_get_help_table_group(
|
||||
const Option options[],
|
||||
const Option options_end[],
|
||||
Table **ret,
|
||||
const char **ret_group) {
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
assert(ret_group);
|
||||
|
||||
_cleanup_(table_unrefp) Table *table = table_new("names", "help");
|
||||
if (!table)
|
||||
return log_oom();
|
||||
|
||||
assert(options_end > options);
|
||||
|
||||
bool group_marker = FLAGS_SET(options[0].flags, OPTION_GROUP_MARKER);
|
||||
const char *group = group_marker ? ASSERT_PTR(options[0].long_code) : NULL;
|
||||
|
||||
const Option *opt;
|
||||
for (opt = options + group_marker; opt < options_end; opt++) {
|
||||
if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER))
|
||||
/* End of our namespace */
|
||||
break;
|
||||
|
||||
if (FLAGS_SET(opt->flags, OPTION_GROUP_MARKER))
|
||||
/* End of the group */
|
||||
break;
|
||||
|
||||
if (!opt->help)
|
||||
/* No help string — we do not show the option */
|
||||
continue;
|
||||
|
||||
_cleanup_free_ char *s = option_get_synopsis(opt, " ", /* show_metavar= */ true);
|
||||
if (!s)
|
||||
return log_oom();
|
||||
|
||||
/* We indent the option string by two spaces. We could set the minimum cell width and
|
||||
* right-align for a similar result, but that'd be more work. This is only used for
|
||||
* display. */
|
||||
bool shift_left = opt->short_code != 0 || FLAGS_SET(opt->flags, OPTION_HELP_ENTRY_VERBATIM);
|
||||
const char *prefix = shift_left ? " " : " ";
|
||||
_cleanup_free_ char *t = strjoin(prefix, s);
|
||||
if (!t)
|
||||
return log_oom();
|
||||
|
||||
r = table_add_many(table, TABLE_STRING, t);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
|
||||
_cleanup_strv_free_ char **split = strv_split(opt->help, /* separators= */ NULL);
|
||||
if (!split)
|
||||
return log_oom();
|
||||
|
||||
r = table_add_many(table, TABLE_STRV_WRAPPED, split);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
assert(!table_isempty(table)); /* Empty group? Something is off. */
|
||||
|
||||
table_set_header(table, false);
|
||||
*ret = TAKE_PTR(table);
|
||||
*ret_group = group;
|
||||
|
||||
assert(opt - options < INT_MAX);
|
||||
return opt - options;
|
||||
}
|
||||
|
||||
@@ -266,6 +266,11 @@ char* option_parser_get_arg(const OptionParser *state, size_t i);
|
||||
|
||||
char* option_get_synopsis(const Option *opt, const char *joiner, bool show_metavar);
|
||||
|
||||
const Option* options_find_namespace(
|
||||
const Option options[],
|
||||
const Option options_end[],
|
||||
const char *namespace);
|
||||
|
||||
int _option_parser_get_help_table_full(
|
||||
const Option options[],
|
||||
const Option options_end[],
|
||||
@@ -280,3 +285,9 @@ int _option_parser_get_help_table_full(
|
||||
option_parser_get_help_table_full(/* namespace= */ NULL, group, ret)
|
||||
#define option_parser_get_help_table(ret) \
|
||||
option_parser_get_help_table_group(/* group= */ NULL, ret)
|
||||
|
||||
int options_get_help_table_group(
|
||||
const Option options[],
|
||||
const Option options_end[],
|
||||
Table **ret,
|
||||
const char **ret_group);
|
||||
|
||||
@@ -180,8 +180,13 @@ int terminal_urlify_path(const char *path, const char *text, char **ret) {
|
||||
int terminal_urlify_man_full(const char *page, const char *section, const char *suffix, char **ret) {
|
||||
const char *url, *text;
|
||||
|
||||
url = strjoina("man:", page, "(", section, ")");
|
||||
text = strjoina(page, "(", section, ")", suffix);
|
||||
if (section) {
|
||||
url = strjoina("man:", page, "(", section, ")");
|
||||
text = strjoina(page, "(", section, ")", suffix);
|
||||
} else {
|
||||
url = strjoina("man:", page);
|
||||
text = strjoina(page, suffix);
|
||||
}
|
||||
|
||||
return terminal_urlify(url, text, ret);
|
||||
}
|
||||
|
||||
+210
-5
@@ -2,7 +2,11 @@
|
||||
|
||||
#include "env-util.h"
|
||||
#include "format-table.h"
|
||||
#include "help-util.h"
|
||||
#include "log.h"
|
||||
#include "nulstr-util.h"
|
||||
#include "options.h"
|
||||
#include "pretty-print.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
@@ -58,15 +62,16 @@ bool should_bypass(const char *env_prefix) {
|
||||
}
|
||||
|
||||
static bool verb_is_metadata(const Verb *verb) {
|
||||
/* A metadata entry that is not a real verb, like the group marker */
|
||||
return FLAGS_SET(ASSERT_PTR(verb)->flags, VERB_GROUP_MARKER);
|
||||
/* A metadata entry that is not a real verb, i.e. either the command and group markers. */
|
||||
return FLAGS_SET(ASSERT_PTR(verb)->flags, VERB_COMMAND_MARKER) ||
|
||||
FLAGS_SET(ASSERT_PTR(verb)->flags, VERB_GROUP_MARKER);
|
||||
}
|
||||
|
||||
const Verb* verbs_find_verb(const char *name, const Verb verbs[], const Verb verbs_end[]) {
|
||||
assert(verbs);
|
||||
assert(verbs_end > verbs);
|
||||
assert((uintptr_t) verbs % sizeof(void*) == 0);
|
||||
assert(verbs[0].verb);
|
||||
assert(FLAGS_SET(verbs[0].flags, VERB_COMMAND_MARKER) || verbs[0].verb);
|
||||
|
||||
for (const Verb *verb = verbs; verb < verbs_end; verb++) {
|
||||
if (verb_is_metadata(verb))
|
||||
@@ -86,7 +91,7 @@ int _dispatch_verb(char **args, const Verb verbs[], const Verb verbs_end[], void
|
||||
assert(verbs);
|
||||
assert(verbs_end > verbs);
|
||||
assert((uintptr_t) verbs % sizeof(void*) == 0);
|
||||
assert(verbs[0].verb);
|
||||
assert(FLAGS_SET(verbs[0].flags, VERB_COMMAND_MARKER) || verbs[0].verb);
|
||||
|
||||
const char *name = args ? args[0] : NULL;
|
||||
size_t left = strv_length(args);
|
||||
@@ -214,6 +219,26 @@ static int verb_add_help_one(Table *table, const Verb *verb) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const Verb* verbs_get_command(const Verb verbs[], const Verb verbs_end[], const char *name) {
|
||||
assert(verbs);
|
||||
assert(verbs_end > verbs);
|
||||
assert((uintptr_t) verbs % sizeof(void*) == 0);
|
||||
assert(FLAGS_SET(verbs[0].flags, VERB_COMMAND_MARKER) || verbs[0].verb);
|
||||
assert(name);
|
||||
|
||||
for (const Verb *verb = verbs; verb < verbs_end; verb++) {
|
||||
if (!FLAGS_SET(verb->flags, VERB_COMMAND_MARKER))
|
||||
continue;
|
||||
|
||||
const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(verb->data);
|
||||
if (nulstr_contains(cmd->names, name))
|
||||
return verb;
|
||||
}
|
||||
|
||||
/* At the end of the list? */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int _verbs_get_help_table(
|
||||
const Verb verbs[],
|
||||
const Verb verbs_end[],
|
||||
@@ -234,7 +259,18 @@ int _verbs_get_help_table(
|
||||
* group <group>? The first part is the default group, so
|
||||
* if the group was not specified, we are in. */
|
||||
|
||||
for (const Verb *verb = verbs; verb < verbs_end; verb++) {
|
||||
const Verb *verb;
|
||||
for (verb = verbs; verb < verbs_end; verb++) {
|
||||
/* Binaries with one or more VERB_COMMAND entries call this function
|
||||
* through print_verb_option_help() which provides a verbs slice that
|
||||
* starts after the VERB_COMMAND entry.
|
||||
*
|
||||
* In unconverted binaries, this function is called directly. */
|
||||
// TODO: make this function static after everything has been converted.
|
||||
|
||||
if (FLAGS_SET(verb->flags, VERB_COMMAND_MARKER))
|
||||
break;
|
||||
|
||||
assert(verb->verb);
|
||||
|
||||
bool group_marker = FLAGS_SET(verb->flags, VERB_GROUP_MARKER);
|
||||
@@ -256,5 +292,174 @@ int _verbs_get_help_table(
|
||||
|
||||
table_set_header(table, false);
|
||||
*ret = TAKE_PTR(table);
|
||||
|
||||
assert(verb - verbs < INT_MAX);
|
||||
return verb - verbs; /* Return the count of verb entries "consumed". */
|
||||
}
|
||||
|
||||
static void table_array_freep(Table ***tables) {
|
||||
assert(tables);
|
||||
|
||||
for (Table **t = *tables; t && *t; t++)
|
||||
*t = table_unref(*t);
|
||||
free(*tables);
|
||||
}
|
||||
|
||||
static int print_verb_option_help(
|
||||
const CommandDescription *cmd,
|
||||
const Verb verbs[],
|
||||
const Verb verbs_end[],
|
||||
const Option options[],
|
||||
const Option options_end[]) {
|
||||
|
||||
assert(verbs);
|
||||
assert(verbs_end >= verbs);
|
||||
assert((uintptr_t) verbs % sizeof(void*) == 0);
|
||||
|
||||
_cleanup_free_ const char **groups = NULL;
|
||||
_cleanup_(table_array_freep) Table **tables = NULL;
|
||||
size_t n_verbs = 0, n_opts = 0;
|
||||
int r;
|
||||
|
||||
for (const Verb *verb = verbs; verb < verbs_end;) {
|
||||
if (FLAGS_SET(verb->flags, VERB_COMMAND_MARKER))
|
||||
/* Start of entries for another command */
|
||||
break;
|
||||
|
||||
assert(verb->verb);
|
||||
|
||||
/* What is the the first group? */
|
||||
const char *group = FLAGS_SET(verb->flags, VERB_GROUP_MARKER) ? verb->verb : NULL;
|
||||
|
||||
_cleanup_(table_unrefp) Table *table = NULL;
|
||||
r = _verbs_get_help_table(verb, verbs_end, group, &table);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!GREEDY_REALLOC0(tables, n_verbs + 2))
|
||||
return -ENOMEM;
|
||||
if (!GREEDY_REALLOC0(groups, n_verbs + 2))
|
||||
return -ENOMEM;
|
||||
|
||||
tables[n_verbs] = TAKE_PTR(table);
|
||||
groups[n_verbs++] = group ?: "Commands";
|
||||
|
||||
verb += r; /* Skip over the whole verb group */
|
||||
}
|
||||
|
||||
const Option *optns = options_find_namespace(options, options_end, cmd->option_namespace);
|
||||
if (!optns)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN),
|
||||
"Option namespace %s not found.",
|
||||
cmd->option_namespace ?: "(unnamed)");
|
||||
|
||||
for (const Option *opt = optns; opt < options_end;) {
|
||||
_cleanup_(table_unrefp) Table *table = NULL;
|
||||
const char *group;
|
||||
|
||||
if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER))
|
||||
/* End of our namespace */
|
||||
break;
|
||||
|
||||
r = options_get_help_table_group(opt, options_end, &table, &group);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!GREEDY_REALLOC0(tables, n_verbs + n_opts + 2))
|
||||
return -ENOMEM;
|
||||
if (!GREEDY_REALLOC0(groups, n_verbs + n_opts + 2))
|
||||
return -ENOMEM;
|
||||
|
||||
tables[n_verbs + n_opts] = TAKE_PTR(table);
|
||||
groups[n_verbs + n_opts++] = group ?: "Options";
|
||||
|
||||
opt += r; /* Skip over the whole option group */
|
||||
}
|
||||
|
||||
(void) table_sync_all_column_widths(0, tables);
|
||||
|
||||
for (size_t i = 0; i < n_verbs + n_opts; i++) {
|
||||
if (table_isempty(tables[i]))
|
||||
continue;
|
||||
|
||||
help_section(groups[i]);
|
||||
|
||||
r = table_print_or_warn(tables[i]);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int print_man_links(const char *pages) {
|
||||
int r;
|
||||
|
||||
NULSTR_FOREACH(page, pages) {
|
||||
_cleanup_free_ char *link = NULL;
|
||||
|
||||
r = terminal_urlify_man(page, /* section= */ NULL, &link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
printf("\nSee the %s for details.\n", link);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool verbs_array_has_verbs(const Verb verbs[], const Verb verbs_end[]) {
|
||||
for (const Verb *verb = verbs; verb < verbs_end; verb++) {
|
||||
if (FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) /* start of another command section */
|
||||
break;
|
||||
if (!verb->help) /* a hidden verb */
|
||||
continue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int _command_print_help(
|
||||
const Verb verbs[],
|
||||
const Verb verbs_end[],
|
||||
const Option options[],
|
||||
const Option options_end[],
|
||||
const char *name) {
|
||||
int r;
|
||||
|
||||
const Verb *cmdverb = verbs_get_command(verbs, verbs_end, name);
|
||||
if (!cmdverb)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENODATA),
|
||||
"Command %s not known", name);
|
||||
|
||||
const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(cmdverb->data);
|
||||
|
||||
if (cmd->argspec)
|
||||
NULSTR_FOREACH(spec, cmd->argspec) {
|
||||
_cleanup_free_ char *line = strjoin("[OPTION…] ", spec);
|
||||
if (!line)
|
||||
return log_oom();
|
||||
help_cmdline(line);
|
||||
}
|
||||
else {
|
||||
bool have_verbs = verbs_array_has_verbs(cmdverb + 1, verbs_end);
|
||||
help_cmdline(have_verbs ? "[OPTION…] COMMAND …" : "[OPTION…]");
|
||||
}
|
||||
|
||||
if (cmd->abstract)
|
||||
help_abstract(cmd->abstract);
|
||||
|
||||
r = print_verb_option_help(cmd, cmdverb + 1, verbs_end, options, options_end);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to print verb&option help: %m");
|
||||
|
||||
if (cmd->footer)
|
||||
printf("\n%s\n", cmd->footer);
|
||||
|
||||
r = print_man_links(cmd->man_pages);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to print man page links: %m");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+33
-4
@@ -2,17 +2,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "forward.h"
|
||||
#include "options.h"
|
||||
|
||||
#define VERB_ANY (UINT_MAX)
|
||||
|
||||
typedef struct CommandDescription {
|
||||
const char *names; /* nulstr with the main command name and potentially aliases */
|
||||
const char *abstract; /* concrete abstract */
|
||||
const char *argspec; /* optional specification of positional args in synopsis */
|
||||
const char *footer; /* optional footer to print right above man page links */
|
||||
const char *man_pages; /* nulstr with man page names */
|
||||
const char *option_namespace; /* to be used when the options are in a namespace */
|
||||
} CommandDescription;
|
||||
|
||||
typedef enum VerbFlags {
|
||||
VERB_DEFAULT = 1 << 0, /* The verb to run if no verb is specified */
|
||||
VERB_ONLINE_ONLY = 1 << 1, /* Just do nothing when running in chroot or offline */
|
||||
VERB_GROUP_MARKER = 1 << 2, /* Fake verb entry to separate groups */
|
||||
VERB_DEFAULT = 1 << 0, /* The verb to run if no verb is specified */
|
||||
VERB_ONLINE_ONLY = 1 << 1, /* Just do nothing when running in chroot or offline */
|
||||
VERB_COMMAND_MARKER = 1 << 2, /* Header entry with command description */
|
||||
VERB_GROUP_MARKER = 1 << 3, /* Fake verb entry to separate groups */
|
||||
} VerbFlags;
|
||||
|
||||
/* Note: see the comment on struct Option in options.h for why _alignptr_ is required here. */
|
||||
typedef struct _alignptr_ {
|
||||
typedef struct _alignptr_ Verb {
|
||||
const char *verb;
|
||||
unsigned min_args, max_args;
|
||||
VerbFlags flags;
|
||||
@@ -71,6 +82,12 @@ assert_cc(sizeof(Verb) % sizeof(void*) == 0);
|
||||
_VERB_DATA(/* d= */ NULL, /* v= */ gr, /* a= */ NULL, /* amin= */ 0, /* amax= */ 0, \
|
||||
/* f= */ VERB_GROUP_MARKER, /* dat= */ 0, /* h= */ NULL)
|
||||
|
||||
#define _COMMAND(u, ...) \
|
||||
static const CommandDescription UNIQ_T(description, u) = { __VA_ARGS__ }; \
|
||||
_VERB_DATA(/* d= */ NULL, /* v= */ NULL, /* a= */ NULL, /* amin= */ 0, /* amax= */ 0, \
|
||||
/* f= */ VERB_COMMAND_MARKER, /* dat= */ (uintptr_t) &UNIQ_T(description, u), /* h= */ NULL)
|
||||
#define COMMAND(...) _COMMAND(UNIQ, __VA_ARGS__)
|
||||
|
||||
/* This is magically mapped to the beginning and end of the section */
|
||||
extern const Verb __start_SYSTEMD_VERBS[];
|
||||
extern const Verb __stop_SYSTEMD_VERBS[];
|
||||
@@ -107,3 +124,15 @@ int _verbs_get_help_table(
|
||||
#define VERB_COMMON_HELP_HIDDEN(impl) \
|
||||
VERB(verb_help, "help", NULL, VERB_ANY, VERB_ANY, 0, NULL); \
|
||||
_VERB_COMMON_HELP_IMPL(impl)
|
||||
|
||||
int _command_print_help(
|
||||
const Verb verbs[],
|
||||
const Verb verbs_end[],
|
||||
const Option options[],
|
||||
const Option options_end[],
|
||||
const char *name);
|
||||
#define command_print_help(name) \
|
||||
_command_print_help( \
|
||||
__start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, \
|
||||
__start_SYSTEMD_OPTIONS, __stop_SYSTEMD_OPTIONS, \
|
||||
name)
|
||||
|
||||
Reference in New Issue
Block a user