stat: revert %X-%Y-%Z change; use e.g., %:X to print fractional seconds
authorJim Meyering <meyering@redhat.com>
Thu, 21 Oct 2010 16:41:24 +0000 (18:41 +0200)
committerJim Meyering <meyering@redhat.com>
Wed, 3 Nov 2010 12:10:50 +0000 (13:10 +0100)
This reverts part of the recent commit 9069af45,
"stat: print timestamps to full resolution", which made %X, %Y, %Z
print floating point numbers.  We prefer to retain portability of
%X, %Y and %Z uses, while still providing access to full-resolution
time stamps via modified format strings.  Also make the new
%W consistent.
* src/stat.c: Include "xstrtol.h".
(print_it): Accept a new %...:[XYZ] format directive,
e.g., %:X, to print the nanoseconds portion of the corresponding time.
For example, %3.3:Y prints the zero-padded, truncated, milliseconds
part of the time of last modification.
(print_it): Update print_func signature to match.
(neg_to_zero): New helper function.
(epoch_time): Remove function; replace with...
(epoch_sec): New function; use timetostr.
(out_ns): New function.  Use "09" only when no other modifier
is specified.
(print_statfs): Change type of "m" to unsigned int,
now that it must accommodate values larger than 255.
(print_stat): Likewise.
Map :X to a code of 'X' + 256.  Likewise for Y, Z and W.
(usage): Update.
* tests/touch/60-seconds: Use %Y.%:Y in place of %Y.
* tests/misc/stat-nanoseconds: New file.
* tests/Makefile.am (TESTS): Add it.
* NEWS (Changes in behavior): Mention this.
With improvements by Pádraig Brady.
Thanks to Andreas Schwab for raising the issue.

NEWS
doc/coreutils.texi
src/stat.c
tests/Makefile.am
tests/misc/stat-nanoseconds [new file with mode: 0755]
tests/touch/60-seconds

diff --git a/NEWS b/NEWS
index dcb9745..596d2ec 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,19 @@ GNU coreutils NEWS                                    -*- outline -*-
   cp --attributes-only now completely overrides --reflink.
   Previously a reflink was needlessly attempted.
 
+  stat's %X, %Y, and %Z directives once again print only the integer
+  part of seconds since the epoch.  This reverts a change from
+  coreutils-8.6, that was deemed unnecessarily disruptive.  To obtain
+  the nanoseconds portion corresponding to %X, you may now use %:X.
+  I.e., to print the floating point number of seconds using maximum
+  precision, use this format string: %X.%:X.  Likewise for %Y, %Z and %W.
+
+  stat's new %W format directive would print floating point seconds.
+  However, with the above change to %X, %Y and %Z, we've made %W work
+  the same way:  %W now expands to seconds since the epoch (or 0 when
+  not supported), and %:W expands to the nanoseconds portion, or to
+  0 if not supported.
+
 
 * Noteworthy changes in release 8.6 (2010-10-15) [stable]
 
index 0b5a3d3..be5999f 100644 (file)
@@ -10706,15 +10706,40 @@ The valid @var{format} directives for files with @option{--format} and
 @item %u - User ID of owner
 @item %U - User name of owner
 @item %w - Time of file birth, or @samp{-} if unknown
-@item %W - Time of file birth as seconds since Epoch, or @samp{-}
+@item %W - Time of file birth as seconds since Epoch, or @samp{0}
+@item %:W - Time of file birth: nanoseconds remainder, or @samp{0}
 @item %x - Time of last access
 @item %X - Time of last access as seconds since Epoch
+@item %:X - Time of last access: nanoseconds remainder
 @item %y - Time of last modification
 @item %Y - Time of last modification as seconds since Epoch
+@item %:Y - Time of last modification: nanoseconds remainder
 @item %z - Time of last change
 @item %Z - Time of last change as seconds since Epoch
+@item %:Z - Time of last change: nanoseconds remainder
 @end itemize
 
