[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