date: make %N work without -lrt on glibc
[oweals/busybox.git] / coreutils / date.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * Mini date implementation for busybox
4  *
5  * by Matthew Grant <grantma@anathoth.gen.nz>
6  *
7  * iso-format handling added by Robert Griebl <griebl@gmx.de>
8  * bugfixes and cleanup by Bernhard Reutner-Fischer
9  *
10  * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
11 */
12
13 /* This 'date' command supports only 2 time setting formats,
14    all the GNU strftime stuff (its in libc, lets use it),
15    setting time using UTC and displaying it, as well as
16    an RFC 2822 compliant date output for shell scripting
17    mail commands */
18
19 /* Input parsing code is always bulky - used heavy duty libc stuff as
20    much as possible, missed out a lot of bounds checking */
21
22 /* Default input handling to save surprising some people */
23
24 /* GNU coreutils 6.9 man page:
25  * date [OPTION]... [+FORMAT]
26  * date [-u|--utc|--universal] [MMDDhhmm[[CC]YY][.ss]]
27  * -d, --date=STRING
28  *      display time described by STRING, not `now'
29  * -f, --file=DATEFILE
30  *      like --date once for each line of DATEFILE
31  * -r, --reference=FILE
32  *      display the last modification time of FILE
33  * -R, --rfc-2822
34  *      output date and time in RFC 2822 format.
35  *      Example: Mon, 07 Aug 2006 12:34:56 -0600
36  * --rfc-3339=TIMESPEC
37  *      output date and time in RFC 3339 format.
38  *      TIMESPEC='date', 'seconds', or 'ns'
39  *      Date and time components are separated by a single space:
40  *      2006-08-07 12:34:56-06:00
41  * -s, --set=STRING
42  *      set time described by STRING
43  * -u, --utc, --universal
44  *      print or set Coordinated Universal Time
45  *
46  * Busybox:
47  * long options are not supported
48  * -f is not supported
49  * -I seems to roughly match --rfc-3339, but -I has _optional_ param
50  *    (thus "-I seconds" doesn't work, only "-Iseconds"),
51  *    and does not support -Ins
52  * -D FMT is a bbox extension for _input_ conversion of -d DATE
53  */
54
55 //kbuild:lib-$(CONFIG_DATE) += date.o
56
57 //config:config DATE
58 //config:       bool "date"
59 //config:       default y
60 //config:       help
61 //config:         date is used to set the system date or display the
62 //config:         current time in the given format.
63 //config:
64 //config:config FEATURE_DATE_ISOFMT
65 //config:       bool "Enable ISO date format output (-I)"
66 //config:       default y
67 //config:       depends on DATE
68 //config:       help
69 //config:         Enable option (-I) to output an ISO-8601 compliant
70 //config:         date/time string.
71 //config:
72 //config:config FEATURE_DATE_NANO
73 //config:       bool "Support %[num]N nanosecond format specifier"
74 //config:       default n
75 //config:       depends on DATE
76 //config:       help
77 //config:         Support %[num]N format specifier. Adds ~250 bytes of code.
78 //config:
79 //config:config FEATURE_DATE_COMPAT
80 //config:       bool "Support weird 'date MMDDhhmm[[YY]YY][.ss]' format"
81 //config:       default y
82 //config:       depends on DATE
83 //config:       help
84 //config:         System time can be set by 'date -s DATE' and simply 'date DATE',
85 //config:         but formats of DATE string are different. 'date DATE' accepts
86 //config:         a rather weird MMDDhhmm[[YY]YY][.ss] format with completely
87 //config:         unnatural placement of year between minutes and seconds.
88 //config:         date -s (and other commands like touch -d) use more sensible
89 //config:         formats (for one, ISO format YYYY-MM-DD hh:mm:ss.ssssss).
90 //config:
91 //config:         With this option off, 'date DATE' is 'date -s DATE' support
92 //config:         the same format. With it on, 'date DATE' additionally supports
93 //config:         MMDDhhmm[[YY]YY][.ss] format.
94
95 #include "libbb.h"
96 #if ENABLE_FEATURE_DATE_NANO
97 # include <sys/syscall.h>
98 #endif
99
100 enum {
101         OPT_RFC2822   = (1 << 0), /* R */
102         OPT_SET       = (1 << 1), /* s */
103         OPT_UTC       = (1 << 2), /* u */
104         OPT_DATE      = (1 << 3), /* d */
105         OPT_REFERENCE = (1 << 4), /* r */
106         OPT_TIMESPEC  = (1 << 5) * ENABLE_FEATURE_DATE_ISOFMT, /* I */
107         OPT_HINT      = (1 << 6) * ENABLE_FEATURE_DATE_ISOFMT, /* D */
108 };
109
110 static void maybe_set_utc(int opt)
111 {
112         if (opt & OPT_UTC)
113                 putenv((char*)"TZ=UTC0");
114 }
115
116 #if ENABLE_LONG_OPTS
117 static const char date_longopts[] ALIGN1 =
118                 "rfc-822\0"   No_argument       "R"
119                 "rfc-2822\0"  No_argument       "R"
120                 "set\0"       Required_argument "s"
121                 "utc\0"       No_argument       "u"
122         /*      "universal\0" No_argument       "u" */
123                 "date\0"      Required_argument "d"
124                 "reference\0" Required_argument "r"
125                 ;
126 #endif
127
128 int date_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
129 int date_main(int argc UNUSED_PARAM, char **argv)
130 {
131         struct timespec ts;
132         struct tm tm_time;
133         char buf_fmt_dt2str[64];
134         unsigned opt;
135         int ifmt = -1;
136         char *date_str;
137         char *fmt_dt2str;
138         char *fmt_str2dt;
139         char *filename;
140         char *isofmt_arg = NULL;
141
142         opt_complementary = "d--s:s--d"
143                 IF_FEATURE_DATE_ISOFMT(":R--I:I--R");
144         IF_LONG_OPTS(applet_long_options = date_longopts;)
145         opt = getopt32(argv, "Rs:ud:r:"
146                         IF_FEATURE_DATE_ISOFMT("I::D:"),
147                         &date_str, &date_str, &filename
148                         IF_FEATURE_DATE_ISOFMT(, &isofmt_arg, &fmt_str2dt));
149         argv += optind;
150         maybe_set_utc(opt);
151
152         if (ENABLE_FEATURE_DATE_ISOFMT && (opt & OPT_TIMESPEC)) {
153                 ifmt = 0; /* default is date */
154                 if (isofmt_arg) {
155                         static const char isoformats[] ALIGN1 =
156                                 "date\0""hours\0""minutes\0""seconds\0"; /* ns? */
157                         ifmt = index_in_substrings(isoformats, isofmt_arg);
158                         if (ifmt < 0)
159                                 bb_show_usage();
160                 }
161         }
162
163         fmt_dt2str = NULL;
164         if (argv[0] && argv[0][0] == '+') {
165                 fmt_dt2str = &argv[0][1]; /* skip over the '+' */
166                 argv++;
167         }
168         if (!(opt & (OPT_SET | OPT_DATE))) {
169                 opt |= OPT_SET;
170                 date_str = argv[0]; /* can be NULL */
171                 if (date_str) {
172 #if ENABLE_FEATURE_DATE_COMPAT
173                         int len = strspn(date_str, "0123456789");
174                         if (date_str[len] == '\0'
175                          || (date_str[len] == '.'
176                             && isdigit(date_str[len+1])
177                             && isdigit(date_str[len+2])
178                             && date_str[len+3] == '\0'
179                             )
180                         ) {
181                                 /* Dreaded MMDDhhmm[[CC]YY][.ss] format!
182                                  * It does not match -d or -s format.
183                                  * Some users actually do use it.
184                                  */
185                                 len -= 8;
186                                 if (len < 0 || len > 4 || (len & 1))
187                                         bb_error_msg_and_die(bb_msg_invalid_date, date_str);
188                                 if (len != 0) { /* move YY or CCYY to front */
189                                         char buf[4];
190                                         memcpy(buf, date_str + 8, len);
191                                         memmove(date_str + len, date_str, 8);
192                                         memcpy(date_str, buf, len);
193                                 }
194                         }
195 #endif
196                         argv++;
197                 }
198         }
199         if (*argv)
200                 bb_show_usage();
201
202         /* Now we have parsed all the information except the date format
203          * which depends on whether the clock is being set or read */
204
205         if (opt & OPT_REFERENCE) {
206                 struct stat statbuf;
207                 xstat(filename, &statbuf);
208                 ts.tv_sec = statbuf.st_mtime;
209 #if ENABLE_FEATURE_DATE_NANO
210                 ts.tv_nsec = statbuf.st_mtim.tv_nsec;
211 #endif
212         } else {
213 #if ENABLE_FEATURE_DATE_NANO
214                 /* libc has incredibly messy way of doing this,
215                  * typically requiring -lrt. We just skip all this mess */
216                 syscall(__NR_clock_gettime, CLOCK_REALTIME, &ts);
217 #else
218                 time(&ts.tv_sec);
219 #endif
220         }
221         localtime_r(&ts.tv_sec, &tm_time);
222
223         /* If date string is given, update tm_time, and maybe set date */
224         if (date_str != NULL) {
225                 /* Zero out fields - take her back to midnight! */
226                 tm_time.tm_sec = 0;
227                 tm_time.tm_min = 0;
228                 tm_time.tm_hour = 0;
229
230                 /* Process any date input to UNIX time since 1 Jan 1970 */
231                 if (ENABLE_FEATURE_DATE_ISOFMT && (opt & OPT_HINT)) {
232                         if (strptime(date_str, fmt_str2dt, &tm_time) == NULL)
233                                 bb_error_msg_and_die(bb_msg_invalid_date, date_str);
234                 } else {
235                         parse_datestr(date_str, &tm_time);
236                 }
237
238                 /* Correct any day of week and day of year etc. fields */
239                 tm_time.tm_isdst = -1;  /* Be sure to recheck dst */
240                 ts.tv_sec = validate_tm_time(date_str, &tm_time);
241
242                 maybe_set_utc(opt);
243
244                 /* if setting time, set it */
245                 if ((opt & OPT_SET) && stime(&ts.tv_sec) < 0) {
246                         bb_perror_msg("can't set date");
247                 }
248         }
249
250         /* Display output */
251
252         /* Deal with format string */
253         if (fmt_dt2str == NULL) {
254                 int i;
255                 fmt_dt2str = buf_fmt_dt2str;
256                 if (ENABLE_FEATURE_DATE_ISOFMT && ifmt >= 0) {
257                         /* -I[SPEC]: 0:date 1:hours 2:minutes 3:seconds */
258                         strcpy(fmt_dt2str, "%Y-%m-%dT%H:%M:%S");
259                         i = 8 + 3 * ifmt;
260                         if (ifmt != 0) {
261                                 /* TODO: if (ifmt==4) i += sprintf(&fmt_dt2str[i], ",%09u", nanoseconds); */
262  format_utc:
263                                 fmt_dt2str[i++] = '%';
264                                 fmt_dt2str[i++] = (opt & OPT_UTC) ? 'Z' : 'z';
265                         }
266                         fmt_dt2str[i] = '\0';
267                 } else if (opt & OPT_RFC2822) {
268                         /* -R. undo busybox.c setlocale */
269                         if (ENABLE_LOCALE_SUPPORT)
270                                 setlocale(LC_TIME, "C");
271                         strcpy(fmt_dt2str, "%a, %d %b %Y %H:%M:%S ");
272                         i = sizeof("%a, %d %b %Y %H:%M:%S ")-1;
273                         goto format_utc;
274                 } else { /* default case */
275                         fmt_dt2str = (char*)"%a %b %e %H:%M:%S %Z %Y";
276                 }
277         }
278 #if ENABLE_FEATURE_DATE_NANO
279         else {
280                 /* User-specified fmt_dt2str */
281                 /* Search for and process "%N" */
282                 char *p = fmt_dt2str;
283                 while ((p = strchr(p, '%')) != NULL) {
284                         int n, m;
285                         unsigned pres, scale;
286
287                         p++;
288                         if (*p == '%') {
289                                 p++;
290                                 continue;
291                         }
292                         n = strspn(p, "0123456789");
293                         if (p[n] != 'N') {
294                                 p += n;
295                                 continue;
296                         }
297                         /* We have "%[nnn]N" */
298                         p[-1] = '\0';
299                         p[n] = '\0';
300                         scale = 1;
301                         pres = 9;
302                         if (n) {
303                                 pres = xatoi_u(p);
304                                 if (pres == 0)
305                                         pres = 9;
306                                 m = 9 - pres;
307                                 while (--m >= 0)
308                                         scale *= 10;
309                         }
310
311                         m = p - fmt_dt2str;
312                         p += n + 1;
313                         fmt_dt2str = xasprintf("%s%0*u%s", fmt_dt2str, pres, (unsigned)ts.tv_nsec / scale, p);
314                         p = fmt_dt2str + m;
315                 }
316         }
317 #endif
318
319 #define date_buf bb_common_bufsiz1
320         if (*fmt_dt2str == '\0') {
321                 /* With no format string, just print a blank line */
322                 date_buf[0] = '\0';
323         } else {
324                 /* Handle special conversions */
325                 if (strncmp(fmt_dt2str, "%f", 2) == 0) {
326                         fmt_dt2str = (char*)"%Y.%m.%d-%H:%M:%S";
327                 }
328                 /* Generate output string */
329                 strftime(date_buf, sizeof(date_buf), fmt_dt2str, &tm_time);
330         }
331         puts(date_buf);
332
333         return EXIT_SUCCESS;
334 }