]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared/options: split out helper to format -o/--option=
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Sat, 25 Apr 2026 11:31:43 +0000 (13:31 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Sun, 26 Apr 2026 10:34:14 +0000 (12:34 +0200)
src/shared/options.c
src/shared/options.h
src/test/test-options.c

index fbf9a3118175435094e4aa043e83777cccacd5fe..a4c6514508c4370bb709f7a68003b0e05c877d1c 100644 (file)
@@ -311,6 +311,59 @@ size_t option_parser_get_n_args(const OptionParser *state) {
         return state->argc - state->positional_offset;
 }
 
+char* option_get_synopsis(const char *prefix, const Option *opt, const char *joiner, bool show_metavar) {
+        assert(opt);
+        assert(!FLAGS_SET(opt->flags, OPTION_GROUP_MARKER));  /* A group marker should not be displayed */
+
+        if (!prefix)
+                prefix = "";
+
+        if (opt->flags & (OPTION_HELP_ENTRY_VERBATIM | OPTION_POSITIONAL_ENTRY))
+                return strjoin(prefix, ASSERT_PTR(opt->long_code));
+
+        /* The option formatted appropriately for --help strings, error messages, and similar:
+         *   <prefix>-<short><joiner>--<long>=[<metavar>]
+         * "=" is shown only when a long form is defined: -l --long=ARG, --long=ARG, -s ARG.
+         * The joiner arg is used between the short and long forms.
+         * As a special case, if the option has no long form and show_metavar is true,
+         * a space is used ('-a ARG' or '-a [ARG]').
+         */
+        assert(opt->short_code != 0 || opt->long_code);
+
+        char sc[3] = "";
+        if (opt->short_code != 0)
+                xsprintf(sc, "-%c", opt->short_code);
+
+        if (show_metavar && opt->metavar && !opt->long_code)
+                joiner = " ";  /* Return '-x ARG', no matter what joiner was specified. */
+        else if (opt->short_code == 0 || !opt->long_code)
+                joiner = "";
+        else if (!joiner)
+                joiner = " ";
+
+        bool need_eq = option_takes_arg(opt) && opt->long_code;
+        if (!show_metavar)
+                return strjoin(prefix,
+                               sc,
+                               joiner,
+                               opt->long_code ? "--" : "",
+                               strempty(opt->long_code),
+                               need_eq ? "=" : "");
+
+        bool need_quote = opt->metavar && strchr(opt->metavar, ' ');
+        return strjoin(prefix,
+                       sc,
+                       joiner,
+                       opt->long_code ? "--" : "",
+                       strempty(opt->long_code),
+                       option_arg_optional(opt) ? "[" : "",
+                       need_eq ? "=" : "",
+                       need_quote ? "'" : "",
+                       strempty(opt->metavar),
+                       need_quote ? "'" : "",
+                       option_arg_optional(opt) ? "]" : "");
+}
+
 int _option_parser_get_help_table(
                 const Option options[],
                 const Option options_end[],
@@ -341,38 +394,10 @@ int _option_parser_get_help_table(
                         /* No help string — we do not show the option */
                         continue;
 
-                _cleanup_free_ char *s = NULL;
-
-                if (FLAGS_SET(opt->flags, OPTION_HELP_ENTRY_VERBATIM)) {
-                        assert(opt->long_code);
-
-                        s = strjoin("  ",
-                                    opt->long_code);
-                } else {
-                        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;
-                        bool need_quote = opt->metavar && strchr(opt->metavar, ' ');
-                        s = strjoin("  ",
-                                    sc,
-                                    " ",
-                                    opt->long_code ? "--" : "",
-                                    strempty(opt->long_code),
-                                    option_arg_optional(opt) ? "[" : "",
-                                    need_eq ? "=" : "",
-                                    need_quote ? "'" : "",
-                                    strempty(opt->metavar),
-                                    need_quote ? "'" : "",
-                                    option_arg_optional(opt) ? "]" : "");
-                }
+                /* 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. */
+                _cleanup_free_ char *s = option_get_synopsis("  ", opt, " ", /* show_metavar= */ true);
                 if (!s)
                         return log_oom();
 
index f0c19af49c3d9f5ebddec248af922965b815394f..59b20bc047cd397df36e56edd45df05566d57918 100644 (file)
@@ -172,6 +172,7 @@ char* option_parser_consume_next_arg(OptionParser *state);
 
 char** option_parser_get_args(const OptionParser *state);
 size_t option_parser_get_n_args(const OptionParser *state);
+char* option_get_synopsis(const char *prefix, const Option *opt, const char *joiner, bool show_metavar);
 
 int _option_parser_get_help_table(
                 const Option options[],
index 1c9073b7a56d7ccae3ddbff429505dfa92c39552..49e1b3069fc911fd048018efb51f6abacd34b75f 100644 (file)
@@ -1302,4 +1302,53 @@ TEST(option_optional_arg_consume) {
         }
 }
 
+static void test_option_get_synopsis_one(
+                const Option *opt,
+                const char *joiner,
+                bool show_metavar,
+                const char *expected) {
+        log_debug("%s", expected);
+        _cleanup_free_ char *s = option_get_synopsis(". ", opt, joiner, show_metavar);
+        ASSERT_STREQ(s, expected);
+}
+
+TEST(option_get_synopsis) {
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', "xxx", "X" }, "/",  true,  ". -x/--xxx=X");
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', "xxx", "X" }, NULL, true,  ". -x --xxx=X");
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', "xxx", "X" }, "/",  false, ". -x/--xxx=" );
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', "xxx", "X" }, " ",  true,  ". -x --xxx=X");
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', "xxx", "X" }, " ",  false, ". -x --xxx=" );
+        test_option_get_synopsis_one(&(const Option) { 0, 0,   0, "xxx", "X" }, "+",  true,  ". --xxx=X"   );
+        test_option_get_synopsis_one(&(const Option) { 0, 0,   0, "xxx", "X" }, "+",  false, ". --xxx="    );
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', NULL,  "X" }, " ",  true,  ". -x X"      );
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', NULL,  "X" }, "/",  false, ". -x"        );
+
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', "xxx", "A B" }, "/", true,  ". -x/--xxx='A B'");
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', "xxx", "A B" }, " ", true,  ". -x --xxx='A B'");
+        test_option_get_synopsis_one(&(const Option) { 0, 0,   0, "xxx", "A B" }, "+", true,  ". --xxx='A B'"   );
+        test_option_get_synopsis_one(&(const Option) { 0, 0, 'x', NULL,  "A B" }, " ", true,  ". -x 'A B'"      );
+
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', "xxx", "X" }, "/",  true,  ". -x/--xxx[=X]");
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', "xxx", "X" }, NULL, true,  ". -x --xxx[=X]");
+        /* Note: --xxx[=] would be silly, so we show --xxx=. It's a corner case. Maybe this should change. */
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', "xxx", "X" }, "/",  false, ". -x/--xxx="   );
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', "xxx", "X" }, " ",  true,  ". -x --xxx[=X]");
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', "xxx", "X" }, " ",  false, ". -x --xxx="   );
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG,   0, "xxx", "X" }, "+",  true,  ". --xxx[=X]"   );
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG,   0, "xxx", "X" }, "+",  false, ". --xxx="      );
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', NULL,  "X" }, " ",  true,  ". -x [X]"      );
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', NULL,  "X" }, "/",  false, ". -x"          );
+
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', "xxx", "A B" }, "/", true,  ". -x/--xxx[='A B']");
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', "xxx", "A B" }, " ", true,  ". -x --xxx[='A B']");
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG,   0, "xxx", "A B" }, "+", true,  ". --xxx[='A B']"   );
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG, 'x', NULL,  "A B" }, " ", true,  ". -x ['A B']"      );
+
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_OPTIONAL_ARG | OPTION_HELP_ENTRY | OPTION_STOPS_PARSING,
+                                                       'x', "xxx", "A B" }, "/", true,  ". -x/--xxx[='A B']");
+
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_HELP_ENTRY_VERBATIM, 'u', "special special",  "unused" }, "/",  true, ". special special");
+        test_option_get_synopsis_one(&(const Option) { 0, OPTION_POSITIONAL_ENTRY, 'u', "(fixed)", "unused" }, "/",  true, ". (fixed)");
+}
+
 DEFINE_TEST_MAIN(LOG_DEBUG);