[PATCH v18 01/11] posix: allow getopt_long to match translated option names
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Mon Dec 1 20:34:28 GMT 2025
On 23/11/25 14:07, Vivien Kraus wrote:
> It is possible to support translated long option names in a program
> with no change to glibc by duplicating the option names in the struct
> options array: one version untranslated, and one version translated.
> However, doing so is significant work for all packages.
>
> 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.
>
> _getopt_internal{_r} has a new argument: a translation function
> pointer. This way, we can pass NULL to avoid linking to gettext in
> the posix version of getopt, or pass something that calls gettext
> otherwise.
>
> The test tstgetoptl is adapted from tstgetopt and modernized to use
> the test driver.
> ---
>
> Dear Adhemerval Zanella Netto, you wrote:
>
> | This patch alone triggers a failure with the new test:
> |
> | $ make test t=posix/tstgetoptl
> | [...]
> | The mo file does not work.
> | Error while setting up localedir.
> | make[2]: Leaving directory '/home/azanella/projects/glibc/glibc-git/posix'
> | FAIL: posix/tstgetoptl
> | original exit status 1
> |
> | However I think checking this patchset earlier I haven't seen this failure.
> | Does this new test requires some other patch to be applied beforehand?
>
> I sometimes have to run a full
>
> $ make check
>
> so that the locales are made usable for tests. I don’t fully
> understand how this works. The failure seems to happen during
> setlocale().
>
> manual/getopt.texi | 25 +++++++--
> posix/Makefile | 12 +++++
> posix/getopt.c | 93 ++++++++++++++++++++++++++------
> posix/getopt1.c | 11 ++--
> posix/getopt_int.h | 9 +++-
> posix/tstgetoptl.c | 126 ++++++++++++++++++++++++++++++++++++++++++++
> posix/tstgetoptl.po | 25 +++++++++
> 7 files changed, 274 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..5ae22a1595 100644
> --- a/manual/getopt.texi
> +++ b/manual/getopt.texi
> @@ -202,6 +202,15 @@ declared in @file{getopt.h}, not @file{unistd.h}. You should make every
> program accept long options if it uses any options, for this takes
> little extra work and helps beginners remember how to use the program.
>
> +Both long option names and their translations provided by the program
> +for the user's current locale are recognized. This helps users of
> +your program who do not speak English understand the meaning of the
> +options, and it does not break the function of the program in scripts
> +if the untranslated option names are used. If international
> +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.
> +
> @deftp {Data Type} {struct option}
> @standards{GNU, getopt.h}
> This structure describes a single long option name for the sake of
> @@ -213,7 +222,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 +259,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 f6421e5379..21761d4088 100644
> --- a/posix/Makefile
> +++ b/posix/Makefile
> @@ -329,8 +329,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)\"
> +
I think this tests fails when invoked in isolation (with make test t=...)
or prior the make check because it requires the en_GB.UTF-8 localte. For
other tests, we explicit add the $(gen-locales) as a pre-requisite to
handle it.
> # Test for the glob symbol version that was replaced in glibc 2.27.
> ifeq ($(have-GLIBC_2.26)$(build-shared),yesyes)
> tests += \
> diff --git a/posix/getopt.c b/posix/getopt.c
> index 66b43850ee..b49dfb643f 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 != NULL)
> + translated = translate (opt_name);
> + return (!strncmp (translated, argument, argument_length)
> + && argument_length == strlen (translated));
> +}
> +
I think that we can optimize it to the following:
if (translated[argument_length] != '\0')
return 0;
return memcmp (translated, argument, argument_length) == 0;
> /* 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);
> + }
I think the usual GNU code style is to omit brackets for one-line directive.
> + }
> d->optopt = pfound->val;
> return '?';
> }
> @@ -356,9 +403,18 @@ 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 +526,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 +630,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 +653,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 +708,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 +762,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 +771,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 +789,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..26691c6831 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));
Same style issue as before, where the current file uses tab and the new code
only use whitespace. I think we should keep the current style.
>
>
> /* 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..ce5cdc209b
> --- /dev/null
> +++ b/posix/tstgetoptl.c
> @@ -0,0 +1,126 @@
> +/* Check that getopt uses translated option names. */
> +/* Copyright (C) 2025 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#include <getopt.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +#include <libintl.h>
> +#include <locale.h>
> +#include <support/check.h>
> +
> +/* This tests that --colour is accepted as a translation of --color.
> + 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
> + precedence over translated options, by translated "optional" as
> + "required". */
Should we also check when a translation is required, but there is no
suitable option to check for the error paths?
> +
> +static void
> +prepare_localedir (void)
> +{
> + unsetenv ("LANGUAGE");
> + TEST_VERIFY_EXIT (setlocale (LC_MESSAGES, "en_GB.UTF-8") != NULL);
We have xsetlocale that already handles calls that should not fail.
> + TEST_VERIFY_EXIT (bindtextdomain ("tstgetoptl", OBJPFX "domaindir") != NULL);
> + TEST_VERIFY_EXIT (textdomain ("tstgetoptl") != NULL);
> + /* Check that the catalog is OK: */
> + TEST_COMPARE_STRING (gettext ("color"), "colour");
> +}
> +
> +static char **
> +prepare_argv (int *argc)
> +{
> + static char *argv[] =
> + {
> + (char *) "tstgetoptl", (char *) "--required", (char *) "foobar",
> + (char *) "--optional=bazbug", (char *) "--col", (char *) "--color",
> + (char *) "--colour", NULL
> + };
> + *argc = (sizeof (argv) / sizeof (argv[0])) - 1;
Use array_length (argv) here.
> + return argv;
> +}
> +
> +static void
> +do_my_test (void)
> +{
> + int argc;
> + char **argv = prepare_argv (&argc);
> + static const struct option options[] =
> + {
> + {"required", required_argument, NULL, 'r'},
> + {"optional", optional_argument, NULL, 'o'},
> + {"color", no_argument, NULL, 'C'},
> + /* Now colour is handled as a translation of color */
> + {NULL, 0, NULL, 0 }
> + };
> +
> + /* This tests the same arguments as tstgetopt.c. */
> +
> + int Cflag = 0;
> + int index;
> + int c;
> +
> + optind = 0;
> + while ((c = getopt_long (argc, argv, "", options, NULL)) >= 0)
> + switch (c)
> + {
> + case 'C':
> + ++Cflag;
> + break;
> + case '?':
> + FAIL ("Unknown option.\n");
> + return;
> + default:
> + /* This should not happen. */
> + support_record_failure_reset ();
> + return;
> +
> + case 'r':
> + printf ("--required %s\n", optarg);
> + TEST_COMPARE_STRING (optarg, "foobar");
> + break;
> + case 'o':
> + printf ("--optional %s\n", optarg);
> + if (optarg != NULL)
> + TEST_COMPARE_STRING (optarg, "bazbug");
> + break;
> + }
> +
> + printf ("Cflags = %d\n", Cflag);
> +
> + TEST_COMPARE (Cflag, 3);
> +
> + for (index = optind; index < argc; index++)
> + printf ("Non-option argument %s\n", argv[index]);
> +
> + TEST_COMPARE (optind, argc);
> +}
> +
> +int
> +do_test (void)
> +{
> + prepare_localedir ();
> + do_my_test ();
> + return 0;
> +}
> +
> +#define TEST_FUNCTION do_test
> +#include <support/test-driver.c>
> 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"
More information about the Libc-alpha
mailing list