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