/* * Copyright (c) 2024 DTB * SPDX-License-Identifier: AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ #include /* errno */ #include /* signal(2), SIG_ERR, SIG_IGN, SIGINT */ #include /* fclose(3), fopen(3), fprintf(3), getc(3), putc(3), * setvbuf(3), size_t, _IONBF, NULL */ #include /* free(3), realloc(3) */ #include /* strcmp(3), strerror(3) */ #include /* getopt(3) */ #if !defined EX_IOERR || !defined EX_OK || !defined EX_OSERR \ || !defined EX_USAGE # include #endif extern int errno; /* This structure is how open files are tracked. */ struct Files{ size_t a; /* allocation */ size_t s; /* used size */ char *mode; /* file opening mode */ char **names; /* file names */ FILE **files; /* file pointers */ }; /* How much to grow the allocation when it's saturated. */ #ifndef ALLOC_INCREMENT # define ALLOC_INCREMENT 1 #endif /* How much to grow the allocation at program start. */ #ifndef ALLOC_INITIAL # define ALLOC_INITIAL 10 #endif /* pre-allocated strings */ static char *program_name = ""; static char *stdin_name = ""; static char *stdout_name = ""; static char *stderr_name = ""; static char *(fmode[]) = { (char []){"rb"}, (char []){"rb+"} }; static char *wharsh = "wb"; /* Adds the open FILE pointer for the file at the path s to the files struct, * returning the FILE if successful and NULL if not, allocating more memory in * the files buffers as needed. */ static FILE * Files_append(struct Files *files, FILE *file, char *name){ if(file == NULL || (files->s == files->a && ((files->files = realloc(files->files, (files->a += (files->a == 0) ? ALLOC_INITIAL : ALLOC_INCREMENT) * sizeof *(files->files))) == NULL || (files->names = realloc(files->names, files->a * sizeof *(files->names))) == NULL))) return NULL; files->names[files->s] = name; return files->files[files->s++] = file; } /* Opens the file at the path p and puts it in the files struct, returning NULL * if either the opening or the placement of the open FILE pointer fail. */ #define Files_open(files, p) \ Files_append((files), fopen((p), (files)->mode), (p)) /* Prints a diagnostic message based on errno and returns an exit status * appropriate for an OS error. */ static int oserr(char *s, char *r){ fprintf(stderr, "%s: %s: %s\n", s, r, strerror(errno)); return EX_OSERR; } /* Hijacks i and j from main and destructs the files[2] struct used by main by * closing its files and freeing its files and names arrays, returning retval * from main. */ #define terminate \ for(i = 0; i < 2; ++i){ \ for(j = 0; j < files[i].s; ++j) \ if(files[i].files[j] != stdin \ && files[i].files[j] != stdout \ && files[i].files[j] != stderr) \ fclose(files[i].files[j]); \ free(files[i].files); \ free(files[i].names); \ } \ return retval int main(int argc, char *argv[]){ int c; struct Files files[2]; /* {read, write} */ size_t i; size_t j; size_t k; /* loop index but also unbuffer status */ int retval; /* Initializes the files structs with their default values, standard * input and standard output. If an input or an output is specified * these initial values will be overwritten, so to, say, use mm(1) * equivalently to tee(1p), -o - will need to be specified before * additional files to ensure standard output is still written. */ for(i = 0; i < 2; ++i){ files[i].a = 0; files[i].s = 0; files[i].mode = fmode[i]; files[i].files = NULL; files[i].names = NULL; Files_append(&files[i], i == 0 ? stdin : stdout, i == 0 ? stdin_name : stdout_name); files[i].s = 0; } k = 0; if(argc > 0) program_name = argv[0]; if(argc > 1) while((c = getopt(argc, argv, "aehi:no:u")) != -1) switch(c){ case 'a': /* "rb+" -> "ab" */ files[1].mode[0] = 'a'; files[1].mode[2] = '\0'; break; case 'e': if(Files_append(&files[1], stderr, stderr_name) != NULL) break; retval = oserr(argv[0], "-e"); terminate; case 'i': if((strcmp(optarg, "-") == 0 && Files_append(&files[0], stdin, stdin_name) != NULL) || Files_open(&files[0], optarg) != NULL) break; retval = oserr(argv[0], optarg); terminate; case 'o': if((strcmp(optarg, "-") == 0 && Files_append(&files[1], stdout, stdout_name) != NULL) || Files_open(&files[1], optarg) != NULL) break; /* does not exist, so try to create it */ if(errno == ENOENT){ files[1].mode = wharsh; if(Files_open(&files[1], optarg) != NULL){ files[1].mode = fmode[1]; break; } } retval = oserr(argv[0], optarg); terminate; case 'n': if(signal(SIGINT, SIG_IGN) != SIG_ERR) break; retval = oserr(argv[0], "-n"); terminate; case 'u': k = 1; break; default: fprintf(stderr, "Usage: %s (-aenu) (-i [input])..." " (-o [output])...\n", argv[0]); retval = EX_USAGE; terminate; } files[0].s += files[0].s == 0; files[1].s += files[1].s == 0; /* Unbuffer files. */ if(k){ for(i = 0; i < files[0].s; setvbuf(files[0].files[i++], NULL, _IONBF, 0)); for(i = 0; i < files[1].s; setvbuf(files[1].files[i++], NULL, _IONBF, 0)); } retval = EX_OK; /* Actual program loop. */ for(i = 0; i < files[0].s; ++i) /* iterate ins */ while((c = getc(files[0].files[i])) != EOF) /* iterate chars */ for(j = 0; j < files[1].s; ++j) /* iterate outs */ if(putc(c, files[1].files[j]) == EOF){ /* notebook's full */ retval = EX_IOERR; fprintf(stderr, "%s: %s: %s\n", program_name, files[1].names[j], strerror(errno)); if(fclose(files[1].files[j]) == EOF) fprintf(stderr, "%s: %s: %s\n", program_name, files[1].names[j], strerror(errno)); /* massage out the tense muscle */ for(k = j--; k < files[1].s - 1; ++k){ files[1].files[k] = files[1].files[k+1]; files[1].names[k] = files[1].names[k+1]; } if(--files[1].s == 0) terminate; } terminate; }