diff --git a/usr.bin/Makefile b/usr.bin/Makefile --- a/usr.bin/Makefile +++ b/usr.bin/Makefile @@ -61,6 +61,7 @@ hexdump \ id \ ident \ + preserve \ ipcrm \ ipcs \ join \ diff --git a/usr.bin/preserve/Makefile b/usr.bin/preserve/Makefile new file mode 100644 --- /dev/null +++ b/usr.bin/preserve/Makefile @@ -0,0 +1,3 @@ +PROG = preserve + +.include diff --git a/usr.bin/preserve/preserve.1 b/usr.bin/preserve/preserve.1 new file mode 100644 --- /dev/null +++ b/usr.bin/preserve/preserve.1 @@ -0,0 +1,112 @@ +.\"- +.\" Copyright (c) 2023 Dag-Erling Smørgrav +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd March 29, 2023 +.Dt PRESERVE 1 +.Os +.Sh NAME +.Nm preserve +.Nd run a command and replace its output only if it has changed +.Sh SYNOPSIS +.Nm +.Op Fl v +.Op Fl o Ar filename +.Ar command +.Op Ar args +.Sh DESCRIPTION +The +.Nm +utility runs the specified command, which is expected to either write to +.Va stdout +or produce a single output file. +.Pp +If the +.Fl o +option is provided before the command, the command is assumed to write +its output to +.Va stdout , +and +.Nm +redirects it to a temporary file. +.Pp +If the +.Fl o +option is not provided, +.Nm +scans +.Ar args +to determine the output file name. +If +.Ar args +includes a word that begins with +.Dq Fl o , +the remainder of that word is taken to be the output file name. +Otherwise, if it includes the word +.Dq Fl o +in a non-terminal position, the next word is taken to be the output +file name. +Otherwise, if the last word of +.Ar args +does not begin with a hyphen, it is taken to be the output file name. +In all cases, the actual filename is replaced in +.Ar args +with a temporary filename. +.Pp +If +.Nm +is unable to determine the output file name, the original command is +executed without further interference. +.Pp +Otherwse, if the command succeeds and the final output file either +does not exist, or exists but its contents differ from those of the +temporary file, the temporary file is renamed to the final output +file. +In all other cases, the temporary file is deleted. +.Pp +The following options are available: +.Bl -tag -width Fl +.It Fl o Ar filename +Assume that +.Ar command +writes its output to +.Va stdout +rather than to a file, and use +.Ar filename +as the final output file name. +.It Fl v +Write additional information to +.Va stderr +explaining whether the output file was retained or replaced and why. +.El +.Sh EXIT STATUS +The exit status of the +.Nm +utility is that of the command it ran, unless it failed to run the +command or an error occurred while handling the output, in which case +its exit status is 1. +.Sh AUTHORS +The +.Nm +command and this manual page were written by +.An Dag-Erling Sm\(/orgrav Aq Mt des@FreeBSD.org . diff --git a/usr.bin/preserve/preserve.c b/usr.bin/preserve/preserve.c new file mode 100644 --- /dev/null +++ b/usr.bin/preserve/preserve.c @@ -0,0 +1,221 @@ +/*- + * Copyright (c) 2023 Dag-Erling Smørgrav + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool vflag; + +static bool +files_are_identical(const char *afn, const char *bfn) +{ + static char abuf[4096], bbuf[4096]; + struct stat asb, bsb; + ssize_t alen, blen; + off_t off; + int afd = -1, bfd = -1, i; + bool identical = false; + + if ((afd = open(afn, O_RDONLY)) < 0) { + if (errno != ENOENT) + warn("%s", afn); + goto done; + } + if ((bfd = open(bfn, O_RDONLY)) < 0) { + if (errno != ENOENT) + warn("%s", bfn); + goto done; + } + if (fstat(afd, &asb) != 0 || fstat(bfd, &bsb) != 0) { + /* can't happen */ + warn("fstat()"); + goto done; + } + if (asb.st_size != bsb.st_size) { + if (vflag) { + warnx("%s and %s differ in length: %zu %c %zu", + afn, bfn, (size_t)asb.st_size, + asb.st_size < bsb.st_size ? '<' : '>', + (size_t)bsb.st_size); + } + goto done; + } + off = 0; + do { + if ((alen = read(afd, abuf, sizeof(abuf))) < 0) { + warn("%s", afn); + goto done; + } + if ((blen = read(bfd, bbuf, sizeof(bbuf))) < 0) { + warn("%s", bfn); + goto done; + } + if (alen != blen) { + /* either or both have changed since fstat() */ + if (vflag) + warnx("%s and %s differ in length\n", afn, bfn); + goto done; + } + if (memcmp(abuf, bbuf, alen) != 0) { + if (vflag) { + for (i = 0; i < alen; i++) + if (abuf[i] != bbuf[i]) + break; + warnx("%s and %s differ at offset %zu\n", + afn, bfn, (size_t)(off + i)); + } + goto done; + } + off += alen; + } while (alen > 0); + if (vflag) + warnx("%s and %s are identical", afn, bfn); + identical = true; +done: + if (afd >= 0) + close(afd); + if (bfd >= 0) + close(bfd); + return (identical); +} + +static void +usage(void) +{ + fprintf(stderr, "usage: preserve [-v] [-o file] command [args]\n"); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + char *fn = NULL, *tmp = NULL; + pid_t pid; + int opt, serrno, wstatus; + + while ((opt = getopt(argc, argv, "o:v")) != -1) + switch (opt) { + case 'o': + fn = optarg; + break; + case 'v': + vflag = true; + break; + default: + usage(); + } + + argc -= optind; + argv += optind; + if (argc == 0) + usage(); + + if (fn != NULL) { + if (asprintf(&tmp, "%s.tmp", fn) < 0) + err(1, "asprintf()"); + if (freopen(tmp, "w", stdout) == NULL) + err(1, "%s", tmp); + } else if (argc > 1) { + /* look for output file name in arguments */ + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-' && argv[i][1] == 'o') { + if (argv[i][2] != '\0') { + fn = &argv[i][2]; + if (asprintf(&tmp, "-o%s.tmp", fn) < 0) + err(1, "asprintf()"); + argv[i] = tmp; + tmp += 2; + } else if (i + 1 < argc) { + fn = argv[i + 1]; + if (asprintf(&tmp, "%s.tmp", fn) < 0) + err(1, "asprintf()"); + argv[i + 1] = tmp; + } + break; + } + } + if (fn == NULL && argv[argc - 1][0] != '-') { + fn = argv[argc - 1]; + if (asprintf(&tmp, "%s.tmp", fn) < 0) + err(1, "asprintf()"); + argv[argc - 1] = tmp; + } + } + if (fn == NULL) { + /* still no output file, just run the original command */ + if (vflag) + warnx("unable to determine output file name"); + execvp(argv[0], argv); + err(1, "%s", argv[0]); + } + + if ((pid = fork()) < 0) + err(1, "fork()"); + if (pid == 0) { + /* child */ + execvp(argv[0], argv); + err(1, "%s", argv[0]); + } + + /* parent */ + setprogname(argv[0]); + (void)freopen("/dev/null", "a", stdout); + + if (waitpid(pid, &wstatus, 0) < 0) + err(1, "waitpid()"); + if (WIFSIGNALED(wstatus)) { + (void)unlink(tmp); + raise(WTERMSIG(wstatus)); + } + if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { + (void)unlink(tmp); + exit(wstatus & 0xff); + } + if (files_are_identical(fn, tmp)) { + if (vflag) + warnx("removing %s", tmp); + (void)unlink(tmp); + } else { + if (vflag) + warnx("replacing %s with %s", fn, tmp); + if (rename(tmp, fn) != 0) { + serrno = errno; + (void)unlink(tmp); + errno = serrno; + err(1, "%s", fn); + } + } + exit(0); +}