ESH

Executive Shell
git clone http://git.omkov.net/ESH
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-12-27 00:01:35
Commita1eb486624da6a305653a383152eb1791351a6b8
Parent0811e1a731716c55924617b0c51c6aafae8e472c

Add lineread from early ESH

Diffstat

A README.md | 17 +++++++++++++++++
A src/lineread.c | 391 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A src/lineread.h | 10 ++++++++++
A src/main.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A src/util/log.c | 36 ++++++++++++++++++++++++++++++++++++
A src/util/log.h | 19 +++++++++++++++++++
A src/util/optget.c | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A src/util/optget.h | 26 ++++++++++++++++++++++++++
A src/util/util.c | 20 ++++++++++++++++++++
A src/util/util.h | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

10 files changed, 751 insertions, 0 deletions

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..53a1cd7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# ESH
+
+A modern shell with an emphasis on performance and usability. It is not indended
+to be POSIX compliant, but may still exhibit POSIX-like behaviour.
+
+## Features
+
+At present, **ESH** is unfinished and is missing a number of basic features.
+
+## Usage
+
+To build **ESH**, from the project root, run `make build`.
+
+## Meta
+
+Copyright (C) 2023, Jakob Wakeling  
+All rights reserved.
diff --git a/src/lineread.c b/src/lineread.c
new file mode 100644
index 0000000..71050a3
--- /dev/null
+++ b/src/lineread.c
@@ -0,0 +1,391 @@
+// Copyright (C) 2021, Jakob Wakeling
+// All rights reserved.
+
+#include "lineread.h"
+#include "util/log.h"
+#include "util/util.h"
+
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct {
+	char *s; uptr sp, sl, sc;
+	char *prompt; uptr pl, cols;
+} line;
+
+typedef struct {
+	line *a; uptr ap, ah, at, al, ac;
+} hist;
+
+static struct termios tco, tcn;
+static bool rawflag = false, ateflag = false;
+
+static hist h = { NULL, 0, 0, 0, 0 };
+
+static char *linentty(void);
+static char *lineedit(void);
+static void  line_esc(line *l, register int c);
+
+static inline void tcraw(void);
+static inline void tcrestore(void);
+static size_t getcols(void);
+static void clearscreen(line *l);
+
+static void line_refresh(line *l);
+static void line_reset(line *l);
+static void line_move_left(line *l);
+static void line_move_right(line *l);
+static void line_move_word_home(line *l);
+static void line_move_word_end(line *l);
+static void line_move_home(line *l);
+static void line_move_end(line *l);
+static void line_push(line *l, char c);
+static int  line_insert(line *l, char c);
+static void line_backspace(line *l);
+static void line_delete(line *l);
+static void line_delete_word_home(line *l);
+static void line_delete_word_end(line *l);
+static void line_delete_home(line *l);
+static void line_delete_end(line *l);
+
+static int  hist_init(hist *h);
+static void hist_free(hist *h);
+static void hist_move_prior(hist *h);
+static void hist_move_next(hist *h);
+static void hist_move_home(hist *h);
+static void hist_move_end(hist *h);
+static void hist_push(hist *h, line l);
+
+/* Read a line from stdin */
+char *lineread(void) {
+	if (!isatty(STDIN_FILENO)) { errno = 0; return linentty(); }
+	else { return lineedit(); }
+}
+
+/* Free memory allocated by lineread */
+void linefree(void) { hist_free(&h); }
+
+/* Read from a non-terminal stdin */
+static char *linentty(void) {
+	line l; register char *r;
+	l.sp = 0; l.sl = 0; l.sc = 1024;
+	
+	if (!(l.s = xmalloc(l.sc * sizeof (*l.s)))) { return NULL; }
+	
+	for (register int c; (c = fgetc(stdin));) {
+		if (c == EOF) { if (l.sl) { break; } r = NULL; goto ret; }
+		else { line_push(&l, c); } continue;
+	}
+	
+end:;
+	r = strndup(l.s, l.sl);
+ret:;
+	free(l.s); return r;
+}
+
+#define l h.a[h.ap]
+
+/* Dynamically read a line from stdin */
+static char *lineedit(void) {
+	register char *r;
+	
+	{
+		if (h.a == NULL) { if (hist_init(&h)) { return NULL; } }
+		
+		line m = { NULL, 0, 0, 1024 };
+		m.prompt = "$ "; m.pl = strlen(m.prompt);
+		
+		if (!(m.cols = getcols())) { return NULL; }
+		if (!(m.s = xmalloc(m.sc * sizeof (*m.s)))) { return NULL; } m.s[0] = 0;
+		
+		hist_push(&h, m);
+	}
+	
+	tcraw(); fputs(l.prompt, stdout);
+	
+	for (register int c; (c = fgetc(stdin));) {
+		if (errno) { log_warn("FIXME: %s", strerror(errno)); errno = 0; }
+		// printf("%02X\n", c); continue;
+		
+		switch (c) {
+		case '\x01': { line_move_home(&l);        } continue; // CTRL + A
+		case '\x02': { line_move_left(&l);        } continue; // CTRL + B
+		case '\x03': { line_reset(&l);            } continue; // CTRL + C
+		case '\x04': { r = NULL;                  } goto ret; // CTRL + D
+		case '\x05': { line_move_end(&l);         } continue; // CTRL + E
+		case '\x06': { line_move_right(&l);       } continue; // CTRL + F
+		case '\x07': {} continue;                      // IGNORE CTRL + G
+		case '\x08': { line_backspace(&l);        } continue; // CTRL + H
+		case '\x09': {} continue;                      // IGNORE CTRL + I
+		case '\x0A': {} goto end;                             // CTRL + J
+		case '\x0B': { line_delete_end(&l);       } continue; // CTRL + K
+		case '\x0C': { clearscreen(&l);           } continue; // CTRL + L
+		case '\x0D': {} goto end;                    // ENTER or CTRL + M
+		case '\x0E': { /* Next history */         } continue; // CTRL + N
+		case '\x0F': {} goto end;                             // CTRL + O
+		case '\x10': { /* Prior history */        } continue; // CTRL + P
+		case '\x11': { /* Start output */         } continue; // CTRL + Q
+		case '\x12': {} continue;                      // IGNORE CTRL + R
+		case '\x13': { /* Stop output */          } continue; // CTRL + S
+		case '\x14': { /* Swap with prior */      } continue; // CTRL + T
+		case '\x15': { line_delete_home(&l);      } continue; // CTRL + U
+		case '\x16': { /* Insert char code */     } continue; // CTRL + V
+		case '\x17': { line_delete_word_home(&l); } continue; // CTRL + W
+		case '\x18': {} continue;                      // IGNORE CTRL + X
+		case '\x19': { /* Paste deleted */        } continue; // CTRL + Y
+		case '\x1B': switch ((c = fgetc(stdin))) {
+			case 'b': { line_move_word_home(&l);  } continue; // ALT + B
+			case 'd': { line_delete_word_end(&l); } continue; // ALT + D
+			case 'f': { line_move_word_end(&l);   } continue; // ALT + F
+			case '[': switch ((c = fgetc(stdin))) {
+				case '1': case '2': case '3': case '4': case '5': case '6':
+				case '7': case '8': case '9': { line_esc(&l, c); } continue;
+				case 'A': { hist_move_prior(&h); } continue; // UP
+				case 'B': { hist_move_next(&h);  } continue; // DOWN
+				case 'C': { line_move_right(&l); } continue; // RIGHT
+				case 'D': { line_move_left(&l);  } continue; // LEFT
+				case 'F': { line_move_end(&l);   } continue; // END
+				case 'H': { line_move_home(&l);  } continue; // HOME
+				default:  {} continue;
+			}
+			default:  {} continue;
+		}
+		case '\x7F': { line_backspace(&l);        } continue; // BACKSPACE
+		default:     { line_insert(&l, c);        } continue;
+		}
+	}
+	
+end:;
+	r = strndup(l.s, l.sl);
+ret:;
+	tcrestore(); fputc('\n', stdout); return r;
+}
+
+#undef l // h.a[h.ap]
+
+/* Handle an extended ^[ sequence */
+static void line_esc(line *l, register int c) {
+	switch (c) {
+	case '2': switch ((c = fgetc(stdin))) {
+		case '~': { /* Insert char code */ } return; // INSERT
+	}
+	case '3': switch ((c = fgetc(stdin))) {
+		case '~': { line_delete(l); } return; // DELETE
+	}
+	case '5': switch ((c = fgetc(stdin))) {
+		case '~': { hist_move_home(&h); } return; // PAGE UP
+	}
+	case '6': switch ((c = fgetc(stdin))) {
+		case '~': { hist_move_end(&h); } return; // PAGE DOWN
+	}
+	}
+}
+
+/* Put stdin into raw mode */
+static inline void tcraw(void) {
+	if (rawflag) { return; }
+	
+	tcgetattr(STDIN_FILENO, &tco); tcn = tco;
+	if (!ateflag) { atexit(tcrestore); ateflag = true; }
+	
+	/* No break, no CR to NL, no parity check, no strip bit, no flow control */
+	tcn.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+	/* No post process, 8-bit chars */
+	tcn.c_oflag &= ~(OPOST); tcn.c_cflag |= (CS8);
+	/* Canonical off, echo off, no extensions, no signal characters */
+	tcn.c_lflag &= ~(ICANON | ECHO | IEXTEN | ISIG);
+	/* Read every byte with no delay */
+	tcn.c_cc[VMIN] = 1; tcn.c_cc[VTIME] = 0;
+	
+	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tcn); rawflag = true; return;
+}
+
+/* Restore stdin to canonical mode */
+static inline void tcrestore(void) {
+	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tco); rawflag = false; return;
+}
+
+/* Get the number of columns in the terminal */
+static size_t getcols(void) {
+	struct winsize ws;
+	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) { return 0; }
+	return ws.ws_col;
+}
+
+/* Clear the screen */
+static void clearscreen(line *l) {
+	fputs("\x1B[H\x1B[2J", stdout); line_refresh(l); return;
+}
+
+/* Refresh line */
+static void line_refresh(line *l) {
+	fputs("\r\x1B[0K", stdout);
+	fputs(l->prompt, stdout); fputs(l->s, stdout);
+	fprintf(stdout, "\r\x1B[%zuC", l->pl + l->sp);
+	return;
+}
+
+/* Reset line */
+static void line_reset(line *l) {
+	l->s[0] = 0; l->sp = 0; l->sl = 0;
+	fputs("^C\n", stdout); line_refresh(l); return;
+}
+
+/* Move cursor left */
+static void line_move_left(line *l) {
+	if (l->sp) { --l->sp; fputs("\x1B[D", stdout); } return;
+}
+
+/* Move cursor right */
+static void line_move_right(line *l) {
+	if (l->sp != l->sl) { ++l->sp; fputs("\x1B[C", stdout); } return;
+}
+
+/* Move cursor to the word home */
+static void line_move_word_home(line *l) {
+	for (; l->sp && l->s[l->sp - 1] == ' '; --l->sp);
+	for (; l->sp && l->s[l->sp - 1] != ' '; --l->sp);
+	line_refresh(l); return;
+}
+
+/* Move cursor to the word end */
+static void line_move_word_end(line *l) {
+	for (; l->sp != l->sl && l->s[l->sp] == ' '; ++l->sp);
+	for (; l->sp != l->sl && l->s[l->sp] != ' '; ++l->sp);
+	line_refresh(l); return;
+}
+
+/* Move cursor to the line home */
+static void line_move_home(line *l) {
+	l->sp = 0; line_refresh(l); return;
+}
+
+/* Move cursor to the line end */
+static void line_move_end(line *l) {
+	l->sp = l->sl; line_refresh(l); return;
+}
+
+/* Push character onto end of line */
+static void line_push(line *l, char c) {
+	if (l->sl + 1 > l->sc) { l->sc *= 2;
+		l->s = xrealloc(l->s, l->sc * sizeof (*l->s));
+	} l->s[l->sl] = c; l->s[++l->sl] = 0; return;
+}
+
+/* Insert character at cursor */
+static int line_insert(line *l, char c) {
+	if (l->sl + 1 == l->sc) { /* TODO expand string */ }
+	if (l->sp == l->sl) { // Cursor is at line end
+		l->s[l->sp++] = c; l->s[++l->sl] = 0;
+		return fputc(c, stdout);
+	}
+	else {
+		memmove(l->s + l->sp + 1, l->s + l->sp, l->sl - l->sp);
+		l->s[l->sp++] = c; l->s[++l->sl] = 0; line_refresh(l);
+	} return c;
+}
+
+/* Delete character before cursor */
+static void line_backspace(line *l) {
+	if (l->sp) {
+		memmove(l->s + l->sp - 1, l->s + l->sp, l->sl - l->sp);
+		--l->sp; l->s[--l->sl] = 0; line_refresh(l);
+	} return;
+}
+
+/* Delete character at cursor */
+static void line_delete(line *l) {
+	if (l->sp != l->sl) {
+		memmove(l->s + l->sp, l->s + l->sp + 1, l->sl - l->sp);
+		l->s[--l->sl] = 0; line_refresh(l);
+	} return;
+}
+
+/* Delete the word preceeding the cursor */
+static void line_delete_word_home(line *l) {
+	size_t p = l->sp;
+	
+	for (; l->sp && l->s[l->sp - 1] == ' '; --l->sp);
+	for (; l->sp && l->s[l->sp - 1] != ' '; --l->sp);
+	
+	memmove(l->s + l->sp, l->s + p, l->sl - p + 1);
+	l->sl -= p - l->sp; line_refresh(l); return;
+}
+
+/* Delete the word following the cursor */
+static void line_delete_word_end(line *l) {
+	size_t p = l->sp;
+	
+	for (; p != l->sl && l->s[p] == ' '; ++p);
+	for (; p != l->sl && l->s[p] != ' '; ++p);
+	
+	memmove(l->s + l->sp, l->s + p, l->sl - p + 1);
+	l->sl -= p - l->sp; line_refresh(l); return;
+}
+
+/* Delete characters from cursor to home */
+static void line_delete_home(line *l) {
+	memmove(l->s, l->s + l->sp, l->sl - l->sp + 1);
+	l->sl -= l->sp; l->sp = 0; line_refresh(l); return;
+}
+
+/* Delete characters from cursor to end */
+static void line_delete_end(line *l) {
+	l->s[(l->sl = l->sp)] = 0; line_refresh(l); return;
+}
+
+/* Initialise history */
+static int hist_init(hist *h) {
+	/* FIXME do not hardcode history size */
+	h->ap = 0; h->ah = 0; h->at = 0; h->al = 0; h->ac = 1000 + 1;
+	if (!(h->a = xcalloc(h->ac, sizeof (*h->a)))) { return 1; }
+	/* TODO load history from file */ return 0;
+}
+
+/* Free history */
+static void hist_free(hist *h) {
+	for (size_t i = 0; i != h->ac; ++i) { free(h->a[i].s); } free(h->a);
+}
+
+/* Move backwards in history */
+static void hist_move_prior(hist *h) {
+	if (h->ap != h->ah) { h->ap == 0 ? h->ap = h->ac - 1 : --h->ap; }
+	line_refresh(&h->a[h->ap]); return;
+}
+
+/* Move forwards in history */
+static void hist_move_next(hist *h) {
+	register size_t an = h->ap == h->ac - 1 ? 0 : h->ap + 1;
+	if (an != h->at) { h->ap = an; }
+	line_refresh(&h->a[h->ap]); return;
+}
+
+/* Move to the start of history */
+static void hist_move_home(hist *h) {
+	/* TODO */
+	h->ap = h->ah; line_refresh(&h->a[h->ap]); return;
+}
+
+/* Move to the end of history */
+static void hist_move_end(hist *h) {
+	/* TODO */
+	// h->ap = h->at; lineRefresh(&h->a[h->ap]); return;
+}
+
+/* Push a line onto the end of history */
+static void hist_push(hist *h, line l) {
+	if (h->al != h->ac) {
+		++h->al; h->ap = h->at; if (++h->at == h->ac) { h->at = 0; }
+	}
+	else { h->ap = h->at; if (++h->at == h->ac) { h->at = 0; } h->ah = h->at; }
+	
+	free(h->a[h->ap].s); h->a[h->ap] = l; return;
+}
diff --git a/src/lineread.h b/src/lineread.h
new file mode 100644
index 0000000..a79c0af
--- /dev/null
+++ b/src/lineread.h
@@ -0,0 +1,10 @@
+// Copyright (C) 2021, Jakob Wakeling
+// All rights reserved.
+
+#ifndef LINEREAD_H_TVNWORP2
+#define LINEREAD_H_TVNWORP2
+
+extern char *lineread();
+extern void  linefree();
+
+#endif // LINEREAD_H_TVNWORP2
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..de6382e
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,76 @@
+// Copyright (C) 2023, Jakob Wakeling
+// All rights reserved.
+
+#include "lineread.h"
+#include "util/log.h"
+#include "util/optget.h"
+#include "util/util.h"
+
+#include <errno.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void reset(int);
+
+static const char *const help;
+static const char *const version;
+
+static jmp_buf jmp;
+static sig_atomic_t jmpflag = false;
+
+int main(int, char *av[]) {
+	struct opt opt = OPTGET_INIT; opt.str = ""; opt.lops = (struct lop[]){
+		{ "help",    ARG_NUL, 256 },
+		{ "version", ARG_NUL, 257 },
+		{ "debug",   ARG_NUL, 258 },
+		{ NULL, 0, 0 },
+	};
+	
+	struct {} args = {};
+	
+	for (int c; (c = optget(&opt, av, 1)) != -1;) {
+		switch (c) {
+			case 256: { fputs(help, stdout);    } return 0;
+			case 257: { fputs(version, stdout); } return 0;
+			case 258: { __debug = true;         } break;
+			default: { return -1; }
+		}
+	}
+	
+	signal(SIGINT, &reset); signal(SIGQUIT, SIG_IGN); signal(SIGTSTP, SIG_IGN);
+	atexit(&linefree);
+	
+	do {
+		if (sigsetjmp(jmp, 1)) { fputc('\n', stdout); } jmpflag = true;
+		
+		char *line = lineread();
+		if (!line) { if (errno) { log_warn("lineread: %s", strerror(errno)); } break; }
+		
+		printf("%s", line); free(line);
+	} while (true);
+	
+	return __warned;
+}
+
+static void reset(int) {
+	if (jmpflag) { siglongjmp(jmp, 1); }
+}
+
+static const char *const help =
+	"ESH - Executive Shell\n"
+	"Usage:\n"
+	"  esh [--debug]\n"
+	"Options:\n"
+	"  --help     Display help information\n"
+	"  --version  Display version information\n"
+	"  --debug    Enable debug logging\n"
+;
+
+static const char *const version =
+	"ESH, version " PROJECT_VERSION "\n"
+	"Copyright (C) 2023, Jakob Wakeling\n"
+	"All rights reserved.\n"
+;
diff --git a/src/util/log.c b/src/util/log.c
new file mode 100644
index 0000000..1eaf8d8
--- /dev/null
+++ b/src/util/log.c
@@ -0,0 +1,36 @@
+// Copyright (C) 2023, Jakob Wakeling
+// All rights reserved.
+
+#include "log.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+bool __debug, __warned;
+
+[[noreturn]] void log_abort(const char *restrict format, ...) {
+	fflush(stderr); va_list args; va_start(args, format);
+	vfprintf(stderr, format, args); fputc('\n', stderr);
+	va_end(args); abort();
+}
+
+[[noreturn]] void log_fatal(int status, const char *restrict format, ...) {
+	fflush(stderr); va_list args; va_start(args, format);
+	vfprintf(stderr, format, args); fputc('\n', stderr);
+	va_end(args); exit(status);
+}
+
+void log_print(const char *restrict format, ...) {
+	fflush(stderr); va_list args; va_start(args, format);
+	vfprintf(stderr, format, args); fputc('\n', stderr);
+	va_end(args);
+}
+
+void log_debug(const char *restrict format, ...) {
+	if (!__debug) { return; }
+
+	fflush(stderr); va_list args; va_start(args, format);
+	vfprintf(stderr, format, args); fputc('\n', stderr);
+	va_end(args);
+}
diff --git a/src/util/log.h b/src/util/log.h
new file mode 100644
index 0000000..8a5a09e
--- /dev/null
+++ b/src/util/log.h
@@ -0,0 +1,19 @@
+// Copyright (C) 2023, Jakob Wakeling
+// All rights reserved.
+
+#ifndef UTIL_LOG_H_MNZFBC4G
+#define UTIL_LOG_H_MNZFBC4G
+
+#define log_warn(format, ...) do { \
+	log_print(format __VA_OPT__(,) __VA_ARGS__); \
+	__warned = true; \
+} while (0)
+
+extern bool __debug, __warned;
+
+[[noreturn]] extern void log_abort(const char *restrict format, ...);
+[[noreturn]] extern void log_fatal(int status, const char *restrict format, ...);
+extern void log_print(const char *restrict format, ...);
+extern void log_debug(const char *restrict format, ...);
+
+#endif // UTIL_LOG_H_MNZFBC4G
diff --git a/src/util/optget.c b/src/util/optget.c
new file mode 100644
index 0000000..a21e736
--- /dev/null
+++ b/src/util/optget.c
@@ -0,0 +1,92 @@
+// Copyright (C) 2020, Jakob Wakeling
+// MIT Licence
+
+#include "optget.h"
+
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#define cur av[opt->ind]
+
+const struct opt OPTGET_INIT = { 1, 0, 1, 0, NULL, NULL, NULL, NULL };
+
+static inline void permute(char **av, int i, int n);
+
+int optget(struct opt *opt, char *av[], int flags) {
+	if (flags & 1) {
+		for (; cur && (cur[0] != '-' || cur[1] == 0); ++opt->ind, ++opt->nop);
+		if (!cur) { opt->ind -= opt->nop; opt->nop = 0; return -1; }
+	}
+	else if (!cur || (cur[0] != '-' || cur[1] == 0)) { return -1; }
+	
+	int optind = opt->ind, optret;
+	
+	if (cur[1] == '-') {
+		if (cur[2] == 0) { if (opt->nop) {
+				permute(av, opt->ind++, opt->nop);
+				opt->ind -= opt->nop; opt->nop = 0;
+			} else { ++opt->ind; } return -1;
+		}
+		
+		int optend, lop; optret = '?'; opt->opt = 0; opt->lop = cur;
+		if (!opt->lops) { goto nol; }
+		for (optend = 2; cur[optend] != '=' && cur[optend] != 0; ++optend);
+		
+		for (lop = 0; opt->lops[lop].str; ++lop) {
+			if (strncmp(&cur[2], opt->lops[lop].str, (size_t)optend - 2) == 0) {
+				if (!opt->lops[lop].str[optend - 2]) {
+					optret = opt->opt = opt->lops[lop].val; break;
+				}
+			}
+		}
+		
+		if (opt->lops[lop].arg > ARG_NUL) {
+			if (cur[optend]) { opt->arg = &cur[optend + 1]; }
+			else if (av[opt->ind + 1]) { opt->arg = av[++opt->ind]; }
+			else {
+				if (opt->lops[lop].arg == ARG_REQ) { optret = ':'; }
+				opt->arg = NULL;
+			}
+		}
+		else { opt->arg = NULL; }
+		
+nol:	opt->pos = 0;
+	}
+	else {
+		optret = opt->opt = cur[opt->pos++]; opt->lop = NULL;
+		const char *optchr = strchr(opt->str, opt->opt);
+		
+		if (!optchr) { optret = '?'; }
+		else if (optchr[1] == ':') {
+			if (cur[opt->pos]) { opt->arg = &cur[opt->pos]; }
+			else if (av[opt->ind + 1]) { opt->arg = av[++opt->ind]; }
+			else { opt->arg = NULL; optret = ':'; }
+			opt->pos = 0;
+		}
+		else { opt->arg = NULL; }
+	}
+	
+	if (!opt->pos || !cur[opt->pos]) {
+		++opt->ind; opt->pos = 1;
+		if (opt->nop) for (; optind < opt->ind; ++optind) {
+			permute(av, optind, opt->nop);
+		}
+	}
+	
+	if (optret == '?' && opt->str[0] != ':') {
+		if (opt->opt) { fprintf(stderr, "%c: invalid option", opt->opt); }
+		else if (opt->lop) { fprintf(stderr, "%s: invalid option", opt->lop); }
+	}
+	if (optret == ':' && opt->str[0] != ':') {
+		if (opt->opt) { fprintf(stderr, "%c: option requires argument", opt->opt); }
+		else if (opt->lop) { fprintf(stderr, "%s: option requires argument", opt->lop); }
+	}
+	
+	return optret;
+}
+
+static inline void permute(char **av, int i, int n) {
+	char *a = av[i]; memmove(&av[i - n + 1], &av[i - n], n * sizeof (av));
+	av[i - n] = a; return;
+}
diff --git a/src/util/optget.h b/src/util/optget.h
new file mode 100644
index 0000000..dba5ab8
--- /dev/null
+++ b/src/util/optget.h
@@ -0,0 +1,26 @@
+// Copyright (C) 2020, Jakob Wakeling
+// MIT Licence
+
+#ifndef UTIL_OPTGET_H_ZPCLTG8D
+#define UTIL_OPTGET_H_ZPCLTG8D
+
+#define ARG_NUL 0
+#define ARG_REQ 1
+#define ARG_OPT 2
+
+struct lop {
+	char *str;
+	int arg, val;
+};
+
+struct opt {
+	int ind, opt, pos, nop;
+	char *arg, *lop, *str;
+	struct lop *lops;
+};
+
+extern const struct opt OPTGET_INIT;
+
+extern int optget(struct opt *opt, char *av[], int flags);
+
+#endif // UTIL_OPTGET_H_ZPCLTG8D
diff --git a/src/util/util.c b/src/util/util.c
new file mode 100644
index 0000000..3e58f93
--- /dev/null
+++ b/src/util/util.c
@@ -0,0 +1,20 @@
+// Copyright (C) 2023, Jakob Wakeling
+// All rights reserved.
+
+#include "log.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+void *_xmalloc(size_t size, const char *file, int line) {
+	void *p = malloc(size); if (!p) { log_abort("%s:%d: %s", file, line, strerror(errno)); } return p;
+}
+
+void *_xcalloc(size_t nmemb, size_t size, const char *file, int line) {
+	void *p = calloc(nmemb, size); if (!p) { log_abort("%s:%d: %s", file, line, strerror(errno)); } return p;
+}
+
+void *_xrealloc(void *ptr, size_t size, const char *file, int line) {
+	void *p = realloc(ptr, size); if (!p) { log_abort("%s:%d: %s", file, line, strerror(errno)); } return p;
+}
diff --git a/src/util/util.h b/src/util/util.h
new file mode 100644
index 0000000..216aaaa
--- /dev/null
+++ b/src/util/util.h
@@ -0,0 +1,64 @@
+// Copyright (C) 2023, Jakob Wakeling
+// All rights reserved.
+
+#ifndef UTIL_UTIL_H_WIAX91EM
+#define UTIL_UTIL_H_WIAX91EM
+
+#include <float.h>
+#include <stddef.h>
+#include <stdint.h>
+
+typedef uint8_t   u8;
+typedef uint16_t  u16;
+typedef uint32_t  u32;
+typedef uint64_t  u64;
+typedef uintptr_t uptr;
+
+typedef int8_t   s8;
+typedef int16_t  s16;
+typedef int32_t  s32;
+typedef int64_t  s64;
+typedef intptr_t sptr;
+
+typedef float       f32;
+typedef double      f64;
+typedef long double f128;
+
+#define U8_MIN   UINT8_MIN
+#define U8_MAX   UINT8_MAX
+#define U16_MIN  UINT16_MIN
+#define U16_MAX  UINT16_MAX
+#define U32_MIN  UINT32_MIN
+#define U32_MAX  UINT32_MAX
+#define U64_MIN  UINT64_MIN
+#define U64_MAX  UINT64_MAX
+#define UPTR_MIN UINTPTR_MIN
+#define UPTR_MAX UINTPTR_MAX
+
+#define S8_MIN   INT8_MIN
+#define S8_MAX   INT8_MAX
+#define S16_MIN  INT16_MIN
+#define S16_MAX  INT16_MAX
+#define S32_MIN  INT32_MIN
+#define S32_MAX  INT32_MAX
+#define S64_MIN  INT64_MIN
+#define S64_MAX  INT64_MAX
+#define SPTR_MIN INTPTR_MIN
+#define SPTR_MAX INTPTR_MAX
+
+#define F32_MIN  FLT_MIN
+#define F32_MAX  FLT_MAX
+#define F64_MIN  DBL_MIN
+#define F64_MAX  DBL_MAX
+#define F128_MIN LDBL_MIN
+#define F128_MAX LDBL_MAX
+
+#define xmalloc(size) _xmalloc(size, __FILE__, __LINE__)
+#define xcalloc(nmemb, size) _xcalloc(nmemb, size, __FILE__, __LINE__)
+#define xrealloc(ptr, size) _xrealloc(ptr, size, __FILE__, __LINE__)
+
+extern void *_xmalloc(size_t size, const char *file, int line);
+extern void *_xcalloc(size_t nmemb, size_t size, const char *file, int line);
+extern void *_xrealloc(void *ptr, size_t size, const char *file, int line);
+
+#endif // UTIL_UTIL_H_WIAX91EM