diff --git a/src/shared/forward.h b/src/shared/forward.h index 38c8dc03e5..323163a1d6 100644 --- a/src/shared/forward.h +++ b/src/shared/forward.h @@ -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; diff --git a/src/shared/options.c b/src/shared/options.c index 41f9c6fd28..ec6c142af3 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -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( * ? 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; } diff --git a/src/shared/options.h b/src/shared/options.h index 561cd349e8..f4f62ff9b2 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -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); diff --git a/src/shared/pretty-print.c b/src/shared/pretty-print.c index c5ace49f60..1b978832f7 100644 --- a/src/shared/pretty-print.c +++ b/src/shared/pretty-print.c @@ -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); } diff --git a/src/shared/verbs.c b/src/shared/verbs.c index 34e8a897d6..726ed30ac2 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -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 ? 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; } diff --git a/src/shared/verbs.h b/src/shared/verbs.h index b7ecfbcb6d..0cbaba8203 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -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)