/* Copyright (C) 2008 Emmanuel Varoquaux This file is part of XOS. This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* Note : cette version de more presente quelques differences majeures avec son * homologue Linux (Berkeley) : * - tous les caracteres imprimables definis par la norme ISO 8859 sont comptes * (contre les seuls caracteres imprimables ASCII dans Linux) ; * - les lignes comptees sont les lignes affichees a l'ecran (contre les lignes * logiques dans Linux) ; * - certaines touches directionnelles sont reconnues pour la navigation dans * le fichier. */ #include #include #include #include #include #include #include #include #include #include #include #include #define CS_REVERSE "\e[7m" #define CS_NORMAL "\e[0m" #define CS_ERASE_BEGLINE "\e[1K" #define CS_ERASE_SCREEN "\e[2J" #define CS_HOME "\e[H" #define LINE_BUF_SIZE 256 #define ctrl(letter) ((letter) & 077) #define min(a, b) ({ \ typeof (a) _a = (a); \ typeof (b) _b = (b); \ _a < _b ? _a : _b; \ }) enum { CM_NONE, CM_NEXT_LINE, CM_NEXT_SCREENFUL, CM_SCROLL, CM_SKIP_BW_LINE, CM_SKIP_BW_SCREENFUL, CM_SKIP_FW_LINE, CM_SKIP_FW_SCREENFUL, CM_BEGIN, CM_END, CM_REDRAW, CM_DISPLAY_LINE_NR, CM_DISPLAY_FILENAME, CM_PREVIOUS_FILE, CM_NEXT_FILE, CM_EXIT, CM_HELP, CM_REPEAT }; enum {BT_NONE, BT_BANNER, BT_PAUSE}; /* caracteres textuels non imprimables */ static const char textcntrl[] = {'\b', '\t', '\n', '\f', '\r'}; /* extension de isprint() prenant en compte les caracteres imprimables definis par la norme ISO 8859 */ #define isprint_ext(c) (isprint(c) || (c) >= 160) /* more est concu pour visualiser des fichiers dont tous les caracteres sont textuels (caracteres imprimables ou caracteres de controle). Le comportement de more sur des fichiers binaires est indefini. */ #define istext(c) (isprint_ext(c) || memchr(textcntrl, (c), sizeof textcntrl)) static int has_screen; static struct winsize winsize; static int screen_lines; static int scroll_lines; static struct termios saved_termios; static int last_cmd; static inline void ring_bell() { fputc('\a', stderr); } /* terminal */ static void setup_term() { struct termios new_termios; tcgetattr(fileno(stderr), &saved_termios); new_termios = saved_termios; new_termios.c_lflag &= ~(ICANON | ECHO | ISIG); new_termios.c_cc[VMIN] = 1; new_termios.c_cc[VTIME] = 0; tcsetattr(fileno(stderr), TCSANOW, &new_termios); } static void restore_term() { tcsetattr(fileno(stderr), TCSANOW, &saved_termios); } /* pause */ static int read_char() { unsigned char c; if (read(fileno(stderr), &c, 1) == -1) { restore_term(); error(EXIT_FAILURE, errno, "read error"); } return c; } static int read_cmd() { int state; int c; state = 0; while (1) { c = read_char(); switch (state) { case 0: switch (c) { case 'h': case '?': return CM_HELP; case ' ': return CM_NEXT_SCREENFUL; case 'z': return CM_NEXT_SCREENFUL; case '\n': return CM_NEXT_LINE; case 'd': case ctrl('D'): return CM_SCROLL; case 'q': case 'Q': case ctrl('C'): return CM_EXIT; case 's': return CM_SKIP_FW_LINE; case 'f': return CM_SKIP_FW_SCREENFUL; case 'b': case ctrl('B'): return CM_SKIP_BW_SCREENFUL; case '=': return CM_DISPLAY_LINE_NR; case ctrl('L'): return CM_REDRAW; case '.': return CM_REPEAT; case ':': state = 1; continue; case '\e': /* extension */ /* touches directionnelles UP \e[A CM_SKIP_BW_LINE DOWN \e[B CM_NEXT_LINE RIGHT \e[C CM_NEXT_FILE LEFT \e[D CM_PREVIOUS_FILE PGUP \e[5~ CM_SKIP_BW_SCREENFUL PGDN \e[6~ CM_NEXT_SCREENFUL HOME \e[1~ CM_BEGIN END \e[4~ CM_END */ state = 2; continue; } break; case 1: switch (c) { case 'n': return CM_NEXT_FILE; case 'p': return CM_PREVIOUS_FILE; case 'f': return CM_DISPLAY_FILENAME; } break; case 2: if (c == '[') { state = 3; continue; } break; case 3: switch (c) { case 'A': return CM_SKIP_BW_LINE; case 'B': return CM_NEXT_LINE; case 'C': return CM_NEXT_FILE; case 'D': return CM_PREVIOUS_FILE; case '1': state = 4; continue; case '4': state = 5; continue; case '5': state = 6; continue; case '6': state = 7; continue; } break; case 4: if (c == '~') return CM_BEGIN; break; case 5: if (c == '~') return CM_END; break; case 6: if (c == '~') return CM_SKIP_BW_SCREENFUL; break; case 7: if (c == '~') return CM_NEXT_SCREENFUL; break; } ring_bell(); state = 0; } } static void display_prompt(const char *format, va_list ap) { fputs(CS_REVERSE, stdout); fputs("--More--", stdout); if (format) { putchar('('); vprintf(format, ap); putchar(')'); } fputs(CS_NORMAL, stdout); fflush(stdout); } static void display_line(int line) { printf("%d", line); fflush(stdout); } static void display_filename(const char *filename, int line) { if (filename) printf("\"%s\"", filename); else printf("[Not a file]"); printf(" line %d", line); fflush(stdout); } static void erase_prompt() { fputs(CS_ERASE_BEGLINE, stdout); putchar('\r'); } static int pause_format(const char *format, ...) { va_list ap; int cmd; va_start(ap, format); display_prompt(format, ap); va_end(ap); cmd = read_cmd(); erase_prompt(); return cmd; } static int pause_void() { return pause_format(NULL); } static int pause_percent(int percent) { return pause_format("%d%%", percent); } static int pause_nextfile(const char *filename) { return pause_format("Next file: %s", filename); } static int pause_line(int line) { int cmd; display_line(line); cmd = read_cmd(); erase_prompt(); return cmd; } static int pause_filename(const char *filename, int line) { int cmd; display_filename(filename, line); cmd = read_cmd(); erase_prompt(); return cmd; } /* lecture du fichier */ static int must_clear_screen(FILE *fp) { int c; if ((c = getc(fp)) != EOF) ungetc(c, fp); return c == '\f'; } /* Retourne 1 si une pause est requise. */ static int get_line(FILE *fp, char buf[], size_t *len) { int col; int formfeed; int need_wrap; char *p; int c; col = 0; formfeed = 0; need_wrap = 0; p = buf; while (p != &buf[LINE_BUF_SIZE - 1]) { if ((c = getc(fp)) == EOF) { if (p > buf) goto end_line; else goto end; } *p++ = c; switch (c) { case '\b': /* BS */ if (col > 0) { col--; need_wrap = 0; } continue; case '\t': /* HT */ col = min((col | 7) + 1, winsize.ws_col - 1); continue; case '\n': /* LF */ goto end; case '\f': /* FF */ if (need_wrap) goto wrap; p[-1] = '^'; *p++ = 'L'; formfeed = 1; if (col + 2 < winsize.ws_col) col += 2; else goto end; continue; case '\r': /* CR */ col = 0; need_wrap = 0; continue; } if (isprint_ext(c)) { /* caracteres imprimables */ if (need_wrap) goto wrap; if (col + 1 < winsize.ws_col) col++; else need_wrap = 1; } continue; wrap: ungetc(c, fp); p[-1] = '\n'; goto end; } end_line: *p++ = '\n'; end: *len = p - buf; return formfeed; } static int more_chars(FILE *fp) { int c; if ((c = getc(fp)) == EOF) return 0; ungetc(c, fp); return 1; } /* impression */ static void clear_screen() { fputs(CS_ERASE_SCREEN, stdout); fputs(CS_HOME, stdout); } static void print_separator() { puts("-------------------------------------------------------------------------------"); } static void print_skipping_lines(int lines) { putchar('\n'); if (lines == 1) puts("...skipping one line"); else printf("...skipping %d lines\n", lines); } static void print_skipping_back_lines(int lines) { putchar('\n'); if (lines == 1) puts("...skipping back one line"); else printf("...skipping back %d lines\n", lines); } static void print_skipping_back_pages(int pages) { putchar('\n'); if (pages == 1) puts("...back 1 page"); else printf("...back %d pages\n", pages); } static void print_skipping_back_to_beginning() { putchar('\n'); puts("...skipping back to beginning"); } static void print_skipping_to_file(const char *filename) { putchar('\n'); puts("...Skipping"); printf("...Skipping to file %s\n", filename); putchar('\n'); } static void print_skipping_back_to_file(const char *filename) { putchar('\n'); puts("...Skipping"); printf("...Skipping back to file %s\n", filename); putchar('\n'); } static void print_banner(const char *filename) { puts("::::::::::::::"); puts(filename); puts("::::::::::::::"); } static void print_directory(const char *filename) { putchar('\n'); printf("*** %s: directory ***\n", filename); putchar('\n'); } static void print_nottextfile(const char *filename) { putchar('\n'); printf("******** %s: Not a text file ********\n", filename); putchar('\n'); } static void print_file(FILE *fp) { int c; while ((c = getc(fp)) != EOF) putchar(c); } static void skip_lines(FILE *fp, int lines) { char buf[LINE_BUF_SIZE + 1]; size_t len; while (lines > 0) { get_line(fp, buf, &len); if (!len) break; lines--; } } static void print_line(const char *s, size_t len) { while (len--) putchar(*s++); } /* Si lines >= 0, affiche lines lignes de texte. * Si lines < 0, affiche toutes les lignes de texte jusqu'a la fin du fichier. * Retourne le nombre de lignes affichees, ou -1 si le fichier est termine. */ static int print_lines(FILE *fp, int lines) { int n; char buf[LINE_BUF_SIZE + 1]; size_t len; int need_pause; if (lines >= 0) { n = 0; while (n < lines) { need_pause = get_line(fp, buf, &len); if (!len) return -1; print_line(buf, len); if (!more_chars(fp)) return -1; n++; if (need_pause) break; } return n; } else while (1) { get_line(fp, buf, &len); if (!len) return -1; print_line(buf, len); } } static void print_commands() { puts(" h or ? Help: display a summary of these commands"); puts(" Display next screenful of text"); puts(" z Display next screenful of text"); puts(" Display next line of text"); puts(" d or ctrl-D Scroll 1 half-screenful of text"); puts(" q or Q Exit from more"); puts(" s Skip forward 1 line of text"); puts(" f Skip forward 1 screenful of text"); puts(" b or ctrl-B Skip backwards 1 screenful of text"); puts(" = Display current line number"); puts(" ctrl-L Redraw screen"); puts(" :n Go to next file"); puts(" :p Go to previous file"); puts(" :f Display current file name and line number"); puts(" . Repeat previous command"); puts(" Skip backward 1 line of text"); puts(" Display next line of text"); puts(" Go to next file"); puts(" Go to previous file"); puts(" Skip backwards 1 screenful of text"); puts(" Display next screenful of text"); puts(" Go to beginning of current file"); puts(" Go to end of current file"); } /* more */ static inline int get_percent(FILE *fp, const struct stat *statbuf) { long offset; if (statbuf->st_size == 0 || (offset = ftell(fp)) == -1) return 0; return offset * 100 / statbuf->st_size; } /* filename == NULL pour visualiser l'entree standard. * Si filename == NULL, banner_type doit etre BT_NONE. * more() retourne dans next_file l'indice du prochain fichier a traiter a * partir du fichier courant. Si aucun fichier n'a ete demande par * l'utilisateur, la valeur par defaut 0 est retournee. */ static int more(const char *filename, int banner_type, int *next_file) { int err; struct stat statbuf; int seekable; FILE *fp; unsigned char buf[1]; int line; int started; int cmd; int lines; err = 0; *next_file = 0; /* valeur par defaut */ if (filename) { if (stat(filename, &statbuf) == -1) { error(0, errno, filename); err = 1; goto end; } if (S_ISDIR(statbuf.st_mode)) { print_directory(filename); goto end; } seekable = S_ISREG(statbuf.st_mode); if (!(fp = fopen(filename, "r"))) { error(0, errno, filename); err = 1; goto end; } if (fread(buf, 1, sizeof buf, fp) == sizeof buf) { if (!istext(buf[0])) { print_nottextfile(filename); goto fclose; } rewind(fp); } } else { if (fstat(fileno(stdin), &statbuf) == -1) { error(0, errno, "standard input"); err = 1; goto end; } seekable = 0; fp = stdin; } if (has_screen) { line = 0; started = 0; if (banner_type == BT_PAUSE) goto pause; lines = screen_lines; while (1) { if (!started) { if (must_clear_screen(fp)) clear_screen(); if (banner_type != BT_NONE) { print_banner(filename); if (screen_lines < 3) lines = 0; else if (lines > screen_lines - 3) lines = screen_lines - 3; /* banniere de 3 lignes */ } started = 1; } if ((lines = print_lines(fp, lines)) == -1) break; line += lines; pause: if (!started) cmd = pause_nextfile(filename); else if (seekable) cmd = pause_percent(get_percent(fp, &statbuf)); else cmd = pause_void(); process_cmd: if (cmd == CM_REPEAT) { if (last_cmd == CM_NONE) goto invalid_cmd; cmd = last_cmd; } else last_cmd = cmd; switch (cmd) { case CM_NEXT_LINE: lines = 1; continue; case CM_NEXT_SCREENFUL: lines = screen_lines; continue; case CM_SCROLL: lines = scroll_lines; continue; case CM_SKIP_BW_LINE: if (!seekable) goto invalid_cmd; print_skipping_back_lines(1); fseek(fp, 0, SEEK_SET); if ((line -= screen_lines + 1) < 0) line = 0; skip_lines(fp, line); lines = screen_lines; continue; case CM_SKIP_BW_SCREENFUL: if (!seekable) goto invalid_cmd; print_skipping_back_pages(1); fseek(fp, 0, SEEK_SET); if ((line -= 2 * screen_lines) < 0) line = 0; skip_lines(fp, line); lines = screen_lines; continue; case CM_SKIP_FW_LINE: print_skipping_lines(1); skip_lines(fp, 1); line++; lines = screen_lines; continue; case CM_SKIP_FW_SCREENFUL: print_skipping_lines(screen_lines); skip_lines(fp, screen_lines); line += screen_lines; lines = screen_lines; continue; case CM_BEGIN: if (!seekable) goto invalid_cmd; print_skipping_back_to_beginning(); fseek(fp, 0, SEEK_SET); line = 0; lines = screen_lines; continue; case CM_END: lines = -1; continue; case CM_REDRAW: if (!seekable) goto invalid_cmd; clear_screen(); fseek(fp, 0, SEEK_SET); if ((line -= screen_lines) < 0) line = 0; skip_lines(fp, line); lines = screen_lines; continue; case CM_DISPLAY_LINE_NR: cmd = pause_line(line); goto process_cmd; case CM_DISPLAY_FILENAME: cmd = pause_filename(filename, line); goto process_cmd; case CM_NEXT_FILE: *next_file = 1; goto end_file; case CM_PREVIOUS_FILE: if (!filename) goto invalid_cmd; *next_file = -1; goto end_file; case CM_EXIT: restore_term(); exit(EXIT_SUCCESS); case CM_HELP: print_separator(); print_commands(); print_separator(); goto pause; } invalid_cmd: ring_bell(); goto pause; } } else { if (banner_type != BT_NONE) print_banner(filename); print_file(fp); } end_file: if (filename) { fclose: fflush(stdout); if (fclose(fp) == EOF) { error(0, errno, filename); err = 1; goto end; } } end: return err ? -1 : 0; } static void __attribute__ ((noreturn)) die_bad_usage() { fprintf(stderr, "Try `%s --help' for more information.\n", program_invocation_name); exit(EXIT_FAILURE); } static void print_help() { printf("Usage: %s [OPTION]... [FILE]...\n", program_invocation_name); puts("Print file(s) to standard output, screen by screen."); putchar('\n'); puts("Options are:"); puts(" -h, --help display this help and exit"); putchar('\n'); puts("Commands are:"); print_commands(); } int main(int argc, char **argv) { static const struct option longopts[] = { {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; int err; char **filename_list; int filename_count; int filename_ind; int next_file; int banner_type; int c; while ((c = getopt_long(argc, argv, "h", longopts, NULL)) != -1) switch (c) { case 'h': print_help(); return EXIT_SUCCESS; case '?': die_bad_usage(); } has_screen = ioctl(fileno(stdout), TIOCGWINSZ, &winsize) == 0 && winsize.ws_row > 1 && winsize.ws_col > 0; if (has_screen) { screen_lines = winsize.ws_row - 1; scroll_lines = winsize.ws_row / 2 - 1 ? : 1; setup_term(); } err = 0; filename_list = argv + optind; filename_count = argc - optind; last_cmd = CM_NONE; if (!isatty(fileno(stdin))) { if (filename_count > 0 && !strcmp(filename_list[0], "-")) { filename_list++; filename_count--; } if (more(NULL, BT_NONE, &next_file) == -1) { err = 1; next_file = 0; } filename_ind = (next_file ? : 1) - 1; banner_type = BT_PAUSE; } else if (filename_count > 0) { filename_ind = 0; next_file = 0; banner_type = filename_count > 1 ? BT_BANNER : BT_NONE; } else { if (has_screen) restore_term(); error(0, 0, "missing operand"); die_bad_usage(); } while (1) { if (filename_ind < 0) filename_ind = 0; if (filename_ind >= filename_count) break; if (next_file < 0) print_skipping_back_to_file(filename_list[filename_ind]); else if (next_file > 0) print_skipping_to_file(filename_list[filename_ind]); if (more(filename_list[filename_ind], banner_type, &next_file) == -1) err = 1; banner_type = BT_PAUSE; filename_ind += next_file ? : 1; } if (has_screen) restore_term(); return err ? EXIT_FAILURE : EXIT_SUCCESS; }