[PATCH v2] awk: add '*' as a valid specifier for width and precision in printf

Anubhav Kokane via busybox <[email protected]> Tue, 30 Jun 2026 06:32:17 +0000
Newsgroups gmane.linux.busybox
Message-ID <[email protected]>
Parse flags, width, precision into a local fmt_buf, resolving each
'*' via nextarg() and substituting the values into fmt_buf.
If '*' is present, a new format string is built using literal
prefix and fmt_buf. And if '*' is absent original format string
is used without any changes.

Signed-off-by: Anubhav Kokane <[email protected]>
---
 editors/awk.c       | 80 ++++++++++++++++++++++++++++++++++++++++-----
 testsuite/awk.tests | 22 +++++++++++++
 2 files changed, 94 insertions(+), 8 deletions(-)

diff --git a/editors/awk.c b/editors/awk.c
index dd8f4ac42..22a606c1d 100644
--- a/editors/awk.c
+++ b/editors/awk.c
@@ -2443,9 +2443,17 @@ static char *awk_printf(node *n, size_t *len)
 		char sv;
 		var *arg;
 		size_t slen;
+		char fmt_buf[64];
+		char *fmt_str;
+		char *out;
+		char *p;
+		int has_star;
+		int w;
 
 		/* Find end of the next format spec, or end of line */
 		s = f;
+		out = fmt_buf;
+		has_star = 0;
 		while (1) {
 			c = *f;
 			if (!c) /* no percent chars found at all */
@@ -2456,6 +2464,8 @@ static char *awk_printf(node *n, size_t *len)
 		}
 		/* we are past % in "....%..." */
 		c = *f;
+		p = f - 1; /* points to '%' */
+		*out++ = '%';
 		if (!c) /* "....%" */
 			goto nul;
 		if (c == '%') { /* "....%%...." */
@@ -2464,11 +2474,50 @@ static char *awk_printf(node *n, size_t *len)
 			f++;
 			goto append; /* print "....%" part verbatim */
 		}
