[PATCH v4 1/5] posix: allow getopt_long to match translated option names
Vivien Kraus
vivien@planete-kraus.eu
Tue Jun 3 19:12:36 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