/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
/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
/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
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
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
$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
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
# 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
+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
## 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
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
--- /dev/null
+@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.
--- /dev/null
+@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.
## 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
# 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 \
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 \
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$$//'`; \
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
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=''; \
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=''; \
--- /dev/null
+[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.
--- /dev/null
+[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.
# 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:
src/envsubst.c
src/gettext.c
src/ngettext.c
+src/printf-command.c
+src/printf_gettext.c
+src/printf_ngettext.c
## 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
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).
# 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
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
+
+#include <config.h>
+
+/* Specification. */
+#include "printf-command.h"
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <uchar.h>
+
+#include <error.h>
+#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
+ {
+ /* <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html>
+ 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:
+ /* <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html>
+ 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:
+ /* <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html>
+ 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);
+}
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
+
+/* This file implements the bulk of the POSIX:2024 specification for the 'printf'
+ command:
+ <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html>
+ <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap05.html#tag_05>
+ 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 <stddef.h>
+
+#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 */
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 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 <config.h>
+#endif
+
+#include <getopt.h>
+#include <locale.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <error.h>
+#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);
+}
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 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 <config.h>
+#endif
+
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <locale.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <error.h>
+#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);
+}
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 \
* 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.
* 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
* 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
@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