/* * UNG's Not GNU * * Copyright (c) 2011-2022, Jakob Kaivo * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct more_file { struct more_file *next; struct more_file *prev; FILE *f; FILE *backing; size_t topline; fpos_t *tlines; size_t nlines; size_t mark[26]; size_t tick; size_t nbytes; size_t *bytepos; char *buf; size_t nbuf; char *path; int eof; }; typedef enum { DISABLE, ENABLE, CLEAR } more_clear_action; static struct termios more_term; static int more_fastexit = 0; static int more_backspace = 1; static int more_lines = 24; static FILE *more_in = NULL; enum { CTRL_B = 0x02, CTRL_D = 0x04, CTRL_F = 0x06, CTRL_G = 0x07, CTRL_L = 0x0c, CTRL_U = 0x15, }; static void more_tput(size_t n, char buf[static n], const char *cap) { char cmd[strlen(cap) + 6]; sprintf(cmd, "tput %s", cap); FILE *p = popen(cmd, "r"); if (p == NULL) { return; } fread(buf, 1, n, p); fclose(p); } static void more_echo(int on) { struct termios ti; tcgetattr(fileno(more_in), &ti); if (on) { ti.c_lflag |= ECHO; } else { ti.c_lflag &= ~ECHO; } ti.c_cc[VMIN] = 1; ti.c_cc[VTIME] = 0; tcsetattr(fileno(more_in), TCSANOW, &ti); } static void more_status(const char *fmt, ...) { static char smso[64] = ""; static char rmso[64] = ""; if (smso[0] == '\0') { more_tput(sizeof(smso), smso, "smso"); more_tput(sizeof(rmso), rmso, "rmso"); } fprintf(stderr, "%s", smso); va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fprintf(stderr, "%s", rmso); } static void more_clear(more_clear_action action) { static char clearcmd[64] = ""; if (action == ENABLE) { more_tput(sizeof(clearcmd), clearcmd, "clear"); } if (clearcmd[0] == '\0') { return; } printf("%s", clearcmd); } static ssize_t more_getline(struct more_file *mf, size_t lineno) { if (mf->nlines <= lineno && mf->nlines != 0) { fsetpos(mf->backing, &(mf->tlines[mf->nlines - 1])); getline(&(mf->buf), &(mf->nbuf), mf->backing); } while (mf->nlines <= lineno) { mf->nlines++; mf->tlines = realloc(mf->tlines, mf->nlines * sizeof(*mf->tlines)); mf->bytepos = realloc(mf->bytepos, mf->nlines * sizeof(*mf->bytepos)); fgetpos(mf->f, &(mf->tlines[mf->nlines - 1])); if (getline(&(mf->buf), &(mf->nbuf), mf->f) == -1) { mf->nlines--; mf->eof = 1; return -1; } if (mf->nlines > 1) { mf->bytepos[mf->nlines - 1] = mf->bytepos[mf->nlines - 2] + strlen(mf->buf); } else { mf->bytepos[0] = 0; } if (mf->backing != mf->f) { fgetpos(mf->backing, &(mf->tlines[mf->nlines - 1])); fputs(mf->buf, mf->backing); } } fsetpos(mf->backing, &(mf->tlines[lineno])); return getline(&(mf->buf), &(mf->nbuf), mf->backing); } static int more_open(struct more_file *mf) { mf->f = stdin; if (mf->path && strcmp(mf->path, "-")) { mf->f = fopen(mf->path, "r"); if (!mf->f) { fprintf(stderr, "more: %s: %s\r\n", mf->path, strerror(errno)); return 1; } } fpos_t pos; if (fgetpos(mf->f, &pos) != 0) { mf->backing = tmpfile(); } else { mf->backing = mf->f; struct stat st; fstat(fileno(mf->f), &st); mf->nbytes = st.st_size; } if (mf->path == NULL) { mf->path = ""; } return 0; } static void more_close(struct more_file *mf) { /* TODO: maybe save these ? */ if (mf->backing != mf->f) { fclose(mf->backing); } if (mf->f != stdin) { fclose(mf->f); } free(mf->tlines); free(mf->buf); } static void more_underline(int on) { static char underline[64] = ""; static char ununderline[64] = ""; if (underline[0] == '\0') { more_tput(sizeof(underline), underline, "smul"); more_tput(sizeof(ununderline), ununderline, "rmul"); } printf("%s", on ? underline : ununderline); } static void more_bold(int on) { static char bold[64] = ""; static char unbold[64] = ""; if (bold[0] == '\0') { more_tput(sizeof(bold), bold, "bold"); more_tput(sizeof(unbold), unbold, "sgr0"); } printf("%s", on ? bold : unbold); } static int more_printline(char *s) { static int columns = 0; if (columns == 0) { char *colenv = getenv("COLUMNS"); columns = colenv ? atoi(colenv) : 80; } int ret = -1; int output = 0; for (size_t i = 0; s[i] != '\0'; i++) { enum { NONE, UNDERLINE, BOLD } attr = NONE; if (more_backspace && s[i + 1] == '\b') { if (s[i + 2] == '_') { more_underline(1); attr = UNDERLINE; } else if (s[i + 2] == s[i]) { more_bold(1); attr = BOLD; } else { i++; continue; } } putchar(s[i]); output++; if (((output % columns) == columns ) || (s[i] == '\n')) { ret++; } if (attr == UNDERLINE) { more_underline(0); i += 2; } if (attr == BOLD) { more_bold(0); i += 2; } } return ret; } static void more_refresh(struct more_file *mf) { int printed = 0; for (size_t i = 0; (int)i + printed < more_lines - 1; i++) { if (more_getline(mf, mf->topline + i) == -1) { break; } printed += more_printline(mf->buf); } } static void more_scroll(struct more_file *mf, int count, int multiple) { int by = count ? count * multiple : multiple; if (abs(by) > more_lines) { more_clear(CLEAR); mf->tick = mf->topline; } if (by < 0) { if ((size_t)(-by) > mf->topline) { by = -mf->topline; mf->topline = 0; } else { mf->topline += by; } if (by != 0) { more_clear(CLEAR); more_refresh(mf); } return; } while (by-- > 0) { /* FIXME: account for long lines here, too */ if (more_getline(mf, mf->topline + more_lines - 1) < 0) { break; } mf->topline++; more_printline(mf->buf); } } static void more_mark(struct more_file *mf) { int c = fgetc(more_in); if (islower(c)) { mf->mark[c - 'a'] = mf->topline; } } static void more_jump(struct more_file *mf) { int c = fgetc(more_in); if (islower(c)) { mf->topline = mf->mark[c - 'a']; more_refresh(mf); } if (c == '\'') { mf->topline = mf->tick; more_refresh(mf); } } static void more_help(void) { char path[L_tmpnam + 1]; FILE *f = fopen(tmpnam(path), "w"); if (f == NULL) { fprintf(stderr, "error creating help: %s", strerror(errno)); return; } fprintf(f, "^X means hold control and press X\n"); fprintf(f, "\n"); fprintf(f, "Non-Scrolling Commands\n"); fprintf(f, "h display this help\n"); fprintf(f, ":e filename open filename\n"); fprintf(f, "#:n go to the count-th next file\n"); fprintf(f, "#:p go to the count-th previous file\n"); fprintf(f, "v edit file with $EDITOR (default vi)\n"); fprintf(f, "= ^G display current file information\n"); fprintf(f, "q :q ZZ quit\n"); fprintf(f, "r ^L redraw the current screen\n"); fprintf(f, "R discard buffered input and redraw screen\n"); fprintf(f, "\n"); fprintf(f, "Scrolling Commands: Prefix with a count for number of lines, otherwise as shown\n"); fprintf(f, "%-20s%-30s%-10s\n", " ", "forward", "backward"); fprintf(f, "%-20s%-30s%-10s\n", "one line", "j ", "k"); fprintf(f, "%-20s%-30s%-10s\n", "one half screen", "d ^D", "u ^U"); fprintf(f, "%-20s%-30s%-10s\n", "one screen", "f ^F", "b ^B"); fprintf(f, "\n"); fprintf(f, "Jumping Commands\n"); fprintf(f, "#s skip count lines after the end of the current screen\n"); fprintf(f, "#g go to line count, or 1\n"); fprintf(f, "#G go to line count, or end of file\n"); fprintf(f, ":t tag jump to tag\n"); fprintf(f, "mletter mark the current position as letter\n"); fprintf(f, "'letter return to the position marked letter\n"); fprintf(f, "'' return to last position before large movement\n"); fprintf(f, "\n"); fprintf(f, "Searching Commands\n"); fprintf(f, "#/[!]pattern search for the count-th occurence of pattern\n"); fprintf(f, "#?[!]pattern search backward\n"); fprintf(f, "#n repeat search\n"); fprintf(f, "#N repeat search backwards\n"); fclose(f); //more(path); remove(path); } static void more_invoke_editor(struct more_file *mf) { extern char **environ; char *editor = getenv("EDITOR"); if (editor == NULL) { editor = "vi"; } char line[64]; snprintf(line, sizeof(line), "%zd", mf->topline + 1); char *argv[] = { editor, "-c", line, mf->path, NULL }; char *base = editor; char *slash = strrchr(editor, '/'); if (slash) { base = slash + 1; } if (strcmp(base, "ex") && strcmp(base, "vi")) { argv[1] = mf->path; argv[2] = NULL; } pid_t pid; if (posix_spawnp(&pid, editor, NULL, NULL, argv, environ) == 0) { size_t line = mf->topline; more_close(mf); waitpid(pid, NULL, 0); more_open(mf); mf->topline = line; } else { fprintf(stderr, "%s %s: %s\n", editor, mf->path, strerror(errno)); } } static struct more_file more_examine(struct more_file *mf) { //wprintw(more_status, "e"); more_echo(1); printf(":e"); fflush(stdout); char filename[1024]; fgets(filename, sizeof(filename), more_in); more_echo(0); printf("\r%*s\r", (int)strlen(filename) + 2, " "); wordexp_t we = { 0 }; if (wordexp(filename, &we, WRDE_NOCMD) != 0) { fprintf(stderr, "couldn't expand %s", filename); return *mf; } if (we.we_wordc == 0) { return *mf; } strcpy(filename, we.we_wordv[0]); wordfree(&we); if (we.we_wordc > 1) { fprintf(stderr, "not opening multiple files"); return *mf; } if (!strcmp(filename, "#")) { strcpy(filename, mf->path); } /* struct more_file ret = more_open(filename); if (ret.f == NULL) { fprintf(stderr, "%s: %s", filename, strerror(errno)); return *mf; } more_close(mf); more_refresh(&ret); return ret; */ return *mf; } static struct more_file * more_next(struct more_file *mf, int n) { more_close(mf); if (n > 0) { while (n-- > 0 && mf->next) { mf = mf->next; } } else { while (n++ < 0 && mf->prev) { mf = mf->prev; } } more_open(mf); more_refresh(mf); return mf; } static void more_search(struct more_file *mf, int count, int reuse) { static regex_t re; static int match = -1; if (!reuse) { char *buf = NULL; size_t blen = 0; more_echo(1); printf("%c", (count > 0) ? '/' : '?'); getline(&buf, &blen, more_in); more_echo(0); if (buf[0] != '\n') { char *nl = strchr(buf, '\n'); *nl = '\0'; char *exp = buf; regfree(&re); if (buf[0] == '!') { match = REG_NOMATCH; exp++; } else { match = 0; } if (regcomp(&re, exp, REG_NOSUB) != 0) { fprintf(stderr, "more: invalid expression"); match = -1; } free(buf); } } else if (match == -1) { fprintf(stderr, "more: no previous expression"); } if (match == -1) { return; } while (count != 0) { mf->topline += (count > 0) ? 1 : -1; if (more_getline(mf, mf->topline) == -1) { fprintf(stderr, "more: expression not found"); return; } if (regexec(&re, mf->buf, 0, NULL, 0) == match) { count += (count > 0) ? -1 : 1; } } more_refresh(mf); } static int more(struct more_file *mf) { if (more_open(mf) != 0) { /* FIXME: try next file */ return 1; } more_refresh(mf); int count = 0; while (mf) { int at_eof = (mf->eof && (mf->topline + more_lines >= mf->nlines)); if (at_eof) { if (more_fastexit && mf->next == NULL) { return 0; } more_status("%s ", mf->path); } else { more_status("%s", mf->path); } int c = fgetc(more_in); fprintf(stderr, "\r%*s\r", 80, " "); switch (c) { case EOF: perror("fgetc"); exit(2); break; case 'h': more_help(); break; case 'f': case CTRL_F: if (at_eof) { mf = more_next(mf, 1); break; } if (count == 0) { count = more_lines - 1; } more_scroll(mf, count, 1); break; case 'b': case CTRL_B: if (count == 0) { count = more_lines - 1; } more_scroll(mf, count, -1); break; case ' ': count = count ? count : more_lines - 1; /* FALLTHRU */ case 'j': case '\n': if (at_eof) { mf = more_next(mf, 1); break; } more_scroll(mf, count, 1); break; case 'k': more_scroll(mf, count, -1); break; case 'd': case CTRL_D: if (at_eof) { mf = more_next(mf, 1); break; } more_scroll(mf, count, (more_lines - 1) / 2); break; case 's': if (at_eof) { mf = more_next(mf, 1); } count = count ? count : 1; more_scroll(mf, more_lines - 1 + count, 1); break; case 'u': case CTRL_U: more_scroll(mf, count, -(more_lines - 1) / 2); break; case 'G': if (count == 0) { count = mf->nbytes; } /* FALLTHRU */ case 'g': more_scroll(mf, (count - 1) - mf->topline, 1); break; case 'R': // discard(); /* FALLTHRU */ case 'r': case CTRL_L: more_clear(CLEAR); more_refresh(mf); break; case 'm': more_mark(mf); break; case '\'': more_jump(mf); break; case '/': more_search(mf, count ? count : 1, 0); break; case '?': more_search(mf, count ? -count : -1, 0); break; case 'n': more_search(mf, count ? count : 1, 1); break; case 'N': more_search(mf, count ? -count : -1, 1); break; case ':': //waddch(more_status, c); ; int c2 = fgetc(more_in); //wprintw(more_status, "\b \b"); switch (c2) { case 'e': more_examine(mf); break; case 'p': count = count ? -count : -1; /* FALLTHRU */ case 'n': count = count ? count : 1; mf = more_next(mf, count); break; case 't': // tagstring(); break; case 'q': exit(0); default: break; } break; case 'v': more_invoke_editor(mf); break; case '=': case CTRL_G: more_status("%s; File %d/%d; Line %zd; Byte %zd/%zd; %zd%%", mf->path, 0, 0, mf->topline, mf->bytepos[mf->topline], mf->nbytes, 100 * mf->bytepos[mf->topline] / mf->nbytes); break; case 'Z': if (fgetc(more_in) != 'Z') { break; } /* FALLTHRU */ case 'q': exit(0); case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': count = count * 10 + (c - '0'); break; default: break; } if (!isdigit(c)) { count = 0; } } return 0; } static void adjust_args(int *argc, char ***argv) { char *env = getenv("MORE"); if (env) { char **newargv = malloc((*argc + 2) * sizeof(*newargv)); newargv[0] = (*argv)[0]; /* TODO: account for spaces in env */ newargv[1] = env; for (int i = 1; i < *argc; i++) { newargv[i + 1] = (*argv)[i]; } newargv[*argc + 1] = NULL; *argv = newargv; (*argc)++; } for (int i = 1; i < *argc; i++) { if (!strcmp((*argv)[i], "--")) { return; } if ((*argv)[i][0] == '+') { fprintf(stderr, "more: adjusting command line option +%s to -%s\n", (*argv)[i] + 1, (*argv)[i] + 1); (*argv)[i][0] = '-'; } } } static void cat_loop(FILE *f) { int c = 0; while ((c = fgetc(f)) != EOF) { putchar(c); } } static void compress_loop(FILE *f) { static int nl = 0; int c = 0; while ((c = fgetc(f)) != EOF) { if (c == '\n') { if (nl == 2) { continue; } else if (nl == 1) { nl = 2; } else { nl = 1; } } else { nl = 0; } putchar(c); } } static int more_cat(const char *path, void (*loop)(FILE *)) { FILE *f = stdin; if (path != NULL && strcmp(path, "-") != 0) { f = fopen(path, "r"); if (!f) { fprintf(stderr, "more: %s: %s\n", path, strerror(errno)); return 1; } } loop(f); if (f != stdin) { fclose(f); } return 0; } static void more_exit(void) { tcsetattr(fileno(more_in), TCSANOW, &more_term); printf("\n"); } int main(int argc, char *argv[]) { setlocale(LC_ALL, ""); int ignorecase = 0; int compressempty = 0; adjust_args(&argc, &argv); int c; while ((c = getopt(argc, argv, "ceisun:p:t:")) != -1) { switch (c) { case 'c': more_clear(ENABLE); break; case 'e': more_fastexit = 1; break; case 'i': ignorecase = 1; break; case 's': compressempty = 1; break; case 'u': more_backspace = 0; break; case 'n': more_lines = atoi(optarg); break; case 'p': //perfile = optarg; break; case 't': //tag = optarg; break; default: return 1; } } if (!isatty(STDOUT_FILENO)) { int ret = 0; void (*loop)(FILE*) = compressempty ? compress_loop : cat_loop; do { ret |= more_cat(argv[optind++], loop); } while (optind < argc); return ret; } more_in = fdopen(STDERR_FILENO, "r"); if (more_in == NULL) { more_in = fopen("/dev/tty", "r"); } setbuf(more_in, NULL); tcgetattr(fileno(more_in), &more_term); struct termios tinit = more_term; tinit.c_lflag &= ~(ECHO | ICANON); tinit.c_cc[VMIN] = 1; tinit.c_cc[VTIME] = 0; tcsetattr(fileno(more_in), TCSANOW, &tinit); if (more_in == NULL) { perror("more: can't read commands"); return 1; } atexit(more_exit); char *lines = getenv("LINES"); if (lines) { more_lines = atoi(lines); } more_clear(CLEAR); struct more_file *head = NULL; do { if (head != NULL && argv[optind] == NULL) { break; } struct more_file *mf = calloc(1, sizeof(*mf)); if (!mf) { perror("more"); return 1; } mf->path = argv[optind]; if (head == NULL) { head = mf; } else { struct more_file *c = head; while (c->next != NULL) { c = c->next; } c->next = mf; mf->prev = c; } } while (argv[optind++]); (void)ignorecase; return more(head); }