[PATCH v3 next 15/17] tools/nolibc/printf: Add support for zero padding and field precision

From: david . laight . linux

Date: Mon Feb 23 2026 - 05:59:59 EST


From: David Laight <david.laight.linux@xxxxxxxxx>

Includes support for variable field widths (eg "%*.*d").

Zero padding is limited to 31 zero characters.
This is wider than the largest numeric field so shouldn't be a problem.

All the standard printf formats are now supported except octal
and floating point.

Add tests for new features

Acked-by: Willy Tarreau <w@xxxxxx>
Signed-off-by: David Laight <david.laight.linux@xxxxxxxxx>
---

Changes for v3:
- Formerly patch 8
- Extra comments.
- Adjust offsets in outbuf[], support adding 31 '0' digits while
still allowing enough room for octal support to be added and keeping
outbuf[] at a nice round 56 bytes.

Changes for v2:
- These changes were previously in patch 9.
However you need to apply the old patch 10 to get anything like
the same source. The files then more of less match apart from 'c'
being renamed 'ch' and the 'magic' #defines.

tools/include/nolibc/stdio.h | 77 ++++++++++++++++----
tools/testing/selftests/nolibc/nolibc-test.c | 18 ++++-
2 files changed, 79 insertions(+), 16 deletions(-)

diff --git a/tools/include/nolibc/stdio.h b/tools/include/nolibc/stdio.h
index 482e5b143c86..cc2870a6a7cb 100644
--- a/tools/include/nolibc/stdio.h
+++ b/tools/include/nolibc/stdio.h
@@ -292,12 +292,10 @@ int fseek(FILE *stream, long offset, int whence)


/* printf(). Supports most of the normal integer and string formats.
- * - %[#-+ 0][width][{l,t,z,ll,L,j,q}]{c,d,i,u,x,X,p,s,m,%}
+ * - %[#0-+ ][width|*[.precision|*}][{l,t,z,ll,L,j,q}]{c,d,i,u,x,X,p,s,m,%}
* - %% generates a single %
* - %m outputs strerror(errno).
* - %X outputs a..f the same as %x.
- * - The modifiers [-0] are currently ignored.
- * - No support for precision or variable widths.
* - No support for floating point or wide characters.
* - Invalid formats are copied to the output buffer.
*
@@ -343,9 +341,9 @@ int __nolibc_printf(__nolibc_printf_cb cb, void *state, const char *fmt, va_list
char ch;
unsigned long long v;
long long signed_v;
- int written, width, len;
+ int written, width, precision, len;
unsigned int flags, ch_flag;
- char outbuf[2 + 22 + 1];
+ char outbuf[2 + 31 + 22 + 1];
char *out;
const char *outstr;
unsigned int sign_prefix;
@@ -378,12 +376,24 @@ int __nolibc_printf(__nolibc_printf_cb cb, void *state, const char *fmt, va_list
flags |= ch_flag;
}

- /* width */
- while (ch >= '0' && ch <= '9') {
- width *= 10;
- width += ch - '0';
-
- ch = *fmt++;
+ /* Width and precision */
+ for (;; ch = *fmt++) {
+ if (ch == '*') {
+ precision = va_arg(args, unsigned int);
+ ch = *fmt++;
+ } else {
+ for (precision = 0; ch >= '0' && ch <= '9'; ch = *fmt++)
+ precision = precision * 10 + (ch - '0');
+ }
+ if (_NOLIBC_PF_FLAGS_CONTAIN(flags, '.'))
+ break;
+ width = precision;
+ if (ch != '.') {
+ /* Default precision for strings */
+ precision = INT_MAX;
+ break;
+ }
+ flags |= _NOLIBC_PF_FLAG('.');
}

/* Length modifier.
@@ -446,6 +456,9 @@ int __nolibc_printf(__nolibc_printf_cb cb, void *state, const char *fmt, va_list
outstr = (const char *)(uintptr_t)v;
if (!outstr) {
outstr = "(null)";
+ /* Match glibc, nothing output if precision too small */
+ len = precision >= 6 ? 6 : 0;
+ goto do_output;
}
goto do_strlen_output;
}
@@ -467,11 +480,11 @@ int __nolibc_printf(__nolibc_printf_cb cb, void *state, const char *fmt, va_list
}

