Compare commits

..

6 Commits
main ... pg

7 changed files with 358 additions and 100 deletions

View File

@@ -17,8 +17,6 @@
DESTDIR ?= dist DESTDIR ?= dist
PREFIX ?= /usr/local PREFIX ?= /usr/local
MANDIR != [ $(PREFIX) = / ] && printf '/usr/share/man\n' \
|| printf '/share/man\n'
SYSEXITS != printf '\043include <sysexits.h>\n' | cpp -M - | sed 's/ /\n/g' \ SYSEXITS != printf '\043include <sysexits.h>\n' | cpp -M - | sed 's/ /\n/g' \
| sed -n 's/sysexits\.h//p' || printf 'include\n' | sed -n 's/sysexits\.h//p' || printf 'include\n'
@@ -30,7 +28,7 @@ RUSTLIBS = --extern getopt=build/o/libgetopt.rlib \
CFLAGS += -I$(SYSEXITS) CFLAGS += -I$(SYSEXITS)
.PHONY: all .PHONY: all
all: dj false fop hru intcmp mm npc rpn scrut sleep str strcmp swab true all: dj false fop hru intcmp mm npc rpn scrut str strcmp swab true
build: build:
# keep build/include until bindgen(1) has stdin support # keep build/include until bindgen(1) has stdin support
@@ -44,7 +42,7 @@ clean:
dist: all dist: all
mkdir -p $(DESTDIR)/$(PREFIX)/bin $(DESTDIR)/$(PREFIX)/share/man/man1 mkdir -p $(DESTDIR)/$(PREFIX)/bin $(DESTDIR)/$(PREFIX)/share/man/man1
cp build/bin/* $(DESTDIR)/$(PREFIX)/bin cp build/bin/* $(DESTDIR)/$(PREFIX)/bin
cp docs/*.1 $(DESTDIR)/$(PREFIX)/$(MANDIR)/man1 cp docs/*.1 $(DESTDIR)/$(PREFIX)/share/man/man1
.PHONY: install .PHONY: install
install: dist install: dist
@@ -107,7 +105,12 @@ build/bin/mm: src/mm.c build
.PHONY: npc .PHONY: npc
npc: build/bin/npc npc: build/bin/npc
build/bin/npc: src/npc.c build build/bin/npc: src/npc.c build
$(CC) $(CFLAGAS) -o $@ src/npc.c $(CC) $(CFLAGS) -o $@ src/npc.c
.PHONY: pg
pg: build/bin/pg
build/bin/pg: src/pg.c build
$(CC) $(CFLAGS) -o $@ src/pg.c
.PHONY: rpn .PHONY: rpn
rpn: build/bin/rpn rpn: build/bin/rpn
@@ -119,13 +122,6 @@ scrut: build/bin/scrut
build/bin/scrut: src/scrut.c build build/bin/scrut: src/scrut.c build
$(CC) $(CFLAGS) -o $@ src/scrut.c $(CC) $(CFLAGS) -o $@ src/scrut.c
.PHONY: sleep
sleep: build/bin/sleep
build/bin/sleep: src/sleep.rs build rustlibs
$(RUSTC) $(RUSTFLAGS) \
--extern sysexits=build/o/libsysexits.rlib \
-o $@ src/sleep.rs
.PHONY: str .PHONY: str
str: build/bin/str str: build/bin/str
build/bin/str: src/str.c build build/bin/str: src/str.c build

57
README
View File

@@ -1,27 +1,27 @@
For the Bonsai coreutils, a better Unix toolset, please visit “Seek not to walk the path of the masters; seek what they sought.”
<https://git.tebibyte.media/bonsai/coreutils>. Matsuo Basho
wwwww / The Bonsai core utilities are the result of the careful examination of the
wWWWWWw /_ _____ _ * "It's not a very good tree, but current state of POSIX and Unix utilies. The Unix Philosophy, “do one thing and
w|||w / \/ \ / | /\ / /|/ occasionally it bears fruit!" do it well” is its core but these tools do not cling to the names of the past.
_,|||._ _/\__/|__// /_/_|_/_//|__
bonsix : "BOHN zix" : "Bonsai" but "POSIX"
The excellent Bonsai core utilities are the result of the careful examination The era of the original Unix tools has been long and fruitful, but they have
of the current state of POSIX and Unix utilities, ironing out wrinkles in the their flaws. The new, non-POSIX era of this project started with frustration
implementations that ended up being worn by the POSIX standard. Bonsai's with the way certain tools work and how other projects that extend POSIX dont
guiding quote is this one from Matsuo Basho: make anything better.
"Seek not to walk the path of the masters; seek what they sought." This project will not follow in the footsteps of GNU; extensions of POSIX will
not be found here. GNU extensions are a gateway to the misuse of the shell. The
Bonsai core utilities will intentionally discourage use of the shell for
purposes beyond its scope.
Unlike Bonsai, Bonsix intends to walk the path of the masters, implementing a See docs/ for more on the specific utilities currently implemented.
POSIX userland under an AGPLv3 license and taking inspiration from the
practices used by Bonsai.
Building Building
Bonsix requires an existing POSIX-compliant environment to compile, including a The coreutils require a POSIX-compliant environment to compile, including a C
C compiler and preprocessor (cc(1) and cpp(1) by default) and a POSIX-compliant compiler and preprocessor (cc(1) and cpp(1) by default) with the -idirafter
flag, a Rust compiler (rustc(1) by default), bindgen(1), and a POSIX-compliant
make(1) utility. make(1) utility.
To build and install: To build and install:
@@ -32,6 +32,29 @@ $ make PREFIX="/your/preferred/location" install
To build with a different compiler than the default: To build with a different compiler than the default:
$ make CC=clang $ make CC=clang
$ make RUSTC=gccrs
To test the utilities:
$ make test
To remove all untracked files:
$ make clean
Read More
An Introduction to the Unix Shell
<https://porkmail.org/era/unix/shell>
Master Foo and the Ten Thousand Lines
<http://www.catb.org/~esr/writings/unix-koans/ten-thousand.html>
Master Foo Discourses on the Unix-Nature
<http://www.catb.org/~esr/writings/unix-koans/unix-nature.html>
Shell Programming!
<https://tldp.org/LDP/abs/html/why-shell.html>
-- --
Copyright © 20232024 Emma Tebibyte <emma@tebibyte.media> Copyright © 20232024 Emma Tebibyte <emma@tebibyte.media>

41
docs/pg.1 Normal file
View File

@@ -0,0 +1,41 @@
.\" 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 PG 1
.SH NAME
pg \(en paginate
.SH SYNOPSIS
pg
.RB ( -p
.RB [ prompt ])
.SH DESCRIPTION
Pg prints standard input to standard output, accepting commands between pages.
.SH OPTIONS
.B -p
.RS
Replaces the default prompt (": ") with the next argument.
.RE
.SH DIAGNOSTICS
Pg returns an unsuccessful exit code if the tty couldn't be opened or if pg was
invoked incorrectly (with any arguments).
.SH RATIONALE
Plan 9 from Bell Labs had p(1), a similar "cooked"-mode paginator (as opposed
to "raw" mode, which a vast majority of paginators use).
.SH SEE ALSO
more(1)

View File

@@ -106,15 +106,6 @@ oserr(char *s, char *r){
} \ } \
return retval 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 main(int argc, char *argv[]){
int c; int c;
struct Files files[2]; /* {read, write} */ struct Files files[2]; /* {read, write} */
@@ -187,15 +178,12 @@ int main(int argc, char *argv[]){
k = 1; k = 1;
break; break;
default: default:
retval = usage(argv[0]); fprintf(stderr, "Usage: %s (-aenu) (-i [input])..."
" (-o [output])...\n", argv[0]);
retval = EX_USAGE;
terminate; terminate;
} }
if(optind != argc){
retval = usage(argv[0]);
terminate;
}
files[0].s += files[0].s == 0; files[0].s += files[0].s == 0;
files[1].s += files[1].s == 0; files[1].s += files[1].s == 0;

