Add infrastracture to introspect commandline binaries
This adds the helpers to add a hidden --introspect-cli option which prints a machine-readable description of the program's interface and exits. Applicable to any programs that have been converted to VERB_COMMAND(). The output format follows the draft CLI-Introspection Specification (https://github.com/behrmann/uapi-specifications/blob/cli-introspection/specs/cli_introspection.md): a single JSON document with a fixed "mediaType" string and an array of command objects, one per VERB_COMMAND() defined in the binary, so multicall binaries are described in full. Options are taken from the command's option namespace, so e.g. systemd-dissect does not list the mount.ddi mount-helper options. $ build/systemd-id128 --json=pretty --introspect-cli { "mediaType" : "application/vnd.io.systemd.cli-introspection-0", "commands" : [ { "names" : [ "systemd-id128" ], "version" : "262~devel", "features" : [ "-PAM", ... ], "abstract" : [ "Generate and print 128-bit identifiers." ], "options" : [ { "names" : [ "-h", "--help" ], "argument" : "no_argument", "help" : "Show this help" }, ... ], "verbs" : [ { "names" : [ "new" ], "abstract" : [ "Generate a new ID" ], "maxArguments" : 1 }, ... ] } ] } $ build/systemd-id128 --introspect-cli | jq '.commands[0].options[].names[-1]' "--help" "--version" "--no-pager" "--no-legend" "--json" "-j" "--pretty" "--value" "--app-specific" "--uuid" "--introspect-cli"
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user