logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/

date.c (10169B)


  1. // utils-std: Collection of commonly available Unix tools
  2. // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
  3. // SPDX-License-Identifier: MPL-2.0
  4. #define _DEFAULT_SOURCE // tm_gmtoff/tm_zone (POSIX.1-2024)
  5. #define _POSIX_C_SOURCE 200809L
  6. #define _XOPEN_SOURCE 700 // strptime is in XSI
  7. #include "../config.h"
  8. #include "../lib/absu.h"
  9. #include "../libutils/datetime_parse.h" /* datetime_parse */
  10. #include "../libutils/getopt_nolong.h"
  11. #include <assert.h>
  12. #include <errno.h>
  13. #include <inttypes.h> /* PRId64 */
  14. #include <locale.h> /* setlocale() */
  15. #include <stdbool.h>
  16. #include <stdio.h> /* BUFSIZ, fprintf(), puts() */
  17. #include <stdlib.h> /* strtol() */
  18. #include <string.h> /* strerror */
  19. #include <time.h> /* time, localtime, tm, strftime, strptime, clock_settime */
  20. #include <unistd.h> /* getopt(), opt… */
  21. #ifdef HAS_GETOPT_LONG
  22. #include <getopt.h>
  23. #endif
  24. const char *argv0 = "date";
  25. static size_t
  26. date_strftime(char *restrict buf,
  27. size_t buflen,
  28. const char *restrict fmt,
  29. const struct tm *restrict tm,
  30. long nsec)
  31. {
  32. size_t fmtlen = strlen(fmt);
  33. size_t printed = 0;
  34. if(fmtlen == 0)
  35. {
  36. buf[0] = '\0';
  37. return 0;
  38. }
  39. for(size_t i = 0; i < fmtlen;)
  40. {
  41. // size taken from musl strftime
  42. static char fmt_buf[100] = "";
  43. size_t fmt_bufi = 0;
  44. if(fmt[i] == '%')
  45. {
  46. fmt_buf[fmt_bufi++] = fmt[i++];
  47. if(fmt[i] == '%') // handle '%%'
  48. {
  49. *buf = '%';
  50. buf++;
  51. buflen--;
  52. printed++;
  53. i++;
  54. continue;
  55. }
  56. }
  57. for(; fmt[i] != '%' && i < fmtlen;)
  58. {
  59. fmt_buf[fmt_bufi++] = fmt[i++];
  60. assert(fmt_bufi < 100);
  61. }
  62. fmt_buf[fmt_bufi] = '\0';
  63. size_t fmt_bufused = 0;
  64. if(fmt_buf[0] == '%' && fmt_buf[1] == ':' && fmt_buf[2] == 'z')
  65. {
  66. size_t got = snprintf(
  67. buf, buflen, "%+.2ld:%.2u", tm->tm_gmtoff / 3600, absu(tm->tm_gmtoff % 3600 / 60));
  68. if(got == 0) return got;
  69. buf += got;
  70. buflen -= got;
  71. printed += got;
  72. fmt_bufused += 3;
  73. }
  74. else if(fmt_buf[0] == '%' && fmt_buf[1] == ':' && fmt_buf[2] == ':' && fmt_buf[3] == 'z')
  75. {
  76. unsigned tzhr = absu(tm->tm_gmtoff % 3600 / 60);
  77. unsigned tzsec = absu(tm->tm_gmtoff % 3600 % 60);
  78. size_t got = snprintf(buf, buflen, "%+.2ld:%.2u:%.2u", tm->tm_gmtoff / 3600, tzhr, tzsec);
  79. if(got == 0) return got;
  80. buf += got;
  81. buflen -= got;
  82. printed += got;
  83. fmt_bufused += 4;
  84. }
  85. else if(fmt_buf[0] == '%' && fmt_buf[1] == 'N')
  86. {
  87. size_t got = snprintf(buf, buflen, "%09ld", nsec);
  88. if(got == 0) return got;
  89. buf += got;
  90. buflen -= got;
  91. printed += got;
  92. fmt_bufused += 2;
  93. }
  94. if(fmt_bufi > fmt_bufused)
  95. {
  96. size_t got = strftime(buf, buflen, fmt_buf + fmt_bufused, tm);
  97. if(got == 0) return got;
  98. buf += got;
  99. buflen -= got;
  100. printed += got;
  101. }
  102. }
  103. return printed;
  104. }
  105. static void
  106. usage(void)
  107. {
  108. fprintf(stderr, "\
  109. Usage:\n\
  110. date [-jRu] [-I iso_fmt] [-d datetime | -r epoch] [+format]\n\
  111. date [-jRu] [-I iso_fmt] mmddHHMM[[CC]yy] [+format]\n\
  112. date [-jRu] [-I iso_fmt] -f now_format now [+format]\n\
  113. ");
  114. }
  115. int
  116. main(int argc, char *argv[])
  117. {
  118. char outstr[BUFSIZ] = "";
  119. struct timespec tp = {
  120. .tv_sec = 0,
  121. .tv_nsec = 0,
  122. };
  123. const char *format = "%c";
  124. const char *input_format = NULL;
  125. int uflag = 0;
  126. int dflag = 0, rflag = 0;
  127. bool jflag = false;
  128. bool settime = false;
  129. char *lc_all = setlocale(LC_ALL, "");
  130. if(lc_all == NULL)
  131. {
  132. fprintf(stderr,
  133. "%s: warning: Failed loading locales. setlocale(LC_ALL, \"\"): %s\n",
  134. argv0,
  135. strerror(errno));
  136. }
  137. errno = 0;
  138. if(clock_gettime(CLOCK_REALTIME, &tp) != 0)
  139. {
  140. fprintf(stderr, "%s: error: Failed getting current time: %s\n", argv0, strerror(errno));
  141. return 1;
  142. }
  143. #ifdef HAS_GETOPT_LONG
  144. // Strictly for GNUisms compatibility so no long-only options
  145. // clang-format off
  146. static struct option opts[] = {
  147. {"date", required_argument, NULL, 'd'},
  148. {"iso-8601", optional_argument, NULL, 'I'},
  149. {"rfc-email", no_argument, NULL, 'R'},
  150. {"universal", no_argument, NULL, 'u'},
  151. {"utc", no_argument, NULL, 'u'},
  152. {0, 0, 0, 0},
  153. };
  154. // clang-format on
  155. // Need + as first character to get POSIX-style option parsing
  156. // Assume all getopt_long implementations support :: for optional args
  157. for(int c = -1; (c = getopt_long(argc, argv, "+:d:f:I::jr:Ru", opts, NULL)) != -1;)
  158. #else
  159. for(int c = -1; (c = getopt_nolong(argc, argv, ":d:f:I:jr:Ru")) != -1;)
  160. #endif
  161. {
  162. const char *errstr = NULL;
  163. switch(c)
  164. {
  165. case 'd': /* Custom datetime */
  166. if(input_format != NULL)
  167. {
  168. fprintf(stderr, "%s: error: Cannot both use '-d' and '-f'\n", argv0);
  169. return 1;
  170. }
  171. if(rflag == 1)
  172. {
  173. fprintf(stderr, "%s: error: Cannot both use '-d' and '-r'\n", argv0);
  174. return 1;
  175. }
  176. datetime_parse(optarg, &tp.tv_sec, &tp.tv_nsec, &errstr);
  177. dflag = 1;
  178. if(errstr != NULL)
  179. {
  180. fprintf(stderr, "%s: error: datetime_parse(\"%s\", …): %s\n", argv0, optarg, errstr);
  181. return 1;
  182. }
  183. break;
  184. case 'f': /* input datetime format */
  185. if(dflag == 1)
  186. {
  187. fprintf(stderr, "%s: error: Cannot both use '-d' and '-f'\n", argv0);
  188. return 1;
  189. }
  190. if(rflag == 1)
  191. {
  192. fprintf(stderr, "%s: error: Cannot both use '-f' and '-r'\n", argv0);
  193. return 1;
  194. }
  195. input_format = optarg;
  196. settime = true;
  197. break;
  198. case 'r': /* seconds relative to epoch */
  199. {
  200. if(input_format != NULL)
  201. {
  202. fprintf(stderr, "%s: error: Cannot both use '-f' and '-r'\n", argv0);
  203. return 1;
  204. }
  205. if(dflag == 1)
  206. {
  207. fprintf(stderr, "%s: error: Cannot both use '-d' and '-r'\n", argv0);
  208. return 1;
  209. }
  210. char *endptr = NULL;
  211. errno = 0;
  212. tp.tv_sec = strtol(optarg, &endptr, 10);
  213. if(errno != 0)
  214. {
  215. fprintf(stderr, "%s: error: Failed parsing '-r %s'\n", argv0, optarg);
  216. return 1;
  217. }
  218. if(!(endptr == NULL || *endptr == '\0'))
  219. {
  220. fprintf(stderr, "%s: error: Invalid characters in '-r %s': %s\n", argv0, optarg, endptr);
  221. return 1;
  222. }
  223. break;
  224. }
  225. case 'R': /* Email (RFC 5322) format */
  226. format = "%a, %d %b %Y %H:%M:%S %z";
  227. break;
  228. case 'u': /* UTC timezone */
  229. uflag++;
  230. setenv("TZ", "UTC", 1);
  231. tzset();
  232. break;
  233. case 'I': /* ISO 8601 */
  234. if(optarg)
  235. {
  236. /* note: %:z (±ZZ:ZZ) and %N (nanoseconds) are date(1) GNU-isms absent from C libraries including glibc */
  237. switch(optarg[0])
  238. {
  239. case 'h': // hours
  240. format = "%Y-%m-%dT%H%:z";
  241. break;
  242. case 'm': // minutes
  243. format = "%Y-%m-%dT%H:%M%:z";
  244. break;
  245. case 's': // seconds
  246. format = "%Y-%m-%dT%H:%M:%S%:z";
  247. break;
  248. case 'n': // ns, nanoseconds
  249. format = "%Y-%m-%dT%H:%M:%S,%N%:z";
  250. break;
  251. case 'd': // date
  252. default:
  253. format = "%Y-%m-%d";
  254. break;
  255. }
  256. }
  257. else
  258. format = "%Y-%m-%d";
  259. break;
  260. case 'j':
  261. jflag = true;
  262. break;
  263. case ':':
  264. fprintf(stderr, "%s: error: Missing operand for option: '-%c'\n", argv0, optopt);
  265. usage();
  266. return 1;
  267. case '?':
  268. GETOPT_UNKNOWN_OPT
  269. usage();
  270. return 1;
  271. }
  272. }
  273. argc -= optind;
  274. argv += optind;
  275. if(argc > 0 && input_format != NULL)
  276. {
  277. struct tm input_tm;
  278. time_t timez = 0;
  279. gmtime_r(&timez, &input_tm); // init
  280. char *res = strptime(argv[0], input_format, &input_tm);
  281. if(res == NULL)
  282. {
  283. fprintf(stderr,
  284. "%s: error: strptime(\"%s\", \"%s\", …) as passed by '-f' option failed\n",
  285. argv0,
  286. argv[0],
  287. input_format);
  288. return 1;
  289. }
  290. /* TODO: %N (nanoseconds) support */
  291. tp.tv_sec = utils_timegm(&input_tm);
  292. tp.tv_nsec = 0;
  293. if(tp.tv_sec == (time_t)-1)
  294. {
  295. fprintf(stderr, "%s: error: timegm: %s\n", argv0, strerror(errno));
  296. return 1;
  297. }
  298. errno = 0;
  299. argv++;
  300. argc--;
  301. }
  302. if(input_format == NULL && argc > 0 && *argv && **argv != '+')
  303. {
  304. struct tm arg_tm;
  305. const char *fmt = "%m%d%H%M";
  306. int year = 0;
  307. int eyear = 0;
  308. if(gmtime_r(&tp.tv_sec, &arg_tm) == NULL)
  309. eyear = errno;
  310. else
  311. year = arg_tm.tm_year;
  312. char *s = strptime(argv[0], fmt, &arg_tm);
  313. if(s == NULL)
  314. {
  315. fprintf(stderr, "%s: error: strptime(\"%s\", \"%s\", …) returned NULL\n", argv0, *argv, fmt);
  316. return 1;
  317. }
  318. size_t rem = strlen(s);
  319. if(rem > 0)
  320. {
  321. switch(rem)
  322. {
  323. case 2:
  324. fmt = "%y";
  325. break;
  326. case 4:
  327. fmt = "%Y";
  328. break;
  329. default:
  330. fprintf(stderr,
  331. "%s: error: Got %zu trailing characters after \"%s\" for mmddHHMM\n",
  332. argv0,
  333. rem,
  334. fmt);
  335. return 1;
  336. }
  337. s = strptime(s, fmt, &arg_tm);
  338. if(s == NULL)
  339. {
  340. fprintf(
  341. stderr, "%s: error: strptime(\"%s\", \"%s\", …) returned NULL\n", argv0, *argv, fmt);
  342. return 1;
  343. }
  344. }
  345. else if(eyear != 0)
  346. {
  347. fprintf(stderr,
  348. "%s: error: Year wasn't specified in '%s' and failed to get current time: %s\n",
  349. argv0,
  350. *argv,
  351. strerror(eyear));
  352. return 1;
  353. }
  354. /* TODO: %N (nanoseconds) support */
  355. tp.tv_sec = utils_timegm(&arg_tm);
  356. tp.tv_nsec = 0;
  357. if(tp.tv_sec == (time_t)-1)
  358. {
  359. fprintf(stderr, "%s: error: timegm: %s\n", argv0, strerror(errno));
  360. return 1;
  361. }
  362. errno = 0;
  363. argv++;
  364. argc--;
  365. settime = true;
  366. }
  367. struct tm tm;
  368. if(uflag)
  369. {
  370. if(gmtime_r(&tp.tv_sec, &tm) == NULL)
  371. {
  372. fprintf(stderr, "%s: error: gmtime_r: %s\n", argv0, strerror(errno));
  373. return 1;
  374. }
  375. }
  376. else
  377. {
  378. if(localtime_r(&tp.tv_sec, &tm) == NULL)
  379. {
  380. fprintf(stderr, "%s: error: localtime_r: %s\n", argv0, strerror(errno));
  381. return 1;
  382. }
  383. }
  384. if(settime && !jflag)
  385. {
  386. if(clock_settime(CLOCK_REALTIME, &tp) != 0)
  387. {
  388. fprintf(stderr,
  389. "%s: error: clock_settime(CLOCK_REALTIME, {%" PRId64 ", %" PRId64 "}): %s\n",
  390. argv0,
  391. (int64_t)tp.tv_sec,
  392. (int64_t)tp.tv_nsec,
  393. strerror(errno));
  394. return 1;
  395. }
  396. }
  397. if(argc > 0 && *argv && **argv == '+')
  398. {
  399. format = *argv + 1;
  400. argv++;
  401. argc--;
  402. }
  403. errno = 0;
  404. if(date_strftime(outstr, sizeof(outstr), format, &tm, tp.tv_nsec) == 0 && errno != 0)
  405. {
  406. fprintf(stderr, "%s: error: Failed formatting time: %s\n", argv0, strerror(errno));
  407. return 1;
  408. }
  409. if(puts(outstr) < 0)
  410. {
  411. fprintf(stderr, "%s: error: Failed writing time: %s\n", argv0, strerror(errno));
  412. return 1;
  413. }
  414. return 0;
  415. }