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:
Zbigniew Jędrzejewski-Szmek
2026-08-13 13:23:05 +02:00
parent adbec875af
commit 6c83ea9eda
4 changed files with 282 additions and 0 deletions
+92
View File
@@ -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;
}
+16
View File
@@ -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);
+159
View File
@@ -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;
}
+15
View File
@@ -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)