Re: [PATCH v22 3/9] argp: document translated names in --help and --usage
Adhemerval Zanella Netto <[email protected]> Wed, 5 Aug 2026 09:29:17 -0300
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Organization | Linaro |
| Message-ID | <[email protected]> |
On 23/04/26 13:04, Vivien Kraus wrote:
> They are displayed in the --usage and --help output as the main option
> name, and the untranslated name is also displayed in parenthesis, so
> that someone reading a script (presumably with untranslated option
> names) can then figure out which translated option name it relates
> to.
>
> This help message processing uses dynamic memory allocation. If
> malloc fails, the translated option name will not be displayed.
>
> A translation context is required for translated options processing.
> However, its configuration would be most likely expected to be in the
> struct argp definition, but it would change this struct type. The
> easiest solution is to use a fixed context.
>
> Since the configuration of the translation domain is on a per-parser
> basis, and getopt_long is called with the union of all
> options (including options from children parsers), then all option
> names in all parsers must be in the same domain. However, it makes
> sense to use documentation from different domains. Thus, the
> translations for option names will all be searched in the current
> textdomain, while documentation will respect the parser’s domain.
> ---
> argp/Makefile | 19 +++++++
> argp/argp-help.c | 55 +++++++++++++++++++-
> argp/argp-parse.c | 1 +
> argp/tst-argphelp-localized.c | 92 ++++++++++++++++++++++++++++++++++
> argp/tst-argphelp-localized.po | 25 +++++++++
> argp/tst-argpusage-localized.c | 81 ++++++++++++++++++++++++++++++
> manual/argp.texi | 22 +++++---
> 7 files changed, 286 insertions(+), 9 deletions(-)
> create mode 100644 argp/tst-argphelp-localized.c
> create mode 100644 argp/tst-argphelp-localized.po
> create mode 100644 argp/tst-argpusage-localized.c
>
> diff --git a/argp/Makefile b/argp/Makefile
> index 38573fbc66..ff5fa3d77e 100644
> --- a/argp/Makefile
> +++ b/argp/Makefile
> @@ -44,6 +44,8 @@ tests = \
> bug-argp2 \
> tst-argp1 \
> tst-argp2 \
> + tst-argphelp-localized \
> + tst-argpusage-localized \
> tst-ldbl-argp \
> # tests
>
> @@ -51,6 +53,23 @@ CFLAGS-argp-help.c += $(uses-callbacks) -fexceptions
> CFLAGS-argp-parse.c += $(uses-callbacks)
> CFLAGS-argp-fmtstream.c += -fexceptions
>
> +tst_argphelp_localized_mo = $(objpfx)domaindir/en_GB/LC_MESSAGES/tst-argphelp-localized.mo
> +
> +$(tst_argphelp_localized_mo): tst-argphelp-localized.po
> + $(make-target-directory)
> + msgfmt -o $@T $<
> + mv -f $@T $@
> +
> +LOCALES := \
> + en_GB.UTF-8 \
> + # LOCALES
> +include ../gen-locales.mk
> +
> +$(objpfx)tst-argphelp-localized.out: $(tst_argphelp_localized_mo) $(gen-locales)
> +$(objpfx)tst-argpusage-localized.out: $(tst_argphelp_localized_mo) $(gen-locales)
> +CFLAGS-tst-argphelp-localized.c += -DOBJPFX=\"$(objpfx)\"
> +CFLAGS-tst-argpusage-localized.c += -DOBJPFX=\"$(objpfx)\"
> +
> bug-argp1-ARGS = -- --help
> bug-argp2-ARGS = -- -d 111 --dstaddr 222 -p 333 --peer 444
>
> diff --git a/argp/argp-help.c b/argp/argp-help.c
> index 9dd4f6564f..0cacf19e33 100644
> --- a/argp/argp-help.c
> +++ b/argp/argp-help.c
> @@ -1205,6 +1205,35 @@ comma (unsigned col, struct pentry_state *pest)
> indent_to (pest->stream, col);
> }
>
> +/* Help and usage output show the translated option name. *allocated
> + holds a pointer that should be freed by the caller, or a NULL
> + pointer. */
> +static const char *
> +translate_option_name (const char *name, char **allocated)
> +{
> + /* Argp does not have a configuration for the context, so a default
> + one is used. */
> + /* FIXME: use pgettext_expr. */
Again, can't we not add FIXME ?
> + *allocated = NULL;
> + if (__asprintf (allocated, "command-line option\004%s", name) == -1)
> + {
> + /* *allocated is NULL */
> + return name;
> + }
> + const char *translated = gettext (*allocated);
> + if (strcmp (translated, *allocated) == 0)
> + {
> + /* No translation performed. */
> + free (*allocated);
> + *allocated = NULL;
> + return name;
> + }
> + /* FIXME: is it safe to discard *allocated early here? Won’t the
> + return value alias it? */
> + /* *allocated is to be freed by the caller. */
> + return translated;
> +}
> +
> /* Print help for ENTRY to STREAM. */
> static void
> hol_entry_help (struct hol_entry *entry, const struct argp_state *state,
> @@ -1213,6 +1242,7 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,
> unsigned num;
> const struct argp_option *real = entry->opt, *opt;
> char *so = entry->short_options;
> + const char *translated_option_name;
> int have_long_opt = 0; /* We have any long options. */
> /* Saved margins. */
> int old_lm = __argp_fmtstream_set_lmargin (stream, 0);
> @@ -1276,9 +1306,14 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,
> if (opt->name && ovisible (opt))
> {
> comma (uparams.long_opt_col, &pest);
> - __argp_fmtstream_printf (stream, "--%s", opt->name);
> + char *name_allocated = NULL;
> + translated_option_name = translate_option_name (opt->name, &name_allocated);
> + __argp_fmtstream_printf (stream, "--%s", translated_option_name);
> arg (real, "=%s", "[=%s]",
> state == NULL ? NULL : state->root_argp->argp_domain, stream);
> + if (strcmp (translated_option_name, opt->name))
> + __argp_fmtstream_printf (stream, " (--%s)", opt->name);
> + free (name_allocated);
> }
> }
>
> @@ -1420,6 +1455,7 @@ usage_long_opt (const struct argp_option *opt,
> {
> argp_fmtstream_t stream = cookie;
> const char *arg = opt->arg;
> + const char *translated_option_name = opt->name;
> int flags = opt->flags | real->flags;
>
> if (! arg)
> @@ -1427,16 +1463,31 @@ usage_long_opt (const struct argp_option *opt,
>
> if (! (flags & OPTION_NO_USAGE))
> {
> + char *name_allocated = NULL;
> + translated_option_name =
> + translate_option_name (opt->name, &name_allocated);
> + int translation_differs =
> + (strcmp (translated_option_name, opt->name) != 0);
> if (arg)
> {
> arg = dgettext (domain, arg);
> - if (flags & OPTION_ARG_OPTIONAL)
> + if ((flags & OPTION_ARG_OPTIONAL) && translation_differs)
> + __argp_fmtstream_printf (stream, " [--%s[=%s] (--%s)]",
> + translated_option_name, arg, opt->name);
> + else if (flags & OPTION_ARG_OPTIONAL)
> __argp_fmtstream_printf (stream, " [--%s[=%s]]", opt->name, arg);
> + else if (translation_differs)
> + __argp_fmtstream_printf (stream, " [--%s=%s (--%s)]",
> + translated_option_name, arg, opt->name);
> else
> __argp_fmtstream_printf (stream, " [--%s=%s]", opt->name, arg);
> }
> + else if (translation_differs)
> + __argp_fmtstream_printf (stream, " [--%s (--%s)]",
> + translated_option_name, opt->name);
> else
> __argp_fmtstream_printf (stream, " [--%s]", opt->name);
> + free (name_allocated);
> }
>
> return 0;
> diff --git a/argp/argp-parse.c b/argp/argp-parse.c
> index 55a54e9765..a220b32cef 100644
> --- a/argp/argp-parse.c
> +++ b/argp/argp-parse.c
> @@ -472,6 +472,7 @@ parser_init (struct parser *parser, const struct argp *argp,
> struct parser_sizes szs;
> struct _getopt_data opt_data = _GETOPT_DATA_INITIALIZER;
>
> + opt_data.optctxt = "command-line option";
It now hardcodes optctxt = "command-line option" — every existing argp program gets
translation matching enabled unconditionally, in the current textdomain
(not argp_domain).
Combined with patch 7 this also means every argp_parse pays the collision scan
> szs.short_len = (flags & ARGP_NO_ARGS) ? 0 : 1;
> szs.long_len = 0;
> szs.num_groups = 0;
> diff --git a/argp/tst-argphelp-localized.c b/argp/tst-argphelp-localized.c
> new file mode 100644
> index 0000000000..8703742b8f
> --- /dev/null
> +++ b/argp/tst-argphelp-localized.c
> @@ -0,0 +1,92 @@
> +/* Test program for argp argument parser
> + 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 <stdlib.h>
> +#include <time.h>
> +#include <string.h>
> +#include <argp.h>
> +#include <libintl.h>
> +#include <locale.h>
> +#include <unistd.h>
> +#include <support/support.h>
> +#include <support/check.h>
> +
> +/* Note that the final invocation of argp --help will terminate the
> + program with exit code 0. */
> +
> +#define PN_(ctxt, str) (str)
> +#define N_(str) (str)
> +
> +const char *argp_program_version = "argphelp-test 1.0";
> +
> +const struct argp_option options[] =
> +{
> + {PN_ ("command-line option", "color"), 'c', N_ ("HUE"), 0, "Rainbow!"},
> + {PN_ ("command-line option", "flavor"), 'f',
> + N_ ("COOKIE"), OPTION_ARG_OPTIONAL, "Sweet!"},
> + {PN_ ("command-line option", "texture"), 't', 0, 0, "Smooth!"},
> + {0}
> +};
> +
> +static bool color_set = false;
> +
> +static error_t
> +parse_opt (int key, char *arg, struct argp_state *state)
> +{
> + (void) state;
> + if (key == 'c' && color_set)
> + FAIL ("color already set.\n");
> + else if (key == 'c')
> + color_set = true;
> + return 0;
> +}
> +
> +static const struct argp argp = { options, parse_opt };
> +
> +static int
> +do_test (void)
> +{
> + char *test1_argv[3] =
> + { (char *) "/bin/tst-argphelp-localized", (char *) "--colour=yellow", NULL };
> + char *test2_argv[3] =
> + { (char *) "/bin/tst-argphelp-localized", (char *) "--help", NULL };
> +
> + unsetenv ("LANGUAGE");
> + xsetlocale (LC_ALL, "en_GB.UTF-8");
> + TEST_VERIFY_EXIT (bindtextdomain ("tst-argphelp-localized",
> + OBJPFX "domaindir") != NULL);
> + TEST_VERIFY_EXIT (textdomain ("tst-argphelp-localized") != NULL);
> + /* Check that the catalog is OK: */
> + TEST_COMPARE_STRING (gettext ("command-line option\004color"), "colour");
> + TEST_COMPARE_STRING (gettext ("COOKIE"), "BISCUIT");
> + argp_parse (&argp, 2, test1_argv, 0, 0, NULL);
> + TEST_VERIFY (color_set);
> + color_set = false;
> +
> + /* This is the last chance to fail. */
> + if (support_record_failure_is_failed ())
> + FAIL_EXIT1 ("There were test failures before the final invocation of --help");
> + /* This last test will exit the program with code 0 and ignore
> + previous failures. */
> + argp_parse (&argp, 2, test2_argv, 0, 0, NULL);
> + FAIL_EXIT1 ("--help did not exit the program");
> + return 0;
> +}
> +
> +#define TEST_FUNCTION do_test
> +#include <support/test-driver.c>
> diff --git a/argp/tst-argphelp-localized.po b/argp/tst-argphelp-localized.po
> new file mode 100644
> index 0000000000..718cb39ab7
> --- /dev/null
> +++ b/argp/tst-argphelp-localized.po
> @@ -0,0 +1,25 @@
> +# English translations for a GNU C Library test.
> +# This file is distributed under the same license as the GNU C Library.
> +#
> +msgid ""
> +msgstr ""
> +"Project-Id-Version: tst-argphelp-localized\n"
> +"Report-Msgid-Bugs-To: \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"
> +
> +#: tst-argphelp-localized.c:73
> +msgctxt "command-line option"
> +msgid "color"
> +msgstr "colour"
> +
> +msgctxt "command-line option"
> +msgid "flavor"
> +msgstr "flavour"
> +
> +msgid "COOKIE"
> +msgstr "BISCUIT"
> diff --git a/argp/tst-argpusage-localized.c b/argp/tst-argpusage-localized.c
> new file mode 100644
> index 0000000000..f93c2a156e
> --- /dev/null
> +++ b/argp/tst-argpusage-localized.c
> @@ -0,0 +1,81 @@
> +/* Test program for argp argument parser
> + 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 <stdlib.h>
> +#include <time.h>
> +#include <string.h>
> +#include <argp.h>
> +#include <libintl.h>
> +#include <locale.h>
> +#include <unistd.h>
> +#include <support/support.h>
> +#include <support/check.h>
> +
> +/* Note that the final invoking argp --usage will terminate the
> + program with exit code 0. */
> +
> +#define PN_(ctxt, str) (str)
> +#define N_(str) (str)
> +
> +const char *argp_program_version = "argpusage-test 1.0";
> +
> +const struct argp_option options[] =
> +{
> + {PN_ ("command-line option", "color"), 'c', 0, 0, "Rainbow!"},
> + {PN_ ("command-line option", "flavor"), 'f',
> + N_ ("COOKIE"), OPTION_ARG_OPTIONAL, "Sweet!"},
> + {0}
> +};
> +
> +static error_t
> +parse_opt (int key, char *arg, struct argp_state *state)
> +{
> + return 0;
> +}
> +
> +static const struct argp argp = { options, parse_opt };
> +
> +static int
> +do_test (void)
> +{
> + char *test_argv[3] =
> + { (char *) "/bin/tst-argpusage-localized", (char *) "--usage", NULL };
> +
> + unsetenv ("LANGUAGE");
> + xsetlocale (LC_ALL, "en_GB.UTF-8");
> + /* We reuse the tst-argphelp-localized domain to avoid making a new
> + PO file. */
> + TEST_VERIFY_EXIT (bindtextdomain ("tst-argphelp-localized",
> + OBJPFX "domaindir") != NULL);
> + TEST_VERIFY_EXIT (textdomain ("tst-argphelp-localized") != NULL);
> + /* Check that the catalog is OK: */
> + TEST_COMPARE_STRING (gettext ("command-line option\004color"), "colour");
> + TEST_COMPARE_STRING (gettext ("COOKIE"), "BISCUIT");
> + /* This is the last chance to fail. */
> + if (support_record_failure_is_failed ())
> + FAIL_EXIT1 (
> + "There were test failures before the final invocation of --usage");
> + /* This last test will exit the program with code 0 and ignore
> + previous failures. */
> + argp_parse (&argp, 2, test_argv, 0, 0, NULL);
> + FAIL_EXIT1 ("--usage did not exit the program");
> + return 0;
> +}
> +
> +#define TEST_FUNCTION do_test
> +#include <support/test-driver.c>
> diff --git a/manual/argp.texi b/manual/argp.texi
> index 0023441812..97456ef20e 100644
> --- a/manual/argp.texi
> +++ b/manual/argp.texi
> @@ -206,8 +206,10 @@ messages. @xref{Argp Help Filtering}.
>
> @item const char *argp_domain
> If non-zero, the strings used in the argp library are translated using
> -the domain described by this string. If zero, the current default domain
> -is used.
> +the domain described by this string. If zero, the current default
> +domain is used. The long option names are always translated with the
> +current default domain, and with the @samp{"command-line option"}
> +disambiguation string.
>
> @end table
> @end deftp
> @@ -233,7 +235,9 @@ beginning, the unused fields left unspecified.
> The @code{options} field in a @code{struct argp} points to a vector of
> @code{struct argp_option} structures, each of which specifies an option
> that the argp parser supports. Multiple entries may be used for a single
> -option provided it has multiple names. This should be terminated by an
> +option provided it has multiple names. In any case, option names are
> +translated, so either the translated or untranslated form is
> +recognized for each option. This should be terminated by an
> entry with zero in all fields. Note that when using an initialized C
> array for options, writing @code{@{ 0 @}} is enough to achieve this.
>
> @@ -247,9 +251,12 @@ the following fields:
> @item const char *name
> The long name for this option, corresponding to the long option
> @samp{--@var{name}}; this field may be zero if this option @emph{only}
> -has a short name. To specify multiple names for an option, additional
> -entries may follow this one, with the @code{OPTION_ALIAS} flag
> -set. @xref{Argp Option Flags}.
> +has a short name. You should mark this string for translation with
> +the fixed @samp{"command-line option"} context. To specify multiple
> +names for an option, additional entries may follow this one, with the
> +@code{OPTION_ALIAS} flag set. @xref{Argp Option Flags}. Translations
> +are added automatically, it is not necessary to use an alias for
> +translations.
>
> @item int key
> The integer key provided by the current option to the option parser. If
> @@ -323,7 +330,8 @@ This option isn't displayed in any help messages.
> This option is an alias for the closest previous non-alias option. This
> means that it will be displayed in the same help entry, and will inherit
> fields other than @code{name} and @code{key} from the option being
> -aliased.
> +aliased. It is not necessary to list the translation of an option
> +name as an alias.
>
>
> @item OPTION_DOC