Subject: Re: bug#7325: new test failure due to non-portability of printf formats like %05.3s

From: Jim Meyering <[email-masked]>
Date: Wed, 03 Nov 2010 22:26:40 +0100

Eric Blake wrote:
> On 11/03/2010 12:59 PM, Jim Meyering wrote:
>> Note that coreutils' printf does not accept the '0' modifier in a %s format.
>>
>>     $ env printf '%05.3s\n' 23
>>     printf: %05.3s: invalid conversion specification
>>
>> That's because POSIX says the "0" modifier applies only to the
>> d, i, o, u, x, X, a, A, e, E, f, F, g, and G conversion specifiers.
>>
>> One solution is to trim off the "0".
>> It's probably a good idea regardless, in case some implementation rejects it.
>>
>> On the other hand, I find the zero-padding you currently get
>> with stat on solaris to be slightly more intuitive.
>
> I agree that %05.3:X resulting in 00023 would be ideal.  And I agree
> that we'd have to trim off the 0 modifier before calling the underlying
> printf %s, and thus be responsible for putting in '0' padding ourselves
> rather than relying on printf() padding.

Thanks for confirming.
Here's the patch I'm considering:

diff --git a/src/stat.c b/src/stat.c
index d05a93b..993db48 100644
--- a/src/stat.c
+++ b/src/stat.c
@@ -472,6 +472,23 @@ epoch_sec (struct timespec t)
   return timetostr (t.tv_sec, str);
 }

+/* Convert a LEN-byte FORMAT modifier, e.g., "0009.4", to "9.4".
+   Do it in place and return the new length.  */
+static size_t
+sanitize_format_string (char *format, size_t len)
+{
+  char *p = format;
+  char const *end = format + len;
+  while (p < end && *p == '0')
+    p++;
+  if (p == format)
+    return len;
+
+  len -= p - format;
+  memmove (format, p, len);
+  return len;
+}
+
 /* Output the number of nanoseconds, ARG.tv_nsec, honoring a
    WIDTH.PRECISION format modifier, where PRECISION specifies
    how many leading digits(on a field of 9) to print.  */
@@ -496,8 +513,25 @@ out_ns (char *pformat, size_t prefix_len, struct timespec arg)
     {
       char tmp[INT_BUFSIZE_BOUND (uintmax_t)];
       snprintf (tmp, sizeof tmp, "%09lu", ns);
+      /* Handle formats like %5.3:X and %05.3:X, which, with the likes of
+         "023" in TMP, should print "  023" and "00023" respectively.
+         Do not use a format like "%05s" with *printf, since POSIX says
+         the '0' modifier does not apply to the "s" conversion specifier.  */
+      size_t new_len = sanitize_format_string (pformat + 1, prefix_len - 1);
+      bool zero_pad = (new_len < prefix_len - 1);
+      prefix_len = new_len + 1;
       strcpy (pformat + prefix_len, "s");
-      printf (pformat, tmp);
+      char b2[INT_BUFSIZE_BOUND (uintmax_t)];
+      /* Format the result, as usual...  */
+      snprintf (b2, sizeof b2, pformat, tmp);
+      /* But if zero-padding is desired, convert leading blanks to zeros.  */
+      if (zero_pad)
+        {
+          char *p = b2;
+          while (*p == ' ')
+            *p++ = '0';
+        }
+      fputs (b2, stdout);
     }
   else
     {
diff --git a/tests/misc/stat-nanoseconds b/tests/misc/stat-nanoseconds
index 314f43e..9c06a51 100755
--- a/tests/misc/stat-nanoseconds
+++ b/tests/misc/stat-nanoseconds
@@ -30,7 +30,7 @@ test "$(stat -c   %3.3:X k)" = 023        || fail=1
 test "$(stat -c    %.3:X k)" = 023        || fail=1
 test "$(stat -c  %03.3:X k)" = 023        || fail=1
 test "$(stat -c  %-5.3:X k)" = '023  '    || fail=1
-test "$(stat -c  %05.3:X k)" = '  023'    || fail=1
-test "$(stat -c %010.3:X k)" = '       023' || fail=1
+test "$(stat -c  %05.3:X k)" = 00023      || fail=1
+test "$(stat -c %010.3:X k)" = 0000000023 || fail=1

 Exit $fail