[PATCH v3 1/6] posix: allow getopt_long to match translated option names

Vivien Kraus vivien@planete-kraus.eu
Sat May 31 21:14:44 GMT 2025


It is possible to support translated long option names in a program by
adding new option records with the translated names in the options
array. However, it is significant work for all packages.

The discussion was started on bug-standards and support in glibc may
be desirable [1].

With this change, getopt will try and match the untranslated options
names, then the translated option names if not found.  Abbreviations
will only match the untranslated names.

[1]: https://lists.gnu.org/archive/html/bug-standards/2025-05/msg00000.html
---
 manual/getopt.texi  |  16 ++++--
 posix/Makefile      |  13 +++++
 posix/getopt.c      |  72 ++++++++++++++++++++++++---
 posix/tstgetoptl.c  | 119 ++++++++++++++++++++++++++++++++++++++++++++
 posix/tstgetoptl.po |  25 ++++++++++
 5 files changed, 232 insertions(+), 13 deletions(-)
 create mode 100644 posix/tstgetoptl.c
 create mode 100644 posix/tstgetoptl.po

diff --git a/manual/getopt.texi b/manual/getopt.texi
index 79a942307c..36d409e1f7 100644
--- a/manual/getopt.texi
+++ b/manual/getopt.texi
@@ -213,7 +213,9 @@ The @code{struct option} structure has these fields:
 
 @table @code
 @item const char *name
-This field is the name of the option.  It is a string.
+This field is the name of the option.  It is a string.  In order for
+@command{getopt_long} to accept either the long option name or its
+translated form, you should mark this string for translation.
 
 @item int has_arg
 This field says whether the option takes an argument.  It is an integer,
@@ -248,10 +250,14 @@ When @code{getopt_long} encounters a short option, it does the same
 thing that @code{getopt} would do: it returns the character code for the
 option, and stores the option's argument (if it has one) in @code{optarg}.
 
-When @code{getopt_long} encounters a long option, it takes actions based
-on the @code{flag} and @code{val} fields of the definition of that
-option.  The option name may be abbreviated as long as the abbreviation is
-unique.
+When @code{getopt_long} encounters a long option or its translation in
+the current textdomain, it takes actions based on the @code{flag} and
+@code{val} fields of the definition of that option.  The english name
+of the option name may be abbreviated as long as the abbreviation is
+unique.  No abbreviation of the translated option name is recognized.
+Since the untranslated option names have precedence over the
+translated option names, it is not possible to hide or divert an
+option with a translation.
 
 If @code{flag} is a null pointer, then @code{getopt_long} returns the
 contents of @code{val} to indicate which option it found.  You should
diff --git a/posix/Makefile b/posix/Makefile
index c0e224236a..587a03f318 100644
--- a/posix/Makefile
+++ b/posix/Makefile
@@ -327,8 +327,20 @@ tests := \
   tst-waitid \
   tst-wordexp-nocmd \
   tstgetopt \
+  tstgetoptl \
   # tests
 
+# tstgetoptl uses a translation catalog for translated option names.
+tstgetoptl_mo = $(objpfx)domaindir/en_GB/LC_MESSAGES/tstgetoptl.mo
+
+$(tstgetoptl_mo): tstgetoptl.po
+	$(make-target-directory)
+	msgfmt -o $@T $<
+	mv -f $@T $@
+
+$(objpfx)tstgetoptl.out: $(tstgetoptl_mo)
+CFLAGS-tstgetoptl.c += -DOBJPFX=\"$(objpfx)\"
+
 # Test for the glob symbol version that was replaced in glibc 2.27.
 ifeq ($(have-GLIBC_2.26)$(build-shared),yesyes)
 tests += \
@@ -599,6 +611,7 @@ CFLAGS-fork.c = $(libio-mtsafe) $(config-cflags-wno-ignored-attributes)
 
 tstgetopt-ARGS = -a -b -cfoobar --required foobar --optional=bazbug \
 		--none random --col --color --colour
+tstgetoptl-ARGS = $(tstgetopt-ARGS)
 
 tst-exec-ARGS = -- $(host-test-program-cmd)
 tst-exec-static-ARGS = $(tst-exec-ARGS)