260
src/pg.c Normal file
View File

@@ -0,0 +1,260 @@
/*
* 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 <stdio.h> /* fclose(3), feof(3), fgetc(3), fgets(3), fopen(3),
* fprintf(3), fputc(3), stderr, stdin, stdout, EOF, FILE,
* NULL */
#include <stdlib.h> /* size_t */
#include <string.h> /* strchr(3), strcmp(3), strerror(3), strtok(3) */
#include <unistd.h> /* getopt(3) */
/* Commands start with cmd_. They take an argc and NULL-terminated argv, like
* main, and return a status from <sysexits.h>. Return values other than EX_OK
* and EX_USAGE cause pg(1) to exit with that value, except EX_UNAVAILABLE,
* which causes pg(1) to exit with the status EX_OK. */
#include <sysexits.h>
#define CMDLINE_MAX 99+1 /* Maximum length of command line. */
static char *whitespace = " \n\r\t\v";
static struct {
size_t quantity;
enum { LINES = 0, BYTES = 1 } type;
} default_page_unit = { 22, LINES } /* Plan 9 default */;
static FILE *input;
static char *prompt = ": ";
static char *program_name = "pg";
static char *permute_out(char *str, size_t index){
size_t j;
for(j = index; str[j - 1] != '\0'; ++j)
str[j - 1] = str[j];
return str;
}
/* strtok(3p), but supports double-quotes and escapes (but only for escaping
* quotes). UTF-8 is safe only in str. Unmatched quotes in str are considered
* literal. The behavior of strtok_quoted when '"' or '\\' are in sep is
* undefined. */
/* TODO: Seems to only ever return NULL. */
static char *strtok_quoted(char *str, char *sep){
static char *s;
size_t i;
size_t j;
if(str != NULL)
s = str;
while(strchr(sep, *s) == NULL)
if(*++s == '\0')
return NULL; /* no remaining characters except seperators */
{
char in_escape;
int in_quotes;
in_escape = 0;
in_quotes = -1;
for(i = 0; s[i] != '\0'; ++i)
switch(s[i]){
case '\\':
if(in_escape)
permute_out(s, i--);
in_escape = !in_escape;
break;
case '"':
if(in_escape){
s[i] = s[i - 1];
permute_out(s, i--);
}else if(in_quotes != -1){
permute_out(s, in_quotes);
--i;
permute_out(s, i--);
in_quotes = -1;
}else
in_quotes = i;
break;
case '\0':
return s;
default:
if(!in_escape && strchr(sep, s[i]) != NULL){
s[i] = '\0';
return s;
}
}
}
return s;
}
/* Page at most l bytes from f without consideration of a buffer (print them to
* stdout). */
static int pg_b_u(FILE *f, size_t l){
int c;
while((c = fgetc(f)) != EOF)
if((c = fputc(c, stdout)) == EOF || --l == 0)
break;
return c;
}
/* Page at most l lines, which are lengths of characters terminated by nl, from
* f without consideration of a buffer (print them to stdout). */
static int pg_l_u(FILE *f, size_t l, char nl){
int c;
while((c = fgetc(f)) != EOF)
if((c = fputc(c, stdout)) == EOF || (l -= (c == nl)) == 0)
break;
return c;
}
static int cmd_quit(int argc, char **argv, char **envp){return EX_UNAVAILABLE;}
static int cmd_default_page(int argc, char **argv, char **envp){
if(argc > 1) /* This shouldn't be possible. */
return EX_USAGE;
if(default_page_unit.type == BYTES)
return pg_b_u(input, default_page_unit.quantity) == EOF
? EX_UNAVAILABLE
: EX_OK;
else if(default_page_unit.type == LINES)
return pg_l_u(input, default_page_unit.quantity, '\n') == EOF
? EX_UNAVAILABLE
: EX_OK;
else
return EX_SOFTWARE;
}
static int cmd_page_down_lines(int argc, char **argv, char **envp){
switch(argc){
case 1:
return cmd_default_page(argc, argv, envp);
case 2: /* not implemented */
default:
fprintf(stderr, "Usage: %s" /*" (lines)"*/ "\n", argv[0]);
return EX_USAGE;
}
}
/* A CmdMap must be NULL-terminated. */
static struct CmdMap{ char *name; int (*fn)(int, char **, char **); }
builtins[] = {
{ "", cmd_default_page },
{ "+", cmd_page_down_lines },
/* don't make the user feel trapped */
{ ":q", cmd_quit }, { ":q!", cmd_quit },
{ "exit", cmd_quit }, { "q", cmd_quit }, { "Q", cmd_quit },
{ "quit", cmd_quit }, { "ZZ", cmd_quit },
{ NULL, NULL }
};
#define ARGV_MAX 10
/* Find and execute the command in the command map, given a corresponding
* command line. */
static int cmdline_exec(struct CmdMap *map, char *cmdline, char **envp){
/* Command line word splitting is naive and based on whitespace ONLY; no
* fancy quoting or escaping here. Adding that would (ideally) entail
* replacing strtok(3) with something specific to this task. */
static int argc;
static char *argv[ARGV_MAX];
if((argv[(argc = 0)] = strtok(cmdline, whitespace)) == NULL){
while(cmdline[0] != '\0')
cmdline = &cmdline[1];
argv[argc] = cmdline;
argv[++argc] = NULL;
}else{
while((argv[++argc] = strtok(NULL, whitespace)) != NULL
&& argc < ARGV_MAX);
}
for(; map->name != NULL; map = &map[1])
if(strcmp(map->name, argv[0]) == 0)
return map->fn(argc, argv, envp);
fprintf(stderr, "%s: %s: not found\n", program_name, argv[0]);
return EX_USAGE;
}
int usage(char *s){
fprintf(stderr, "Usage: %s (-p [prompt])\n", s);
return EX_USAGE;
}
int main(int argc, char *argv[]){
unsigned char cmd[CMDLINE_MAX];
FILE *t;
input = stdin;
if(argc < 1)
program_name = argv[0];
else{
int c;
while((c = getopt(argc, argv, "p:")) != -1)
switch(c){
case 'p': prompt = optarg; break;
default:
return usage(program_name);
}
}
if(argc > optind)
return usage(program_name);
if((t = fopen("/dev/tty", "rb")) == NULL){
fprintf(stderr, "%s: /dev/tty: %s\n", program_name, strerror(errno));
return EX_OSERR;
}
for(;;){
fputs(prompt, stderr);
fgets((char *)cmd, (sizeof cmd) / (sizeof *cmd), t);
if(strchr((char *)cmd, '\n') == NULL){ /* fast-forward stream */
int c;
while((c = fgetc(t)) != '\n')
if(c == EOF)
break;
}
if(feof(t))
return EX_OK;
{ int r;
switch((r = cmdline_exec(builtins, (char *)cmd, NULL))){
case EX_OK: case EX_USAGE: break;
case EX_UNAVAILABLE: return EX_OK;
default: return r;
} }
}
/* UNREACHABLE */
return EX_SOFTWARE;
}