+Note that each of @samp{%:W}, @samp{%:X}, @samp{%:Y} and @samp{%:Z}
+prints its zero-padded number of nanoseconds on a field of width 9.
+However, if you specify anything between the @samp{%} and @samp{:},
+e.g., @samp{%10:X}, your modifier replaces the default of @samp{09}.
+
+Here are some examples:
+
+@example
+zero pad:
+  $ stat -c '[%015:Y]' /usr
+  [000000031367045]
+space align:
+  $ stat -c '[%15:Y]' /usr
+  [       31367045]
+  $ stat -c '[%-15:Y]' /usr
+  [31367045       ]
+truncate:
+  $ stat -c '[%.3:Y]' /usr
+  [031]
+@end example
+
 The mount point printed by @samp{%m} is similar to that output
 by @command{df}, except that:
 @itemize @bullet
index fabbc17..d05a93b 100644 (file)
@@ -70,6 +70,7 @@
 #include "stat-time.h"
 #include "strftime.h"
 #include "find-mount-point.h"
+#include "xstrtol.h"
 #include "xvasprintf.h"
 
 #if USE_STATVFS
@@ -462,24 +463,50 @@ human_time (struct timespec t)
   return str;
 }
 
+/* Return a string representation (in static storage)
+   of the number of seconds in T since the epoch.  */
 static char * ATTRIBUTE_WARN_UNUSED_RESULT
-epoch_time (struct timespec t)
+epoch_sec (struct timespec t)
 {
-  static char str[INT_STRLEN_BOUND (time_t) + sizeof ".NNNNNNNNN"];
-  /* Note that time_t can technically be a floating point value, such
-     that casting to [u]intmax_t could lose a fractional value or
-     suffer from overflow.  However, most porting targets have an
-     integral time_t; also, we know of no file systems that store
-     valid time values outside the bounds of intmax_t even if that
-     value were represented as a floating point.  Besides, the cost of
-     converting to struct tm just to use nstrftime (str, len, "%s.%N",
-     tm, 0, t.tv_nsec) is pointless, since nstrftime would have to
-     convert back to seconds as time_t.  */
-  if (TYPE_SIGNED (time_t))
-    sprintf (str, "%" PRIdMAX ".%09ld", (intmax_t) t.tv_sec, t.tv_nsec);
+  static char str[INT_BUFSIZE_BOUND (time_t)];
+  return timetostr (t.tv_sec, str);
+}
+
+/* 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.  */
+static void
+out_ns (char *pformat, size_t prefix_len, struct timespec arg)
+{
+  /* If no format modifier is specified, i.e., nothing between the
+     "%" and ":" of "%:X", then use the default of zero-padding and
+     a width of 9.  Otherwise, use the specified modifier(s).
+     This is to avoid the mistake of omitting the zero padding on
+     a number with fewer digits than the field width: when printing
+     nanoseconds after a decimal place, the resulting floating point
+     fraction would be off by a factor of 10 or more.
+
+     If a precision/max width is specified, i.e., a '.' is present
+     in the modifier, then then treat the modifier as operating
+     on the default representation, i.e., a zero padded number
+     of width 9.  */
+  unsigned long int ns = arg.tv_nsec;
+
+  if (memchr (pformat, '.', prefix_len)) /* precision specified.  */
+    {
+      char tmp[INT_BUFSIZE_BOUND (uintmax_t)];
+      snprintf (tmp, sizeof tmp, "%09lu", ns);
+      strcpy (pformat + prefix_len, "s");
+      printf (pformat, tmp);
+    }
   else
-    sprintf (str, "%" PRIuMAX ".%09ld", (uintmax_t) t.tv_sec, t.tv_nsec);
-  return str;
+    {
+      char const *fmt = (prefix_len == 1) ? "09lu" : "lu";
+      /* Note that pformat is big enough, as %:X -> %09lu
+         and two extra bytes are already allocated.  */
+      strcpy (pformat + prefix_len, fmt);
+      printf (pformat, ns);
+    }
 }
 
 static void