diff --git a/posix/getopt.c b/posix/getopt.c
index 66b43850ee..bcf2203faf 100644
--- a/posix/getopt.c
+++ b/posix/getopt.c
@@ -182,6 +182,18 @@ exchange (char **argv, struct _getopt_data *d)
   d->__last_nonopt = d->optind;
 }
 
+/* Return 1 iff a translation for opt_name has been found and it
+   matches the substring from argument, length argument_length.
+*/
+static const int
+match_translated_option_name (const char *argument, size_t argument_length,
+                              const char *opt_name)
+{
+  const char *translated = gettext (opt_name);
+  return (!strncmp (translated, argument, argument_length)
+          && argument_length == strlen (translated));
+}
+
 /* Process the argument starting with d->__nextchar as a long option.
    d->optind should *not* have been advanced over this argument.
 
@@ -202,6 +214,7 @@ process_long_option (int argc, char **argv, const char *optstring,
   const struct option *pfound = NULL;
   int n_options;
   int option_index;
+  const char *translated_option_name;
 
   for (nameend = d->__nextchar; *nameend && *nameend != '='; nameend++)
     /* Do nothing.  */ ;
@@ -221,7 +234,23 @@ process_long_option (int argc, char **argv, const char *optstring,
 
   if (pfound == NULL)
     {
-      /* Didn't find an exact match, so look for abbreviations.  */
+      /* 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 (d->__nextchar, namelen, p->name))
+          {
+            /* Exact match found with translation.  */
+            pfound = p;
+            option_index = option_index;
+            break;
+          }
+    }
+
+  if (pfound == NULL)
+    {
+      /* Didn't find an exact match with translations, so look for
+         abbreviations, but only for the option name in the C
+         locale.  */
       unsigned char *ambig_set = NULL;
       int ambig_malloced = 0;
       int ambig_fallback = 0;
@@ -332,6 +361,7 @@ process_long_option (int argc, char **argv, const char *optstring,
   /* We have found a matching long option.  Consume it.  */
   d->optind++;
   d->__nextchar = NULL;
+  translated_option_name = gettext (pfound->name);
   if (*nameend)
     {
       /* Don't test has_arg with >, because some C compilers don't
@@ -341,10 +371,23 @@ process_long_option (int argc, char **argv, const char *optstring,
       else
 	{
 	  if (print_errors)
-	    fprintf (stderr,
-		     _("%s: option '%s%s' doesn't allow an argument\n"),
-		     argv[0], prefix, pfound->name);
-
+            {
+              if (strcmp (translated_option_name, pfound->name) != 0)
+                {
+                  /* Print both names of the option. */
+                  fprintf (stderr,
+                           _("%s: option '%s%s' / '%s%s' doesn't allow an argument\n"),
+                           argv[0], prefix, translated_option_name, prefix, pfound->name);
+                }
+              else
+                {
+                  /* Either the option name is not translated, or its
+                     translation is the same as the option name. */
+                  fprintf (stderr,
+                           _("%s: option '%s%s' doesn't allow an argument\n"),
+                           argv[0], prefix, pfound->name);
+                }
+            }
 	  d->optopt = pfound->val;
 	  return '?';
 	}
