[git commit] awk: fix printing of very large numbers with printf("%d")

Denys Vlasenko vda.linux at googlemail.com
Thu Jul 2 02:58:08 UTC 2026


commit: https://git.busybox.net/busybox/commit/?id=a0667b2def4c05aeb333448195127392a22c21a8
branch: https://git.busybox.net/busybox/log/?h=master

function                                             old     new   delta
awk_printf                                          1086    1349    +263
.rodata                                           107108  107121     +13
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 2/0 up/down: 276/0)             Total: 276 bytes

Signed-off-by: Denys Vlasenko <vda.linux at googlemail.com>
---
 editors/awk.c       | 244 +++++++++++++++++++++++++++++++++-------------------
 testsuite/awk.tests |  24 +++++-
 2 files changed, 179 insertions(+), 89 deletions(-)

diff --git a/editors/awk.c b/editors/awk.c
index 23bfb75a0..2db19c35e 100644
--- a/editors/awk.c
+++ b/editors/awk.c
@@ -2414,18 +2414,6 @@ static int awk_getline(rstream *rsm, var *v)
 	return retval;
 }
 
-/* xasprintf with support of supplying width and/or precision */
-//TODO: deinline this somehow...
-#define xasprintf_width_prec(format, stars, width, prec, ...) \
-({ \
-	char *xstr; \
-	/*bb_error_msg("stars:%d w:%d p:%d FMT:'%s'", stars, width, prec, format);*/ \
-	if (stars > 1)       xstr = xasprintf(format, width, prec, __VA_ARGS__); \
-	else if (stars == 1) xstr = xasprintf(format, width, __VA_ARGS__); \
-	else                 xstr = xasprintf(format, __VA_ARGS__); \
-	xstr; \
-})
-
 /* formatted output into an allocated buffer, return ptr to buffer */
 #if !ENABLE_FEATURE_AWK_GNU_EXTENSIONS
 # define awk_printf(a, b) awk_printf(a)
@@ -2453,147 +2441,227 @@ static char *awk_printf(node *n, size_t *len)
 		char c;
 		char sv;
 		var *arg;
-		/* s is the next string to append to res_buf */
-		/* it can be either a literal trailing segment of fmt (if c == NUL), or allocated result of the formatting */
+		/* s is the next string to append to res_buf.
+		 * it can be either a literal trailing segment of fmt (then c == NUL),
+		 * or allocated result of the formatting */
 		char *s;
 		size_t slen;
-		int stars;
-		int width = width;
-		int prec = prec;
+		char *after_fmt;
+		int starred;
+		int width;
+		int prec;
+		enum { MAX_FMT_LEN = sizeof("%+- 0#'99.99llx plus paranoia") + 2 * sizeof(int)*3 };
+		char baked_format[MAX_FMT_LEN];
+		char *baked_cur;
+		char *use_fmt;
+		const char *diouxX;
 
 		/* Find end of the next format spec, or end of line */
 		s = fmt_cur;
-		stars = 0;
+//bb_error_msg("s:'%s'", s);
 		while (1) {
 			c = *fmt_cur;
-			if (!c) /* no percent chars found at all */
+			if (!c) { /* no percent chars found at all */
 				goto nul;
+			}
 			fmt_cur++;
 			if (c == '%')
 				break;
 		}
 		/* we are past % in "....%..." */
 		c = *fmt_cur;
-		if (!c) /* "....%" */
-			goto nul;
 		if (c == '%') { /* "....%%...." */
 			slen = fmt_cur - s;
-			s = xstrndup(s, slen);
 			fmt_cur++;
-			goto append; /* append "....%" part verbatim */
+			goto append_slen; /* append "....%" part verbatim */
+		}
+
+		/* find trailing %FMT letter */
+		starred = 0;
+		after_fmt = fmt_cur;
+		while (!isalpha(c) && c != '\0') {
+			starred |= (c == '*');
+			c = *++after_fmt;
+		}
+		if (!c) { /* unterminated "....%FMT" */
+			fmt_cur = after_fmt;
+ nul:
+			slen = fmt_cur - s;
+			goto tail; /* append remaining string, exit loop */
+		}
+		diouxX = strchr("diouxX", c);
+		if (!(diouxX || strchr("cs""eEfFgGaA", c))
+		 || (after_fmt - fmt_cur) >= MAX_FMT_LEN
+		) {
+			/* unknown, or overlong "....%FMT...." */
+			/* append "....%<notletters><letter>" part verbatim */
+//this works (unknown conversion char means "print %FMT verbatim"):
+//  awk 'BEGIN { printf "%W\n"; }
+//since we check for this early, even this works:
+//  awk 'BEGIN { printf "%*W\n"; }
+//whereas GNU awk 5.3.0 says "not enough arguments to satisfy format string"
+			fmt_cur = after_fmt + 1;
+			slen = fmt_cur - s;
+			goto append_slen;
 		}
 
