[PATCH v5 1/4] posix: allow getopt_long to match translated option names
Vivien Kraus
vivien@planete-kraus.eu
Wed Jun 4 22:06:15 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.
Another argument to the _getopt_internal{_r} is added to avoid linking
to gettext from the posix version of getopt.
[1]: https://lists.gnu.org/archive/html/bug-standards/2025-05/msg00000.html
---
manual/getopt.texi | 16 ++++--
posix/Makefile | 13 +++++
posix/getopt.c | 97 ++++++++++++++++++++++++++++++------
posix/getopt1.c | 11 ++--
posix/getopt_int.h | 9 +++-
posix/tstgetoptl.c | 119 ++++++++++++++++++++++++++++++++++++++++++++
posix/tstgetoptl.po | 25 ++++++++++
7 files changed, 263 insertions(+), 27 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..9540821e1f 100644
--- a/posix/getopt.c
+++ b/posix/getopt.c
@@ -182,6 +182,21 @@ 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 (char *(*translate) (const char *msgid),
+ const char *argument, size_t argument_length,
+ const char *opt_name)
+{
+ const char *translated = opt_name;
+ if (translate)
+ translated = translate (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.
@@ -194,7 +209,8 @@ static int
process_long_option (int argc, char **argv, const char *optstring,
const struct option *longopts, int *longind,
int long_only, struct _getopt_data *d,
- int print_errors, const char *prefix)
+ int print_errors, const char *prefix,
+ char *(*translate) (const char *msgid))
{
char *nameend;
size_t namelen;
@@ -202,6 +218,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 +238,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 (translate, 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 +365,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 = translate (pfound->name);
if (*nameend)
{
/* Don't test has_arg with >, because some C compilers don't
@@ -341,10 +375,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 +403,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] == ':' ? ':' : '?';
@@ -470,7 +530,8 @@ _getopt_initialize (_GL_UNUSED int argc,
int
_getopt_internal_r (int argc, char **argv, const char *optstring,
const struct option *longopts, int *longind,
- int long_only, struct _getopt_data *d, int posixly_correct)
+ int long_only, struct _getopt_data *d, int posixly_correct,
+ char *(*translate) (const char *msgid))
{
int print_errors = d->opterr;
@@ -573,7 +634,8 @@ _getopt_internal_r (int argc, char **argv, const char *optstring,
d->__nextchar = argv[d->optind] + 2;
return process_long_option (argc, argv, optstring, longopts,
longind, long_only, d,
- print_errors, "--");
+ print_errors, "--",
+ translate);
}
/* If long_only and the ARGV-element has the form "-f",
@@ -595,7 +657,8 @@ _getopt_internal_r (int argc, char **argv, const char *optstring,
d->__nextchar = argv[d->optind] + 1;
code = process_long_option (argc, argv, optstring, longopts,
longind, long_only, d,
- print_errors, "-");
+ print_errors, "-",
+ translate);
if (code != -1)
return code;
}
@@ -649,7 +712,8 @@ _getopt_internal_r (int argc, char **argv, const char *optstring,
d->__nextchar = d->optarg;
d->optarg = NULL;
return process_long_option (argc, argv, optstring, longopts, longind,
- 0 /* long_only */, d, print_errors, "-W ");
+ 0 /* long_only */, d, print_errors, "-W ",
+ translate);
}
if (temp[1] == ':')
{
@@ -702,7 +766,7 @@ _getopt_internal_r (int argc, char **argv, const char *optstring,
int
_getopt_internal (int argc, char **argv, const char *optstring,
const struct option *longopts, int *longind, int long_only,
- int posixly_correct)
+ int posixly_correct, char *(*translate) (const char *))
{
int result;
@@ -711,7 +775,7 @@ _getopt_internal (int argc, char **argv, const char *optstring,
result = _getopt_internal_r (argc, argv, optstring, longopts,
longind, long_only, &getopt_data,
- posixly_correct);
+ posixly_correct, translate);
optind = getopt_data.optind;
optarg = getopt_data.optarg;
@@ -729,7 +793,8 @@ _getopt_internal (int argc, char **argv, const char *optstring,
NAME (int argc, char *const *argv, const char *optstring) \
{ \
return _getopt_internal (argc, (char **)argv, optstring, \
- NULL, NULL, 0, POSIXLY_CORRECT); \
+ NULL, NULL, 0, POSIXLY_CORRECT, \
+ NULL); \
}
#ifdef _LIBC
diff --git a/posix/getopt1.c b/posix/getopt1.c
index 733f58122a..6bf0087344 100644
--- a/posix/getopt1.c
+++ b/posix/getopt1.c
@@ -19,6 +19,9 @@
#ifndef _LIBC
# include <config.h>
+# include "gettext.h"
+#else
+# include <libintl.h>
#endif
#include "getopt.h"
@@ -29,7 +32,7 @@ getopt_long (int argc, char *__getopt_argv_const *argv, const char *options,
const struct option *long_options, int *opt_index)
{
return _getopt_internal (argc, (char **) argv, options, long_options,
- opt_index, 0, 0);
+ opt_index, 0, 0, gettext);
}
int
@@ -38,7 +41,7 @@ _getopt_long_r (int argc, char **argv, const char *options,
struct _getopt_data *d)
{
return _getopt_internal_r (argc, argv, options, long_options, opt_index,
- 0, d, 0);
+ 0, d, 0, gettext);
}
/* Like getopt_long, but '-' as well as '--' can indicate a long option.
@@ -52,7 +55,7 @@ getopt_long_only (int argc, char *__getopt_argv_const *argv,
const struct option *long_options, int *opt_index)
{
return _getopt_internal (argc, (char **) argv, options, long_options,
- opt_index, 1, 0);
+ opt_index, 1, 0, gettext);
}
int
@@ -61,7 +64,7 @@ _getopt_long_only_r (int argc, char **argv, const char *options,
struct _getopt_data *d)
{
return _getopt_internal_r (argc, argv, options, long_options, opt_index,
- 1, d, 0);
+ 1, d, 0, gettext);
}
diff --git a/posix/getopt_int.h b/posix/getopt_int.h
index 94c1945c5f..b15b21256a 100644
--- a/posix/getopt_int.h
+++ b/posix/getopt_int.h
@@ -22,10 +22,14 @@
#include <getopt.h>
+/* The translate argument here is optional (can be NULL), it is used
+ to avoid depending on the gettext functions in the posix getopt
+ function. */
extern int _getopt_internal (int ___argc, char **___argv,
const char *__shortopts,
const struct option *__longopts, int *__longind,
- int __long_only, int __posixly_correct);
+ int __long_only, int __posixly_correct,
+ char *(*translate) (const char *msgid));
/* Reentrant versions which can handle parsing multiple argument
@@ -102,7 +106,8 @@ extern int _getopt_internal_r (int ___argc, char **___argv,
const char *__shortopts,
const struct option *__longopts, int *__longind,
int __long_only, struct _getopt_data *__data,
- int __posixly_correct);
+ int __posixly_correct,
+ char *(*translate) (const char *msgid));
extern int _getopt_long_r (int ___argc, char **___argv,
const char *__shortopts,
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