shared/options: split out helper to format -o/--option=

This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-04-26 12:34:14 +02:00
parent beea13b9d1
commit 4339197f5d
3 changed files with 107 additions and 32 deletions
+57 -32
View 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();
+1
View 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[],
+49
View 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);