+		if (*s != '%') {
+			/* the %FMT does not start immediately at the beginning of s */
+			if (starred || diouxX) {
+				/* It is a "%*s"-like starred %FMT, or an integer conversion.
+				 * In this case we will use fixed baked_format[].
+				 * Which means "literal_prefix%FMT" can't be used as one piece.
+				 * Append "literal_prefix" to res_buf.
+				 */
+				fmt_cur--;
+				slen = fmt_cur - s;
+ append_slen:
+				/* append "....%" part verbatim */
+				//s = xstrndup(s, slen);
+				//goto append;
+				/* a bit bloaty, but avoids dup+free: */
+				res_buf = xrealloc(res_buf, res_len + slen + 1);
+				strncpy(res_buf + res_len, s, slen); /* s can't contain NULs here */
+				res_len += slen;
+				continue;
+			}
+		}
+
+		/* NUL-terminate "...%FMT" */
+		after_fmt++;
+		if (*after_fmt && *after_fmt != '%') {
+			// optimization for common cases:
+			// printf "Number:%d\n" (one extra char between %FMT and EOL)
+			// printf "String:'%s'\n" (two extra chars)
+			// - we can handle a small tail of literals after %FMT in one pass!
+			// (careful: do not overflow baked_format[] array!)
+			after_fmt++;
+			if (*after_fmt && *after_fmt != '%')
+				after_fmt++;
+		}
+		sv = *after_fmt;
+		*after_fmt = '\0';
+
 		/* flags */
-		while (c && strchr("+- 0#'", c))
+		baked_cur = baked_format;
+		*baked_cur++ = '%';
+		c = *fmt_cur;
+		while (strchr("+- 0#'", c) && (baked_cur - baked_format) <= 6) {
+			*baked_cur++ = c;
 			c = *++fmt_cur;
+		}
 
 		/* width */
+		width = -1;
 		if (c == '*') {
-			stars = 1;
 			width = (int)getvar_i(evaluate(nextarg(&n), TMPVAR));
+//bb_error_msg("*width=%d", width);
 			c = *++fmt_cur;
 			//if (isdigit(c)) /* we do not support "%*2$d" format */
 			//	syntax_error("invalid format specifier");
+		} else if (isdigit(c)) {
+			width = atoi(fmt_cur);
+			do
+				c = *++fmt_cur;
+			while (isdigit(c));
 		}
-		while (isdigit(c))
-			c = *++fmt_cur;
 
 		/* precision? */
