Re: [PATCH v22 1/9] posix: allow getopt_long to match translated option names
Adhemerval Zanella Netto <[email protected]> Wed, 5 Aug 2026 09:29:10 -0300
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Organization | Linaro |
| Message-ID | <[email protected]> |
On 23/04/26 13:03, 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.
> ---
> NEWS | 2 +
> manual/getopt.texi | 25 ++++++--
> posix/Makefile | 13 +++++
> posix/getopt.c | 92 ++++++++++++++++++++++++-----
> posix/getopt1.c | 11 ++--
> posix/getopt_int.h | 9 ++-
> posix/tstgetoptl.c | 139 ++++++++++++++++++++++++++++++++++++++++++++
> posix/tstgetoptl.po | 29 +++++++++
> 8 files changed, 293 insertions(+), 27 deletions(-)
> create mode 100644 posix/tstgetoptl.c
> create mode 100644 posix/tstgetoptl.po
>
> diff --git a/NEWS b/NEWS
> index eac9322161..4e53a079e9 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -18,6 +18,8 @@ Major new features:
>
> * New locale added: hrx_BR (Hunsrik language spoken in Brazil).
>
> +* The getopt_long function now accepts translated long option names.
> +
> Deprecated and removed features, and other changes affecting compatibility:
>
> * Although malloc and related functions currently return pointers
> 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 a5e5162c61..8755f42bdc 100644
> --- a/posix/Makefile
> +++ b/posix/Makefile
> @@ -332,6 +332,7 @@ tests := \
> tst-wordexp-nocmd \
> tst-wordexp-reuse \
> tstgetopt \
> + tstgetoptl \
> # tests
>
> # Test for the glob symbol version that was replaced in glibc 2.27.
> @@ -529,6 +530,7 @@ LOCALES := \
> da_DK.ISO-8859-1 \
> de_DE.ISO-8859-1 \
> de_DE.UTF-8 \
> + en_GB.UTF-8 \
> en_US.UTF-8 \
> es_US.ISO-8859-1 \
> es_US.UTF-8 \
> @@ -802,3 +804,14 @@ tst-wordexp-reuse-ENV += MALLOC_TRACE=$(objpfx)tst-wordexp-reuse.mtrace \
> $(objpfx)tst-wordexp-reuse-mem.out: $(objpfx)tst-wordexp-reuse.out
> $(common-objpfx)malloc/mtrace $(objpfx)tst-wordexp-reuse.mtrace > $@; \
> $(evaluate-test)
> +
> +# 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) $(gen-locales)
> +CFLAGS-tstgetoptl.c += -DOBJPFX=\"$(objpfx)\"
> diff --git a/posix/getopt.c b/posix/getopt.c
> index 3e10579670..cdc02d4da9 100644
> --- a/posix/getopt.c
> +++ b/posix/getopt.c
> @@ -182,6 +182,24 @@ exchange (char **argv, struct _getopt_data *d)
> d->__last_nonopt = d->optind;
> }
>
> +/* Return true iff a translation for opt_name has been found and it
> + matches the substring from argument, length argument_length.
> +*/
> +static bool
I think it would require to explicit include stdbool.h for gnulib sync.
> +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);
> +
> + if (strncmp (translated, argument, argument_length) != 0)
> + return false;
> + /* We know that argument is a prefix of translated. */
> + return translated[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 +212,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 +221,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 +241,22 @@ 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;
> + break;
> + }
> + }
> +
So for instance with the option --col with options = {"color", "verbose"} if the
selection just before this snippets finds no option named col; this new code looks
up each option's translation in the catalog.
If the translator rendered verbose as col, verbose is selected; only this snippet
fails does block 3 resolve --col as the unique abbreviation of --color.
I give us it a bougs translation definition, it only affects programs that opted in,
and only in the affected locale. But it is arguebily a hole in this design that at
least I think it should be documented.
> + 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;
> @@ -341,10 +376,20 @@ 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);
> -
> + {
> + translated_option_name = translate (pfound->name);
The match_translated_option_name guards translate != NULL, but this calls translate
unconditionally. Not reachable with NULL in-tree today, but might be an issue
if _getopt_internal caller callls with translate == NULL with long options.
> + 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 +401,19 @@ 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. */
> + translated_option_name = translate (pfound->name);
> + 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 +525,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 +629,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 +652,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 +707,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 +761,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 +770,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 +788,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 e6fc867de0..6342e2d417 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);
I think we should make the the opt-in mechanism (from patch 02) from the first
commit, so no intermediate commit ships always-on behavior.
> }
>
> 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 023d8a4fe6..579233b08c 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..afefcf46f5
> --- /dev/null
> +++ b/posix/tstgetoptl.c
> @@ -0,0 +1,139 @@
> +/* Check that getopt uses translated option names. */
> +/* Copyright (C) 2026 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 <array_length.h>
> +#include <support/support.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 translating "optional" as
> + "required". We also check that getopt only matches translations
> + for actual options, by having the user pass --flavour (which is a
> + known translation of flavor) without the program recognizing a
> + --flavor option. */
> +
> +static void
> +prepare_localedir (void)
> +{
> + unsetenv ("LANGUAGE");
> + xsetlocale (LC_MESSAGES, "en_GB.UTF-8");
> + 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");
> + TEST_COMPARE_STRING (gettext ("flavor"), "flavour");
> +}
> +
> +static char **
> +prepare_argv (int *argc)
> +{
> + static char *argv[] =
> + {
> + (char *) "tstgetoptl", (char *) "--required", (char *) "foobar",
> + (char *) "--optional=bazbug", (char *) "--col", (char *) "--color",
> + (char *) "--colour", (char *) "--flavour", NULL
> + };
> + *argc = array_length (argv) - 1;
> + 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. */
> + /* Note that there’s no "--flavor" option, so the "flavor" ->
> + "flavour" translation is useless. */
> + {NULL, 0, NULL, 0 }
> + };
> +
> + /* This tests the same arguments as tstgetopt.c. */
> +
> + int Cflag = 0;
> + int index;
> + int c;
> + bool found_flavor = false;
> +
> + optind = 0;
> + fputs ("Reminder that --flavor is not an option of the program.\n", stderr);
> + while ((c = getopt_long (argc, argv, "", options, NULL)) >= 0)
> + switch (c)
> + {
> + case 'C':
> + ++Cflag;
> + break;
> + case '?':
> + TEST_VERIFY (!found_flavor);
> + found_flavor = true;
> + break;
> + default:
> + /* This should not happen. */
> + support_record_failure_reset ();
That erases *previously* recorded failures and returns, making unexpected results
pass silently. This should be FAIL_EXIT1.
> + 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;
> + }
> +
> + TEST_VERIFY (found_flavor);
> +
> + 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..7091884faf
> --- /dev/null
> +++ b/posix/tstgetoptl.po
> @@ -0,0 +1,29 @@
> +# English translations for tstgetoptl, a test case in glibc.
> +# Copyright (C) 2026 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"
> +
> +#: xxx.c:yy
> +msgid "flavor"
> +msgstr "flavour"
> +
> +# This is to make sure the translator cannot redirect options.
> +#: xxx.c:yy
> +msgid "optional"
> +msgstr "required"