From patchwork Tue Jan 27 19:18:11 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 129067 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 2A8E14BA2E3E for ; Tue, 27 Jan 2026 19:26:58 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 2A8E14BA2E3E 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=albinoniB header.b=kMsUtrOo 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 12BEE4BA23E9 for ; Tue, 27 Jan 2026 19:23:32 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 12BEE4BA23E9 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 12BEE4BA23E9 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=1769541813; cv=none; b=VgjOSvWI+tsJtzCPNluqQGQymFBjs9y7xvnClN/6I66VBLphvdrCKrNat0/wITmluTbOMWxsZUj+oFfuSYF6UDMJBMK5BelBtMhv8y8/9X6F8ArsER9jRUPlhdNAznARzNUu64Y0hA114jeDRH9+OOMZDZnlRhJzLaeRUDriUFU= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1769541813; c=relaxed/simple; bh=tmY1Lud64IRqEDSR+Yj5ghxELPWLzNZmOx0HOtnf+C4=; h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version; b=JOVL4do6hfl77Qiy+6nn4/sJz/ysc1RqBmNeRx96lZabx5gzrmjNLKPIZv/3fULumqkJ8Wt3lrbleDPkGo9GqP0m53AbL1TYnOdbm6gAl8qBhArF6rdku9xbFa8Tl2ao8G5ZTpzCXQdgQ54TJTB/+tBSVtEmuiTOdXqU8dfIxwo= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 12BEE4BA23E9 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id cd120c3f; Tue, 27 Jan 2026 19:23:22 +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= albinoniB; bh=0iBdE5LHl0wWdpkBE6Rr8TV3Yv4=; b=kMsUtrOo8xMSCRR8LL IGdcAX9N3ai728ksXOL0v2iywjewzhdsWfk8xWGtv1ZUpmZ3DfFWUelwbJl0Dtyi n1MC0soK0UDZrmTMdKz9cn9LrdmJQB7rD3f4leiu1nfGz5GIc8oiRdS43WT8gXAi ewNH51vOPmurjrAIdLIUXxRHHaTe4JttRL6MeBOHjm6ajiQ8HL/qvr4JO/k4nmS7 /pJRjPq3ybSJVLngFHOJiIHdtX9gcw0xlNfwjsbZZvU9iEQ8JgHQrmT61weBkEwG li1192sfefEtl76HibFBb5p1Xv/tCo8LPTtnSNn2Z6rNkGO+duKAxSVvWu/Yv84p wC5A== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id f19ca2e0 (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Tue, 27 Jan 2026 19:23:22 +0000 (UTC) From: Vivien Kraus To: libc-alpha@sourceware.org, adhemerval.zanella@linaro.org Cc: Vivien Kraus Subject: [PATCH v20 06/11] posix, argp: Support deprecation of long option name translations Date: Tue, 27 Jan 2026 20:18:11 +0100 Message-Id: <4848ef4a9e14e0ac6fa087c8212944021fc4bcee.1769539987.git.vivien@planete-kraus.eu> X-Mailer: git-send-email 2.34.1 In-Reply-To: References: <68a758ae45c064bad35bfec73c3d5ffd050398e3.1748369494.git.vivien@planete-kraus.eu> 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, RCVD_IN_DNSWL_BLOCKED, 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 The translation for an option name is expected to be a space-separated list. The first field is the canonical translation. The other fields are deprecated translations. getopt will warn if they are recognized, and argp will not print them in help or usage output. I chose a space because it would be a bad choice to have a space character in an option name. --- argp/argp-help.c | 84 +++++++++++++++++++++------------- argp/tst-argphelp-localized.c | 55 ++++++++++++++++++++-- argp/tst-argphelp-localized.po | 7 ++- argp/tst-argpusage-localized.c | 3 +- manual/getopt.texi | 5 ++ posix/getopt.c | 55 +++++++++++++++++++++- posix/tstgetoptl.c | 17 ++++--- posix/tstgetoptl.po | 4 +- 8 files changed, 183 insertions(+), 47 deletions(-) diff --git a/argp/argp-help.c b/argp/argp-help.c index 0cacf19e33..b38e6b9ea7 100644 --- a/argp/argp-help.c +++ b/argp/argp-help.c @@ -1205,33 +1205,50 @@ 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. */ +/* Return the canonical translation of an option name. There might be + multiple alternative translation for backward compatibility, but + only one should be documented. */ static const char * -translate_option_name (const char *name, char **allocated) +canonical_option_translation (const char *name, + char **allocated) { /* Argp does not have a configuration for the context, so a default one is used. */ /* FIXME: use pgettext_expr. */ + char *full_msgid = NULL; + const char *all_names = NULL; *allocated = NULL; - if (__asprintf (allocated, "command-line option\004%s", name) == -1) + if (__asprintf (&full_msgid, "command-line option\004%s", name) == -1) { /* *allocated is NULL */ + free (full_msgid); return name; } - const char *translated = gettext (*allocated); - if (strcmp (translated, *allocated) == 0) + all_names = gettext (full_msgid); + if (strcmp (all_names, full_msgid) == 0) { - /* No translation performed. */ - free (*allocated); - *allocated = NULL; + /* No translation available: return the untranslated name. */ + /* *allocated still NULL */ + free (full_msgid); 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; + /* all_names is a space-separated list, keep only the first + token. */ + size_t canonical_length = strlen (all_names); + if (strchr (all_names, ' ')) + canonical_length = strchr (all_names, ' ') - all_names; + *allocated = malloc (canonical_length + 1); + if (*allocated == NULL) + { + /* Continue without translations. */ + /* *allocated still NULL */ + free (full_msgid); + return name; + } + memcpy (*allocated, all_names, canonical_length); + (*allocated)[canonical_length] = '\0'; + free (full_msgid); + return *allocated; } /* Print help for ENTRY to STREAM. */ @@ -1242,7 +1259,8 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state, unsigned num; const struct argp_option *real = entry->opt, *opt; char *so = entry->short_options; - const char *translated_option_name; + char *canonical_translation_buffer; + const char *canonical_translation; int have_long_opt = 0; /* We have any long options. */ /* Saved margins. */ int old_lm = __argp_fmtstream_set_lmargin (stream, 0); @@ -1306,14 +1324,15 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state, 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); + canonical_translation = + canonical_option_translation (opt->name, + &canonical_translation_buffer); + __argp_fmtstream_printf (stream, "--%s", canonical_translation); arg (real, "=%s", "[=%s]", state == NULL ? NULL : state->root_argp->argp_domain, stream); - if (strcmp (translated_option_name, opt->name)) + if (strcmp (canonical_translation, opt->name)) __argp_fmtstream_printf (stream, " (--%s)", opt->name); - free (name_allocated); + free (canonical_translation_buffer); } } @@ -1455,7 +1474,8 @@ 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; + const char *canonical_translation = opt->name; + char *canonical_translation_buffer; int flags = opt->flags | real->flags; if (! arg) @@ -1463,31 +1483,29 @@ 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); + canonical_translation = + canonical_option_translation (opt->name, &canonical_translation_buffer); if (arg) { arg = dgettext (domain, arg); - if ((flags & OPTION_ARG_OPTIONAL) && translation_differs) + if ((flags & OPTION_ARG_OPTIONAL) + && strcmp (canonical_translation, opt->name) != 0) __argp_fmtstream_printf (stream, " [--%s[=%s] (--%s)]", - translated_option_name, arg, opt->name); + canonical_translation, arg, opt->name); else if (flags & OPTION_ARG_OPTIONAL) __argp_fmtstream_printf (stream, " [--%s[=%s]]", opt->name, arg); - else if (translation_differs) + else if (strcmp (canonical_translation, opt->name) != 0) __argp_fmtstream_printf (stream, " [--%s=%s (--%s)]", - translated_option_name, arg, opt->name); + canonical_translation, arg, opt->name); else __argp_fmtstream_printf (stream, " [--%s=%s]", opt->name, arg); } - else if (translation_differs) + else if (strcmp (canonical_translation, opt->name) != 0) __argp_fmtstream_printf (stream, " [--%s (--%s)]", - translated_option_name, opt->name); + canonical_translation, opt->name); else __argp_fmtstream_printf (stream, " [--%s]", opt->name); - free (name_allocated); + free (canonical_translation_buffer); } return 0; diff --git a/argp/tst-argphelp-localized.c b/argp/tst-argphelp-localized.c index 0596d6c2b4..337e8b7f08 100644 --- a/argp/tst-argphelp-localized.c +++ b/argp/tst-argphelp-localized.c @@ -42,6 +42,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) @@ -51,6 +53,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; } @@ -62,6 +72,16 @@ do_test (void) char *test1_argv[3] = { (char *) "/bin/tst-argphelp-localized", (char *) "--colour", NULL }; char *test2_argv[3] = + { (char *) "/bin/tst-argphelp-localized", (char *) "--color", NULL }; + char *test3_argv[3] = + { (char *) "/bin/tst-argphelp-localized", (char *) "--coolur", 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"); @@ -70,17 +90,46 @@ 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"); 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 4e301bf278..e87330f7b5 100644 --- a/argp/tst-argphelp-localized.po +++ b/argp/tst-argphelp-localized.po @@ -15,4 +15,9 @@ msgstr "" #: tst-argphelp-localized.c:73 msgctxt "command-line option" msgid "color" -msgstr "colour" \ No newline at end of file +msgstr "colour coolur" + +#: tst-argphelp-localized.c:74 +msgctxt "command-line option" +msgid "flavor" +msgstr "flavour" \ No newline at end of file diff --git a/argp/tst-argpusage-localized.c b/argp/tst-argpusage-localized.c index d8c9304272..0a852be4de 100644 --- a/argp/tst-argpusage-localized.c +++ b/argp/tst-argpusage-localized.c @@ -61,7 +61,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"); /* This is the last chance to fail. */ if (support_record_failure_is_failed ()) FAIL_EXIT1 ( diff --git a/manual/getopt.texi b/manual/getopt.texi index bd58e1b7d8..9f4238beb7 100644 --- a/manual/getopt.texi +++ b/manual/getopt.texi @@ -255,6 +255,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/getopt.c b/posix/getopt.c index aec2bbd48a..4cc4d78c1b 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -191,6 +191,7 @@ exchange (char **argv, struct _getopt_data *d) static const bool match_translated_option_name (char *(*translate) (const char *, const char *, const char *, char **), + const char *program_name, const char *prefix, const char *argument, size_t argument_length, const char *translation_context, const char *opt_textdomain, @@ -199,6 +200,17 @@ match_translated_option_name (char *(*translate) (const char *, const char *, const char *translated = opt_name; char *translation_buffer = NULL; bool matches = false; + /* Multiple alternative names can be provided by the translator, so + that continuous improvement of translations is possible. To + allow multiple translations, separate the translation with a + space character. */ + bool canonical = true; + const char *names_list = NULL; + const char *next_item; + size_t canonical_length; + char *canonical_name; + char *matched_name; + size_t item_length; if (translate != NULL && !__libc_enable_secure) translated = translate (opt_textdomain, translation_context, opt_name, &translation_buffer); @@ -208,6 +220,45 @@ match_translated_option_name (char *(*translate) (const char *, const char *, else /* We know that argument is a prefix of translated. */ matches = translated[argument_length] == '\0'; + if (!matches && strchr (translated, ' ')) + /* The translation is a space-separated list of translations. */ + names_list = translated; + while (!matches && names_list != NULL) + { + item_length = strlen (names_list); + next_item = strchr (names_list, ' '); + if (next_item) + { + item_length = next_item - names_list; + next_item++; + } + if (item_length == argument_length + && strncmp (names_list, argument, argument_length) == 0) + { + if (!canonical) + { + canonical_length = strchr (translated, ' ') - translated; + canonical_name = malloc (canonical_length + 1); + matched_name = malloc (item_length + 1); + if (canonical_name != NULL && matched_name != NULL) + { + memcpy (canonical_name, translated, canonical_length); + canonical_name[canonical_length] = '\0'; + memcpy (matched_name, names_list, item_length); + matched_name[item_length] = '\0'; + fprintf (stderr, _("%s: option '%s%s' is deprecated, use '%s%s' instead\n"), + program_name, + prefix, matched_name, + prefix, canonical_name); + } + free (canonical_name); + free (matched_name); + } + matches = true; + } + canonical = false; + names_list = next_item; + } free (translation_buffer); return matches; } @@ -258,7 +309,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, + if (match_translated_option_name (translate, + argv[0], prefix, + d->__nextchar, namelen, d->optctxt, d->opttextdomain, p->name)) { diff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c index 08609d783b..5796c5ed2c 100644 --- a/posix/tstgetoptl.c +++ b/posix/tstgetoptl.c @@ -31,8 +31,12 @@ 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 +/* This uses the en_GB locale so that colour means color. Oh no! The + translator made a mistake and translated with “coolur”. A bug-fix + has been released, but it has been decided to support both “colour” + and “coolur” with a deprecation warning. + + 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 @@ -51,7 +55,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"); @@ -64,7 +68,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; @@ -82,7 +86,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 } @@ -142,7 +147,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 7dc15e71f3..0363aa01b0 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"