+		prec = -1;
 		if (c == '.') {
 			c = *++fmt_cur;
 			if (c == '*') {
 				prec = (int)getvar_i(evaluate(nextarg(&n), TMPVAR));
-				if (++stars == 1)
-					width = prec;
+//bb_error_msg("*prec=%d", prec);
 				c = *++fmt_cur;
 				//if (isdigit(c)) /* we do not support "%5.*2$d" format */
 				//	syntax_error("invalid format specifier");
-			}
-			//while (isdigit(c))
-			//	c = *++fmt_cur;
-			//^^^^ unnecessary, the while () below skips everything to format specifier letter
-		}
-
-		// The fact that we skip everything
-		// until we see a letter could allow
-		// even this format to maybe work: "%*.*2$f"
-		// ... sans the fact that GNU awk 5.3.0
-		// does NOT advance the "next positional param"
-		// internal variable when it sees *N$, and therefore
-		// "%*.*2$f" is printing param #2, not param #3:
-		// awk 'BEGIN { printf "[%*.*f]\n",10,5,44; }'     [  44.00000]
-		// awk 'BEGIN { printf "[%*.*2$f]\n",10,5,44; }'   [   5.00000] <- THIS
-		// awk 'BEGIN { printf "[%*1$.*f]\n",10,5,44; }'   [5.0000000000]
-		// awk 'BEGIN { printf "[%*1$.*2$f]\n",10,5,44; }' [  10.00000]
-		// whereas glibc prints param #3, but forgets to set width (!).
-		//
-		// The behavior is undefined by standards anyway,
-		// let's use minimal valid code.
-		while (1) {
-			if (isalpha(c))
-				break;
-			c = *++fmt_cur;
-			if (!c) { /* "....%...." and no letter found after % */
-				/* Example: awk 'BEGIN { printf "^^^%^^^\n"; }' */
- nul:
-				slen = fmt_cur - s;
-				goto tail; /* print remaining string, exit loop */
+			} else if (isdigit(c)) {
+				prec = atoi(fmt_cur);
+				//do
+				//	c = *++fmt_cur;
+				//while (isdigit(c));
+				//^^^^ unnecessary, while () below skips everything to format type letter
 			}
 		}
 
-		/* we are at A in "....%...A..." */
+		while (!isalpha(c))
+			c = *++fmt_cur;
+		/* we are at A in "....%fmtA..." */
+
 		arg = evaluate(nextarg(&n), TMPVAR);
 
