diff --git a/src/shared/options.c b/src/shared/options.c index ec6c142af3..2bc160f4cd 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-json.h" + #include "format-table.h" #include "log.h" #include "options.h" @@ -636,3 +638,93 @@ int options_get_help_table_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 f4f62ff9b2..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") @@ -291,3 +299,11 @@ int options_get_help_table_group( 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/verbs.c b/src/shared/verbs.c index cc0b358fcd..d6804a9154 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -1,5 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-json.h" + +#include "build.h" #include "env-util.h" #include "format-table.h" #include "help-util.h" @@ -469,3 +472,159 @@ int _command_print_help( 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 52b3cfd9ba..7ae669dc8a 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -151,3 +151,18 @@ int _command_print_help( 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)