Compare commits

...

6 Commits
main ... pg

3 changed files with 307 additions and 2 deletions

View File

@ -102,11 +102,15 @@ mm: build/bin/mm
build/bin/mm: src/mm.c build build/bin/mm: src/mm.c build
$(CC) $(CFLAGS) -o $@ src/mm.c $(CC) $(CFLAGS) -o $@ src/mm.c
.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

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)

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;
}