diff --git a/src/ac-power/ac-power.c b/src/ac-power/ac-power.c index 87242a3b08..e52074bcf5 100644 --- a/src/ac-power/ac-power.c +++ b/src/ac-power/ac-power.c @@ -1,13 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-json.h" + #include "battery-util.h" #include "build.h" -#include "format-table.h" -#include "help-util.h" #include "log.h" #include "main-func.h" -#include "options.h" #include "string-util.h" +#include "verbs.h" static bool arg_verbose = false; @@ -16,25 +16,11 @@ static enum { ACTION_LOW, } arg_action = ACTION_AC_POWER; -static int help(void) { - int r; - - _cleanup_(table_unrefp) Table *options = NULL; - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - help_cmdline("[OPTIONS...]"); - help_abstract("Report whether we are connected to an external power source."); - - help_section("Options"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - help_man_page_reference("systemd-ac-power", "1"); - return 0; -} +COMMAND( + "systemd-ac-power\0", + "Report whether we are connected to an external power source.", + .man_pages = "systemd-ac-power.1\0", +); static int parse_argv(int argc, char *argv[]) { @@ -46,7 +32,7 @@ static int parse_argv(int argc, char *argv[]) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-ac-power"); OPTION_COMMON_VERSION: return version(); @@ -58,6 +44,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_LONG("low", NULL, "Check if battery is discharging and low"): arg_action = ACTION_LOW; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } if (option_parser_get_n_args(&opts) > 0) diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index d6675f5eb0..38c17606d7 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -57,8 +57,6 @@ #include "dissect-image.h" #include "dlopen-note.h" #include "extract-word.h" -#include "format-table.h" -#include "help-util.h" #include "image-policy.h" #include "log.h" #include "loop-util.h" @@ -198,62 +196,14 @@ static int verb_transient_settings(int argc, char *argv[], uintptr_t _data, void return 0; } -static int help(void) { - static const char *const vgroups[] = { - "Boot Analysis", - "Dependency Analysis", - "Configuration Files and Search Paths", - "Enumerate OS Concepts", - "Expression Evaluation", - "Clock & Time", - "Unit & Service Analysis", - "Executable Analysis", - "TPM Operations", - }; +COMMAND( + "systemd-analyze\0", + .abstract = "Profile systemd, show unit dependencies, check unit files.", + .man_pages = "systemd-analyze.1\0", + .pager_flags = &arg_pager_flags, +); - Table *vtables[ELEMENTSOF(vgroups)] = {}; - CLEANUP_ELEMENTS(vtables, table_unref_array_clear); - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - pager_open(arg_pager_flags); - - for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { - r = verbs_get_help_table_group(vgroups[i], &vtables[i]); - if (r < 0) - return r; - } - - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - assert_cc(ELEMENTSOF(vtables) == 9); - (void) table_sync_column_widths(0, options, vtables[0], vtables[1], vtables[2], - vtables[3], vtables[4], vtables[5], vtables[6], - vtables[7], vtables[8]); - - help_cmdline("[OPTIONS...] COMMAND ..."); - help_abstract("Profile systemd, show unit dependencies, check unit files."); - - for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { - help_section(vgroups[i]); - r = table_print_or_warn(vtables[i]); - if (r < 0) - return r; - } - - help_section("Options"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - help_man_page_reference("systemd-analyze", "1"); - - return 0; -} - -VERB_COMMON_HELP_HIDDEN(help); +VERB_COMMON_HELP_AUTO_HIDDEN("systemd-analyze"); /* When updating this list, including descriptions, apply changes to * shell-completion/bash/systemd-analyze and shell-completion/zsh/_systemd-analyze too. */ @@ -386,7 +336,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { break; OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-analyze"); OPTION_COMMON_VERSION: return version(); @@ -610,6 +560,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { strv_free_and_replace(arg_debugger_args, l); break; } + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } _cleanup_strv_free_ char **args = strv_copy(option_parser_get_args(&opts)); /* args is [arg1, arg2, …] */ diff --git a/src/ask-password/ask-password.c b/src/ask-password/ask-password.c index fd0def44f1..725e72ba44 100644 --- a/src/ask-password/ask-password.c +++ b/src/ask-password/ask-password.c @@ -9,20 +9,18 @@ #include "build.h" #include "bus-polkit.h" #include "constants.h" -#include "format-table.h" #include "hashmap.h" #include "json-util.h" #include "log.h" #include "main-func.h" -#include "options.h" #include "parse-argument.h" -#include "pretty-print.h" #include "string-table.h" #include "string-util.h" #include "strv.h" #include "time-util.h" #include "varlink-io.systemd.AskPassword.h" #include "varlink-util.h" +#include "verbs.h" static const char *arg_icon = NULL; static const char *arg_id = NULL; /* identifier for 'ask-password' protocol */ @@ -38,35 +36,14 @@ static bool arg_varlink = false; STATIC_DESTRUCTOR_REGISTER(arg_message, freep); -static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - r = terminal_urlify_man("systemd-ask-password", "1", &link); - if (r < 0) - return log_oom(); - - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - printf("%s [OPTIONS...] MESSAGE\n" - "\n%sQuery the user for a passphrase, via the TTY or a UI agent.%s\n" - "\nOptions:\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal()); - r = table_print_or_warn(options); - if (r < 0) - return r; - - printf("\nSee the %s for details.\n", link); - return 0; -} +COMMAND( + "systemd-ask-password\0", + "Query the user for a passphrase, via the TTY or a UI agent.", + .argspec = "MESSAGE\0", + .man_pages = "systemd-ask-password.1\0", +); static int parse_argv(int argc, char *argv[]) { - const char *emoji = NULL; int r; @@ -78,7 +55,7 @@ static int parse_argv(int argc, char *argv[]) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-ask-password"); OPTION_COMMON_VERSION: return version(); @@ -159,6 +136,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_SHORT('n', NULL, "Do not suffix password written to standard output with newline"): arg_newline = false; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } if (isempty(emoji) || streq(emoji, "auto")) diff --git a/src/backlight/backlight.c b/src/backlight/backlight.c index a84640d45d..42b7936abe 100644 --- a/src/backlight/backlight.c +++ b/src/backlight/backlight.c @@ -4,15 +4,14 @@ #include #include "sd-device.h" +#include "sd-json.h" #include "alloc-util.h" -#include "argv-util.h" +#include "build.h" #include "device-private.h" #include "device-util.h" #include "escape.h" #include "fileio.h" -#include "format-table.h" -#include "help-util.h" #include "main-func.h" #include "parse-util.h" #include "percent-util.h" @@ -23,25 +22,12 @@ #define PCI_CLASS_GRAPHICS_CARD 0x30000 -static int help(void) { - _cleanup_(table_unrefp) Table *verbs = NULL; - int r; - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - help_cmdline("COMMAND [backlight|leds]:DEVICE"); - help_abstract("Save and restore backlight brightness at shutdown and boot."); - - help_section("Commands"); - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - help_man_page_reference("systemd-backlight", "8"); - return 0; -} +COMMAND( + "systemd-backlight\0", + "Save and restore backlight brightness at shutdown and boot.", + .argspec = "COMMAND [backlight|leds]:DEVICE\0", + .man_pages = "systemd-backlight.8\0", +); static int has_multiple_graphics_cards(void) { _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; @@ -639,15 +625,45 @@ static int verb_save(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } -static int run(int argc, char *argv[]) { - log_setup(); +VERB_COMMON_HELP_AUTO_HIDDEN("systemd-backlight"); - if (argv_looks_like_help(argc, argv)) - return help(); +static int parse_argv(int argc, char *argv[], char ***ret_args) { + assert(argc >= 0); + assert(argv); + assert(ret_args); + + OptionParser opts = { argc, argv }; + + FOREACH_OPTION_OR_RETURN(c, &opts) + switch (c) { + + OPTION_COMMON_HELP: + return command_print_help("systemd-backlight"); + + OPTION_COMMON_VERSION: + return version(); + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); + } + + *ret_args = option_parser_get_args(&opts); + return 1; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); umask(0022); - return dispatch_verb(strv_skip(argv, 1), /* userdata= */ NULL); + char **args = NULL; + r = parse_argv(argc, argv, &args); + if (r <= 0) + return r; + + return dispatch_verb(args, /* userdata= */ NULL); } DEFINE_MAIN_FUNCTION(run); diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 04701429ad..ed0182950d 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -42,7 +42,6 @@ #include "mountpoint-util.h" #include "namespace-util.h" #include "nsresource.h" -#include "options.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -60,6 +59,7 @@ #include "tmpfile-util.h" #include "uid-classification.h" #include "user-util.h" +#include "verbs.h" #include "vpick.h" static enum { @@ -121,63 +121,28 @@ STATIC_DESTRUCTOR_REGISTER(arg_loop_ref, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_image_filter, image_filter_freep); -static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *options = NULL, *commands = NULL; - int r; - - pager_open(arg_pager_flags); - - r = terminal_urlify_man("systemd-dissect", "1", &link); - if (r < 0) - return log_oom(); - - r = option_parser_get_help_table_ns("systemd-dissect", &options); - if (r < 0) - return r; - - r = option_parser_get_help_table_full("systemd-dissect", "Commands", &commands); - if (r < 0) - return r; - - /* Make the 1st column same width in both tables */ - (void) table_sync_column_widths(0, options, commands); - - printf("%1$s [OPTIONS...] IMAGE\n" - "%1$s [OPTIONS...] --mount IMAGE PATH\n" - "%1$s [OPTIONS...] --umount PATH\n" - "%1$s [OPTIONS...] --attach IMAGE\n" - "%1$s [OPTIONS...] --detach PATH\n" - "%1$s [OPTIONS...] --list IMAGE\n" - "%1$s [OPTIONS...] --mtree IMAGE\n" - "%1$s [OPTIONS...] --with IMAGE [COMMAND…]\n" - "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n" - "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n" - "%1$s [OPTIONS...] --make-archive IMAGE [TARGET]\n" - "%1$s [OPTIONS...] --discover\n" - "%1$s [OPTIONS...] --validate IMAGE\n" - "%1$s [OPTIONS...] --shift IMAGE UIDBASE\n" - "\n%2$sDissect a Discoverable Disk Image (DDI).%3$s\n" - "\n%4$sOptions:%5$s\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal(), - ansi_underline(), - ansi_normal()); - - r = table_print_or_warn(options); - if (r < 0) - return r; - - printf("\n%sCommands:%s\n", ansi_underline(), ansi_normal()); - - r = table_print_or_warn(commands); - if (r < 0) - return r; - - printf("\nSee the %s for details.\n", link); - return 0; -} +COMMAND( + "systemd-dissect\0", + "Dissect a Discoverable Disk Image (DDI).", + .argspec = + "IMAGE\0" + "--mount IMAGE PATH\0" + "--umount PATH\0" + "--attach IMAGE\0" + "--detach PATH\0" + "--list IMAGE\0" + "--mtree IMAGE\0" + "--with IMAGE [COMMAND…]\0" + "--copy-from IMAGE PATH [TARGET]\0" + "--copy-to IMAGE [SOURCE] PATH\0" + "--make-archive IMAGE [TARGET]\0" + "--discover\0" + "--validate IMAGE\0" + "--shift IMAGE UIDBASE\0", + .man_pages = "systemd-dissect.1\0", + .option_namespace = "systemd-dissect", + .pager_flags = &arg_pager_flags, +); static int parse_image_path_argument(const char *path, char **ret_root, char **ret_image) { _cleanup_free_ char *p = NULL; @@ -439,7 +404,7 @@ static int parse_argv(int argc, char *argv[]) { OPTION_GROUP("Commands"): {} OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-dissect"); OPTION_COMMON_VERSION: return version(); @@ -512,6 +477,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_LONG("shift", NULL, "Shift UID range to selected base"): arg_action = ACTION_SHIFT; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } if (system_scope_requested || user_scope_requested) diff --git a/src/home/homectl.c b/src/home/homectl.c index 98c24bafe1..e5c2857134 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -32,7 +32,6 @@ #include "fs-util.h" #include "glyph-util.h" #include "hashmap.h" -#include "help-util.h" #include "hexdecoct.h" #include "home-util.h" #include "homectl-fido2.h" @@ -180,6 +179,14 @@ static int acquire_bus(sd_bus **bus) { return 0; } +COMMAND( + "homectl\0", + "Create, manipulate or inspect home directories.", + .man_pages = "homectl.1\0", + .pager_flags = &arg_pager_flags, + .flags = COMMAND_HELP_SEPARATE, /* the option table is very wide */ +); + VERB_GROUP("Basic User Manipulation Commands"); VERB(verb_list_homes, "list", /* argspec= */ NULL, VERB_ANY, 1, VERB_DEFAULT, "List home areas"); @@ -4002,90 +4009,7 @@ static int parse_fido2_device_field(const char *arg) { return 1; } -static int help(void) { - static const char* const vgroups[] = { - "Basic User Manipulation Commands", - "Advanced User Manipulation Commands", - "User Migration Commands", - "Signing Keys Commands", - "Lock/Unlock Commands", - "Other Commands", - }; - - static const char* const ogroups[] = { - NULL, - "General User Record Properties", - "Authentication User Record Properties", - "Blob Directory User Record Properties", - "Account Management User Record Properties", - "Password Policy User Record Properties", - "Resource Management User Record Properties", - "Storage User Record Properties", - "LUKS Storage User Record Properties", - "Mounting User Record Properties", - "CIFS User Record Properties", - "Login Behaviour User Record Properties", - }; - - Table *vtables[ELEMENTSOF(vgroups)] = {}; - CLEANUP_ELEMENTS(vtables, table_unref_array_clear); - Table *otables[ELEMENTSOF(ogroups)] = {}; - CLEANUP_ELEMENTS(otables, table_unref_array_clear); - int r; - - for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { - r = verbs_get_help_table_group(vgroups[i], &vtables[i]); - if (r < 0) - return r; - } - - for (size_t i = 0; i < ELEMENTSOF(ogroups); i++) { - r = option_parser_get_help_table_group(ogroups[i], &otables[i]); - if (r < 0) - return r; - } - - /* The two groups are not synchronized because the option table is very wide. */ - - assert_cc(ELEMENTSOF(vtables) == 6); - (void) table_sync_column_widths(0, vtables[0], vtables[1], vtables[2], - vtables[3], vtables[4], vtables[5]); - - assert_cc(ELEMENTSOF(otables) == 12); - (void) table_sync_column_widths(0, otables[0], otables[1], otables[2], otables[3], - otables[4], otables[5], otables[6], otables[7], - otables[8], otables[9], otables[10], otables[11]); - - pager_open(arg_pager_flags); - - help_cmdline("[OPTIONS…] COMMAND …"); - help_abstract("Create, manipulate or inspect home directories."); - - for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { - help_section(vgroups[i]); - r = table_print_or_warn(vtables[i]); - if (r < 0) - return r; - } - - help_section("Options"); - r = table_print_or_warn(otables[0]); - if (r < 0) - return r; - - for (size_t i = 1; i < ELEMENTSOF(ogroups); i++) { - help_section(ogroups[i]); - r = table_print_or_warn(otables[i]); - if (r < 0) - return r; - } - - help_man_page_reference("homectl", "1"); - - return 0; -} - -VERB_COMMON_HELP_HIDDEN(help); +VERB_COMMON_HELP_AUTO_HIDDEN("homectl"); static int parse_argv(int argc, char *argv[], char ***remaining_args) { _cleanup_strv_free_ char **arg_languages = NULL; @@ -4114,7 +4038,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("homectl"); OPTION_COMMON_VERSION: return version(); @@ -4929,6 +4853,9 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { OPTION_SHORT('N', NULL, /* help= */ NULL): match_identity = &arg_identity_extra_other_machines; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } if (!strv_isempty(arg_languages)) { diff --git a/src/id128/id128.c b/src/id128/id128.c index 5326ef1f51..54d41b24c9 100644 --- a/src/id128/id128.c +++ b/src/id128/id128.c @@ -14,7 +14,6 @@ #include "main-func.h" #include "options.h" #include "parse-argument.h" -#include "pretty-print.h" #include "string-util.h" #include "strv.h" #include "verbs.h" @@ -26,6 +25,13 @@ static PagerFlags arg_pager_flags = 0; static bool arg_legend = true; static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; +COMMAND( + "systemd-id128\0", + "Generate and print 128-bit identifiers.", + .man_pages = "systemd-id128.1\0", + .pager_flags = &arg_pager_flags, +); + static int print_id(sd_id128_t id) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; int r; @@ -228,46 +234,7 @@ static int verb_show(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } -static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL; - int r; - - r = terminal_urlify_man("systemd-id128", "1", &link); - if (r < 0) - return log_oom(); - - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - /* Make the 1st column same width in both tables */ - (void) table_sync_column_widths(0, options, verbs); - - printf("%s [OPTIONS...] COMMAND\n\n" - "%sGenerate and print 128-bit identifiers.%s\n" - "\nCommands:\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal()); - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - printf("\nOptions:\n"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - printf("\nSee the %s for details.\n", link); - return 0; -} - -VERB_COMMON_HELP(help); +VERB_COMMON_HELP_AUTO("systemd-id128"); static int parse_argv(int argc, char *argv[], char ***ret_args) { int r; @@ -280,7 +247,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-id128"); OPTION_COMMON_VERSION: return version(); @@ -326,6 +293,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION('u', "uuid", NULL, "Output in UUID format"): arg_mode = ID128_PRINT_UUID; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } if (arg_mode == ID128_PRINT_PRETTY && sd_json_format_enabled(arg_json_format_flags)) diff --git a/src/notify/notify.c b/src/notify/notify.c index 667a7a6447..46acaec6f1 100644 --- a/src/notify/notify.c +++ b/src/notify/notify.c @@ -6,6 +6,7 @@ #include "sd-daemon.h" #include "sd-event.h" +#include "sd-json.h" #include "alloc-util.h" #include "build.h" @@ -15,13 +16,10 @@ #include "exit-status.h" #include "fd-util.h" #include "fdset.h" -#include "format-table.h" #include "format-util.h" -#include "help-util.h" #include "log.h" #include "main-func.h" #include "notify-recv.h" -#include "options.h" #include "parse-util.h" #include "pidref.h" #include "process-util.h" @@ -30,6 +28,7 @@ #include "strv.h" #include "time-util.h" #include "user-util.h" +#include "verbs.h" static enum { ACTION_NOTIFY, @@ -56,27 +55,15 @@ STATIC_DESTRUCTOR_REGISTER(arg_exec, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_fds, fdset_freep); STATIC_DESTRUCTOR_REGISTER(arg_fdname, freep); -static int help(void) { - int r; - - _cleanup_(table_unrefp) Table *options = NULL; - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - help_cmdline("[OPTIONS...] [VARIABLE=VALUE...]"); - help_cmdline("[OPTIONS...] --exec [VARIABLE=VALUE...] ; -- CMDLINE..."); - help_cmdline("[OPTIONS...] --fork -- CMDLINE..."); - help_abstract("Notify the service manager about service status updates."); - - help_section("Options"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - help_man_page_reference("systemd-notify", "1"); - return 0; -} +COMMAND( + "systemd-notify\0", + "Notify the service manager about service status updates.", + .argspec = + "[VARIABLE=VALUE…]\0" + "--exec [VARIABLE=VALUE…] ; -- CMDLINE…\0" + "--fork -- CMDLINE…\0", + .man_pages = "systemd-notify.1\0", +); static int get_manager_pid(PidRef *ret) { int r; @@ -159,7 +146,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-notify"); OPTION_COMMON_VERSION: return version(); @@ -273,6 +260,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION('q', "quiet", NULL, "Do not show PID of child when forking"): arg_quiet = true; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } bool have_env = arg_ready || arg_stopping || arg_reloading || arg_status || pidref_is_set(&arg_pid) || !fdset_isempty(arg_fds); @@ -283,7 +273,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { case ACTION_NOTIFY: { if (arg_fdname && fdset_isempty(arg_fds)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No file descriptors passed, but --fdname= set, refusing."); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No file descriptors passed, but --fdname= used, refusing."); size_t n_arg_env; @@ -308,14 +298,10 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { n_arg_env = strv_length(args); have_env = have_env || n_arg_env > 0; - if (!have_env) { - if (do_exec) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No notify message specified while --exec, refusing."); - - /* No argument at all? */ - help(); - return -EINVAL; - } + if (!have_env) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + do_exec ? "No notify message specified with --exec, refusing." : + "Nothing to notify about was specified."); if (n_arg_env > 0) { arg_env = strv_copy_n(args, n_arg_env); @@ -417,7 +403,6 @@ static int on_child(sd_event_source *s, const siginfo_t *si, void *userdata) { } static int action_fork(char *const *_command) { - static const int forward_signals[] = { SIGHUP, SIGTERM, diff --git a/src/resolve/resolvconf-compat.c b/src/resolve/resolvconf-compat.c index dcac5c130e..b85fc9ada7 100644 --- a/src/resolve/resolvconf-compat.c +++ b/src/resolve/resolvconf-compat.c @@ -2,18 +2,18 @@ #include +#include "sd-json.h" + #include "alloc-util.h" #include "build.h" #include "extract-word.h" #include "fileio.h" -#include "format-table.h" -#include "help-util.h" #include "log.h" -#include "options.h" #include "resolvconf-compat.h" #include "resolvectl.h" #include "string-util.h" #include "strv.h" +#include "verbs.h" typedef enum LookupType { LOOKUP_TYPE_REGULAR, @@ -21,35 +21,22 @@ typedef enum LookupType { LOOKUP_TYPE_EXCLUSIVE, /* -x */ } LookupType; -static int resolvconf_help(void) { - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - r = option_parser_get_help_table_ns("resolvconf", &options); - if (r < 0) - return r; - - help_cmdline("-a INTERFACE 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 +498,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 +514,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 +566,165 @@ int _option_parser_get_help_table_full( table_set_header(table, false); *ret = TAKE_PTR(table); + 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; +} + +static int option_build_json(const Option *opt, const char *group, sd_json_variant **ret) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *names = NULL; + int r; + + assert(opt); + assert(opt->short_code != 0 || opt->long_code); + assert(ret); + + if (opt->short_code != 0) { + char s[3] = { '-', opt->short_code }; + + r = sd_json_variant_append_arrayb(&names, SD_JSON_BUILD_STRING(s)); + if (r < 0) + return r; + } + + if (opt->long_code) { + _cleanup_free_ char *s = strjoin("--", opt->long_code); + if (!s) + return -ENOMEM; + + r = sd_json_variant_append_arrayb(&names, SD_JSON_BUILD_STRING(s)); + if (r < 0) + return r; + } + + const char *argtype = + option_arg_required(opt) ? "required_argument" : + option_arg_optional(opt) ? "optional_argument" : + "no_argument"; + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_VARIANT("names", names), + SD_JSON_BUILD_PAIR_STRING("argument", argtype), + SD_JSON_BUILD_PAIR_CONDITION( + !!opt->metavar, + "metavar", SD_JSON_BUILD_STRING(opt->metavar)), + SD_JSON_BUILD_PAIR_CONDITION(!!opt->help, "help", SD_JSON_BUILD_STRING(opt->help)), + SD_JSON_BUILD_PAIR_CONDITION( + !!group, + "group", SD_JSON_BUILD_STRV(STRV_MAKE(group)))); +} + +int options_build_json( + const Option options[], + const Option options_end[], + const char *namespace, + sd_json_variant **ret) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + const char *group = NULL; + int r; + + assert(ret); + + const Option *start = options_find_namespace(options, options_end, namespace); + if (!start) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Option namespace %s not found.", + namespace ?: "(unnamed)"); + + for (const Option *opt = start; opt < options_end; opt++) { + if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER)) + break; /* End of our namespace */ + + if (FLAGS_SET(opt->flags, OPTION_GROUP_MARKER)) { + group = opt->long_code; + continue; + } + + if (option_is_metadata(opt)) + /* Positional and help entries are only used for display */ + continue; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *o = NULL; + r = option_build_json(opt, group, &o); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object for option '%s': %m", + opt->long_code ?: CHAR_TO_STR(opt->short_code)); + + r = sd_json_variant_append_array(&array, o); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object to array: %m"); + } + + *ret = TAKE_PTR(array); return 0; } diff --git a/src/shared/options.h b/src/shared/options.h index 561cd349e8..8fe0cae2e5 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -95,6 +95,14 @@ assert_cc(sizeof(Option) % sizeof(void*) == 0); #define OPTION_COMMON_VERSION \ OPTION_LONG("version", NULL, "Show package version") +#define OPTION_COMMON_INTROSPECT_CLI \ + /* This option is internal-only for now and not shown in --help */ \ + OPTION_LONG("introspect-cli", NULL, /* help= */ NULL) + +/* For programs which do not do option parsing, but check the argv array directly. */ +#define OPTION_COMMON_INTROSPECT_CLI_ENTRY \ + _OPTION_ENTRY(__COUNTER__, /* fl= */ 0, /* sc= */ 0, "introspect-cli", /* mv= */ NULL, /* d= */ 0u, /* h= */ NULL) + #define OPTION_COMMON_NO_PAGER \ OPTION_LONG("no-pager", NULL, "Do not start a pager") @@ -266,6 +274,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 +293,17 @@ 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); + +/* Build a JSON array of CLI-introspection option objects for the options in the given namespace. + * Returns NULL in *ret if the namespace defines no options. */ +int options_build_json( + const Option options[], + const Option options_end[], + const char *namespace, + sd_json_variant **ret); 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..0b27f4850c 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -1,8 +1,17 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-json.h" + +#include "build.h" #include "env-util.h" +#include "extract-word.h" #include "format-table.h" +#include "help-util.h" #include "log.h" +#include "nulstr-util.h" +#include "options.h" +#include "pager.h" +#include "pretty-print.h" #include "string-util.h" #include "strv.h" #include "terminal-util.h" @@ -58,15 +67,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 +96,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 +224,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 +264,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 +297,379 @@ 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 */ + } + + if (cmd->flags & COMMAND_HELP_SEPARATE) + (void) table_sync_all_column_widths(0, tables); + + 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 */ + } + + if (cmd->flags & COMMAND_HELP_SEPARATE) + (void) table_sync_all_column_widths(0, tables + n_verbs); + else + (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; +} + +static int print_wrapped(const char *text, bool abstract) { + + /* Print the text wrapped at spaces to the specified width. Newlines embedded in the text + * are preserved and each line is wrapped independently, so explicit line breaks (e.g. + * before an example command line) can be used where needed. */ + + if (!text) + return 0; + + _cleanup_strv_free_ char **lines = NULL, **lines2 = NULL; + int r; + + size_t width = MIN(columns(), 80U); /* This is a reasonable default. */ + + r = strv_split_full(&lines, text, "\n", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + + r = strv_rebreak_lines(lines, width, &lines2); + if (r < 0) + return r; + + /* Seperate this block from the previous input. */ + putchar('\n'); + + STRV_FOREACH(line, lines2) + if (abstract) + printf("%s%s%s%s\n", + ansi_highlight(), + ansi_add_italics(), + *line, + ansi_normal()); + else + puts(*line); + + return 0; +} + +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->pager_flags) + pager_open(*cmd->pager_flags); + + 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…]"); + } + + r = print_wrapped(cmd->abstract, /* abstract= */ true); + if (r < 0) + return r; + + 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"); + + r = print_wrapped(cmd->footer, /* abstract= */ false); + if (r < 0) + return r; + + r = print_man_links(cmd->man_pages); + if (r < 0) + return log_error_errno(r, "Failed to print man page links: %m"); + + return 0; +} + +static int verb_build_json(const Verb *verb, sd_json_variant **ret) { + int r; + + assert(verb); + assert(verb->verb); + assert(ret); + + /* Verbs are represented as command objects, as described in the CLI-Introspection + * Specification (https://uapi-group.org/specifications/specs/cli_introspection/). + * + * The "minArguments", "maxArguments", "isDefault", and "isOnlineOnly" fields + * are extensions not (yet) covered by the specification. Note that the argument + * counts include the verb itself. */ + + r = sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRV("names", STRV_MAKE(verb->verb)), + SD_JSON_BUILD_PAIR_CONDITION( + !!verb->help, + "abstract", SD_JSON_BUILD_STRV(STRV_MAKE(verb->help))), + SD_JSON_BUILD_PAIR_CONDITION( + verb->min_args != VERB_ANY, + "minArguments", SD_JSON_BUILD_UNSIGNED(verb->min_args)), + SD_JSON_BUILD_PAIR_CONDITION( + verb->max_args != VERB_ANY, + "maxArguments", SD_JSON_BUILD_UNSIGNED(verb->max_args)), + SD_JSON_BUILD_PAIR_CONDITION( + FLAGS_SET(verb->flags, VERB_DEFAULT), + "isDefault", SD_JSON_BUILD_BOOLEAN(true)), + SD_JSON_BUILD_PAIR_CONDITION( + FLAGS_SET(verb->flags, VERB_ONLINE_ONLY), + "isOnlineOnly", SD_JSON_BUILD_BOOLEAN(true))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + return 0; +} + +static int command_build_json( + const Verb *cmdverb, + const Verb verbs_end[], + const Option options[], + const Option options_end[], + sd_json_variant **ret) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *names = NULL, *opts = NULL, *cmds = NULL; + int r; + + assert(cmdverb); + assert(FLAGS_SET(cmdverb->flags, VERB_COMMAND_MARKER)); + assert(ret); + + const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(cmdverb->data); + + NULSTR_FOREACH(name, cmd->names) { + r = sd_json_variant_append_arrayb(&names, SD_JSON_BUILD_STRING(name)); + if (r < 0) + return log_error_errno(r, "Failed to append JSON string to array: %m"); + } + assert(names); /* At least the primary name must be defined */ + + r = options_build_json(options, options_end, cmd->option_namespace, &opts); + if (r < 0) + return r; + + for (const Verb *verb = cmdverb + 1; verb < verbs_end; verb++) { + if (FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) + break; /* Start of entries for another command */ + + if (FLAGS_SET(verb->flags, VERB_GROUP_MARKER)) + continue; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *o = NULL; + r = verb_build_json(verb, &o); + if (r < 0) + return r; + + r = sd_json_variant_append_array(&cmds, o); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object to array: %m"); + } + + _cleanup_strv_free_ char **features = strv_split(systemd_features, /* separators= */ NULL); + if (!features) + return log_oom(); + + r = sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_VARIANT("names", names), + SD_JSON_BUILD_PAIR_STRING("version", PROJECT_VERSION_FULL), + SD_JSON_BUILD_PAIR_STRV("features", features), + SD_JSON_BUILD_PAIR_CONDITION( + !!cmd->abstract, + "abstract", SD_JSON_BUILD_STRV(STRV_MAKE(cmd->abstract))), + SD_JSON_BUILD_PAIR_CONDITION( + !!cmd->footer, + "footer", SD_JSON_BUILD_STRV(STRV_MAKE(cmd->footer))), + SD_JSON_BUILD_PAIR_CONDITION(!!opts, "options", SD_JSON_BUILD_VARIANT(opts)), + SD_JSON_BUILD_PAIR_CONDITION(!!cmds, "verbs", SD_JSON_BUILD_VARIANT(cmds))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + return 0; +} + +int _introspect_cli( + const Verb verbs[], + const Verb verbs_end[], + const Option options[], + const Option options_end[], + sd_json_format_flags_t flags) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *commands = NULL, *w = NULL; + int r; + + assert(verbs); + assert(verbs_end > verbs); + assert((uintptr_t) verbs % sizeof(void*) == 0); + + if (flags == SD_JSON_FORMAT_OFF) + flags = SD_JSON_FORMAT_PRETTY_AUTO; + + for (const Verb *verb = verbs; verb < verbs_end; verb++) { + if (!FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) + continue; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *c = NULL; + r = command_build_json(verb, verbs_end, options, options_end, &c); + if (r < 0) + return r; + + r = sd_json_variant_append_array(&commands, c); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object to array: %m"); + } + + if (!commands) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), + "No command description defined, cannot introspect."); + + r = sd_json_buildo( + &w, + SD_JSON_BUILD_PAIR_STRING( + "mediaType", + "application/vnd.io.systemd.cli-introspection-0"), + SD_JSON_BUILD_PAIR_VARIANT("commands", commands)); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + r = sd_json_variant_dump(w, flags, stdout, /* prefix= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to print JSON object: %m"); + return 0; } diff --git a/src/shared/verbs.h b/src/shared/verbs.h index b7ecfbcb6d..cbe5b4e9cb 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -2,17 +2,34 @@ #pragma once #include "forward.h" +#include "options.h" #define VERB_ANY (UINT_MAX) +typedef enum CommandFlags { + COMMAND_HELP_SEPARATE = 1 << 0, /* Do not synchronize the width between verbs and options */ +} CommandFlags; + +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 */ + const PagerFlags *pager_flags; /* optional pointer to the runtime pager flags variable */ + CommandFlags flags; +} 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 +88,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 +130,40 @@ 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) + +#define VERB_COMMON_HELP_AUTO(program) \ + VERB_FULL(verb_help_auto, "help", NULL, VERB_ANY, VERB_ANY, 0, (uintptr_t) program, "Show this help") + +#define VERB_COMMON_HELP_AUTO_HIDDEN(program) \ + VERB_FULL(verb_help_auto, "help", NULL, VERB_ANY, VERB_ANY, 0, (uintptr_t) program, NULL) + +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) + +static inline int verb_help_auto(int argc, char **argv, uintptr_t data, void *userdata) { + return command_print_help((const char*) ASSERT_PTR(data)); +} + +/* Print a machine-readable description of the program's commands, verbs, and options in the format + * defined by the CLI-Introspection Specification. One command object is emitted for each + * VERB_COMMAND() in the verb table, so multicall binaries are described in full. */ +int _introspect_cli( + const Verb verbs[], + const Verb verbs_end[], + const Option options[], + const Option options_end[], + sd_json_format_flags_t flags); +#define introspect_cli(flags) \ + _introspect_cli( \ + __start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, \ + __start_SYSTEMD_OPTIONS, __stop_SYSTEMD_OPTIONS, \ + flags) diff --git a/src/timedate/timedatectl.c b/src/timedate/timedatectl.c index 2d76488f90..9926748cbc 100644 --- a/src/timedate/timedatectl.c +++ b/src/timedate/timedatectl.c @@ -24,7 +24,6 @@ #include "parse-argument.h" #include "parse-util.h" #include "polkit-agent.h" -#include "pretty-print.h" #include "runtime-scope.h" #include "sparse-endian.h" #include "string-table.h" @@ -180,6 +179,13 @@ static int print_status_info(const StatusInfo *i) { return 0; } +COMMAND( + "timedatectl\0", + .abstract = "Query or change system time and date settings.", + .man_pages = "timedatectl.1\0", + .pager_flags = &arg_pager_flags, +); + VERB_DEFAULT_NOARG(verb_status, "status", "Show current time settings"); static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { StatusInfo info = {}; @@ -893,54 +899,7 @@ static int verb_revert(int argc, char *argv[], uintptr_t _data, void *userdata) return 0; } -static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *verbs = NULL, *verbs2 = NULL, *options = NULL; - int r; - - r = terminal_urlify_man("timedatectl", "1", &link); - if (r < 0) - return log_oom(); - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - r = verbs_get_help_table_group("systemd-timesyncd Commands", &verbs2); - if (r < 0) - return r; - - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - (void) table_sync_column_widths(0, verbs, verbs2, options); - - printf("%s [OPTIONS...] COMMAND ...\n" - "\n%sQuery or change system time and date settings.%s\n" - "\nCommands:\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal()); - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - printf("\nsystemd-timesyncd Commands:\n"); - r = table_print_or_warn(verbs2); - if (r < 0) - return r; - - printf("\nOptions:\n"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - printf("\nSee the %s for details.\n", link); - return 0; -} - -VERB_COMMON_HELP_HIDDEN(help); +VERB_COMMON_HELP_AUTO_HIDDEN("timedatectl"); static int parse_argv(int argc, char *argv[], char ***ret_args) { int r; @@ -953,7 +912,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("timedatectl"); OPTION_COMMON_VERSION: return version(); @@ -1005,6 +964,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION('a', "all", NULL, "Show all properties, including empty ones"): SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true); break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } *ret_args = option_parser_get_args(&opts); diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index 48c500f7c4..2a1c9a1ce2 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -23,7 +23,6 @@ #include "format-table.h" #include "format-util.h" #include "fs-util.h" -#include "help-util.h" #include "log.h" #include "main-func.h" #include "memfd-util.h" @@ -81,47 +80,6 @@ static void push_fds_done(PushFds *p) { STATIC_DESTRUCTOR_REGISTER(arg_graceful, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_push_fds, push_fds_done); -static int help(void) { - _cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL; - int r; - - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - (void) table_sync_column_widths(0, options, verbs); - - pager_open(arg_pager_flags); - - help_cmdline("[OPTIONS...] COMMAND ..."); - help_abstract("Introspect Varlink Services."); - - help_section("Commands"); - - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - help_section("Options"); - - r = table_print_or_warn(options); - if (r < 0) - return r; - - printf("\nWith --exec, specify the command to invoke:\n" - " %s --exec call ADDRESS METHOD PARAMS -- CMDLINE…\n", - program_invocation_short_name); - - help_man_page_reference("varlinkctl", "1"); - return 0; -} - -VERB_COMMON_HELP(help); - static int parse_argv(int argc, char *argv[], char ***ret_args) { int r; @@ -134,7 +92,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("varlinkctl"); OPTION_COMMON_VERSION: return version(); @@ -248,6 +206,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { arg_push_fds.fds[arg_push_fds.n_fds++] = TAKE_FD(add_fd); break; } + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } /* If more than one reply is expected, imply JSON-SEQ output, and set SD_JSON_FORMAT_FLUSH */ @@ -325,6 +286,15 @@ static void get_info_data_done(GetInfoData *d) { d->interfaces = strv_free(d->interfaces); } +COMMAND( + "varlinkctl\0", + "Introspect Varlink Services.", + .man_pages = "varlinkctl.1\0", + .footer = "With --exec, specify the command to invoke:\n" + " varlinkctl --exec call ADDRESS METHOD PARAMS -- CMDLINE…", + .pager_flags = &arg_pager_flags, +); + VERB(verb_info, "info", "ADDRESS", 2, 2, 0, "Show service information"); VERB(verb_info, "list-interfaces", "ADDRESS", 2, 2, 0, "List interfaces implemented by service"); static int verb_info(int argc, char *argv[], uintptr_t _data, void *userdata) { @@ -1488,6 +1458,8 @@ static int verb_serve(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } +VERB_COMMON_HELP_AUTO("varlinkctl"); + static int run(int argc, char *argv[]) { int r; diff --git a/src/veritysetup/veritysetup.c b/src/veritysetup/veritysetup.c index 8e8543ba5b..afce00bbe7 100644 --- a/src/veritysetup/veritysetup.c +++ b/src/veritysetup/veritysetup.c @@ -4,16 +4,15 @@ #include #include "sd-device.h" +#include "sd-json.h" #include "alloc-util.h" -#include "argv-util.h" +#include "build.h" #include "cryptsetup-util.h" #include "dlopen-note.h" #include "extract-word.h" #include "fileio.h" -#include "format-table.h" #include "fstab-util.h" -#include "help-util.h" #include "hexdecoct.h" #include "log.h" #include "main-func.h" @@ -51,26 +50,6 @@ STATIC_DESTRUCTOR_REGISTER(arg_fec_what, freep); STATIC_DESTRUCTOR_REGISTER(arg_root_hash_signature, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_nvpcr, freep); -static int help(void) { - _cleanup_(table_unrefp) Table *verbs = NULL; - int r; - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - help_cmdline("COMMAND ..."); - help_abstract("Attach or detach a verity protected block device."); - - help_section("Commands"); - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - help_man_page_reference("systemd-veritysetup@.service", "8"); - return 0; -} - static int parse_roothashsig_option(const char *option, bool strict) { _cleanup_free_ void *rhs = NULL; size_t rhss = 0; @@ -318,8 +297,14 @@ static int parse_options(const char *options) { return r; } +COMMAND( + "systemd-veritysetup\0", + "Attach or detach a verity-protected block device.", + .man_pages = "systemd-veritysetup@.service.8\0", +); + VERB(verb_attach, "attach", "VOLUME DATADEVICE HASHDEVICE ROOTHASH [OPTIONS]", 5, 6, 0, - "Attach a verity protected block device"); + "Attach a verity-protected block device"); static int verb_attach(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _cleanup_free_ void *rh = NULL; @@ -457,7 +442,7 @@ static int verb_attach(int argc, char *argv[], uintptr_t _data, void *userdata) } VERB(verb_detach, "detach", "VOLUME", 2, 2, 0, - "Detach a verity protected block device"); + "Detach a verity-protected block device"); static int verb_detach(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; int r; @@ -486,11 +471,39 @@ static int verb_detach(int argc, char *argv[], uintptr_t _data, void *userdata) return 0; } +VERB_COMMON_HELP_AUTO_HIDDEN("systemd-veritysetup"); + +static int parse_argv(int argc, char *argv[], char ***ret_args) { + assert(argc >= 0); + assert(argv); + assert(ret_args); + + OptionParser opts = { argc, argv }; + + FOREACH_OPTION_OR_RETURN(c, &opts) + switch (c) { + + OPTION_COMMON_HELP: + return command_print_help("systemd-veritysetup"); + + OPTION_COMMON_VERSION: + return version(); + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); + } + + *ret_args = option_parser_get_args(&opts); + return 1; +} + static int run(int argc, char *argv[]) { int r; - if (argv_looks_like_help(argc, argv)) - return help(); + char **args = NULL; + r = parse_argv(argc, argv, &args); + if (r <= 0) + return r; log_setup(); @@ -503,7 +516,7 @@ static int run(int argc, char *argv[]) { umask(0022); - return dispatch_verb(strv_skip(argv, 1), /* userdata= */ NULL); + return dispatch_verb(args, /* userdata= */ NULL); } DEFINE_MAIN_FUNCTION(run); diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh new file mode 100755 index 0000000000..8e8789bd31 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Just a smoke test for the introspection code +for i in systemd-ac-power systemd-analyze systemd-ask-password systemd-dissect \ + systemd-id128 systemd-notify homectl resolvectl timedatectl varlinkctl; do + command -v "$i" >/dev/null || continue + + $i --introspect-cli | jq -e \ + '.mediaType == "application/vnd.io.systemd.cli-introspection-0"' + $i --introspect-cli | jq -e --arg name "$i" \ + 'any(.commands[]; .names[0] == $name)' + $i --introspect-cli | jq -e \ + 'any(.commands[]; [.options[].names[-1]] | contains(["--help", "--version", "--introspect-cli"]))' + $i --intro | grep -e --help +done + +# Those are not in $PATH and do not do regular option parsing, so the option must +# be given verbatim +for i in /usr/lib/systemd/systemd-backlight /usr/lib/systemd/systemd-veritysetup; do + [[ -x "$i" ]] || continue + + $i --introspect-cli | jq -e \ + '.mediaType == "application/vnd.io.systemd.cli-introspection-0"' +done + +# systemd-id128 defines verbs, check that they are described +systemd-id128 --introspect-cli | jq -e \ + '.commands[0].verbs | map(.names[0]) | contains(["new", "machine-id", "show", "help"])' + +# resolvectl is a multicall binary, check that both commands are described +resolvectl --introspect-cli | jq -e \ + '[.commands[].names[0]] | sort == ["resolvconf", "resolvectl", "systemd-resolve"]'