From: Bruno Haible Date: Wed, 25 Jun 2025 02:16:03 +0000 (+0200) Subject: gettext-runtime: New programs 'printf_gettext', 'printf_ngettext'. X-Git-Tag: v0.26~60 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=22546c9c060d567fc25a0cccf19020df4393b3d4;p=thirdparty%2Fgettext.git gettext-runtime: New programs 'printf_gettext', 'printf_ngettext'. * autogen.sh (GNULIB_MODULES_RUNTIME_FOR_SRC): Add c-ctype, c-strtold, fzprintf-posix, mbrtoc32, mbszero, quote, stdint-h, strtoimax, strtold, strtoumax, xstrtold. * gettext-runtime/src/printf-command.h: New file. * gettext-runtime/src/printf-command.c: New file. * gettext-runtime/src/printf_gettext.c: New file. * gettext-runtime/src/Makefile.am (bin_PROGRAMS): Add printf_gettext, printf_ngettext. (noinst_LIBRARIES, libgrtsrc_a_SOURCES): New variables. (printf_gettext_SOURCES, printf_gettext_CFLAGS, printf_gettext_LDFLAGS): New variables. (printf_ngettext_SOURCES, printf_ngettext_CFLAGS, printf_ngettext_LDFLAGS): New variables. (LDADD): Add libgrtsrc.a. * gettext-runtime/po/POTFILES.in: Add src/printf-command.c, src/printf_gettext.c, src/printf_ngettext.c. * gettext-runtime/man/printf_gettext.x: New file. * gettext-runtime/man/printf_ngettext.x: New file. * gettext-runtime/man/Makefile.am (man_aux): Add printf_gettext.x, printf_ngettext.x. (man_MAN1GEN): Add printf_gettext.1, printf_ngettext.1. (man_MAN1IN): Add printf_gettext.1.in, printf_ngettext.1.in. (man_HTML1GEN): Add printf_gettext.1.html, printf_ngettext.1.html. (man_HTML1IN): Add printf_gettext.1.html.in, printf_ngettext.1.html.in. (printf_gettext.1, rintf_ngettext.1, printf_gettext.1.in, printf_ngettext.1.in): Add dependencies. (printf_gettext.1.html, printf_ngettext.1.html, printf_gettext.1.html.in, printf_ngettext.1.html.in): Likewise. * gettext-runtime/Makefile.am (distdir1): Depend on man/printf_gettext.1 and man/printf_ngettext.1. (man/printf_gettext.1, man/printf_ngettext.1): Depend on gen-man1. (gen-man1): Make src/printf_gettext, src/printf_ngettext and printf_gettext.1, printf_ngettext.1. * gettext-runtime/doc/rt-printf_gettext.texi: New file. * gettext-runtime/doc/rt-printf_ngettext.texi: New file. * gettext-runtime/doc/Makefile.am (EXTRA_DIST): Add them. * gettext-tools/doc/lang-sh.texi (printf_gettext Invocation, printf_ngettext Invocation): New subsubsections. * gettext-tools/doc/gettext.texi (@direntry): Add printf_gettext, . * gettext-tools/doc/Makefile.am (gettext_TEXINFOS): Add rt-printf_gettext.texi, rt-printf_ngettext.texi. * gettext-runtime/NEWS: Mention the new programs. * NEWS: Likewise. * PACKAGING: Add the printf_gettext and printf_ngettext programs and their documentation. --- diff --git a/.gitignore b/.gitignore index a520ac084..ca70c6b64 100644 --- a/.gitignore +++ b/.gitignore @@ -473,6 +473,10 @@ /gettext-runtime/man/gettext.1.html.in /gettext-runtime/man/ngettext.1.in /gettext-runtime/man/ngettext.1.html.in +/gettext-runtime/man/printf_gettext.1.in +/gettext-runtime/man/printf_gettext.1.html.in +/gettext-runtime/man/printf_ngettext.1.in +/gettext-runtime/man/printf_ngettext.1.html.in /gettext-runtime/man/bind_textdomain_codeset.3 /gettext-runtime/man/bind_textdomain_codeset.3.html /gettext-runtime/man/bindtextdomain.3 @@ -699,6 +703,10 @@ autom4te.cache/ /gettext-runtime/man/gettext.1.html /gettext-runtime/man/ngettext.1 /gettext-runtime/man/ngettext.1.html +/gettext-runtime/man/printf_gettext.1 +/gettext-runtime/man/printf_gettext.1.html +/gettext-runtime/man/printf_ngettext.1 +/gettext-runtime/man/printf_ngettext.1.html /gettext-tools/libgrep/libgrep.a /gettext-tools/src/**/*.class /gettext-tools/src/gettext.jar @@ -716,6 +724,10 @@ autom4te.cache/ /gettext-runtime/src/gettext.exe /gettext-runtime/src/ngettext /gettext-runtime/src/ngettext.exe +/gettext-runtime/src/printf_gettext +/gettext-runtime/src/printf_gettext.exe +/gettext-runtime/src/printf_ngettext +/gettext-runtime/src/printf_ngettext.exe /gettext-runtime/tests/test-lock /gettext-runtime/tests/test-lock.exe /gettext-tools/libgettextpo/gettext-po.h diff --git a/Admin/release-steps b/Admin/release-steps index 0aebdd258..b95e75947 100644 --- a/Admin/release-steps +++ b/Admin/release-steps @@ -121,6 +121,8 @@ We assume that the following environment variables are set: gettext-runtime/src/gettext.c gettext-runtime/src/gettext.sh.in gettext-runtime/src/ngettext.c + gettext-runtime/src/printf_gettext.c + gettext-runtime/src/printf_ngettext.c gettext-tools/misc/autopoint.in gettext-tools/misc/convert-archive.in gettext-tools/misc/gettextize.in diff --git a/NEWS b/NEWS index 0b7218c6b..2394045e8 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,9 @@ Version 0.26 - July 2025 They are marked as 'sh-printf-format' in POT and PO files. - xgettext now recognizes the \c, \u, and \U escape sequences in dollar- single-quoted strings $'...'. + - Two new programs 'printf_gettext' and 'printf_ngettext' are provided, + that do formatted output with a localized format string in a more + efficient way (without spawning a subshell). # Bug fixes: - The AM_GNU_GETTEXT macro now rejects the dysfunctional gettext() function diff --git a/PACKAGING b/PACKAGING index 1ae8fe827..73fb94dc0 100644 --- a/PACKAGING +++ b/PACKAGING @@ -76,13 +76,19 @@ the following file list. $prefix/bin/gettext $prefix/bin/ngettext + $prefix/bin/printf_gettext + $prefix/bin/printf_ngettext $prefix/bin/envsubst $prefix/bin/gettext.sh $prefix/share/man/man1/gettext.1 $prefix/share/man/man1/ngettext.1 + $prefix/share/man/man1/printf_gettext.1 + $prefix/share/man/man1/printf_ngettext.1 $prefix/share/man/man1/envsubst.1 $prefix/share/doc/gettext/gettext.1.html $prefix/share/doc/gettext/ngettext.1.html + $prefix/share/doc/gettext/printf_gettext.1.html + $prefix/share/doc/gettext/printf_ngettext.1.html $prefix/share/doc/gettext/envsubst.1.html $prefix/share/locale/*/LC_MESSAGES/gettext-runtime.mo diff --git a/autogen.sh b/autogen.sh index cac02dedc..320b626d2 100755 --- a/autogen.sh +++ b/autogen.sh @@ -101,25 +101,36 @@ if ! $skip_gnulib; then basename-lgpl binary-io bool + c-ctype + c-strtold closeout error + fzprintf-posix getopt-gnu gettext-h havelib + mbrtoc32 + mbszero memmove noreturn progname propername + quote relocatable-prog setlocale sigpipe + stdint-h stdio-h stdlib-h + strtoimax + strtold strtoul + strtoumax unistd-h unlocked-io xalloc xstring-buffer + xstrtold ' GNULIB_MODULES_RUNTIME_OTHER=' gettext-runtime-misc diff --git a/gettext-runtime/Makefile.am b/gettext-runtime/Makefile.am index 86c0ea754..189468941 100644 --- a/gettext-runtime/Makefile.am +++ b/gettext-runtime/Makefile.am @@ -52,15 +52,17 @@ EXTRA_DIST += INSTALL.windows # Hidden from automake, but really activated. Works around an automake bug. #distdir: distdir1 .PHONY: distdir1 -distdir1: man/gettext.1 man/ngettext.1 man/envsubst.1 -man/gettext.1 man/ngettext.1 man/envsubst.1: gen-man1 +distdir1: man/gettext.1 man/ngettext.1 man/printf_gettext.1 man/printf_ngettext.1 man/envsubst.1 +man/gettext.1 man/ngettext.1 man/printf_gettext.1 man/printf_ngettext.1 man/envsubst.1: gen-man1 .PHONY: gen-man1 gen-man1: src/gettext.c man/gettext.x \ src/ngettext.c man/ngettext.x \ + src/printf_gettext.c man/printf_gettext.x \ + src/printf_ngettext.c man/printf_ngettext.x \ src/envsubst.c man/envsubst.x cd gnulib-lib && $(MAKE) $(AM_MAKEFLAGS) - cd src && $(MAKE) $(AM_MAKEFLAGS) gettext$(EXEEXT) ngettext$(EXEEXT) envsubst$(EXEEXT) - cd man && $(MAKE) $(AM_MAKEFLAGS) gettext.1 ngettext.1 envsubst.1 + cd src && $(MAKE) $(AM_MAKEFLAGS) gettext$(EXEEXT) ngettext$(EXEEXT) printf_gettext$(EXEEXT) printf_ngettext$(EXEEXT) envsubst$(EXEEXT) + cd man && $(MAKE) $(AM_MAKEFLAGS) gettext.1 ngettext.1 printf_gettext.1 printf_ngettext.1 envsubst.1 maintainer-update-po: $(top_builddir)/config.status diff --git a/gettext-runtime/NEWS b/gettext-runtime/NEWS index b6ec64fde..8a007c0db 100644 --- a/gettext-runtime/NEWS +++ b/gettext-runtime/NEWS @@ -1,3 +1,9 @@ +Version 0.26 - July 2025 + +* Two new programs 'printf_gettext' and 'printf_ngettext' are provided, + that do formatted output with a localized format string in a more + efficient way (without spawning a subshell). + Version 0.25 - April 2025 * New library: libintl_d.a contains the runtime for using GNU gettext diff --git a/gettext-runtime/doc/Makefile.am b/gettext-runtime/doc/Makefile.am index b65ccd0c5..812020d3e 100644 --- a/gettext-runtime/doc/Makefile.am +++ b/gettext-runtime/doc/Makefile.am @@ -1,5 +1,5 @@ ## Makefile for the gettext-runtime/doc subdirectory of GNU gettext -## Copyright (C) 1995-1997, 2001-2003, 2006 Free Software Foundation, Inc. +## Copyright (C) 1995-2025 Free Software Foundation, Inc. ## ## This program is free software: you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by @@ -21,4 +21,9 @@ EXTRA_DIST = EXTRA_DIST += nls.texi matrix.texi -EXTRA_DIST += rt-gettext.texi rt-ngettext.texi rt-envsubst.texi +EXTRA_DIST += \ + rt-gettext.texi \ + rt-ngettext.texi \ + rt-printf_gettext.texi \ + rt-printf_ngettext.texi \ + rt-envsubst.texi diff --git a/gettext-runtime/doc/rt-printf_gettext.texi b/gettext-runtime/doc/rt-printf_gettext.texi new file mode 100644 index 000000000..8e9871c0a --- /dev/null +++ b/gettext-runtime/doc/rt-printf_gettext.texi @@ -0,0 +1,141 @@ +@c This file is part of the GNU gettext manual. +@c Copyright (C) 2025 Free Software Foundation, Inc. +@c See the file gettext.texi for copying conditions. + +@pindex printf_gettext +@cindex @code{printf_gettext} program, usage +@example +printf_gettext [@var{option}] @var{format} [@var{argument}]... +@end example + +@cindex lookup format string translation +@cindex formatted output in Shell +The @code{printf_gettext} program produces formatted output, +applying the native language translation of @var{format} +to the @var{argument}s. + +@noindent @strong{Options and arguments} + +@table @samp +@item -c @var{context} +@itemx --context=@var{context} +@opindex -c@r{, @code{printf_gettext} option} +@opindex --context@r{, @code{printf_gettext} option} +Specify the context for the format string to be translated. +See @ref{Contexts} for details. + +@item @var{format} +The format string. + +@item @var{argument} +A string or numeric argument. + +@end table + +@noindent @strong{Informative output} + +@table @samp +@item -h +@itemx --help +@opindex -h@r{, @code{printf_gettext} option} +@opindex --help@r{, @code{printf_gettext} option} +Display this help and exit. + +@item -V +@itemx --version +@opindex -V@r{, @code{printf_gettext} option} +@opindex --version@r{, @code{printf_gettext} option} +Output version information and exit. + +@end table + +The format string consists of +@itemize @bullet +@item +plain text, +@item +directives, that start with @samp{%}, +@item +escape sequences, that start with a backslash. +@end itemize + +A directive that consumes an argument +@itemize @bullet +@item +starts with @samp{%} or @samp{%@var{m}$} where @var{m} is a positive integer, +@item +is optionally followed by any of the characters +@samp{#}, @samp{0}, @samp{-}, @samp{ }, @samp{+}, +each of which acts as a flag, +@item +is optionally followed by a width specification (a nonnegative integer), +@item +is optionally followed by @samp{.} and a precision specification +(an optional nonnegative integer), +@item +is finished by a specifier +@itemize @bullet +@item +@samp{c}, that prints a character, +@item +@samp{s}, that prints a string, +@item +@samp{i}, @samp{d}, that print an integer, +@item +@samp{u}, @samp{o}, @samp{x}, @samp{X}, +that print an unsigned (nonnegative) integer, +@item +@samp{e}, @samp{E}, that print a floating-point number in scientific notation, +@item +@samp{f}, @samp{F}, that print a floating-point number without an exponent, +@item +@samp{g}, @samp{G}, that print a floating-point number in general notation, +@item +@samp{a}, @samp{A}, that print a floating-point number in hexadecimal notation. +@end itemize +@end itemize + +Some flag+specifier combinations are invalid: +@itemize @bullet +@item +The @samp{#} flag with the specifiers +@samp{c}, @samp{s}, @samp{i}, @samp{d}, @samp{u}. +@item +The @samp{0} flag with the specifiers +@samp{c}, @samp{s}. +@end itemize + +Additionally there is the directive @samp{%%}, that prints a single @code{%}. + +If a directive specifies the argument by its number (@samp{%@var{m}$} notation), +all directives that consume an argument must do so. + +The escape sequences are: +@table @code +@item \\ +backslash +@item \a +alert (BEL) +@item \b +backspace (BS) +@item \f +form feed (FF) +@item \n +new line (LF) +@item \r +carriage return (CR) +@item \t +horizontal tab (HT) +@item \v +vertical tab (VT) +@item \@var{nnn} +octal number with 1 to 3 octal digits +@end table + +@noindent @strong{Environment Variables} + +The translation of the format string is looked up in the translation domain +given by the environment variable @code{TEXTDOMAIN}. + +It is looked up in the catalogs directory given by the environment variable +@code{TEXTDOMAINDIR} or, if not present, in the default catalogs directory. diff --git a/gettext-runtime/doc/rt-printf_ngettext.texi b/gettext-runtime/doc/rt-printf_ngettext.texi new file mode 100644 index 000000000..59d540e41 --- /dev/null +++ b/gettext-runtime/doc/rt-printf_ngettext.texi @@ -0,0 +1,149 @@ +@c This file is part of the GNU gettext manual. +@c Copyright (C) 2025 Free Software Foundation, Inc. +@c See the file gettext.texi for copying conditions. + +@pindex printf_ngettext +@cindex @code{printf_ngettext} program, usage +@example +printf_ngettext [@var{option}] @var{format} @var{format-plural} @var{count} [@var{argument}]... +@end example + +@cindex lookup format string translation with plural +@cindex formatted output in Shell +The @code{printf_ngettext} program produces formatted output, +applying the native language translation of +@var{format} and @var{format-plural}, depending on @var{count}, +to the @var{argument}s. + +@noindent @strong{Options and arguments} + +@table @samp +@item -c @var{context} +@itemx --context=@var{context} +@opindex -c@r{, @code{printf_ngettext} option} +@opindex --context@r{, @code{printf_ngettext} option} +Specify the context for the format string to be translated. +See @ref{Contexts} for details. + +@item @var{format} +English singular form of format string. + +@item @var{format-plural} +English plural form of format string. + +@item @var{count} +A cardinal number. +The singular/plural form is chosen based on this value. + +@item @var{argument} +A string or numeric argument. + +@end table + +@noindent @strong{Informative output} + +@table @samp +@item -h +@itemx --help +@opindex -h@r{, @code{printf_ngettext} option} +@opindex --help@r{, @code{printf_ngettext} option} +Display this help and exit. + +@item -V +@itemx --version +@opindex -V@r{, @code{printf_ngettext} option} +@opindex --version@r{, @code{printf_ngettext} option} +Output version information and exit. + +@end table + +Each format string consists of +@itemize @bullet +@item +plain text, +@item +directives, that start with @samp{%}, +@item +escape sequences, that start with a backslash. +@end itemize + +A directive that consumes an argument +@itemize @bullet +@item +starts with @samp{%} or @samp{%@var{m}$} where @var{m} is a positive integer, +@item +is optionally followed by any of the characters +@samp{#}, @samp{0}, @samp{-}, @samp{ }, @samp{+}, +each of which acts as a flag, +@item +is optionally followed by a width specification (a nonnegative integer), +@item +is optionally followed by @samp{.} and a precision specification +(an optional nonnegative integer), +@item +is finished by a specifier +@itemize @bullet +@item +@samp{c}, that prints a character, +@item +@samp{s}, that prints a string, +@item +@samp{i}, @samp{d}, that print an integer, +@item +@samp{u}, @samp{o}, @samp{x}, @samp{X}, +that print an unsigned (nonnegative) integer, +@item +@samp{e}, @samp{E}, that print a floating-point number in scientific notation, +@item +@samp{f}, @samp{F}, that print a floating-point number without an exponent, +@item +@samp{g}, @samp{G}, that print a floating-point number in general notation, +@item +@samp{a}, @samp{A}, that print a floating-point number in hexadecimal notation. +@end itemize +@end itemize + +Some flag+specifier combinations are invalid: +@itemize @bullet +@item +The @samp{#} flag with the specifiers +@samp{c}, @samp{s}, @samp{i}, @samp{d}, @samp{u}. +@item +The @samp{0} flag with the specifiers +@samp{c}, @samp{s}. +@end itemize + +Additionally there is the directive @samp{%%}, that prints a single @code{%}. + +If a directive specifies the argument by its number (@samp{%@var{m}$} notation), +all directives that consume an argument must do so. + +The escape sequences are: +@table @code +@item \\ +backslash +@item \a +alert (BEL) +@item \b +backspace (BS) +@item \f +form feed (FF) +@item \n +new line (LF) +@item \r +carriage return (CR) +@item \t +horizontal tab (HT) +@item \v +vertical tab (VT) +@item \@var{nnn} +octal number with 1 to 3 octal digits +@end table + +@noindent @strong{Environment Variables} + +The translation of the format string is looked up in the translation domain +given by the environment variable @code{TEXTDOMAIN}. + +It is looked up in the catalogs directory given by the environment variable +@code{TEXTDOMAINDIR} or, if not present, in the default catalogs directory. diff --git a/gettext-runtime/man/Makefile.am b/gettext-runtime/man/Makefile.am index 9e6f85f22..3273157ef 100644 --- a/gettext-runtime/man/Makefile.am +++ b/gettext-runtime/man/Makefile.am @@ -1,6 +1,5 @@ ## Makefile for the gettext-runtime/man subdirectory of GNU gettext -## Copyright (C) 2001-2003, 2006, 2009, 2013-2014, 2018-2019 Free Software Foundation, -## Inc. +## Copyright (C) 2001-2025 Free Software Foundation, Inc. ## ## This program is free software: you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by @@ -24,12 +23,12 @@ EXTRA_DIST = # A manual page for each of the bin_PROGRAMS in src/Makefile.am. -man_aux = gettext.x ngettext.x envsubst.x +man_aux = gettext.x ngettext.x printf_gettext.x printf_ngettext.x envsubst.x # Likewise, plus additional manual pages for the libintl functions. -man_MAN1GEN = gettext.1 ngettext.1 -man_MAN1IN = gettext.1.in ngettext.1.in +man_MAN1GEN = gettext.1 ngettext.1 printf_gettext.1 printf_ngettext.1 +man_MAN1IN = gettext.1.in ngettext.1.in printf_gettext.1.in printf_ngettext.1.in man_MAN1OTHER = envsubst.1 man_MAN1 = $(man_MAN1GEN) $(man_MAN1OTHER) man_MAN3 = gettext.3 ngettext.3 \ @@ -40,8 +39,8 @@ man_MAN3LINK = dgettext.3 dcgettext.3 dngettext.3 dcngettext.3 man_MANS = $(man_MAN1) notrans_man_MANS = $(man_MAN3) $(man_MAN3LINK) -man_HTML1GEN = gettext.1.html ngettext.1.html -man_HTML1IN = gettext.1.html.in ngettext.1.html.in +man_HTML1GEN = gettext.1.html ngettext.1.html printf_gettext.1.html printf_ngettext.1.html +man_HTML1IN = gettext.1.html.in ngettext.1.html.in printf_gettext.1.html.in printf_ngettext.1.html.in man_HTML1OTHER = envsubst.1.html man_HTML1 = $(man_HTML1GEN) $(man_HTML1OTHER) man_HTML3 = gettext.3.html ngettext.3.html \ @@ -108,6 +107,8 @@ $(man_MAN1GEN): Makefile gettext.1: gettext.1.in ngettext.1: ngettext.1.in +printf_gettext.1: printf_gettext.1.in +printf_ngettext.1: printf_ngettext.1.in $(man_MAN1IN) $(man_MAN1OTHER): help2man $(top_srcdir)/../.version progname=`echo $@ | sed -e 's/\.in$$//' -e 's/\.1$$//'`; \ @@ -116,6 +117,8 @@ $(man_MAN1IN) $(man_MAN1OTHER): help2man $(top_srcdir)/../.version gettext.1.in: gettext.x ../src/gettext.c ngettext.1.in: ngettext.x ../src/ngettext.c +printf_gettext.1.in: printf_gettext.x ../src/printf_gettext.c +printf_ngettext.1.in: printf_ngettext.x ../src/printf_ngettext.c envsubst.1: envsubst.x ../src/envsubst.c $(man_MAN3): $(top_srcdir)/../.version @@ -143,6 +146,8 @@ $(man_HTML1GEN): Makefile gettext.1.html: gettext.1.html.in ngettext.1.html: ngettext.1.html.in +printf_gettext.1.html: printf_gettext.1.html.in +printf_ngettext.1.html: printf_ngettext.1.html.in $(man_HTML1IN): srcdir=''; \ @@ -153,6 +158,8 @@ $(man_HTML1IN): gettext.1.html.in: gettext.1.in ngettext.1.html.in: ngettext.1.in +printf_gettext.1.html.in: printf_gettext.1.in +printf_ngettext.1.html.in: printf_ngettext.1.in $(man_HTML1OTHER): srcdir=''; \ diff --git a/gettext-runtime/man/printf_gettext.x b/gettext-runtime/man/printf_gettext.x new file mode 100644 index 000000000..c8eb2bd73 --- /dev/null +++ b/gettext-runtime/man/printf_gettext.x @@ -0,0 +1,7 @@ +[NAME] +printf_gettext \- translate format string and apply it +[DESCRIPTION] +.\" Add any additional description here +The \fBprintf_gettext\fP program translates a format string into the user's +language, by looking up the translation in a message catalog, and applies +the translated format string to the specified arguments. diff --git a/gettext-runtime/man/printf_ngettext.x b/gettext-runtime/man/printf_ngettext.x new file mode 100644 index 000000000..0047726c7 --- /dev/null +++ b/gettext-runtime/man/printf_ngettext.x @@ -0,0 +1,9 @@ +[NAME] +printf_ngettext \- translate format string and apply it +[DESCRIPTION] +.\" Add any additional description here +The \fBprintf_ngettext\fP program translates a format string into the user's +language, by looking up the translation in a message catalog and then +choosing the appropriate plural form, which depends on the number \fICOUNT\fP +and the language of the message catalog where the translation was found, and +applies the translated format string to the specified arguments. diff --git a/gettext-runtime/po/POTFILES.in b/gettext-runtime/po/POTFILES.in index 791de449a..a86066f77 100644 --- a/gettext-runtime/po/POTFILES.in +++ b/gettext-runtime/po/POTFILES.in @@ -1,5 +1,5 @@ # List of files which contain translatable strings. -# Copyright (C) 1995-2024 Free Software Foundation, Inc. +# Copyright (C) 1995-2025 Free Software Foundation, Inc. # This file is free software, distributed under GNU GPL v3+. # For updating this file, look at the result of: @@ -9,3 +9,6 @@ src/envsubst.c src/gettext.c src/ngettext.c +src/printf-command.c +src/printf_gettext.c +src/printf_ngettext.c diff --git a/gettext-runtime/src/Makefile.am b/gettext-runtime/src/Makefile.am index bb6e569e2..bec7e47de 100644 --- a/gettext-runtime/src/Makefile.am +++ b/gettext-runtime/src/Makefile.am @@ -1,5 +1,5 @@ ## Makefile for the gettext-runtime/src subdirectory of GNU gettext -## Copyright (C) 1995-2024 Free Software Foundation, Inc. +## Copyright (C) 1995-2025 Free Software Foundation, Inc. ## ## This program is free software: you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by @@ -24,7 +24,9 @@ DISTCLEANFILES = RM = rm -f -bin_PROGRAMS = gettext ngettext envsubst +bin_PROGRAMS = gettext ngettext printf_gettext printf_ngettext envsubst + +noinst_LIBRARIES = libgrtsrc.a # Note that Automake's $(DEFAULT_INCLUDES) already contains # -I. -I$(srcdir) -I$(top_builddir). @@ -39,19 +41,29 @@ AM_CFLAGS = @WARN_CFLAGS@ # Source dependencies. gettext_SOURCES = gettext.c escapes.h ngettext_SOURCES = ngettext.c escapes.h +printf_gettext_SOURCES = printf_gettext.c +printf_ngettext_SOURCES = printf_ngettext.c envsubst_SOURCES = envsubst.c +# libgrtsrc contains all code that is needed by at least two programs. +libgrtsrc_a_SOURCES = \ + printf-command.h printf-command.c + # Link dependencies. # Need @LTLIBICONV@ because striconv.c uses iconv(). -LDADD = ../gnulib-lib/libgrt.a @LTLIBINTL@ @LTLIBICONV@ $(WOE32_LDADD) +LDADD = libgrtsrc.a ../gnulib-lib/libgrt.a @LTLIBINTL@ @LTLIBICONV@ $(WOE32_LDADD) # Specify installation directory, for --enable-relocatable. gettext_CFLAGS = -DINSTALLDIR=$(bindir_c_make) ngettext_CFLAGS = -DINSTALLDIR=$(bindir_c_make) +printf_gettext_CFLAGS = -DINSTALLDIR=$(bindir_c_make) +printf_ngettext_CFLAGS = -DINSTALLDIR=$(bindir_c_make) envsubst_CFLAGS = -DINSTALLDIR=$(bindir_c_make) if RELOCATABLE_VIA_LD gettext_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)` ngettext_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)` +printf_gettext_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)` +printf_ngettext_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)` envsubst_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)` endif diff --git a/gettext-runtime/src/printf-command.c b/gettext-runtime/src/printf-command.c new file mode 100644 index 000000000..92767bd5c --- /dev/null +++ b/gettext-runtime/src/printf-command.c @@ -0,0 +1,744 @@ +/* Formatted output with a POSIX compatible format string. + Copyright (C) 2025 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +#include + +/* Specification. */ +#include "printf-command.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "attribute.h" +#include "c-ctype.h" +#include "strnlen1.h" +#include "c-strtod.h" +#include "xstrtod.h" +#include "quote.h" +#include "xalloc.h" +#include "gettext.h" + +#define _(str) gettext (str) + +/* The argument type consumed by a directive. */ +enum format_arg_type +{ + FAT_CHARACTER, + FAT_STRING, + FAT_INTEGER, + FAT_UNSIGNED_INTEGER, + FAT_FLOAT +}; + +/* A piece of output. */ +struct format_piece +{ + /* For plain text, directives that take no argument, and escape sequences: */ + const char *text_start; + size_t text_length; + /* For directives that take an argument: */ + enum format_arg_type arg_type; + size_t arg_number; /* > 0 */ + const char *arg_fmt; +}; + +/* The entire format string. */ +struct format_string +{ + struct format_piece *pieces; + size_t npieces; +}; + +/* Parses the format string. + Returns the number of arguments that it consumes. */ +static size_t +parse_format_string (struct format_string *fmts, const char *format) +{ + struct format_piece *pieces = NULL; + size_t npieces = 0; + size_t npieces_allocated = 0; + + size_t directives = 0; + size_t numbered_arg_count = 0; + size_t unnumbered_arg_count = 0; + size_t max_numbered_arg = 0; + const char *current_piece_start = NULL; + + for (;;) + { + /* Invariant: numbered_arg_count == 0 || unnumbered_arg_count == 0. */ + /* Invariant: current_piece_start == NULL || current_piece_start < format. */ + if (*format == '\0' || *format == '%' || *format == '\\') + { + if (current_piece_start != NULL) + { + if (npieces == npieces_allocated) + { + npieces_allocated = 2 * npieces_allocated + 1; + pieces = (struct format_piece *) xrealloc (pieces, npieces_allocated * sizeof (struct format_piece)); + } + pieces[npieces].text_start = current_piece_start; + pieces[npieces].text_length = format - current_piece_start; + npieces++; + current_piece_start = NULL; + } + } + else + { + if (current_piece_start == NULL) + current_piece_start = format; + } + + if (*format == '\0') + break; + + if (*format == '%') + { + /* A directive. */ + format++; + directives++; + + if (*format == '%') + { + /* "%%" produces a literal '%'. */ + if (npieces == npieces_allocated) + { + npieces_allocated = 2 * npieces_allocated + 1; + pieces = (struct format_piece *) xrealloc (pieces, npieces_allocated * sizeof (struct format_piece)); + } + pieces[npieces].text_start = "%"; + pieces[npieces].text_length = 1; + npieces++; + } + else + { + size_t number = 0; + if (c_isdigit (*format)) + { + const char *f = format; + size_t m = 0; + + do + { + m = 10 * m + (*f - '0'); + f++; + } + while (c_isdigit (*f)); + + if (*f == '$') + { + if (m == 0) + error (EXIT_FAILURE, 0, + _("In the directive number %zu, the argument number 0 is not a positive integer."), + directives); + number = m; + format = ++f; + } + } + + /* Parse flags. */ + bool have_space_flag = false; + bool have_plus_flag = false; + bool have_minus_flag = false; + bool have_hash_flag = false; + bool have_zero_flag = false; + for (;; format++) + { + switch (*format) + { + case ' ': + have_space_flag = true; + continue; + case '+': + have_plus_flag = true; + continue; + case '-': + have_minus_flag = true; + continue; + case '#': + have_hash_flag = true; + continue; + case '0': + have_zero_flag = true; + continue; + default: + break; + } + break; + } + + /* Parse width. */ + const char *width_start = NULL; + size_t width_length = 0; + if (c_isdigit (*format)) + { + width_start = format; + do format++; while (c_isdigit (*format)); + width_length = format - width_start; + } + + /* Parse precision. */ + const char *precision_start = NULL; + size_t precision_length = 0; + if (*format == '.') + { + format++; + + precision_start = format; + while (c_isdigit (*format)) + format++; + precision_length = format - precision_start; + } + + enum format_arg_type type; + switch (*format) + { + case 'c': + type = FAT_CHARACTER; + break; + case 's': + type = FAT_STRING; + break; + case 'i': case 'd': + type = FAT_INTEGER; + break; + case 'u': case 'o': case 'x': case 'X': + type = FAT_UNSIGNED_INTEGER; + break; + case 'e': case 'E': case 'f': case 'F': case 'g': case 'G': + case 'a': case 'A': + type = FAT_FLOAT; + break; + default: + if (*format == '\0') + error (EXIT_FAILURE, 0, + _("The string ends in the middle of a directive.")); + else + { + if (c_isprint (*format)) + error (EXIT_FAILURE, 0, + _("In the directive number %zu, the character '%c' is not a valid conversion specifier."), + directives, *format); + else + error (EXIT_FAILURE, 0, + _("The character that terminates the directive number %zu is not a valid conversion specifier."), + directives); + } + } + + if (have_hash_flag + && (*format == 'c' || *format == 's' + || *format == 'i' || *format == 'd' || *format == 'u')) + error (EXIT_FAILURE, 0, + _("In the directive number %zu, the flag '%c' is invalid for the conversion '%c'."), + directives, '#', *format); + if (have_zero_flag && (*format == 'c' || *format == 's')) + error (EXIT_FAILURE, 0, + _("In the directive number %zu, the flag '%c' is invalid for the conversion '%c'."), + directives, '0', *format); + + if (npieces == npieces_allocated) + { + npieces_allocated = 2 * npieces_allocated + 1; + pieces = (struct format_piece *) xrealloc (pieces, npieces_allocated * sizeof (struct format_piece)); + } + pieces[npieces].text_start = NULL; + pieces[npieces].text_length = 0; + pieces[npieces].arg_type = type; + + if (number) + { + /* Numbered argument. */ + + /* Numbered and unnumbered specifications are exclusive. */ + if (unnumbered_arg_count > 0) + error (EXIT_FAILURE, 0, + _("The string refers to arguments both through absolute argument numbers and through unnumbered argument specifications.")); + + pieces[npieces].arg_number = number; + numbered_arg_count++; + if (max_numbered_arg < number) + max_numbered_arg = number; + } + else + { + /* Unnumbered argument. */ + + /* Numbered and unnumbered specifications are exclusive. */ + if (numbered_arg_count > 0) + error (EXIT_FAILURE, 0, + _("The string refers to arguments both through absolute argument numbers and through unnumbered argument specifications.")); + + pieces[npieces].arg_number = unnumbered_arg_count + 1; + unnumbered_arg_count++; + } + + if (fmts != NULL) + { + char *arg_fmt = (char *) xmalloc (1 + 5 + width_length + 1 + precision_length + 2 + 1); + { + char *f = arg_fmt; + *f++ = '%'; + if (have_space_flag) + *f++ = ' '; + if (have_plus_flag) + *f++ = '+'; + if (have_minus_flag) + *f++ = '-'; + if (have_hash_flag) + *f++ = '#'; + if (have_zero_flag) + *f++ = '0'; + if (width_start != NULL) + { + memcpy (f, width_start, width_length); + f += width_length; + } + if (precision_start != NULL) + { + *f++ = '.'; + memcpy (f, precision_start, precision_length); + f += precision_length; + } + switch (type) + { + case FAT_INTEGER: + case FAT_UNSIGNED_INTEGER: + *f++ = 'j'; + break; + case FAT_FLOAT: + *f++ = 'L'; + break; + default: + break; + } + *f++ = (*format == 'c' ? 's' : *format); + *f = '\0'; + } + pieces[npieces].arg_fmt = arg_fmt; + } + + npieces++; + } + + format++; + } + else if (*format == '\\') + { + /* An escape sequence. */ + format++; + + const char *one_char; + switch (*format) + { + case '\\': one_char = "\\"; format++; break; + case 'a': one_char = "\a"; format++; break; + case 'b': one_char = "\b"; format++; break; + case 'f': one_char = "\f"; format++; break; + case 'n': one_char = "\n"; format++; break; + case 'r': one_char = "\r"; format++; break; + case 't': one_char = "\t"; format++; break; + case 'v': one_char = "\v"; format++; break; + + case '0': case '1': case '2': case '3': case '4': case '5': + case '6': case '7': + { + unsigned int n = (*format - '0'); + format++; + if (*format >= '0' && *format <= '7') + { + n = (n << 3) + (*format - '0'); + format++; + if (*format >= '0' && *format <= '7') + { + n = (n << 3) + (*format - '0'); + format++; + } + } + if (fmts != NULL) + { + char *text = (char *) xmalloc (1); + *text = (unsigned char) n; + one_char = text; + } + else + one_char = ""; /* just a dummy */ + } + break; + + default: + if (*format == '\0') + error (EXIT_FAILURE, 0, + _("The string ends in the middle of an escape sequence.")); + else + { + if (c_isprint (*format)) + error (EXIT_FAILURE, 0, + (*format == 'c' + || *format == 'x' + || *format == 'u' || *format == 'U' + ? _("The escape sequence '%c%c' is unsupported (not in POSIX).") + : _("The escape sequence '%c%c' is invalid.")), + '\\', *format); + else + error (EXIT_FAILURE, 0, + _("This escape sequence is invalid.")); + } + } + + if (npieces == npieces_allocated) + { + npieces_allocated = 2 * npieces_allocated + 1; + pieces = (struct format_piece *) xrealloc (pieces, npieces_allocated * sizeof (struct format_piece)); + } + pieces[npieces].text_start = one_char; + pieces[npieces].text_length = 1; + npieces++; + } + else + format++; + } + + if (fmts != NULL) + { + fmts->pieces = pieces; + fmts->npieces = npieces; + } + else + free (pieces); + + /* The number of consumed arguments: */ + return (numbered_arg_count > 0 ? max_numbered_arg : unnumbered_arg_count); +} + +static int status; + +/* Applies the format string to the array of remaining arguments. */ +static void +apply_format_string (const struct format_string *fmts, + size_t argc, char *argv[]) +{ + size_t npieces = fmts->npieces; + size_t i; + + for (i = 0; i < npieces; i++) + { + struct format_piece *piece = &fmts->pieces[i]; + + if (piece->text_start != NULL) + { + /* Print some fixed text. */ + if (fwrite (piece->text_start, 1, piece->text_length, stdout) + < piece->text_length) + error (EXIT_FAILURE, 0, _("write error")); + } + else + { + /* Convert and print an argument. */ + char *arg; + char zero[2] = { '0', '\0' }; + char *empty = zero + 1; + + if (piece->arg_number - 1 < argc) + arg = argv[piece->arg_number - 1]; + else + { + /* + point 11 suggests that we make "%1$x" behave differently from + "%x". We don't do this, because translators are free to switch + from unnumbered arguments to numbered arguments or vice versa. */ + arg = (piece->arg_type == FAT_CHARACTER + || piece->arg_type == FAT_STRING + ? empty + : zero); + } + + switch (piece->arg_type) + { + case FAT_CHARACTER: + /* + point 13 suggests to print the first *byte* of arg. But this + is not appropriate in multibyte locales. Therefore, print the + first multibyte character instead, if arg starts with a valid + multibyte character. */ + { + mbstate_t state; + char32_t wc; + + mbszero (&state); + size_t ret = mbrtoc32 (&wc, arg, strnlen1 (arg, MB_CUR_MAX), &state); + arg[(int) ret >= 0 ? ret : 1] = '\0'; + } + FALLTHROUGH; + case FAT_STRING: + errno = 0; + if (fzprintf (stdout, piece->arg_fmt, arg) < 0) + { + if (errno == ENOMEM) + xalloc_die (); + error (EXIT_FAILURE, 0, _("write error")); + } + break; + + case FAT_INTEGER: + { + intmax_t arg_value; + if (*arg == '\'' || *arg == '"') + { + /* POSIX says: "If the leading character is a single-quote + or double-quote, the value shall be the numeric value + in the underlying codeset of the character following the + single-quote or double-quote." + Use the first first multibyte character, if arg starts + with a valid multibyte character. */ + mbstate_t state; + char32_t wc; + + mbszero (&state); + size_t ret = mbrtoc32 (&wc, arg + 1, strnlen1 (arg + 1, MB_CUR_MAX), &state); + if ((int) ret > 0) + arg_value = wc; + else if (arg[1] != '\0') + arg_value = (unsigned char) arg[1]; + else + { + arg_value = 0; + error (EXIT_SUCCESS, 0, + _("%s: expected a numeric value"), + quote (arg)); + status = EXIT_FAILURE; + } + } + else + { + /* xstrtoimax is a nicer API than strtoimax. + Let's hope that I don't make a mistake with strtoimax's + horrible calling convention here. */ + char *ptr; + arg_value = (errno = 0, strtoimax (arg, &ptr, 0)); + bool parsed = (ptr != arg && errno == 0); + + if (parsed && *ptr == '\0') + /* Successful parse of arg. */ + ; + else + { + if (parsed) + error (EXIT_SUCCESS, 0, + _("%s: value not completely converted"), + quote (arg)); + else + { + arg_value = 0; + error (EXIT_SUCCESS, 0, + _("%s: expected a numeric value"), + quote (arg)); + } + status = EXIT_FAILURE; + } + } + + errno = 0; + if (fzprintf (stdout, piece->arg_fmt, arg_value) < 0) + { + if (errno == ENOMEM) + xalloc_die (); + error (EXIT_FAILURE, 0, _("write error")); + } + } + break; + + case FAT_UNSIGNED_INTEGER: + { + uintmax_t arg_value; + if (*arg == '\'' || *arg == '"') + { + /* POSIX says: "If the leading character is a single-quote + or double-quote, the value shall be the numeric value + in the underlying codeset of the character following the + single-quote or double-quote." + Use the first first multibyte character, if arg starts + with a valid multibyte character. */ + mbstate_t state; + char32_t wc; + + mbszero (&state); + size_t ret = mbrtoc32 (&wc, arg + 1, strnlen1 (arg + 1, MB_CUR_MAX), &state); + if ((int) ret > 0) + arg_value = wc; + else if (arg[1] != '\0') + arg_value = (unsigned char) arg[1]; + else + { + arg_value = 0; + error (EXIT_SUCCESS, 0, + _("%s: expected a numeric value"), + quote (arg)); + status = EXIT_FAILURE; + } + } + else + { + /* xstrtoumax is a nicer API than strtoumax. + But here, we need to accept a leading '-' sign, as in + "-3" or " -3". + Let's hope that I don't make a mistake with strtoumax's + horrible calling convention here. */ + char *ptr; + arg_value = (errno = 0, strtoumax (arg, &ptr, 0)); + bool parsed = (ptr != arg && errno == 0); + + if (parsed && *ptr == '\0') + /* Successful parse of arg. */ + ; + else + { + if (parsed) + error (EXIT_SUCCESS, 0, + _("%s: value not completely converted"), + quote (arg)); + else + { + arg_value = 0; + error (EXIT_SUCCESS, 0, + _("%s: expected a numeric value"), + quote (arg)); + } + status = EXIT_FAILURE; + } + } + + errno = 0; + if (fzprintf (stdout, piece->arg_fmt, arg_value) < 0) + { + if (errno == ENOMEM) + xalloc_die (); + error (EXIT_FAILURE, 0, _("write error")); + } + } + break; + + case FAT_FLOAT: + /* + suggests to use strtod(), i.e. a 'double'. We prefer a + 'long double', because it has higher precision. */ + /* Try interpreting the argument as a number in the current locale + and, if that fails, in the "C" locale. Like coreutils 'printf' + does. */ + { + long double arg_value; + const char *ptr; + bool parsed = xstrtold (arg, &ptr, &arg_value, strtold); + if (parsed && *ptr == '\0') + /* Successful parse of arg in the current locale. */ + ; + else + { + long double arg_value2; + const char *ptr2; + bool parsed2 = xstrtold (arg, &ptr2, &arg_value2, c_strtold); + if (parsed2 && *ptr2 == '\0') + { + /* Successful parse of arg in the "C" locale. */ + arg_value = arg_value2; + } + else + { + if (parsed2 && (!parsed || ptr2 > ptr)) + arg_value = arg_value2; + if (parsed || parsed2) + error (EXIT_SUCCESS, 0, + _("%s: value not completely converted"), + quote (arg)); + else + { + arg_value = 0.0L; + error (EXIT_SUCCESS, 0, + _("%s: expected a numeric value"), + quote (arg)); + } + status = EXIT_FAILURE; + } + } + + errno = 0; + if (fzprintf (stdout, piece->arg_fmt, arg_value) < 0) + { + if (errno == ENOMEM) + xalloc_die (); + error (EXIT_FAILURE, 0, _("write error")); + } + } + break; + } + } + } +} + +size_t +printf_consumed_arguments (const char *format) +{ + return parse_format_string (NULL, format); +} + +void +printf_command (const char *format, size_t args_each_round, + size_t argc, char *argv[]) +{ + /* Parse the format string, and bail out early if it is invalid. */ + struct format_string fmts; + size_t consumed_arguments = parse_format_string (&fmts, format); + + /* Validate consumed_arguments against args_each_round. */ + if (consumed_arguments > args_each_round) + error (EXIT_FAILURE, 0, + _("The translated format string consumes %zu arguments, whereas the original format string consumes only %zu arguments."), + consumed_arguments, args_each_round); + /* Here consumed_arguments <= args_each_round. + It is OK if consumed_arguments < args_each_round; this happens for example + in 'printf_ngettext', when the chosen format string applies only to a + single value. */ + + /* Repeatedly apply the format string to the remaining arguments. */ + if (args_each_round == 0 && argc > 0) + { + error (0, 0, + _("warning: ignoring excess arguments, starting with %s"), + quote(argv[0])); + argc = 0; + } + status = EXIT_SUCCESS; + for (;;) + { + apply_format_string (&fmts, argc, argv); + if (argc <= args_each_round) + break; + argc -= args_each_round; + argv += args_each_round; + } + + if (status != EXIT_SUCCESS) + exit (status); +} diff --git a/gettext-runtime/src/printf-command.h b/gettext-runtime/src/printf-command.h new file mode 100644 index 000000000..ac66a72ad --- /dev/null +++ b/gettext-runtime/src/printf-command.h @@ -0,0 +1,46 @@ +/* Formatted output with a POSIX compatible format string. + Copyright (C) 2025 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +/* This file implements the bulk of the POSIX:2024 specification for the 'printf' + command: + + + including the floating-point conversion specifiers 'a', 'A', 'e', 'E', + 'f', 'F', 'g', 'G', but without the obsolescent 'b' conversion specifier. */ + +#ifndef _PRINTF_COMMAND_H +#define _PRINTF_COMMAND_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Returns the number of arguments that a format string consumes. */ +extern size_t printf_consumed_arguments (const char *format); + +/* Applies a format string to a sequence of string arguments. */ +extern void printf_command (const char *format, size_t args_each_round, + size_t argc, char *argv[]); + +#ifdef __cplusplus +} +#endif + +#endif /* _PRINTF_COMMAND_H */ diff --git a/gettext-runtime/src/printf_gettext.c b/gettext-runtime/src/printf_gettext.c new file mode 100644 index 000000000..e92e06a74 --- /dev/null +++ b/gettext-runtime/src/printf_gettext.c @@ -0,0 +1,287 @@ +/* Formatted output with a localized format string. + Copyright (C) 2025 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +/* This program is a combination of the 'gettext' program with the 'printf' + program. It takes a format string and arguments, looks up the translation + of the format string (for the current locale, according to the environment + variables TEXTDOMAIN and TEXTDOMAINDIR), and applies that translated format + string to the arguments. */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include + +#include +#include "printf-command.h" +#include "noreturn.h" +#include "closeout.h" +#include "progname.h" +#include "relocatable.h" +#include "basename-lgpl.h" +#include "propername.h" +#include "gettext.h" + +#define _(str) gettext (str) + +/* Forward declaration of local functions. */ +_GL_NORETURN_FUNC static void usage (int status); + +int +main (int argc, char *argv[]) +{ + /* Default values for command line options. */ + bool do_help = false; + bool do_version = false; + const char *domain = getenv ("TEXTDOMAIN"); + const char *domaindir = getenv ("TEXTDOMAINDIR"); + const char *context = NULL; + + /* Set program name for message texts. */ + set_program_name (argv[0]); + + /* Set locale via LC_ALL. */ + setlocale (LC_ALL, ""); + + /* Set the text message domain. */ + bindtextdomain (PACKAGE, relocate (LOCALEDIR)); + bindtextdomain ("gnulib", relocate (GNULIB_LOCALEDIR)); + textdomain (PACKAGE); + + /* Ensure that write errors on stdout are detected. */ + atexit (close_stdout); + + /* Parse command line options. */ + { + /* Long options. */ + static const struct option long_options[] = + { + { "context", required_argument, NULL, 'c' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL, 0, NULL, 0 } + }; + + int optchar; + + while ((optchar = getopt_long (argc, argv, "+c:hV", long_options, NULL)) + != EOF) + switch (optchar) + { + case '\0': /* Long option. */ + break; + case 'c': + context = optarg; + break; + case 'h': + do_help = true; + break; + case 'V': + do_version = true; + break; + default: + usage (EXIT_FAILURE); + } + } + + /* Version information is requested. */ + if (do_version) + { + printf ("%s (GNU %s) %s\n", last_component (program_name), + PACKAGE, VERSION); + /* xgettext: no-wrap */ + printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\ +License GPLv3+: GNU GPL version 3 or later <%s>\n\ +This is free software: you are free to change and redistribute it.\n\ +There is NO WARRANTY, to the extent permitted by law.\n\ +"), + "2025", "https://gnu.org/licenses/gpl.html"); + printf (_("Written by %s.\n"), proper_name ("Bruno Haible")); + exit (EXIT_SUCCESS); + } + + /* Help is requested. */ + if (do_help) + usage (EXIT_SUCCESS); + + /* The format string is the first non-option argument. */ + if (!(argc - optind >= 1)) + { + error (EXIT_SUCCESS, 0, _("missing format string")); + usage (EXIT_FAILURE); + } + const char *format = argv[optind++]; + + argc -= optind; + argv += optind; + + /* The number of arguments consumed in each processing round is determined + by the FORMAT argument. This is necessary to avoid havoc if the translated + format string happens to consume a different number of arguments. */ + size_t args_each_round = printf_consumed_arguments (format); + + if (domain != NULL && domain[0] != '\0') + { + /* Bind domain to appropriate directory. */ + if (domaindir != NULL && domaindir[0] != '\0') + bindtextdomain (domain, domaindir); + + /* Look up the localized format string. */ + format = (context != NULL + ? dpgettext_expr (domain, context, format) + : dgettext (domain, format)); + } + + /* Execute a 'printf' command. */ + printf_command (format, args_each_round, argc, argv); + + exit (EXIT_SUCCESS); +} + + +/* Display usage information and exit. */ +static void +usage (int status) +{ + if (status != EXIT_SUCCESS) + fprintf (stderr, _("Try '%s --help' for more information.\n"), + program_name); + else + { + /* xgettext: no-wrap */ + printf (_("\ +Usage: %s [OPTION] FORMAT [ARGUMENT]...\n\ +"), program_name); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +Produces formatted output, applying the native language translation of FORMAT\n\ +to the ARGUMENTs.\n")); + printf ("\n"); + printf (_("\ +Options and arguments:\n")); + printf (_("\ + -c, --context=CONTEXT specify context for FORMAT\n")); + printf (_("\ + FORMAT format string\n")); + printf (_("\ + ARGUMENT string or numeric argument\n")); + printf ("\n"); + printf (_("\ +Informative output:\n")); + printf (_("\ + -h, --help display this help and exit\n")); + printf (_("\ + -V, --version display version information and exit\n")); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +The format string consists of\n\ + - plain text,\n\ + - directives, that start with '%c',\n\ + - escape sequences, that start with a backslash.\n"), + '%'); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +A directive that consumes an argument\n\ + - starts with '%s' or '%s' where %s is a positive integer,\n\ + - is optionally followed by any of the characters '%c', '%c', '%c', '%c', '%c',\n\ + each of which acts as a flag,\n\ + - is optionally followed by a width specification (a nonnegative integer),\n\ + - is optionally followed by '%c' and a precision specification (an optional\n\ + nonnegative integer),\n\ + - is finished by a specifier\n\ + - '%c', that prints a character,\n\ + - '%c', that prints a string,\n\ + - '%c', '%c', that print an integer,\n\ + - '%c', '%c', '%c', '%c', that print an unsigned (nonnegative) integer,\n\ + - '%c', '%c', that print a floating-point number in scientific notation,\n\ + - '%c', '%c', that print a floating-point number without an exponent,\n\ + - '%c', '%c', that print a floating-point number in general notation,\n\ + - '%c', '%c', that print a floating-point number in hexadecimal notation.\n\ +Additionally there is the directive '%s', that prints a single '%c'.\n"), + "%", "%m$", "m", + '#', '0', '-', ' ', '+', + '.', + 'c', + 's', + 'i', 'd', + 'u', 'o', 'x', 'X', + 'e', 'E', 'f', 'F', 'g', 'G', 'a', 'A', + "%%", '%'); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +If a directive specifies the argument by its number ('%s' notation),\n\ +all directives that consume an argument must do so.\n"), + "%m$"); + printf ("\n"); + /* TRANSLATORS: Most of the placeholders expand to 2 characters. + The last placeholder expands to 4 characters. */ + printf (_("\ +The escape sequences are:\n\ +\n\ + %s backslash\n\ + %s alert (BEL)\n\ + %s backspace (BS)\n\ + %s form feed (FF)\n\ + %s new line (LF)\n\ + %s carriage return (CR)\n\ + %s horizontal tab (HT)\n\ + %s vertical tab (VT)\n\ + %s octal number with 1 to 3 octal digits\n"), + "\\\\", "\\a", "\\b", "\\f", "\\n", "\\r", "\\t", "\\v", + "\\nnn"); + printf ("\n"); + printf (_("\ +Environment variables:\n")); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +The translation of the format string is looked up in the translation domain\n\ +given by the environment variable %s.\n"), + "TEXTDOMAIN"); + /* xgettext: no-wrap */ + printf (_("\ +It is looked up in the catalogs directory given by the environment variable\n\ +%s or, if not present, in the default catalogs directory.\n\ +This binary is configured to use the default catalogs directory:\n\ +%s\n"), + "TEXTDOMAINDIR", + getenv ("IN_HELP2MAN") == NULL ? relocate (LOCALEDIR) : "@localedir@"); + printf ("\n"); + /* TRANSLATORS: The first placeholder is the web address of the Savannah + project of this package. The second placeholder is the bug-reporting + email address for this package. Please add _another line_ saying + "Report translation bugs to <...>\n" with the address for translation + bugs (typically your translation team's web or email address). */ + printf (_("\ +Report bugs in the bug tracker at <%s>\n\ +or by email to <%s>.\n"), + "https://savannah.gnu.org/projects/gettext", + "bug-gettext@gnu.org"); + } + + exit (status); +} diff --git a/gettext-runtime/src/printf_ngettext.c b/gettext-runtime/src/printf_ngettext.c new file mode 100644 index 000000000..9511cb067 --- /dev/null +++ b/gettext-runtime/src/printf_ngettext.c @@ -0,0 +1,323 @@ +/* Formatted output with a plural form of a localized format string. + Copyright (C) 2025 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +/* This program is a combination of the 'ngettext' program with the 'printf' + program. It takes the (English) singular and plural form of a format string, + a cardinal number, and arguments. It finds the translation of the format + string (for the current locale, according to the environment variables + TEXTDOMAIN and TEXTDOMAINDIR), by looking it up in a message catalog and + then choosing the appropriate plural form, which depends on the number and + the language of the message catalog where the translation was found, and + applies that translated format string to the arguments. */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "printf-command.h" +#include "noreturn.h" +#include "closeout.h" +#include "progname.h" +#include "relocatable.h" +#include "basename-lgpl.h" +#include "propername.h" +#include "gettext.h" + +#define _(str) gettext (str) + +/* Forward declaration of local functions. */ +_GL_NORETURN_FUNC static void usage (int status); + +int +main (int argc, char *argv[]) +{ + /* Default values for command line options. */ + bool do_help = false; + bool do_version = false; + const char *domain = getenv ("TEXTDOMAIN"); + const char *domaindir = getenv ("TEXTDOMAINDIR"); + const char *context = NULL; + + /* Set program name for message texts. */ + set_program_name (argv[0]); + + /* Set locale via LC_ALL. */ + setlocale (LC_ALL, ""); + + /* Set the text message domain. */ + bindtextdomain (PACKAGE, relocate (LOCALEDIR)); + bindtextdomain ("gnulib", relocate (GNULIB_LOCALEDIR)); + textdomain (PACKAGE); + + /* Ensure that write errors on stdout are detected. */ + atexit (close_stdout); + + /* Parse command line options. */ + { + /* Long options. */ + static const struct option long_options[] = + { + { "context", required_argument, NULL, 'c' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL, 0, NULL, 0 } + }; + + int optchar; + + while ((optchar = getopt_long (argc, argv, "+c:hV", long_options, NULL)) + != EOF) + switch (optchar) + { + case '\0': /* Long option. */ + break; + case 'c': + context = optarg; + break; + case 'h': + do_help = true; + break; + case 'V': + do_version = true; + break; + default: + usage (EXIT_FAILURE); + } + } + + /* Version information is requested. */ + if (do_version) + { + printf ("%s (GNU %s) %s\n", last_component (program_name), + PACKAGE, VERSION); + /* xgettext: no-wrap */ + printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\ +License GPLv3+: GNU GPL version 3 or later <%s>\n\ +This is free software: you are free to change and redistribute it.\n\ +There is NO WARRANTY, to the extent permitted by law.\n\ +"), + "2025", "https://gnu.org/licenses/gpl.html"); + printf (_("Written by %s.\n"), proper_name ("Bruno Haible")); + exit (EXIT_SUCCESS); + } + + /* Help is requested. */ + if (do_help) + usage (EXIT_SUCCESS); + + /* The format string is the first non-option argument. */ + if (!(argc - optind >= 3)) + { + error (EXIT_SUCCESS, 0, _("missing arguments")); + usage (EXIT_FAILURE); + } + const char *format = argv[optind++]; + const char *format_plural = argv[optind++]; + const char *count = argv[optind++]; + + unsigned long n; + { + char *endp; + unsigned long tmp_val; + + if (isdigit ((unsigned char) count[0]) + && (errno = 0, + tmp_val = strtoul (count, &endp, 10), + errno == 0 && endp[0] == '\0')) + n = tmp_val; + else + /* When COUNT is not valid, use plural. */ + n = 99; + } + + argc -= optind; + argv += optind; + + /* The number of arguments consumed in each processing round is determined + by the FORMAT and FORMAT-PLURAL arguments. This is necessary to avoid + havoc if the translated format string happens to consume a different + number of arguments. */ + size_t args_each_round; + { + size_t args_consumed_1 = printf_consumed_arguments (format); + size_t args_consumed_2 = printf_consumed_arguments (format_plural); + args_each_round = + (args_consumed_1 >= args_consumed_2 ? args_consumed_1 : args_consumed_2); + } + + if (domain != NULL && domain[0] != '\0') + { + /* Bind domain to appropriate directory. */ + if (domaindir != NULL && domaindir[0] != '\0') + bindtextdomain (domain, domaindir); + + /* Look up the localized format string. */ + format = (context != NULL + ? dnpgettext_expr (domain, context, format, format_plural, n) + : dngettext (domain, format, format_plural, n)); + } + else + /* Use English plural form handling. */ + format = (n == 1 ? format : format_plural); + + /* Execute a 'printf' command. */ + printf_command (format, args_each_round, argc, argv); + + exit (EXIT_SUCCESS); +} + + +/* Display usage information and exit. */ +static void +usage (int status) +{ + if (status != EXIT_SUCCESS) + fprintf (stderr, _("Try '%s --help' for more information.\n"), + program_name); + else + { + /* xgettext: no-wrap */ + printf (_("\ +Usage: %s [OPTION] FORMAT FORMAT-PLURAL COUNT [ARGUMENT]...\n\ +"), program_name); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +Produces formatted output, applying the native language translation of FORMAT\n\ +and FORMAT-PLURAL, depending on COUNT, to the ARGUMENTs.\n")); + printf ("\n"); + printf (_("\ +Options and arguments:\n")); + printf (_("\ + -c, --context=CONTEXT specify context for FORMAT\n")); + printf (_("\ + FORMAT English singular form of format string\n")); + printf (_("\ + FORMAT-PLURAL English plural form of format string\n")); + printf (_("\ + COUNT choose singular/plural form based on this value\n")); + printf (_("\ + ARGUMENT string or numeric argument\n")); + printf ("\n"); + printf (_("\ +Informative output:\n")); + printf (_("\ + -h, --help display this help and exit\n")); + printf (_("\ + -V, --version display version information and exit\n")); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +Each format string consists of\n\ + - plain text,\n\ + - directives, that start with '%c',\n\ + - escape sequences, that start with a backslash.\n"), + '%'); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +A directive that consumes an argument\n\ + - starts with '%s' or '%s' where %s is a positive integer,\n\ + - is optionally followed by any of the characters '%c', '%c', '%c', '%c', '%c',\n\ + each of which acts as a flag,\n\ + - is optionally followed by a width specification (a nonnegative integer),\n\ + - is optionally followed by '%c' and a precision specification (an optional\n\ + nonnegative integer),\n\ + - is finished by a specifier\n\ + - '%c', that prints a character,\n\ + - '%c', that prints a string,\n\ + - '%c', '%c', that print an integer,\n\ + - '%c', '%c', '%c', '%c', that print an unsigned (nonnegative) integer,\n\ + - '%c', '%c', that print a floating-point number in scientific notation,\n\ + - '%c', '%c', that print a floating-point number without an exponent,\n\ + - '%c', '%c', that print a floating-point number in general notation,\n\ + - '%c', '%c', that print a floating-point number in hexadecimal notation.\n\ +Additionally there is the directive '%s', that prints a single '%c'.\n"), + "%", "%m$", "m", + '#', '0', '-', ' ', '+', + '.', + 'c', + 's', + 'i', 'd', + 'u', 'o', 'x', 'X', + 'e', 'E', 'f', 'F', 'g', 'G', 'a', 'A', + "%%", '%'); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +If a directive specifies the argument by its number ('%s' notation),\n\ +all directives that consume an argument must do so.\n"), + "%m$"); + printf ("\n"); + /* TRANSLATORS: Most of the placeholders expand to 2 characters. + The last placeholder expands to 4 characters. */ + printf (_("\ +The escape sequences are:\n\ +\n\ + %s backslash\n\ + %s alert (BEL)\n\ + %s backspace (BS)\n\ + %s form feed (FF)\n\ + %s new line (LF)\n\ + %s carriage return (CR)\n\ + %s horizontal tab (HT)\n\ + %s vertical tab (VT)\n\ + %s octal number with 1 to 3 octal digits\n"), + "\\\\", "\\a", "\\b", "\\f", "\\n", "\\r", "\\t", "\\v", + "\\nnn"); + printf ("\n"); + printf (_("\ +Environment variables:\n")); + printf ("\n"); + /* xgettext: no-wrap */ + printf (_("\ +The translation of the format string is looked up in the translation domain\n\ +given by the environment variable %s.\n"), + "TEXTDOMAIN"); + /* xgettext: no-wrap */ + printf (_("\ +It is looked up in the catalogs directory given by the environment variable\n\ +%s or, if not present, in the default catalogs directory.\n\ +This binary is configured to use the default catalogs directory:\n\ +%s\n"), + "TEXTDOMAINDIR", + getenv ("IN_HELP2MAN") == NULL ? relocate (LOCALEDIR) : "@localedir@"); + printf ("\n"); + /* TRANSLATORS: The first placeholder is the web address of the Savannah + project of this package. The second placeholder is the bug-reporting + email address for this package. Please add _another line_ saying + "Report translation bugs to <...>\n" with the address for translation + bugs (typically your translation team's web or email address). */ + printf (_("\ +Report bugs in the bug tracker at <%s>\n\ +or by email to <%s>.\n"), + "https://savannah.gnu.org/projects/gettext", + "bug-gettext@gnu.org"); + } + + exit (status); +} diff --git a/gettext-tools/doc/Makefile.am b/gettext-tools/doc/Makefile.am index f84751609..e725e430f 100644 --- a/gettext-tools/doc/Makefile.am +++ b/gettext-tools/doc/Makefile.am @@ -76,6 +76,8 @@ gettext_TEXINFOS = \ lang-sh.texi \ $(top_srcdir)/../gettext-runtime/doc/rt-gettext.texi \ $(top_srcdir)/../gettext-runtime/doc/rt-ngettext.texi \ + $(top_srcdir)/../gettext-runtime/doc/rt-printf_gettext.texi \ + $(top_srcdir)/../gettext-runtime/doc/rt-printf_ngettext.texi \ $(top_srcdir)/../gettext-runtime/doc/rt-envsubst.texi \ lang-bash.texi \ lang-gawk.texi \ diff --git a/gettext-tools/doc/gettext.texi b/gettext-tools/doc/gettext.texi index fcbb42130..6cb70fbc8 100644 --- a/gettext-tools/doc/gettext.texi +++ b/gettext-tools/doc/gettext.texi @@ -80,6 +80,8 @@ * msgunfmt: (gettext)msgunfmt Invocation. Uncompile MO file into PO file. * msguniq: (gettext)msguniq Invocation. Unify duplicates for PO file. * ngettext: (gettext)ngettext Invocation. Translate a message with plural. +* printf_gettext: (gettext)printf_gettext Invocation. Translate a format string. +* printf_ngettext: (gettext)printf_ngettext Invocation. Translate a format string with plural. * xgettext: (gettext)xgettext Invocation. Extract strings into a PO file. * ISO639: (gettext)Language Codes. ISO 639 language codes. * ISO3166: (gettext)Country Codes. ISO 3166 country codes. @@ -466,6 +468,8 @@ sh - Shell Script * gettext Invocation:: Invoking the @code{gettext} program * ngettext Invocation:: Invoking the @code{ngettext} program * envsubst Invocation:: Invoking the @code{envsubst} program +* printf_gettext Invocation:: Invoking the @code{printf_gettext} program +* printf_ngettext Invocation:: Invoking the @code{printf_ngettext} program * eval_gettext Invocation:: Invoking the @code{eval_gettext} function * eval_ngettext Invocation:: Invoking the @code{eval_ngettext} function * eval_pgettext Invocation:: Invoking the @code{eval_pgettext} function diff --git a/gettext-tools/doc/lang-sh.texi b/gettext-tools/doc/lang-sh.texi index e068a8f60..47d7f422b 100644 --- a/gettext-tools/doc/lang-sh.texi +++ b/gettext-tools/doc/lang-sh.texi @@ -70,6 +70,8 @@ An example is available in the @file{examples} directory: @code{hello-sh}. * gettext.sh:: Contents of @code{gettext.sh} * gettext Invocation:: Invoking the @code{gettext} program * ngettext Invocation:: Invoking the @code{ngettext} program +* printf_gettext Invocation:: Invoking the @code{printf_gettext} program +* printf_ngettext Invocation:: Invoking the @code{printf_ngettext} program * envsubst Invocation:: Invoking the @code{envsubst} program * eval_gettext Invocation:: Invoking the @code{eval_gettext} function * eval_ngettext Invocation:: Invoking the @code{eval_ngettext} function @@ -238,6 +240,16 @@ Note: @code{xgettext} supports only the three-arguments form of the @code{ngettext} invocation, where no options are present and the @var{textdomain} is implicit, from the environment. +@node printf_gettext Invocation +@subsubsection Invoking the @code{printf_gettext} program + +@include rt-printf_gettext.texi + +@node printf_ngettext Invocation +@subsubsection Invoking the @code{printf_ngettext} program + +@include rt-printf_ngettext.texi + @node envsubst Invocation @subsubsection Invoking the @code{envsubst} program