+
+		/* flags */
+		while (c && strchr("+- 0#", c)) {
+			*out++ = c;
+			c = *++f;
+		}
+
+		/* width */
+		if (c == '*') {
+			has_star = 1;
+			w = (int)getvar_i(evaluate(nextarg(&n), TMPVAR));
+			out += sprintf(out, "%d", w);
+			c = *++f;
+			if (c >= '0' && c <= '9') /* invalidate "^^^%*6d^^^" */
+				syntax_error("invalid format specifier");
+		} else {
+			while (c >= '0' && c <= '9') {
+				*out++ = c;
+				c = *++f;
+			}
+		}
+
+		/* precision */
+		if (c == '.') {
+			*out++ = '.';
+			c = *++f;
+			if (c == '*') {
+				has_star = 1;
+				w = (int)getvar_i(evaluate(nextarg(&n), TMPVAR));
+				out += sprintf(out, "%d", w);
+				c = *++f;
+				if (c >= '0' && c <= '9') /* invalidate "^^^%5.*8f^^^" */
+					syntax_error("invalid format specifier");
+			} else {
+				while (c >= '0' && c <= '9') {
+					*out++ = c;
+					c = *++f;
+				}
+			}
+		}
+
 		while (1) {
 			if (isalpha(c))
 				break;
-			if (c == '*') /* gawk supports %*d and %*.*f, we don't... */
-				syntax_error("%*x formats are not supported");
 			c = *++f;
 			if (!c) { /* "....%...." and no letter found after % */
 				/* Example: awk 'BEGIN { printf "^^^%^^^\n"; }' */
@@ -2484,18 +2533,31 @@ static char *awk_printf(node *n, size_t *len)
 		/* Result can be arbitrarily long. Example:
 		 *  printf "%99999s", "BOOM"
 		 */
+		*out++ = c;
+		*out = '\0';
 		sv = *++f;
 		*f = '\0';
+
+		if (has_star) {
+			size_t prefix_len = p - s;
+			size_t fmt_buf_len = strlen(fmt_buf);
+			fmt_str = xmalloc(prefix_len + fmt_buf_len + 1);
+			memcpy(fmt_str, s, prefix_len);
+			strcpy(fmt_str + prefix_len, fmt_buf);
+		} else {
+			fmt_str = s;
+		}
+
 		if (c == 'c') {
 			char cc = is_numeric(arg) ? getvar_i(arg) : *getvar_s(arg);
-			char *r = xasprintf(s, cc ? cc : '^' /* else strlen will be wrong */);
+			char *r = xasprintf(fmt_str, cc ? cc : '^' /* else strlen will be wrong */);
 			slen = strlen(r);
 			if (cc == '\0') /* if cc is NUL, re-format the string with it */
-				sprintf(r, s, cc);
+				sprintf(r, fmt_str, cc);
 			s = r;
 		} else {
 			if (c == 's') {
-				s = xasprintf(s, getvar_s(arg));
+				s = xasprintf(fmt_str, getvar_s(arg));
 			} else {
 				double d = getvar_i(arg);
 				if (strchr("diouxX", c)) {
@@ -2506,17 +2568,19 @@ static char *awk_printf(node *n, size_t *len)
 //but some replacements are not equivalent:
 //%09d -> %09s: breaks zero-padding;
 //%+d -> %+s: won't prepend +; etc
-					s = xasprintf(s, (int)d);
+					s = xasprintf(fmt_str, (int)d);
 				} else if (strchr("eEfFgGaA", c)) {
-					s = xasprintf(s, d);
+					s = xasprintf(fmt_str, d);
 				} else {
 					/* gawk 5.1.1 printf("%W") prints "%W", does not error out */
-					s = xstrndup(s, f - s);
+					s = xstrdup(fmt_str);
 				}
 			}
 			slen = strlen(s);
 		}
 		*f = sv;
+		if (has_star)
+			free(fmt_str);
  append:
 		if (i == 0) {
 			b = s;
diff --git a/testsuite/awk.tests b/testsuite/awk.tests
index df1078bdb..76eb0f920 100755
--- a/testsuite/awk.tests
+++ b/testsuite/awk.tests
@@ -24,6 +24,28 @@ testing "awk if operator >= "  "awk 'BEGIN{if(23>=23) print \"foo\"}'" "foo\n" "
 testing "awk if operator < "   "awk 'BEGIN{if(2 < 13) print \"foo\"}'" "foo\n" "" ""
 testing "awk if string == "    "awk 'BEGIN{if(\"a\"==\"ab\") print \"bar\"}'" "" "" ""
 
+# width and precision
+testing "awk only width" \
+"awk 'BEGIN { printf \"%*d\", 10, -42 }'" "       -42" "" ""
+
+testing "awk negative width" \
+"awk 'BEGIN { printf \"%*s\", -10, \"hello\" }'" "hello     " "" ""
+
+testing "awk only precision" \
+"awk 'BEGIN { printf \"%.*f\n\", 2, 3.14159 }'" "3.14\n" "" ""
+
+testing "awk width and precision" \
+"awk 'BEGIN { printf \"%*.*f\", 10, 2, 3.14159 }'" "      3.14" "" ""
+
+testing "awk 0 padding with width and precision" \
+"awk 'BEGIN { printf \"%0*.*f\", 10, 2, 3.14 }'" "0000003.14" "" ""
+
+testing "awk multiple format specifications" \
+"awk 'BEGIN { printf \"%d %*d %d\", 1, 5, 2, 3 }'" "1     2 3" "" ""
+
+testing "awk width with char" \
+"awk 'BEGIN { printf \"%*c\", 5, 65 }'" "    A" "" ""
+
 # 4294967295 = 0xffffffff
 testing "awk bitwise op"  "awk '{ print or(4294967295,1) }'" "4294967295\n" "" "\n"
 
-- 
2.43.0