logo

utils-std

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

touch.c (6455B)


  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
  5. #define _XOPEN_SOURCE 700 // strptime (NetBSD)
  6. #define _POSIX_C_SOURCE 200809L // O_NOFOLLOW, st_atim/st_mtim
  7. #include "../lib/bitmasks.h" /* FIELD_* */
  8. #include "../lib/iso_parse.h" /* iso_parse */
  9. #include <assert.h>
  10. #include <errno.h> /* errno */
  11. #include <fcntl.h> /* open */
  12. #include <stdbool.h> /* bool */
  13. #include <stdio.h> /* perror */
  14. #include <stdlib.h> /* exit, EXIT_FAILURE */
  15. #include <string.h> /* strrchr, strlen */
  16. #include <sys/stat.h> /* futimens, stat, utimensat */
  17. #include <time.h> /* mktime */
  18. #include <unistd.h> /* getopt, opt*, close */
  19. // [[CC]YY]MMDDhhmm[.SS]
  20. static struct timespec
  21. opt_t_parse(char *arg, const char **errstr)
  22. {
  23. struct timespec res = {.tv_sec = 0, .tv_nsec = 0};
  24. struct tm tm = {
  25. .tm_year = 0,
  26. .tm_mon = 0,
  27. .tm_mday = 0,
  28. .tm_hour = 0,
  29. .tm_min = 0,
  30. .tm_sec = 0,
  31. .tm_isdst = -1, // unknown if DST is in effect
  32. };
  33. size_t len = strlen(arg);
  34. char *secs = strrchr(arg, '.');
  35. if(secs != NULL)
  36. {
  37. if(arg[len - 3] != '.')
  38. {
  39. *errstr = "Wrong location of second separator";
  40. return res;
  41. }
  42. secs[0] = 0;
  43. secs++;
  44. len -= 3;
  45. char *s = strptime(secs, "%S", &tm);
  46. if(s == NULL)
  47. {
  48. *errstr = strerror(errno);
  49. errno = 0;
  50. return res;
  51. }
  52. if(s[0] != 0)
  53. {
  54. *errstr = "Extraneous character in seconds";
  55. return res;
  56. }
  57. }
  58. const char *fmt = NULL;
  59. switch(len)
  60. {
  61. case 8:
  62. fmt = "%m%d%H%M"; // MMDDhhmm
  63. break;
  64. case 10:
  65. fmt = "%y%m%d%H%M"; // YYMMDDhhmm
  66. break;
  67. case 12:
  68. fmt = "%Y%m%d%H%M"; // CCYYMMDDhhmm
  69. break;
  70. default:
  71. *errstr = "Invalid datetime";
  72. return res;
  73. }
  74. assert(fmt != NULL);
  75. char *dt = strptime(arg, fmt, &tm);
  76. if(dt == NULL)
  77. {
  78. *errstr = strerror(errno);
  79. errno = 0;
  80. return res;
  81. }
  82. if(dt[0] != 0)
  83. {
  84. *errstr = "Extraneous character in datetime";
  85. return res;
  86. }
  87. res.tv_sec = mktime(&tm);
  88. if(res.tv_sec == (time_t)-1)
  89. {
  90. *errstr = strerror(errno);
  91. errno = 0;
  92. return res;
  93. }
  94. // As observed on FreeBSD 14.0, non-errorneous mktime can still end up setting errno
  95. // cf. https://builds.sr.ht/~lanodan/job/1181509
  96. errno = 0;
  97. return res;
  98. }
  99. int
  100. main(int argc, char *argv[])
  101. {
  102. bool ch_atime = false, ch_mtime = false;
  103. char *ref_file = NULL;
  104. struct timespec times[2] = {
  105. {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, // access
  106. {.tv_sec = 0, .tv_nsec = UTIME_OMIT} // modification
  107. };
  108. struct timespec target = {0, UTIME_NOW};
  109. int open_flags = O_WRONLY | O_CREAT | O_NOCTTY;
  110. int utimensat_flags = 0;
  111. int c = 0;
  112. while((c = getopt(argc, argv, ":acfhmr:t:d:")) != -1)
  113. {
  114. const char *errstr = NULL;
  115. switch(c)
  116. {
  117. case 'a':
  118. ch_atime = true;
  119. break;
  120. case 'c':
  121. FIELD_CLR(open_flags, O_CREAT);
  122. break;
  123. case 'f':
  124. /* Legacy from BSD, ignored
  125. *
  126. * As of 2024-12-05:
  127. *
  128. * coreutils: Ignored since first commit in 1992
  129. * OpenBSD: Ignored since OpenBSD 3.8 (commit in 2005)
  130. * NetBSD: Ignored since NetBSD 6.0 (commit in 2011)
  131. * FreeBSD: Ignored since FreeBSD 10 (commit in 2012)
  132. * BusyBox: Ignored (and not documented)
  133. * illumos: Ignored in SystemV emulation, supported otherwise
  134. */
  135. break;
  136. case 'h':
  137. FIELD_SET(open_flags, O_NOFOLLOW);
  138. FIELD_SET(utimensat_flags, AT_SYMLINK_NOFOLLOW);
  139. break;
  140. case 'm':
  141. ch_mtime = true;
  142. break;
  143. case 'r':
  144. ref_file = optarg;
  145. break;
  146. case 't':
  147. target = opt_t_parse(optarg, &errstr);
  148. if(errstr != NULL)
  149. {
  150. fprintf(stderr, "touch: error: opt_t_parse(\"%s\", …): %s\n", optarg, errstr);
  151. exit(EXIT_FAILURE);
  152. }
  153. break;
  154. case 'd':
  155. {
  156. long nsec = 0;
  157. struct tm iso_res = {
  158. .tm_year = 0,
  159. .tm_mon = 0,
  160. .tm_mday = 0,
  161. .tm_hour = 0,
  162. .tm_min = 0,
  163. .tm_sec = 0,
  164. .tm_isdst = -1, // unknown if DST is in effect
  165. .tm_gmtoff = 0,
  166. .tm_zone = NULL,
  167. };
  168. char *s = iso_parse(optarg, &iso_res, &nsec, &errstr);
  169. if(errstr != NULL)
  170. {
  171. fprintf(stderr, "touch: error: iso_parse(\"%s\", …): %s\n", optarg, errstr);
  172. exit(EXIT_FAILURE);
  173. }
  174. if(s == NULL)
  175. {
  176. fprintf(stderr, "touch: error: iso_parse(\"%s\", …) returned NULL\n", optarg);
  177. exit(EXIT_FAILURE);
  178. }
  179. target.tv_sec = mktime_tz(&iso_res);
  180. target.tv_nsec = nsec;
  181. if(target.tv_sec == (time_t)-1)
  182. {
  183. fprintf(stderr, "touch: error: mktime: %s\n", strerror(errno));
  184. exit(EXIT_FAILURE);
  185. }
  186. errno = 0;
  187. break;
  188. }
  189. case ':':
  190. fprintf(stderr, "touch: error: Missing operand for option: '-%c'\n", optopt);
  191. return 1;
  192. case '?':
  193. fprintf(stderr, "touch: error: Unrecognised option: '-%c'\n", optopt);
  194. return 1;
  195. }
  196. }
  197. argc -= optind;
  198. argv += optind;
  199. // When neither -a nor -m are specified, change both
  200. if(!ch_atime && !ch_mtime)
  201. {
  202. ch_atime = true;
  203. ch_mtime = true;
  204. }
  205. if(ref_file == NULL)
  206. {
  207. if(ch_atime) times[0] = target;
  208. if(ch_mtime) times[1] = target;
  209. }
  210. else
  211. {
  212. struct stat ref;
  213. if(stat(ref_file, &ref) != 0)
  214. {
  215. fprintf(stderr,
  216. "touch: error: Failed getting status of file '%s': %s\n",
  217. ref_file,
  218. strerror(errno));
  219. return 1;
  220. }
  221. if(ch_atime)
  222. {
  223. times[0] = ref.st_atim;
  224. }
  225. if(ch_mtime)
  226. {
  227. times[1] = ref.st_mtim;
  228. }
  229. }
  230. for(int i = 0; i < argc; i++)
  231. {
  232. const char *file = argv[i];
  233. int fd = open(file, open_flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
  234. if(fd == -1)
  235. {
  236. if(errno == EISDIR)
  237. {
  238. if(utimensat(AT_FDCWD, file, times, utimensat_flags) != 0)
  239. {
  240. fprintf(stderr,
  241. "touch: error: Failed setting new times on directory '%s': %s\n",
  242. file,
  243. strerror(errno));
  244. return 1;
  245. }
  246. return 0;
  247. }
  248. if(errno != ENOENT || FIELD_MATCH(open_flags, O_CREAT))
  249. fprintf(stderr, "touch: error: Failed opening file '%s': %s\n", file, strerror(errno));
  250. return 1;
  251. }
  252. if(futimens(fd, times) != 0)
  253. {
  254. fprintf(stderr,
  255. "touch: error: Failed setting new times on file '%s': %s\n",
  256. file,
  257. strerror(errno));
  258. return 1;
  259. }
  260. if(close(fd) != 0)
  261. {
  262. fprintf(stderr,
  263. "touch: error: Failed closing file-descriptor for file '%s': %s\n",
  264. file,
  265. strerror(errno));
  266. return 1;
  267. }
  268. }
  269. return 0;
  270. }