From patchwork Thu Apr 23 16:03:59 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133833 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id 929104BBCDD2 for ; Thu, 23 Apr 2026 16:06:43 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 929104BBCDD2 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=pDKq69mq X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [IPv6:2a00:5881:4008:2810::309]) by sourceware.org (Postfix) with ESMTPS id D2B1A4BBF6D4 for ; Thu, 23 Apr 2026 16:05:05 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D2B1A4BBF6D4 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org D2B1A4BBF6D4 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2a00:5881:4008:2810::309 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960306; cv=none; b=dBhw6ivarQ8LE0a92dnxVGIjsOtnebJI4uBuwkikHXC4FLDO+eHYjKbJQaHrQ0Aou+tbvrxXKrcXimDbPHbJ9YbtsLl+DRV+sTD7s2CfYQfuiqy87n+E/KZSKTIrZHsmB2m/f2haCanAXnS/G+qkTbgeuW49+ioEi7qo734035Y= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960306; c=relaxed/simple; bh=MlWvWj2kUbR0ZDmKsug4IAxbtTilrLBjVM5BNof6FWQ=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=G2T6/MYyhSjwa1Jmyro0GuXZMVKkhnH/XnQG0XfpWWNEiXXs9+DB5N5SPNbAko+zFJdb2jYtaKT4rbWhVNVaEGoHpcX0g6ErRG8cORIwgNDG5m+th1iccKapnrEqYo/PwXKf7iA03gW4v5VD+HP2D/iMF8cHuFcH9tQOi425bM0= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D2B1A4BBF6D4 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 92001c5a; Thu, 23 Apr 2026 16:05:04 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-type:content-transfer-encoding; s= albinoniA; bh=MoSHxuOGtwVVZp0kzYaX0fSV/pI=; b=pDKq69mq5PVGQ1wFNk bVrDVHndRbKFtirOWG1foW3Cjblq15trgPzebFfdqxYR3By2R6YTrhin1BnHiHpr W7rMMKFAQhZpuISHCTNKIhbrpPVq3822M4ktvnI/CDjNN2755VOIiTKFfHgtGV83 yWHEMR7ub9h6qoxCTsXWUOuTDJ7yjA3Q/OK11l2tgO7amXyQLgiziQbBmF+GRmEl E/ELYr0w4ZayJk5BxEyFHV/oGP75InWbAmhHQr2J6V8j0dKKbD7VY60h540kigFJ +tKA5StH7H7IcZ47eZ77EN1fqg5hc+QZRE/rE609d1T/Gss+wYVdqT+w1YIQriqO L2EA== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id f820a652 (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:02 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 1/9] posix: allow getopt_long to match translated option names Date: Thu, 23 Apr 2026 18:03:59 +0200 Message-ID: <54fc5096228e57b86e75f5e2fb8e322d5254d431.1776957778.git.vivien@planete-kraus.eu> X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, KAM_SHORT, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org 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 +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; + } + } + + 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); + 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 +# include "gettext.h" +#else +# include #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); } 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 +/* 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 + . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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 (); + 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 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" From patchwork Thu Apr 23 16:04:00 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133830 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id 691F44BAE7F7 for ; Thu, 23 Apr 2026 16:06:22 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 691F44BAE7F7 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=WRt1gkZM X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [IPv6:2a00:5881:4008:2810::309]) by sourceware.org (Postfix) with ESMTPS id 4BADB4BBCD99 for ; Thu, 23 Apr 2026 16:05:45 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 4BADB4BBCD99 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 4BADB4BBCD99 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2a00:5881:4008:2810::309 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960345; cv=none; b=oFUTWhbpmll0WEvOWMN49RlvnZ0lDmt1tS3S/1WZDAqBxa+4XpvfXOsbxrMlOyhmbk5L7zXFNCE/6JGNn39WkEwShOu2IpnSnFJR6qBxLKTt8NiVeD8RiL2dRnQ0Unu31qnLBzpchE8xa8WTuXTZRcZ3a3vqKtInlwQ2hKSYf2w= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960345; c=relaxed/simple; bh=kq0TZE1Ket815HGvWCAP16ALohK+NMwkIMLxehgvcAg=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=pSKzmoPTGTv/Hha8riNBDrFiTOAxsPaZG+KhO4rQlti7x5ue+ZutfC704VkVZvol3NKiGuL6s1G2jJ3GqPD5pmNHiiF94M2Bpa3j9ztkMidDm9tXonQju9NWxK7i61DEh9QsuGvHskb7QvxP0+wVfb/96eVh7OUSgt4P2YkfpXA= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 4BADB4BBCD99 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 98fb7cc2; Thu, 23 Apr 2026 16:05:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-type:content-transfer-encoding; s= albinoniA; bh=Z11kl9vzYOI16ij7RC0nLU/EqiY=; b=WRt1gkZMAWuatyojKl Qj5gaUULyHDloTpLPBz+DCjxm4q7FNjB7+2CleaAZwYOeJJ0+J5tzzioDz6PLZDd ETKXXz1bPAQSe3spHYQFukVMTVMcU8GYqC/bx2+SFdp6SYkBTcxev4vzcJ6k3BtT IUuyEjv8P9ZUrVybQGhRxmuoX9qw2zbwCAuzcY6gJhmYfP6i0b6OtfEz4UOfyNOL yDLhTexaDGlQo7wc6R9SKlaG8JOaUmYCYTCh31NVFR2R5jrix5O7G1zGkhy9YXFY dKYGLkxkDd7KS5TibJ83npWTGmG0app4xuEnPcExhOtYzo3poFsi4wY8Q0kLzXEK vGpQ== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id b71f3820 (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:39 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 2/9] posix: let the getopt caller set the translation context Date: Thu, 23 Apr 2026 18:04:00 +0200 Message-ID: <081a71d7a620b3f704bab0c0705ae02f3c582d9f.1776957778.git.vivien@planete-kraus.eu> X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org Option names are typically one word, so they could be translated differently in different parts of the program. The use of a context lets the translator pick the most appropriate translation when used in the command-line. Another possibility would be to prepend two dashes to the option name before translation, such that it would be obvious this is the command-line option name. However, it would be difficult to mark this string for translation (to be processed by xgettext). This patch creates a new global variable / reentrant state field, optctxt, a pointer, so that the caller can override it. If it is not set, or set to NULL, then no translated option name will be recognized. To avoid exporting global variables, a couple of functions have been added to set it. pgettext_expr is not available yet, so we use a custom function to combine the context and the long option name, and discard the context if no translation was performed. --- NEWS | 5 ++ manual/getopt.texi | 41 +++++++--- posix/Versions | 4 + posix/bits/getopt_ext.h | 3 + posix/getopt.c | 47 ++++++++---- posix/getopt1.c | 74 ++++++++++++++++++- posix/getopt_int.h | 8 +- posix/tstgetoptl.c | 38 ++++++++-- posix/tstgetoptl.po | 3 + sysdeps/mach/hurd/i386/libc.abilist | 2 + sysdeps/mach/hurd/x86_64/libc.abilist | 2 + sysdeps/unix/sysv/linux/aarch64/libc.abilist | 2 + sysdeps/unix/sysv/linux/alpha/libc.abilist | 2 + sysdeps/unix/sysv/linux/arc/libc.abilist | 2 + sysdeps/unix/sysv/linux/arm/be/libc.abilist | 2 + sysdeps/unix/sysv/linux/arm/le/libc.abilist | 2 + sysdeps/unix/sysv/linux/csky/libc.abilist | 2 + sysdeps/unix/sysv/linux/hppa/libc.abilist | 2 + sysdeps/unix/sysv/linux/i386/libc.abilist | 2 + .../sysv/linux/loongarch/ilp32/libc.abilist | 2 + .../sysv/linux/loongarch/lp64/libc.abilist | 2 + .../sysv/linux/m68k/coldfire/libc.abilist | 2 + .../unix/sysv/linux/m68k/m680x0/libc.abilist | 2 + .../sysv/linux/microblaze/be/libc.abilist | 2 + .../sysv/linux/microblaze/le/libc.abilist | 2 + .../sysv/linux/mips/mips32/fpu/libc.abilist | 2 + .../sysv/linux/mips/mips32/nofpu/libc.abilist | 2 + .../sysv/linux/mips/mips64/n32/libc.abilist | 2 + .../sysv/linux/mips/mips64/n64/libc.abilist | 2 + sysdeps/unix/sysv/linux/or1k/libc.abilist | 2 + .../linux/powerpc/powerpc32/fpu/libc.abilist | 2 + .../powerpc/powerpc32/nofpu/libc.abilist | 2 + .../linux/powerpc/powerpc64/be/libc.abilist | 2 + .../linux/powerpc/powerpc64/le/libc.abilist | 2 + .../unix/sysv/linux/riscv/rv32/libc.abilist | 2 + .../unix/sysv/linux/riscv/rv64/libc.abilist | 2 + sysdeps/unix/sysv/linux/s390/libc.abilist | 2 + sysdeps/unix/sysv/linux/sh/be/libc.abilist | 2 + sysdeps/unix/sysv/linux/sh/le/libc.abilist | 2 + .../sysv/linux/sparc/sparc32/libc.abilist | 2 + .../sysv/linux/sparc/sparc64/libc.abilist | 2 + .../unix/sysv/linux/x86_64/64/libc.abilist | 2 + .../unix/sysv/linux/x86_64/x32/libc.abilist | 2 + 43 files changed, 253 insertions(+), 38 deletions(-) diff --git a/NEWS b/NEWS index 4e53a079e9..48f7589f49 100644 --- a/NEWS +++ b/NEWS @@ -19,6 +19,11 @@ Major new features: * New locale added: hrx_BR (Hunsrik language spoken in Brazil). * The getopt_long function now accepts translated long option names. + This functionality is enabled or disabled by calling + getopt_long_enable_translations / getopt_long_disable_translations. + +* Argp parsers enable translated long option names with "command-line + option" as the message context. Deprecated and removed features, and other changes affecting compatibility: diff --git a/manual/getopt.texi b/manual/getopt.texi index 5ae22a1595..fe45ae55a3 100644 --- a/manual/getopt.texi +++ b/manual/getopt.texi @@ -202,15 +202,6 @@ 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 @@ -224,7 +215,8 @@ The @code{struct option} structure has these fields: @item const char *name 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. +translated form, you should mark this string for translation with a +translation context, and call @code{getopt_long_enable_translations}. @item int has_arg This field says whether the option takes an argument. It is an integer, @@ -246,6 +238,35 @@ was seen. @end table @end deftp +@deftypefun int getopt_long_enable_translations (const char *@var{msgctxt}) +@deftypefunx void getopt_long_disable_translations (void) +@standards{GNU, getopt.h} +@c FIXME: I copied that from getopt_long, but I don't understand +@c it. getopt_long_*able_translations use malloc/free and modifies +@c global state. +@safety{@prelim{}@mtunsafe{@mtasurace{:getopt} @mtsenv{}}@asunsafe{@ascuheap{} @ascuintl{} @asulock{} @asucorrupt{}}@acunsafe{@acsmem{} @aculock{} @acucorrupt{}}} +If long option translations are enabled, then 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, without breaking +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. + +Since option names may be short words instead of long sentences, they +may have different translations in different contexts within the same +program. @xref{Contexts, , Using contexts for solving ambiguities, +gettext, the GNU Gettext manual}, for more information. @var{msgctxt} +should be a non-NULL string to disambiguate option name translations. +Passing NULL, or calling @code{getopt_long_disable_translations()}, +will disable option name translation. + +@code{getopt_long_enable_translations} returns 0 on success, or -1 and +sets errno. +@end deftypefun + @deftypefun int getopt_long (int @var{argc}, char *const *@var{argv}, const char *@var{shortopts}, const struct option *@var{longopts}, int *@var{indexptr}) @standards{GNU, getopt.h} @safety{@prelim{}@mtunsafe{@mtasurace{:getopt} @mtsenv{}}@asunsafe{@ascuheap{} @ascuintl{} @asulock{} @asucorrupt{}}@acunsafe{@acsmem{} @aculock{} @acucorrupt{}}} diff --git a/posix/Versions b/posix/Versions index 0624d24bcc..10ca3b7ea3 100644 --- a/posix/Versions +++ b/posix/Versions @@ -159,6 +159,10 @@ libc { GLIBC_2.35 { posix_spawn_file_actions_addtcsetpgrp_np; } + GLIBC_2.44 { + getopt_long_enable_translations; + getopt_long_disable_translations; + } GLIBC_PRIVATE { __libc_fork; __libc_pread; __libc_pwrite; __nanosleep_nocancel; __pause_nocancel; diff --git a/posix/bits/getopt_ext.h b/posix/bits/getopt_ext.h index 94ca758fcd..8f065bbe9f 100644 --- a/posix/bits/getopt_ext.h +++ b/posix/bits/getopt_ext.h @@ -71,6 +71,9 @@ extern int getopt_long_only (int ___argc, char *__getopt_argv_const *___argv, const char *__shortopts, const struct option *__longopts, int *__longind) __THROW __nonnull ((2, 3)); +extern int getopt_long_enable_translations (const char *__msgctxt) + __attribute_warn_unused_result__; +extern void getopt_long_disable_translations (void); __END_DECLS diff --git a/posix/getopt.c b/posix/getopt.c index cdc02d4da9..6717449b5c 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -182,22 +182,30 @@ 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. +/* Return true iff translation_context is not NULL, a translation for + opt_name has been found and it matches the substring from argument, + length argument_length. */ static bool -match_translated_option_name (char *(*translate) (const char *msgid), +match_translated_option_name (char *(*translate) (const char *, const char *, + char **), const char *argument, size_t argument_length, + const char *translation_context, const char *opt_name) { const char *translated = opt_name; + char *translation_buffer = NULL; + bool matches = false; if (translate != NULL) - translated = translate (opt_name); + translated = translate (translation_context, opt_name, &translation_buffer); if (strncmp (translated, argument, argument_length) != 0) - return false; - /* We know that argument is a prefix of translated. */ - return translated[argument_length] == '\0'; + matches = false; + else + /* We know that argument is a prefix of translated. */ + matches = translated[argument_length] == '\0'; + free (translation_buffer); + return matches; } /* Process the argument starting with d->__nextchar as a long option. @@ -213,7 +221,8 @@ 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, - char *(*translate) (const char *msgid)) + char *(*translate) (const char *, const char *, + char **)) { char *nameend; size_t namelen; @@ -222,6 +231,7 @@ process_long_option (int argc, char **argv, const char *optstring, int n_options; int option_index; const char *translated_option_name; + char *translation_buffer = NULL; for (nameend = d->__nextchar; *nameend && *nameend != '='; nameend++) /* Do nothing. */ ; @@ -244,7 +254,9 @@ process_long_option (int argc, char **argv, const char *optstring, /* 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)) + if (match_translated_option_name (translate, + d->__nextchar, namelen, + d->optctxt, p->name)) { /* Exact match found with translation. */ pfound = p; @@ -377,7 +389,8 @@ process_long_option (int argc, char **argv, const char *optstring, { if (print_errors) { - translated_option_name = translate (pfound->name); + translated_option_name = translate (d->optctxt, pfound->name, + &translation_buffer); if (strcmp (translated_option_name, pfound->name) != 0) /* Print both names of the option. */ fprintf (stderr, @@ -389,6 +402,7 @@ process_long_option (int argc, char **argv, const char *optstring, fprintf (stderr, _("%s: option '%s%s' doesn't allow an argument\n"), argv[0], prefix, pfound->name); + free (translation_buffer); } d->optopt = pfound->val; return '?'; @@ -404,7 +418,8 @@ process_long_option (int argc, char **argv, const char *optstring, { /* Same dichotomy as when the option does not allow an argument. */ - translated_option_name = translate (pfound->name); + translated_option_name = translate (d->optctxt, pfound->name, + &translation_buffer); if (strcmp (translated_option_name, pfound->name) != 0) fprintf (stderr, _("%s: option '%s%s' / '%s%s' requires an argument\n"), @@ -413,6 +428,7 @@ process_long_option (int argc, char **argv, const char *optstring, fprintf (stderr, _("%s: option '%s%s' requires an argument\n"), argv[0], prefix, pfound->name); + free (translation_buffer); } d->optopt = pfound->val; @@ -526,7 +542,7 @@ 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, - char *(*translate) (const char *msgid)) + char *(*translate) (const char *, const char *, char **)) { int print_errors = d->opterr; @@ -761,12 +777,15 @@ _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, char *(*translate) (const char *)) + int posixly_correct, + char *(*translate) (const char *, const char *, char **), + const char *ctxt) { int result; getopt_data.optind = optind; getopt_data.opterr = opterr; + getopt_data.optctxt = ctxt; result = _getopt_internal_r (argc, argv, optstring, longopts, longind, long_only, &getopt_data, @@ -789,7 +808,7 @@ _getopt_internal (int argc, char **argv, const char *optstring, { \ return _getopt_internal (argc, (char **)argv, optstring, \ NULL, NULL, 0, POSIXLY_CORRECT, \ - NULL); \ + NULL, NULL); \ } #ifdef _LIBC diff --git a/posix/getopt1.c b/posix/getopt1.c index 6342e2d417..87fe067655 100644 --- a/posix/getopt1.c +++ b/posix/getopt1.c @@ -26,13 +26,53 @@ #include "getopt.h" #include "getopt_int.h" +#include +#include +#include +#include + +/* Callers store an optional context to enable option name + translation. The argument is allocated. */ + +char *optctxt = NULL; + +/* FIXME: use pgettext_expr. */ +static char * +do_translate (const char *context, const char *msgid, char **allocated) +{ + char *full_msgid; + const char *translated = msgid; + int output_length = 0; + + *allocated = NULL; + if (context != NULL) + { + output_length = __asprintf (&full_msgid, "%s\004%s", context, msgid); + *allocated = full_msgid; + if (output_length >= 0) + { + translated = __dcgettext (NULL, full_msgid, LC_MESSAGES); + if (strcmp (translated, full_msgid) == 0) + { + /* No translation for this context and message, so drop + the context + ^D prefix. */ + translated = msgid; + } + } + /* Otherwise, if memory allocation failed, then we won’t accept + translations. translated remains an alias to msgid. */ + } + else + translated = msgid; + return (char *) translated; +} int 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, gettext); + opt_index, 0, 0, do_translate, optctxt); } int @@ -41,7 +81,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, gettext); + 0, d, 0, do_translate); } /* Like getopt_long, but '-' as well as '--' can indicate a long option. @@ -55,7 +95,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, gettext); + opt_index, 1, 0, do_translate, optctxt); } int @@ -64,7 +104,33 @@ _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, gettext); + 1, d, 0, do_translate); +} + +static void +disable_translations (void) +{ + free (optctxt); + optctxt = NULL; +} + +int +getopt_long_enable_translations (const char *msgctxt) +{ + disable_translations (); + if (msgctxt != NULL) + { + optctxt = __strdup (msgctxt); + if (optctxt == NULL) + return -1; + } + return 0; +} + +void +getopt_long_disable_translations (void) +{ + disable_translations (); } diff --git a/posix/getopt_int.h b/posix/getopt_int.h index 579233b08c..fcfec242c1 100644 --- a/posix/getopt_int.h +++ b/posix/getopt_int.h @@ -29,7 +29,9 @@ extern int _getopt_internal (int ___argc, char **___argv, const char *__shortopts, const struct option *__longopts, int *__longind, int __long_only, int __posixly_correct, - char *(*translate) (const char *msgid)); + char *(*translate) (const char *, const char *, + char **), + const char *__optctxt); /* Reentrant versions which can handle parsing multiple argument @@ -71,6 +73,7 @@ struct _getopt_data int opterr; int optopt; char *optarg; + const char *optctxt; /* Internal members. */ @@ -107,7 +110,8 @@ extern int _getopt_internal_r (int ___argc, char **___argv, const struct option *__longopts, int *__longind, int __long_only, struct _getopt_data *__data, int __posixly_correct, - char *(*translate) (const char *msgid)); + char *(*translate) (const char *, const char *, + char **)); extern int _getopt_long_r (int ___argc, char **___argv, const char *__shortopts, diff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c index afefcf46f5..1e970ad407 100644 --- a/posix/tstgetoptl.c +++ b/posix/tstgetoptl.c @@ -39,6 +39,8 @@ known translation of flavor) without the program recognizing a --flavor option. */ +#define TRANSLATION_CONTEXT "command-line option" + static void prepare_localedir (void) { @@ -47,8 +49,8 @@ prepare_localedir (void) 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"); + TEST_COMPARE_STRING (gettext (TRANSLATION_CONTEXT "\004" "color"), "colour"); + TEST_COMPARE_STRING (gettext (TRANSLATION_CONTEXT "\004" "flavor"), "flavour"); } static char ** @@ -65,8 +67,9 @@ prepare_argv (int *argc) } static void -do_my_test (void) +do_my_test (bool with_optctxt) { + static const char *translation_context = TRANSLATION_CONTEXT; int argc; char **argv = prepare_argv (&argc); static const struct option options[] = @@ -87,8 +90,14 @@ do_my_test (void) int c; bool found_flavor = false; + if (with_optctxt) + TEST_VERIFY_EXIT (getopt_long_enable_translations (translation_context) == 0); + else + getopt_long_disable_translations (); optind = 0; fputs ("Reminder that --flavor is not an option of the program.\n", stderr); + if (!with_optctxt) + fputs ("No optctxt set, so --colour should not be recognized.\n", stderr); while ((c = getopt_long (argc, argv, "", options, NULL)) >= 0) switch (c) { @@ -96,8 +105,13 @@ do_my_test (void) ++Cflag; break; case '?': - TEST_VERIFY (!found_flavor); - found_flavor = true; + if (with_optctxt) + { + TEST_VERIFY (!found_flavor); + found_flavor = true; + } + /* Otherwise, this is OK; --colour should not exist if we did not set + optctxt. */ break; default: /* This should not happen. */ @@ -115,11 +129,18 @@ do_my_test (void) break; } - TEST_VERIFY (found_flavor); + if (with_optctxt) + TEST_VERIFY (found_flavor); printf ("Cflags = %d\n", Cflag); - TEST_COMPARE (Cflag, 3); + if (with_optctxt) + TEST_COMPARE (Cflag, 3); + else + TEST_COMPARE (Cflag, 2); + + if (with_optctxt) + getopt_long_disable_translations (); for (index = optind; index < argc; index++) printf ("Non-option argument %s\n", argv[index]); @@ -131,7 +152,8 @@ int do_test (void) { prepare_localedir (); - do_my_test (); + do_my_test (false); + do_my_test (true); return 0; } diff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po index 7091884faf..7dc15e71f3 100644 --- a/posix/tstgetoptl.po +++ b/posix/tstgetoptl.po @@ -16,14 +16,17 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: xxx.c:yy +msgctxt "command-line option" msgid "color" msgstr "colour" #: xxx.c:yy +msgctxt "command-line option" msgid "flavor" msgstr "flavour" # This is to make sure the translator cannot redirect options. #: xxx.c:yy +msgctxt "command-line option" msgid "optional" msgstr "required" diff --git a/sysdeps/mach/hurd/i386/libc.abilist b/sysdeps/mach/hurd/i386/libc.abilist index 0166703bdb..b3e5510297 100644 --- a/sysdeps/mach/hurd/i386/libc.abilist +++ b/sysdeps/mach/hurd/i386/libc.abilist @@ -2818,6 +2818,8 @@ GLIBC_2.44 gai_cancel F GLIBC_2.44 gai_error F GLIBC_2.44 gai_suspend F GLIBC_2.44 getaddrinfo_a F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.44 lio_listio F GLIBC_2.44 lio_listio64 F GLIBC_2.44 mq_close F diff --git a/sysdeps/mach/hurd/x86_64/libc.abilist b/sysdeps/mach/hurd/x86_64/libc.abilist index 0262a079aa..dcfac4bd51 100644 --- a/sysdeps/mach/hurd/x86_64/libc.abilist +++ b/sysdeps/mach/hurd/x86_64/libc.abilist @@ -2494,6 +2494,8 @@ GLIBC_2.44 gai_cancel F GLIBC_2.44 gai_error F GLIBC_2.44 gai_suspend F GLIBC_2.44 getaddrinfo_a F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.44 lio_listio F GLIBC_2.44 lio_listio64 F GLIBC_2.44 mq_close F diff --git a/sysdeps/unix/sysv/linux/aarch64/libc.abilist b/sysdeps/unix/sysv/linux/aarch64/libc.abilist index 3156688add..7dd7c7aa88 100644 --- a/sysdeps/unix/sysv/linux/aarch64/libc.abilist +++ b/sysdeps/unix/sysv/linux/aarch64/libc.abilist @@ -2775,3 +2775,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/alpha/libc.abilist b/sysdeps/unix/sysv/linux/alpha/libc.abilist index 8af5b0b581..bfbee2c31f 100644 --- a/sysdeps/unix/sysv/linux/alpha/libc.abilist +++ b/sysdeps/unix/sysv/linux/alpha/libc.abilist @@ -3122,6 +3122,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/arc/libc.abilist b/sysdeps/unix/sysv/linux/arc/libc.abilist index 35fcef2cc4..00224995bb 100644 --- a/sysdeps/unix/sysv/linux/arc/libc.abilist +++ b/sysdeps/unix/sysv/linux/arc/libc.abilist @@ -2536,3 +2536,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/arm/be/libc.abilist b/sysdeps/unix/sysv/linux/arm/be/libc.abilist index a6c6b951bf..8856423c37 100644 --- a/sysdeps/unix/sysv/linux/arm/be/libc.abilist +++ b/sysdeps/unix/sysv/linux/arm/be/libc.abilist @@ -2828,6 +2828,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/arm/le/libc.abilist b/sysdeps/unix/sysv/linux/arm/le/libc.abilist index e76015fe66..6718047590 100644 --- a/sysdeps/unix/sysv/linux/arm/le/libc.abilist +++ b/sysdeps/unix/sysv/linux/arm/le/libc.abilist @@ -2825,6 +2825,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/csky/libc.abilist b/sysdeps/unix/sysv/linux/csky/libc.abilist index 1fb7cdcad5..c48b5fbbd5 100644 --- a/sysdeps/unix/sysv/linux/csky/libc.abilist +++ b/sysdeps/unix/sysv/linux/csky/libc.abilist @@ -2812,3 +2812,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/hppa/libc.abilist b/sysdeps/unix/sysv/linux/hppa/libc.abilist index 0710ccecf9..9afdd0a2ea 100644 --- a/sysdeps/unix/sysv/linux/hppa/libc.abilist +++ b/sysdeps/unix/sysv/linux/hppa/libc.abilist @@ -2849,6 +2849,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/i386/libc.abilist b/sysdeps/unix/sysv/linux/i386/libc.abilist index 3afe3a88eb..adf364236d 100644 --- a/sysdeps/unix/sysv/linux/i386/libc.abilist +++ b/sysdeps/unix/sysv/linux/i386/libc.abilist @@ -3032,6 +3032,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/loongarch/ilp32/libc.abilist b/sysdeps/unix/sysv/linux/loongarch/ilp32/libc.abilist index 7c6d7055c3..a90b2a83ca 100644 --- a/sysdeps/unix/sysv/linux/loongarch/ilp32/libc.abilist +++ b/sysdeps/unix/sysv/linux/loongarch/ilp32/libc.abilist @@ -995,6 +995,8 @@ GLIBC_2.44 getnetgrent F GLIBC_2.44 getnetgrent_r F GLIBC_2.44 getopt F GLIBC_2.44 getopt_long F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.44 getopt_long_only F GLIBC_2.44 getpagesize F GLIBC_2.44 getpass F diff --git a/sysdeps/unix/sysv/linux/loongarch/lp64/libc.abilist b/sysdeps/unix/sysv/linux/loongarch/lp64/libc.abilist index c2b3a66d3a..2a547f5d7d 100644 --- a/sysdeps/unix/sysv/linux/loongarch/lp64/libc.abilist +++ b/sysdeps/unix/sysv/linux/loongarch/lp64/libc.abilist @@ -2296,3 +2296,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/m68k/coldfire/libc.abilist b/sysdeps/unix/sysv/linux/m68k/coldfire/libc.abilist index d6855131e8..a967c60f07 100644 --- a/sysdeps/unix/sysv/linux/m68k/coldfire/libc.abilist +++ b/sysdeps/unix/sysv/linux/m68k/coldfire/libc.abilist @@ -2808,6 +2808,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/m68k/m680x0/libc.abilist b/sysdeps/unix/sysv/linux/m68k/m680x0/libc.abilist index 4e3fe9c42f..486d5975ec 100644 --- a/sysdeps/unix/sysv/linux/m68k/m680x0/libc.abilist +++ b/sysdeps/unix/sysv/linux/m68k/m680x0/libc.abilist @@ -2975,6 +2975,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/microblaze/be/libc.abilist b/sysdeps/unix/sysv/linux/microblaze/be/libc.abilist index 29f0c5f954..68ae7ff5ce 100644 --- a/sysdeps/unix/sysv/linux/microblaze/be/libc.abilist +++ b/sysdeps/unix/sysv/linux/microblaze/be/libc.abilist @@ -2861,3 +2861,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/microblaze/le/libc.abilist b/sysdeps/unix/sysv/linux/microblaze/le/libc.abilist index 2ef62838f7..eb0d3ddf68 100644 --- a/sysdeps/unix/sysv/linux/microblaze/le/libc.abilist +++ b/sysdeps/unix/sysv/linux/microblaze/le/libc.abilist @@ -2858,3 +2858,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/mips/mips32/fpu/libc.abilist b/sysdeps/unix/sysv/linux/mips/mips32/fpu/libc.abilist index 031e8961ac..0a9bad7ad0 100644 --- a/sysdeps/unix/sysv/linux/mips/mips32/fpu/libc.abilist +++ b/sysdeps/unix/sysv/linux/mips/mips32/fpu/libc.abilist @@ -2938,6 +2938,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/mips/mips32/nofpu/libc.abilist b/sysdeps/unix/sysv/linux/mips/mips32/nofpu/libc.abilist index 8dc99d81b4..072c9df2c1 100644 --- a/sysdeps/unix/sysv/linux/mips/mips32/nofpu/libc.abilist +++ b/sysdeps/unix/sysv/linux/mips/mips32/nofpu/libc.abilist @@ -2936,6 +2936,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/mips/mips64/n32/libc.abilist b/sysdeps/unix/sysv/linux/mips/mips64/n32/libc.abilist index 054c5b6391..9f738374d3 100644 --- a/sysdeps/unix/sysv/linux/mips/mips64/n32/libc.abilist +++ b/sysdeps/unix/sysv/linux/mips/mips64/n32/libc.abilist @@ -2944,6 +2944,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/mips/mips64/n64/libc.abilist b/sysdeps/unix/sysv/linux/mips/mips64/n64/libc.abilist index 13f0148bc0..8b8b718d86 100644 --- a/sysdeps/unix/sysv/linux/mips/mips64/n64/libc.abilist +++ b/sysdeps/unix/sysv/linux/mips/mips64/n64/libc.abilist @@ -2846,6 +2846,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/or1k/libc.abilist b/sysdeps/unix/sysv/linux/or1k/libc.abilist index e7ffe07dd8..c63bc5d110 100644 --- a/sysdeps/unix/sysv/linux/or1k/libc.abilist +++ b/sysdeps/unix/sysv/linux/or1k/libc.abilist @@ -2286,3 +2286,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/powerpc/powerpc32/fpu/libc.abilist b/sysdeps/unix/sysv/linux/powerpc/powerpc32/fpu/libc.abilist index dea4b20f05..2c5141623d 100644 --- a/sysdeps/unix/sysv/linux/powerpc/powerpc32/fpu/libc.abilist +++ b/sysdeps/unix/sysv/linux/powerpc/powerpc32/fpu/libc.abilist @@ -3165,6 +3165,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/powerpc/powerpc32/nofpu/libc.abilist b/sysdeps/unix/sysv/linux/powerpc/powerpc32/nofpu/libc.abilist index b45e127463..8d2194f22d 100644 --- a/sysdeps/unix/sysv/linux/powerpc/powerpc32/nofpu/libc.abilist +++ b/sysdeps/unix/sysv/linux/powerpc/powerpc32/nofpu/libc.abilist @@ -3210,6 +3210,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/powerpc/powerpc64/be/libc.abilist b/sysdeps/unix/sysv/linux/powerpc/powerpc64/be/libc.abilist index 942cf6a027..b5f1a9cc83 100644 --- a/sysdeps/unix/sysv/linux/powerpc/powerpc64/be/libc.abilist +++ b/sysdeps/unix/sysv/linux/powerpc/powerpc64/be/libc.abilist @@ -2919,6 +2919,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/powerpc/powerpc64/le/libc.abilist b/sysdeps/unix/sysv/linux/powerpc/powerpc64/le/libc.abilist index 65d78e5076..0b9c5761da 100644 --- a/sysdeps/unix/sysv/linux/powerpc/powerpc64/le/libc.abilist +++ b/sysdeps/unix/sysv/linux/powerpc/powerpc64/le/libc.abilist @@ -2995,3 +2995,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/riscv/rv32/libc.abilist b/sysdeps/unix/sysv/linux/riscv/rv32/libc.abilist index dcab30d72e..75f37a6c9d 100644 --- a/sysdeps/unix/sysv/linux/riscv/rv32/libc.abilist +++ b/sysdeps/unix/sysv/linux/riscv/rv32/libc.abilist @@ -2539,3 +2539,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/riscv/rv64/libc.abilist b/sysdeps/unix/sysv/linux/riscv/rv64/libc.abilist index 796ef35e26..d2e98816c7 100644 --- a/sysdeps/unix/sysv/linux/riscv/rv64/libc.abilist +++ b/sysdeps/unix/sysv/linux/riscv/rv64/libc.abilist @@ -2739,3 +2739,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F diff --git a/sysdeps/unix/sysv/linux/s390/libc.abilist b/sysdeps/unix/sysv/linux/s390/libc.abilist index 8f2350ee0b..e19857bb81 100644 --- a/sysdeps/unix/sysv/linux/s390/libc.abilist +++ b/sysdeps/unix/sysv/linux/s390/libc.abilist @@ -2956,6 +2956,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/sh/be/libc.abilist b/sysdeps/unix/sysv/linux/sh/be/libc.abilist index 7aa98c5aed..55bc8458c0 100644 --- a/sysdeps/unix/sysv/linux/sh/be/libc.abilist +++ b/sysdeps/unix/sysv/linux/sh/be/libc.abilist @@ -2855,6 +2855,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/sh/le/libc.abilist b/sysdeps/unix/sysv/linux/sh/le/libc.abilist index 6bd4f8f63a..5bf43c1855 100644 --- a/sysdeps/unix/sysv/linux/sh/le/libc.abilist +++ b/sysdeps/unix/sysv/linux/sh/le/libc.abilist @@ -2852,6 +2852,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/sparc/sparc32/libc.abilist b/sysdeps/unix/sysv/linux/sparc/sparc32/libc.abilist index b52cab2a35..417e67f73b 100644 --- a/sysdeps/unix/sysv/linux/sparc/sparc32/libc.abilist +++ b/sysdeps/unix/sysv/linux/sparc/sparc32/libc.abilist @@ -3186,6 +3186,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/sparc/sparc64/libc.abilist b/sysdeps/unix/sysv/linux/sparc/sparc64/libc.abilist index ff99cd4f21..57c9590334 100644 --- a/sysdeps/unix/sysv/linux/sparc/sparc64/libc.abilist +++ b/sysdeps/unix/sysv/linux/sparc/sparc64/libc.abilist @@ -2822,6 +2822,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/x86_64/64/libc.abilist b/sysdeps/unix/sysv/linux/x86_64/64/libc.abilist index 306cd627fd..6749f30c83 100644 --- a/sysdeps/unix/sysv/linux/x86_64/64/libc.abilist +++ b/sysdeps/unix/sysv/linux/x86_64/64/libc.abilist @@ -2771,6 +2771,8 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F GLIBC_2.5 __readlinkat_chk F GLIBC_2.5 inet6_opt_append F GLIBC_2.5 inet6_opt_find F diff --git a/sysdeps/unix/sysv/linux/x86_64/x32/libc.abilist b/sysdeps/unix/sysv/linux/x86_64/x32/libc.abilist index 8b9c448742..d8899a2048 100644 --- a/sysdeps/unix/sysv/linux/x86_64/x32/libc.abilist +++ b/sysdeps/unix/sysv/linux/x86_64/x32/libc.abilist @@ -2790,3 +2790,5 @@ GLIBC_2.43 memset_explicit F GLIBC_2.43 mseal F GLIBC_2.43 openat2 F GLIBC_2.43 umaxabs F +GLIBC_2.44 getopt_long_disable_translations F +GLIBC_2.44 getopt_long_enable_translations F From patchwork Thu Apr 23 16:04:01 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133836 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id EAF214BAE7E3 for ; Thu, 23 Apr 2026 16:07:56 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org EAF214BAE7E3 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=TNtjKlwm X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [89.234.140.182]) by sourceware.org (Postfix) with ESMTPS id A870A4BBCDC9 for ; Thu, 23 Apr 2026 16:05:47 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org A870A4BBCDC9 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org A870A4BBCDC9 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=89.234.140.182 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960348; cv=none; b=ssLN2DLCOjPEiPWyu+x7rwQWmvCchE9gu+afUgjkDDr6xV31WdCbxHKSsPHGQOyrA/rvxfWA0rt9No+umx1lX3pQQc14jUXySB6wpq2LV62SOGIPFaAz5u+VpFXCOiBmpbcZLlh+GSNP1unM5CkUGEgC+5vJJcWM62WJI1nkwSw= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960348; c=relaxed/simple; bh=eSJGJJyfSjMkXzfJvEGi/ncCldRIKsF6qmVy00W9xWY=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=c2fsQZ3aO6MwllNlx69WTnPK9Vo9MkgGIOPBrgmGylLiVru9QGClr4nCa27fJ29UbUA1FsfBJwno497j05AedE+qUPTk7V6ZcbK2e0IR6hxvSKdm/IgcZAEIEXMKM2KC6dxRj6Jv8ypl9NfDmknWYGLJT6vZ4bBG+bB/xa5dB1c= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org A870A4BBCDC9 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 7dd75780; Thu, 23 Apr 2026 16:05:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-type:content-transfer-encoding; s= albinoniA; bh=I1wpMqa4bSkKrhu9ly53588gmh0=; b=TNtjKlwm9gBQYKpFOm Wbo/9o3bOIOSunsZ+5YEcJeefUAkixnMHMyX28B4LMfcAtzt2xwaHZLitBWd1ap0 YJVE5/SK8bKryoKJT7DUBX2HsI6MYRGejP2jI3DqsUvC8/7MPjthrmmY8unVAOUz TzSpADGrhQbHLw5iZiQXtoJblOqFFlvjuiV6o8CJWWvY3Kq59fpm0bAR5TrGlCnU OPw9uTPBI7UL1IEWHF+qXTI6EZ6SqALc+f+BlDKLz6BPuSDzqbRSJ/dJWoEIAdtK 3cBPs6KW1zdaHtbvh8qLCPzkAC0MtiBmQeOig5HGB9sIq0aIDz6BIez1RW0oOvIv iobg== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id 9324f3ae (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:40 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 3/9] argp: document translated names in --help and --usage Date: Thu, 23 Apr 2026 18:04:01 +0200 Message-ID: X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.8 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, KAM_SHORT, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org 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. */ + *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 + . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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 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 + . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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 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 From patchwork Thu Apr 23 16:04:02 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133831 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id D25AD4BA23CA for ; Thu, 23 Apr 2026 16:06:33 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D25AD4BA23CA Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=vEPcmhzz X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [IPv6:2a00:5881:4008:2810::309]) by sourceware.org (Postfix) with ESMTPS id BBC4C4B8A6BF for ; Thu, 23 Apr 2026 16:05:50 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org BBC4C4B8A6BF Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org BBC4C4B8A6BF Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2a00:5881:4008:2810::309 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960351; cv=none; b=N5XRh1wosxVyj1l0yTozUgEJJ6i32nBi9k6Fc4b3m5B8UgeCPuzafqtjmK1Iszt0yRk6GS6WsEXupmuW/NgeN34PaGBLjqkQN+dd7wVaLFB+kQUanQWzeWcXCYl6xK27BzvtnupkNzNaoyyoFNJR/4JUNn8uwA+aqJQUju2c5Zc= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960351; c=relaxed/simple; bh=NMcMsHbHULVSPirFE/HLRgnZymx18YTMD8aFuw0gTXc=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=LX0/LLSRNbMgGWRvFCEef938V7eF7G/gTO47IcdYqyTRTM03gSBtW3VB/6FKTJHyfJzohj9FDMS3AOoKoiH2dEFUYKd3j9bdiUFg8vn1It3JBtFh8CrZvqvcZZ4/gyX2NPOIJZ3BTgD8ggQYN2sp7cjjJSZHrPnngjmzqi3yW8o= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org BBC4C4B8A6BF Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 74f0defd; Thu, 23 Apr 2026 16:05:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; s=albinoniA; bh=glAiG7k ZQcjLAKraBTQ475yvyTk=; b=vEPcmhzznVmWu6FloOSLLXt80iPqfOQQBiA5q+4 dcgmZKqHeSWmtJ1SQu511AtE60RQH66V376wV4J0ZUlUKTxj//hJUpM+0jQ34JuK q9/AwYRtn0S9zOn1thK6Tdc78efmh9IakiK9Z+XzBc/4HKHeRn69SZmqhuAlqDAH s8fLsgdg7IC99kCd7qhQ2y+sQuwRRz6JPFqn8tSz4b4iNscSTYcHfnC4vP262VAR 26YIp7gphIms/jSdfsLJGbrgh3Os50mIjh+8LwjMHBAyzKxa1/rJ6IkS1upVenPq +sGdyfDyz146VnOOGXdiBA7bcvP0nu1H0Vp567Aof2aKing== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id 1f57b6bd (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:40 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 4/9] posix: let the getopt caller choose the textdomain for translation Date: Thu, 23 Apr 2026 18:04:02 +0200 Message-ID: X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org Using the same solution as for the option translation context, a new opttextdomain variable is defined. Note that all options in the call to getopt_long are looked up in the same domain. --- manual/argp.texi | 7 +++++-- manual/getopt.texi | 7 ++++++- posix/bits/getopt_ext.h | 3 ++- posix/getopt.c | 34 ++++++++++++++++++++++------------ posix/getopt1.c | 31 ++++++++++++++++++++++++------- posix/getopt_int.h | 9 +++++---- posix/tstgetoptl.c | 13 ++++++++++--- 7 files changed, 74 insertions(+), 30 deletions(-) diff --git a/manual/argp.texi b/manual/argp.texi index 97456ef20e..50d67b6c55 100644 --- a/manual/argp.texi +++ b/manual/argp.texi @@ -208,8 +208,11 @@ messages. @xref{Argp Help Filtering}. 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 long option names are always translated with the -current default domain, and with the @samp{"command-line option"} -disambiguation string. +current default domain (not this one), and with the +@samp{"command-line option"} disambiguation string. This is because +all the option names, including those defined in sub-parsers, must be +in the same textdomain for @command{getopt} to process the options +correctly. @end table @end deftp diff --git a/manual/getopt.texi b/manual/getopt.texi index fe45ae55a3..bd58e1b7d8 100644 --- a/manual/getopt.texi +++ b/manual/getopt.texi @@ -238,7 +238,7 @@ was seen. @end table @end deftp -@deftypefun int getopt_long_enable_translations (const char *@var{msgctxt}) +@deftypefun int getopt_long_enable_translations (const char *@var{msgctxt}, const char *@var{textdomain}) @deftypefunx void getopt_long_disable_translations (void) @standards{GNU, getopt.h} @c FIXME: I copied that from getopt_long, but I don't understand @@ -263,6 +263,11 @@ should be a non-NULL string to disambiguate option name translations. Passing NULL, or calling @code{getopt_long_disable_translations()}, will disable option name translation. +Option names may be translated in a textdomain that is not currently +the default (@pxref{Interface to gettext, , The Interface, gettext, +the GNU Gettext manual}). If this is @code{NULL} (the default), the +translation will be searched in the current text domain. + @code{getopt_long_enable_translations} returns 0 on success, or -1 and sets errno. @end deftypefun diff --git a/posix/bits/getopt_ext.h b/posix/bits/getopt_ext.h index 8f065bbe9f..f40cb5046e 100644 --- a/posix/bits/getopt_ext.h +++ b/posix/bits/getopt_ext.h @@ -71,7 +71,8 @@ extern int getopt_long_only (int ___argc, char *__getopt_argv_const *___argv, const char *__shortopts, const struct option *__longopts, int *__longind) __THROW __nonnull ((2, 3)); -extern int getopt_long_enable_translations (const char *__msgctxt) +extern int getopt_long_enable_translations (const char *__msgctxt, + const char *__textdomain) __attribute_warn_unused_result__; extern void getopt_long_disable_translations (void); diff --git a/posix/getopt.c b/posix/getopt.c index 6717449b5c..ae823eec29 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -185,19 +185,23 @@ exchange (char **argv, struct _getopt_data *d) /* Return true iff translation_context is not NULL, a translation for opt_name has been found and it matches the substring from argument, length argument_length. + + The translate function pointer is like dpgettext. */ static bool match_translated_option_name (char *(*translate) (const char *, const char *, - char **), + const char *, char **), const char *argument, size_t argument_length, const char *translation_context, + const char *opt_textdomain, const char *opt_name) { const char *translated = opt_name; char *translation_buffer = NULL; bool matches = false; if (translate != NULL) - translated = translate (translation_context, opt_name, &translation_buffer); + translated = translate (opt_textdomain, translation_context, + opt_name, &translation_buffer); if (strncmp (translated, argument, argument_length) != 0) matches = false; @@ -222,7 +226,7 @@ process_long_option (int argc, char **argv, const char *optstring, int long_only, struct _getopt_data *d, int print_errors, const char *prefix, char *(*translate) (const char *, const char *, - char **)) + const char *, char **)) { char *nameend; size_t namelen; @@ -254,9 +258,9 @@ process_long_option (int argc, char **argv, const char *optstring, /* 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, - d->optctxt, p->name)) + if (match_translated_option_name (translate, d->__nextchar, namelen, + d->optctxt, d->opttextdomain, + p->name)) { /* Exact match found with translation. */ pfound = p; @@ -389,7 +393,8 @@ process_long_option (int argc, char **argv, const char *optstring, { if (print_errors) { - translated_option_name = translate (d->optctxt, pfound->name, + translated_option_name = translate (d->opttextdomain, d->optctxt, + pfound->name, &translation_buffer); if (strcmp (translated_option_name, pfound->name) != 0) /* Print both names of the option. */ @@ -418,7 +423,8 @@ process_long_option (int argc, char **argv, const char *optstring, { /* Same dichotomy as when the option does not allow an argument. */ - translated_option_name = translate (d->optctxt, pfound->name, + translated_option_name = translate (d->opttextdomain, d->optctxt, + pfound->name, &translation_buffer); if (strcmp (translated_option_name, pfound->name) != 0) fprintf (stderr, @@ -542,7 +548,8 @@ 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, - char *(*translate) (const char *, const char *, char **)) + char *(*translate) (const char *, const char *, + const char *, char **)) { int print_errors = d->opterr; @@ -778,14 +785,17 @@ int _getopt_internal (int argc, char **argv, const char *optstring, const struct option *longopts, int *longind, int long_only, int posixly_correct, - char *(*translate) (const char *, const char *, char **), - const char *ctxt) + char *(*translate) (const char *, const char *, + const char *, char **), + const char *ctxt, + const char *domain) { int result; getopt_data.optind = optind; getopt_data.opterr = opterr; getopt_data.optctxt = ctxt; + getopt_data.opttextdomain = domain; result = _getopt_internal_r (argc, argv, optstring, longopts, longind, long_only, &getopt_data, @@ -808,7 +818,7 @@ _getopt_internal (int argc, char **argv, const char *optstring, { \ return _getopt_internal (argc, (char **)argv, optstring, \ NULL, NULL, 0, POSIXLY_CORRECT, \ - NULL, NULL); \ + NULL, NULL, NULL); \ } #ifdef _LIBC diff --git a/posix/getopt1.c b/posix/getopt1.c index 87fe067655..cc844a0508 100644 --- a/posix/getopt1.c +++ b/posix/getopt1.c @@ -36,9 +36,15 @@ char *optctxt = NULL; +/* Callers store the textdomain in which the option names are to be + looked up. */ + +char *opttextdomain = NULL; + /* FIXME: use pgettext_expr. */ static char * -do_translate (const char *context, const char *msgid, char **allocated) +do_translate (const char *domain, const char *context, const char *msgid, + char **allocated) { char *full_msgid; const char *translated = msgid; @@ -51,7 +57,7 @@ do_translate (const char *context, const char *msgid, char **allocated) *allocated = full_msgid; if (output_length >= 0) { - translated = __dcgettext (NULL, full_msgid, LC_MESSAGES); + translated = __dcgettext (domain, full_msgid, LC_MESSAGES); if (strcmp (translated, full_msgid) == 0) { /* No translation for this context and message, so drop @@ -72,7 +78,8 @@ 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, do_translate, optctxt); + opt_index, 0, 0, do_translate, + optctxt, opttextdomain); } int @@ -95,7 +102,8 @@ 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, do_translate, optctxt); + opt_index, 1, 0, do_translate, + optctxt, opttextdomain); } int @@ -111,18 +119,27 @@ static void disable_translations (void) { free (optctxt); + free (opttextdomain); optctxt = NULL; + opttextdomain = NULL; } int -getopt_long_enable_translations (const char *msgctxt) +getopt_long_enable_translations (const char *msgctxt, const char *textdomain) { disable_translations (); if (msgctxt != NULL) { optctxt = __strdup (msgctxt); - if (optctxt == NULL) - return -1; + if (textdomain) + opttextdomain = __strdup (textdomain); + if (optctxt == NULL + || (textdomain != NULL && opttextdomain == NULL)) + { + /* strdup failure */ + disable_translations (); + return -1; + } } return 0; } diff --git a/posix/getopt_int.h b/posix/getopt_int.h index fcfec242c1..a770776dc1 100644 --- a/posix/getopt_int.h +++ b/posix/getopt_int.h @@ -24,14 +24,14 @@ /* The translate argument here is optional (can be NULL), it is used to avoid depending on the gettext functions in the posix getopt - function. */ + function. It is like dpgettext. */ extern int _getopt_internal (int ___argc, char **___argv, const char *__shortopts, const struct option *__longopts, int *__longind, int __long_only, int __posixly_correct, char *(*translate) (const char *, const char *, - char **), - const char *__optctxt); + const char *, char **), + const char *__optctxt, const char *__optdomain); /* Reentrant versions which can handle parsing multiple argument @@ -74,6 +74,7 @@ struct _getopt_data int optopt; char *optarg; const char *optctxt; + const char *opttextdomain; /* Internal members. */ @@ -111,7 +112,7 @@ extern int _getopt_internal_r (int ___argc, char **___argv, int __long_only, struct _getopt_data *__data, int __posixly_correct, char *(*translate) (const char *, const char *, - char **)); + const char *, char **)); extern int _getopt_long_r (int ___argc, char **___argv, const char *__shortopts, diff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c index 1e970ad407..ad5755ddbd 100644 --- a/posix/tstgetoptl.c +++ b/posix/tstgetoptl.c @@ -49,8 +49,12 @@ prepare_localedir (void) TEST_VERIFY_EXIT (bindtextdomain ("tstgetoptl", OBJPFX "domaindir") != NULL); TEST_VERIFY_EXIT (textdomain ("tstgetoptl") != NULL); /* Check that the catalog is OK: */ - TEST_COMPARE_STRING (gettext (TRANSLATION_CONTEXT "\004" "color"), "colour"); - TEST_COMPARE_STRING (gettext (TRANSLATION_CONTEXT "\004" "flavor"), "flavour"); + TEST_COMPARE_STRING (dgettext ("tstgetoptl", + TRANSLATION_CONTEXT "\004" "color"), + "colour"); + TEST_COMPARE_STRING (dgettext ("tstgetoptl", + TRANSLATION_CONTEXT "\004" "flavor"), + "flavour"); } static char ** @@ -70,6 +74,7 @@ static void do_my_test (bool with_optctxt) { static const char *translation_context = TRANSLATION_CONTEXT; + static const char *translation_textdomain = "tstgetoptl"; int argc; char **argv = prepare_argv (&argc); static const struct option options[] = @@ -91,7 +96,9 @@ do_my_test (bool with_optctxt) bool found_flavor = false; if (with_optctxt) - TEST_VERIFY_EXIT (getopt_long_enable_translations (translation_context) == 0); + TEST_VERIFY_EXIT (getopt_long_enable_translations (translation_context, + translation_textdomain) + == 0); else getopt_long_disable_translations (); optind = 0; From patchwork Thu Apr 23 16:04:03 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133834 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id 541174BBCD89 for ; Thu, 23 Apr 2026 16:07:35 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 541174BBCD89 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=ViL1o/pJ X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [IPv6:2a00:5881:4008:2810::309]) by sourceware.org (Postfix) with ESMTPS id 47DEE4BBC0E9 for ; Thu, 23 Apr 2026 16:05:51 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 47DEE4BBC0E9 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 47DEE4BBC0E9 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2a00:5881:4008:2810::309 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960351; cv=none; b=P+w2J/maB3TUk2cVKSbpsd7NANqZ3UFu1Eb9jmkrFgZ8HUrA386udEwC1gUPbAFVTgaReTEgUqO9J1SOA+duFRMbO5FjMoJu1eWnCFDa3ywlX2yaRAk4l/V38CmYuQOiCsMLxqeqtnYBuS6WkLpE34ZVjCXinuTh4807OOnp/jM= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960351; c=relaxed/simple; bh=t32QflExz4/Co/8s+Z2fcpALgYqaCVwm0JHnfvCWjTQ=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=AdPomwRP+ixYQgu9lDfHlbGuO242gfDAmtETopFHnV2lz44bpFujso0Sf0mX/IpDskE/FcfAVjo90WyJ3vi0mRwcaV1nWpdjDzNGzQNxVFwhvOvOjgtrbyOLB9re287hN9tS5Il6QWg05pJAaToQC3aLm7fyf0P5hqb59BEAjwM= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 47DEE4BBC0E9 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 3d1a8f6a; Thu, 23 Apr 2026 16:05:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; s=albinoniA; bh=Nn0ct0d PAhW1FfEqoLRQOR7FT74=; b=ViL1o/pJjtAgzbz56cwhYLmNu7ODUJbtBLcyDVm Io/mUV9zn2cJL9Eaa5COj0zK5EIPrma+LzfCX1t2Uco6+WBeWqX/Sp5y3Yc6fcMO hR06z2uPUy8qZHmKzo8OU8sdfNrQwmJ8hJH8YsZ50lQbk1QMO3dyLCCmEauQIbTL zFZmL34dPPCOpxWMTiiptaLI8+nd+l5bo0BdjXX+MAnZsJAJdLPGIo5jWcvpeqhZ YVId1st+2h2Wi9iQuj/TSOgvKTN8wq6XxfwQksrL1fF0uBgPRFTEWn+SgrtQSRcn Vky/xIabdQfZIKz7vWpFfXuGuvcqnzBD3Ho8OmBN9hF6yeg== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id 1e25998a (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:40 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 5/9] posix: do not allow option name translations for secure programs Date: Thu, 23 Apr 2026 18:04:03 +0200 Message-ID: <0f4a0180b879bfd9a05c841af1e425a52296f1c0.1776957778.git.vivien@planete-kraus.eu> X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org SETUID / SETGID / AT_SECURE programs should not accept translated names, so that the programmer knows exactly how the program can be invoked. --- posix/getopt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posix/getopt.c b/posix/getopt.c index ae823eec29..399abeea74 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -199,7 +199,7 @@ match_translated_option_name (char *(*translate) (const char *, const char *, const char *translated = opt_name; char *translation_buffer = NULL; bool matches = false; - if (translate != NULL) + if (translate != NULL && !__libc_enable_secure) translated = translate (opt_textdomain, translation_context, opt_name, &translation_buffer); From patchwork Thu Apr 23 16:04:04 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133832 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id F04F34BBCD9B for ; Thu, 23 Apr 2026 16:06:41 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org F04F34BBCD9B Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=FQB8bB+6 X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [89.234.140.182]) by sourceware.org (Postfix) with ESMTPS id EBCF04B87BB2 for ; Thu, 23 Apr 2026 16:05:53 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org EBCF04B87BB2 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org EBCF04B87BB2 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=89.234.140.182 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960354; cv=none; b=xGtMgwcabNg7NCVr1jlTeolQX3aAHdPaBhXQWg5BdPciDHj07ls+TyVOQ2Zdyvtfj3F6whI+t+L6rPdI8IoxFxXU4IVmF4a20CZCUp/yTindVeBmUUGAAGWNFLCreq9PzYPfgtmw4ddAW1gyYeu50jzg+iLNNB0HVaQIIsu8f0A= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960354; c=relaxed/simple; bh=n9rUeRi66C8oE2mnxNyp/SRwypahpes1rtBz44gPpaM=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=mDVYm6C40KrM+BaoZs5BYdbQ+n4uZPRx8WORZBmfGLguSFmsDVMIoHmwx37y2Yln3Yyk4tbpRbLWiXcc5YlTwgWgYw9JbIq9HT2H46LsuhtDGE+lJsRtCdrh8e3IDAn1NPhnXp19c6Hllmc3BDpFnkXk5ivnANp07BzdGOjoqzI= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org EBCF04B87BB2 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 7de97c4f; Thu, 23 Apr 2026 16:05:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; s=albinoniA; bh=Hbdx2jQ 59XDJ5JQ/wNV7ABalUEA=; b=FQB8bB+60uVIowKYv9+3ei3t42stwfNU8DZMRwX bP8Czx6XKkXoymKtXGh5Sz24U4hKeO0cgTY0p8+mAh81NOVFJpu+WponBTDDywca Iuy/JxFfOqZXvbUvG87VlUnwpIFJSmT+/BRZZkJvsuqqGcK0OdKOp40tA/6U9HOj PBursTrg0B2opXwAR0i3sHuYsXn0rtYbs7sEFCEmH5wlCyAcao6wliVPAuzD68H8 Ar5iZD69pMhhWJW+U04gMwaqvwDLzs5Ohue786wwH69C3GdsK+6rRveZFMMVHNfC Qx+xpgI21A3acgF+ZpXYejUj9PErhLz5xkz4E8MWQkeg91g== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id 16c0834c (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:41 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 6/9] argp: do not display option name translations if __libc_enable_secure Date: Thu, 23 Apr 2026 18:04:04 +0200 Message-ID: <53ee606bab9eba01c1e9cd1d1bfc3298518d1ffd.1776957778.git.vivien@planete-kraus.eu> X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.8 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org --- argp/argp-help.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/argp/argp-help.c b/argp/argp-help.c index 0cacf19e33..aad5d7be13 100644 --- a/argp/argp-help.c +++ b/argp/argp-help.c @@ -1215,6 +1215,9 @@ translate_option_name (const char *name, char **allocated) one is used. */ /* FIXME: use pgettext_expr. */ *allocated = NULL; + if (__libc_enable_secure) + /* Translations are disabled. */ + return name; if (__asprintf (allocated, "command-line option\004%s", name) == -1) { /* *allocated is NULL */ From patchwork Thu Apr 23 16:04:05 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133837 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id A5EC24BBA16F for ; Thu, 23 Apr 2026 16:08:08 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org A5EC24BBA16F Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=tDFvHciL X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [IPv6:2a00:5881:4008:2810::309]) by sourceware.org (Postfix) with ESMTPS id 09D774B88954 for ; Thu, 23 Apr 2026 16:05:54 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 09D774B88954 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 09D774B88954 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2a00:5881:4008:2810::309 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960355; cv=none; b=C4NyTFeWIVfKhk1LDcQ1hjpe3OgBJDUkMGvhq6OwbY3mimzCA18lVnuq7gMgANYBD2jDJsSJ9ysTEZpVXwr7mHeE9mvANI6BkyStEZ4Vy+HUUogr4rxbQGQSncDpwJlhaho0ENiYwIFNJSf8LMskVk40UtIplutVCYLZEts//kE= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960355; c=relaxed/simple; bh=0oI5C24yifBoEzFxX+gUlsNVpbB7t+YpxFbZW/iiCcs=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=NFKljRQlHNpWibeVRfVvnbd02GIEnok2T8IbaXmer1y6lnL11u5qLnMEDIhOnKlFl3H3cdjmqS5WgJQt5T3xP28Y2e/22OZZUNR///zeCgzSBnUGm01WqU8NUpVMePqK7gz66oMs/s1jcsu/ezfieT7VzJ6Rb2dylJuTvvhNVmQ= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 09D774B88954 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 5b5f4af9; Thu, 23 Apr 2026 16:05:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-type:content-transfer-encoding; s= albinoniA; bh=O8Ur1hp/8m+d7jDJjICw0Pekapw=; b=tDFvHciLOS9b+rvjxH 3tJkSEl8LqNp4J0JyaICzzA3IcmZd3fQF3avt1FRPMtfOTzQ4oFlhqpnsw44qi4Z U/PMdpmtHhxjuR4Y/sasSYf2dBiDb3N+WAVHaoagEi9e/1qYxN1d6KGYsOh+AheI U1v8InbM4ffw93DYeoQzAN/KgjTv9OEsxiLFOP/uOcSM0VBWx0AAVDpiW3bOkevh 4Q2YekdGDDVD5VH9zIfi3LxBKQzfO4NYIQojHi/1QPMdN3t9qtHAIDTB0u61Iorn NGC4UVLrbIhrvnpje4hsFWbmWDAu7Bl1PH7FK8Hb7RNVSirvDtmg9l78NBn2njIb E6mg== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id 00bfe0f6 (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:41 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 7/9] posix: check for collisions in getopt_long Date: Thu, 23 Apr 2026 18:04:05 +0200 Message-ID: <5e2bb861a461cf836105efab8cadf833ad3155b4.1776957778.git.vivien@planete-kraus.eu> X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, KAM_SHORT, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org There are 2 kinds of problems that can arise with translations: 1. The developer introduces a new option name that collides with a translation of another option; 2. The translator gave the same translation to 2 different options. Both are bugs, as the program behavior changes while the invocation is the same. With this check, those bugs are detected at run-time, and getopt_long returns '?' with optind set to 1, after printing a message to stderr. The message helps the translator know what to change to fix the issue. While the checks do not do anything if translations are disabled, they necessarily loop over all pairs of distinct options otherwise, so it may be costly for a program with many arguments. The proposal is to only run the test during the first call to getopt_long, and never try again after. If we failed repeatedly until collisions were resolved, then the common pattern to continue processing options until the return value is -1 would lead to an infinite loop. Since getopt does not have an initialization function, knowing whether this is the first time we parse arguments is not trivial. The solution is this: - for reentrant use: we add a boolean to the state, initially set to false, and set to true once the first call returns. - for non-reentrant use: we add a global (but not exported) boolean, initially set to false, set to false when we call getopt_long_enable_translations, and set to true when we disable the translations or a non-reentrant getopt_long version returns. Another approach would be to only check collisions when an option is recognized (while processing argv), but the translator would then have to try all the program options to detect the collisions. Another approach would be to only check collisions with the argp API, since it has a proper initialization method, but it is useful also for getopt_long users. --- manual/getopt.texi | 11 ++- posix/Makefile | 11 +++ posix/getopt.c | 104 ++++++++++++++++++++++- posix/getopt1.c | 28 +++++-- posix/getopt_int.h | 10 ++- posix/tst-getopt_long_collision.c | 128 +++++++++++++++++++++++++++++ posix/tst-getopt_long_collision.po | 32 ++++++++ posix/tstgetoptl.c | 11 +-- posix/tstgetoptl.po | 6 -- 9 files changed, 317 insertions(+), 24 deletions(-) create mode 100644 posix/tst-getopt_long_collision.c create mode 100644 posix/tst-getopt_long_collision.po diff --git a/manual/getopt.texi b/manual/getopt.texi index bd58e1b7d8..dbee16b62e 100644 --- a/manual/getopt.texi +++ b/manual/getopt.texi @@ -23,7 +23,9 @@ use this facility, your program must include the header file @deftypevar int opterr @standards{POSIX.2, unistd.h} If the value of this variable is nonzero, then @code{getopt} prints an -error message to the standard error stream if it encounters an unknown +error message to the standard error stream if it detects a collision +between an untranslated long option name and a translation of a +different option, or it encounters an unknown option character or an option with a missing required argument. This is the default behavior. If you set this variable to zero, @code{getopt} does not print any messages, but it still returns the character @code{?} @@ -268,6 +270,13 @@ the default (@pxref{Interface to gettext, , The Interface, gettext, the GNU Gettext manual}). If this is @code{NULL} (the default), the translation will be searched in the current text domain. +Once translations have been enabled, the next call to +@code{getopt_long} or @code{getopt_long_only}, and only this next +call, will check for collisions between long option name translations. +If a collision is found, a message is printed to @code{stderr}, this +next call will fail, returning @code{'?'}, while @code{optind} will be +set to 1. + @code{getopt_long_enable_translations} returns 0 on success, or -1 and sets errno. @end deftypefun diff --git a/posix/Makefile b/posix/Makefile index 8755f42bdc..e8d5d0661c 100644 --- a/posix/Makefile +++ b/posix/Makefile @@ -289,6 +289,7 @@ tests := \ tst-fork \ tst-gai_strerror \ tst-getopt_long1 \ + tst-getopt_long_collision \ tst-glob-bz30635 \ tst-glob-tilde \ tst-glob_symlinks \ @@ -534,6 +535,7 @@ LOCALES := \ en_US.UTF-8 \ es_US.ISO-8859-1 \ es_US.UTF-8 \ + fr_FR.UTF-8 \ ja_JP.EUC-JP \ tr_TR.UTF-8 \ # LOCALES @@ -815,3 +817,12 @@ $(tstgetoptl_mo): tstgetoptl.po $(objpfx)tstgetoptl.out: $(tstgetoptl_mo) $(gen-locales) CFLAGS-tstgetoptl.c += -DOBJPFX=\"$(objpfx)\" + +tst_getopt_long_collision_mo = $(objpfx)domaindir/fr_FR/LC_MESSAGES/tst-getopt_long_collision.mo +$(tst_getopt_long_collision_mo): tst-getopt_long_collision.po + $(make-target-directory) + msgfmt -o $@T $< + mv -f $@T $@ + +$(objpfx)tst-getopt_long_collision.out: $(tst_getopt_long_collision_mo) $(gen-locales) +CFLAGS-tst-getopt_long_collision.c += -DOBJPFX=\"$(objpfx)\" diff --git a/posix/getopt.c b/posix/getopt.c index 399abeea74..2983fe6ea7 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -483,11 +483,93 @@ _getopt_initialize (_GL_UNUSED int argc, d->__ordering = REQUIRE_ORDER; else d->__ordering = PERMUTE; - d->__initialized = 1; return optstring; } + +static bool +has_translation_collisions (const char *domain, + const char *context, + const struct option *long_options, + char *(*do_translate) (const char *__domain, + const char *__context, + const char *__name, + char **__allocated), + bool print_errors, + const char *argv0) +{ + /* Otherwise, this is a double loop. */ + size_t n_options = 0; + size_t option_index_a, option_index_b; + char *a_buffer = NULL; + const char *a_name = NULL; + const struct option *option_a; + char *b_buffer = NULL; + const char *b_name = NULL; + const struct option *option_b; + bool has_collision = false; + + if (do_translate == NULL || context == NULL) + /* Translations are disabled, we can skip. */ + return false; + /* Count the number of options. */ + for (n_options = 0; long_options[n_options].name; n_options++) + ; + /* Detect collisions between the non-translated name of an option + and the translation of a *different* option, or the translations + of two different options. */ + for (option_index_a = 0; + option_index_a < n_options; + option_index_a++) + { + option_a = &(long_options[option_index_a]); + a_name = do_translate (domain, context, option_a->name, &a_buffer); + for (option_index_b = 0; + option_index_b < n_options; + option_index_b++) + if (option_index_b != option_index_a) + { + option_b = &(long_options[option_index_b]); + b_name = do_translate (domain, context, option_b->name, &b_buffer); + if (strcmp (option_a->name, b_name) == 0) + { + if (print_errors) + /* Since we do not consider a particular use of an + option, but its general name, we do not know what + prefix it has ("--", "-", or "-W "). */ + fprintf (stderr, + _("%s: you found a translation bug! " + "domain '%s', context '%s': " + "option *'%s'* exists " + "and option '%s' translates to *'%s'*\n"), + argv0, + domain, context, + option_a->name, + option_b->name, b_name); + has_collision = true; + } + if (strcmp (a_name, b_name) == 0 + && strcmp (option_a->name, a_name) != 0 + && strcmp (option_b->name, b_name) != 0 + && option_index_a < option_index_b) + { + if (print_errors) + fprintf (stderr, + _("%s: you found a translation bug! " + "domain '%s', context '%s': " + "both '%s' and '%s' translate to '%s'\n"), + argv0, domain, context, + option_a->name, option_b->name, a_name); + has_collision = true; + } + free (b_buffer); + } + free (a_buffer); + } + return has_collision; +} + /* Scan elements of ARGV (whose length is ARGC) for option characters given in OPTSTRING. @@ -563,6 +645,19 @@ _getopt_internal_r (int argc, char **argv, const char *optstring, else if (optstring[0] == '-' || optstring[0] == '+') optstring++; + /* Only ever check translations for the first time we call + getopt_long, since it is costly. We cannot check them in + _getopt_initialize, because gettext may not be set up yet when it + is called. */ + if (!d->__translation_collisions_checked) + { + d->__translation_collisions_checked = true; + if (has_translation_collisions (d->opttextdomain, d->optctxt, + longopts, translate, print_errors, + argv[0])) + return '?'; + } + if (optstring[0] == ':') print_errors = 0; @@ -788,7 +883,8 @@ _getopt_internal (int argc, char **argv, const char *optstring, char *(*translate) (const char *, const char *, const char *, char **), const char *ctxt, - const char *domain) + const char *domain, + bool translation_collisions_checked) { int result; @@ -796,6 +892,8 @@ _getopt_internal (int argc, char **argv, const char *optstring, getopt_data.opterr = opterr; getopt_data.optctxt = ctxt; getopt_data.opttextdomain = domain; + getopt_data.__translation_collisions_checked = + translation_collisions_checked; result = _getopt_internal_r (argc, argv, optstring, longopts, longind, long_only, &getopt_data, @@ -818,7 +916,7 @@ _getopt_internal (int argc, char **argv, const char *optstring, { \ return _getopt_internal (argc, (char **)argv, optstring, \ NULL, NULL, 0, POSIXLY_CORRECT, \ - NULL, NULL, NULL); \ + NULL, NULL, NULL, true); \ } #ifdef _LIBC diff --git a/posix/getopt1.c b/posix/getopt1.c index cc844a0508..06639c4b96 100644 --- a/posix/getopt1.c +++ b/posix/getopt1.c @@ -41,6 +41,11 @@ char *optctxt = NULL; char *opttextdomain = NULL; +/* This is reset each time we call getopt_long_enable_translations, + and set to true as soon as getopt_long is called. */ + +bool translation_collisions_checked = false; + /* FIXME: use pgettext_expr. */ static char * do_translate (const char *domain, const char *context, const char *msgid, @@ -77,9 +82,13 @@ int 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, do_translate, - optctxt, opttextdomain); + int c = _getopt_internal (argc, (char **) argv, options, long_options, + opt_index, 0, 0, do_translate, + optctxt, opttextdomain, + translation_collisions_checked); + /* Translations are checked at most once. */ + translation_collisions_checked = true; + return c; } int @@ -101,9 +110,12 @@ getopt_long_only (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, 1, 0, do_translate, - optctxt, opttextdomain); + int c = _getopt_internal (argc, (char **) argv, options, long_options, + opt_index, 1, 0, do_translate, + optctxt, opttextdomain, + translation_collisions_checked); + translation_collisions_checked = true; + return c; } int @@ -122,6 +134,8 @@ disable_translations (void) free (opttextdomain); optctxt = NULL; opttextdomain = NULL; + /* No translations so no possibilities for collisions. */ + translation_collisions_checked = true; } int @@ -140,6 +154,8 @@ getopt_long_enable_translations (const char *msgctxt, const char *textdomain) disable_translations (); return -1; } + /* Next call to getopt_long will check for collisions. */ + translation_collisions_checked = false; } return 0; } diff --git a/posix/getopt_int.h b/posix/getopt_int.h index a770776dc1..2d7079e770 100644 --- a/posix/getopt_int.h +++ b/posix/getopt_int.h @@ -21,6 +21,7 @@ #define _GETOPT_INT_H 1 #include +#include /* The translate argument here is optional (can be NULL), it is used to avoid depending on the gettext functions in the posix getopt @@ -31,7 +32,8 @@ extern int _getopt_internal (int ___argc, char **___argv, int __long_only, int __posixly_correct, char *(*translate) (const char *, const char *, const char *, char **), - const char *__optctxt, const char *__optdomain); + const char *__optctxt, const char *__optdomain, + bool __translation_collisions_checked); /* Reentrant versions which can handle parsing multiple argument @@ -100,6 +102,12 @@ struct _getopt_data int __first_nonopt; int __last_nonopt; + + /* Checking for collision in translations of long options. */ + + /* Checking for collisions is costly; it must compare O(n²) strings, + when there are n options. So, it is only done once. */ + bool __translation_collisions_checked; }; /* The initializer is necessary to set OPTIND and OPTERR to their diff --git a/posix/tst-getopt_long_collision.c b/posix/tst-getopt_long_collision.c new file mode 100644 index 0000000000..2e603ec9f8 --- /dev/null +++ b/posix/tst-getopt_long_collision.c @@ -0,0 +1,128 @@ +/* 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 + . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PN_(ctxt, str) (str) + +/* There are 2 types of collision that can happen: a translation equal + to an existing option, or two different options translating to the + same thing. + + We test both kinds. In the first test, we have this setup: + foo -> bar + bar -> baz + + In the second test, we have this setup: + foo -> same + bar -> same + + In the third, we don’t translate anything: + foo -> foo + bar -> bar + */ + +static const struct option options[] = + { + {"foo", no_argument, NULL, 'f'}, + {"bar", no_argument, NULL, 'b'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + +static void +setup_catalog (void) +{ + xsetlocale (LC_MESSAGES, "fr_FR.UTF-8"); + TEST_VERIFY_EXIT ( + bindtextdomain ("tst-getopt_long_collision", OBJPFX "domaindir") + != NULL); + TEST_VERIFY_EXIT (textdomain ("tst-getopt_long_collision") != NULL); + /* Check that the catalog is OK: */ + TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 1\004foo"), + "bar"); + TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 1\004bar"), + "baz"); + TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 2\004foo"), + "same"); + TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 2\004bar"), + "same"); + TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 3\004foo"), + "kind 3\004foo"); + TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 3\004bar"), + "kind 3\004bar"); +} + +static void +do_test (int kind, int expected, int optind_after_first_run, + int expected_second_run) +{ + /* Check test --help with one of the 3 tests. We expect the first + call to getopt_long to return expected, while setting optind to + optind_after_first_run. We call getopt_long a second time, and + we expect it to return expected_second_run, while setting optind + to 2. */ + static const char *contexts[] = { "kind 1", "kind 2", "kind 3" }; + const char *context = contexts[kind]; + int c; + int option_index = 0; + const static char *argv[] = + { (char *) "tst-getopt_long_collision", "--help", NULL }; + const static int argc = 2; + optind = 0; + TEST_VERIFY_EXIT (getopt_long_enable_translations (context, NULL) == 0); + fprintf (stderr, "Start test %d.\n", kind + 1); + /* First pass should detect the problem immediately, even if we do + not trigger the option. */ + c = getopt_long (argc, (char **) argv, "fbh", options, &option_index); + TEST_COMPARE (c, expected); + TEST_COMPARE (optind, optind_after_first_run); + /* The translations check is only run once. */ + fprintf (stderr, "Restart test %d, we expect no problems.\n", kind + 1); + c = getopt_long (argc, (char **) argv, "fbh", options, &option_index); + TEST_COMPARE (c, expected_second_run); + TEST_COMPARE (optind, 2); +} + +static int +do_all_tests (void) +{ + setup_catalog (); + /* In failure cases, the first time we parse, we should get '?', and + optind stays at 1. The second time, we parse the first option. + + In the normal case, the first time we parse, we should get the + first option and optind jumps directly to 2. The second time, we + parsed everything. + */ + do_test (0, '?', 1, 'h'); + do_test (1, '?', 1, 'h'); + do_test (2, 'h', 2, -1); + getopt_long_disable_translations (); + return 0; +} + +#define TEST_FUNCTION do_all_tests +#include diff --git a/posix/tst-getopt_long_collision.po b/posix/tst-getopt_long_collision.po new file mode 100644 index 0000000000..2f39001c6d --- /dev/null +++ b/posix/tst-getopt_long_collision.po @@ -0,0 +1,32 @@ +# French translations for tst-getopt_long_collision.c +# Copyright (C) 2026 THE GNU C Library'S COPYRIGHT HOLDER +# This file is distributed under the same license as the GNU C Library. +# +msgid "" +msgstr "" +"Project-Id-Version: GNU C Library (see version.h)\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-06-06 22:37+0200\n" +"PO-Revision-Date: 2025-06-06 22:38+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" + +msgctxt "kind 1" +msgid "foo" +msgstr "bar" + +msgctxt "kind 1" +msgid "bar" +msgstr "baz" + +msgctxt "kind 2" +msgid "foo" +msgstr "same" + +msgctxt "kind 2" +msgid "bar" +msgstr "same" diff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c index ad5755ddbd..bdc20b7e3d 100644 --- a/posix/tstgetoptl.c +++ b/posix/tstgetoptl.c @@ -31,13 +31,10 @@ 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. */ +/* This uses the en_GB locale so that colour means color. 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. */ #define TRANSLATION_CONTEXT "command-line option" diff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po index 7dc15e71f3..b1dc11c468 100644 --- a/posix/tstgetoptl.po +++ b/posix/tstgetoptl.po @@ -24,9 +24,3 @@ msgstr "colour" msgctxt "command-line option" msgid "flavor" msgstr "flavour" - -# This is to make sure the translator cannot redirect options. -#: xxx.c:yy -msgctxt "command-line option" -msgid "optional" -msgstr "required" From patchwork Thu Apr 23 16:04:06 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133838 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id 9B9234BABF05 for ; Thu, 23 Apr 2026 16:08:48 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 9B9234BABF05 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=uYMhQj78 X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [IPv6:2a00:5881:4008:2810::309]) by sourceware.org (Postfix) with ESMTPS id 2B9314B87B99 for ; Thu, 23 Apr 2026 16:05:55 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 2B9314B87B99 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 2B9314B87B99 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2a00:5881:4008:2810::309 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960355; cv=none; b=YEwMdSlOp3gtuizkkf8lkTAjtwoDOWNRS7aPOvB4X+Z/9vh83tCoZMahN+5lTNynQAifqJcQeX2wLQleTFTjZZIjOeCVKrNJ0FdnnYJi6wxY2rZbJ0RsM29Pbor/kPBcnkjLS3UNNLYIYD9Vt5QIFsP32w2yILi2skfrwgdz08Q= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960355; c=relaxed/simple; bh=i+4Wuw8L9h8suYBLhyS5MO13nksynonVNFWamuDXcRE=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=kJi/s7rd0UO5EjBZS9EkY89SKtDwjuasItUGdH1fbP7H4YMEuX2Irpwg2dx7D52dJcEAXdWx8QhOGBkspzKP8FQg5oJfIkCiR68RS39HSHNHOo+aAkHCqPENuiR8VdMqNO4zel3lOLPw3oa+vVC/ZN9zcuGy0tP5Y6hupj+zf0I= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 2B9314B87B99 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 3e682d36; Thu, 23 Apr 2026 16:05:43 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-type:content-transfer-encoding; s= albinoniA; bh=MvYut27SYpjwbiSsccm5JJfsfTk=; b=uYMhQj7866NwjXvFAj B5yUAEwgicaUVGn6dbSDago+STNdr96Ej2wybpvUGh0mVw6F8PU2coRTpXFP+1Xe Q2rbDIZnZYpUGT7bYZ/9eI1CRSMWaiUB+9UlEn4rUItpzp2SNfifFknA9/HqJpaW Dj5v0kbhdgNtlXeAP+6f3JpN+AxbQufL8MrAhpcBR4UjqSxkheVZcGWUC9heOo7o IPd8qLos/8slhmOEP8SCyjHpQxN5MJvsoJakVuXW+MZcWMogTEGxImFI3MnDZ6se LjgX5NUurESWGJXS+8+oxdKbD/82kSncEtVx3DJocfTKTEll287GG55z58H+G2go G9xw== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id 0b40b333 (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:41 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 8/9] posix: Add a script for static validation of getopt_long PO files Date: Thu, 23 Apr 2026 18:04:06 +0200 Message-ID: <2955a3592a6b97298fbcd76ba9eb152739e6e363.1776957778.git.vivien@planete-kraus.eu> X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, KAM_SHORT, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org It is better to statically check the PO files on the developer’s side, because there is a chance to detect the problem early and not embarrass the translation team just before a release. This is a perl script that I made by adapting bits and pieces from mtrace.pl. On the test case, it should fail with the following output: ----- Translation toto is used for more than one option: - bar - foo bar is a translation of pub, but it is also a different option. There were 2 failures. ----- --- NEWS | 3 + manual/getopt.texi | 13 ++ manual/install.texi | 7 +- posix/Makefile | 28 ++- posix/check-getopt-translations.pl | 195 ++++++++++++++++++ .../standalone-multiple-getopt-collisions.po | 45 ++++ posix/tst-check-getopt-translations.sh | 61 ++++++ 7 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 posix/check-getopt-translations.pl create mode 100644 posix/standalone-multiple-getopt-collisions.po create mode 100644 posix/tst-check-getopt-translations.sh diff --git a/NEWS b/NEWS index 48f7589f49..11cef60879 100644 --- a/NEWS +++ b/NEWS @@ -25,6 +25,9 @@ Major new features: * Argp parsers enable translated long option names with "command-line option" as the message context. +* The new installed script check-getopt-translations parses PO files to + check for collisions between long option names and translations. + 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 dbee16b62e..e39f3e3f85 100644 --- a/manual/getopt.texi +++ b/manual/getopt.texi @@ -394,6 +394,19 @@ not match a long option (or its abbreviation). @end deftypefun +It is possible for the programmer to introduce a new option name that +conflicts with the translation of an existing option name. Such a +case would disrupt the workflow of users as the new option would +replace the existing option. Before adding a new option to a program, +the developer should check for collisions with all known translations. +This can be done with the installed +@command{check-getopt-translations} script, by calling for each PO +file in the project: + +@smallexample +check-getopt-translations "context used for translations" @file{file.po} +@end smallexample + @node Getopt Long Option Example @subsection Example of Parsing Long Options with @code{getopt_long} diff --git a/manual/install.texi b/manual/install.texi index 073cda0530..2d0e78f64a 100644 --- a/manual/install.texi +++ b/manual/install.texi @@ -601,9 +601,10 @@ verified to work to build @theglibc{}. Perl 5 Perl is not required, but if present it is used in some tests and the -@code{mtrace} program, to build the @glibcadj{} manual. As of release -time @code{perl} version 5.42.0 is the newest verified to work to -build @theglibc{}. +@code{mtrace} program, to build the @glibcadj{} manual. It is also +used for the @code{check-getopt-translations} installed script. As of +release time @code{perl} version 5.42.0 is the newest verified to work +to build @theglibc{}. @item GNU @code{sed} 3.02 or newer diff --git a/posix/Makefile b/posix/Makefile index e8d5d0661c..0f99241d4a 100644 --- a/posix/Makefile +++ b/posix/Makefile @@ -379,7 +379,7 @@ xtests-time64 := \ ifeq (yes,$(build-shared)) test-srcs := \ - globtest + globtest \ # tests-src tests += \ tst-exec \ @@ -390,6 +390,11 @@ tests += \ # tests endif +ifneq ($(PERL),no) +test-srcs += \ + tst-check-getopt-translations +endif + ifeq (yesyes,$(build-shared)$(have-thread-library)) tests += \ tst-_Fork \ @@ -419,6 +424,9 @@ install-others-programs := \ $(inst_libexecdir)/getconf \ # install-others-programs +install-bin-script = check-getopt-translations +generated += check-getopt-translations + before-compile += \ $(objpfx)posix-conf-vars-def.h \ # before-compile @@ -431,6 +439,7 @@ generated += \ getconf.speclist \ ptestcases.h \ testcases.h \ + tst-check-getopt-translations.out \ tst-getconf.out \ wordexp-tst.out \ # generated @@ -509,6 +518,11 @@ endif endif endif +ifneq ($(PERL),no) +tests-special += \ + $(objpfx)tst-check-getopt-translations.out +endif + include ../Rules ifeq ($(run-built-tests),yes) @@ -826,3 +840,15 @@ $(tst_getopt_long_collision_mo): tst-getopt_long_collision.po $(objpfx)tst-getopt_long_collision.out: $(tst_getopt_long_collision_mo) $(gen-locales) CFLAGS-tst-getopt_long_collision.c += -DOBJPFX=\"$(objpfx)\" + +$(objpfx)check-getopt-translations: check-getopt-translations.pl + rm -f $@.new + sed -e 's|@XXX@|$(address-width)|' \ + -e 's|@VERSION@|$(version)|' \ + -e 's|@PKGVERSION@|$(PKGVERSION)|' \ + -e 's|@REPORT_BUGS_TO@|$(REPORT_BUGS_TO)|' $^ > $@.new \ + && rm -f $@ && mv $@.new $@ && chmod +x $@ + +$(objpfx)tst-check-getopt-translations.out: tst-check-getopt-translations.sh $(objpfx)check-getopt-translations standalone-multiple-getopt-collisions.po + $(SHELL) $^ $(common-objpfx)posix/tst-check-getopt-translations.out + $(evaluate-test) diff --git a/posix/check-getopt-translations.pl b/posix/check-getopt-translations.pl new file mode 100644 index 0000000000..c3c3cff1eb --- /dev/null +++ b/posix/check-getopt-translations.pl @@ -0,0 +1,195 @@ +#! /usr/bin/perl + +# 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 +# . + +use strict; +use warnings; +use Data::Dumper; + +my $VERSION = "@VERSION@"; + +my $PKGVERSION = "@PKGVERSION@"; +my $REPORT_BUGS_TO = '@REPORT_BUGS_TO@'; +my $progname = $_; + +sub usage { + print "Usage: getopt-check [OPTION]... msgctxt lang.po\n"; + print " --help print this help, then exit\n"; + print " --version print version number, then exit\n"; + print "\n"; + print "For bug reporting instructions, please see:\n"; + print "$REPORT_BUGS_TO.\n"; + exit 0; +} + +sub fatal { + print STDERR "$_[0]\n"; + exit 1; +} + +# This script takes two positional arguments: the context for +# translated option names, and the PO file to check. Then, the PO +# file is parsed, looking at three things: +# 1. The msgctxt: it must be equal to the first positional argument, msgctxt; +# 2. The msgid; +# 3. The space-separated list msgstr. +# +# We are looking for two different problems: +# +# 1. Every translation element, current or obsolete, must be unique +# across all option names. +# 2. For every option name, for every translation, current or +# deprecated, if it doesn’t match the untranslated name, then it +# should not match any other untranslated option names. +# +# If we detect an example of the first case, it is a problem with the +# translator only. They have to remove one use of the word, +# preferably one that is deprecated. +# +# If we detect an example of the second case, then it is a problem +# with the developer: they want to introduce an option name that is +# already used for something else by users of this native language! If +# nothing is done, these users will be surprised that the same word +# now means another option, as the untranslated options have +# precedence over the translations. If the translated name is already +# deprecated, then the language team may agree to completely remove +# it. Otherwise, it may be better to find a new untranslated name. + + arglist: while (@ARGV) { + if ($ARGV[0] eq "--v" || $ARGV[0] eq "--ve" || $ARGV[0] eq "--ver" || + $ARGV[0] eq "--vers" || $ARGV[0] eq "--versi" || + $ARGV[0] eq "--versio" || $ARGV[0] eq "--version") { + print "getopt-check $PKGVERSION$VERSION\n"; + print "Copyright (C) 2026 Free Software Foundation, Inc.\n"; + print "This is free software; see the source for copying conditions. There is NO\n"; + print "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"; + print "Written by Vivien Kraus \n"; + + exit 0; + } elsif ($ARGV[0] eq "--h" || $ARGV[0] eq "--he" || $ARGV[0] eq "--hel" || + $ARGV[0] eq "--help") { + &usage; + } elsif ($ARGV[0] =~ /^-/) { + print "$progname: unrecognized option `$ARGV[0]'\n"; + print "Try `$progname --help' for more information.\n"; + exit 1; + } else { + last arglist; + } +} + +if ($#ARGV != 1) { + fatal "You must provide two arguments: the msgctxt for option names, and the name of the PO file."; +} + +my $relevant_msgctxt = $ARGV[0]; +my $pofilename = $ARGV[1]; +my %translations; + +# %translation_used will be populated to detect multiple use of a +# %translation directly when we parse. + +my $entry_msgid; + +# The ad-hoc PO file parser has 3 states: +# 1. Waiting for msgctxt; +# 2. Waiting for msgid; +# 3. Waiting for msgstr. +# +# At the start, the state is 1. Then, if we find "msgctxt +# \"$relevant_msgctxt\"" in a single line, we jump to 2. Otherwise, +# if this is the end of the file, stop parsing. Otherwise, whatever +# the line, stay in 1. This includes: the empty line, meaning we are +# considering a new entry; or a comment, a #: location, or another +# relevant line. +# +# When we are in state 2., we are waiting for the msgid (untranslated +# option name). If we find an empty line, we jump back to 1. If we +# find a line starting with "msgid \"" and ending with a double quote, +# we store what is in the middle in $entry_msgid and jump to 3. +# Otherwise, we stay in state 2. +# +# When we are in state 3., we are waiting for msgstr. If we find an +# empty line, drop $entry_msgid, and back to 1. If the line starts +# with "msgstr \"", we add a record to %translations: the key is +# $entry_msgid, and the value, what is between the detected prefix and +# the end quote. Then, back to state 1. + +my $parser_state = 1; + +open (my $pofile, "<", $pofilename) || fatal "PO file name ${pofilename} cannot be read."; + +while (my $line = <$pofile>) { + chomp $line; + if ($parser_state == 1 && $line =~ /^msgctxt\s*"${relevant_msgctxt}"$/) { + $parser_state = 2; + } elsif ($parser_state == 2 && $line eq "") { + $parser_state = 1; + } elsif ($parser_state == 2 && $line =~ /^msgid\s*"([^"]+)"$/) { + $parser_state = 3; + $entry_msgid = $1; + } elsif ($parser_state == 3 && $line eq "") { + $parser_state = 1; + } elsif ($parser_state == 3 && $line =~ /^msgstr\s*"([^"]*)"$/) { + $translations{$entry_msgid} = $1; + $parser_state = 1; + } +} + +my $number_of_errors = 0; + +# Verify that every option name is unique. +my %untranslated_name; +for my $option_name (sort(keys %translations)) { + my $translation = $translations{$option_name}; + my @existing; + if (exists $untranslated_name{$translation}) { + @existing = @{$untranslated_name{$translation}}; + } + push(@existing, $option_name); + $untranslated_name{$translation} = \@existing; +} +for my $translation (sort(keys %untranslated_name)) { + my $names = $untranslated_name{$translation}; + if (@{$names} > 1) { + print STDERR "Translation ${translation} is used for more than one option:\n"; + for my $untranslated (@{$names}) { + print STDERR " - ${untranslated}\n"; + } + ++$number_of_errors; + } +} + +# Verify that every option translation does not match any other +# untranslated name. +for my $option_name (sort(keys %translations)) { + for my $other_option_name (sort(keys %translations)) { + if ($option_name ne $other_option_name) { + if ($translations{$option_name} eq $other_option_name) { + print STDERR "${translations{$option_name}} is a translation of ${option_name}, but it is also a different option.\n"; + ++$number_of_errors; + } + } + } +} + +if ($number_of_errors eq 0) { + exit 0 +} +print STDERR "There were ${number_of_errors} failures.\n"; +exit 1 diff --git a/posix/standalone-multiple-getopt-collisions.po b/posix/standalone-multiple-getopt-collisions.po new file mode 100644 index 0000000000..14b876a2a3 --- /dev/null +++ b/posix/standalone-multiple-getopt-collisions.po @@ -0,0 +1,45 @@ +# French translations for the getopt static checker +# Copyright (C) 2026 THE GNU C Library'S COPYRIGHT HOLDER +# This file is distributed under the same license as the GNU C Library. +# +# This has two errors: +# 1. "toto" is used both as a translation of "foo" and "bar"; +# 2. "bar" is used as a translation of "pub", but it is another option. +msgid "" +msgstr "" +"Project-Id-Version: GNU C Library (see version.h)\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-06-06 22:37+0200\n" +"PO-Revision-Date: 2025-06-06 22:38+0200\n" +"Language-Team: French \n" +"Language: fr\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" + +# This is not an option name, so it’s OK for it to clash with option +# names. +msgctxt "fish" +msgid "bass" +msgstr "bar" + +# This is the --foo option. +msgctxt "command-line option" +msgid "foo" +msgstr "toto" + +# This is the --bar option. Oops, I translated with toto here too. +msgctxt "command-line option" +msgid "bar" +msgstr "toto" + +# Let’s go to the --pub! +msgctxt "command-line option" +msgid "pub" +msgstr "bar" + +# Wait, it’s OK if baz is translated to baz though. +msgctxt "command-line option" +msgid "baz" +msgstr "baz" diff --git a/posix/tst-check-getopt-translations.sh b/posix/tst-check-getopt-translations.sh new file mode 100644 index 0000000000..038fa3eafa --- /dev/null +++ b/posix/tst-check-getopt-translations.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Test for check-getopt-translations. +# 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 +# . + +set -e + +check_getopt_translations_program=$1; shift +po_file=$1; shift +logfile=$1; shift + +rm -f $logfile +result=0 +expected_output="\ +Translation toto is used for more than one option: + - bar + - foo +bar is a translation of pub, but it is also a different option. +There were 2 failures." + +if output=$(${check_getopt_translations_program} "command-line option" ${po_file} 2>&1) ; then + echo "the errors were not caught." >> $logfile + echo "*** check-getopt-translations FAILED" >> $logfile + result=1 +fi + +if test "$output" != "$expected_output"; then + echo "Expected:" >> $logfile + echo "$expected_output" >> $logfile + echo "Actual:" >> $logfile + echo "$output" >> $logfile + echo "*** check-getopt-translations FAILED" >> $logfile + result=1 +fi + +echo "*** check-getopt-translations PASSED" >> $logfile + +exit $result + +# Preserve executable bits for this shell script. +Local Variables: +eval:(defun frobme () (set-file-modes buffer-file-name file-mode)) +eval:(make-local-variable 'file-mode) +eval:(setq file-mode (file-modes (buffer-file-name))) +eval:(make-local-variable 'after-save-hook) +eval:(add-hook 'after-save-hook 'frobme) +End: From patchwork Thu Apr 23 16:04:07 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 133835 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id 0D1584B8E07C for ; Thu, 23 Apr 2026 16:07:45 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 0D1584B8E07C Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu header.a=rsa-sha1 header.s=albinoniA header.b=QIhMstwb X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from planete-kraus.eu (planete-kraus.eu [89.234.140.182]) by sourceware.org (Postfix) with ESMTPS id D11DB4B87BAB for ; Thu, 23 Apr 2026 16:05:59 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D11DB4B87BAB Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=planete-kraus.eu Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=planete-kraus.eu ARC-Filter: OpenARC Filter v1.0.0 sourceware.org D11DB4B87BAB Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=89.234.140.182 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960360; cv=none; b=UKPJtkYs/f+29v7zlrXScn2aOU1IMa2CXpqKAJ+mi2vg/PUdItda7Rcpl+svn1V0sq2HAfKuwNzT0ovGCM8fYwvuuFYOX/s8Ri32F7ZCgyquYAe7soE8yH6lGj6Q9PVAyGbiVvHNYNCOGuAmOCjoJUrFgYIwhUAHO3RYQmWxcI0= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960360; c=relaxed/simple; bh=6ymLAM/23KYyfPgDZWCFrAMCfsdGFieFnkh9RN/ocOk=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=xifGoYwVEcoOMQWp8SDtQdXNorsH7VCqtXMh6VDHCOAKIM1shAP8Ycb7Ol4VlxjpOLFVrBoTSlBPLS5sK6MV9KBhCPozBfyc/WQ1Xe4C6opofGq4bj38sL4TKv5x83IwyQERbL1doEMGZk9wzY/qEeUcYraMouWBvRoKWwDeEQ4= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D11DB4B87BAB Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id c0ab0975; Thu, 23 Apr 2026 16:05:43 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from :to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-type:content-transfer-encoding; s= albinoniA; bh=adNwM/4Gy4Mf6b5oCxAs88zBKo4=; b=QIhMstwboOcMxJXqJ5 t+FPUV3WBPNbhaDFlThBY/JG13YCkqopNWr8kzgD3zhZhIlEyFIXM+BieXp0Oa1j BIN0T6CtaE2YOcmJmR4901SpOGHzhwiMGSmPwaX8r0VkiYlNc6qE3kLIBEYCNN+x KeKPDP5WJcz9tuWiuNPHDRyGtlckMlRhSlgCXaSjdUf6BoECXm3cgs8cS1GN614T Cm5yNIKMYrSovkhbx4xD1WYFRM968dE1xmk7Mek6Ge7VCLh3HTS5sWn622EG9GgM ArX9bHh5Q0VhBaFncAKY7WAuRZotVfjILgPbi+LLofyRsrl5Fs4ym24tNWAHb7yn kKFg== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id fdbe342a (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Thu, 23 Apr 2026 16:05:42 +0000 (UTC) From: Vivien Kraus To: adhemerval.zanella@linaro.org, libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v22 9/9] posix, argp: Support multiple long option name translations Date: Thu, 23 Apr 2026 18:04:07 +0200 Message-ID: <450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu> X-Mailer: git-send-email 2.52.0 In-Reply-To: References: MIME-Version: 1.0 X-Spam-Status: No, score=-12.9 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, SPF_HELO_PASS, SPF_PASS, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org There are programs that have different options having the same behavior. For instance, emacs: the --eval and --execute options have the same behavior. So, if Emacs were written in a different language than English where “foo” translates both to “eval” and “execute”, then the English translator would have a hard time deciding between “eval” and “execute” for the translation of the only option --foo. Other example I’ve found: bash --init-file / --rcfile. In conclusion: the translator lacks the liberty to name equivalent behaviors differently, and this liberty is sometimes useful. In this patch, the translation for an option name is expected to be a space-separated list of equally accepted translations of a command-line option. I chose a space because it would be a bad choice to have a space character in an option name anyway. The translation type is extended to have a list of translations. We change the following behaviors: 1. Argument matching tries any translation; 2. Collision detection searches collisions with all translations; 3. Argp --help / --usage now lists all translations, but the argument placeholder is not repeated. --- argp/argp-help.c | 170 ++++++++++++------ argp/tst-argphelp-localized.c | 55 +++++- argp/tst-argphelp-localized.po | 3 +- argp/tst-argpusage-localized.c | 3 +- manual/getopt.texi | 5 + posix/check-getopt-translations.pl | 26 +-- posix/getopt.c | 136 ++++++++++++-- .../standalone-multiple-getopt-collisions.po | 6 +- posix/tst-getopt_long_collision.c | 10 +- posix/tst-getopt_long_collision.po | 8 +- posix/tstgetoptl.c | 34 +++- posix/tstgetoptl.po | 4 +- 12 files changed, 355 insertions(+), 105 deletions(-) diff --git a/argp/argp-help.c b/argp/argp-help.c index aad5d7be13..e748e34258 100644 --- a/argp/argp-help.c +++ b/argp/argp-help.c @@ -1205,36 +1205,81 @@ 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) +/* Help and usage output show the translated option name. Since an + option name may have multiple translations, we return all of them. + The result is to be freed by the caller, each element of the array + first, and then the array itself. */ +static char ** +translate_option_name (const char *name) { /* Argp does not have a configuration for the context, so a default one is used. */ + char *msgid = NULL; + const char *full_translation = NULL; + size_t n_translations; + char **results = NULL; /* FIXME: use pgettext_expr. */ - *allocated = NULL; if (__libc_enable_secure) /* Translations are disabled. */ - return name; - if (__asprintf (allocated, "command-line option\004%s", name) == -1) { - /* *allocated is NULL */ - return name; + results = calloc (2, sizeof (char *)); + if (results != NULL) + { + results[0] = __strdup (name); + if (results[0] == NULL) + { + free (results); + results = NULL; + } + } + return results; } - const char *translated = gettext (*allocated); - if (strcmp (translated, *allocated) == 0) + if (__asprintf (&msgid, "command-line option\004%s", name) == -1) + /* Do not bother trying to strdup name. */ + return NULL; + full_translation = gettext (msgid); + if (strcmp (full_translation, msgid) == 0) + full_translation = name; + /* Split full_translation into results. Do it in 2 passes: first + count, then copy. */ + for (int pass = 0; pass < 2; pass++) { - /* No translation performed. */ - free (*allocated); - *allocated = NULL; - return name; + n_translations = 0; + const char *start = full_translation; + const char *end = NULL; + while (start != NULL) + { + end = strchr (start, ' '); + if (pass == 1) + { + if (end == NULL) + results[n_translations] = __strdup (start); + else + results[n_translations] = __strndup (start, end - start); + if (results[n_translations] == NULL) + { + /* Abort. */ + for (size_t i = 0; i < n_translations; i++) + free (results[i]); + free (results); + return NULL; + } + } + start = end; + if (start != NULL) + /* Skip ' ' */ + start++; + n_translations++; + } + if (pass == 0) + results = calloc (n_translations + 1, sizeof (char *)); + if (results == NULL) + return NULL; } - /* 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; + /* We did not touch that index, and allocated the array with + calloc. */ + assert (results[n_translations] == NULL); + return results; } /* Print help for ENTRY to STREAM. */ @@ -1245,7 +1290,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; + char **translated_option_names; int have_long_opt = 0; /* We have any long options. */ /* Saved margins. */ int old_lm = __argp_fmtstream_set_lmargin (stream, 0); @@ -1304,19 +1349,35 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state, else /* A real long option. */ { + bool needs_untranslated = true; __argp_fmtstream_set_wmargin (stream, uparams.long_opt_col); for (opt = real, num = entry->num; num > 0; opt++, num--) if (opt->name && ovisible (opt)) { comma (uparams.long_opt_col, &pest); - 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)) + translated_option_names = translate_option_name (opt->name); + for (size_t i = 0; + (translated_option_names != NULL + && translated_option_names[i] != NULL); + i++) + { + if (i != 0) + __argp_fmtstream_printf (stream, ", "); + __argp_fmtstream_printf (stream, "--%s", translated_option_names[i]); + /* Only display the argument for the first translation. */ + if (i == 0) + arg (real, "=%s", "[=%s]", + state == NULL ? NULL : state->root_argp->argp_domain, stream); + /* If we see the untranslated name, we won’t repeat it. */ + if (strcmp (translated_option_names[i], opt->name) == 0) + needs_untranslated = false; + free (translated_option_names[i]); + } + free (translated_option_names); + if (needs_untranslated) __argp_fmtstream_printf (stream, " (--%s)", opt->name); - free (name_allocated); + /* If memory allocation failed, the --help output will + just display the untranslated name in parenthesis. */ } } @@ -1458,39 +1519,46 @@ 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; + char **translated_option_names = NULL; int flags = opt->flags | real->flags; + bool needs_untranslated = true; if (! arg) arg = real->arg; 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); + translated_option_names = + translate_option_name (opt->name); + __argp_fmtstream_printf (stream, " ["); if (arg) + arg = dgettext (domain, arg); + for (size_t i = 0; + (translated_option_names != NULL + && translated_option_names[i] != NULL); + i++) { - arg = dgettext (domain, arg); - 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); + if (i != 0) + __argp_fmtstream_printf (stream, " / "); + __argp_fmtstream_printf (stream, "--%s", translated_option_names[i]); + if (arg && i == 0) + { + if (flags & OPTION_ARG_OPTIONAL) + __argp_fmtstream_printf (stream, "[=%s]", arg); + else + __argp_fmtstream_printf (stream, "=%s", arg); + } + if (strcmp (translated_option_names[i], opt->name) == 0) + needs_untranslated = false; + free (translated_option_names[i]); } - 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); + free (translated_option_names); + if (needs_untranslated) + __argp_fmtstream_printf (stream, " (--%s)", opt->name); + __argp_fmtstream_printf (stream, "]"); + /* If memory allocation failed, the output will be like + [ (--option)] + */ } return 0; diff --git a/argp/tst-argphelp-localized.c b/argp/tst-argphelp-localized.c index 8703742b8f..6c36eb9cf8 100644 --- a/argp/tst-argphelp-localized.c +++ b/argp/tst-argphelp-localized.c @@ -44,6 +44,8 @@ const struct argp_option options[] = }; static bool color_set = false; +static bool flavor_set = false; +static bool texture_set = false; static error_t parse_opt (int key, char *arg, struct argp_state *state) @@ -53,6 +55,14 @@ parse_opt (int key, char *arg, struct argp_state *state) FAIL ("color already set.\n"); else if (key == 'c') color_set = true; + else if (key == 'f' && flavor_set) + FAIL ("flavor already set.\n"); + else if (key == 'f') + flavor_set = true; + else if (key == 't' && texture_set) + FAIL ("texture already set.\n"); + else if (key == 't') + texture_set = true; return 0; } @@ -64,6 +74,16 @@ 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 *) "--color=yellow", NULL }; + char *test3_argv[3] = + { (char *) "/bin/tst-argphelp-localized", (char *) "--coolur=yellow", NULL }; + char *test4_argv[3] = + { (char *) "/bin/tst-argphelp-localized", (char *) "--flavour", NULL }; + char *test5_argv[3] = + { (char *) "/bin/tst-argphelp-localized", (char *) "--flavor", NULL }; + char *test6_argv[3] = + { (char *) "/bin/tst-argphelp-localized", (char *) "--texture", NULL }; + char *test7_argv[3] = { (char *) "/bin/tst-argphelp-localized", (char *) "--help", NULL }; unsetenv ("LANGUAGE"); @@ -72,18 +92,47 @@ do_test (void) 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 ("command-line option\004color"), + "colour coolur"); TEST_COMPARE_STRING (gettext ("COOKIE"), "BISCUIT"); argp_parse (&argp, 2, test1_argv, 0, 0, NULL); TEST_VERIFY (color_set); + TEST_VERIFY (!flavor_set); + TEST_VERIFY (!texture_set); color_set = false; + argp_parse (&argp, 2, test2_argv, 0, 0, NULL); + TEST_VERIFY (color_set); + TEST_VERIFY (!flavor_set); + TEST_VERIFY (!texture_set); + color_set = false; + argp_parse (&argp, 2, test3_argv, 0, 0, NULL); + TEST_VERIFY (color_set); + TEST_VERIFY (!flavor_set); + TEST_VERIFY (!texture_set); + color_set = false; + argp_parse (&argp, 2, test4_argv, 0, 0, NULL); + TEST_VERIFY (!color_set); + TEST_VERIFY (flavor_set); + TEST_VERIFY (!texture_set); + flavor_set = false; + argp_parse (&argp, 2, test5_argv, 0, 0, NULL); + TEST_VERIFY (!color_set); + TEST_VERIFY (flavor_set); + TEST_VERIFY (!texture_set); + flavor_set = false; + argp_parse (&argp, 2, test6_argv, 0, 0, NULL); + TEST_VERIFY (!color_set); + TEST_VERIFY (!flavor_set); + TEST_VERIFY (texture_set); + texture_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"); + 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); + argp_parse (&argp, 2, test7_argv, 0, 0, NULL); FAIL_EXIT1 ("--help did not exit the program"); return 0; } diff --git a/argp/tst-argphelp-localized.po b/argp/tst-argphelp-localized.po index 718cb39ab7..5b3a672a6d 100644 --- a/argp/tst-argphelp-localized.po +++ b/argp/tst-argphelp-localized.po @@ -15,7 +15,7 @@ msgstr "" #: tst-argphelp-localized.c:73 msgctxt "command-line option" msgid "color" -msgstr "colour" +msgstr "colour coolur" msgctxt "command-line option" msgid "flavor" @@ -23,3 +23,4 @@ msgstr "flavour" msgid "COOKIE" msgstr "BISCUIT" + diff --git a/argp/tst-argpusage-localized.c b/argp/tst-argpusage-localized.c index f93c2a156e..5d1f568679 100644 --- a/argp/tst-argpusage-localized.c +++ b/argp/tst-argpusage-localized.c @@ -64,7 +64,8 @@ do_test (void) 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 ("command-line option\004color"), + "colour coolur"); TEST_COMPARE_STRING (gettext ("COOKIE"), "BISCUIT"); /* This is the last chance to fail. */ if (support_record_failure_is_failed ()) diff --git a/manual/getopt.texi b/manual/getopt.texi index e39f3e3f85..e7216c2baf 100644 --- a/manual/getopt.texi +++ b/manual/getopt.texi @@ -257,6 +257,11 @@ invocation of your program, the program users should be encouraged to use untranslated option names or publish the locale used for this invocation. +If the translation of an option name contains a space character, then +it means multiple translations recognize the same option name. This +is useful to upgrade a translation without disrupting the user's +workflow. + Since option names may be short words instead of long sentences, they may have different translations in different contexts within the same program. @xref{Contexts, , Using contexts for solving ambiguities, diff --git a/posix/check-getopt-translations.pl b/posix/check-getopt-translations.pl index c3c3cff1eb..5cabd9be31 100644 --- a/posix/check-getopt-translations.pl +++ b/posix/check-getopt-translations.pl @@ -146,7 +146,8 @@ while (my $line = <$pofile>) { } elsif ($parser_state == 3 && $line eq "") { $parser_state = 1; } elsif ($parser_state == 3 && $line =~ /^msgstr\s*"([^"]*)"$/) { - $translations{$entry_msgid} = $1; + my @translations_for_this = split(/\s+/, $1); + $translations{$entry_msgid} = \@translations_for_this; $parser_state = 1; } } @@ -156,13 +157,14 @@ my $number_of_errors = 0; # Verify that every option name is unique. my %untranslated_name; for my $option_name (sort(keys %translations)) { - my $translation = $translations{$option_name}; - my @existing; - if (exists $untranslated_name{$translation}) { - @existing = @{$untranslated_name{$translation}}; + for my $translation (@{$translations{$option_name}}) { + my @existing; + if (exists $untranslated_name{$translation}) { + @existing = @{$untranslated_name{$translation}}; + } + push(@existing, $option_name); + $untranslated_name{$translation} = \@existing; } - push(@existing, $option_name); - $untranslated_name{$translation} = \@existing; } for my $translation (sort(keys %untranslated_name)) { my $names = $untranslated_name{$translation}; @@ -180,10 +182,12 @@ for my $translation (sort(keys %untranslated_name)) { for my $option_name (sort(keys %translations)) { for my $other_option_name (sort(keys %translations)) { if ($option_name ne $other_option_name) { - if ($translations{$option_name} eq $other_option_name) { - print STDERR "${translations{$option_name}} is a translation of ${option_name}, but it is also a different option.\n"; - ++$number_of_errors; - } + for my $translation (@{$translations{$option_name}}) { + if ($translation eq $other_option_name) { + print STDERR "${translation} is a translation of ${option_name}, but it is also a different option.\n"; + ++$number_of_errors; + } + } } } } diff --git a/posix/getopt.c b/posix/getopt.c index 2983fe6ea7..32cd4cf120 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -182,6 +182,50 @@ exchange (char **argv, struct _getopt_data *d) d->__last_nonopt = d->optind; } +/* Return TRUE if other is equal to reference. */ +static bool +complete_match (const char *reference, + size_t reference_length, + const char *other) +{ + return (strncmp (reference, other, reference_length) == 0 + /* So, reference is a prefix of other */ + && other[reference_length] == '\0'); +} + +/* Match a string against a space-separated string list. We save an + allocated copy of the exact match for the collision checker. */ +static bool +match_any_translation (const char *reference, + size_t reference_length, + const char *translation, + char **match) +{ + const char *start = translation; + const char *end; + + if (match) + *match = NULL; + while (start != NULL) + { + end = strchr (start, ' '); + if ((end == NULL && complete_match (reference, reference_length, start)) + || (end != NULL + && end - start == reference_length + && strncmp (reference, start, reference_length) == 0)) + { + if (match) + *match = __strndup (reference, reference_length); + return true; + } + start = end; + if (start != NULL) + /* Skip the space character. */ + start++; + } + return false; +} + /* Return true iff translation_context is not NULL, a translation for opt_name has been found and it matches the substring from argument, length argument_length. @@ -202,16 +246,42 @@ match_translated_option_name (char *(*translate) (const char *, const char *, if (translate != NULL && !__libc_enable_secure) translated = translate (opt_textdomain, translation_context, opt_name, &translation_buffer); - - if (strncmp (translated, argument, argument_length) != 0) - matches = false; - else - /* We know that argument is a prefix of translated. */ - matches = translated[argument_length] == '\0'; + matches = match_any_translation (argument, argument_length, translated, NULL); free (translation_buffer); return matches; } +/* Translate opt_name, but only keep the first item of the list. This + is used for error messages. */ +static const char * +first_translation (char *(*translate) (const char *, const char *, + const char *, char **), + const char *translation_context, + const char *opt_textdomain, + const char *opt_name, + char **allocated) +{ + char *translation_buffer = NULL; + const char *all_translations = + translate (opt_textdomain, translation_context, opt_name, + &translation_buffer); + const char *end = strchr (all_translations, ' '); + if (end == NULL) + { + /* There is only 1 translation for opt_name. No extra + processing is needed. */ + *allocated = translation_buffer; + return all_translations; + } + *allocated = __strndup (all_translations, end - all_translations); + free (translation_buffer); + if (*allocated == NULL) + /* Memory allocation failed; return opt_name. No extra memory + needs to be kept around. */ + return opt_name; + return *allocated; +} + /* Process the argument starting with d->__nextchar as a long option. d->optind should *not* have been advanced over this argument. @@ -393,9 +463,9 @@ process_long_option (int argc, char **argv, const char *optstring, { if (print_errors) { - translated_option_name = translate (d->opttextdomain, d->optctxt, - pfound->name, - &translation_buffer); + translated_option_name = + first_translation (translate, d->optctxt, d->opttextdomain, + pfound->name, &translation_buffer); if (strcmp (translated_option_name, pfound->name) != 0) /* Print both names of the option. */ fprintf (stderr, @@ -423,9 +493,9 @@ process_long_option (int argc, char **argv, const char *optstring, { /* Same dichotomy as when the option does not allow an argument. */ - translated_option_name = translate (d->opttextdomain, d->optctxt, - pfound->name, - &translation_buffer); + translated_option_name = + first_translation (translate, d->optctxt, d->opttextdomain, + pfound->name, &translation_buffer); if (strcmp (translated_option_name, pfound->name) != 0) fprintf (stderr, _("%s: option '%s%s' / '%s%s' requires an argument\n"), @@ -488,6 +558,32 @@ _getopt_initialize (_GL_UNUSED int argc, } +/* Match any item of a string list against any item of another. This + is used by the collision checker. */ +static bool +match_any_translation_pair (const char *list_a, + const char *list_b, + char **match) +{ + const char *start = list_a; + const char *end; + + while (start != NULL) + { + end = strchr (start, ' '); + if ((end == NULL + && match_any_translation (start, strlen (start), list_b, match)) + || (end != NULL + && match_any_translation (start, end - start, list_b, match))) + return true; + start = end; + if (start != NULL) + /* Skip the space character. */ + start++; + } + return false; +} + static bool has_translation_collisions (const char *domain, const char *context, @@ -508,6 +604,7 @@ has_translation_collisions (const char *domain, char *b_buffer = NULL; const char *b_name = NULL; const struct option *option_b; + char *collision = NULL; bool has_collision = false; if (do_translate == NULL || context == NULL) @@ -532,7 +629,9 @@ has_translation_collisions (const char *domain, { option_b = &(long_options[option_index_b]); b_name = do_translate (domain, context, option_b->name, &b_buffer); - if (strcmp (option_a->name, b_name) == 0) + collision = NULL; + if (match_any_translation (option_a->name, strlen (option_a->name), b_name, + &collision)) { if (print_errors) /* Since we do not consider a particular use of an @@ -546,13 +645,15 @@ has_translation_collisions (const char *domain, argv0, domain, context, option_a->name, - option_b->name, b_name); + option_b->name, collision); has_collision = true; } - if (strcmp (a_name, b_name) == 0 + free (collision); + collision = NULL; + if (option_index_a < option_index_b && strcmp (option_a->name, a_name) != 0 && strcmp (option_b->name, b_name) != 0 - && option_index_a < option_index_b) + && match_any_translation_pair (a_name, b_name, &collision)) { if (print_errors) fprintf (stderr, @@ -560,9 +661,10 @@ has_translation_collisions (const char *domain, "domain '%s', context '%s': " "both '%s' and '%s' translate to '%s'\n"), argv0, domain, context, - option_a->name, option_b->name, a_name); + option_a->name, option_b->name, collision); has_collision = true; } + free (collision); free (b_buffer); } free (a_buffer); diff --git a/posix/standalone-multiple-getopt-collisions.po b/posix/standalone-multiple-getopt-collisions.po index 14b876a2a3..edd2231d8f 100644 --- a/posix/standalone-multiple-getopt-collisions.po +++ b/posix/standalone-multiple-getopt-collisions.po @@ -27,17 +27,17 @@ msgstr "bar" # This is the --foo option. msgctxt "command-line option" msgid "foo" -msgstr "toto" +msgstr "tata toto" # This is the --bar option. Oops, I translated with toto here too. msgctxt "command-line option" msgid "bar" -msgstr "toto" +msgstr "titi toto" # Let’s go to the --pub! msgctxt "command-line option" msgid "pub" -msgstr "bar" +msgstr "bar club" # Wait, it’s OK if baz is translated to baz though. msgctxt "command-line option" diff --git a/posix/tst-getopt_long_collision.c b/posix/tst-getopt_long_collision.c index 2e603ec9f8..ff90ec1131 100644 --- a/posix/tst-getopt_long_collision.c +++ b/posix/tst-getopt_long_collision.c @@ -42,6 +42,8 @@ In the third, we don’t translate anything: foo -> foo bar -> bar + + For added fun, we add some noise to the translations. */ static const struct option options[] = @@ -62,13 +64,13 @@ setup_catalog (void) TEST_VERIFY_EXIT (textdomain ("tst-getopt_long_collision") != NULL); /* Check that the catalog is OK: */ TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 1\004foo"), - "bar"); + "bar noise1"); TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 1\004bar"), - "baz"); + "noise2 baz noise3"); TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 2\004foo"), - "same"); + "same noise4"); TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 2\004bar"), - "same"); + "noise5 same"); TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 3\004foo"), "kind 3\004foo"); TEST_COMPARE_STRING (dgettext ("tst-getopt_long_collision", "kind 3\004bar"), diff --git a/posix/tst-getopt_long_collision.po b/posix/tst-getopt_long_collision.po index 2f39001c6d..a196e81f38 100644 --- a/posix/tst-getopt_long_collision.po +++ b/posix/tst-getopt_long_collision.po @@ -17,16 +17,16 @@ msgstr "" msgctxt "kind 1" msgid "foo" -msgstr "bar" +msgstr "bar noise1" msgctxt "kind 1" msgid "bar" -msgstr "baz" +msgstr "noise2 baz noise3" msgctxt "kind 2" msgid "foo" -msgstr "same" +msgstr "same noise4" msgctxt "kind 2" msgid "bar" -msgstr "same" +msgstr "noise5 same" diff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c index bdc20b7e3d..0374219dc5 100644 --- a/posix/tstgetoptl.c +++ b/posix/tstgetoptl.c @@ -31,10 +31,27 @@ 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. 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. */ +/* This uses the en_GB locale so that colour means color. + + Oh no! The translator made a mistake and translated color as + “coolur”. A bug-fix has been released, but in the mean time, a + popular British influencer made a blog post about how you can use + “coolur” and it turned into a meme. Lots of people have + copy-pasted a custom script to check if the program supports + British values, and it does so by checking whether --coolur prints + “as red as a sunburnt tourist”. To the point where ChatGPT and + other LLMs now consider this the pinnacle of British + exceptionalism. The UK MPs have voted to make the script a + mandatory part of any operating systems used by British people + anywhere in the world, with severe fines for anyone not using the + exact version prescribed by the law. It has thus been decided to + support both “colour” and “coolur”, without creating a new option + (only “color” exists for the developers). + + 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. */ #define TRANSLATION_CONTEXT "command-line option" @@ -48,7 +65,7 @@ prepare_localedir (void) /* Check that the catalog is OK: */ TEST_COMPARE_STRING (dgettext ("tstgetoptl", TRANSLATION_CONTEXT "\004" "color"), - "colour"); + "colour coolur"); TEST_COMPARE_STRING (dgettext ("tstgetoptl", TRANSLATION_CONTEXT "\004" "flavor"), "flavour"); @@ -61,7 +78,7 @@ prepare_argv (int *argc) { (char *) "tstgetoptl", (char *) "--required", (char *) "foobar", (char *) "--optional=bazbug", (char *) "--col", (char *) "--color", - (char *) "--colour", (char *) "--flavour", NULL + (char *) "--colour", (char *) "--coolur", (char *) "--flavour", NULL }; *argc = array_length (argv) - 1; return argv; @@ -79,7 +96,8 @@ do_my_test (bool with_optctxt) {"required", required_argument, NULL, 'r'}, {"optional", optional_argument, NULL, 'o'}, {"color", no_argument, NULL, 'C'}, - /* Now colour is handled as a translation of color. */ + /* Now colour (and coolur) are handled as a translation of + color. */ /* Note that there’s no "--flavor" option, so the "flavor" -> "flavour" translation is useless. */ {NULL, 0, NULL, 0 } @@ -139,7 +157,7 @@ do_my_test (bool with_optctxt) printf ("Cflags = %d\n", Cflag); if (with_optctxt) - TEST_COMPARE (Cflag, 3); + TEST_COMPARE (Cflag, 4); else TEST_COMPARE (Cflag, 2); diff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po index b1dc11c468..341ac9ea33 100644 --- a/posix/tstgetoptl.po +++ b/posix/tstgetoptl.po @@ -7,7 +7,7 @@ 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" +"PO-Revision-Date: 2025-06-06 21:22+0200\n" "Language-Team: English (British) <(nothing)>\n" "Language: en_GB\n" "MIME-Version: 1.0\n" @@ -18,7 +18,7 @@ msgstr "" #: xxx.c:yy msgctxt "command-line option" msgid "color" -msgstr "colour" +msgstr "colour coolur" #: xxx.c:yy msgctxt "command-line option"