[v22,3/9] argp: document translated names in --help and --usage

Message ID f7ee811c6f1995dc399a48388241ceffb79a23dd.1776957778.git.vivien@planete-kraus.eu (mailing list archive)
State New
Headers
Series Support translated long option names in getopt and argp |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent

Commit Message

Vivien Kraus April 23, 2026, 4:04 p.m. UTC
  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
  

Patch

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.  */
+  *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";
   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