@@ -539,7 +566,8 @@ out_file_context (char *pformat, size_t prefix_len, char const *filename)
 
 /* Print statfs info.  Return zero upon success, nonzero upon failure.  */
 static bool ATTRIBUTE_WARN_UNUSED_RESULT
-print_statfs (char *pformat, size_t prefix_len, char m, char const *filename,
+print_statfs (char *pformat, size_t prefix_len, unsigned int m,
+              char const *filename,
               void const *data)
 {
   STRUCT_STATVFS const *statfsbuf = data;
@@ -711,9 +739,19 @@ print_mount_point:
   return fail;
 }
 
+/* Map a TS with negative TS.tv_nsec to {0,0}.  */
+static inline struct timespec
+neg_to_zero (struct timespec ts)
+{
+  if (0 <= ts.tv_nsec)
+    return ts;
+  struct timespec z = {0, 0};
+  return z;
+}
+
 /* Print stat info.  Return zero upon success, nonzero upon failure.  */
 static bool
-print_stat (char *pformat, size_t prefix_len, char m,
+print_stat (char *pformat, size_t prefix_len, unsigned int m,
             char const *filename, void const *data)
 {
   struct stat *statbuf = (struct stat *) data;
@@ -815,31 +853,38 @@ print_stat (char *pformat, size_t prefix_len, char m,
       }
       break;
     case 'W':
-      {
-        struct timespec t = get_stat_birthtime (statbuf);
-        if (t.tv_nsec < 0)
-          out_string (pformat, prefix_len, "-");
-        else
-          out_string (pformat, prefix_len, epoch_time (t));
-      }
+      out_string (pformat, prefix_len,
+                  epoch_sec (neg_to_zero (get_stat_birthtime (statbuf))));
+      break;
+    case 'W' + 256:
+      out_ns (pformat, prefix_len, neg_to_zero (get_stat_birthtime (statbuf)));
       break;
     case 'x':
       out_string (pformat, prefix_len, human_time (get_stat_atime (statbuf)));
       break;
     case 'X':
-      out_string (pformat, prefix_len, epoch_time (get_stat_atime (statbuf)));
+      out_string (pformat, prefix_len, epoch_sec (get_stat_atime (statbuf)));
+      break;
+    case 'X' + 256:
+      out_ns (pformat, prefix_len, get_stat_atime (statbuf));
       break;
     case 'y':
       out_string (pformat, prefix_len, human_time (get_stat_mtime (statbuf)));
       break;
     case 'Y':
-      out_string (pformat, prefix_len, epoch_time (get_stat_mtime (statbuf)));
+      out_string (pformat, prefix_len, epoch_sec (get_stat_mtime (statbuf)));
+      break;
+    case 'Y' + 256:
+      out_ns (pformat, prefix_len, get_stat_mtime (statbuf));
       break;
     case 'z':
       out_string (pformat, prefix_len, human_time (get_stat_ctime (statbuf)));
       break;
     case 'Z':
-      out_string (pformat, prefix_len, epoch_time (get_stat_ctime (statbuf)));
+      out_string (pformat, prefix_len, epoch_sec (get_stat_ctime (statbuf)));
+      break;
+    case 'Z' + 256:
+      out_ns (pformat, prefix_len, get_stat_ctime (statbuf));
       break;
     case 'C':
       fail |= out_file_context (pformat, prefix_len, filename);
@@ -897,7 +942,8 @@ print_esc_char (char c)
    Return zero upon success, nonzero upon failure.  */
 static bool ATTRIBUTE_WARN_UNUSED_RESULT
 print_it (char const *format, char const *filename,
-          bool (*print_func) (char *, size_t, char, char const *, void const *),
+          bool (*print_func) (char *, size_t, unsigned int,
+                              char const *, void const *),
           void const *data)
 {
   bool fail = false;
@@ -922,10 +968,23 @@ print_it (char const *format, char const *filename,
           {
             size_t len = strspn (b + 1, "#-+.I 0123456789");
             char const *fmt_char = b + len + 1;
+            unsigned int fmt_code;
             memcpy (dest, b, len + 1);
 
+            /* The ":" modifier just before the letter in %W, %X, %Y, %Z
+               tells stat to print the nanoseconds portion of the date.  */
+            if (*fmt_char == ':' && strchr ("WXYZ", fmt_char[1]))
+              {
+                fmt_code = fmt_char[1] + 256;
+                ++fmt_char;
+              }
+            else
+              {
+                fmt_code = fmt_char[0];
+              }
+
             b = fmt_char;
-            switch (*fmt_char)
+            switch (fmt_code)
               {
               case '\0':
                 --b;
@@ -941,7 +1000,7 @@ print_it (char const *format, char const *filename,
                 putchar ('%');
                 break;
               default:
-                fail |= print_func (dest, len + 1, *fmt_char, filename, data);
+                fail |= print_func (dest, len + 1, fmt_code, filename, data);
                 break;
               }
             break;
@@ -1215,14 +1274,18 @@ The valid format sequences for files (without --file-system):\n\
       fputs (_("\
   %u   User ID of owner\n\
   %U   User name of owner\n\
-  %w   Time of file birth, or - if unknown\n\
-  %W   Time of file birth as seconds since Epoch, or - if unknown\n\
-  %x   Time of last access\n\
-  %X   Time of last access as seconds since Epoch\n\
-  %y   Time of last modification\n\
-  %Y   Time of last modification as seconds since Epoch\n\
-  %z   Time of last change\n\
-  %Z   Time of last change as seconds since Epoch\n\
+  %w   Time of file birth, human-readable; - if unknown\n\
+  %W   Time of file birth, seconds since Epoch; 0 if unknown\n\
+  %:W  Time of file birth, nanoseconds remainder; 0 if unknown\n\
+  %x   Time of last access, human-readable\n\
+  %X   Time of last access, seconds since Epoch\n\
+  %:X  Time of last access, nanoseconds remainder\n\
+  %y   Time of last modification, human-readable\n\
+  %Y   Time of last modification, seconds since Epoch\n\
+  %:Y  Time of last modification, nanoseconds remainder\n\
+  %z   Time of last change, human-readable\n\
+  %Z   Time of last change, seconds since Epoch\n\
+  %:Z  Time of last change, nanoseconds remainder\n\
 \n\
 "), stdout);
 
index 84db367..dd1c509 100644 (file)
@@ -244,6 +244,7 @@ TESTS =                                             \
   misc/stat-fmt                                        \
   misc/stat-hyphen                             \
   misc/stat-mount                              \
+  misc/stat-nanoseconds                                \
   misc/stat-printf                             \
   misc/stat-slash                              \
   misc/stdbuf                                  \
diff --git a/tests/misc/stat-nanoseconds b/tests/misc/stat-nanoseconds
new file mode 100755 (executable)
index 0000000..314f43e
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/sh
+# Exercise format strings involving %:X, %:Y, etc.
+
+# Copyright (C) 2010 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+test "$VERBOSE" = yes && stat --version
+. "${srcdir=.}/init.sh"; path_prepend_ ../src
+
+touch -d '2010-10-21 18:43:33.023456789' k || framework_failure_
+
+ls --full-time | grep 18:43:33.023456789 \
+  || skip_ this file system does not support sub-second time stamps
+
+test "$(stat -c      %:X k)" = 023456789  || fail=1
+test "$(stat -c     %3:X k)" = 23456789   || fail=1
+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
+
+Exit $fail
index f98f0c5..d008296 100755 (executable)
@@ -30,7 +30,7 @@ echo 60.000000000 > exp || framework_failure
 # an `invalid date format'.  Specifying 60 seconds *is* valid.
 TZ=UTC0 touch -t 197001010000.60 f || fail=1
 
-stat --p='%Y\n' f > out || fail=1
+stat --p='%Y.%:Y\n' f > out || fail=1
 
 compare out exp || fail=1