@@ -356,9 +399,22 @@ process_long_option (int argc, char **argv, const char *optstring,
       else
 	{
 	  if (print_errors)
-	    fprintf (stderr,
-		     _("%s: option '%s%s' requires an argument\n"),
-		     argv[0], prefix, pfound->name);
+            {
+              /* Same dichotomy as when the option does not allow an
+                 argument. */
+              if (strcmp (translated_option_name, pfound->name) != 0)
+                {
+                  fprintf (stderr,
+                           _("%s: option '%s%s' / '%s%s' requires an argument\n"),
+                           argv[0], prefix, translated_option_name, prefix, pfound->name);
+                }
+              else
+                {
+                  fprintf (stderr,
+                           _("%s: option '%s%s' requires an argument\n"),
+                           argv[0], prefix, pfound->name);
+                }
+            }
 
 	  d->optopt = pfound->val;
 	  return optstring[0] == ':' ? ':' : '?';
diff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c
new file mode 100644
index 0000000000..04b07093e4
--- /dev/null
+++ b/posix/tstgetoptl.c
@@ -0,0 +1,119 @@
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <libintl.h>
+#include <locale.h>
+
+/* This is a modified copy of tstgetopt.c, but instead of having two
+   different options with the same short name, it has only one, and
+   the other one is a translation. */
+
+/* This uses the en_GB locale so that colour means color.  As a
+   special case, we also check that non-translated options have
+   precedence over translated options, by translated "optional" as
+   "required". */
+
+static int
+prepare_localedir (void)
+{
+  unsetenv ("LANGUAGE");
+  setlocale (LC_ALL, "en_GB.UTF-8");
+  if (bindtextdomain ("tstgetoptl", OBJPFX "domaindir") == NULL)
+    {
+      fputs ("Cannot call bindtextdomain.\n", stderr);
+      return -1;
+    }
+  if (textdomain ("tstgetoptl") == NULL)
+    {
+      fputs ("Cannot call textdomain.\n", stderr);
+      return -1;
+    }
+  /* Check that the catalog is OK: */
+  if (strcmp (gettext ("color"), "colour") != 0)
+    {
+      fputs ("The mo file does not work.\n", stderr);
+      return -1;
+    }
+  return 0;
+}
+
+int
+main (int argc, char **argv)
+{
+  static const struct option options[] =
+    {
+      {"required", required_argument, NULL, 'r'},
+      {"optional", optional_argument, NULL, 'o'},
+      {"none",     no_argument,       NULL, 'n'},
+      {"color",    no_argument,       NULL, 'C'},
+      /* Now colour is handled as a translation of color */
+      {NULL,       0,                 NULL, 0 }
+    };
+
+  /* The rest of the function is the same as in tstgetopt.c. */
+
+  int aflag = 0;
+  int bflag = 0;
+  char *cvalue = NULL;
+  int Cflag = 0;
+  int nflag = 0;
+  int index;
+  int c;
+  int result = 0;
+
+  if (prepare_localedir () != 0)
+    {
+      fputs ("Error while setting up localedir.\n", stderr);
+      return 1;
+    }
+  while ((c = getopt_long (argc, argv, "abc:", options, NULL)) >= 0)
+    switch (c)
+      {
+      case 'a':
+	aflag = 1;
+	break;
+      case 'b':
+	bflag = 1;
+	break;
+      case 'c':
+	cvalue = optarg;
+	break;
+      case 'C':
+	++Cflag;
+	break;
+      case '?':
+	fputs ("Unknown option.\n", stderr);
+	return 1;
+      default:
+	fprintf (stderr, "This should never happen!\n");
+	return 1;
+
+      case 'r':
+	printf ("--required %s\n", optarg);
+	result |= strcmp (optarg, "foobar") != 0;
+	break;
+      case 'o':
+	printf ("--optional %s\n", optarg);
+	result |= optarg == NULL || strcmp (optarg, "bazbug") != 0;
+	break;
+      case 'n':
+	puts ("--none");
+	nflag = 1;
+	break;
+      }
+
+  printf ("aflag = %d, bflag = %d, cvalue = %s, Cflags = %d, nflag = %d\n",
+	  aflag, bflag, cvalue, Cflag, nflag);
+
+  result |= (aflag != 1 || bflag != 1 || cvalue == NULL
+	     || strcmp (cvalue, "foobar") != 0 || Cflag != 3 || nflag != 1);
+
+  for (index = optind; index < argc; index++)
+    printf ("Non-option argument %s\n", argv[index]);
+
+  result |= optind + 1 != argc || strcmp (argv[optind], "random") != 0;
+
+  return result;
+}
diff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po
new file mode 100644
index 0000000000..25cd595790
--- /dev/null
+++ b/posix/tstgetoptl.po
@@ -0,0 +1,25 @@
+# English translations for tstgetoptl, a test case in glibc.
+# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the glibc package.
+#
+msgid ""
+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"
+"Language-Team: English (British) <(nothing)>\n"
+"Language: en_GB\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: xxx.c:yy
+msgid "color"
+msgstr "colour"
+
+# This is to make sure the translator cannot redirect options.
+#: xxx.c:yy
+msgid "optional"
+msgstr "required"
-- 
2.49.0



More information about the Libc-alpha mailing list