/* The value is converted offset into the buffer so that
- * the sign/prefix can be added in front.
+ * 31 zero pad characters and the sign/prefix can be added in front.
* The longest digit string is 22 + 1 for octal conversions, the
* space is reserved even though octal isn't currently supported.
*/
- out = outbuf + 2;
+ out = outbuf + 2 + 31;

if (v == 0) {
/* There are special rules for zero. */
@@ -481,6 +494,11 @@ int __nolibc_printf(__nolibc_printf_cb cb, void *state, const char *fmt, va_list
len = 5;
goto do_output;
}
+ if (!precision) {
+ /* Explicit %nn.0d, no digits output */
+ len = 0;
+ goto prepend_sign;
+ }
/* All other formats (including "%#x") just output "0". */
out[0] = '0';
len = 1;
@@ -499,6 +517,35 @@ int __nolibc_printf(__nolibc_printf_cb cb, void *state, const char *fmt, va_list
}
}

+ /* Add zero padding */
+ if (_NOLIBC_PF_FLAGS_CONTAIN(flags, '0', '.')) {
+ if (!_NOLIBC_PF_FLAGS_CONTAIN(flags, '.')) {
+ if (_NOLIBC_PF_FLAGS_CONTAIN(flags, '-'))
+ /* Left justify overrides zero pad */
+ goto prepend_sign;
+ /* eg "%05d", Zero pad to field width less sign.
+ * Note that precision can end up negative so all
+ * the variables have to be 'signed int'.
+ */
+ precision = width;
+ if (sign_prefix) {
+ precision--;
+ if (sign_prefix >= 256)
+ precision--;
+ }
+ }
+ if (precision > 31)
+ /* Don't run off the start of outbuf[], arbitrary limit
+ * longer than the longest number field. */
+ precision = 31;
+ for (; len < precision; len++) {
+ /* Stop gcc generating horrid code and memset(). */
+ _NOLIBC_OPTIMIZER_HIDE_VAR(len);
+ *--out = '0';
+ }
+ }
+
+prepend_sign:
/* Add the 0, 1 or 2 ("0x") sign/prefix characters at the front. */
for (; sign_prefix; sign_prefix >>= 8) {
/* Force gcc to increment len inside the loop. */
@@ -529,8 +576,8 @@ int __nolibc_printf(__nolibc_printf_cb cb, void *state, const char *fmt, va_list
goto do_output;

do_strlen_output:
- /* Open coded strlen() (slightly smaller). */
- for (len = 0;; len++)
+ /* Open coded strnlen() (slightly smaller). */
+ for (len = 0; len < precision; len++)
if (!outstr[len])
break;

diff --git a/tools/testing/selftests/nolibc/nolibc-test.c b/tools/testing/selftests/nolibc/nolibc-test.c
index cc59c0116855..e0cfe8d12ed1 100644
--- a/tools/testing/selftests/nolibc/nolibc-test.c
+++ b/tools/testing/selftests/nolibc/nolibc-test.c
@@ -1838,10 +1838,12 @@ static int run_printf(int min, int max)
CASE_TEST(signed_min); EXPECT_VFPRINTF(1, "-2147483648", "%i", (~0u >> 1) + 1); break;
CASE_TEST(unsigned_max); EXPECT_VFPRINTF(1, "4294967295", "%u", ~0u); break;
CASE_TEST(char); EXPECT_VFPRINTF(1, "c", "%c", 'c'); break;
+ CASE_TEST(char); EXPECT_VFPRINTF(1, "|c|d| e|", "|%c|%.0c|%4c|", 'c', 'd', 'e'); break;
CASE_TEST(hex_nolibc); EXPECT_VFPRINTF(is_nolibc, "|f|d|", "|%x|%X|", 0xf, 0xd); break;
CASE_TEST(hex_libc); EXPECT_VFPRINTF(!is_nolibc, "|f|D|", "|%x|%X|", 0xf, 0xd); break;
CASE_TEST(pointer); EXPECT_VFPRINTF(1, "0x1", "%p", (void *) 0x1); break;
- CASE_TEST(pointer_NULL); EXPECT_VFPRINTF(1, "(nil)", "%p", (void *)0); break;
+ CASE_TEST(pointer_NULL); EXPECT_VFPRINTF(1, "|(nil)|(nil)|", "|%p|%.4p|", (void *)0, (void *)0); break;
+ CASE_TEST(string_NULL); EXPECT_VFPRINTF(1, "|(null)||(null)|", "|%s|%.5s|%.6s|", (void *)0, (void *)0, (void *)0); break;
CASE_TEST(percent); EXPECT_VFPRINTF(1, "a%d42%69%", "a%%d%d%%%d%%", 42, 69); break;
CASE_TEST(perc_qual); EXPECT_VFPRINTF(1, "a%d2", "a%-14l%d%d", 2); break;
CASE_TEST(invalid); EXPECT_VFPRINTF(1, "a%12yx3%y42%P", "a%12yx%d%y%d%P", 3, 42); break;
@@ -1850,11 +1852,25 @@ static int run_printf(int min, int max)
CASE_TEST(uintmax_max); EXPECT_VFPRINTF(1, "18446744073709551615", "%ju", ~0ULL); break;
CASE_TEST(truncation); EXPECT_VFPRINTF(1, "012345678901234567890123456789", "%s", "012345678901234567890123456789"); break;
CASE_TEST(string_width); EXPECT_VFPRINTF(1, " 1", "%10s", "1"); break;
+ CASE_TEST(string_trunc); EXPECT_VFPRINTF(1, " 12345", "%10.5s", "1234567890"); break;
CASE_TEST(number_width); EXPECT_VFPRINTF(1, " 1", "%10d", 1); break;
CASE_TEST(number_left); EXPECT_VFPRINTF(1, "|-5 |", "|%-8d|", -5); break;
CASE_TEST(string_align); EXPECT_VFPRINTF(1, "|foo |", "|%-8s|", "foo"); break;
CASE_TEST(width_trunc); EXPECT_VFPRINTF(1, " 1", "%30d", 1); break;
+ CASE_TEST(width_tr_lft); EXPECT_VFPRINTF(1, "1 ", "%-30d", 1); break;
+ CASE_TEST(number_pad); EXPECT_VFPRINTF(1, "0000000005", "%010d", 5); break;
+ CASE_TEST(number_pad); EXPECT_VFPRINTF(1, "|0000000005|0x1234|", "|%010d|%#01x|", 5, 0x1234); break;
+ CASE_TEST(num_pad_neg); EXPECT_VFPRINTF(1, "-000000005", "%010d", -5); break;
+ CASE_TEST(num_pad_hex); EXPECT_VFPRINTF(1, "00fffffffb", "%010x", -5); break;
+ CASE_TEST(num_pad_trunc);EXPECT_VFPRINTF(is_nolibc, " 0000000000000000000000000000005", "%035d", 5); break; /* max 31 '0' can be added */
+ CASE_TEST(num_p_tr_libc);EXPECT_VFPRINTF(!is_nolibc, "00000000000000000000000000000000005", "%035d", 5); break;
+ CASE_TEST(number_prec); EXPECT_VFPRINTF(1, " 00005", "%10.5d", 5); break;
+ CASE_TEST(num_prec_neg); EXPECT_VFPRINTF(1, " -00005", "%10.5d", -5); break;
+ CASE_TEST(num_prec_var); EXPECT_VFPRINTF(1, " -00005", "%*.*d", 10, 5, -5); break;
+ CASE_TEST(num_0_prec_0); EXPECT_VFPRINTF(1, "|| |+||||", "|%.0d|% .0d|%+.0d|%.0u|%.0x|%#.0x|", 0, 0, 0, 0, 0, 0); break;
CASE_TEST(hex_alt); EXPECT_VFPRINTF(1, "|0x1| 0x2| 0|", "|%#x|%#5x|%#5x|", 1, 2, 0); break;
+ CASE_TEST(hex_alt_prec); EXPECT_VFPRINTF(1, "| 0x02|0x03| 0x123|", "|%#5.2x|%#04x|%#6.2x|", 2, 3, 0x123); break;
+ CASE_TEST(hex_0_alt); EXPECT_VFPRINTF(1, "|0|0000| 00|", "|%#x|%#04x|%#5.2x|", 0, 0, 0); break;
CASE_TEST(errno); EXPECT_VFPRINTF(is_nolibc, "22:errno=22 ", "%d:%-12m", errno=22); break;
CASE_TEST(errno-neg); EXPECT_VFPRINTF(is_nolibc, "-22: errno=-22", "%d:%12m", errno=-22); break;
CASE_TEST(scanf); EXPECT_ZR(1, test_scanf()); break;
--
2.39.5