From patchwork Sat Dec 6 13:19:13 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 126035 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id D62484BCA1A6 for ; Sat, 6 Dec 2025 13:25:19 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D62484BCA1A6 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=m0teb/8N 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 ABA9A48EFFB9 for ; Sat, 6 Dec 2025 13:22:43 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org ABA9A48EFFB9 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 ABA9A48EFFB9 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=1765027364; cv=none; b=tCZXFzpfyPuTQ15Z3R1rqqhI26OtXNDQksNc6kXdhiB1UVFl0G+vX4LOowDeu0HpaPKH30OruFXHqeP+mVYPcP/UX1opyAykvvVuwzSc25RQLyvmoyLzLhXlfXIYT4z6cRN12H18IGPLmfOID8E6lr839IwgR+KQzgrbIPjqx8w= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1765027364; c=relaxed/simple; bh=8rM+h/rHa/tVuBmhaqWVnQDTKzCNibwavIWxySF84Ek=; h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version; b=S0thsHjhdsfwyMfxo0OPkskePX50xZnbIFhHpyA/YKDG0/oLs2Fjim/tOknIItCqXAqS4H9PAVLTFV/dcQU+kd8aw5Mj2lhHg3IhuFaCq9ngBGmd6DWLZYsfNKXtCmlwNLnDVPeyyQQ5v8Oz7pDJ3JR8zncLxbeTwiHTHwVYB20= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org ABA9A48EFFB9 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 911903ce; Sat, 6 Dec 2025 13:22: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=46LUOD+YWlrgU7jvBddjlXO7D8U=; b=m0teb/8NJdLIf5A396 U5PAcooyvGeOZKLvRv+X3igEs/8jRUhJ2/JSE4rGWCT1uZ+ok2c2X+lDX9F8fTWg cHV/ZrmUy3RipMbtZdTkR/VR90Hx2tFJXAITcdME2zONyzaN0fO/KJpPoXzuFkV6 FUpZdtLcQ/fn11e+DCMsAQ60W6iylo5L6XBSFY+DMkmpJgD7rOiQnqBDWg/uV0Vs HSag/r/K2/xK9O4VKJuUoJ/+6kvgj8ZOqy6IpTYN0E+WiOeGLEnvRHeUqqGNXgwg IBmbOUviJwYJ4FE/3WNSGr1wRyLgw2A5klihpJVYsbpcbqlFw+6ZrC8kODAV+j+z kEKQ== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id e4119e70 (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Sat, 6 Dec 2025 13:22:41 +0000 (UTC) From: Vivien Kraus To: libc-alpha@sourceware.org, adhemerval.zanella@linaro.org Cc: Vivien Kraus Subject: [PATCH v19 06/11] posix, argp: Support deprecation of long option name translations Date: Sat, 6 Dec 2025 14:19:13 +0100 Message-Id: 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.9 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, RCVD_IN_VALIDITY_RPBL_BLOCKED, RCVD_IN_VALIDITY_SAFE_BLOCKED, SPF_HELO_PASS, SPF_PASS, TXREP, URIBL_BLOCKED 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 | 51 ++++++++++++++++++++- argp/tst-argphelp-localized.po | 7 ++- argp/tst-argpusage-localized.c | 2 +- manual/getopt.texi | 5 ++ posix/getopt.c | 55 +++++++++++++++++++++- posix/tstgetoptl.c | 17 ++++--- posix/tstgetoptl.po | 4 +- 8 files changed, 179 insertions(+), 46 deletions(-) diff --git a/argp/argp-help.c b/argp/argp-help.c index 9dc9c9b734..34ecc19b38 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 8f484c49b0..0979bd82e4 100644 --- a/argp/tst-argphelp-localized.c +++ b/argp/tst-argphelp-localized.c @@ -46,6 +46,8 @@ struct argp_option options[] = }; static int color_set = 0; +static int flavor_set = 0; +static int texture_set = 0; static error_t parse_opt (int key, char *arg, struct argp_state *state) @@ -55,6 +57,14 @@ parse_opt (int key, char *arg, struct argp_state *state) FAIL("color already set.\n"); else if (key == 'c') color_set = 1; + else if (key == 'f' && flavor_set) + FAIL ("flavor already set.\n"); + else if (key == 'f') + flavor_set = 1; + else if (key == 't' && texture_set) + FAIL ("texture already set.\n"); + else if (key == 't') + texture_set = 1; return 0; } @@ -66,6 +76,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"); @@ -74,17 +94,44 @@ do_test (void) TEST_VERIFY_EXIT (textdomain ("tst-argphelp-localized") != NULL); /* Check that the catalog is OK: */ TEST_COMPARE_STRING (gettext ("command-line option\004color"), - "colour"); + "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 = 0; + argp_parse (&argp, 2, test2_argv, 0, 0, NULL); + TEST_VERIFY (color_set); + TEST_VERIFY (!flavor_set); + TEST_VERIFY (!texture_set); + color_set = 0; + argp_parse (&argp, 2, test3_argv, 0, 0, NULL); + TEST_VERIFY (color_set); + TEST_VERIFY (!flavor_set); + TEST_VERIFY (!texture_set); + color_set = 0; + argp_parse (&argp, 2, test4_argv, 0, 0, NULL); + TEST_VERIFY (!color_set); + TEST_VERIFY (flavor_set); + TEST_VERIFY (!texture_set); + flavor_set = 0; + argp_parse (&argp, 2, test5_argv, 0, 0, NULL); + TEST_VERIFY (!color_set); + TEST_VERIFY (flavor_set); + TEST_VERIFY (!texture_set); + flavor_set = 0; + argp_parse (&argp, 2, test6_argv, 0, 0, NULL); + TEST_VERIFY (!color_set); + TEST_VERIFY (!flavor_set); + TEST_VERIFY (texture_set); + texture_set = 0; /* 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); + 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 5af4609d1a..1ac40f703a 100644 --- a/argp/tst-argpusage-localized.c +++ b/argp/tst-argpusage-localized.c @@ -65,7 +65,7 @@ do_test (void) TEST_VERIFY_EXIT (textdomain ("tst-argphelp-localized") != NULL); /* Check that the catalog is OK: */ TEST_COMPARE_STRING (gettext ("command-line option\004color"), - "colour"); + "colour coolur"); /* 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"); diff --git a/manual/getopt.texi b/manual/getopt.texi index 16fa608418..17d238c255 100644 --- a/manual/getopt.texi +++ b/manual/getopt.texi @@ -228,6 +228,11 @@ 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. +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. + @deftp {Data Type} {struct option} @standards{GNU, getopt.h} This structure describes a single long option name for the sake of diff --git a/posix/getopt.c b/posix/getopt.c index ba2a703174..404cae7d74 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -191,6 +191,7 @@ exchange (char **argv, struct _getopt_data *d) static const int 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,11 +200,61 @@ match_translated_option_name (char *(*translate) (const char *, const char *, const char *translated = opt_name; char *translation_buffer = NULL; int matches = 0; + /* 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. */ + int canonical = 1; + 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); matches = (!strncmp (translated, argument, argument_length) && argument_length == strlen (translated)); + 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 = 1; + } + canonical = 0; + names_list = next_item; + } free (translation_buffer); return matches; } @@ -254,7 +305,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 1b54ffb4fb..e1da72f0b3 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 (int 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 } @@ -141,7 +146,7 @@ do_my_test (int 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 68be89cfdf..712601a306 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"