touch.c (6455B)
- // utils-std: Collection of commonly available Unix tools
- // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
- // SPDX-License-Identifier: MPL-2.0
- #define _DEFAULT_SOURCE // tm_gmtoff/tm_zone
- #define _XOPEN_SOURCE 700 // strptime (NetBSD)
- #define _POSIX_C_SOURCE 200809L // O_NOFOLLOW, st_atim/st_mtim
- #include "../lib/bitmasks.h" /* FIELD_* */
- #include "../lib/iso_parse.h" /* iso_parse */
- #include <assert.h>
- #include <errno.h> /* errno */
- #include <fcntl.h> /* open */
- #include <stdbool.h> /* bool */
- #include <stdio.h> /* perror */
- #include <stdlib.h> /* exit, EXIT_FAILURE */
- #include <string.h> /* strrchr, strlen */
- #include <sys/stat.h> /* futimens, stat, utimensat */
- #include <time.h> /* mktime */
- #include <unistd.h> /* getopt, opt*, close */
- // [[CC]YY]MMDDhhmm[.SS]
- static struct timespec
- opt_t_parse(char *arg, const char **errstr)
- {
- struct timespec res = {.tv_sec = 0, .tv_nsec = 0};
- struct tm tm = {
- .tm_year = 0,
- .tm_mon = 0,
- .tm_mday = 0,
- .tm_hour = 0,
- .tm_min = 0,
- .tm_sec = 0,
- .tm_isdst = -1, // unknown if DST is in effect
- };
- size_t len = strlen(arg);
- char *secs = strrchr(arg, '.');
- if(secs != NULL)
- {
- if(arg[len - 3] != '.')
- {
- *errstr = "Wrong location of second separator";
- return res;
- }
- secs[0] = 0;
- secs++;
- len -= 3;
- char *s = strptime(secs, "%S", &tm);
- if(s == NULL)
- {
- *errstr = strerror(errno);
- errno = 0;
- return res;
- }
- if(s[0] != 0)
- {
- *errstr = "Extraneous character in seconds";
- return res;
- }
- }
- const char *fmt = NULL;
- switch(len)
- {
- case 8:
- fmt = "%m%d%H%M"; // MMDDhhmm
- break;
- case 10:
- fmt = "%y%m%d%H%M"; // YYMMDDhhmm
- break;
- case 12:
- fmt = "%Y%m%d%H%M"; // CCYYMMDDhhmm
- break;
- default:
- *errstr = "Invalid datetime";
- return res;
- }
- assert(fmt != NULL);
- char *dt = strptime(arg, fmt, &tm);
- if(dt == NULL)
- {
- *errstr = strerror(errno);
- errno = 0;
- return res;
- }
- if(dt[0] != 0)
- {
- *errstr = "Extraneous character in datetime";
- return res;
- }
- res.tv_sec = mktime(&tm);
- if(res.tv_sec == (time_t)-1)
- {
- *errstr = strerror(errno);
- errno = 0;
- return res;
- }
- // As observed on FreeBSD 14.0, non-errorneous mktime can still end up setting errno
- // cf. https://builds.sr.ht/~lanodan/job/1181509
- errno = 0;
- return res;
- }
- int
- main(int argc, char *argv[])
- {
- bool ch_atime = false, ch_mtime = false;
- char *ref_file = NULL;
- struct timespec times[2] = {
- {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, // access
- {.tv_sec = 0, .tv_nsec = UTIME_OMIT} // modification
- };
- struct timespec target = {0, UTIME_NOW};
- int open_flags = O_WRONLY | O_CREAT | O_NOCTTY;
- int utimensat_flags = 0;
- int c = 0;
- while((c = getopt(argc, argv, ":acfhmr:t:d:")) != -1)
- {
- const char *errstr = NULL;
- switch(c)
- {
- case 'a':
- ch_atime = true;
- break;
- case 'c':
- FIELD_CLR(open_flags, O_CREAT);
- break;
- case 'f':
- /* Legacy from BSD, ignored
- *
- * As of 2024-12-05:
- *
- * coreutils: Ignored since first commit in 1992
- * OpenBSD: Ignored since OpenBSD 3.8 (commit in 2005)
- * NetBSD: Ignored since NetBSD 6.0 (commit in 2011)
- * FreeBSD: Ignored since FreeBSD 10 (commit in 2012)
- * BusyBox: Ignored (and not documented)
- * illumos: Ignored in SystemV emulation, supported otherwise
- */
- break;
- case 'h':
- FIELD_SET(open_flags, O_NOFOLLOW);
- FIELD_SET(utimensat_flags, AT_SYMLINK_NOFOLLOW);
- break;
- case 'm':
- ch_mtime = true;
- break;
- case 'r':
- ref_file = optarg;
- break;
- case 't':
- target = opt_t_parse(optarg, &errstr);
- if(errstr != NULL)
- {
- fprintf(stderr, "touch: error: opt_t_parse(\"%s\", …): %s\n", optarg, errstr);
- exit(EXIT_FAILURE);
- }
- break;
- case 'd':
- {
- long nsec = 0;
- struct tm iso_res = {
- .tm_year = 0,
- .tm_mon = 0,
- .tm_mday = 0,
- .tm_hour = 0,
- .tm_min = 0,
- .tm_sec = 0,
- .tm_isdst = -1, // unknown if DST is in effect
- .tm_gmtoff = 0,
- .tm_zone = NULL,
- };
- char *s = iso_parse(optarg, &iso_res, &nsec, &errstr);
- if(errstr != NULL)
- {
- fprintf(stderr, "touch: error: iso_parse(\"%s\", …): %s\n", optarg, errstr);
- exit(EXIT_FAILURE);
- }
- if(s == NULL)
- {
- fprintf(stderr, "touch: error: iso_parse(\"%s\", …) returned NULL\n", optarg);
- exit(EXIT_FAILURE);
- }
- target.tv_sec = mktime_tz(&iso_res);
- target.tv_nsec = nsec;
- if(target.tv_sec == (time_t)-1)
- {
- fprintf(stderr, "touch: error: mktime: %s\n", strerror(errno));
- exit(EXIT_FAILURE);
- }
- errno = 0;
- break;
- }
- case ':':
- fprintf(stderr, "touch: error: Missing operand for option: '-%c'\n", optopt);
- return 1;
- case '?':
- fprintf(stderr, "touch: error: Unrecognised option: '-%c'\n", optopt);
- return 1;
- }
- }
- argc -= optind;
- argv += optind;
- // When neither -a nor -m are specified, change both
- if(!ch_atime && !ch_mtime)
- {
- ch_atime = true;
- ch_mtime = true;
- }
- if(ref_file == NULL)
- {
- if(ch_atime) times[0] = target;
- if(ch_mtime) times[1] = target;
- }
- else
- {
- struct stat ref;
- if(stat(ref_file, &ref) != 0)
- {
- fprintf(stderr,
- "touch: error: Failed getting status of file '%s': %s\n",
- ref_file,
- strerror(errno));
- return 1;
- }
- if(ch_atime)
- {
- times[0] = ref.st_atim;
- }
- if(ch_mtime)
- {
- times[1] = ref.st_mtim;
- }
- }
- for(int i = 0; i < argc; i++)
- {
- const char *file = argv[i];
- int fd = open(file, open_flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
- if(fd == -1)
- {
- if(errno == EISDIR)
- {
- if(utimensat(AT_FDCWD, file, times, utimensat_flags) != 0)
- {
- fprintf(stderr,
- "touch: error: Failed setting new times on directory '%s': %s\n",
- file,
- strerror(errno));
- return 1;
- }
- return 0;
- }
- if(errno != ENOENT || FIELD_MATCH(open_flags, O_CREAT))
- fprintf(stderr, "touch: error: Failed opening file '%s': %s\n", file, strerror(errno));
- return 1;
- }
- if(futimens(fd, times) != 0)
- {
- fprintf(stderr,
- "touch: error: Failed setting new times on file '%s': %s\n",
- file,
- strerror(errno));
- return 1;
- }
- if(close(fd) != 0)
- {
- fprintf(stderr,
- "touch: error: Failed closing file-descriptor for file '%s': %s\n",
- file,
- strerror(errno));
- return 1;
- }
- }
- return 0;
- }