diff --git a/Makefile b/Makefile index 159865e..cbfe3a7 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ CC=cc RUSTC=rustc .PHONY: all -all: dj false fop hru intcmp rpn scrut str strcmp swab true +all: dj false fop hru intcmp mm rpn scrut str strcmp swab true build: # keep build/include until bindgen(1) has stdin support @@ -86,6 +86,11 @@ intcmp: build/bin/intcmp build/bin/intcmp: src/intcmp.c build $(CC) $(CFLAGS) -o $@ src/intcmp.c +.PHONY: mm +mm: build/bin/mm +build/bin/mm: src/mm.c build + $(CC) $(CFLAGS) -o $@ src/mm.c + .PHONY: rpn rpn: build/bin/rpn build/bin/rpn: src/rpn.rs build build/o/libsysexits.rlib diff --git a/docs/mm.1 b/docs/mm.1 new file mode 100644 index 0000000..2244588 --- /dev/null +++ b/docs/mm.1 @@ -0,0 +1,76 @@ +.\" Copyright (c) 2024 DTB +.\" +.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, +.\" visit . + +.TH mm 1 + +.SH NAME + +mm \(en middleman + +.SH SYNOPSIS + +mm +.RB ( -aenu ) +.RB ( -i +.RB [ input ]) +.RB ( -o +.RB [ output ]) + +.SH DESCRIPTION + +Mm catenates input files and writes them to the start of each output file. + +.SH OPTIONS + +Mm, upon receiving the +.B -a +option, will open subsequent outputs for appending rather than updating. +.PP +The +.B -i +option opens a path as an input. Without any inputs specified mm will use +standard input. Standard input itself can be specified by giving the path '-'. +.PP +The +.B -o +option opens a path as an output. Without any outputs specified mm will use +standard output. Standard output itself can be specified by giving the +path '-'. Standard error itself can be specified with the +.B -e +option. +.PP +The +.B -u +option ensures neither input or output will be buffered. +.PP +The +.B -n +option tells mm to ignore SIGINT signals. + +.SH DIAGNOSTICS + +If an output can no longer be written mm prints a diagnostic message, ceases +writing to that particular output, and if there are more outputs specified, +continues, eventually exiting unsuccessfully. +.PP +On error mm prints a diagnostic message and exits with the appropriate +sysexits.h(3) status. + +.SH BUGS + +Mm does not truncate existing files, which may lead to unexpected results. + +.SH RATIONALE + +Mm was modeled after the cat and tee utilities specified in POSIX. + +.SH COPYRIGHT + +Copyright (c) 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later +. + +.SH SEE ALSO + +cat(1p), dd(1), dj(1), tee(1p) diff --git a/src/mm.c b/src/mm.c new file mode 100644 index 0000000..ff62148 --- /dev/null +++ b/src/mm.c @@ -0,0 +1,224 @@ +/* + * 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; +}