/* * 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 /* fclose(3), feof(3), fgetc(3), fgets(3), fopen(3), * fprintf(3), fputc(3), stderr, stdin, stdout, EOF, FILE, * NULL */ #include /* size_t */ #include /* strchr(3), strcmp(3), strerror(3), strtok(3) */ #include /* getopt(3) */ /* Commands start with cmd_. They take an argc and NULL-terminated argv, like * main, and return a status from . 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 #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; }