diff --git a/Makefile b/Makefile
index 5e2c70d..a523591 100644
--- a/Makefile
+++ b/Makefile
@@ -114,8 +114,8 @@ build/bin/intcmp: src/intcmp.c build
.PHONY: mm
mm: build/bin/mm
-build/bin/mm: src/mm.c build
- $(CC) $(CFLAGS) -o $@ src/mm.c
+build/bin/mm: src/mm.rs build rustlibs
+ $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/mm.rs
.PHONY: npc
npc: build/bin/npc
diff --git a/docs/mm.1 b/docs/mm.1
index 2916aa7..f641156 100644
--- a/docs/mm.1
+++ b/docs/mm.1
@@ -3,14 +3,14 @@
.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license,
.\" visit .
.\"
-.TH MM 1 2024-06-17 "Harakit X.X.X"
+.TH MM 1 2024-07-14 "Harakit X.X.X"
.SH NAME
mm \(en middleman
.\"
.SH SYNOPSIS
mm
-.RB [ -aenu ]
+.RB [ -aetu ]
.RB [ -i\ input ]
.RB [ -o\ output ]
.\"
@@ -21,19 +21,25 @@ Catenate input files and write them to the start of each output file or stream.
.SH OPTIONS
.IP \fB-a\fP
-Opens subsequent outputs for appending rather than updating.
+Opens outputs for appending rather than updating.
.IP \fB-e\fP
Use the standard error as an output.
-.IP \fB-i\fP\ \fIinput\fP
-Opens a path as an input. If one or more of the input files is \(lq-\(rq or if
-no inputs are specified, the standard input shall be used.
-.IP \fB-o\fP\ \fIoutput\fP
-Opens a path as an output. If one or more of the output files is \(lq-\(rq or if
-no outputs are specified, the standard output shall be used.
+.IP \fB-t\fP
+Causes outputs to be overwritten instead of being truncated.
.IP \fB-u\fP
Ensures neither input or output will be buffered.
-.IP \fB-n\fP
-Causes SIGINT signals to be ignored.
+.IP \fB-i\fP\ \fIinput\fP
+Opens a path as an input. If one or more of the input files is \(lq-\(rq or if
+no inputs are specified, the standard input shall be used. If specified as the
+last option and if there are trailing arguments to the program, they shall be
+appended to the list of files to use as inputs.
+.IP \fB-o\fP\ \fIoutput\fP
+Opens a path as an output. If one or more of the output files is \(lq-\(rq or if
+no outputs are specified and the
+.B -e
+option is not specified, the standard output shall be used. If specified as the
+last option and if there are trailing arguments to the program, they shall be
+appended to the list of files to use as outputs.
.\"
.SH DIAGNOSTICS
@@ -45,10 +51,6 @@ exits with the appropriate
.BR sysexits.h (3)
status.
.\"
-.SH CAVEATS
-
-Existing files are not truncated on ouput and are instead overwritten.
-.\"
.SH RATIONALE
The
diff --git a/src/mm.c b/src/mm.c
deleted file mode 100644
index e905b35..0000000
--- a/src/mm.c
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * 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
-
-/* Prints a usage text, in which s is the program being run (i.e. argv[0]), and
- * returns an exit status appropriate for a usage error. */
-int usage(char *s){
-
- fprintf(stderr, "Usage: %s [-aenu] [-i input]... [-o output]...\n", s);
-
- return EX_USAGE;
-}
-
-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:
- retval = usage(argv[0]);
- terminate;
- }
-
- if(optind != argc){
- retval = usage(argv[0]);
- 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;
-}
diff --git a/src/mm.rs b/src/mm.rs
new file mode 100644
index 0000000..41b7857
--- /dev/null
+++ b/src/mm.rs
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2024 Emma Tebibyte
+ * 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/.
+ */
+
+use std::{
+ env::args,
+ fs::File,
+ io::{ stdin, stdout, stderr, BufWriter, Read, Write },
+ os::fd::{ AsRawFd, FromRawFd },
+ process::{ exit, ExitCode },
+};
+
+extern crate getopt;
+extern crate strerror;
+extern crate sysexits;
+
+use getopt::GetOpt;
+use strerror::StrError;
+use sysexits::{ EX_IOERR, EX_USAGE };
+
+use ArgMode::*;
+
+enum ArgMode { In, Out }
+
+fn main() -> ExitCode {
+ let argv = args().collect::>();
+ let usage = format!("Usage: {} [-aetu] [-i input] [-o output]", argv[0]);
+
+ let mut a = false; /* append to the file */
+ let mut e = false; /* use stderr as an output */
+ let mut t = true; /* do not truncate the file before writing */
+ let mut u = false; /* unbuffer i/o */
+ let mut ins = Vec::new(); /* initial input file path vector */
+ let mut outs = Vec::new(); /* initial output file path vector */
+ let mut mode: Option = None; /* mode set by last-used option */
+ let mut optind = 0;
+
+ while let Some(opt) = argv.getopt("aei:o:tu") {
+ match opt.opt() {
+ Ok("a") => a = true,
+ Ok("e") => e = true,
+ Ok("u") => u = true,
+ Ok("t") => t = false,
+ Ok("i") => { /* add inputs */
+ let input = opt.arg().unwrap();
+ ins.push(input);
+ mode = Some(In); /* latest argument == -i */
+ },
+ Ok("o") => { /* add output */
+ let output = opt.arg().unwrap();
+ outs.push(output);
+ mode = Some(Out); /* latest argument == -o */
+ },
+ Err(_) | Ok(_) => {
+ eprintln!("{}", usage);
+ return ExitCode::from(EX_USAGE as u8);
+ },
+ };
+
+ optind = opt.ind();
+ }
+
+ let remaining = argv.iter().skip(optind);
+
+ /* check the last flag specified */
+ if let Some(m) = mode {
+ for arg in remaining {
+ /* move the subsequent arguments to the list of inputs or outputs */
+ match m {
+ In => ins.push(arg.to_string()),
+ Out => outs.push(arg.to_string()),
+ };
+ }
+ } else {
+ eprintln!("{}", usage);
+ return ExitCode::from(EX_USAGE as u8);
+ }
+
+ /* use stdin if no inputs are specified */
+ if ins.is_empty() { ins.push("-".to_string()); }
+
+ /* use stdout if no outputs are specified */
+ if outs.is_empty() && !e { outs.push("-".to_string()); }
+
+ /* map all path strings to files */
+ let inputs = ins.iter().map(|file| {
+ /* if a file is “-”, it is stdin */
+ if *file == "-" {
+ /* portable way to access stdin as a file */
+ return unsafe { File::from_raw_fd(stdin().as_raw_fd()) };
+ }
+
+ match File::open(file) {
+ Ok(f) => f,
+ Err(e) => {
+ eprintln!("{}: {}: {}", argv[0], file, e.strerror());
+ exit(EX_IOERR);
+ },
+ }
+ }).collect::>();
+
+ /* map all path strings to files */
+ let mut outputs = outs.iter().map(|file| {
+ /* of a file is “-”, it is stdout */
+ if *file == "-" {
+ /* portable way to access stdout as a file */
+ return unsafe { File::from_raw_fd(stdout().as_raw_fd()) };
+ }
+
+ let options = File::options()
+ /* don’t truncate if -t is specified, append if -a is specified */
+ .truncate(t)
+ .append(a)
+ /* enable the ability to create and write to files */
+ .create(true)
+ .write(true)
+ /* finally, open the file! */
+ .open(file);
+
+ match options {
+ Ok(f) => return f,
+ Err(e) => {
+ eprintln!("{}: {}: {}", argv[0], file, e.strerror());
+ exit(EX_IOERR);
+ },
+ };
+ }).collect::>();
+
+ /* if -e is specified, use stderr */
+ if e {
+ /* portable way to access stderr as a file */
+ outputs.push(unsafe { File::from_raw_fd(stderr().as_raw_fd()) });
+ }
+
+ let mut outputs = outputs.iter().map(|o| {
+ if u {
+ /* unbuffered writing through a buffer of capacity 0 */
+ BufWriter::with_capacity(0, o)
+ } else {
+ /* theoretically buffered writing */
+ BufWriter::new(o)
+ }
+ }).collect::>();
+
+ for file in inputs {
+ for byte in file.bytes().map(|b| {
+ b.unwrap_or_else(|e| {
+ eprintln!("{}: {}", argv[0], e.strerror());
+ exit(EX_IOERR);
+ })
+ }) {
+ for out in &mut outputs {
+ if let Err(e) = out.write(&[byte]) {
+ eprintln!("{}: {}", argv[0], e.strerror());
+ return ExitCode::from(EX_IOERR as u8);
+ }
+
+ if u {
+ /* immediately flush the output for -u */
+ if let Err(e) = out.flush() {
+ eprintln!("{}: {}", argv[0], e.strerror());
+ return ExitCode::from(EX_IOERR as u8);
+ }
+ }
+ }
+ }
+ }
+
+ ExitCode::SUCCESS
+}