Created
June 25, 2022 00:53
-
-
Save ryancdotorg/61b0c02713d8ac815a02d7c16562f18f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright ©2022 Ryan Castellucci, some rights reserved. | |
* Written using code for `tee` from GNU coreutils as refrence. | |
* I stand by my signature of https://rms-open-letter.github.io/ | |
* Licensed GPLv2, because I did literally copy things from `tee`. | |
* 🏳️⚧️ */ | |
#include <stdio.h> | |
#include <signal.h> | |
#include <string.h> | |
#include <stdbool.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
#include <errno.h> | |
#include <fcntl.h> | |
#define PROGRAM_NAME "sav" | |
#define VERSION_MAJOR 0 | |
#define VERSION_MINOR 1 | |
#define VERSION_PATCH 0 | |
#define _STR(X) #X | |
#define STR(X) _STR(X) | |
#ifndef VERSION | |
#define VERSION STR(VERSION_MAJOR) "." STR(VERSION_MINOR) "." STR(VERSION_PATCH) | |
#endif | |
static void cursed() { | |
#ifndef BUT_I_LIKE_CURSED | |
fprintf(stderr, | |
"Look, I know that the GNU coreutils folks think it's fine, but interpreting\n" | |
"a bare '-' as a filename when followed by other options is a level of \"cursed\"\n" | |
"behaviour that I'm unwilling to be a party to.\n" | |
); | |
exit(EXIT_FAILURE); | |
#endif | |
} | |
static void usage(char *arg0, int status) { | |
fprintf(status == EXIT_SUCCESS ? stdout : stderr, | |
"Usage: %s [OPTION]... FILE [FILE]...\n" | |
"Copy standard input to each FILE -- `tee` except it doesn't write to stdout\n" | |
"Intended to replace use of `sudo tee ... > /dev/null` with `sudo %s ...`.\n\n" | |
" -a, --append append to the given FILEs, do not overwrite\n" | |
" -i, --ignore-interrupts ignore interrupt signals\n" | |
" --output-error[=MODE] set behavior on write error. See MODE below\n" | |
" -h, --help display this help and exit\n" | |
" -V, --version output version information and exit\n\n" | |
"MODE determines behavior with write errors on the outputs:\n" | |
" 'warn' diagnose errors writing to any output\n" | |
" 'warn-nopipe' same as 'warn', included for compatibility with `tee`\n" | |
" 'exit' exit on error writing to any output\n" | |
" 'exit-nopipe' same as 'exit', included for compatibility with `tee`\n" | |
"The default MODE is 'warn'.\n" | |
, arg0, arg0 | |
); | |
exit(status); | |
} | |
static int strmatch(const char *s1, const char *s2) { | |
for (size_t p = 0;;) { | |
if (s1[p] != s2[p]) { | |
return 0; | |
} else if (s1[p] == 0) { // implied: s1[p] == s2[p], so s2[p] == 0 if true | |
return 1; | |
} else { | |
++p; | |
} | |
} | |
} | |
static int sav_files(int nfiles, char **files, bool opt_append, bool opt_exit) { | |
FILE *desc, **descs; | |
int ndescs = 0, errors = 0, outputs = 0; | |
ssize_t count; | |
char buf[BUFSIZ]; | |
if ((descs = calloc(nfiles, sizeof(*descs))) == NULL) { | |
fprintf(stderr, "calloc failed (desc)\n"); | |
exit(EXIT_FAILURE); | |
} | |
for (int i = 0; i < nfiles; ++i) { | |
if ((desc = fopen(files[i], opt_append ? "a" : "w")) == NULL) { | |
++errors; | |
fprintf(stderr, "Error opening `%s`: %s\n", files[i], strerror(errno)); | |
if (opt_exit) exit(EXIT_FAILURE); | |
} else { | |
posix_fadvise(fileno(desc), 0, 0, POSIX_FADV_SEQUENTIAL); | |
setvbuf(desc, NULL, _IONBF, 0); | |
descs[ndescs++] = desc; | |
++outputs; | |
} | |
} | |
while (outputs) { | |
count = read(STDIN_FILENO, buf, sizeof(buf)); | |
if (count < 0 && errno == EINTR) { | |
continue; | |
} else if (count <= 0) { | |
break; | |
} | |
for (int i = 0; i < ndescs; i++) { | |
if (descs[i] == NULL) continue; | |
errno = 0; | |
if (fwrite(buf, count, 1, descs[i]) != 1) { | |
++errors; | |
fprintf(stderr, "Failed to write `%s`: %s\n", files[i], strerror(errno)); | |
errno = 0; | |
if (opt_exit) return EXIT_FAILURE; | |
descs[i] = NULL; | |
--outputs; | |
} | |
} | |
} | |
if (count < 0) { | |
++errors; | |
if (errno) { | |
fprintf(stderr, "read error: %s\n", strerror(errno)); | |
} else { | |
fprintf(stderr, "read error\n"); | |
} | |
} | |
for (int i = 0; i < ndescs; i++) { | |
if (descs[i] == NULL) continue; | |
errno = 0; | |
if (fclose(descs[i]) != 0) { | |
++errors; | |
fprintf(stderr, "Failed to close `%s`: %s\n", files[i], strerror(errno)); | |
} | |
} | |
free(descs); | |
return errors ? EXIT_FAILURE : EXIT_SUCCESS; | |
} | |
int main(int argc, char *argv[]) { | |
bool opt_append = false, opt_nointr = false, opt_exit = false; | |
bool bare_dash = false; | |
int r = 0, nfiles = 0; | |
char **files; | |
if ((files = calloc(argc, sizeof(*files))) == NULL) { | |
fprintf(stderr, "calloc failed (files)\n"); | |
exit(EXIT_FAILURE); | |
} | |
// hand rolling this is easier than reading docs for a standard parser | |
for (int n = 1, parsing_files = false; n < argc; ++n) { | |
char *opt = argv[n]; | |
if (opt[0] != '-' || parsing_files) { | |
files[nfiles++] = opt; | |
} else { | |
if (opt[1] == '\0') { | |
files[nfiles++] = opt; | |
bare_dash = true; | |
} else if (opt[1] == '-') { | |
if (bare_dash) cursed(); | |
if (strmatch("--append", opt)) { goto case_a; } | |
else if (strmatch("--ignore-interrupts", opt)) { goto case_i; } | |
else if (strmatch("--help", opt)) { goto case_h; } | |
else if (strmatch("--version", opt)) { goto case_V; } | |
else if (strmatch("--", opt)) { parsing_files = true; } | |
/* do nothing */ | |
else if (strmatch("--output-error", opt)) { continue; } | |
else if (strmatch("--output-error=warn", opt)) { opt_exit = false; } | |
else if (strmatch("--output-error=warn-pipe", opt)) { opt_exit = false; } | |
else if (strmatch("--output-error=exit", opt)) { opt_exit = true; } | |
else if (strmatch("--output-error=exit-pipe", opt)) { opt_exit = true; } | |
else if (strncmp("--output-error=", opt, 15) == 0) { continue; } | |
/* unrecognized option */ | |
else { | |
fprintf(stderr, "%s: unrecognized option '%s'\n", argv[0], opt); | |
usage(argv[0], EXIT_FAILURE); | |
} | |
} else { | |
char c; | |
if (bare_dash) cursed(); | |
for (int p = 1; (c = opt[p]) != '\0'; ++p) { | |
switch (c) { | |
case 'a': | |
case_a: opt_append = true; | |
break; | |
case 'i': | |
case_i: opt_nointr = true; | |
break; | |
case 'h': | |
case_h: usage(argv[0], EXIT_SUCCESS); | |
break; | |
case 'V': | |
case_V: printf("%s", PROGRAM_NAME " " VERSION "\n"); | |
return EXIT_SUCCESS; | |
break; | |
/* ignored for compatibility */ | |
case 'p': continue; break; | |
default: | |
fprintf(stderr, "%s: invalid option -- '%c'\n", argv[0], c); | |
usage(argv[0], EXIT_FAILURE); | |
break; | |
} // switch | |
} // for opt char | |
} // which type of opt | |
} // if opt | |
} // for opt | |
// Perhaps the user would like to ignore SIGINT (Ctrl-C)? | |
if (opt_nointr) signal(SIGINT, SIG_IGN); | |
posix_fadvise(STDIN_FILENO, 0, 0, POSIX_FADV_SEQUENTIAL); | |
// POSIX requires `tee` to handle zero files, so do we do, too | |
r = sav_files(nfiles, files, opt_append, opt_exit); | |
free(files); | |
return r; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment