Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/1.1/patches/2227403/?format=api
{ "id": 2227403, "url": "http://patchwork.ozlabs.org/api/1.1/patches/2227403/?format=api", "web_url": "http://patchwork.ozlabs.org/project/glibc/patch/450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu/", "project": { "id": 41, "url": "http://patchwork.ozlabs.org/api/1.1/projects/41/?format=api", "name": "GNU C Library", "link_name": "glibc", "list_id": "libc-alpha.sourceware.org", "list_email": "libc-alpha@sourceware.org", "web_url": "", "scm_url": "", "webscm_url": "" }, "msgid": "<450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu>", "date": "2026-04-23T16:04:07", "name": "[v22,9/9] posix, argp: Support multiple long option name translations", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "87d55de7f0621c60ffcb0502cd3b10bd80c8952e", "submitter": { "id": 90948, "url": "http://patchwork.ozlabs.org/api/1.1/people/90948/?format=api", "name": "Vivien Kraus", "email": "vivien@planete-kraus.eu" }, "delegate": null, "mbox": "http://patchwork.ozlabs.org/project/glibc/patch/450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu/mbox/", "series": [ { "id": 501215, "url": "http://patchwork.ozlabs.org/api/1.1/series/501215/?format=api", "web_url": "http://patchwork.ozlabs.org/project/glibc/list/?series=501215", "date": "2026-04-23T16:03:58", "name": "Support translated long option names in getopt and argp", "version": 22, "mbox": "http://patchwork.ozlabs.org/series/501215/mbox/" } ], "comments": "http://patchwork.ozlabs.org/api/patches/2227403/comments/", "check": "pending", "checks": "http://patchwork.ozlabs.org/api/patches/2227403/checks/", "tags": {}, "headers": { "Return-Path": "<libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org>", "X-Original-To": [ "incoming@patchwork.ozlabs.org", "libc-alpha@sourceware.org" ], "Delivered-To": [ "patchwork-incoming@legolas.ozlabs.org", "libc-alpha@sourceware.org" ], "Authentication-Results": [ "legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu\n header.a=rsa-sha1 header.s=albinoniA header.b=QIhMstwb;\n\tdkim-atps=neutral", "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=sourceware.org\n (client-ip=2620:52:6:3111::32; helo=vm01.sourceware.org;\n envelope-from=libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org;\n receiver=patchwork.ozlabs.org)", "sourceware.org;\n\tdkim=pass (2048-bit key,\n secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu\n header.a=rsa-sha1 header.s=albinoniA header.b=QIhMstwb", "sourceware.org; dmarc=pass (p=reject dis=none)\n header.from=planete-kraus.eu", "sourceware.org;\n spf=pass smtp.mailfrom=planete-kraus.eu", "server2.sourceware.org;\n arc=none smtp.remote-ip=89.234.140.182" ], "Received": [ "from vm01.sourceware.org (vm01.sourceware.org\n [IPv6:2620:52:6:3111::32])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4g1gwR6Nmfz1y2d\n\tfor <incoming@patchwork.ozlabs.org>; Fri, 24 Apr 2026 02:07:35 +1000 (AEST)", "from vm01.sourceware.org (localhost [127.0.0.1])\n\tby sourceware.org (Postfix) with ESMTP id DA5E44BB3BD0\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 23 Apr 2026 16:07:33 +0000 (GMT)", "from planete-kraus.eu (planete-kraus.eu [89.234.140.182])\n by sourceware.org (Postfix) with ESMTPS id D11DB4B87BAB\n for <libc-alpha@sourceware.org>; Thu, 23 Apr 2026 16:05:59 +0000 (GMT)", "from planete-kraus.eu (localhost [127.0.0.1])\n by planete-kraus.eu (OpenSMTPD) with ESMTP id c0ab0975;\n Thu, 23 Apr 2026 16:05:43 +0000 (UTC)", "by planete-kraus.eu (OpenSMTPD) with ESMTPSA id fdbe342a\n (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO);\n Thu, 23 Apr 2026 16:05:42 +0000 (UTC)" ], "DKIM-Filter": [ "OpenDKIM Filter v2.11.0 sourceware.org DA5E44BB3BD0", "OpenDKIM Filter v2.11.0 sourceware.org D11DB4B87BAB" ], "DMARC-Filter": "OpenDMARC Filter v1.4.2 sourceware.org D11DB4B87BAB", "ARC-Filter": "OpenARC Filter v1.0.0 sourceware.org D11DB4B87BAB", "ARC-Seal": "i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960360; cv=none;\n b=UKPJtkYs/f+29v7zlrXScn2aOU1IMa2CXpqKAJ+mi2vg/PUdItda7Rcpl+svn1V0sq2HAfKuwNzT0ovGCM8fYwvuuFYOX/s8Ri32F7ZCgyquYAe7soE8yH6lGj6Q9PVAyGbiVvHNYNCOGuAmOCjoJUrFgYIwhUAHO3RYQmWxcI0=", "ARC-Message-Signature": "i=1; a=rsa-sha256; d=sourceware.org; s=key;\n t=1776960360; c=relaxed/simple;\n bh=6ymLAM/23KYyfPgDZWCFrAMCfsdGFieFnkh9RN/ocOk=;\n h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version;\n b=xifGoYwVEcoOMQWp8SDtQdXNorsH7VCqtXMh6VDHCOAKIM1shAP8Ycb7Ol4VlxjpOLFVrBoTSlBPLS5sK6MV9KBhCPozBfyc/WQ1Xe4C6opofGq4bj38sL4TKv5x83IwyQERbL1doEMGZk9wzY/qEeUcYraMouWBvRoKWwDeEQ4=", "ARC-Authentication-Results": "i=1; server2.sourceware.org", "DKIM-Signature": "v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from\n :to:cc:subject:date:message-id:in-reply-to:references\n :mime-version:content-type:content-transfer-encoding; s=\n albinoniA; bh=adNwM/4Gy4Mf6b5oCxAs88zBKo4=; b=QIhMstwboOcMxJXqJ5\n t+FPUV3WBPNbhaDFlThBY/JG13YCkqopNWr8kzgD3zhZhIlEyFIXM+BieXp0Oa1j\n BIN0T6CtaE2YOcmJmR4901SpOGHzhwiMGSmPwaX8r0VkiYlNc6qE3kLIBEYCNN+x\n KeKPDP5WJcz9tuWiuNPHDRyGtlckMlRhSlgCXaSjdUf6BoECXm3cgs8cS1GN614T\n Cm5yNIKMYrSovkhbx4xD1WYFRM968dE1xmk7Mek6Ge7VCLh3HTS5sWn622EG9GgM\n ArX9bHh5Q0VhBaFncAKY7WAuRZotVfjILgPbi+LLofyRsrl5Fs4ym24tNWAHb7yn\n kKFg==", "From": "Vivien Kraus <vivien@planete-kraus.eu>", "To": "adhemerval.zanella@linaro.org,\n\tlibc-alpha@sourceware.org", "Cc": "Vivien Kraus <vivien@planete-kraus.eu>", "Subject": "[PATCH v22 9/9] posix,\n argp: Support multiple long option name translations", "Date": "Thu, 23 Apr 2026 18:04:07 +0200", "Message-ID": "\n <450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu>", "X-Mailer": "git-send-email 2.52.0", "In-Reply-To": "<cover.1776957777.git.vivien@planete-kraus.eu>", "References": "<cover.1776957777.git.vivien@planete-kraus.eu>", "MIME-Version": "1.0", "Content-Type": "text/plain; charset=UTF-8", "Content-Transfer-Encoding": "8bit", "X-BeenThere": "libc-alpha@sourceware.org", "X-Mailman-Version": "2.1.30", "Precedence": "list", "List-Id": "Libc-alpha mailing list <libc-alpha.sourceware.org>", "List-Unsubscribe": "<https://sourceware.org/mailman/options/libc-alpha>,\n <mailto:libc-alpha-request@sourceware.org?subject=unsubscribe>", "List-Archive": "<https://sourceware.org/pipermail/libc-alpha/>", "List-Post": "<mailto:libc-alpha@sourceware.org>", "List-Help": "<mailto:libc-alpha-request@sourceware.org?subject=help>", "List-Subscribe": "<https://sourceware.org/mailman/listinfo/libc-alpha>,\n <mailto:libc-alpha-request@sourceware.org?subject=subscribe>", "Errors-To": "libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org" }, "content": "There are programs that have different options having the same\nbehavior. For instance, emacs: the --eval and --execute options have\nthe same behavior. So, if Emacs were written in a different language\nthan English where “foo” translates both to “eval” and “execute”, then\nthe English translator would have a hard time deciding between “eval”\nand “execute” for the translation of the only option --foo.\n\nOther example I’ve found: bash --init-file / --rcfile.\n\nIn conclusion: the translator lacks the liberty to name equivalent\nbehaviors differently, and this liberty is sometimes useful.\n\nIn this patch, the translation for an option name is expected to be a\nspace-separated list of equally accepted translations of a\ncommand-line option. I chose a space because it would be a bad choice\nto have a space character in an option name anyway.\n\nThe translation type is extended to have a list of translations. We\nchange the following behaviors:\n1. Argument matching tries any translation;\n2. Collision detection searches collisions with all\ntranslations;\n3. Argp --help / --usage now lists all translations, but the argument\nplaceholder is not repeated.\n---\n argp/argp-help.c | 170 ++++++++++++------\n argp/tst-argphelp-localized.c | 55 +++++-\n argp/tst-argphelp-localized.po | 3 +-\n argp/tst-argpusage-localized.c | 3 +-\n manual/getopt.texi | 5 +\n posix/check-getopt-translations.pl | 26 +--\n posix/getopt.c | 136 ++++++++++++--\n .../standalone-multiple-getopt-collisions.po | 6 +-\n posix/tst-getopt_long_collision.c | 10 +-\n posix/tst-getopt_long_collision.po | 8 +-\n posix/tstgetoptl.c | 34 +++-\n posix/tstgetoptl.po | 4 +-\n 12 files changed, 355 insertions(+), 105 deletions(-)", "diff": "diff --git a/argp/argp-help.c b/argp/argp-help.c\nindex aad5d7be13..e748e34258 100644\n--- a/argp/argp-help.c\n+++ b/argp/argp-help.c\n@@ -1205,36 +1205,81 @@ comma (unsigned col, struct pentry_state *pest)\n indent_to (pest->stream, col);\n }\n \f\n-/* Help and usage output show the translated option name. *allocated\n- holds a pointer that should be freed by the caller, or a NULL\n- pointer. */\n-static const char *\n-translate_option_name (const char *name, char **allocated)\n+/* Help and usage output show the translated option name. Since an\n+ option name may have multiple translations, we return all of them.\n+ The result is to be freed by the caller, each element of the array\n+ first, and then the array itself. */\n+static char **\n+translate_option_name (const char *name)\n {\n /* Argp does not have a configuration for the context, so a default\n one is used. */\n+ char *msgid = NULL;\n+ const char *full_translation = NULL;\n+ size_t n_translations;\n+ char **results = NULL;\n /* FIXME: use pgettext_expr. */\n- *allocated = NULL;\n if (__libc_enable_secure)\n /* Translations are disabled. */\n- return name;\n- if (__asprintf (allocated, \"command-line option\\004%s\", name) == -1)\n {\n- /* *allocated is NULL */\n- return name;\n+ results = calloc (2, sizeof (char *));\n+ if (results != NULL)\n+\t{\n+\t results[0] = __strdup (name);\n+\t if (results[0] == NULL)\n+\t {\n+\t free (results);\n+\t results = NULL;\n+\t }\n+\t}\n+ return results;\n }\n- const char *translated = gettext (*allocated);\n- if (strcmp (translated, *allocated) == 0)\n+ if (__asprintf (&msgid, \"command-line option\\004%s\", name) == -1)\n+ /* Do not bother trying to strdup name. */\n+ return NULL;\n+ full_translation = gettext (msgid);\n+ if (strcmp (full_translation, msgid) == 0)\n+ full_translation = name;\n+ /* Split full_translation into results. Do it in 2 passes: first\n+ count, then copy. */\n+ for (int pass = 0; pass < 2; pass++)\n {\n- /* No translation performed. */\n- free (*allocated);\n- *allocated = NULL;\n- return name;\n+ n_translations = 0;\n+ const char *start = full_translation;\n+ const char *end = NULL;\n+ while (start != NULL)\n+\t{\n+\t end = strchr (start, ' ');\n+\t if (pass == 1)\n+\t {\n+\t if (end == NULL)\n+\t\tresults[n_translations] = __strdup (start);\n+\t else\n+\t\tresults[n_translations] = __strndup (start, end - start);\n+\t if (results[n_translations] == NULL)\n+\t\t{\n+\t\t /* Abort. */\n+\t\t for (size_t i = 0; i < n_translations; i++)\n+\t\t free (results[i]);\n+\t\t free (results);\n+\t\t return NULL;\n+\t\t}\n+\t }\n+\t start = end;\n+\t if (start != NULL)\n+\t /* Skip ' ' */\n+\t start++;\n+\t n_translations++;\n+\t}\n+ if (pass == 0)\n+\tresults = calloc (n_translations + 1, sizeof (char *));\n+ if (results == NULL)\n+\treturn NULL;\n }\n- /* FIXME: is it safe to discard *allocated early here? Won’t the\n- return value alias it? */\n- /* *allocated is to be freed by the caller. */\n- return translated;\n+ /* We did not touch that index, and allocated the array with\n+ calloc. */\n+ assert (results[n_translations] == NULL);\n+ return results;\n }\n \f\n /* Print help for ENTRY to STREAM. */\n@@ -1245,7 +1290,7 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,\n unsigned num;\n const struct argp_option *real = entry->opt, *opt;\n char *so = entry->short_options;\n- const char *translated_option_name;\n+ char **translated_option_names;\n int have_long_opt = 0;\t/* We have any long options. */\n /* Saved margins. */\n int old_lm = __argp_fmtstream_set_lmargin (stream, 0);\n@@ -1304,19 +1349,35 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,\n else\n /* A real long option. */\n {\n+ bool needs_untranslated = true;\n __argp_fmtstream_set_wmargin (stream, uparams.long_opt_col);\n for (opt = real, num = entry->num; num > 0; opt++, num--)\n \tif (opt->name && ovisible (opt))\n \t {\n \t comma (uparams.long_opt_col, &pest);\n-\t char *name_allocated = NULL;\n-\t translated_option_name = translate_option_name (opt->name, &name_allocated);\n-\t __argp_fmtstream_printf (stream, \"--%s\", translated_option_name);\n-\t arg (real, \"=%s\", \"[=%s]\",\n-\t\t state == NULL ? NULL : state->root_argp->argp_domain, stream);\n-\t if (strcmp (translated_option_name, opt->name))\n+\t translated_option_names = translate_option_name (opt->name);\n+\t for (size_t i = 0;\n+\t\t (translated_option_names != NULL\n+\t\t && translated_option_names[i] != NULL);\n+\t\t i++)\n+\t {\n+\t\tif (i != 0)\n+\t\t __argp_fmtstream_printf (stream, \", \");\n+\t\t__argp_fmtstream_printf (stream, \"--%s\", translated_option_names[i]);\n+\t\t/* Only display the argument for the first translation. */\n+\t\tif (i == 0)\n+\t\t arg (real, \"=%s\", \"[=%s]\",\n+\t\t state == NULL ? NULL : state->root_argp->argp_domain, stream);\n+\t\t/* If we see the untranslated name, we won’t repeat it. */\n+\t\tif (strcmp (translated_option_names[i], opt->name) == 0)\n+\t\t needs_untranslated = false;\n+\t\tfree (translated_option_names[i]);\n+\t }\n+\t free (translated_option_names);\n+\t if (needs_untranslated)\n \t __argp_fmtstream_printf (stream, \" (--%s)\", opt->name);\n-\t free (name_allocated);\n+\t /* If memory allocation failed, the --help output will\n+\t just display the untranslated name in parenthesis. */\n \t }\n }\n \n@@ -1458,39 +1519,46 @@ usage_long_opt (const struct argp_option *opt,\n {\n argp_fmtstream_t stream = cookie;\n const char *arg = opt->arg;\n- const char *translated_option_name = opt->name;\n+ char **translated_option_names = NULL;\n int flags = opt->flags | real->flags;\n+ bool needs_untranslated = true;\n \n if (! arg)\n arg = real->arg;\n \n if (! (flags & OPTION_NO_USAGE))\n {\n- char *name_allocated = NULL;\n- translated_option_name =\n-\ttranslate_option_name (opt->name, &name_allocated);\n- int translation_differs =\n-\t(strcmp (translated_option_name, opt->name) != 0);\n+ translated_option_names =\n+\ttranslate_option_name (opt->name);\n+ __argp_fmtstream_printf (stream, \" [\");\n if (arg)\n+\targ = dgettext (domain, arg);\n+ for (size_t i = 0;\n+\t (translated_option_names != NULL\n+\t && translated_option_names[i] != NULL);\n+\t i++)\n \t{\n-\t arg = dgettext (domain, arg);\n-\t if ((flags & OPTION_ARG_OPTIONAL) && translation_differs)\n-\t __argp_fmtstream_printf (stream, \" [--%s[=%s] (--%s)]\",\n-\t\t\t\t translated_option_name, arg, opt->name);\n-\t else if (flags & OPTION_ARG_OPTIONAL)\n-\t __argp_fmtstream_printf (stream, \" [--%s[=%s]]\", opt->name, arg);\n-\t else if (translation_differs)\n-\t __argp_fmtstream_printf (stream, \" [--%s=%s (--%s)]\",\n-\t\t\t\t translated_option_name, arg, opt->name);\n-\t else\n-\t __argp_fmtstream_printf (stream, \" [--%s=%s]\", opt->name, arg);\n+\t if (i != 0)\n+\t __argp_fmtstream_printf (stream, \" / \");\n+\t __argp_fmtstream_printf (stream, \"--%s\", translated_option_names[i]);\n+\t if (arg && i == 0)\n+\t {\n+\t if (flags & OPTION_ARG_OPTIONAL)\n+\t\t__argp_fmtstream_printf (stream, \"[=%s]\", arg);\n+\t else\n+\t\t__argp_fmtstream_printf (stream, \"=%s\", arg);\n+\t }\n+\t if (strcmp (translated_option_names[i], opt->name) == 0)\n+\t needs_untranslated = false;\n+\t free (translated_option_names[i]);\n \t}\n- else if (translation_differs)\n-\t__argp_fmtstream_printf (stream, \" [--%s (--%s)]\",\n-\t\t\t\t translated_option_name, opt->name);\n- else\n-\t__argp_fmtstream_printf (stream, \" [--%s]\", opt->name);\n- free (name_allocated);\n+ free (translated_option_names);\n+ if (needs_untranslated)\n+\t__argp_fmtstream_printf (stream, \" (--%s)\", opt->name);\n+ __argp_fmtstream_printf (stream, \"]\");\n+ /* If memory allocation failed, the output will be like\n+\t [ (--option)]\n+ */\n }\n \n return 0;\ndiff --git a/argp/tst-argphelp-localized.c b/argp/tst-argphelp-localized.c\nindex 8703742b8f..6c36eb9cf8 100644\n--- a/argp/tst-argphelp-localized.c\n+++ b/argp/tst-argphelp-localized.c\n@@ -44,6 +44,8 @@ const struct argp_option options[] =\n };\n \n static bool color_set = false;\n+static bool flavor_set = false;\n+static bool texture_set = false;\n \n static error_t\n parse_opt (int key, char *arg, struct argp_state *state)\n@@ -53,6 +55,14 @@ parse_opt (int key, char *arg, struct argp_state *state)\n FAIL (\"color already set.\\n\");\n else if (key == 'c')\n color_set = true;\n+ else if (key == 'f' && flavor_set)\n+ FAIL (\"flavor already set.\\n\");\n+ else if (key == 'f')\n+ flavor_set = true;\n+ else if (key == 't' && texture_set)\n+ FAIL (\"texture already set.\\n\");\n+ else if (key == 't')\n+ texture_set = true;\n return 0;\n }\n \n@@ -64,6 +74,16 @@ do_test (void)\n char *test1_argv[3] =\n { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--colour=yellow\", NULL };\n char *test2_argv[3] =\n+ { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--color=yellow\", NULL };\n+ char *test3_argv[3] =\n+ { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--coolur=yellow\", NULL };\n+ char *test4_argv[3] =\n+ { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--flavour\", NULL };\n+ char *test5_argv[3] =\n+ { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--flavor\", NULL };\n+ char *test6_argv[3] =\n+ { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--texture\", NULL };\n+ char *test7_argv[3] =\n { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--help\", NULL };\n \n unsetenv (\"LANGUAGE\");\n@@ -72,18 +92,47 @@ do_test (void)\n \t\t\t\t OBJPFX \"domaindir\") != NULL);\n TEST_VERIFY_EXIT (textdomain (\"tst-argphelp-localized\") != NULL);\n /* Check that the catalog is OK: */\n- TEST_COMPARE_STRING (gettext (\"command-line option\\004color\"), \"colour\");\n+ TEST_COMPARE_STRING (gettext (\"command-line option\\004color\"),\n+\t\t \"colour coolur\");\n TEST_COMPARE_STRING (gettext (\"COOKIE\"), \"BISCUIT\");\n argp_parse (&argp, 2, test1_argv, 0, 0, NULL);\n TEST_VERIFY (color_set);\n+ TEST_VERIFY (!flavor_set);\n+ TEST_VERIFY (!texture_set);\n color_set = false;\n+ argp_parse (&argp, 2, test2_argv, 0, 0, NULL);\n+ TEST_VERIFY (color_set);\n+ TEST_VERIFY (!flavor_set);\n+ TEST_VERIFY (!texture_set);\n+ color_set = false;\n+ argp_parse (&argp, 2, test3_argv, 0, 0, NULL);\n+ TEST_VERIFY (color_set);\n+ TEST_VERIFY (!flavor_set);\n+ TEST_VERIFY (!texture_set);\n+ color_set = false;\n+ argp_parse (&argp, 2, test4_argv, 0, 0, NULL);\n+ TEST_VERIFY (!color_set);\n+ TEST_VERIFY (flavor_set);\n+ TEST_VERIFY (!texture_set);\n+ flavor_set = false;\n+ argp_parse (&argp, 2, test5_argv, 0, 0, NULL);\n+ TEST_VERIFY (!color_set);\n+ TEST_VERIFY (flavor_set);\n+ TEST_VERIFY (!texture_set);\n+ flavor_set = false;\n+ argp_parse (&argp, 2, test6_argv, 0, 0, NULL);\n+ TEST_VERIFY (!color_set);\n+ TEST_VERIFY (!flavor_set);\n+ TEST_VERIFY (texture_set);\n+ texture_set = false;\n \n /* This is the last chance to fail. */\n if (support_record_failure_is_failed ())\n- FAIL_EXIT1 (\"There were test failures before the final invocation of --help\");\n+ FAIL_EXIT1 (\n+\t\"There were test failures before the final invocation of --help\");\n /* This last test will exit the program with code 0 and ignore\n previous failures. */\n- argp_parse (&argp, 2, test2_argv, 0, 0, NULL);\n+ argp_parse (&argp, 2, test7_argv, 0, 0, NULL);\n FAIL_EXIT1 (\"--help did not exit the program\");\n return 0;\n }\ndiff --git a/argp/tst-argphelp-localized.po b/argp/tst-argphelp-localized.po\nindex 718cb39ab7..5b3a672a6d 100644\n--- a/argp/tst-argphelp-localized.po\n+++ b/argp/tst-argphelp-localized.po\n@@ -15,7 +15,7 @@ msgstr \"\"\n #: tst-argphelp-localized.c:73\n msgctxt \"command-line option\"\n msgid \"color\"\n-msgstr \"colour\"\n+msgstr \"colour coolur\"\n \n msgctxt \"command-line option\"\n msgid \"flavor\"\n@@ -23,3 +23,4 @@ msgstr \"flavour\"\n \n msgid \"COOKIE\"\n msgstr \"BISCUIT\"\n+\ndiff --git a/argp/tst-argpusage-localized.c b/argp/tst-argpusage-localized.c\nindex f93c2a156e..5d1f568679 100644\n--- a/argp/tst-argpusage-localized.c\n+++ b/argp/tst-argpusage-localized.c\n@@ -64,7 +64,8 @@ do_test (void)\n \t\t\t\t OBJPFX \"domaindir\") != NULL);\n TEST_VERIFY_EXIT (textdomain (\"tst-argphelp-localized\") != NULL);\n /* Check that the catalog is OK: */\n- TEST_COMPARE_STRING (gettext (\"command-line option\\004color\"), \"colour\");\n+ TEST_COMPARE_STRING (gettext (\"command-line option\\004color\"),\n+\t\t \"colour coolur\");\n TEST_COMPARE_STRING (gettext (\"COOKIE\"), \"BISCUIT\");\n /* This is the last chance to fail. */\n if (support_record_failure_is_failed ())\ndiff --git a/manual/getopt.texi b/manual/getopt.texi\nindex e39f3e3f85..e7216c2baf 100644\n--- a/manual/getopt.texi\n+++ b/manual/getopt.texi\n@@ -257,6 +257,11 @@ invocation of your program, the program users should be encouraged to\n use untranslated option names or publish the locale used for this\n invocation.\n \n+If the translation of an option name contains a space character, then\n+it means multiple translations recognize the same option name. This\n+is useful to upgrade a translation without disrupting the user's\n+workflow.\n+\n Since option names may be short words instead of long sentences, they\n may have different translations in different contexts within the same\n program. @xref{Contexts, , Using contexts for solving ambiguities,\ndiff --git a/posix/check-getopt-translations.pl b/posix/check-getopt-translations.pl\nindex c3c3cff1eb..5cabd9be31 100644\n--- a/posix/check-getopt-translations.pl\n+++ b/posix/check-getopt-translations.pl\n@@ -146,7 +146,8 @@ while (my $line = <$pofile>) {\n } elsif ($parser_state == 3 && $line eq \"\") {\n $parser_state = 1;\n } elsif ($parser_state == 3 && $line =~ /^msgstr\\s*\"([^\"]*)\"$/) {\n- $translations{$entry_msgid} = $1;\n+ my @translations_for_this = split(/\\s+/, $1);\n+ $translations{$entry_msgid} = \\@translations_for_this;\n $parser_state = 1;\n }\n }\n@@ -156,13 +157,14 @@ my $number_of_errors = 0;\n # Verify that every option name is unique.\n my %untranslated_name;\n for my $option_name (sort(keys %translations)) {\n- my $translation = $translations{$option_name};\n- my @existing;\n- if (exists $untranslated_name{$translation}) {\n-\t@existing = @{$untranslated_name{$translation}};\n+ for my $translation (@{$translations{$option_name}}) {\n+ my @existing;\n+ if (exists $untranslated_name{$translation}) {\n+ @existing = @{$untranslated_name{$translation}};\n+ }\n+ push(@existing, $option_name);\n+ $untranslated_name{$translation} = \\@existing;\n }\n- push(@existing, $option_name);\n- $untranslated_name{$translation} = \\@existing;\n }\n for my $translation (sort(keys %untranslated_name)) {\n my $names = $untranslated_name{$translation};\n@@ -180,10 +182,12 @@ for my $translation (sort(keys %untranslated_name)) {\n for my $option_name (sort(keys %translations)) {\n for my $other_option_name (sort(keys %translations)) {\n if ($option_name ne $other_option_name) {\n-\t if ($translations{$option_name} eq $other_option_name) {\n-\t\tprint STDERR \"${translations{$option_name}} is a translation of ${option_name}, but it is also a different option.\\n\";\n-\t\t++$number_of_errors;\n-\t }\n+ for my $translation (@{$translations{$option_name}}) {\n+ if ($translation eq $other_option_name) {\n+ print STDERR \"${translation} is a translation of ${option_name}, but it is also a different option.\\n\";\n+ ++$number_of_errors;\n+ }\n+ }\n }\n }\n }\ndiff --git a/posix/getopt.c b/posix/getopt.c\nindex 2983fe6ea7..32cd4cf120 100644\n--- a/posix/getopt.c\n+++ b/posix/getopt.c\n@@ -182,6 +182,50 @@ exchange (char **argv, struct _getopt_data *d)\n d->__last_nonopt = d->optind;\n }\n \n+/* Return TRUE if other is equal to reference. */\n+static bool\n+complete_match (const char *reference,\n+\t\tsize_t reference_length,\n+\t\tconst char *other)\n+{\n+ return (strncmp (reference, other, reference_length) == 0\n+\t /* So, reference is a prefix of other */\n+\t && other[reference_length] == '\\0');\n+}\n+\n+/* Match a string against a space-separated string list. We save an\n+ allocated copy of the exact match for the collision checker. */\n+static bool\n+match_any_translation (const char *reference,\n+\t\t size_t reference_length,\n+\t\t const char *translation,\n+\t\t char **match)\n+{\n+ const char *start = translation;\n+ const char *end;\n+\n+ if (match)\n+ *match = NULL;\n+ while (start != NULL)\n+ {\n+ end = strchr (start, ' ');\n+ if ((end == NULL && complete_match (reference, reference_length, start))\n+\t || (end != NULL\n+\t && end - start == reference_length\n+\t && strncmp (reference, start, reference_length) == 0))\n+\t{\n+\t if (match)\n+\t *match = __strndup (reference, reference_length);\n+\t return true;\n+\t}\n+ start = end;\n+ if (start != NULL)\n+\t/* Skip the space character. */\n+\tstart++;\n+ }\n+ return false;\n+}\n+\n /* Return true iff translation_context is not NULL, a translation for\n opt_name has been found and it matches the substring from argument,\n length argument_length.\n@@ -202,16 +246,42 @@ match_translated_option_name (char *(*translate) (const char *, const char *,\n if (translate != NULL && !__libc_enable_secure)\n translated = translate (opt_textdomain, translation_context,\n \t\t\t opt_name, &translation_buffer);\n-\n- if (strncmp (translated, argument, argument_length) != 0)\n- matches = false;\n- else\n- /* We know that argument is a prefix of translated. */\n- matches = translated[argument_length] == '\\0';\n+ matches = match_any_translation (argument, argument_length, translated, NULL);\n free (translation_buffer);\n return matches;\n }\n \n+/* Translate opt_name, but only keep the first item of the list. This\n+ is used for error messages. */\n+static const char *\n+first_translation (char *(*translate) (const char *, const char *,\n+\t\t\t\t const char *, char **),\n+\t\t const char *translation_context,\n+\t\t const char *opt_textdomain,\n+\t\t const char *opt_name,\n+\t\t char **allocated)\n+{\n+ char *translation_buffer = NULL;\n+ const char *all_translations =\n+ translate (opt_textdomain, translation_context, opt_name,\n+\t &translation_buffer);\n+ const char *end = strchr (all_translations, ' ');\n+ if (end == NULL)\n+ {\n+ /* There is only 1 translation for opt_name. No extra\n+\t processing is needed. */\n+ *allocated = translation_buffer;\n+ return all_translations;\n+ }\n+ *allocated = __strndup (all_translations, end - all_translations);\n+ free (translation_buffer);\n+ if (*allocated == NULL)\n+ /* Memory allocation failed; return opt_name. No extra memory\n+ needs to be kept around. */\n+ return opt_name;\n+ return *allocated;\n+}\n+\n /* Process the argument starting with d->__nextchar as a long option.\n d->optind should *not* have been advanced over this argument.\n \n@@ -393,9 +463,9 @@ process_long_option (int argc, char **argv, const char *optstring,\n \t{\n \t if (print_errors)\n \t {\n-\t translated_option_name = translate (d->opttextdomain, d->optctxt,\n-\t\t\t\t\t\t pfound->name,\n-\t\t\t\t\t\t &translation_buffer);\n+\t translated_option_name =\n+\t\tfirst_translation (translate, d->optctxt, d->opttextdomain,\n+\t\t\t\t pfound->name, &translation_buffer);\n \t if (strcmp (translated_option_name, pfound->name) != 0)\n \t\t/* Print both names of the option. */\n \t\tfprintf (stderr,\n@@ -423,9 +493,9 @@ process_long_option (int argc, char **argv, const char *optstring,\n \t {\n \t /* Same dichotomy as when the option does not allow an\n \t\t argument. */\n-\t translated_option_name = translate (d->opttextdomain, d->optctxt,\n-\t\t\t\t\t\t pfound->name,\n-\t\t\t\t\t\t &translation_buffer);\n+\t translated_option_name =\n+\t\tfirst_translation (translate, d->optctxt, d->opttextdomain,\n+\t\t\t\t pfound->name, &translation_buffer);\n \t if (strcmp (translated_option_name, pfound->name) != 0)\n \t\tfprintf (stderr,\n \t\t\t _(\"%s: option '%s%s' / '%s%s' requires an argument\\n\"),\n@@ -488,6 +558,32 @@ _getopt_initialize (_GL_UNUSED int argc,\n }\n \f\n \n+/* Match any item of a string list against any item of another. This\n+ is used by the collision checker. */\n+static bool\n+match_any_translation_pair (const char *list_a,\n+\t\t\t const char *list_b,\n+\t\t\t char **match)\n+{\n+ const char *start = list_a;\n+ const char *end;\n+\n+ while (start != NULL)\n+ {\n+ end = strchr (start, ' ');\n+ if ((end == NULL\n+\t && match_any_translation (start, strlen (start), list_b, match))\n+\t || (end != NULL\n+\t && match_any_translation (start, end - start, list_b, match)))\n+\treturn true;\n+ start = end;\n+ if (start != NULL)\n+\t/* Skip the space character. */\n+\tstart++;\n+ }\n+ return false;\n+}\n+\n static bool\n has_translation_collisions (const char *domain,\n \t\t\t const char *context,\n@@ -508,6 +604,7 @@ has_translation_collisions (const char *domain,\n char *b_buffer = NULL;\n const char *b_name = NULL;\n const struct option *option_b;\n+ char *collision = NULL;\n bool has_collision = false;\n \n if (do_translate == NULL || context == NULL)\n@@ -532,7 +629,9 @@ has_translation_collisions (const char *domain,\n \t {\n \t option_b = &(long_options[option_index_b]);\n \t b_name = do_translate (domain, context, option_b->name, &b_buffer);\n-\t if (strcmp (option_a->name, b_name) == 0)\n+\t collision = NULL;\n+\t if (match_any_translation (option_a->name, strlen (option_a->name), b_name,\n+\t\t\t\t &collision))\n \t {\n \t\tif (print_errors)\n \t\t /* Since we do not consider a particular use of an\n@@ -546,13 +645,15 @@ has_translation_collisions (const char *domain,\n \t\t\t argv0,\n \t\t\t domain, context,\n \t\t\t option_a->name,\n-\t\t\t option_b->name, b_name);\n+\t\t\t option_b->name, collision);\n \t\thas_collision = true;\n \t }\n-\t if (strcmp (a_name, b_name) == 0\n+\t free (collision);\n+\t collision = NULL;\n+\t if (option_index_a < option_index_b\n \t\t&& strcmp (option_a->name, a_name) != 0\n \t\t&& strcmp (option_b->name, b_name) != 0\n-\t\t&& option_index_a < option_index_b)\n+\t\t&& match_any_translation_pair (a_name, b_name, &collision))\n \t {\n \t\tif (print_errors)\n \t\t fprintf (stderr,\n@@ -560,9 +661,10 @@ has_translation_collisions (const char *domain,\n \t\t\t \"domain '%s', context '%s': \"\n \t\t\t \"both '%s' and '%s' translate to '%s'\\n\"),\n \t\t\t argv0, domain, context,\n-\t\t\t option_a->name, option_b->name, a_name);\n+\t\t\t option_a->name, option_b->name, collision);\n \t\thas_collision = true;\n \t }\n+\t free (collision);\n \t free (b_buffer);\n \t }\n free (a_buffer);\ndiff --git a/posix/standalone-multiple-getopt-collisions.po b/posix/standalone-multiple-getopt-collisions.po\nindex 14b876a2a3..edd2231d8f 100644\n--- a/posix/standalone-multiple-getopt-collisions.po\n+++ b/posix/standalone-multiple-getopt-collisions.po\n@@ -27,17 +27,17 @@ msgstr \"bar\"\n # This is the --foo option.\n msgctxt \"command-line option\"\n msgid \"foo\"\n-msgstr \"toto\"\n+msgstr \"tata toto\"\n \n # This is the --bar option. Oops, I translated with toto here too.\n msgctxt \"command-line option\"\n msgid \"bar\"\n-msgstr \"toto\"\n+msgstr \"titi toto\"\n \n # Let’s go to the --pub!\n msgctxt \"command-line option\"\n msgid \"pub\"\n-msgstr \"bar\"\n+msgstr \"bar club\"\n \n # Wait, it’s OK if baz is translated to baz though.\n msgctxt \"command-line option\"\ndiff --git a/posix/tst-getopt_long_collision.c b/posix/tst-getopt_long_collision.c\nindex 2e603ec9f8..ff90ec1131 100644\n--- a/posix/tst-getopt_long_collision.c\n+++ b/posix/tst-getopt_long_collision.c\n@@ -42,6 +42,8 @@\n In the third, we don’t translate anything:\n foo -> foo\n bar -> bar\n+\n+ For added fun, we add some noise to the translations.\n */\n \n static const struct option options[] =\n@@ -62,13 +64,13 @@ setup_catalog (void)\n TEST_VERIFY_EXIT (textdomain (\"tst-getopt_long_collision\") != NULL);\n /* Check that the catalog is OK: */\n TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 1\\004foo\"),\n-\t\t \"bar\");\n+\t\t \"bar noise1\");\n TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 1\\004bar\"),\n-\t\t \"baz\");\n+\t\t \"noise2 baz noise3\");\n TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 2\\004foo\"),\n-\t\t \"same\");\n+\t\t \"same noise4\");\n TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 2\\004bar\"),\n-\t\t \"same\");\n+\t\t \"noise5 same\");\n TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 3\\004foo\"),\n \t\t \"kind 3\\004foo\");\n TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 3\\004bar\"),\ndiff --git a/posix/tst-getopt_long_collision.po b/posix/tst-getopt_long_collision.po\nindex 2f39001c6d..a196e81f38 100644\n--- a/posix/tst-getopt_long_collision.po\n+++ b/posix/tst-getopt_long_collision.po\n@@ -17,16 +17,16 @@ msgstr \"\"\n \n msgctxt \"kind 1\"\n msgid \"foo\"\n-msgstr \"bar\"\n+msgstr \"bar noise1\"\n \n msgctxt \"kind 1\"\n msgid \"bar\"\n-msgstr \"baz\"\n+msgstr \"noise2 baz noise3\"\n \n msgctxt \"kind 2\"\n msgid \"foo\"\n-msgstr \"same\"\n+msgstr \"same noise4\"\n \n msgctxt \"kind 2\"\n msgid \"bar\"\n-msgstr \"same\"\n+msgstr \"noise5 same\"\ndiff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c\nindex bdc20b7e3d..0374219dc5 100644\n--- a/posix/tstgetoptl.c\n+++ b/posix/tstgetoptl.c\n@@ -31,10 +31,27 @@\n This echoes tstgetopt.c, where --colour was an option name alias\n for --color, so it had to be listed twice. */\n \n-/* This uses the en_GB locale so that colour means color. We also\n- check that getopt only matches translations for actual options, by\n- having the user pass --flavour (which is a known translation of\n- flavor) without the program recognizing a --flavor option. */\n+/* This uses the en_GB locale so that colour means color.\n+\n+ Oh no! The translator made a mistake and translated color as\n+ “coolur”. A bug-fix has been released, but in the mean time, a\n+ popular British influencer made a blog post about how you can use\n+ “coolur” and it turned into a meme. Lots of people have\n+ copy-pasted a custom script to check if the program supports\n+ British values, and it does so by checking whether --coolur prints\n+ “as red as a sunburnt tourist”. To the point where ChatGPT and\n+ other LLMs now consider this the pinnacle of British\n+ exceptionalism. The UK MPs have voted to make the script a\n+ mandatory part of any operating systems used by British people\n+ anywhere in the world, with severe fines for anyone not using the\n+ exact version prescribed by the law. It has thus been decided to\n+ support both “colour” and “coolur”, without creating a new option\n+ (only “color” exists for the developers).\n+\n+ We also check that getopt only matches\n+ translations for actual options, by having the user pass --flavour\n+ (which is a known translation of flavor) without the program\n+ recognizing a --flavor option. */\n \n #define TRANSLATION_CONTEXT \"command-line option\"\n \n@@ -48,7 +65,7 @@ prepare_localedir (void)\n /* Check that the catalog is OK: */\n TEST_COMPARE_STRING (dgettext (\"tstgetoptl\",\n \t\t\t\t TRANSLATION_CONTEXT \"\\004\" \"color\"),\n-\t\t \"colour\");\n+\t\t \"colour coolur\");\n TEST_COMPARE_STRING (dgettext (\"tstgetoptl\",\n \t\t\t\t TRANSLATION_CONTEXT \"\\004\" \"flavor\"),\n \t\t \"flavour\");\n@@ -61,7 +78,7 @@ prepare_argv (int *argc)\n {\n (char *) \"tstgetoptl\", (char *) \"--required\", (char *) \"foobar\",\n (char *) \"--optional=bazbug\", (char *) \"--col\", (char *) \"--color\",\n- (char *) \"--colour\", (char *) \"--flavour\", NULL\n+ (char *) \"--colour\", (char *) \"--coolur\", (char *) \"--flavour\", NULL\n };\n *argc = array_length (argv) - 1;\n return argv;\n@@ -79,7 +96,8 @@ do_my_test (bool with_optctxt)\n {\"required\", required_argument, NULL, 'r'},\n {\"optional\", optional_argument, NULL, 'o'},\n {\"color\",\t no_argument,\t NULL, 'C'},\n- /* Now colour is handled as a translation of color. */\n+ /* Now colour (and coolur) are handled as a translation of\n+\t color. */\n /* Note that there’s no \"--flavor\" option, so the \"flavor\" ->\n \t \"flavour\" translation is useless. */\n {NULL, 0, NULL, 0 }\n@@ -139,7 +157,7 @@ do_my_test (bool with_optctxt)\n printf (\"Cflags = %d\\n\", Cflag);\n \n if (with_optctxt)\n- TEST_COMPARE (Cflag, 3);\n+ TEST_COMPARE (Cflag, 4);\n else\n TEST_COMPARE (Cflag, 2);\n \ndiff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po\nindex b1dc11c468..341ac9ea33 100644\n--- a/posix/tstgetoptl.po\n+++ b/posix/tstgetoptl.po\n@@ -7,7 +7,7 @@ msgstr \"\"\n \"Project-Id-Version: tstgetoptl 0.0.0\\n\"\n \"Report-Msgid-Bugs-To: \\n\"\n \"POT-Creation-Date: 2025-05-27 19:29+0200\\n\"\n-\"PO-Revision-Date: 2025-05-27 19:30+0200\\n\"\n+\"PO-Revision-Date: 2025-06-06 21:22+0200\\n\"\n \"Language-Team: English (British) <(nothing)>\\n\"\n \"Language: en_GB\\n\"\n \"MIME-Version: 1.0\\n\"\n@@ -18,7 +18,7 @@ msgstr \"\"\n #: xxx.c:yy\n msgctxt \"command-line option\"\n msgid \"color\"\n-msgstr \"colour\"\n+msgstr \"colour coolur\"\n \n #: xxx.c:yy\n msgctxt \"command-line option\"\n", "prefixes": [ "v22", "9/9" ] }