[git commit] awk: add '*' as a valid specifier for width and precision in printf
Denys Vlasenko
vda.linux at googlemail.com
Wed Jul 1 21:38:27 UTC 2026
commit: https://git.busybox.net/busybox/commit/?id=6ba81ddb0d47d8c5298999c83f439817b7ec9c7a
branch: https://git.busybox.net/busybox/log/?h=master
Based on a patch by Anubhav Kokane <dev.anubhavk at gmail.com>
function old new delta
awk_printf 640 1086 +446
.rodata 107131 107108 -23
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 1/1 up/down: 446/-23) Total: 423 bytes
text data bss dec hex filename
1080210 555 4992 1085757 10913d busybox_old
1080633 555 4992 1086180 1092e4 busybox_unstripped
Signed-off-by: Denys Vlasenko <vda.linux at googlemail.com>
---
editors/awk.c | 83 +++++++++++++++++++++++++++++++++++++++++++++++------
testsuite/awk.tests | 37 ++++++++++++++++++++++++
2 files changed, 111 insertions(+), 9 deletions(-)
diff --git a/editors/awk.c b/editors/awk.c
index dd8f4ac42..2439993ba 100644
--- a/editors/awk.c
+++ b/editors/awk.c
@@ -2414,6 +2414,18 @@ 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)
@@ -2443,9 +2455,13 @@ static char *awk_printf(node *n, size_t *len)
char sv;
var *arg;
size_t slen;
+ int stars;
+ int width = width;
+ int prec = prec;
/* Find end of the next format spec, or end of line */
s = f;
+ stars = 0;
while (1) {
c = *f;
if (!c) /* no percent chars found at all */
@@ -2464,11 +2480,56 @@ static char *awk_printf(node *n, size_t *len)
f++;
goto append; /* print "....%" part verbatim */
}
+
+ /* flags */
+ while (c && strchr("+- 0#'", c))
+ c = *++f;
+
+ /* width */
+ if (c == '*') {
+ stars = 1;
+ width = (int)getvar_i(evaluate(nextarg(&n), TMPVAR));
+ c = *++f;
+ //if (isdigit(c)) /* we do not support "%*2$d" format */
+ // syntax_error("invalid format specifier");
+ }
+ while (isdigit(c))
+ c = *++f;
+
+ /* precision? */
+ if (c == '.') {
+ c = *++f;
+ if (c == '*') {
+ prec = (int)getvar_i(evaluate(nextarg(&n), TMPVAR));
+ if (++stars == 1)
+ width = prec;
+ c = *++f;
+ //if (isdigit(c)) /* we do not support "%5.*2$d" format */
+ // syntax_error("invalid format specifier");
+ }
+ //while (isdigit(c))
+ // c = *++f;
+ //^^^^ 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;
- 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"; }' */
@@ -2477,8 +2538,8 @@ static char *awk_printf(node *n, size_t *len)
goto tail; /* print remaining string, exit loop */
}
}
- /* we are at A in "....%...A..." */
+ /* we are at A in "....%...A..." */
arg = evaluate(nextarg(&n), TMPVAR);
/* Result can be arbitrarily long. Example:
@@ -2486,16 +2547,20 @@ static char *awk_printf(node *n, size_t *len)
*/
sv = *++f;
*f = '\0';
+
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_width_prec(s, stars, width, prec, 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);
+ if (cc == '\0') { /* if cc is NUL, re-format the string with it */
+ free(r);
+ r = xasprintf_width_prec(s, stars, width, prec, cc);
+ }
s = r;
} else {
if (c == 's') {
- s = xasprintf(s, getvar_s(arg));
+ const char *cs = getvar_s(arg);
+ s = xasprintf_width_prec(s, stars, width, prec, cs);
} else {
double d = getvar_i(arg);
if (strchr("diouxX", c)) {
@@ -2506,9 +2571,9 @@ 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_width_prec(s, stars, width, prec, (int)d);
} else if (strchr("eEfFgGaA", c)) {
- s = xasprintf(s, d);
+ s = xasprintf_width_prec(s, stars, width, prec, d);
} else {
/* gawk 5.1.1 printf("%W") prints "%W", does not error out */
s = xstrndup(s, f - s);
diff --git a/testsuite/awk.tests b/testsuite/awk.tests
index df1078bdb..06cc79569 100755
--- a/testsuite/awk.tests
+++ b/testsuite/awk.tests
@@ -24,6 +24,43 @@ 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*.* 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 %*c width with char" \
+"awk 'BEGIN { printf \"%*c\", 5, 65 }'" " A" "" ""
+
+testing "awk %-*c width with char" \
+"awk 'BEGIN { printf \"%-*c\", 5, 65 }'" "A " "" ""
+
+# For some reason, this works in GNU Awk 5.3.0
+# (why does it work? does it allow flag chars _after_ width, not only before width?)
+# This is undocumented, and we do not support this.
+## testing "awk %*-c width with char" \
+## "awk 'BEGIN { printf \"%*-c\", 5, 65 }'" "A " "" ""
+
+testing "awk %*c width with NUL" \
+"awk 'BEGIN { printf \"%*c\", 5, 0 }'" " \0" "" ""
+
+testing "awk %-*c width with NUL" \
+"awk 'BEGIN { printf \"%-*c\", 5, 0 }'" "\0 " "" ""
+
# 4294967295 = 0xffffffff
testing "awk bitwise op" "awk '{ print or(4294967295,1) }'" "4294967295\n" "" "\n"
More information about the busybox-cvs
mailing list