-		/* Result can be arbitrarily long. Example:
-		 *  printf "%99999s", "BOOM"
-		 */
-		sv = *++fmt_cur;
-		*fmt_cur = '\0';
+		use_fmt = s;
+//bb_error_msg("w:%d p:%d conv:'%c' fmt_cur:%p after_fmt:%p", width, prec, c, fmt_cur, after_fmt);
+		if (starred || diouxX) {
+			if (width >= 0
+			 || (starred && (c == 's' || c == 'c'))
+//GNU awk 5.3.0 compat: "%*s",-10,"A" works like "%-10s","A"
+			) {
+				baked_cur += sprintf(baked_cur, "%d", width);
+			}
+			if (diouxX) {
+				sprintf(baked_cur, ".0f%s", fmt_cur + 1);
+			} else {
+				if (prec >= 0)
+					baked_cur += sprintf(baked_cur, ".%u", prec);
+				sprintf(baked_cur, "%s", fmt_cur);
+			}
+			use_fmt = baked_format;
+//bb_error_msg("baked_format:'%s'", baked_format);
+		}
 
 		if (c == 'c') {
 			char cc = is_numeric(arg) ? getvar_i(arg) : *getvar_s(arg);
-			char *r = xasprintf_width_prec(s, stars, width, prec, cc ? cc : '^' /* else strlen will be wrong */);
+			char *r = xasprintf(use_fmt, cc ? cc : '^' /* else strlen will be wrong */);
 			slen = strlen(r);
 			if (cc == '\0') { /* if cc is NUL, re-format the string with it */
-				free(r);
-				r = xasprintf_width_prec(s, stars, width, prec, cc);
+				sprintf(r, use_fmt, cc);
 			}
 			s = r;
 		} else {
 			if (c == 's') {
 				const char *cs = getvar_s(arg);
-				s = xasprintf_width_prec(s, stars, width, prec, cs);
-			} else {
+//bb_error_msg("format_s:'%s'", use_fmt);
+				s = xasprintf(use_fmt, cs);
+			} else { /* numeric conversion */
 				double d = getvar_i(arg);
-				if (strchr("diouxX", c)) {
-//TODO: make it wider here (%x -> %llx etc)?
-//Can even print the value into a temp string with %.0f,
-//then replace diouxX with s and print that string.
-//This will correctly print even very large numbers,
-//but some replacements are not equivalent:
-//%09d -> %09s: breaks zero-padding;
-//%+d -> %+s: won't prepend +; etc
-					s = xasprintf_width_prec(s, stars, width, prec, (int)d);
-				} else if (strchr("eEfFgGaA", c)) {
-					s = xasprintf_width_prec(s, stars, width, prec, d);
+//bb_error_msg("d0:%f", d);
+				if (diouxX)
+					d = trunc(d);
+//bb_error_msg("d1:%f", d);
+				if ((c|0x20) == 'x' || (c == 'u' && d < 0)) {
+					sprintf(baked_cur, "ll%s", fmt_cur);
+					// -1.9 => -1 => integer (0xfff..fff)
+					// => interpret as unsigned integer
+					// => convert to double as POSITIVE integer
+					// awk 'BEGIN { printf "%u\n",-1.9; }': 18446744073709551615
+					// awk 'BEGIN { printf "%x\n",-1.9; }': ffffffffffffffff
+					s = xasprintf(use_fmt, (unsigned long long)d);
 				} else {
-					/* gawk 5.1.1 printf("%W") prints "%W", does not error out */
-					s = xstrndup(s, fmt_cur - s);
+					s = xasprintf(use_fmt, d);
 				}
 			}
 			slen = strlen(s);
 		}
-		*fmt_cur = sv;
- append:
+		*after_fmt = sv;
+		fmt_cur = after_fmt;
+//bb_error_msg("after_fmt:'%s'", after_fmt);
+
+//bb_error_msg("slen:%d", slen);
 		if (res_len == 0) {
+//bb_error_msg("initial:'%.*s'", slen, s);
 			res_buf = s;
 			res_len = slen;
 			continue;
 		}
  tail:
-		res_buf = xrealloc(res_buf, res_len + slen + 1);
-		strcpy(res_buf + res_len, s);
-		res_len += slen;
+		if (slen != 0 || res_len == 0) {
+//bb_error_msg("appending:'%.*s'", slen, s);
+			res_buf = xrealloc(res_buf, res_len + slen + 1);
+			((char*)mempcpy(res_buf + res_len, s, slen))[0] = '\0';
+			res_len += slen;
+		}
 		if (!c) /* s is NOT allocated and this is the last part of string? */
 			break;
 		free(s);
diff --git a/testsuite/awk.tests b/testsuite/awk.tests
index 06cc79569..d0b28cf36 100755
--- a/testsuite/awk.tests
+++ b/testsuite/awk.tests
@@ -61,8 +61,30 @@ testing "awk %*c width with NUL" \
 testing "awk %-*c width with NUL" \
 "awk 'BEGIN { printf \"%-*c\", 5, 0 }'" "\0    " "" ""
 
+testing "awk %W%d - W is unknown format specifier" \
+"awk 'BEGIN { printf \"[%W%d]\n\", 42; }'" "[%W42]\n" "" ""
+
+# do not truncate large integer outout
+# (the ...008 instead of ...000 is due to limited precision of 64-bit floats)
+testing "awk %d 99999999999999000.1234" \
+"awk 'BEGIN { printf \"%d\n\", 99999999999999000.1234; }'" "99999999999999008\n" "" ""
+
+# .precision on integer formatting allowed and ignored
+testing "awk %.3d 99999999999999000.1234" \
+"awk 'BEGIN { printf \"%.3d\n\", 99999999999999000.1234; }'" "99999999999999008\n" "" ""
+testing "awk %.*d 3 99999999999999000.1234" \
+"awk 'BEGIN { printf \"%.*d\n\", 3, 99999999999999000.1234; }'" "99999999999999008\n" "" ""
+
+# fractioals are rounded towards 0
+testing "awk %d -1.9" \
+"awk 'BEGIN { printf \"%d\n\", -1.9; }'" "-1\n" "" ""
+testing "awk %u -1.9" \
+"awk 'BEGIN { printf \"%u\n\", -1.9; }'" "18446744073709551615\n" "" ""
+testing "awk %x -1.9" \
+"awk 'BEGIN { printf \"%x\n\", -1.9; }'" "ffffffffffffffff\n" "" ""
+
 # 4294967295 = 0xffffffff
-testing "awk bitwise op"  "awk '{ print or(4294967295,1) }'" "4294967295\n" "" "\n"
+testing "awk bitwise op" "awk '{ print or(4294967295,1) }'" "4294967295\n" "" "\n"
 
 # we were testing for a non-empty body when deciding if a function was
 # defined or not. The testcase below caused:


More information about the busybox-cvs mailing list