logo

utils-std

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

touch.c (6048B)


  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, ":achmr: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 'h':
  124. FIELD_SET(open_flags, O_NOFOLLOW);
  125. FIELD_SET(utimensat_flags, AT_SYMLINK_NOFOLLOW);
  126. break;
  127. case 'm':
  128. ch_mtime = true;
  129. break;
  130. case 'r':
  131. ref_file = optarg;
  132. break;
  133. case 't':
  134. target = opt_t_parse(optarg, &errstr);
  135. if(errstr != NULL)
  136. {
  137. fprintf(stderr, "touch: error: opt_t_parse(\"%s\", …): %s\n", optarg, errstr);
  138. exit(EXIT_FAILURE);
  139. }
  140. break;
  141. case 'd':
  142. {
  143. long nsec = 0;
  144. struct tm iso_res = {
  145. .tm_year = 0,
  146. .tm_mon = 0,
  147. .tm_mday = 0,
  148. .tm_hour = 0,
  149. .tm_min = 0,
  150. .tm_sec = 0,
  151. .tm_isdst = -1, // unknown if DST is in effect
  152. .tm_gmtoff = 0,
  153. .tm_zone = NULL,
  154. };
  155. char *s = iso_parse(optarg, &iso_res, &nsec, &errstr);
  156. if(errstr != NULL)
  157. {
  158. fprintf(stderr, "touch: error: iso_parse(\"%s\", …): %s\n", optarg, errstr);
  159. exit(EXIT_FAILURE);
  160. }
  161. if(s == NULL)
  162. {
  163. fprintf(stderr, "touch: error: iso_parse(\"%s\", …) returned NULL\n", optarg);
  164. exit(EXIT_FAILURE);
  165. }
  166. target.tv_sec = mktime_tz(&iso_res);
  167. target.tv_nsec = nsec;
  168. if(target.tv_sec == (time_t)-1)
  169. {
  170. fprintf(stderr, "touch: error: mktime: %s\n", strerror(errno));
  171. exit(EXIT_FAILURE);
  172. }
  173. errno = 0;
  174. break;
  175. }
  176. case ':':
  177. fprintf(stderr, "touch: error: Missing operand for option: '-%c'\n", optopt);
  178. return 1;
  179. case '?':
  180. fprintf(stderr, "touch: error: Unrecognised option: '-%c'\n", optopt);
  181. return 1;
  182. }
  183. }
  184. argc -= optind;
  185. argv += optind;
  186. // When neither -a nor -m are specified, change both
  187. if(!ch_atime && !ch_mtime)
  188. {
  189. ch_atime = true;
  190. ch_mtime = true;
  191. }
  192. if(ref_file == NULL)
  193. {
  194. if(ch_atime) times[0] = target;
  195. if(ch_mtime) times[1] = target;
  196. }
  197. else
  198. {
  199. struct stat ref;
  200. if(stat(ref_file, &ref) != 0)
  201. {
  202. fprintf(stderr,
  203. "touch: error: Failed getting status of file '%s': %s\n",
  204. ref_file,
  205. strerror(errno));
  206. return 1;
  207. }
  208. if(ch_atime)
  209. {
  210. times[0] = ref.st_atim;
  211. }
  212. if(ch_mtime)
  213. {
  214. times[1] = ref.st_mtim;
  215. }
  216. }
  217. for(int i = 0; i < argc; i++)
  218. {
  219. const char *file = argv[i];
  220. assert(errno == 0);
  221. int fd = open(file, open_flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
  222. if(fd == -1)
  223. {
  224. if(errno == EISDIR)
  225. {
  226. if(utimensat(AT_FDCWD, file, times, utimensat_flags) != 0)
  227. {
  228. fprintf(stderr,
  229. "touch: error: Failed setting new times on directory '%s': %s\n",
  230. file,
  231. strerror(errno));
  232. return 1;
  233. }
  234. return 0;
  235. }
  236. if(errno != ENOENT || FIELD_MATCH(open_flags, O_CREAT))
  237. fprintf(stderr, "touch: error: Failed opening file '%s': %s\n", file, strerror(errno));
  238. return 1;
  239. }
  240. if(futimens(fd, times) != 0)
  241. {
  242. fprintf(stderr,
  243. "touch: error: Failed setting new times on file '%s': %s\n",
  244. file,
  245. strerror(errno));
  246. return 1;
  247. }
  248. if(close(fd) != 0)
  249. {
  250. fprintf(stderr,
  251. "touch: error: Failed closing file-descriptor for file '%s': %s\n",
  252. file,
  253. strerror(errno));
  254. return 1;
  255. }
  256. }
  257. return 0;
  258. }