[PATCH v18 06/11] posix, argp: Support deprecation of long option name translations
Vivien Kraus
vivien@planete-kraus.eu
Sun Nov 23 17:07:33 GMT 2025
The translation for an option name is expected to be a space-separated
list. The first field is the canonical translation. The other fields
are deprecated translations. getopt will warn if they are recognized,
and argp will not print them in help or usage output.
I chose a space because it would be a bad choice to have a space
character in an option name.
---
argp/argp-help.c | 84 +++++++++++++++++++++-------------
argp/tst-argphelp-localized.c | 51 ++++++++++++++++++++-
argp/tst-argphelp-localized.po | 7 ++-
argp/tst-argpusage-localized.c | 2 +-
manual/getopt.texi | 5 ++
posix/getopt.c | 62 +++++++++++++++++++++++--
posix/tstgetoptl.c | 17 ++++---
posix/tstgetoptl.po | 4 +-
8 files changed, 184 insertions(+), 48 deletions(-)
diff --git a/argp/argp-help.c b/argp/argp-help.c
index 615708f7f7..34ecc19b38 100644
--- a/argp/argp-help.c
+++ b/argp/argp-help.c
@@ -1205,33 +1205,50 @@ comma (unsigned col, struct pentry_state *pest)
indent_to (pest->stream, col);
}
-/* Help and usage output show the translated option name. *allocated
- holds a pointer that should be freed by the caller, or a NULL
- pointer. */
+/* Return the canonical translation of an option name. There might be
+ multiple alternative translation for backward compatibility, but
+ only one should be documented. */
static const char *
-translate_option_name (const char *name, char **allocated)
+canonical_option_translation (const char *name,
+ char **allocated)
{
/* Argp does not have a configuration for the context, so a default
one is used. */
/* FIXME: use pgettext_expr. */
+ char *full_msgid = NULL;
+ const char *all_names = NULL;
*allocated = NULL;
- if (__asprintf (allocated, "command-line option\004%s", name) == -1)
+ if (__asprintf (&full_msgid, "command-line option\004%s", name) == -1)
{
/* *allocated is NULL */
+ free (full_msgid);
return name;
}
- const char *translated = gettext (*allocated);
- if (strcmp (translated, *allocated) == 0)
+ all_names = gettext (full_msgid);
+ if (strcmp (all_names, full_msgid) == 0)
{
- /* No translation performed. */
- free (*allocated);
- *allocated = NULL;
+ /* No translation available: return the untranslated name. */
+ /* *allocated still NULL */
+ free (full_msgid);
return name;
}
- /* FIXME: is it safe to discard *allocated here? Won’t the return
- value alias it? */
- /* *allocated is to be freed by the caller. */
- return translated;
+ /* all_names is a space-separated list, keep only the first
+ token. */
+ size_t canonical_length = strlen (all_names);
+ if (strchr (all_names, ' '))
+ canonical_length = strchr (all_names, ' ') - all_names;
+ *allocated = malloc (canonical_length + 1);
+ if (*allocated == NULL)
+ {
+ /* Continue without translations. */
+ /* *allocated still NULL */
+ free (full_msgid);
+ return name;
+ }
+ memcpy (*allocated, all_names, canonical_length);
+ (*allocated)[canonical_length] = '\0';
+ free (full_msgid);
+ return *allocated;
}
/* Print help for ENTRY to STREAM. */
@@ -1242,7 +1259,8 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,
unsigned num;
const struct argp_option *real = entry->opt, *opt;
char *so = entry->short_options;
- const char *translated_option_name;
+ char *canonical_translation_buffer;
+ const char *canonical_translation;
int have_long_opt = 0; /* We have any long options. */
/* Saved margins. */
int old_lm = __argp_fmtstream_set_lmargin (stream, 0);
@@ -1306,14 +1324,15 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,
if (opt->name && ovisible (opt))
{
comma (uparams.long_opt_col, &pest);
- char *name_allocated = NULL;
- translated_option_name = translate_option_name (opt->name, &name_allocated);
- __argp_fmtstream_printf (stream, "--%s", translated_option_name);
+ canonical_translation =
+ canonical_option_translation (opt->name,
+ &canonical_translation_buffer);
+ __argp_fmtstream_printf (stream, "--%s", canonical_translation);
arg (real, "=%s", "[=%s]",
state == NULL ? NULL : state->root_argp->argp_domain, stream);
- if (strcmp (translated_option_name, opt->name))
+ if (strcmp (canonical_translation, opt->name))
__argp_fmtstream_printf (stream, " (--%s)", opt->name);
- free (name_allocated);
+ free (canonical_translation_buffer);
}
}
@@ -1455,7 +1474,8 @@ usage_long_opt (const struct argp_option *opt,
{
argp_fmtstream_t stream = cookie;
const char *arg = opt->arg;
- const char *translated_option_name = opt->name;
+ const char *canonical_translation = opt->name;
+ char *canonical_translation_buffer;
int flags = opt->flags | real->flags;
if (! arg)
@@ -1463,31 +1483,29 @@ usage_long_opt (const struct argp_option *opt,
if (! (flags & OPTION_NO_USAGE))
{
- char *name_allocated = NULL;
- translated_option_name =
- translate_option_name (opt->name, &name_allocated);
- int translation_differs =
- (strcmp (translated_option_name, opt->name) != 0);
+ canonical_translation =
+ canonical_option_translation (opt->name, &canonical_translation_buffer);
if (arg)
{
arg = dgettext (domain, arg);
- if ((flags & OPTION_ARG_OPTIONAL) && translation_differs)
+ if ((flags & OPTION_ARG_OPTIONAL)
+ && strcmp (canonical_translation, opt->name) != 0)
__argp_fmtstream_printf (stream, " [--%s[=%s] (--%s)]",
- translated_option_name, arg, opt->name);
+ canonical_translation, arg, opt->name);
else if (flags & OPTION_ARG_OPTIONAL)
__argp_fmtstream_printf (stream, " [--%s[=%s]]", opt->name, arg);
- else if (translation_differs)
+ else if (strcmp (canonical_translation, opt->name) != 0)
__argp_fmtstream_printf (stream, " [--%s=%s (--%s)]",
- translated_option_name, arg, opt->name);
+ canonical_translation, arg, opt->name);
else
__argp_fmtstream_printf (stream, " [--%s=%s]", opt->name, arg);
}
- else if (translation_differs)
+ else if (strcmp (canonical_translation, opt->name) != 0)
__argp_fmtstream_printf (stream, " [--%s (--%s)]",
- translated_option_name, opt->name);
+ canonical_translation, opt->name);
else
__argp_fmtstream_printf (stream, " [--%s]", opt->name);
- free (name_allocated);
+ free (canonical_translation_buffer);
}
return 0;
diff --git a/argp/tst-argphelp-localized.c b/argp/tst-argphelp-localized.c
index 610dadb3f3..8401808e88 100644
--- a/argp/tst-argphelp-localized.c
+++ b/argp/tst-argphelp-localized.c
@@ -46,6 +46,8 @@ struct argp_option options[] =
};
static int color_set = 0;
+static int flavor_set = 0;
+static int texture_set = 0;
static error_t
parse_opt (int key, char *arg, struct argp_state *state)
@@ -55,6 +57,14 @@ parse_opt (int key, char *arg, struct argp_state *state)
FAIL("color already set.\n");
else if (key == 'c')
color_set = 1;
+ else if (key == 'f' && flavor_set)
+ FAIL ("flavor already set.\n");
+ else if (key == 'f')
+ flavor_set = 1;
+ else if (key == 't' && texture_set)
+ FAIL ("texture already set.\n");
+ else if (key == 't')
+ texture_set = 1;
return 0;
}
@@ -66,6 +76,16 @@ do_test (void)
char *test1_argv[3] =
{ (char *) "/bin/tst-argphelp-localized", (char *) "--colour", NULL };
char *test2_argv[3] =
+ { (char *) "/bin/tst-argphelp-localized", (char *) "--color", NULL };
+ char *test3_argv[3] =
+ { (char *) "/bin/tst-argphelp-localized", (char *) "--coolur", NULL };
+ char *test4_argv[3] =
+ { (char *) "/bin/tst-argphelp-localized", (char *) "--flavour", NULL };
+ char *test5_argv[3] =
+ { (char *) "/bin/tst-argphelp-localized", (char *) "--flavor", NULL };
+ char *test6_argv[3] =
+ { (char *) "/bin/tst-argphelp-localized", (char *) "--texture", NULL };
+ char *test7_argv[3] =
{ (char *) "/bin/tst-argphelp-localized", (char *) "--help", NULL };
unsetenv ("LANGUAGE");
@@ -74,17 +94,44 @@ do_test (void)
TEST_VERIFY_EXIT (textdomain ("tst-argphelp-localized") != NULL);
/* Check that the catalog is OK: */
TEST_COMPARE_STRING (gettext ("command-line option\004color"),
- "colour");
+ "colour coolur");
argp_parse (&argp, 2, test1_argv, 0, 0, NULL);
TEST_VERIFY (color_set);
+ TEST_VERIFY (!flavor_set);
+ TEST_VERIFY (!texture_set);
color_set = 0;
+ argp_parse (&argp, 2, test2_argv, 0, 0, NULL);
+ TEST_VERIFY (color_set);
+ TEST_VERIFY (!flavor_set);
+ TEST_VERIFY (!texture_set);
+ color_set = 0;
+ argp_parse (&argp, 2, test3_argv, 0, 0, NULL);
+ TEST_VERIFY (color_set);
+ TEST_VERIFY (!flavor_set);
+ TEST_VERIFY (!texture_set);
+ color_set = 0;
+ argp_parse (&argp, 2, test4_argv, 0, 0, NULL);
+ TEST_VERIFY (!color_set);
+ TEST_VERIFY (flavor_set);
+ TEST_VERIFY (!texture_set);
+ flavor_set = 0;
+ argp_parse (&argp, 2, test5_argv, 0, 0, NULL);
+ TEST_VERIFY (!color_set);
+ TEST_VERIFY (flavor_set);
+ TEST_VERIFY (!texture_set);
+ flavor_set = 0;
+ argp_parse (&argp, 2, test6_argv, 0, 0, NULL);
+ TEST_VERIFY (!color_set);
+ TEST_VERIFY (!flavor_set);
+ TEST_VERIFY (texture_set);
+ texture_set = 0;
/* This is the last chance to fail. */
if (support_record_failure_is_failed ())
FAIL_EXIT1("There were test failures before the final invocation of --help");
/* This last test will exit the program with code 0 and ignore
previous failures. */
- argp_parse (&argp, 2, test2_argv, 0, 0, NULL);
+ argp_parse (&argp, 2, test7_argv, 0, 0, NULL);
FAIL_EXIT1("--help did not exit the program");
return 0;
}
diff --git a/argp/tst-argphelp-localized.po b/argp/tst-argphelp-localized.po
index 4e301bf278..e87330f7b5 100644
--- a/argp/tst-argphelp-localized.po
+++ b/argp/tst-argphelp-localized.po
@@ -15,4 +15,9 @@ msgstr ""
#: tst-argphelp-localized.c:73
msgctxt "command-line option"
msgid "color"
-msgstr "colour"
\ No newline at end of file
+msgstr "colour coolur"
+
+#: tst-argphelp-localized.c:74
+msgctxt "command-line option"
+msgid "flavor"
+msgstr "flavour"
\ No newline at end of file
diff --git a/argp/tst-argpusage-localized.c b/argp/tst-argpusage-localized.c
index 43cb449954..63c614a8eb 100644
--- a/argp/tst-argpusage-localized.c
+++ b/argp/tst-argpusage-localized.c
@@ -65,7 +65,7 @@ do_test (void)
TEST_VERIFY_EXIT (textdomain ("tst-argphelp-localized") != NULL);
/* Check that the catalog is OK: */
TEST_COMPARE_STRING (gettext ("command-line option\004color"),
- "colour");
+ "colour coolur");
/* This is the last chance to fail. */
if (support_record_failure_is_failed ())
FAIL_EXIT1("There were test failures before the final invocation of --usage");
diff --git a/manual/getopt.texi b/manual/getopt.texi
index 16fa608418..17d238c255 100644
--- a/manual/getopt.texi
+++ b/manual/getopt.texi
@@ -228,6 +228,11 @@ communication involves the invocation of your program, the program
users should be encouraged to use untranslated option names or publish
the locale used for this invocation.
+If the translation of an option name contains a space character, then
+it means multiple translations recognize the same option name. This
+is useful to upgrade a translation without disrupting the user's
+workflow.
+
@deftp {Data Type} {struct option}
@standards{GNU, getopt.h}
This structure describes a single long option name for the sake of
diff --git a/posix/getopt.c b/posix/getopt.c
index a362e7c14f..ef5116278a 100644
--- a/posix/getopt.c
+++ b/posix/getopt.c
@@ -191,16 +191,70 @@ exchange (char **argv, struct _getopt_data *d)
static const int
match_translated_option_name (char *(*translate) (const char *, const char *,
const char *),
+ const char *program_name, const char *prefix,
const char *argument, size_t argument_length,
const char *translation_context,
const char *opt_textdomain,
const char *opt_name)
{
const char *translated = opt_name;
+ /* Multiple alternative names can be provided by the translator, so
+ that continuous improvement of translations is possible. To
+ allow multiple translations, separate the translation with a
+ space character. */
+ int canonical = 1;
+ const char *names_list;
+ const char *next_item;
+ size_t canonical_length;
+ char *canonical_name;
+ char *matched_name;
+ size_t item_length;
if (translate != NULL && !__libc_enable_secure)
translated = translate (opt_textdomain, translation_context, opt_name);
- return (!strncmp (translated, argument, argument_length)
- && argument_length == strlen (translated));
+ if (strlen (translated) == argument_length
+ && strncmp (translated, argument, argument_length) == 0
+ && !strchr (translated, ' '))
+ return 1;
+ else if (!strchr (translated, ' '))
+ return 0;
+ names_list = translated;
+ while (names_list != NULL)
+ {
+ item_length = strlen (names_list);
+ next_item = strchr (names_list, ' ');
+ if (next_item)
+ {
+ item_length = next_item - names_list;
+ next_item++;
+ }
+ if (item_length == argument_length
+ && strncmp (names_list, argument, argument_length) == 0)
+ {
+ if (!canonical)
+ {
+ canonical_length = strchr (translated, ' ') - translated;
+ canonical_name = malloc (canonical_length + 1);
+ matched_name = malloc (item_length + 1);
+ if (canonical_name != NULL && matched_name != NULL)
+ {
+ memcpy (canonical_name, translated, canonical_length);
+ canonical_name[canonical_length] = '\0';
+ memcpy (matched_name, names_list, item_length);
+ matched_name[item_length] = '\0';
+ fprintf (stderr, _("%s: option '%s%s' is deprecated, use '%s%s' instead\n"),
+ program_name,
+ prefix, matched_name,
+ prefix, canonical_name);
+ }
+ free (canonical_name);
+ free (matched_name);
+ }
+ return 1;
+ }
+ canonical = 0;
+ names_list = next_item;
+ }
+ return 0;
}
/* Process the argument starting with d->__nextchar as a long option.
@@ -248,7 +302,9 @@ process_long_option (int argc, char **argv, const char *optstring,
/* Didn't find an exact match, try with translated option
names. */
for (p = longopts, option_index = 0; p->name; p++, option_index++)
- if (match_translated_option_name (translate, d->__nextchar, namelen,
+ if (match_translated_option_name (translate,
+ argv[0], prefix,
+ d->__nextchar, namelen,
d->optctxt, d->opttextdomain,
p->name))
{
diff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c
index 2e9720f73f..439a9b1716 100644
--- a/posix/tstgetoptl.c
+++ b/posix/tstgetoptl.c
@@ -29,8 +29,12 @@
This echoes tstgetopt.c, where --colour was an option name alias
for --color, so it had to be listed twice. */
-/* This uses the en_GB locale so that colour means color. As a
- special case, we also check that non-translated options have
+/* This uses the en_GB locale so that colour means color. Oh no! The
+ translator made a mistake and translated with “coolur”. A bug-fix
+ has been released, but it has been decided to support both “colour”
+ and “coolur” with a deprecation warning.
+
+ As a special case, we also check that non-translated options have
precedence over translated options, by translated "optional" as
"required". */
@@ -44,7 +48,8 @@ prepare_localedir (void)
TEST_VERIFY_EXIT (bindtextdomain ("tstgetoptl", OBJPFX "domaindir") != NULL);
TEST_VERIFY_EXIT (textdomain ("tstgetoptl") != NULL);
/* Check that the catalog is OK: */
- TEST_COMPARE_STRING (dgettext ("tstgetoptl", TRANSLATION_CONTEXT "\004" "color"), "colour");
+ TEST_COMPARE_STRING (dgettext ("tstgetoptl", TRANSLATION_CONTEXT "\004" "color"),
+ "colour coolur");
}
static char **
@@ -54,7 +59,7 @@ prepare_argv (int *argc)
{
(char *) "tstgetoptl", (char *) "--required", (char *) "foobar",
(char *) "--optional=bazbug", (char *) "--col", (char *) "--color",
- (char *) "--colour", NULL
+ (char *) "--colour", (char *) "--coolur", NULL
};
*argc = (sizeof (argv) / sizeof (argv[0])) - 1;
return argv;
@@ -72,7 +77,7 @@ do_my_test (int with_optctxt)
{"required", required_argument, NULL, 'r'},
{"optional", optional_argument, NULL, 'o'},
{"color", no_argument, NULL, 'C'},
- /* Now colour is handled as a translation of color */
+ /* Now colour (and coolur) is handled as a translation of color */
{NULL, 0, NULL, 0 }
};
@@ -125,7 +130,7 @@ do_my_test (int with_optctxt)
/* We should not have continued past the '?' case if optctxt is
missing. */
TEST_COMPARE (with_optctxt, 1);
- TEST_COMPARE (Cflag, 3);
+ TEST_COMPARE (Cflag, 4);
for (index = optind; index < argc; index++)
printf ("Non-option argument %s\n", argv[index]);
diff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po
index e060c0d6e3..f418776a6b 100644
--- a/posix/tstgetoptl.po
+++ b/posix/tstgetoptl.po
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: tstgetoptl 0.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-27 19:29+0200\n"
-"PO-Revision-Date: 2025-05-27 19:30+0200\n"
+"PO-Revision-Date: 2025-06-06 21:22+0200\n"
"Language-Team: English (British) <(nothing)>\n"
"Language: en_GB\n"
"MIME-Version: 1.0\n"
@@ -18,7 +18,7 @@ msgstr ""
#: xxx.c:yy
msgctxt "command-line option"
msgid "color"
-msgstr "colour"
+msgstr "colour coolur"
# This is to make sure the translator cannot redirect options.
#: xxx.c:yy
--
2.34.1
More information about the Libc-alpha
mailing list