View File

@@ -1,6 +1,5 @@
/* /*
* Copyright (c) 20232024 DTB <trinity@trinity.moe> * Copyright (c) 2023 DTB <trinity@trinity.moe>
* Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
* *
* This program is free software: you can redistribute it and/or modify it under * This program is free software: you can redistribute it and/or modify it under
@@ -18,15 +17,13 @@
*/ */
#include <stdio.h> /* fprintf(3), stderr, NULL */ #include <stdio.h> /* fprintf(3), stderr, NULL */
#include <stdlib.h> /* EXIT_FAILURE, EXIT_SUCCESS */ #include <stdlib.h> /* EXIT_FAILURE */
#include <string.h> /* memset(3), strchr(3) */ #include <string.h> /* memset(3), strchr(3) */
#ifndef EX_USAGE
# include <sysexits.h>
#endif
#include <unistd.h> /* access(3), getopt(3), F_OK, R_OK, W_OK, X_OK */ #include <unistd.h> /* access(3), getopt(3), F_OK, R_OK, W_OK, X_OK */
#include <sys/stat.h> /* lstat(3), stat struct, S_ISBLK, S_ISCHR, S_ISDIR, #include <sys/stat.h> /* lstat(3), stat struct, S_ISBLK, S_ISCHR, S_ISDIR,
* S_ISFIFO, S_ISGID, S_ISREG, S_ISLNK, S_ISSOCK, * S_ISFIFO, S_ISGID, S_ISREG, S_ISLNK, S_ISSOCK,
* S_ISUID, S_ISVTX */ * S_ISUID, S_ISVTX */
#include <sysexits.h>
static char args[] = "bcdefghkprsuwxLS"; static char args[] = "bcdefghkprsuwxLS";
static char ops[(sizeof args) / (sizeof *args)]; static char ops[(sizeof args) / (sizeof *args)];
@@ -60,7 +57,7 @@ int main(int argc, char *argv[]){
argv += optind; argv += optind;
do{ if(access(*argv, F_OK) != 0 || lstat(*argv, &buf) == -1) do{ if(access(*argv, F_OK) != 0 || lstat(*argv, &buf) == -1)
return EXIT_FAILURE; /* doesn't exist or isn't stattable */ return 1; /* doesn't exist or isn't stattable */
for(i = 0; ops[i] != '\0'; ++i) for(i = 0; ops[i] != '\0'; ++i)
if(ops[i] == 'e') if(ops[i] == 'e')
@@ -100,8 +97,8 @@ usage: fprintf(stderr, "Usage: %s (-%s) [file...]\n",
&& !S_ISLNK(buf.st_mode)) && !S_ISLNK(buf.st_mode))
|| (ops[i] == 'S' || (ops[i] == 'S'
&& !S_ISSOCK(buf.st_mode))) && !S_ISSOCK(buf.st_mode)))
return EXIT_FAILURE; return 1;
}while(*++argv != NULL); }while(*++argv != NULL);
return EXIT_SUCCESS; return 0;
} }

View File

@@ -1,47 +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,
process::ExitCode,
thread::sleep,
time::Duration
};
extern crate sysexits;
use sysexits::EX_USAGE;
fn usage(s: &str) -> ExitCode {
eprintln!("Usage: {} [seconds]", s);
ExitCode::from(EX_USAGE as u8)
}
fn main() -> ExitCode {
let argv = args().collect::<Vec<String>>();
if argv.len() == 2 {
if let Ok(s) = argv[1].parse::<u64>() {
sleep(Duration::from_secs(s));
ExitCode::SUCCESS
} else {
usage(&argv[0])
}
} else {
usage(&argv[0])
}
}