diff --git a/src/shared/meson.build b/src/shared/meson.build index bbc0307999..e25f855c71 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -143,6 +143,7 @@ shared_sources = files( 'numa-util.c', 'open-file.c', 'openssl-util.c', + 'options.c', 'osc-context.c', 'output-mode.c', 'pager.c', diff --git a/src/shared/options.c b/src/shared/options.c new file mode 100644 index 0000000000..c5e7057bf8 --- /dev/null +++ b/src/shared/options.c @@ -0,0 +1,319 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "format-table.h" +#include "log.h" +#include "options.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strv.h" + +static bool option_takes_arg(const Option *opt) { + return ASSERT_PTR(opt)->metavar; +} + +static bool option_arg_optional(const Option *opt) { + return option_takes_arg(opt) && FLAGS_SET(opt->flags, OPTION_OPTIONAL_ARG); +} + +static bool option_arg_required(const Option *opt) { + return option_takes_arg(opt) && !FLAGS_SET(opt->flags, OPTION_OPTIONAL_ARG); +} + +static bool option_is_metadata(const Option *opt) { + /* A metadata entry that is not a real option, like the group marker */ + return FLAGS_SET(ASSERT_PTR(opt)->flags, OPTION_GROUP_MARKER); +} + +static void kill_arg(char* argv[], int argc, int index) { + assert(index < argc); + assert(!argv[argc]); + + /* Eliminate argv[index] */ + memmove(argv + index, argv + index + 1, (argc - index) * sizeof(char*)); +} + +static void shift_arg(char* argv[], int target, int source) { + assert(argv); + assert(target <= source); + + /* Move argv[source] before argv[target], shifting arguments inbetween */ + char *saved = argv[source]; + memmove(argv + target + 1, argv + target, (source - target) * sizeof(char*)); + argv[target] = saved; +} + +static int partial_match_error( + const Option options[], + const Option options_end[], + const char *optname, + unsigned n_partial_matches) { + int r; + + assert(startswith(ASSERT_PTR(optname), "--")); + assert(n_partial_matches >= 2); + + /* Find options that match the prefix */ + _cleanup_strv_free_ char **s = NULL; + for (const Option* option = options; option < options_end; option++) + if (!option_is_metadata(option) && + option->long_code && + startswith(option->long_code, optname + 2)) { + + r = strv_extendf(&s, "--%s", option->long_code); + if (r < 0) + return log_error_errno(r, "Failed to format message: %m"); + } + + assert(strv_length(s) == n_partial_matches); + + _cleanup_free_ char *p = strv_join_full(s, ", ", /* prefix= */ NULL, /* escape_separator= */ false); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: option '%s' is ambiguous; possibilities: %s", + program_invocation_short_name, optname, strnull(p)); +} + +int option_parse( + const Option options[], + const Option options_end[], + OptionParser *state, + int argc, char *argv[], + const Option **ret_option, + const char **ret_arg) { + + assert(ret_arg); + + /* Check and initialize */ + if (state->optind == 0) { + if (argc < 1 || strv_isempty(argv)) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "argv cannot be empty"); + + *state = (OptionParser) { + .optind = 1, + .positional_offset = 1, + }; + } + + /* Look for the next option */ + + const Option *option; + const char *optname = NULL, *optval = NULL; + _cleanup_free_ char *_optname = NULL; /* allocated option name */ + bool separate_optval = false; + + if (state->short_option_offset == 0) { + /* Skip over non-option parameters */ + for (;;) { + if (state->optind == argc) + return 0; + + if (streq(argv[state->optind], "--")) { + /* No more options. Eliminate "--" so that the list of positional args is clean. */ + kill_arg(argv, argc, state->optind); + return 0; + } + + if (!state->parsing_stopped && + argv[state->optind][0] == '-' && + argv[state->optind][1] != '\0') + /* Looks like we found an option parameter */ + break; + + state->optind++; + } + + /* Find matching option entry. + * First, figure out if we have a long option or a short option. */ + assert(argv[state->optind][0] == '-'); + + if (argv[state->optind][1] == '-') { + /* We have a long option. */ + char *eq = strchr(argv[state->optind], '='); + if (eq) { + optname = _optname = strndup(argv[state->optind], eq - argv[state->optind]); + if (!_optname) + return log_oom(); + + /* joined argument */ + optval = eq + 1; + } else + /* argument (if any) is separate */ + optname = argv[state->optind]; + + const Option *last_partial = NULL; + unsigned n_partial_matches = 0; /* The commandline option matches a defined prefix. */ + + for (option = options;; option++) { + if (option >= options_end) { + if (n_partial_matches == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: unrecognized option '%s'", + program_invocation_short_name, optname); + if (n_partial_matches > 1) + return partial_match_error(options, options_end, optname, n_partial_matches); + + /* just one partial — good */ + option = last_partial; + break; + } + + if (option_is_metadata(option) || !option->long_code) + continue; + + /* Check if the parameter forms a prefix of the option name */ + const char *rest = startswith(option->long_code, optname + 2); + if (!rest) + continue; + if (isempty(rest)) + /* exact match */ + break; + /* partial match */ + last_partial = option; + n_partial_matches++; + } + } else + /* We have a short option */ + state->short_option_offset = 1; + } + + if (state->short_option_offset > 0) { + char optchar = argv[state->optind][state->short_option_offset]; + + if (asprintf(&_optname, "-%c", optchar) < 0) + return log_oom(); + optname = _optname; + + for (option = options;; option++) { + if (option >= options_end) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: unrecognized option '%s'", + program_invocation_short_name, optname); + + if (option_is_metadata(option) || optchar != option->short_code) + continue; + + const char *rest = argv[state->optind] + state->short_option_offset + 1; + + if (option_takes_arg(option) && !isempty(rest)) { + /* The rest of this parameter is the value. */ + optval = rest; + state->short_option_offset = 0; + } else if (isempty(rest)) + state->short_option_offset = 0; + else + state->short_option_offset++; + + break; + } + } + + if (optval && !option_takes_arg(option)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: option '%s' doesn't allow an argument", + program_invocation_short_name, optname); + if (!optval && option_arg_required(option)) { + if (!argv[state->optind + 1]) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: option '%s' requires an argument", + program_invocation_short_name, optname); + optval = argv[state->optind + 1]; + separate_optval = true; + } + + if (state->short_option_offset == 0) { + /* We're done with this option. Adjust the array and position. */ + shift_arg(argv, state->positional_offset++, state->optind++); + if (separate_optval) + shift_arg(argv, state->positional_offset++, state->optind++); + } + + if (FLAGS_SET(option->flags, OPTION_STOPS_PARSING)) + state->parsing_stopped = true; + + if (ret_option) + /* Return the matched Option structure to allow the caller to "know" what was matched */ + *ret_option = option; + *ret_arg = optval; + return option->id; +} + +char** option_parser_get_args(OptionParser *state, int argc, char *argv[]) { + /* Returns positional args as a strv. + * If "--" was found, it has been removed. */ + + assert(state->optind > 0); + return argv + state->positional_offset; +} + +int _option_parser_get_help_table( + const Option options[], + const Option options_end[], + const char *group, + Table **ret) { + int r; + + assert(ret); + + _cleanup_(table_unrefp) Table *table = table_new("names", "help"); + if (!table) + return log_oom(); + + bool in_group = group == NULL; /* Are we currently in the section on the array that forms + * group ? The first part is the default group, so + * the group was not specified, we are in. */ + + for (const Option *opt = options; opt < options_end; opt++) { + bool group_marker = FLAGS_SET(opt->flags, OPTION_GROUP_MARKER); + if (!in_group) { + in_group = group_marker && streq(group, opt->long_code); + continue; + } + if (group_marker) + break; /* End of group */ + + assert(!option_is_metadata(opt)); + + if (!opt->help) + /* No help string — we do not show the option */ + continue; + + char sc[3] = " "; + if (opt->short_code != 0) + xsprintf(sc, "-%c", opt->short_code); + + /* 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. + * + * "=" is shown only when a long option is defined: -l --long=ARG, --long=ARG, -s ARG. + */ + bool need_eq = option_takes_arg(opt) && opt->long_code; + _cleanup_free_ char *s = strjoin( + " ", + sc, + " ", + opt->long_code ? "--" : "", + strempty(opt->long_code), + option_arg_optional(opt) ? "[" : "", + need_eq ? "=" : "", + strempty(opt->metavar), + option_arg_optional(opt) ? "]" : ""); + if (!s) + return log_oom(); + + r = table_add_many(table, TABLE_STRING, s); + if (r < 0) + return table_log_add_error(r); + + _cleanup_strv_free_ char **t = strv_split(opt->help, /* separators= */ NULL); + if (!t) + return log_oom(); + + r = table_add_many(table, TABLE_STRV_WRAPPED, t); + if (r < 0) + return table_log_add_error(r); + } + + table_set_header(table, false); + *ret = TAKE_PTR(table); + return 0; +} diff --git a/src/shared/options.h b/src/shared/options.h new file mode 100644 index 0000000000..f548538bd0 --- /dev/null +++ b/src/shared/options.h @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +typedef enum OptionFlags { + OPTION_OPTIONAL_ARG = 1U << 0, /* Same as optional_argument in getopt */ + OPTION_STOPS_PARSING = 1U << 1, /* This option acts like "--" */ + OPTION_GROUP_MARKER = 1U << 2, /* Fake option entry to separate groups */ +} OptionFlags; + +typedef struct Option { + int id; + OptionFlags flags; + char short_code; + const char *long_code; + const char *metavar; + const char *help; +} Option; + +#define _OPTION(counter, fl, sc, lc, mv, h) \ + _section_("SYSTEMD_OPTIONS") \ + _alignptr_ \ + _used_ \ + _retain_ \ + _variable_no_sanitize_address_ \ + static const Option CONCATENATE(option, counter) = { \ + .id = 0x100 + counter, \ + .flags = fl, \ + .short_code = sc, \ + .long_code = lc, \ + .metavar = mv, \ + .help = h, \ + }; \ + case (0x100 + counter) + +/* Magic entry in the table (which will not be returned) that designates the start of the group . + * The define is structured as 'case' so that it can be followed by ':' and indented appropriately. + */ +#define OPTION_GROUP(gr) \ + _OPTION(__COUNTER__, OPTION_GROUP_MARKER, /* sc= */ 0, /* lc= */ gr, /* mv= */ NULL, /* h= */ NULL) + +#define OPTION_FULL(fl, sc, lc, mv, h) _OPTION(__COUNTER__, fl, sc, lc, mv, h) +#define OPTION(sc, lc, mv, h) OPTION_FULL(/* fl= */ 0, sc, lc, mv, h) +#define OPTION_LONG(lc, mv, h) OPTION(/* sc= */ 0, lc, mv, h) +#define OPTION_SHORT(sc, mv, h) OPTION(sc, /* lc= */ NULL, mv, h) + +#define OPTION_COMMON_HELP \ + OPTION('h', "help", NULL, "Show this help") +#define OPTION_COMMON_VERSION \ + OPTION_LONG("version", NULL, "Show package version") +#define OPTION_COMMON_NO_PAGER \ + OPTION_LONG("no-pager", NULL, "Do not start a pager") +#define OPTION_COMMON_NO_LEGEND \ + OPTION_LONG("no-legend", NULL, "Do not show headers and footers") +#define OPTION_COMMON_JSON \ + OPTION_LONG("json", "FORMAT", "Generate JSON output (pretty, short, or off)") + +/* This is magically mapped to the beginning and end of the section */ +extern const Option __start_SYSTEMD_OPTIONS[]; +extern const Option __stop_SYSTEMD_OPTIONS[]; + +typedef struct OptionParser { + int optind; /* Position of the parameter being handled. + * 0 → option parsing hasn't been started yet. */ + int short_option_offset; /* Set when we're parsing an argument with one or more short options. + * 0 → we're not parsing short options. */ + int positional_offset; /* Offset to where positional parameters are. After processing has been + * finished, all options and their args are to the left of this offset. */ + bool parsing_stopped; /* We processed "--" or an option that terminates option parsing. */ +} OptionParser; + +int option_parse( + const Option options[], + const Option options_end[], + OptionParser *state, + int argc, char *argv[], + const Option **ret_option, + const char **ret_arg); + +/* Iterate over options. */ +#define FOREACH_OPTION_FULL(parser, opt, argc, argv, ret_o, ret_a, on_error) \ + for (int opt; (opt = option_parse(ALIGN_PTR(__start_SYSTEMD_OPTIONS), __stop_SYSTEMD_OPTIONS, parser, argc, argv, ret_o, ret_a)) != 0; ) \ + if (opt < 0) { \ + on_error; \ + break; \ + } else + +#define FOREACH_OPTION(parser, opt, argc, argv, ret_a, on_error) \ + FOREACH_OPTION_FULL(parser, opt, argc, argv, /* ret_o= */ NULL, ret_a, on_error) + +char** option_parser_get_args(OptionParser *state, int argc, char *argv[]); +int _option_parser_get_help_table( + const Option options[], + const Option options_end[], + const char *group, + Table **ret); +#define option_parser_get_help_table_group(group, ret) \ + _option_parser_get_help_table(ALIGN_PTR(__start_SYSTEMD_OPTIONS), __stop_SYSTEMD_OPTIONS, group, ret) +#define option_parser_get_help_table(ret) \ + option_parser_get_help_table_group(/* group= */ NULL, ret) diff --git a/src/shared/verbs.c b/src/shared/verbs.c index c6d35913b4..8c174a6c88 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -68,24 +68,17 @@ const Verb* verbs_find_verb(const char *name, const Verb verbs[]) { return NULL; } -int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) { - const Verb *verb; - const char *name; - int r, left; +int dispatch_verb_with_args(char **args, const Verb verbs[], void *userdata) { + int r; assert(verbs); assert(verbs[0].dispatch); assert(verbs[0].verb); - assert(argc >= 0); - assert(argv); - assert(argc >= optind); - left = argc - optind; - argv += optind; - optind = 0; - name = argv[0]; + const char *name = args ? args[0] : NULL; + size_t left = strv_length(args); - verb = verbs_find_verb(name, verbs); + const Verb *verb = verbs_find_verb(name, verbs); if (!verb) { _cleanup_strv_free_ char **verb_strv = NULL; @@ -120,12 +113,10 @@ int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) { if (!name) left = 1; - if (verb->min_args != VERB_ANY && - (unsigned) left < verb->min_args) + if (verb->min_args != VERB_ANY && left < verb->min_args) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments."); - if (verb->max_args != VERB_ANY && - (unsigned) left > verb->max_args) + if (verb->max_args != VERB_ANY && left > verb->max_args) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments."); if ((verb->flags & VERB_ONLINE_ONLY) && running_in_chroot_or_offline()) { @@ -136,5 +127,17 @@ int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) { if (!name) return verb->dispatch(1, STRV_MAKE(verb->verb), verb->data, userdata); - return verb->dispatch(left, argv, verb->data, userdata); + assert(left < INT_MAX); /* args are derived from argc+argv, so their size must fit in an int. */ + return verb->dispatch(left, args, verb->data, userdata); +} + +int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) { + /* getopt wrapper for _dispatch_verb_with_args. + * TBD: remove this function when all programs with verbs have been converted. */ + + assert(argc >= 0); + assert(argv); + assert(argc >= optind); + + return dispatch_verb_with_args(strv_skip(argv, optind), verbs, userdata); } diff --git a/src/shared/verbs.h b/src/shared/verbs.h index e330156318..febb8ccfc2 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -23,4 +23,5 @@ bool running_in_chroot_or_offline(void); bool should_bypass(const char *env_prefix); const Verb* verbs_find_verb(const char *name, const Verb verbs[]); +int dispatch_verb_with_args(char **args, const Verb verbs[], void *userdata); int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata);