Compare commits

..

15 Commits

27 changed files with 299 additions and 817 deletions

View File

@@ -97,9 +97,10 @@ their editor or terminal.
For usage text and help messages, do not implement a -h option. Instead, print
usage information when any erroneous option is specified. Follow the NetBSD
style guide for the usage texts output format [1].
style guide for the usage texts output format [0].
[1] <http://cvsweb.netbsd.org/bsdweb.cgi/~checkout~/src/share/misc/style>
If committing a new utility, please include tests and documentation (see
tests/ and docs/) for the new tool.
If committing a new source file, format the commit message following these
guidelines:
@@ -128,6 +129,7 @@ $ git commit -m 'tool(1): fix #42 & add feature x'
Commit messages should be written in the present tense.
[0] <http://cvsweb.netbsd.org/bsdweb.cgi/~checkout~/src/share/misc/style>
--
This work © 20232024 by Emma Tebibyte is licensed under CC BY-SA 4.0. To view a
copy of this license, visit <http://creativecommons.org/licenses/by-sa/4.0/>

100
Makefile
View File

@@ -1,7 +1,6 @@
# Copyright (c) 20232024 Emma Tebibyte <emma@tebibyte.media>
# Copyright (c) 20232024 DTB <trinity@trinity.moe>
# Copyright (c) 2023 Sasha Koshka <sashakoshka@tebibyte.media>
# Copyright (c) 2024 Aaditya Aryal <aryalaadi123@gmail.com>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
@@ -9,26 +8,17 @@
# notice are preserved. This file is offered as-is, without any warranty.
.POSIX:
# if using BSD make(1), remove these pragmas because they break it
.PRAGMA: posix_202x # future POSIX standard support à la pdpmake(1)
.PRAGMA: command_comment # breaks without this?
DESTDIR ?= dist
PREFIX ?= /usr/local
PREFIX=/usr/local
SYSEXITS != printf '\043include <sysexits.h>\n' | cpp -M - | sed 's/ /\n/g' \
| sed -n 's/sysexits\.h//p' || printf 'include\n'
CC ?= cc
RUSTC ?= rustc
RUSTLIBS = --extern getopt=build/o/libgetopt.rlib \
--extern sysexits=build/o/libsysexits.rlib \
--extern strerror=build/o/libstrerror.rlib
CFLAGS += -I$(SYSEXITS)
CC=cc
MAKE=make
RUSTC=rustc
.PHONY: all
all: dj false fop hru intcmp mm npc rpn scrut str strcmp swab true
all: dj false fop hru intcmp rpn scrut str strcmp true
build:
# keep build/include until bindgen(1) has stdin support
@@ -37,40 +27,36 @@ build:
.PHONY: clean
clean:
rm -rf build dist
rm -rf build/ dist/
dist: all
mkdir -p $(DESTDIR)/$(PREFIX)/bin $(DESTDIR)/$(PREFIX)/share/man/man1
cp build/bin/* $(DESTDIR)/$(PREFIX)/bin
cp docs/*.1 $(DESTDIR)/$(PREFIX)/share/man/man1
mkdir -p dist/bin dist/share/man/man1
cp build/bin/* dist/bin/
cp docs/*.1 dist/share/man/man1/
.PHONY: install
install: dist
cp -r $(DESTDIR)/* /
mkdir -p $(PREFIX)
cp -r dist/* $(PREFIX)/
.PHONY: test
test: build
tests/posix-compat.sh
test: all
tests/test.sh
$(RUSTC) --test src/getopt-rs/lib.rs -o build/test/getopt
.PHONY: rustlibs
rustlibs: build/o/libsysexits.rlib build/o/libgetopt.rlib \
build/o/libstrerror.rlib
build/o/libgetopt.rlib: build src/getopt-rs/lib.rs
$(RUSTC) $(RUSTFLAGS) --crate-type=lib --crate-name=getopt \
-o $@ src/getopt-rs/lib.rs
build/o/libstrerror.rlib: build src/strerror.rs
$(RUSTC) $(RUSTFLAGS) --crate-type=lib -o $@ \
src/strerror.rs
build/o/libsysexits.rlib: build $(SYSEXITS)sysexits.h
build/o/libsysexits.rlib: build
# bandage solution until bindgen(1) gets stdin support
printf '#define EXIT_FAILURE 1\n' | cat - $(SYSEXITS)sysexits.h \
printf '#define EXIT_FAILURE 1\n' | cat - include/sysexits.h \
> build/include/sysexits.h
bindgen --default-macro-constant-type signed --use-core --formatter=none \
build/include/sysexits.h | $(RUSTC) $(RUSTFLAGS) --crate-type lib -o $@ -
"$$(printf '#include <sysexits.h>\n' \
| cpp -M -idirafter "build/include" - \
| sed 's/ /\n/g' | grep sysexits.h)" \
| $(RUSTC) $(RUSTFLAGS) --crate-type lib -o build/o/libsysexits.rlib -
build/o/libgetopt.rlib: src/getopt-rs/lib.rs
$(RUSTC) $(RUSTFLAGS) --crate-type=lib --crate-name=getopt \
-o build/o/libgetopt.rlib src/getopt-rs/lib.rs
.PHONY: dj
dj: build/bin/dj
@@ -84,38 +70,29 @@ build/bin/false: src/false.c build
.PHONY: fop
fop: build/bin/fop
build/bin/fop: src/fop.rs build rustlibs
$(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/fop.rs
build/bin/fop: src/fop.rs build build/o/libgetopt.rlib build/o/libsysexits.rlib
$(RUSTC) $(RUSTFLAGS) --extern getopt=build/o/libgetopt.rlib \
--extern sysexits=build/o/libsysexits.rlib \
-o $@ src/fop.rs
.PHONY: hru
hru: build/bin/hru
build/bin/hru: src/hru.rs build rustlibs
$(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/hru.rs
build/bin/hru: src/hru.rs build build/o/libgetopt.rlib build/o/libsysexits.rlib
$(RUSTC) $(RUSTFLAGS) --extern getopt=build/o/libgetopt.rlib \
--extern sysexits=build/o/libsysexits.rlib \
-o $@ src/hru.rs
.PHONY: intcmp
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: npc
npc: build/bin/npc
build/bin/npc: src/npc.c build
$(CC) $(CFLAGS) -o $@ src/npc.c
.PHONY: peek
peek: build/bin/peek
build/bin/peek: src/peek.c build
$(CC) $(CFLAGS) -o $@ src/peek.c
.PHONY: rpn
rpn: build/bin/rpn
build/bin/rpn: src/rpn.rs build rustlibs
$(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/rpn.rs
build/bin/rpn: src/rpn.rs build build/o/libsysexits.rlib
$(RUSTC) $(RUSTFLAGS) \
--extern sysexits=build/o/libsysexits.rlib \
-o $@ src/rpn.rs
.PHONY: scrut
scrut: build/bin/scrut
@@ -132,13 +109,6 @@ strcmp: build/bin/strcmp
build/bin/strcmp: src/strcmp.c build
$(CC) $(CFLAGS) -o $@ src/strcmp.c
.PHONY: swab
swab: build/bin/swab
build/bin/swab: src/swab.rs build build/o/libsysexits.rlib
$(RUSTC) $(RUSTFLAGS) --extern getopt=build/o/libgetopt.rlib \
--extern sysexits=build/o/libsysexits.rlib \
-o $@ src/swab.rs
.PHONY: true
true: build/bin/true
build/bin/true: src/true.c build

View File

@@ -1,76 +0,0 @@
.\" Copyright (c) 2024 DTB <trinity@trinity.moe>
.\"
.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license,
.\" visit <http://creativecommons.org/licenses/by-sa/4.0/>.
.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
<https://gnu.org/licenses/agpl.html>.
.SH SEE ALSO
cat(1p), dd(1), dj(1), tee(1p)

View File

@@ -1,90 +0,0 @@
.\" Copyright (c) 2023-2024 DTB <trinity@trinity.moe>
.\"
.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license,
.\" visit <http://creativecommons.org/licenses/by-sa/4.0/>.
.TH PEEK 1
.SH NAME
peek \(en read from the standard input, furtively
.SH SYNOPSIS
peek
.RB ( -i )
.SH DESCRIPTION
Peek reads input from the standard input with terminal echo disabled.
.SH OPTIONS
.B -i
.RS
Allows input to come from sources other than terminals (i.e. a pipe).
.RE
.SH DIAGNOSTICS
Peek prints an error message and exits with the appropriate status from
sysexits.h(3) if it encounters an unrecoverable error.
Peek tries to handle the signal SIGINT (^C) to ensure the user's terminal is
still usable after premature termination; if the signal can't be handled, it
prints an error message and continues. If peek is interrupted, it exits
unsuccessfully, without an error message.
.SH RATIONALE
This tool was originally written to accept passwords in shell scripts, as an
extremely simple alternative to the GNU Privacy Guard project's pinentry(1).
Accepting input without showing what is being typed is useful when keying in
secrets in public settings or places with installed surveillance cameras.
.SH BUGS
This does nothing to prevent others seeing the keyboard being used to input
secrets or mask the sound of typing. Audio or video recordings of typing can be
used to determine what was input without needing to see the characters appear
on the screen.
Accepting secrets in shell scripts is probably not adviseable.
On systems that support it, the ioctl(2) command TIOCSTI can be used to insert
characters into the standard input going to peek. This doesn't allow snooping
but can be used for general mischief.
.SH EXAMPLES
This is an sh(1p) command line that hashes a given password. It uses head(1p)
to only accept one line of input, xargs(1p) and printf(1p) to strip the
trailing newline, htpasswd(1) from Apache's utilities to hash the input with
the bcrypt algorithm, and cut(1p) to print only the resulting hash:
.RS
.R $ peek | head -n 1 | xargs printf '%s' | htpasswd -nBi _ | cut -d : -f 2
.RE
This is an sh(1p) command line that lets a user blindly write into a text file,
only able to see written lines. Some writers have the habit of prematurely
revising their work and use tools like this to prevent it. It uses mm(1) to
pipe the output of peek to both the standard error and the regular file
writing.txt.
.RS
.R $ echo Input ^D to quit. && peek | mm -eo - >writing.txt
.RE
.SH AUTHOR
Written by DTB <trinity@trinity.moe>.
.SH COPYRIGHT
Copyright (c) 2023-2024 DTB. License AGPLv3+: GNU AGPL version 3 or later
<https://gnu.org/licenses/gpl.html>.
.SH SEE ALSO
ioctl(2), ioctl_tty(2), read(1), sh(1)

View File

@@ -1,71 +0,0 @@
.\" Copyright (c) 2024 DTB <trinity@trinity.moe>
.\"
.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license,
.\" visit <http://creativecommons.org/licenses/by-sa/4.0/>.
.TH swab 1
.SH NAME
swab \(en swap bytes
.SH SYNOPSIS
swab
.RB ( -f )
.RB ( -w
.R [
.B word size
.R ])
.SH USAGE
Swab swaps the latter and former halves of a block of bytes.
.SH EXAMPLES
The following sh(1p) line:
.R printf 'hello world!\n' | swab
Produces the following output:
.R ehll oowlr!d
.SH OPTIONS
The
.B -f
option ignores system call interruptions.
.PP
The
.B -w
option configures the word size; that is, the size in bytes of the block size
on which to operate. By default the word size is 2. The word size must be
cleanly divisible by 2, otherwise the block of bytes being processed can't be
halved.
.SH DIAGNOSTICS
If an error is encountered in input, output, or invocation, a diagnostic
message will be written to standard error and swab will exit with the
appropriate status from sysexits.h(3).
.SH RATIONALE
Swab was modeled after the
.R conv=swab
functionality specified in the POSIX dd utility but additionally allows the
word size to be configured.
.PP
Swab is useful for fixing the endianness of binary files produced on other
machines.
.SH COPYRIGHT
Copyright (c) 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later
<https://gnu.org/licenses/agpl.html>.
.SH SEE ALSO
dd(1p)

View File

@@ -203,28 +203,28 @@ Io_fdseek(struct Io *io){
if(!fdisstd(io->fd) && lseek(io->fd, io->seek, SEEK_SET) != -1)
return -1;
/* repeated code to get the condition out of the loop */
if(io->fl == write_flags){
else if(io->fl == write_flags){
memset(io->buf, '\0', io->bs);
/* We're going to cheat and use bufuse as the retval for write(2),
* which is fine because it'll be zeroed as this function returns
* anyway. */
do{
if((io->bufuse = write(io->fd, io->buf, MIN(io->bs, io->seek)))
== 0)
/* second chance */
io->bufuse = write(io->fd, io->buf, MIN(io->bs, io->seek));
}while((io->seek -= io->bufuse) > 0 && io->bufuse != 0);
}else if(io->fl == read_flags){
do{
if((io->bufuse = read(io->fd, io->buf, MIN(io->bs, io->seek)))
== 0)
/* second chance */
io->bufuse = read(io->fd, io->buf, MIN(io->bs, io->seek));
}while((io->seek -= io->bufuse) > 0 && io->bufuse != 0);
/* This is a dirty trick; rather than testing conditions and operating
* likewise, because the parameters to read or write are going to be
* the same either way, just use a function pointer to keep track of
* the intended operation. */
op = (int (*)(int, void *, size_t))&write;
/* Function pointer casts are risky; this works because the difference
* is in the second parameter and only that write(2) makes the buffer
* const whereas read(2) does not. To avoid even the slightest
* undefined behavior comment out the cast, just be ready for a
* -Wincompatible-function-pointer-types if your compiler notices it.
*/
}else
return EX_SOFTWARE;
op = &read;
/* We're going to cheat and use bufuse as the retval for write(2), which is
* fine because it'll be zeroed as this function returns anyway. */
do{ if( (io->bufuse = (*op)(io->fd, io->buf, MIN(io->bs, io->seek))) == 0)
/* second chance */
io->bufuse = (*op)(io->fd, io->buf, MIN(io->bs, io->seek));
}while((io->seek -= io->bufuse) > 0 && io->bufuse != 0);
io->bufuse = 0;

View File

@@ -22,12 +22,10 @@ use std::{
process::{ Command, exit, Stdio },
};
extern crate getopt;
extern crate strerror;
extern crate sysexits;
extern crate getopt;
use getopt::{ Opt, Parser };
use strerror::StrError;
use sysexits::{ EX_DATAERR, EX_IOERR, EX_UNAVAILABLE, EX_USAGE };
fn main() {
@@ -57,7 +55,7 @@ fn main() {
});
let index = argv[index_arg].parse::<usize>().unwrap_or_else(|e| {
eprintln!("{}: {}: {}", argv[0], argv[1], e);
eprintln!("{}: {}: {}.", argv[0], argv[1], e);
exit(EX_DATAERR);
});
@@ -77,13 +75,13 @@ fn main() {
.stdout(Stdio::piped())
.spawn()
.unwrap_or_else( |e| {
eprintln!("{}: {}: {}", argv[0], argv[command_arg], e.strerror());
eprintln!("{}: {}: {}.", argv[0], argv[command_arg], e);
exit(EX_UNAVAILABLE);
});
let field = fields.get(index).unwrap_or_else(|| {
eprintln!(
"{}: {}: No such index in input",
"{}: {}: No such index in input.",
argv[0],
index.to_string(),
);
@@ -96,7 +94,7 @@ fn main() {
}
let output = spawned.wait_with_output().unwrap_or_else(|e| {
eprintln!("{}: {}: {}", argv[0], argv[command_arg], e.strerror());
eprintln!("{}: {}: {}.", argv[0], argv[command_arg], e);
exit(EX_IOERR);
});
@@ -105,7 +103,7 @@ fn main() {
if replace.pop() != Some(b'\n') { replace = output.stdout; }
let new_field = String::from_utf8(replace).unwrap_or_else(|e| {
eprintln!("{}: {}: {}", argv[0], argv[command_arg], e);
eprintln!("{}: {}: {}.", argv[0], argv[command_arg], e);
exit(EX_IOERR);
});
@@ -113,8 +111,8 @@ fn main() {
stdout().write_all(
fields.join(&d.to_string()).as_bytes()
).unwrap_or_else(|e| {
eprintln!("{}: {}", argv[0], e.strerror());
).unwrap_or_else(|e|{
eprintln!("{}: {}.", argv[0], e);
exit(EX_IOERR);
});
}

View File

@@ -23,10 +23,8 @@ use std::{
process::{ ExitCode, exit },
};
extern crate strerror;
extern crate sysexits;
use strerror::StrError;
use sysexits::{ EX_DATAERR, EX_IOERR, EX_SOFTWARE };
const LIST: [(u32, &str); 10] = [
@@ -51,7 +49,7 @@ fn convert(input: u128) -> Result<(f64, (u32, &'static str)), String> {
let c = match 10_u128.checked_pow(n) {
Some(c) => c,
None => {
return Err(format!("10^{}: Integer overflow", n.to_string()));
return Err(format!("10^{}: Integer overflow.", n.to_string()));
},
};
@@ -81,7 +79,7 @@ fn main() -> ExitCode {
f
},
Err(err) => {
eprintln!("{}: {}", argv[0], err);
eprintln!("{}: {}.", argv[0], err);
return ExitCode::from(EX_DATAERR as u8);
},
};
@@ -89,7 +87,7 @@ fn main() -> ExitCode {
let (number, prefix) = match convert(n) {
Ok(x) => x,
Err(err) => {
eprintln!("{}: {}", argv[0], err);
eprintln!("{}: {}.", argv[0], err);
return ExitCode::from(EX_SOFTWARE as u8);
},
};
@@ -100,7 +98,7 @@ fn main() -> ExitCode {
stdout().write_all(format!("{} {}\n", out, si_prefix).as_bytes())
.unwrap_or_else(|e| {
eprintln!("{}: {}", argv[0], e.strerror());
eprintln!("{}: {}.", argv[0], e);
exit(EX_IOERR);
});
}

224
src/mm.c
View File

@@ -1,224 +0,0 @@
/*
* Copyright (c) 2024 DTB <trinity@trinity.moe>
* 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.h> /* errno */
#include <signal.h> /* signal(2), SIG_ERR, SIG_IGN, SIGINT */
#include <stdio.h> /* fclose(3), fopen(3), fprintf(3), getc(3), putc(3),
* setvbuf(3), size_t, _IONBF, NULL */
#include <stdlib.h> /* free(3), realloc(3) */
#include <string.h> /* strcmp(3), strerror(3) */
#include <unistd.h> /* getopt(3) */
#if !defined EX_IOERR || !defined EX_OK || !defined EX_OSERR \
|| !defined EX_USAGE
# include <sysexits.h>
#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 = "<no argv[0]>";
static char *stdin_name = "<stdin>";
static char *stdout_name = "<stdout>";
static char *stderr_name = "<stderr>";
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;
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright (c) 20232024 DTB <trinity@trinity.moe>
* 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 <signal.h> /* sigaction(2), signal(2), struct sigaction, SIGINT */
#include <stdio.h> /* fprintf(3), fgetc(3), perror(3), fputc(3), stderr, stdin,
* stdout, EOF, NULL */
#include <stdlib.h> /* exit(3), EXIT_FAILURE */
#if !defined EX_IOERR || !defined EX_OK || !defined EX_USAGE
# include <sysexits.h>
#endif
#include <termios.h> /* tcgetattr(3), tcsetattr(3), struct termios, ECHO */
#include <unistd.h> /* getopt(3), isatty(3), STDIN_FILENO */
static char *program_name = "peek";
/* Restores terminal echo; otherwise when a user ^Cs the terminal would
* continue to not display typed text. If sig isn't zero, this will terminate
* the program. */
static void restore_echo(int sig){
static struct termios t;
tcgetattr(STDIN_FILENO, &t);
t.c_lflag |= ECHO;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &t);
/* If, for whatever ungodly reason, exit(3) returns, the user will notice
* their typed characters on the screen. */
if(sig != 0)
exit(EXIT_FAILURE);
return;
}
static int ioerr(char *s){ perror(s); restore_echo(0); return EX_IOERR; }
static int usage(char *s){
fprintf(stderr, "Usage: %s (-1enot) (-p [program [arguments...]])\n", s);
return EX_USAGE;
}
int main(int argc, char *argv[]){
if(argc < 1)
return usage(program_name);
{ /* option parsing */
char allow_nonterminals;
int c;
allow_nonterminals = 0;
while((c = getopt(argc, argv, "i")) != -1)
switch(c){
case 'i': allow_nonterminals = 1; break;
default: return usage(argv[0]);
}
if(argc > optind)
return usage(argv[0]);
if(!allow_nonterminals && isatty(STDIN_FILENO) != 1){
fprintf(stderr, "%s: Must be run in a terminal"
" (option -i skips this check)\n", argv[0]);
return EX_USAGE;
}
}
{ /* install signal handler */
/* There isn't a difference in functionality between the signal(2) and
* sigaction(2) methods. sigaction(2) is vastly preferred for
* portability but some configurations can only use signal(2). */
/* Errors aren't terminating because the worst that happens is some
* terminal phooeyness if things go awry. */
#if defined _POSIX_C_SOURCE
struct sigaction act = { 0 };
act.sa_handler = restore_echo;
if(sigaction(SIGINT, &act, NULL) != 0)
perror(argv[0]);
#else
if(signal(SIGINT, restore_echo) == SIG_ERR)
perror(argv[0]);
#endif
}
{ /* banish terminal echo */
struct termios t;
tcgetattr(STDIN_FILENO, &t);
t.c_lflag ^= ECHO;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &t);
}
{ /* actual input loop */
int c;
while((c = fgetc(stdin)) != EOF)
if(fputc(c, stdout) == EOF)
return ioerr(argv[0]);
}
restore_echo(0);
return EX_OK;
}

View File

@@ -172,7 +172,7 @@ fn eval(
};
} else {
return Err(EvaluationError {
message: format!("{}: Unexpected operation", op),
message: format!("{}: Unexpected operation.", op),
code: EX_DATAERR,
})
}

View File

@@ -1,31 +0,0 @@
/*
* Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
* SPDX-License-Identifier: FSFAP
*
* Copying and distribution of this file, with or without modification, are
* permitted in any medium without royalty provided the copyright notice and
* this notice are preserved. This file is offered as-is, without any warranty.
*/
use std::ffi::{ c_int, c_char, CStr };
pub trait StrError { fn strerror(&self) -> String; }
impl StrError for std::io::Error {
/* wrapper function for use in Rust */
fn strerror(&self) -> String {
/* Get the raw OS error. If its None, what the hell is going on‽ */
let errno = self.raw_os_error().unwrap_or(0) as c_int;
/* Get a CStr from the error message so that its referenced and then
* convert it to an owned value. If the string is not valid UTF-8,
* return that error instead. */
match unsafe { CStr::from_ptr(strerror(errno)) }.to_str() {
Ok(s) => s.to_owned(), // yay!! :D
Err(e) => e.to_string(), // awww :(
}
}
}
/* binding to strerror(3p) */
extern "C" { fn strerror(errnum: c_int) -> *mut c_char; }

View File

@@ -1,90 +0,0 @@
/*
* Copyright (c) 2024 DTB <trinity@trinity.moe>
* 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,
io::{ stdin, stdout, Error, ErrorKind, Read, Write },
process::ExitCode,
vec::Vec
};
extern crate getopt;
use getopt::{ Opt, Parser };
extern crate sysexits;
use sysexits::{ EX_OK, EX_OSERR, EX_USAGE };
fn oserr(s: &str, e: Error) -> ExitCode {
eprintln!("{}: {}", s, e);
ExitCode::from(EX_OSERR as u8)
}
fn usage(s: &str) -> ExitCode {
eprintln!("Usage: {} (-f) (-w [wordsize])", s);
ExitCode::from(EX_USAGE as u8)
}
fn main() -> ExitCode {
let argv = args().collect::<Vec<String>>();
let mut buf: Vec<u8> = Vec::new();
let mut input = stdin();
let mut output = stdout().lock();
let mut opts = Parser::new(&argv, "fw:");
let mut force = false;
let mut wordsize: usize = 2;
loop {
match opts.next() {
None => break,
Some(opt) =>
match opt {
Ok(Opt('f', None)) => force = true,
Ok(Opt('w', Some(arg))) => {
match arg.parse::<usize>() {
Ok(w) if w % 2 == 0 => { wordsize = w; () },
_ => { return usage(&argv[0]); },
}
},
_ => { return usage(&argv[0]); }
}
}
}
buf.resize(wordsize, 0);
loop {
match input.read(&mut buf) {
Ok(0) => break ExitCode::from(EX_OK as u8),
Ok(v) if v == wordsize => {
let (left, right) = buf.split_at(v/2);
if let Err(e) = output.write(&right)
.and_then(|_| output.write(&left)) {
break oserr(&argv[0], e)
}
},
Ok(v) => {
if let Err(e) = output.write(&buf[..v]) {
break oserr(&argv[0], e)
}
},
Err(e) if e.kind() == ErrorKind::Interrupted && force => continue,
Err(e) => break oserr(&argv[0], e)
}
}
}

View File

@@ -1,10 +0,0 @@
extern crate strerror;
use strerror::raw_message;
fn main() {
stdout.write_all(b"meow\n").unwrap_or_else(|e| {
eprintln!("{}", raw_message(e));
std::process::exit(1);
});
}

27
tests/bonsai/dj.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/sh
# Copyright (c) 2024 DTB <trinity@trinity.moe>
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
. tests/bonsai/test_env
! dj -h
# This test is theoretically Linux-dependent; write(2) should return -1 on
# error.
# Right now dj(1) interprets the return value of write(2) as the amount of
# bytes written. This can decrement the stored quantity of bytes written,
# which is an int, so doesn't underflow but goes negative. dj(1) tries to
# again to write(2) if an error occurs in which no bytes are written, so in
# total two write(2)s are attempted and so the written byte quantity is -2.
# This is a bug and will change, but for now is at least documented.
dj -Hi /dev/zero -o /dev/full \
| xargs -I out "$BIN/strcmp" '1+0 > 0+0; 1024 > -2' out
# Read nothing from /dev/null, write nothing to /dev/null.
dj -Hi /dev/null -o /dev/null \
| xargs -I out "$BIN/strcmp" '0+0 > 0+0; 0 > 0' out

13
tests/bonsai/false.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# Copyright (c) 2024 DTB <trinity@trinity.moe>
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
. tests/bonsai/test_env
! false
! false -h

21
tests/bonsai/fop.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
. tests/bonsai/test_env
! fop -h
"$BIN/strcmp" "$(printf 'test0␞test1␞test2\n' | fop 1 sed 's/1/4/g')" \
'test0␞test4␞test2'
"$BIN/strcmp" "$(printf 'test0 test1 test2\n' | fop -d' ' 2 sed 's/2/4/g')" \
'test0 test1 test4'
! printf 'test\n' | fop 1 cat
! printf 'test\n' | fop 'test' cat
! printf 'test\n' | fop -d'test' cat

26
tests/bonsai/hru.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
. tests/bonsai/test_env
alias strcmp="$BIN/strcmp"
alias rpn="$BIN/rpn"
strcmp "$(printf '1234\n' | hru)" '1.2 kB'
strcmp "$(printf '0\n' | hru)" '0 B'
# doesnt currently work but would be useful for testing for regressions
#n=1
#while "$BIN/true"; do
# n="$(rpn "$n" 10 ×)"
#
# printf '%s\n' "$n" | hru || break
#done
#printf 'integer limit: ~%s\n' "$(rpn "$n" 10 ÷)"
! printf '%s\n' '-1' | hru

25
tests/bonsai/intcmp.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/sh
# Copyright (c) 2024 DTB <trinity@trinity.moe>
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
. tests/bonsai/test_env
intcmp -e 3 3 3
intcmp -g 3 2 1
intcmp -l 1 2 3
intcmp -ge 3 3 1
intcmp -le 1 3 3
intcmp -gl 1 2 3
intcmp -egl 3 1 1 2
! intcmp -e 1 2 3
! intcmp -g 1 3 3
! intcmp -l 3 3 1
! intcmp -ge 1 2 3
! intcmp -le 3 2 1
! intcmp -gl 3 3 3
! intcmp -egl foo

19
tests/bonsai/mm.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
. tests/bonsai/test_env
exec 3>&1
! mm -h
# mm(1) will error if positional arguments are given without -i or -o
! mm argument
# check if stderr is empty upon specifying -e
! "$BIN/strcmp" "$(printf 'test\n' | mm -i - -e 2>&1 1>&3)" ''

16
tests/bonsai/strcmp.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# Copyright (c) 2024 DTB <trinity@trinity.moe>
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
. tests/bonsai/test_env
strcmp equals equals
! strcmp inequals equals
strcmp - -
strcmp -h
! strcmp nocmp

5
tests/bonsai/test_env Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -x
alias "$UTIL=$BIN/$UTIL"

13
tests/bonsai/true.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# Copyright (c) 2024 DTB <trinity@trinity.moe>
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
. tests/bonsai/test_env
true
true -h

22
tests/posix/bin/cat Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
# Strictly POSIX-compliant cat(1) implementation. See cat(1p)
for arg in "$@"; do
case "$arg" in
-u) args="$(printf '%s %s\n' "$args" "$arg")" ;;
*) args="$(printf -- '%s -i %s\n' "$args" "$arg")" ;;
esac
done
# See IEEE Std 1003.1-2017 3.282
# https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
IFS=' '
mm $args

12
tests/posix/bin/false Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# Copyright (c) 2024 DTB <trinity@trinity.moe>
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
# Strictly POSIX-compliant false(1) implementation. See false(1p)
false "$@"

11
tests/posix/bin/true Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
# Copyright (c) 2024 DTB <trinity@trinity.moe>
# Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and this
# notice are preserved. This file is offered as-is, without any warranty.
# Strictly POSIX-compliant true(1) implementation. See true(1p)
true "$@"

View File

@@ -9,16 +9,30 @@
set -e
export BIN=build/bin
if ! ls Makefile >/dev/null 2>&1
then
printf '%s: Run this script in the root of the project.\n' "$0" 1>&2
exit 1
fi
printf "Starting POSIX compatibility testing.\n"
printf "Starting Bonsai testing.\n\n"
for utility in tests/posix/*; do
printf '%s: %s: Testing utility.\n' "$0" "$utility"
for script in tests/bonsai/*.sh; do
export UTIL="$(printf '%s\n' "$script" \
| sed -e 's/\.sh//g' -e 's;tests\/bonsai\/;;g')"
printf '%s: %s: Testing utility.\n' "$0" "$UTIL"
"$script"
printf '\n'
done
printf "Starting POSIX compatibility testing.\n\n"
for test in tests/posix/*.sh; do
export PATH="$BIN:$PATH"
printf '%s: %s: Testing utility.\n' "$0" "$test"
"$utility"
printf '\n'
done