[PATCH v19 06/11] posix, argp: Support deprecation of long option name translations

Vivien Kraus vivien@planete-kraus.eu
Sat Dec 6 13:19:13 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                 | 55 +++++++++++++++++++++-
 posix/tstgetoptl.c             | 17 ++++---
 posix/tstgetoptl.po            |  4 +-
 8 files changed, 179 insertions(+), 46 deletions(-)

diff --git a/argp/argp-help.c b/argp/argp-help.c
index 9dc9c9b734..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 early 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 8f484c49b0..0979bd82e4 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 5af4609d1a..1ac40f703a 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 ba2a703174..404cae7d74 100644
--- a/posix/getopt.c
+++ b/posix/getopt.c
@@ -191,6 +191,7 @@ exchange (char **argv, struct _getopt_data *d)
 static const int
 match_translated_option_name (char *(*translate) (const char *, const char *,
 						  const char *, char **),
+			      const char *program_name, const char *prefix,
 			      const char *argument, size_t argument_length,
 			      const char *translation_context,
 			      const char *opt_textdomain,
@@ -199,11 +200,61 @@ match_translated_option_name (char *(*translate) (const char *, const char *,
   const char *translated = opt_name;
   char *translation_buffer = NULL;
   int matches = 0;
+  /* 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 = NULL;
+  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, &translation_buffer);
   matches = (!strncmp (translated, argument, argument_length)
 	     && argument_length == strlen (translated));
+  if (!matches && strchr (translated, ' '))
+    /* The translation is a space-separated list of translations. */
+    names_list = translated;
+  while (!matches && 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);
+	    }
+	  matches = 1;
+	}
+      canonical = 0;
+      names_list = next_item;
+    }
   free (translation_buffer);
   return matches;
 }
@@ -254,7 +305,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 1b54ffb4fb..e1da72f0b3 100644
--- a/posix/tstgetoptl.c
+++ b/posix/tstgetoptl.c
@@ -31,8 +31,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 translating "optional" as
    "required".  We also check that getopt only matches translations
    for actual options, by having the user pass --flavour (which is a
@@ -51,7 +55,7 @@ prepare_localedir (void)
   /* Check that the catalog is OK: */
   TEST_COMPARE_STRING (dgettext ("tstgetoptl",
 				 TRANSLATION_CONTEXT "\004" "color"),
-		       "colour");
+		       "colour coolur");
   TEST_COMPARE_STRING (dgettext ("tstgetoptl",
 				 TRANSLATION_CONTEXT "\004" "flavor"),
 		       "flavour");
@@ -64,7 +68,7 @@ prepare_argv (int *argc)
     {
       (char *) "tstgetoptl", (char *) "--required", (char *) "foobar",
       (char *) "--optional=bazbug", (char *) "--col", (char *) "--color",
-      (char *) "--colour", (char *) "--flavour", NULL
+      (char *) "--colour", (char *) "--coolur", (char *) "--flavour", NULL
     };
   *argc = array_length (argv) - 1;
   return argv;
@@ -82,7 +86,8 @@ 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) are handled as a translation of
+	 color.  */
       /* Note that there’s no "--flavor" option, so the "flavor" ->
 	 "flavour" translation is useless.  */
       {NULL, 0, NULL, 0 }
@@ -141,7 +146,7 @@ do_my_test (int with_optctxt)
   printf ("Cflags = %d\n", Cflag);
 
   if (with_optctxt)
-    TEST_COMPARE (Cflag, 3);
+    TEST_COMPARE (Cflag, 4);
   else
     TEST_COMPARE (Cflag, 2);
 
diff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po
index 68be89cfdf..712601a306 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"
 
 #: xxx.c:yy
 msgctxt "command-line option"
-- 
2.34.1



More information about the Libc-alpha mailing list