]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Add "option parser" infrastracture that helps with cmdline option parsing
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Tue, 24 Feb 2026 15:13:06 +0000 (16:13 +0100)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Fri, 20 Mar 2026 16:26:52 +0000 (17:26 +0100)
The basic idea is that we'll have "one source of truth" for the list of
options. Currently, this is split between:
  1. struct option options[] array for long options
  2. the short option parameter to getopt_long()
  3. --help
so it is easy to forget to add or update one of those places where
appropriate.

An option is defined through a macro that includes the option short
and long codes, and also the metavar and help. Those four items can
be used to generate the help string automatically.

The code is easier to read when various parts are written in the same
order.

We can define common options through a macro in the header file,
reducing boilerplate repeated in different files. Over time, if we
discover that the same pattern is used in multiple files, we can add
another "common option".

The macro is defined in a way that the editor can indent it like a
normal case statement.

The error message for ambiguous options is formatted a bit differently:
$ systemd-id128 --no-
systemd-id128: option '--no-' is ambiguous; possibilities: '--no-pager' '--no-legend'
$ build/systemd-id128 --no-
option '--no-' is ambiguous; possibilities: --no-pager, --no-legend

I think the formatting without commas is ugly, but OTOH, the quotes
around option names are superfluous, real option names are easy to
distinguish.

src/shared/meson.build
src/shared/options.c [new file with mode: 0644]
src/shared/options.h [new file with mode: 0644]
src/shared/verbs.c
src/shared/verbs.h

index bbc03079993242e3eb7a4f1c3164aec74e60068b..e25f855c712f808965b95f856a3721f0cf9fa6bd 100644 (file)
@@ -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 (file)
index 0000000..c5e7057
--- /dev/null
@@ -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 <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 (file)
index 0000000..f548538
--- /dev/null
@@ -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 <gr>.
+ * 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)
index c6d35913b4fc9b35c7b92cb506f8922d4e41c2ab..8c174a6c88de4079770ad62bf57daee55ecd3361 100644 (file)
@@ -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);
 }
index e330156318e96f332a84f355b09bc55ac8077bd4..febb8ccfc24fd030441c7ac5a397a7a3bea35266 100644 (file)
@@ -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);