From patchwork Sun Aug 10 07:46:29 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Vivien Kraus X-Patchwork-Id: 118141 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 66FF93858CDB for ; Sun, 10 Aug 2025 07:58:13 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 66FF93858CDB 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=YK6MvqAr 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 C12333858401 for ; Sun, 10 Aug 2025 07:47:12 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org C12333858401 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 C12333858401 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=1754812033; cv=none; b=GjL7ArI26Bga7WDhwVVX5smUl2FzmHYVIYifHjVTd/1vT31QaEaw8EcsiB2AD4s4t4M0JNC/G/WDrrcxdy0TESYKOPrq+5MnoS7q9phk7wuOBq5iKeujTEH/lq/D3pSVve3TwPpC51U1UvljUUwo+JmwGfmqFZ3H66yMv5LQEQw= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1754812033; c=relaxed/simple; bh=Bco2F2x4srUIhNuXorOwx8nFu22OKxTwlY8yGmb7lzA=; h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version; b=hclixwO6mj2tW71/yUdiVV1inC6wrJQIC5yPT/fv7ofxuv+7X0cWMCRVTcUAbd1d6d1Lek3DxdMiAM8LkMB+faWbY9Ac04TifrpAkpPySnhS4CVZyGMIQxrjt16N1nq1gedwxTtusEOjfJ7jODMCgytc32PeMsx6jRJh8qocMuU= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org C12333858401 Received: from planete-kraus.eu (localhost [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id ff3ff8d2 for ; Sun, 10 Aug 2025 07:47:01 +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=7gjLIbUhrgQWoHW2TGvKbdWHKZ0=; b=YK6MvqArka+E23YP8Z AzwyRV8OMDMpo5h4dP4HhHH+zUFDaKnH5GNDYijxnp21ryQn/jK0ykoYhf4cEwUz /P2sAUOHSiC7mKuDruziPL5zjZO3zLoKOCDpOXdprrlUwpCBb8uR8lLw+AzekQ5Y El/wIQ5UN5I+GpvMDfsgfZlXOHX94wyCibcxuSU8qO2NYr+UfAyaIUYCPBrTHKt/ YbGkl14zsQz+GCKZzSMuY5RX3mpmJOmc0gln0ZIQT/Or7l65VEAlZQjjHhjIvehJ qkPlHc/pVOYRXRq8bKvFIrY+dMMJDKemQrwWk59xifAkvlI7p514+3eKxsEcX0HW LQhw== Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id db58e2c1 (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO); Sun, 10 Aug 2025 07:46:59 +0000 (UTC) From: Vivien Kraus To: libc-alpha@sourceware.org Cc: Vivien Kraus Subject: [PATCH v10 6/8] posix, argp: Support deprecation of long option name translations Date: Sun, 10 Aug 2025 09:46:29 +0200 Message-Id: <20250810074631.1707448-7-vivien@planete-kraus.eu> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20250810074631.1707448-1-vivien@planete-kraus.eu> References: <68a758ae45c064bad35bfec73c3d5ffd050398e3.1748369494.git.vivien@planete-kraus.eu> <20250810074631.1707448-1-vivien@planete-kraus.eu> MIME-Version: 1.0 X-Spam-Status: No, score=-12.6 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, 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 server2.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 | 101 +++++++++++++++++++-------------- argp/tst-argphelp-localized.c | 69 +++++++++++++++++++++- argp/tst-argphelp-localized.po | 7 ++- argp/tst-argpusage-localized.c | 2 +- manual/getopt.texi | 5 ++ posix/Makefile | 2 +- posix/getopt.c | 62 +++++++++++++++++++- posix/tstgetoptl.c | 19 +++++-- posix/tstgetoptl.po | 4 +- 9 files changed, 214 insertions(+), 57 deletions(-) diff --git a/argp/argp-help.c b/argp/argp-help.c index 672e733fc8..08650ea5b1 100644 --- a/argp/argp-help.c +++ b/argp/argp-help.c @@ -1205,6 +1205,45 @@ comma (unsigned col, struct pentry_state *pest) indent_to (pest->stream, col); } +/* 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 * +canonical_option_translation (const char *option_name, + char **allocated) +{ + char *option_msgid; + const char *all_names = option_name; + size_t canonical_length; + static const char *default_context = "command-line option\004"; + *allocated = NULL; + option_msgid = malloc (strlen (default_context) + strlen (option_name) + 1); + all_names = NULL; + if (option_msgid) + { + strcpy (option_msgid, default_context); + strcat (option_msgid, option_name); + all_names = gettext (option_msgid); + if (strcmp (all_names, option_msgid) == 0) + all_names = option_name; + } + if (strcmp (all_names, option_name) == 0) + all_names = option_name; + free (option_msgid); + if (strchr (all_names, ' ')) + { + canonical_length = strchr (all_names, ' ') - all_names; + *allocated = malloc (canonical_length + 1); + if (*allocated != NULL) + { + memcpy (*allocated, all_names, canonical_length); + (*allocated)[canonical_length] = '\0'; + all_names = *allocated; + } + } + return all_names; +} + /* Print help for ENTRY to STREAM. */ static void hol_entry_help (struct hol_entry *entry, const struct argp_state *state, @@ -1213,8 +1252,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; - char *option_msgid; - 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); @@ -1278,25 +1317,15 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state, if (opt->name && ovisible (opt)) { comma (uparams.long_opt_col, &pest); - option_msgid = - malloc (strlen ("command-line option\004") - + strlen (opt->name) + 1); - if (option_msgid) - { - strcpy (option_msgid, "command-line option\004"); - strcat (option_msgid, opt->name); - translated_option_name = gettext (option_msgid); - if (!strcmp (translated_option_name, option_msgid)) - translated_option_name = opt->name; - } - else - translated_option_name = opt->name; - __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 (option_msgid); + free (canonical_translation_buffer); } } @@ -1438,8 +1467,8 @@ usage_long_opt (const struct argp_option *opt, { argp_fmtstream_t stream = cookie; const char *arg = opt->arg; - char *option_msgid; - const char *translated_opt_name = opt->name; + const char *canonical_translation = opt->name; + char *canonical_translation_buffer; int flags = opt->flags | real->flags; if (! arg) @@ -1447,41 +1476,29 @@ usage_long_opt (const struct argp_option *opt, if (! (flags & OPTION_NO_USAGE)) { - /* Since we cannot customize the translation context, we will - use a default one. FIXME: use pgettext_expr(). */ - static const char *default_context = "command-line option\004"; - option_msgid = malloc (strlen (default_context) + strlen (opt->name) + 1); - translated_opt_name = NULL; - if (option_msgid) - { - strcpy (option_msgid, default_context); - strcat (option_msgid, opt->name); - translated_opt_name = gettext (option_msgid); - if (!strcmp (translated_opt_name, option_msgid)) - translated_opt_name = opt->name; - } - if (!strcmp (translated_opt_name, opt->name)) - translated_opt_name = NULL; + canonical_translation = + canonical_option_translation (opt->name, &canonical_translation_buffer); if (arg) { arg = dgettext (domain, arg); - if ((flags & OPTION_ARG_OPTIONAL) && translated_opt_name) + if ((flags & OPTION_ARG_OPTIONAL) + && strcmp (canonical_translation, opt->name) != 0) __argp_fmtstream_printf (stream, " [--%s[=%s] (--%s)]", - translated_opt_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 (translated_opt_name) + else if (strcmp (canonical_translation, opt->name) != 0) __argp_fmtstream_printf (stream, " [--%s=%s (--%s)]", - translated_opt_name, arg, opt->name); + canonical_translation, arg, opt->name); else __argp_fmtstream_printf (stream, " [--%s=%s]", opt->name, arg); } - else if (translated_opt_name) + else if (strcmp (canonical_translation, opt->name) != 0) __argp_fmtstream_printf (stream, " [--%s (--%s)]", - translated_opt_name, opt->name); + canonical_translation, opt->name); else __argp_fmtstream_printf (stream, " [--%s]", opt->name); - free (option_msgid); + free (canonical_translation_buffer); } return 0; diff --git a/argp/tst-argphelp-localized.c b/argp/tst-argphelp-localized.c index abc3325827..214f0d992f 100644 --- a/argp/tst-argphelp-localized.c +++ b/argp/tst-argphelp-localized.c @@ -37,10 +37,14 @@ const char *argp_program_version = "argphelp-test 1.0"; struct argp_option options[] = { {PN_ ("command-line option", "color"), 'c', 0, 0, "Rainbow!"}, + {PN_ ("command-line option", "flavor"), 'f', 0, 0, "Sweet!"}, + {PN_ ("command-line option", "texture"), 't', 0, 0, "Smooth!"}, {0} }; 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 +59,24 @@ parse_opt (int key, char *arg, struct argp_state *state) { color_set = 1; } + if (key == 'f' && flavor_set) + { + fprintf (stderr, "%s:%d: flavor already set.\n", __FILE__, __LINE__); + abort (); + } + else if (key == 'f') + { + flavor_set = 1; + } + else if (key == 't' && texture_set) + { + fprintf (stderr, "%s:%d: texture already set.\n", __FILE__, __LINE__); + abort (); + } + else if (key == 't') + { + texture_set = 1; + } return 0; } @@ -66,6 +88,16 @@ main (int argc, char *argv[]) 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"); @@ -83,7 +115,7 @@ main (int argc, char *argv[]) abort (); } /* Check that the catalog is OK: */ - if (strcmp (gettext ("command-line option\004color"), "colour") != 0) + if (strcmp (gettext ("command-line option\004color"), "colour coolur") != 0) { fprintf (stderr, "%s:%d: the mo file does not work.\n", __FILE__, __LINE__); @@ -95,7 +127,42 @@ main (int argc, char *argv[]) fprintf (stderr, "%s:%d: color not set.\n", __FILE__, __LINE__); abort (); } + color_set = 0; argp_parse (&argp, 2, test2_argv, 0, 0, NULL); + if (!color_set) + { + fprintf (stderr, "%s:%d: color not set.\n", __FILE__, __LINE__); + abort (); + } + color_set = 0; + argp_parse (&argp, 2, test3_argv, 0, 0, NULL); + if (!color_set) + { + fprintf (stderr, "%s:%d: color not set.\n", __FILE__, __LINE__); + abort (); + } + flavor_set = 0; + argp_parse (&argp, 2, test4_argv, 0, 0, NULL); + if (!flavor_set) + { + fprintf (stderr, "%s:%d: flavor not set.\n", __FILE__, __LINE__); + abort (); + } + flavor_set = 0; + argp_parse (&argp, 2, test5_argv, 0, 0, NULL); + if (!flavor_set) + { + fprintf (stderr, "%s:%d: flavor not set.\n", __FILE__, __LINE__); + abort (); + } + texture_set = 0; + argp_parse (&argp, 2, test6_argv, 0, 0, NULL); + if (!flavor_set) + { + fprintf (stderr, "%s:%d: texture not set.\n", __FILE__, __LINE__); + abort (); + } + argp_parse (&argp, 2, test7_argv, 0, 0, NULL); fprintf (stderr, "%s:%d: --help did not exit the program.\n", __FILE__, __LINE__); abort (); 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 4061609fc3..bb8865bc99 100644 --- a/argp/tst-argpusage-localized.c +++ b/argp/tst-argpusage-localized.c @@ -69,7 +69,7 @@ main (int argc, char *argv[]) abort (); } /* Check that the catalog is OK: */ - if (strcmp (gettext ("command-line option\004color"), "colour") != 0) + if (strcmp (gettext ("command-line option\004color"), "colour coolur") != 0) { fprintf (stderr, "%s:%d: the mo file does not work.\n", __FILE__, __LINE__); diff --git a/manual/getopt.texi b/manual/getopt.texi index 20be0fba69..7834f1c2a3 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/Makefile b/posix/Makefile index 1c787f53fb..1a2cdb053f 100644 --- a/posix/Makefile +++ b/posix/Makefile @@ -614,7 +614,7 @@ CFLAGS-fork.c = $(libio-mtsafe) $(config-cflags-wno-ignored-attributes) tstgetopt-ARGS = -a -b -cfoobar --required foobar --optional=bazbug \ --none random --col --color --colour -tstgetoptl-ARGS = $(tstgetopt-ARGS) +tstgetoptl-ARGS = $(tstgetopt-ARGS) --coolur tst-exec-ARGS = -- $(host-test-program-cmd) tst-exec-static-ARGS = $(tst-exec-ARGS) diff --git a/posix/getopt.c b/posix/getopt.c index 6b235fa4e3..55afde8b4c 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -191,16 +191,70 @@ exchange (char **argv, struct _getopt_data *d) static const int match_translated_option_name (char *(*translate) (const char *, const char *, const char *), + const char *program_name, const char *prefix, const char *argument, size_t argument_length, const char *translation_context, const char *opt_textdomain, const char *opt_name) { const char *translated = opt_name; + /* 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; + 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); - return (!strncmp (translated, argument, argument_length) - && argument_length == strlen (translated)); + if (strlen (translated) == argument_length + && strncmp (translated, argument, argument_length) == 0 + && !strchr (translated, ' ')) + return 1; + else if (!strchr (translated, ' ')) + return 0; + names_list = translated; + while (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); + } + return 1; + } + canonical = 0; + names_list = next_item; + } + return 0; } /* Process the argument starting with d->__nextchar as a long option. @@ -248,7 +302,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 3580358a67..75abdac8ef 100644 --- a/posix/tstgetoptl.c +++ b/posix/tstgetoptl.c @@ -10,8 +10,12 @@ different options with the same short name, it has only one, and the other one is a translation. */ -/* 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 translated "optional" as "required". */ @@ -28,7 +32,9 @@ prepare_localedir (void) return -1; } /* Check that the catalog is OK: */ - if (strcmp (dgettext ("tstgetoptl", TRANSLATION_CONTEXT "\004" "color"), "colour") != 0) + if (strcmp (dgettext ("tstgetoptl", + TRANSLATION_CONTEXT "\004" "color"), + "colour coolur") != 0) { fputs ("The mo file does not work.\n", stderr); return -1; @@ -47,11 +53,12 @@ main (int argc, char **argv) {"optional", optional_argument, NULL, 'o'}, {"none", no_argument, NULL, 'n'}, {"color", no_argument, NULL, 'C'}, - /* Now colour is handled as a translation of color */ + /* Now colour (and coolur) is handled as a translation of color */ {NULL, 0, NULL, 0 } }; - /* The rest of the function is the same as in tstgetopt.c. */ + /* The rest of the function is the same as in tstgetopt.c, except we + expect 4 instances of -C instead of just 3. */ int aflag = 0; int bflag = 0; @@ -109,7 +116,7 @@ main (int argc, char **argv) aflag, bflag, cvalue, Cflag, nflag); result |= (aflag != 1 || bflag != 1 || cvalue == NULL - || strcmp (cvalue, "foobar") != 0 || Cflag != 3 || nflag != 1); + || strcmp (cvalue, "foobar") != 0 || Cflag != 4 || nflag != 1); for (index = optind; index < argc; index++) printf ("Non-option argument %s\n", argv[index]); diff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po index e060c0d6e3..f418776a6b 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" # This is to make sure the translator cannot redirect options. #: xxx.c:yy