Skip to content

Instantly share code, notes, and snippets.

@ryancdotorg
Created June 25, 2022 00:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryancdotorg/61b0c02713d8ac815a02d7c16562f18f to your computer and use it in GitHub Desktop.
Save ryancdotorg/61b0c02713d8ac815a02d7c16562f18f to your computer and use it in GitHub Desktop.
/* 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