[PATCH v17 3/9] argp: document translated names in --help and --usage
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Wed Nov 19 18:16:00 GMT 2025
On 18/11/25 18:22, 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 | 14 +++++
> argp/argp-help.c | 63 +++++++++++++++++---
> argp/argp-parse.c | 1 +
> argp/tst-argphelp-localized.c | 102 +++++++++++++++++++++++++++++++++
> argp/tst-argphelp-localized.po | 18 ++++++
> argp/tst-argpusage-localized.c | 82 ++++++++++++++++++++++++++
> manual/argp.texi | 22 ++++---
> 7 files changed, 286 insertions(+), 16 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 65d917ad14..ff4b6d9593 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,18 @@ 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 $@
> +
> +$(objpfx)tst-argphelp-localized.out: $(tst_argphelp_localized_mo)
> +$(objpfx)tst-argpusage-localized.out: $(tst_argphelp_localized_mo)
> +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 3fe3a244ce..672e733fc8 100644
> --- a/argp/argp-help.c
> +++ b/argp/argp-help.c
> @@ -1213,6 +1213,8 @@ 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;
> + char *option_msgid;
> + 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 +1278,25 @@ 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);
> + option_msgid =
> + malloc (strlen ("command-line option\004")
> + + strlen (opt->name) + 1);
> + if (option_msgid)
> + {
> + strcpy (option_msgid, "command-line option\004");
> + strcat (option_msgid, opt->name);
> + translated_option_name = gettext (option_msgid);
> + if (!strcmp (translated_option_name, option_msgid))
> + translated_option_name = opt->name;
> + }
> + else
> + translated_option_name = opt->name;
> + __argp_fmtstream_printf (stream, "--%s", translated_option_name);
Wouldn't be simpler to use __asprintf here?
translated_option_name = opt->name;
char *options_msgid = NULL;
if (__asprintf (&option_msgid, "command-line option\004 %s", opt->name) != -1)
{
translated_option_name = gettext (option_msgid);
if (strcmp (translated_option_name, option_msgid) != 0)
translated_option_name = temp;
}
__argp_fmtstream_printf (stream, "--%s", translated_option_name);
if (options_msgid != NULL)
free (options_msgid);
> 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 (option_msgid);
> }
> }
>
> @@ -1420,6 +1438,8 @@ usage_long_opt (const struct argp_option *opt,
> {
> argp_fmtstream_t stream = cookie;
> const char *arg = opt->arg;
> + char *option_msgid;
> + const char *translated_opt_name = opt->name;
> int flags = opt->flags | real->flags;
>
> if (! arg)
> @@ -1427,16 +1447,41 @@ usage_long_opt (const struct argp_option *opt,
>
> if (! (flags & OPTION_NO_USAGE))
> {
> + /* Since we cannot customize the translation context, we will
> + use a default one. FIXME: use pgettext_expr(). */
> + static const char *default_context = "command-line option\004";
> + option_msgid = malloc (strlen (default_context) + strlen (opt->name) + 1);
> + translated_opt_name = NULL;
> + if (option_msgid)
> + {
> + strcpy (option_msgid, default_context);
> + strcat (option_msgid, opt->name);
> + translated_opt_name = gettext (option_msgid);
> + if (!strcmp (translated_opt_name, option_msgid))
> + translated_opt_name = opt->name;
> + }
> + if (!strcmp (translated_opt_name, opt->name))
> + translated_opt_name = NULL;
> if (arg)
> - {
> - arg = dgettext (domain, arg);
> - if (flags & OPTION_ARG_OPTIONAL)
> - __argp_fmtstream_printf (stream, " [--%s[=%s]]", opt->name, arg);
> - else
> - __argp_fmtstream_printf (stream, " [--%s=%s]", opt->name, arg);
> - }
> + {
> + arg = dgettext (domain, arg);
> + if ((flags & OPTION_ARG_OPTIONAL) && translated_opt_name)
> + __argp_fmtstream_printf (stream, " [--%s[=%s] (--%s)]",
> + translated_opt_name, arg, opt->name);
> + else if (flags & OPTION_ARG_OPTIONAL)
> + __argp_fmtstream_printf (stream, " [--%s[=%s]]", opt->name, arg);
> + else if (translated_opt_name)
> + __argp_fmtstream_printf (stream, " [--%s=%s (--%s)]",
> + translated_opt_name, arg, opt->name);
> + else
> + __argp_fmtstream_printf (stream, " [--%s=%s]", opt->name, arg);
> + }
> + else if (translated_opt_name)
> + __argp_fmtstream_printf (stream, " [--%s (--%s)]",
> + translated_opt_name, opt->name);
> else
> - __argp_fmtstream_printf (stream, " [--%s]", opt->name);
> + __argp_fmtstream_printf (stream, " [--%s]", opt->name);
> + free (option_msgid);
> }
>
> return 0;
> diff --git a/argp/argp-parse.c b/argp/argp-parse.c
> index 99f8d9ecd4..90525cdf61 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";
> 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..abc3325827
> --- /dev/null
> +++ b/argp/tst-argphelp-localized.c
> @@ -0,0 +1,102 @@
> +/* Test program for argp argument parser
> + 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/>. */
> +
> +#ifdef HAVE_CONFIG_H
> +#include <config.h>
> +#endif
> +
> +#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>
> +
> +#define PN_(ctxt, str) (str)
> +
> +const char *argp_program_version = "argphelp-test 1.0";
> +
> +struct argp_option options[] =
> +{
> + {PN_ ("command-line option", "color"), 'c', 0, 0, "Rainbow!"},
> + {0}
> +};
> +
> +static int color_set = 0;
> +
> +static error_t
> +parse_opt (int key, char *arg, struct argp_state *state)
> +{
> + (void) state;
> + if (key == 'c' && color_set)
> + {
> + fprintf (stderr, "%s:%d: color already set.\n", __FILE__, __LINE__);
> + abort ();
> + }
> + else if (key == 'c')
> + {
> + color_set = 1;
> + }
> + return 0;
> +}
> +
> +static struct argp argp = { options, parse_opt };
> +
> +int
> +main (int argc, char *argv[])
> +{
> + char *test1_argv[3] =
> + { (char *) "/bin/tst-argphelp-localized", (char *) "--colour", NULL };
> + char *test2_argv[3] =
> + { (char *) "/bin/tst-argphelp-localized", (char *) "--help", NULL };
> +
> + unsetenv ("LANGUAGE");
> + setlocale (LC_ALL, "en_GB.UTF-8");
> + if (bindtextdomain ("tst-argphelp-localized", OBJPFX "domaindir") == NULL)
> + {
> + fprintf (stderr, "%s:%d: cannot call bindtextdomain.\n",
> + __FILE__, __LINE__);
> + abort ();
> + }
> + if (textdomain ("tst-argphelp-localized") == NULL)
> + {
> + fprintf (stderr, "%s:%d: cannot call textdomain.\n",
> + __FILE__, __LINE__);
> + abort ();
> + }
> + /* Check that the catalog is OK: */
> + if (strcmp (gettext ("command-line option\004color"), "colour") != 0)
> + {
> + fprintf (stderr, "%s:%d: the mo file does not work.\n",
> + __FILE__, __LINE__);
> + abort ();
> + }
> + argp_parse (&argp, 2, test1_argv, 0, 0, NULL);
> + if (!color_set)
> + {
> + fprintf (stderr, "%s:%d: color not set.\n", __FILE__, __LINE__);
> + abort ();
> + }
> + argp_parse (&argp, 2, test2_argv, 0, 0, NULL);
> + fprintf (stderr, "%s:%d: --help did not exit the program.\n", __FILE__, __LINE__);
> + abort ();
> + return 0;
> +}
> diff --git a/argp/tst-argphelp-localized.po b/argp/tst-argphelp-localized.po
> new file mode 100644
> index 0000000000..4e301bf278
> --- /dev/null
> +++ b/argp/tst-argphelp-localized.po
> @@ -0,0 +1,18 @@
> +# 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"
> \ No newline at end of file
> diff --git a/argp/tst-argpusage-localized.c b/argp/tst-argpusage-localized.c
> new file mode 100644
> index 0000000000..4061609fc3
> --- /dev/null
> +++ b/argp/tst-argpusage-localized.c
> @@ -0,0 +1,82 @@
> +/* Test program for argp argument parser
> + 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/>. */
> +
> +#ifdef HAVE_CONFIG_H
> +#include <config.h>
> +#endif
> +
> +#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>
> +
> +#define PN_(ctxt, str) (str)
> +
> +const char *argp_program_version = "argpusage-test 1.0";
> +
> +struct argp_option options[] =
> +{
> + {PN_ ("command-line option", "color"), 'c', 0, 0, "Rainbow!"},
> + {0}
> +};
> +
> +static error_t
> +parse_opt (int key, char *arg, struct argp_state *state)
> +{
> + return 0;
> +}
> +
> +static struct argp argp = { options, parse_opt };
> +
> +int
> +main (int argc, char *argv[])
> +{
> + char *test_argv[3] =
> + { (char *) "/bin/tst-argpusage-localized", (char *) "--usage", NULL };
> +
> + unsetenv ("LANGUAGE");
> + setlocale (LC_ALL, "en_GB.UTF-8");
> + if (bindtextdomain ("tst-argphelp-localized", OBJPFX "domaindir") == NULL)
> + {
> + fprintf (stderr, "%s:%d: cannot call bindtextdomain.\n",
> + __FILE__, __LINE__);
> + abort ();
> + }
> + if (textdomain ("tst-argphelp-localized") == NULL)
> + {
> + fprintf (stderr, "%s:%d: cannot call textdomain.\n",
> + __FILE__, __LINE__);
> + abort ();
> + }
> + /* Check that the catalog is OK: */
> + if (strcmp (gettext ("command-line option\004color"), "colour") != 0)
> + {
> + fprintf (stderr, "%s:%d: the mo file does not work.\n",
> + __FILE__, __LINE__);
> + abort ();
> + }
> + argp_parse (&argp, 2, test_argv, 0, 0, NULL);
> + fprintf (stderr, "%s:%d: --usage did not exit the program.\n", __FILE__, __LINE__);
> + abort ();
> + return 0;
> +}
> 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
More information about the Libc-alpha
mailing list