It is possible to support translated long option names in a program by adding new option records with the translated names in the options array. However, it is significant work for all packages. More importantly, some higher-level systems such as autoopts or guileās (ice-9 getopt-long) support required options, i.e. option that must be set in the command-line. Developers who want to have translated option names cannot use these systems.
The discussion was started on bug-standards and support in glibc may be desirable [1]. With this change, getopt will try and match both the name of each option and its translation. Abbreviations will only match the untranslated names. This change would be better if it could match translated option names by calling pgettext instead of gettext, but as far as I understand, pgettext is not available in libintl. A potential drawback is that translator should be careful not to use a perfect homonym for another long option name. [1]: https://lists.gnu.org/archive/html/bug-standards/2025-05/msg00000.html Sign-off-by: Vivien Kraus <[email protected]> --- Dear Glibc developers, This patch tries to implement support for translated long option names in getopt. I hope it is not too controversial, though I expect the work to achieve this will involve more discussion than code. I started by first contacting the GNU Coding Standards for advice, and it seems that recognizing both translated option names and non-translated option names could keep a stable interface for scripts calling programs, as well as providing better user interface. This is my first contribution to glibc. Best regards, Vivien manual/getopt.texi | 16 ++-- posix/Makefile | 2 + posix/getopt.c | 52 ++++++++--- posix/tstgetoptl.c | 211 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 16 deletions(-) create mode 100644 posix/tstgetoptl.c diff --git a/manual/getopt.texi b/manual/getopt.texi index 79a942307c..d70959e1fe 100644 --- a/manual/getopt.texi +++ b/manual/getopt.texi @@ -213,7 +213,9 @@ The @code{struct option} structure has these fields: @table @code @item const char *name -This field is the name of the option. It is a string. +This field is the name of the option. It is a string. In order for +@command{getopt_long} to accept either the long option name or its +translated form, you should mark this string for translation. @item int has_arg This field says whether the option takes an argument. It is an integer, @@ -248,10 +250,14 @@ When @code{getopt_long} encounters a short option, it does the same thing that @code{getopt} would do: it returns the character code for the option, and stores the option's argument (if it has one) in @code{optarg}. -When @code{getopt_long} encounters a long option, it takes actions based -on the @code{flag} and @code{val} fields of the definition of that -option. The option name may be abbreviated as long as the abbreviation is -unique. +When @code{getopt_long} encounters a long option or its translation in +the current textdomain, it takes actions based on the @code{flag} and +@code{val} fields of the definition of that option. The translation +for the long option name is retrieved via @code{gettext}. Due to the +lack of support for contexts in libintl, it is unfortunately not +possible to use @code{pgettext}. The english name of the option name +may be abbreviated as long as the abbreviation is unique. No +abbreviation of the translated option name is recognized. If @code{flag} is a null pointer, then @code{getopt_long} returns the contents of @code{val} to indicate which option it found. You should diff --git a/posix/Makefile b/posix/Makefile index c0e224236a..d1f99dc2d2 100644 --- a/posix/Makefile +++ b/posix/Makefile @@ -327,6 +327,7 @@ tests := \ tst-waitid \ tst-wordexp-nocmd \ tstgetopt \ + tstgetoptl \ # tests # Test for the glob symbol version that was replaced in glibc 2.27. @@ -599,6 +600,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) 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 66b43850ee..ef82b49723 100644 --- a/posix/getopt.c +++ b/posix/getopt.c @@ -208,10 +208,13 @@ process_long_option (int argc, char **argv, const char *optstring, namelen = nameend - d->__nextchar; /* First look for an exact match, counting the options as a side - effect. */ + effect. Translated long option names can be matched. */ for (p = longopts, n_options = 0; p->name; p++, n_options++) - if (!strncmp (p->name, d->__nextchar, namelen) - && namelen == strlen (p->name)) + if ((!strncmp (p->name, d->__nextchar, namelen) + && namelen == strlen (p->name)) + /* FIXME: use pgettext instead of gettext. */ + || (!strncmp (gettext (p->name), d->__nextchar, namelen) + && namelen == strlen (gettext (p->name)))) { /* Exact match found. */ pfound = p; @@ -221,7 +224,8 @@ process_long_option (int argc, char **argv, const char *optstring, if (pfound == NULL) { - /* Didn't find an exact match, so look for abbreviations. */ + /* Didn't find an exact match, so look for abbreviations, but + only for the option name in the C locale. */ unsigned char *ambig_set = NULL; int ambig_malloced = 0; int ambig_fallback = 0; @@ -341,10 +345,23 @@ process_long_option (int argc, char **argv, const char *optstring, else { if (print_errors) - fprintf (stderr, - _("%s: option '%s%s' doesn't allow an argument\n"), - argv[0], prefix, pfound->name); - + { + if (strcmp (gettext (pfound->name), pfound->name) != 0) + { + /* Print both names of the option. */ + fprintf (stderr, + _("%s: option '%s%s' / '%s%s' doesn't allow an argument\n"), + argv[0], prefix, gettext (pfound->name), prefix, pfound->name); + } + else + { + /* Either the option name is not translated, or its + translation is the same as the option name. */ + fprintf (stderr, + _("%s: option '%s%s' doesn't allow an argument\n"), + argv[0], prefix, pfound->name); + } + } d->optopt = pfound->val; return '?'; } @@ -356,9 +373,22 @@ process_long_option (int argc, char **argv, const char *optstring, else { if (print_errors) - fprintf (stderr, - _("%s: option '%s%s' requires an argument\n"), - argv[0], prefix, pfound->name); + { + /* Same dichotomy as when the option does not allow an + argument. */ + if (strcmp (gettext (pfound->name), pfound->name) != 0) + { + fprintf (stderr, + _("%s: option '%s%s' / '%s%s' requires an argument\n"), + argv[0], prefix, gettext (pfound->name), prefix, pfound->name); + } + else + { + fprintf (stderr, + _("%s: option '%s%s' requires an argument\n"), + argv[0], prefix, pfound->name); + } + } d->optopt = pfound->val; return optstring[0] == ':' ? ':' : '?'; diff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c new file mode 100644 index 0000000000..f37dea11cd --- /dev/null +++ b/posix/tstgetoptl.c @@ -0,0 +1,211 @@ +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/stat.h> +#include <libintl.h> +#include <locale.h> + +/* This is a modified copy of tstgetopt.c, but instead of having two + different options with the same short name, it has only one, and + the other one is a translation. */ + +/* FIXME: it would be better to use pgettext, but libintl does not + have it. */ + +/* This uses the en_GB locale so that colour means color. The PO file + is: + +# English translations for tstgetoptl. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the glibc package. +# Vivien Kraus <[email protected]>, 2025. +# +msgid "" +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" +"Last-Translator: Vivien Kraus <[email protected]>\n" +"Language-Team: English (British) <(nothing)>\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: xxx.c:yy +msgid "color" +msgstr "colour" +*/ + +// I got the MO file content by doing: +// hexdump -C en_GB.mo | cut -b 10-58 | sed 's/ */\\x/g' + +static const char *mo_file_content = "" + "\xde\x12\x04\x95\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00" + "\x2c\x00\x00\x00\x05\x00\x00\x00\x3c\x00\x00\x00\x00\x00\x00\x00" + "\x50\x00\x00\x00\x05\x00\x00\x00\x51\x00\x00\x00\x5e\x01\x00\x00" + "\x57\x00\x00\x00\x06\x00\x00\x00\xb6\x01\x00\x00\x01\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00" + "\x00\x63\x6f\x6c\x6f\x72\x00\x50\x72\x6f\x6a\x65\x63\x74\x2d\x49" + "\x64\x2d\x56\x65\x72\x73\x69\x6f\x6e\x3a\x20\x74\x73\x74\x67\x65" + "\x74\x6f\x70\x74\x6c\x20\x30\x2e\x30\x2e\x30\x0a\x52\x65\x70\x6f" + "\x72\x74\x2d\x4d\x73\x67\x69\x64\x2d\x42\x75\x67\x73\x2d\x54\x6f" + "\x3a\x20\x0a\x50\x4f\x2d\x52\x65\x76\x69\x73\x69\x6f\x6e\x2d\x44" + "\x61\x74\x65\x3a\x20\x32\x30\x32\x35\x2d\x30\x35\x2d\x32\x37\x20" + "\x31\x39\x3a\x33\x30\x2b\x30\x32\x30\x30\x0a\x4c\x61\x73\x74\x2d" + "\x54\x72\x61\x6e\x73\x6c\x61\x74\x6f\x72\x3a\x20\x56\x69\x76\x69" + "\x65\x6e\x20\x4b\x72\x61\x75\x73\x20\x3c\x76\x69\x76\x69\x65\x6e" + "\x40\x70\x6c\x61\x6e\x65\x74\x65\x2d\x6b\x72\x61\x75\x73\x2e\x65" + "\x75\x3e\x0a\x4c\x61\x6e\x67\x75\x61\x67\x65\x2d\x54\x65\x61\x6d" + "\x3a\x20\x45\x6e\x67\x6c\x69\x73\x68\x20\x28\x42\x72\x69\x74\x69" + "\x73\x68\x29\x20\x3c\x28\x6e\x6f\x74\x68\x69\x6e\x67\x29\x3e\x0a" + "\x4c\x61\x6e\x67\x75\x61\x67\x65\x3a\x20\x65\x6e\x5f\x47\x42\x0a" + "\x4d\x49\x4d\x45\x2d\x56\x65\x72\x73\x69\x6f\x6e\x3a\x20\x31\x2e" + "\x30\x0a\x43\x6f\x6e\x74\x65\x6e\x74\x2d\x54\x79\x70\x65\x3a\x20" + "\x74\x65\x78\x74\x2f\x70\x6c\x61\x69\x6e\x3b\x20\x63\x68\x61\x72" + "\x73\x65\x74\x3d\x41\x53\x43\x49\x49\x0a\x43\x6f\x6e\x74\x65\x6e" + "\x74\x2d\x54\x72\x61\x6e\x73\x66\x65\x72\x2d\x45\x6e\x63\x6f\x64" + "\x69\x6e\x67\x3a\x20\x38\x62\x69\x74\x0a\x50\x6c\x75\x72\x61\x6c" + "\x2d\x46\x6f\x72\x6d\x73\x3a\x20\x6e\x70\x6c\x75\x72\x61\x6c\x73" + "\x3d\x32\x3b\x20\x70\x6c\x75\x72\x61\x6c\x3d\x28\x6e\x20\x21\x3d" + "\x20\x31\x29\x3b\x0a\x00\x63\x6f\x6c\x6f\x75\x72\x00"; + +static const size_t mo_file_size = 445; + +/* FIXME: create the tstgetoptl-localedir in the build directory. */ + +static void +cleanup_localedir (void) +{ + remove ("tstgetoptl-localedir/en_GB/LC_MESSAGES/tstgetoptl.mo"); + remove ("tstgetoptl-localedir/en_GB/LC_MESSAGES"); + remove ("tstgetoptl-localedir/en_GB"); + remove ("tstgetoptl-localedir"); +} + +static int +prepare_localedir (void) +{ + cleanup_localedir (); + if (mkdir ("tstgetoptl-localedir", S_IRWXU) != 0 + || mkdir ("tstgetoptl-localedir/en_GB", S_IRWXU) != 0 + || mkdir ("tstgetoptl-localedir/en_GB/LC_MESSAGES", S_IRWXU) != 0) + { + fputs ("Cannot initialize the localedir.\n", stderr); + return -1; + } + FILE *mo_file = fopen ("tstgetoptl-localedir/en_GB/LC_MESSAGES/tstgetoptl.mo", "wb"); + if (mo_file == NULL) + { + fputs ("Cannot create the mo file.\n", stderr); + return -1; + } + if (fwrite (mo_file_content, 1, mo_file_size, mo_file) != mo_file_size) + { + fputs ("Cannot write the mo file.\n", stderr); + fclose (mo_file); + return -1; + } + fclose (mo_file); + unsetenv ("LANGUAGE"); + setlocale (LC_ALL, "en_GB.UTF-8"); + if (bindtextdomain ("tstgetoptl", "tstgetoptl-localedir") == NULL) + { + fputs ("Cannot call bindtextdomain.\n", stderr); + return -1; + } + if (textdomain ("tstgetoptl") == NULL) + { + fputs ("Cannot call textdomain.\n", stderr); + return -1; + } + /* Check that the catalog is OK: */ + if (strcmp (gettext ("color"), "colour") != 0) + { + fputs ("The mo file does not work.\n", stderr); + return -1; + } + return 0; +} + +int +main (int argc, char **argv) +{ + static const struct option options[] = + { + {"required", required_argument, NULL, 'r'}, + {"optional", optional_argument, NULL, 'o'}, + {"none", no_argument, NULL, 'n'}, + {"color", no_argument, NULL, 'C'}, + /* Now colour is handled as a translation of color */ + {NULL, 0, NULL, 0 } + }; + + int aflag = 0; + int bflag = 0; + char *cvalue = NULL; + int Cflag = 0; + int nflag = 0; + int index; + int c; + int result = 0; + + if (prepare_localedir () != 0) + { + fputs ("Error while setting up localedir.\n", stderr); + return 1; + } + while ((c = getopt_long (argc, argv, "abc:", options, NULL)) >= 0) + switch (c) + { + case 'a': + aflag = 1; + break; + case 'b': + bflag = 1; + break; + case 'c': + cvalue = optarg; + break; + case 'C': + ++Cflag; + break; + case '?': + fputs ("Unknown option.\n", stderr); + return 1; + default: + fprintf (stderr, "This should never happen!\n"); + return 1; + + case 'r': + printf ("--required %s\n", optarg); + result |= strcmp (optarg, "foobar") != 0; + break; + case 'o': + printf ("--optional %s\n", optarg); + result |= optarg == NULL || strcmp (optarg, "bazbug") != 0; + break; + case 'n': + puts ("--none"); + nflag = 1; + break; + } + + printf ("aflag = %d, bflag = %d, cvalue = %s, Cflags = %d, nflag = %d\n", + aflag, bflag, cvalue, Cflag, nflag); + + result |= (aflag != 1 || bflag != 1 || cvalue == NULL + || strcmp (cvalue, "foobar") != 0 || Cflag != 3 || nflag != 1); + + for (index = optind; index < argc; index++) + printf ("Non-option argument %s\n", argv[index]); + + result |= optind + 1 != argc || strcmp (argv[optind], "random") != 0; + + cleanup_localedir (); + return result; +} base-commit: 08d7243a6179d5a1f3f65a53aba1ec0803895aeb -- 2.49.0
