| /* |
| * Copyright (C) 1980 The Regents of the University of California. |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms are permitted |
| * provided that the above copyright notice and this paragraph are |
| * duplicated in all such forms and that any documentation, |
| * advertising materials, and other materials related to such |
| * distribution and use acknowledge that the software was developed |
| * by the University of California, Berkeley. The name of the |
| * University may not be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR |
| * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. |
| */ |
| |
| /* more.c - General purpose tty output filter and file perusal program |
| * |
| * by Eric Shienbrood, UC Berkeley |
| * |
| * modified by Geoff Peck |
| * UCB to add underlining, single spacing |
| * modified by John Foderaro |
| * UCB to add -c and MORE environment variable |
| * modified by Erik Troan <ewt@redhat.com> |
| * to be more posix and so compile on linux/axp. |
| * modified by Kars de Jong <jongk@cs.utwente.nl> |
| * to use terminfo instead of termcap. |
| * 1999-02-22 Arkadiusz MiĆkiewicz <misiek@pld.ORG.PL> |
| * added Native Language Support |
| * 1999-03-19 Arnaldo Carvalho de Melo <acme@conectiva.com.br> |
| * more nls translatable strings |
| * 1999-05-09 aeb |
| * applied a RedHat patch (setjmp->sigsetjmp); without it a second |
| * ^Z would fail. |
| * 1999-05-09 aeb |
| * undone Kars' work, so that more works without libcurses (and |
| * hence can be in /bin with libcurses being in |
| * /usr/lib which may not be mounted). However, when termcap is not |
| * present curses can still be used. |
| * 2010-10-21 Davidlohr Bueso <dave@gnu.org> |
| * modified mem allocation handling for util-linux |
| */ |
| |
| #include <stdio.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <stdlib.h> |
| #include <stdarg.h> |
| #include <sys/param.h> |
| #include <ctype.h> |
| #include <signal.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <termios.h> |
| #include <sys/ioctl.h> |
| #include <sys/stat.h> |
| #include <sys/file.h> |
| #include <sys/ttydefaults.h> |
| #include <sys/wait.h> |
| #include <regex.h> |
| #include <assert.h> |
| #include <poll.h> |
| #include <sys/signalfd.h> |
| #include <paths.h> |
| #include <getopt.h> |
| |
| #if defined(HAVE_NCURSESW_TERM_H) |
| # include <ncursesw/term.h> |
| #elif defined(HAVE_NCURSES_TERM_H) |
| # include <ncurses/term.h> |
| #elif defined(HAVE_TERM_H) |
| # include <term.h> |
| #endif |
| |
| #ifdef HAVE_MAGIC |
| # include <magic.h> |
| #endif |
| |
| #include "strutils.h" |
| #include "nls.h" |
| #include "xalloc.h" |
| #include "widechar.h" |
| #include "closestream.h" |
| #include "rpmatch.h" |
| #include "env.h" |
| |
| #ifdef TEST_PROGRAM |
| # define NON_INTERACTIVE_MORE 1 |
| #endif |
| |
| #define BACKSPACE "\b" |
| #define CARAT "^" |
| |
| #define ARROW_UP "\x1b\x5b\x41" |
| #define ARROW_DOWN "\x1b\x5b\x42" |
| #define PAGE_UP "\x1b\x5b\x35\x7e" |
| #define PAGE_DOWN "\x1b\x5b\x36\x7e" |
| |
| #define MIN_LINE_SZ 256 /* minimal line_buf buffer size */ |
| #define ESC '\033' |
| #define SCROLL_LEN 11 |
| #define LINES_PER_PAGE 24 |
| #define NUM_COLUMNS 80 |
| #define TERMINAL_BUF 4096 |
| #define INIT_BUF 80 |
| #define COMMAND_BUF 200 |
| #define REGERR_BUF NUM_COLUMNS |
| |
| #define TERM_AUTO_RIGHT_MARGIN "am" |
| #define TERM_BACKSPACE "cub1" |
| #define TERM_CEOL "xhp" |
| #define TERM_CLEAR "clear" |
| #define TERM_CLEAR_TO_LINE_END "el" |
| #define TERM_CLEAR_TO_SCREEN_END "ed" |
| #define TERM_COLS "cols" |
| #define TERM_CURSOR_ADDRESS "cup" |
| #define TERM_EAT_NEW_LINE "xenl" |
| #define TERM_EXIT_STANDARD_MODE "rmso" |
| #define TERM_HARD_COPY "hc" |
| #define TERM_HOME "home" |
| #define TERM_LINE_DOWN "cud1" |
| #define TERM_LINES "lines" |
| #define TERM_OVER_STRIKE "os" |
| #define TERM_STANDARD_MODE "smso" |
| #define TERM_STD_MODE_GLITCH "xmc" |
| |
| /* Used in read_command() */ |
| typedef enum { |
| more_kc_unknown_command, |
| more_kc_colon, |
| more_kc_repeat_previous, |
| more_kc_backwards, |
| more_kc_jump_lines_per_screen, |
| more_kc_set_lines_per_screen, |
| more_kc_set_scroll_len, |
| more_kc_quit, |
| more_kc_skip_forward_screen, |
| more_kc_skip_forward_line, |
| more_kc_next_line, |
| more_kc_clear_screen, |
| more_kc_previous_search_match, |
| more_kc_display_line, |
| more_kc_display_file_and_line, |
| more_kc_repeat_search, |
| more_kc_search, |
| more_kc_run_shell, |
| more_kc_help, |
| more_kc_next_file, |
| more_kc_previous_file, |
| more_kc_run_editor |
| } more_key_commands; |
| struct number_command { |
| unsigned int number; |
| more_key_commands key; |
| }; |
| |
| struct more_control { |
| struct termios output_tty; /* output terminal */ |
| struct termios original_tty; /* original terminal settings */ |
| FILE *current_file; /* currently open input file */ |
| off_t file_position; /* file position */ |
| off_t file_size; /* file size */ |
| int argv_position; /* argv[] position */ |
| int lines_per_screen; /* screen size in lines */ |
| int d_scroll_len; /* number of lines scrolled by 'd' */ |
| int prompt_len; /* message prompt length */ |
| int current_line; /* line we are currently at */ |
| int next_jump; /* number of lines to skip ahead */ |
| char **file_names; /* The list of file names */ |
| int num_files; /* Number of files left to process */ |
| char *shell; /* name of the shell to use */ |
| int sigfd; /* signalfd() file descriptor */ |
| sigset_t sigset; /* signal operations */ |
| char *line_buf; /* line buffer */ |
| size_t line_sz; /* size of line_buf buffer */ |
| int lines_per_page; /* lines per page */ |
| char *clear; /* clear screen */ |
| char *erase_line; /* erase line */ |
| char *enter_std; /* enter standout mode */ |
| char *exit_std; /* exit standout mode */ |
| char *backspace_ch; /* backspace character */ |
| char *go_home; /* go to home */ |
| char *move_line_down; /* move line down */ |
| char *clear_rest; /* clear rest of screen */ |
| int num_columns; /* number of columns */ |
| char *next_search; /* file beginning search string */ |
| char *previous_search; /* previous search() buf[] item */ |
| struct { |
| off_t row_num; /* row file position */ |
| long line_num; /* line number */ |
| } context, |
| screen_start; |
| unsigned int leading_number; /* number in front of key command */ |
| struct number_command previous_command; /* previous key command */ |
| char *shell_line; /* line to execute in subshell */ |
| #ifdef HAVE_MAGIC |
| magic_t magic; /* libmagic database entries */ |
| #endif |
| unsigned int |
| bad_stdout:1, /* true if overwriting does not turn off standout */ |
| catch_suspend:1, /* we should catch the SIGTSTP signal */ |
| clear_line_ends:1, /* do not scroll, paint each screen from the top */ |
| clear_first:1, /* is first character in file \f */ |
| dumb_tty:1, /* is terminal type known */ |
| eat_newline:1, /* is newline ignored after 80 cols */ |
| erase_input_ok:1, /* is erase input supported */ |
| erase_previous_ok:1, /* is erase previous supported */ |
| first_file:1, /* is the input file the first in list */ |
| fold_long_lines:1, /* fold long lines */ |
| hard_tabs:1, /* print spaces instead of '\t' */ |
| hard_tty:1, /* is this hard copy terminal (a printer or such) */ |
| leading_colon:1, /* key command has leading ':' character */ |
| is_paused:1, /* is output paused */ |
| no_quit_dialog:1, /* suppress quit dialog */ |
| no_scroll:1, /* do not scroll, clear the screen and then display text */ |
| no_tty_in:1, /* is input in interactive mode */ |
| no_tty_out:1, /* is output in interactive mode */ |
| print_banner:1, /* print file name banner */ |
| reading_num:1, /* are we reading leading_number */ |
| report_errors:1, /* is an error reported */ |
| search_at_start:1, /* search pattern defined at start up */ |
| search_called:1, /* previous more command was a search */ |
| squeeze_spaces:1, /* suppress white space */ |
| stdout_glitch:1, /* terminal has standout mode glitch */ |
| stop_after_formfeed:1, /* stop after form feeds */ |
| suppress_bell:1, /* suppress bell */ |
| wrap_margin:1; /* set if automargins */ |
| }; |
| |
| static void __attribute__((__noreturn__)) usage(void) |
| { |
| printf("%s", USAGE_HEADER); |
| printf(_(" %s [options] <file>...\n"), program_invocation_short_name); |
| |
| printf("%s", USAGE_SEPARATOR); |
| printf("%s\n", _("A file perusal filter for CRT viewing.")); |
| |
| printf("%s", USAGE_OPTIONS); |
| printf("%s\n", _(" -d, --silent display help instead of ringing bell")); |
| printf("%s\n", _(" -f, --logical count logical rather than screen lines")); |
| printf("%s\n", _(" -l, --no-pause suppress pause after form feed")); |
| printf("%s\n", _(" -c, --print-over do not scroll, display text and clean line ends")); |
| printf("%s\n", _(" -p, --clean-print do not scroll, clean screen and display text")); |
| printf("%s\n", _(" -s, --squeeze squeeze multiple blank lines into one")); |
| printf("%s\n", _(" -u, --plain suppress underlining and bold")); |
| printf("%s\n", _(" -n, --lines <number> the number of lines per screenful")); |
| printf("%s\n", _(" -<number> same as --lines")); |
| printf("%s\n", _(" +<number> display file beginning from line number")); |
| printf("%s\n", _(" +/<pattern> display file beginning from pattern match")); |
| printf("%s", USAGE_SEPARATOR); |
| printf(USAGE_HELP_OPTIONS(23)); |
| printf(USAGE_MAN_TAIL("more(1)")); |
| exit(EXIT_SUCCESS); |
| } |
| |
| static void argscan(struct more_control *ctl, int as_argc, char **as_argv) |
| { |
| int c, opt; |
| static const struct option longopts[] = { |
| { "silent", no_argument, NULL, 'd' }, |
| { "logical", no_argument, NULL, 'f' }, |
| { "no-pause", no_argument, NULL, 'l' }, |
| { "print-over", no_argument, NULL, 'c' }, |
| { "clean-print", no_argument, NULL, 'p' }, |
| { "squeeze", no_argument, NULL, 's' }, |
| { "plain", no_argument, NULL, 'u' }, |
| { "lines", required_argument, NULL, 'n' }, |
| { "version", no_argument, NULL, 'V' }, |
| { "help", no_argument, NULL, 'h' }, |
| { NULL, 0, NULL, 0 } |
| }; |
| |
| /* Take care of number option and +args. */ |
| for (opt = 0; opt < as_argc; opt++) { |
| int move = 0; |
| |
| if (as_argv[opt][0] == '-' && isdigit_string(as_argv[opt] + 1)) { |
| ctl->lines_per_screen = |
| strtos16_or_err(as_argv[opt], _("failed to parse number")); |
| ctl->lines_per_screen = abs(ctl->lines_per_screen); |
| move = 1; |
| } else if (as_argv[opt][0] == '+') { |
| if (isdigit_string(as_argv[opt] + 1)) { |
| ctl->next_jump = strtos32_or_err(as_argv[opt], |
| _("failed to parse number")) - 1; |
| move = 1; |
| } else if (as_argv[opt][1] == '/') { |
| free(ctl->next_search); |
| ctl->next_search = xstrdup(as_argv[opt] + 2); |
| ctl->search_at_start = 1; |
| move = 1; |
| } |
| } |
| if (move) { |
| as_argc = remote_entry(as_argv, opt, as_argc); |
| opt--; |
| } |
| } |
| |
| while ((c = getopt_long(as_argc, as_argv, "dflcpsun:eVh", longopts, NULL)) != -1) { |
| switch (c) { |
| case 'd': |
| ctl->suppress_bell = 1; |
| break; |
| case 'l': |
| ctl->stop_after_formfeed = 0; |
| break; |
| case 'f': |
| ctl->fold_long_lines = 0; |
| break; |
| case 'p': |
| ctl->no_scroll = 1; |
| break; |
| case 'c': |
| ctl->clear_line_ends = 1; |
| break; |
| case 's': |
| ctl->squeeze_spaces = 1; |
| break; |
| case 'u': |
| break; |
| case 'n': |
| ctl->lines_per_screen = strtou16_or_err(optarg, _("argument error")); |
| break; |
| case 'e': /* ignored silently to be posix compliant */ |
| break; |
| case 'V': |
| print_version(EXIT_SUCCESS); |
| case 'h': |
| usage(); |
| default: |
| errtryhelp(EXIT_FAILURE); |
| break; |
| } |
| } |
| ctl->num_files = as_argc - optind; |
| ctl->file_names = as_argv + optind; |
| } |
| |
| static void env_argscan(struct more_control *ctl, const char *s) |
| { |
| char **env_argv; |
| int env_argc = 1; |
| int size = 8; |
| const char delim[] = { ' ', '\n', '\t', '\0' }; |
| char *str = xstrdup(s); |
| char *key = NULL, *tok; |
| |
| env_argv = xmalloc(sizeof(char *) * size); |
| env_argv[0] = _("MORE environment variable"); /* program name */ |
| for (tok = strtok_r(str, delim, &key); tok; tok = strtok_r(NULL, delim, &key)) { |
| env_argv[env_argc++] = tok; |
| if (size < env_argc) { |
| size *= 2; |
| env_argv = xrealloc(env_argv, sizeof(char *) * size); |
| } |
| } |
| |
| argscan(ctl, env_argc, env_argv); |
| /* Reset optind, command line parsing needs this. */ |
| optind = 0; |
| free(str); |
| free(env_argv); |
| } |
| |
| static void more_fseek(struct more_control *ctl, off_t pos) |
| { |
| ctl->file_position = pos; |
| fseeko(ctl->current_file, pos, SEEK_SET); |
| } |
| |
| static int more_getc(struct more_control *ctl) |
| { |
| int ret = getc(ctl->current_file); |
| ctl->file_position = ftello(ctl->current_file); |
| return ret; |
| } |
| |
| static int more_ungetc(struct more_control *ctl, int c) |
| { |
| int ret = ungetc(c, ctl->current_file); |
| ctl->file_position = ftello(ctl->current_file); |
| return ret; |
| } |
| |
| static void print_separator(const int c, int n) |
| { |
| while (n--) |
| putchar(c); |
| putchar('\n'); |
| } |
| |
| /* check_magic -- check for file magic numbers. */ |
| static int check_magic(struct more_control *ctl, char *fs) |
| { |
| #ifdef HAVE_MAGIC |
| const int fd = fileno(ctl->current_file); |
| const char *mime_encoding = magic_descriptor(ctl->magic, fd); |
| const char *magic_error_msg = magic_error(ctl->magic); |
| |
| if (magic_error_msg) { |
| printf("%s: %s: %s\n", program_invocation_short_name, |
| _("magic failed"), magic_error_msg); |
| return 0; |
| } |
| if (!mime_encoding || !(strcmp("binary", mime_encoding))) { |
| printf(_("\n******** %s: Not a text file ********\n\n"), fs); |
| return 1; |
| } |
| #else |
| signed char twobytes[2]; |
| |
| /* don't try to look ahead if the input is unseekable */ |
| if (fseek(ctl->current_file, 0L, SEEK_SET)) |
| return 0; |
| |
| if (fread(twobytes, 2, 1, ctl->current_file) == 1) { |
| switch (twobytes[0] + (twobytes[1] << 8)) { |
| case 0407: /* a.out obj */ |
| case 0410: /* a.out exec */ |
| case 0413: /* a.out demand exec */ |
| case 0405: |
| case 0411: |
| case 0177545: |
| case 0x457f: /* simple ELF detection */ |
| printf(_("\n******** %s: Not a text file ********\n\n"), |
| fs); |
| return 1; |
| } |
| } |
| fseek(ctl->current_file, 0L, SEEK_SET); /* rewind() not necessary */ |
| #endif |
| return 0; |
| } |
| |
| /* Check whether the file named by fs is an ASCII file which the user may |
| * access. If it is, return the opened file. Otherwise return NULL. */ |
| static void checkf(struct more_control *ctl, char *fs) |
| { |
| struct stat st; |
| int c; |
| |
| ctl->current_line = 0; |
| ctl->file_position = 0; |
| fflush(NULL); |
| |
| ctl->current_file = fopen(fs, "r"); |
| if (ctl->current_file == NULL) { |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| warn(_("cannot open %s"), fs); |
| return; |
| } |
| if (fstat(fileno(ctl->current_file), &st) != 0) { |
| warn(_("cannot stat %s"), fs); |
| return; |
| } |
| if ((st.st_mode & S_IFMT) == S_IFDIR) { |
| printf(_("\n*** %s: directory ***\n\n"), fs); |
| ctl->current_file = NULL; |
| return; |
| } |
| if (st.st_size == 0) { |
| return; |
| } |
| if (check_magic(ctl, fs)) { |
| fclose(ctl->current_file); |
| ctl->current_file = NULL; |
| return; |
| } |
| fcntl(fileno(ctl->current_file), F_SETFD, FD_CLOEXEC); |
| c = more_getc(ctl); |
| ctl->clear_first = (c == '\f'); |
| more_ungetc(ctl, c); |
| if ((ctl->file_size = st.st_size) == 0) |
| ctl->file_size = ~((off_t)0); |
| } |
| |
| static void prepare_line_buffer(struct more_control *ctl) |
| { |
| size_t sz = ctl->num_columns * 4; |
| |
| if (ctl->line_sz >= sz) |
| return; |
| |
| if (sz < MIN_LINE_SZ) |
| sz = MIN_LINE_SZ; |
| |
| /* alloc sz and extra space for \n\0 */ |
| ctl->line_buf = xrealloc(ctl->line_buf, sz + 2); |
| ctl->line_sz = sz; |
| } |
| |
| /* Get a logical line */ |
| static int get_line(struct more_control *ctl, int *length) |
| { |
| int c; |
| char *p; |
| int column; |
| static int column_wrap; |
| |
| #ifdef HAVE_WIDECHAR |
| size_t i; |
| wchar_t wc; |
| int wc_width; |
| mbstate_t state, state_bak; /* Current status of the stream. */ |
| char mbc[MB_LEN_MAX]; /* Buffer for one multibyte char. */ |
| size_t mblength; /* Byte length of multibyte char. */ |
| size_t mbc_pos = 0; /* Position of the MBC. */ |
| int use_mbc_buffer_flag = 0; /* If 1, mbc has data. */ |
| int break_flag = 0; /* If 1, exit while(). */ |
| off_t file_position_bak = ctl->file_position; |
| |
| memset(&state, '\0', sizeof(mbstate_t)); |
| #endif |
| |
| p = ctl->line_buf; |
| column = 0; |
| c = more_getc(ctl); |
| if (column_wrap && c == '\n') { |
| ctl->current_line++; |
| c = more_getc(ctl); |
| } |
| while (p < &ctl->line_buf[ctl->line_sz - 1]) { |
| #ifdef HAVE_WIDECHAR |
| if (ctl->fold_long_lines && use_mbc_buffer_flag && MB_CUR_MAX > 1) { |
| use_mbc_buffer_flag = 0; |
| state_bak = state; |
| mbc[mbc_pos++] = c; |
| process_mbc: |
| mblength = mbrtowc(&wc, mbc, mbc_pos, &state); |
| |
| switch (mblength) { |
| case (size_t)-2: /* Incomplete multibyte character. */ |
| use_mbc_buffer_flag = 1; |
| state = state_bak; |
| break; |
| |
| case (size_t)-1: /* Invalid as a multibyte character. */ |
| *p++ = mbc[0]; |
| state = state_bak; |
| column++; |
| file_position_bak++; |
| if (column >= ctl->num_columns) { |
| more_fseek(ctl, file_position_bak); |
| } else { |
| memmove(mbc, mbc + 1, --mbc_pos); |
| if (mbc_pos > 0) { |
| mbc[mbc_pos] = '\0'; |
| goto process_mbc; |
| } |
| } |
| break; |
| |
| default: |
| wc_width = wcwidth(wc); |
| if (column + wc_width > ctl->num_columns) { |
| more_fseek(ctl, file_position_bak); |
| break_flag = 1; |
| } else { |
| for (i = 0; p < &ctl->line_buf[ctl->line_sz - 1] && |
| i < mbc_pos; i++) |
| *p++ = mbc[i]; |
| if (wc_width > 0) |
| column += wc_width; |
| } |
| } |
| |
| if (break_flag || column >= ctl->num_columns) |
| break; |
| |
| c = more_getc(ctl); |
| continue; |
| } |
| #endif /* HAVE_WIDECHAR */ |
| if (c == EOF) { |
| if (p > ctl->line_buf) { |
| *p = '\0'; |
| *length = p - ctl->line_buf; |
| return column; |
| } |
| *length = p - ctl->line_buf; |
| return EOF; |
| } |
| if (c == '\n') { |
| ctl->current_line++; |
| break; |
| } |
| |
| *p++ = c; |
| if (c == '\t') { |
| if (!ctl->hard_tabs || (column < ctl->prompt_len && !ctl->hard_tty)) { |
| if (ctl->hard_tabs && ctl->erase_line && !ctl->dumb_tty) { |
| column = 1 + (column | 7); |
| putp(ctl->erase_line); |
| ctl->prompt_len = 0; |
| } else { |
| for (--p; p < &ctl->line_buf[ctl->line_sz - 1];) { |
| *p++ = ' '; |
| if ((++column & 7) == 0) |
| break; |
| } |
| if (column >= ctl->prompt_len) |
| ctl->prompt_len = 0; |
| } |
| } else |
| column = 1 + (column | 7); |
| } else if (c == '\b' && column > 0) { |
| column--; |
| } else if (c == '\r') { |
| int next = more_getc(ctl); |
| if (next == '\n') { |
| p--; |
| ctl->current_line++; |
| break; |
| } |
| more_ungetc(ctl, c); |
| column = 0; |
| } else if (c == '\f' && ctl->stop_after_formfeed) { |
| p[-1] = '^'; |
| *p++ = 'L'; |
| column += 2; |
| ctl->is_paused = 1; |
| } else if (c == EOF) { |
| *length = p - ctl->line_buf; |
| return column; |
| } else { |
| #ifdef HAVE_WIDECHAR |
| if (ctl->fold_long_lines && MB_CUR_MAX > 1) { |
| memset(mbc, '\0', MB_LEN_MAX); |
| mbc_pos = 0; |
| mbc[mbc_pos++] = c; |
| state_bak = state; |
| |
| mblength = mbrtowc(&wc, mbc, mbc_pos, &state); |
| /* The value of mblength is always less than 2 here. */ |
| switch (mblength) { |
| case (size_t)-2: |
| p--; |
| file_position_bak = ctl->file_position - 1; |
| state = state_bak; |
| use_mbc_buffer_flag = 1; |
| break; |
| |
| case (size_t)-1: |
| state = state_bak; |
| column++; |
| break; |
| |
| default: |
| wc_width = wcwidth(wc); |
| if (wc_width > 0) |
| column += wc_width; |
| } |
| } else |
| #endif /* HAVE_WIDECHAR */ |
| { |
| if (isprint(c)) |
| column++; |
| } |
| } |
| |
| if (column >= ctl->num_columns && ctl->fold_long_lines) |
| break; |
| #ifdef HAVE_WIDECHAR |
| if (use_mbc_buffer_flag == 0 && p >= &ctl->line_buf[ctl->line_sz - 1 - 4]) |
| /* don't read another char if there is no space for |
| * whole multibyte sequence */ |
| break; |
| #endif |
| c = more_getc(ctl); |
| } |
| if (column >= ctl->num_columns && ctl->num_columns > 0) { |
| if (!ctl->wrap_margin) { |
| *p++ = '\n'; |
| } |
| } |
| column_wrap = column == ctl->num_columns && ctl->fold_long_lines; |
| if (column_wrap && ctl->eat_newline && ctl->wrap_margin) { |
| *p++ = '\n'; /* simulate normal wrap */ |
| } |
| *length = p - ctl->line_buf; |
| *p = 0; |
| return column; |
| } |
| |
| /* Erase the rest of the prompt, assuming we are starting at column col. */ |
| static void erase_to_col(struct more_control *ctl, int col) |
| { |
| |
| if (ctl->prompt_len == 0) |
| return; |
| if (col == 0 && ctl->clear_line_ends) |
| puts(ctl->erase_line); |
| else if (ctl->hard_tty) |
| putchar('\n'); |
| else { |
| if (col == 0) |
| putchar('\r'); |
| if (!ctl->dumb_tty && ctl->erase_line) |
| putp(ctl->erase_line); |
| else { |
| printf("%*s", ctl->prompt_len - col, ""); |
| if (col == 0) |
| putchar('\r'); |
| } |
| } |
| ctl->prompt_len = col; |
| } |
| |
| static void output_prompt(struct more_control *ctl, char *filename) |
| { |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| else if (ctl->prompt_len > 0) |
| erase_to_col(ctl, 0); |
| if (!ctl->hard_tty) { |
| ctl->prompt_len = 0; |
| if (ctl->enter_std) { |
| putp(ctl->enter_std); |
| ctl->prompt_len += (2 * ctl->stdout_glitch); |
| } |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| ctl->prompt_len += printf(_("--More--")); |
| if (filename != NULL) { |
| ctl->prompt_len += printf(_("(Next file: %s)"), filename); |
| } else if (!ctl->no_tty_in) { |
| ctl->prompt_len += |
| printf("(%d%%)", |
| (int)((ctl->file_position * 100) / ctl->file_size)); |
| } |
| if (ctl->suppress_bell) { |
| ctl->prompt_len += |
| printf(_("[Press space to continue, 'q' to quit.]")); |
| } |
| if (ctl->exit_std) |
| putp(ctl->exit_std); |
| if (ctl->clear_line_ends) |
| putp(ctl->clear_rest); |
| } else |
| fprintf(stderr, "\a"); |
| fflush(NULL); |
| } |
| |
| static void reset_tty(struct more_control *ctl) |
| { |
| if (ctl->no_tty_out) |
| return; |
| fflush(NULL); |
| ctl->output_tty.c_lflag |= ICANON | ECHO; |
| ctl->output_tty.c_cc[VMIN] = ctl->original_tty.c_cc[VMIN]; |
| ctl->output_tty.c_cc[VTIME] = ctl->original_tty.c_cc[VTIME]; |
| tcsetattr(STDERR_FILENO, TCSANOW, &ctl->original_tty); |
| } |
| |
| /* Clean up terminal state and exit. Also come here if interrupt signal received */ |
| static void __attribute__((__noreturn__)) more_exit(struct more_control *ctl) |
| { |
| #ifdef HAVE_MAGIC |
| magic_close(ctl->magic); |
| #endif |
| reset_tty(ctl); |
| if (ctl->clear_line_ends) { |
| putchar('\r'); |
| putp(ctl->erase_line); |
| } else if (!ctl->clear_line_ends && (ctl->prompt_len > 0)) |
| erase_to_col(ctl, 0); |
| fflush(NULL); |
| free(ctl->previous_search); |
| free(ctl->shell_line); |
| free(ctl->line_buf); |
| free(ctl->go_home); |
| if (ctl->current_file) |
| fclose(ctl->current_file); |
| del_curterm(cur_term); |
| _exit(EXIT_SUCCESS); |
| } |
| |
| static cc_t read_user_input(struct more_control *ctl) |
| { |
| cc_t c; |
| |
| errno = 0; |
| /* |
| * Key commands can be read() from either stderr or stdin. If they |
| * are read from stdin such as 'cat file.txt | more' then the pipe |
| * input is understood as series key commands - and that is not |
| * wanted. Keep the read() reading from stderr. |
| */ |
| if (read(STDERR_FILENO, &c, 1) <= 0) { |
| if (errno != EINTR) |
| more_exit(ctl); |
| else |
| c = ctl->output_tty.c_cc[VKILL]; |
| } |
| return c; |
| } |
| |
| /* Read a number and command from the terminal. Set cmd to the non-digit |
| * which terminates the number. */ |
| static struct number_command read_command(struct more_control *ctl) |
| { |
| cc_t input[8] = { 0 }; |
| ssize_t i, ilen; |
| struct number_command cmd = { .key = more_kc_unknown_command }; |
| |
| /* See stderr note in read_user_input() */ |
| if ((ilen = read(STDERR_FILENO, &input, sizeof(input))) <= 0) |
| return cmd; |
| if (2 < ilen) { |
| if (!memcmp(input, ARROW_UP, sizeof(ARROW_UP))) { |
| cmd.key = more_kc_backwards; |
| return cmd; |
| } else if (!memcmp(input, ARROW_DOWN, sizeof(ARROW_DOWN))) { |
| cmd.key = more_kc_jump_lines_per_screen; |
| return cmd; |
| } else if (!memcmp(input, PAGE_UP, sizeof(PAGE_UP))) { |
| cmd.key = more_kc_backwards; |
| return cmd; |
| } else if (!memcmp(input, PAGE_DOWN, sizeof(PAGE_DOWN))) { |
| cmd.key = more_kc_jump_lines_per_screen; |
| return cmd; |
| } |
| } |
| for (i = 0; i < ilen; i++) { |
| if (isdigit(input[i])) { |
| if (0 < ctl->reading_num) { |
| ctl->leading_number *= 10; |
| ctl->leading_number += input[i] - '0'; |
| } else |
| ctl->leading_number = input[i] - '0'; |
| ctl->reading_num = 1; |
| continue; |
| } |
| cmd.number = ctl->leading_number; |
| ctl->reading_num = 0; |
| ctl->leading_number = 0; |
| if (ctl->leading_colon) { |
| ctl->leading_colon = 0; |
| switch (input[i]) { |
| case 'f': |
| cmd.key = more_kc_display_file_and_line; |
| return cmd; |
| case 'n': |
| cmd.key = more_kc_next_file; |
| return cmd; |
| case 'p': |
| cmd.key = more_kc_previous_file; |
| return cmd; |
| default: |
| cmd.key = more_kc_unknown_command; |
| return cmd; |
| } |
| } |
| /* command is a single char */ |
| switch (input[i]) { |
| case '.': |
| cmd.key = more_kc_repeat_previous; |
| break; |
| case ':': |
| ctl->leading_colon = 1; |
| break; |
| case 'b': |
| case CTRL('B'): |
| cmd.key = more_kc_backwards; |
| break; |
| case ' ': |
| cmd.key = more_kc_jump_lines_per_screen; |
| break; |
| case 'z': |
| cmd.key = more_kc_set_lines_per_screen; |
| break; |
| case 'd': |
| case CTRL('D'): |
| cmd.key = more_kc_set_scroll_len; |
| break; |
| case 'q': |
| case 'Q': |
| cmd.key = more_kc_quit; |
| break; |
| case 'f': |
| case CTRL('F'): |
| cmd.key = more_kc_skip_forward_screen; |
| break; |
| case 's': |
| cmd.key = more_kc_skip_forward_line; |
| break; |
| case '\n': |
| cmd.key = more_kc_next_line; |
| break; |
| case '\f': |
| cmd.key = more_kc_clear_screen; |
| break; |
| case '\'': |
| cmd.key = more_kc_previous_search_match; |
| break; |
| case '=': |
| cmd.key = more_kc_display_line; |
| break; |
| case 'n': |
| cmd.key = more_kc_repeat_search; |
| break; |
| case '/': |
| cmd.key = more_kc_search; |
| break; |
| case '!': |
| cmd.key = more_kc_run_shell; |
| break; |
| case '?': |
| case 'h': |
| cmd.key = more_kc_help; |
| break; |
| case 'v': |
| cmd.key = more_kc_run_editor; |
| break; |
| } |
| } |
| return cmd; |
| } |
| |
| /* Change displayed file from command line list to next nskip, where nskip |
| * is relative position in argv and can be negative, that is a previous |
| * file. */ |
| static void change_file(struct more_control *ctl, int nskip) |
| { |
| if (nskip == 0) |
| return; |
| if (nskip > 0) { |
| if (ctl->argv_position + nskip > ctl->num_files - 1) |
| nskip = ctl->num_files - ctl->argv_position - 1; |
| } |
| ctl->argv_position += nskip; |
| if (ctl->argv_position < 0) |
| ctl->argv_position = 0; |
| puts(_("\n...Skipping ")); |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| if (nskip > 0) |
| fputs(_("...Skipping to file "), stdout); |
| else |
| fputs(_("...Skipping back to file "), stdout); |
| puts(ctl->file_names[ctl->argv_position]); |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| putchar('\n'); |
| ctl->argv_position--; |
| } |
| |
| static void show(struct more_control *ctl, char c) |
| { |
| if ((c < ' ' && c != '\n' && c != ESC) || c == CERASE) { |
| c += (c == CERASE) ? -0100 : 0100; |
| fputs(CARAT, stderr); |
| ctl->prompt_len++; |
| } |
| fputc(c, stderr); |
| ctl->prompt_len++; |
| } |
| |
| static void more_error(struct more_control *ctl, char *mess) |
| { |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| else |
| erase_to_col(ctl, 0); |
| ctl->prompt_len += strlen(mess); |
| if (ctl->enter_std) |
| putp(ctl->enter_std); |
| fputs(mess, stdout); |
| if (ctl->exit_std) |
| putp(ctl->exit_std); |
| fflush(NULL); |
| ctl->report_errors++; |
| } |
| |
| static void erase_one_column(struct more_control *ctl) |
| { |
| if (ctl->erase_previous_ok) |
| fprintf(stderr, "%s ", ctl->backspace_ch); |
| fputs(ctl->backspace_ch, stderr); |
| } |
| |
| static void ttyin(struct more_control *ctl, char buf[], int nmax, char pchar) |
| { |
| char *sp; |
| cc_t c; |
| int slash = 0; |
| int maxlen; |
| |
| sp = buf; |
| maxlen = 0; |
| while (sp - buf < nmax) { |
| if (ctl->prompt_len > maxlen) |
| maxlen = ctl->prompt_len; |
| c = read_user_input(ctl); |
| if (c == '\\') { |
| slash++; |
| } else if (c == ctl->output_tty.c_cc[VERASE] && !slash) { |
| if (sp > buf) { |
| #ifdef HAVE_WIDECHAR |
| if (MB_CUR_MAX > 1) { |
| wchar_t wc; |
| size_t pos = 0, mblength; |
| mbstate_t state, state_bak; |
| |
| memset(&state, '\0', sizeof(mbstate_t)); |
| |
| while (1) { |
| state_bak = state; |
| mblength = |
| mbrtowc(&wc, buf + pos, |
| sp - buf, &state); |
| |
| switch (mblength) { |
| case (size_t)-2: |
| case (size_t)-1: |
| state = state_bak; |
| /* fallthrough */ |
| case 0: |
| mblength = 1; |
| } |
| if (buf + pos + mblength >= sp) |
| break; |
| |
| pos += mblength; |
| } |
| |
| if (mblength == 1) { |
| erase_one_column(ctl); |
| } else { |
| int wc_width; |
| wc_width = wcwidth(wc); |
| wc_width = |
| (wc_width < |
| 1) ? 1 : wc_width; |
| while (wc_width--) { |
| erase_one_column(ctl); |
| } |
| } |
| |
| while (mblength--) { |
| --ctl->prompt_len; |
| --sp; |
| } |
| } else |
| #endif /* HAVE_WIDECHAR */ |
| { |
| --ctl->prompt_len; |
| erase_one_column(ctl); |
| --sp; |
| } |
| |
| if ((*sp < ' ' && *sp != '\n') || *sp == CERASE) { |
| --ctl->prompt_len; |
| erase_one_column(ctl); |
| } |
| continue; |
| } |
| if (!ctl->erase_line) |
| ctl->prompt_len = maxlen; |
| } else if (c == ctl->output_tty.c_cc[VKILL] && !slash) { |
| if (ctl->hard_tty) { |
| show(ctl, c); |
| putchar('\n'); |
| putchar(pchar); |
| } else { |
| putchar('\r'); |
| putchar(pchar); |
| if (ctl->erase_line) |
| erase_to_col(ctl, 1); |
| else if (ctl->erase_input_ok) |
| while (ctl->prompt_len-- > 1) |
| fprintf(stderr, "%s %s", ctl->backspace_ch, ctl->backspace_ch); |
| ctl->prompt_len = 1; |
| } |
| sp = buf; |
| fflush(NULL); |
| continue; |
| } |
| if (slash && (c == ctl->output_tty.c_cc[VKILL] || |
| c == ctl->output_tty.c_cc[VERASE])) { |
| erase_one_column(ctl); |
| --sp; |
| } |
| if (c != '\\') |
| slash = 0; |
| *sp++ = c; |
| if ((c < ' ' && c != '\n' && c != ESC) || c == CERASE) { |
| c += (c == CERASE) ? -0100 : 0100; |
| fputs(CARAT, stderr); |
| ctl->prompt_len++; |
| } |
| if (c != '\n' && c != ESC) { |
| fputc(c, stderr); |
| ctl->prompt_len++; |
| } else |
| break; |
| } |
| *--sp = '\0'; |
| if (!ctl->erase_line) |
| ctl->prompt_len = maxlen; |
| if (sp - buf >= nmax - 1) |
| more_error(ctl, _("Line too long")); |
| } |
| |
| /* Expand shell command line. */ |
| static void expand(struct more_control *ctl, char *inbuf) |
| { |
| char *inpstr; |
| char *outstr; |
| char c; |
| char *temp; |
| int tempsz, xtra, offset; |
| |
| xtra = strlen(ctl->file_names[ctl->argv_position]) + strlen(ctl->shell_line) + 1; |
| tempsz = COMMAND_BUF + xtra; |
| temp = xmalloc(tempsz); |
| inpstr = inbuf; |
| outstr = temp; |
| while ((c = *inpstr++) != '\0') { |
| offset = outstr - temp; |
| if (tempsz - offset - 1 < xtra) { |
| tempsz += COMMAND_BUF + xtra; |
| temp = xrealloc(temp, tempsz); |
| outstr = temp + offset; |
| } |
| switch (c) { |
| case '%': |
| if (!ctl->no_tty_in) { |
| strcpy(outstr, ctl->file_names[ctl->argv_position]); |
| outstr += strlen(ctl->file_names[ctl->argv_position]); |
| } else |
| *outstr++ = c; |
| break; |
| case '!': |
| if (ctl->shell_line) { |
| strcpy(outstr, ctl->shell_line); |
| outstr += strlen(ctl->shell_line); |
| } else |
| more_error(ctl, _ |
| ("No previous command to substitute for")); |
| break; |
| case '\\': |
| if (*inpstr == '%' || *inpstr == '!') { |
| *outstr++ = *inpstr++; |
| break; |
| } |
| /* fallthrough */ |
| default: |
| *outstr++ = c; |
| } |
| } |
| *outstr++ = '\0'; |
| free(ctl->shell_line); |
| ctl->shell_line = temp; |
| } |
| |
| static void set_tty(struct more_control *ctl) |
| { |
| ctl->output_tty.c_lflag &= ~(ICANON | ECHO); |
| ctl->output_tty.c_cc[VMIN] = 1; /* read at least 1 char */ |
| ctl->output_tty.c_cc[VTIME] = 0; /* no timeout */ |
| tcsetattr(STDERR_FILENO, TCSANOW, &ctl->output_tty); |
| } |
| |
| /* Come here if a quit signal is received */ |
| static void sigquit_handler(struct more_control *ctl) |
| { |
| if (!ctl->dumb_tty && ctl->no_quit_dialog) { |
| ctl->prompt_len += fprintf(stderr, _("[Use q or Q to quit]")); |
| ctl->no_quit_dialog = 0; |
| } else |
| more_exit(ctl); |
| } |
| |
| /* Come here when we get a suspend signal from the terminal */ |
| static void sigtstp_handler(struct more_control *ctl) |
| { |
| reset_tty(ctl); |
| fflush(NULL); |
| kill(getpid(), SIGSTOP); |
| } |
| |
| /* Come here when we get a continue signal from the terminal */ |
| static void sigcont_handler(struct more_control *ctl) |
| { |
| set_tty(ctl); |
| } |
| |
| /* Come here if a signal for a window size change is received */ |
| static void sigwinch_handler(struct more_control *ctl) |
| { |
| struct winsize win; |
| |
| if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &win) != -1) { |
| if (win.ws_row != 0) { |
| ctl->lines_per_page = win.ws_row; |
| ctl->d_scroll_len = ctl->lines_per_page / 2 - 1; |
| if (ctl->d_scroll_len < 1) |
| ctl->d_scroll_len = 1; |
| ctl->lines_per_screen = ctl->lines_per_page - 1; |
| } |
| if (win.ws_col != 0) |
| ctl->num_columns = win.ws_col; |
| } |
| prepare_line_buffer(ctl); |
| } |
| |
| static void execute(struct more_control *ctl, char *filename, char *cmd, ...) |
| { |
| pid_t id; |
| va_list argp; |
| char *arg; |
| char **args; |
| int argcount; |
| |
| fflush(NULL); |
| id = fork(); |
| if (id == 0) { |
| int errsv; |
| if (!isatty(STDIN_FILENO)) { |
| close(STDIN_FILENO); |
| open("/dev/tty", 0); |
| } |
| reset_tty(ctl); |
| |
| va_start(argp, cmd); |
| arg = va_arg(argp, char *); |
| argcount = 0; |
| while (arg) { |
| argcount++; |
| arg = va_arg(argp, char *); |
| } |
| va_end(argp); |
| |
| args = alloca(sizeof(char *) * (argcount + 1)); |
| args[argcount] = NULL; |
| |
| va_start(argp, cmd); |
| arg = va_arg(argp, char *); |
| argcount = 0; |
| while (arg) { |
| args[argcount] = arg; |
| argcount++; |
| arg = va_arg(argp, char *); |
| } |
| va_end(argp); |
| |
| if (geteuid() != getuid() || getegid() != getgid()) { |
| if (setuid(getuid()) < 0) |
| err(EXIT_FAILURE, _("setuid failed")); |
| if (setgid(getgid()) < 0) |
| err(EXIT_FAILURE, _("setgid failed")); |
| } |
| |
| execvp(cmd, args); |
| errsv = errno; |
| fputs(_("exec failed\n"), stderr); |
| exit(errsv == ENOENT ? EX_EXEC_ENOENT : EX_EXEC_FAILED); |
| } |
| if (id > 0) { |
| errno = 0; |
| while (wait(NULL) > 0) { |
| if (errno == EINTR) |
| continue; |
| } |
| } else |
| fputs(_("can't fork\n"), stderr); |
| set_tty(ctl); |
| print_separator('-', 24); |
| output_prompt(ctl, filename); |
| } |
| |
| static void run_shell(struct more_control *ctl, char *filename) |
| { |
| char cmdbuf[COMMAND_BUF]; |
| |
| erase_to_col(ctl, 0); |
| putchar('!'); |
| fflush(NULL); |
| if (ctl->previous_command.key == more_kc_run_shell && ctl->shell_line) |
| fputs(ctl->shell_line, stdout); |
| else { |
| ttyin(ctl, cmdbuf, sizeof(cmdbuf) - 2, '!'); |
| if (strpbrk(cmdbuf, "%!\\")) |
| expand(ctl, cmdbuf); |
| else { |
| free(ctl->shell_line); |
| ctl->shell_line = xstrdup(cmdbuf); |
| } |
| } |
| fputc('\n', stderr); |
| fflush(NULL); |
| ctl->prompt_len = 0; |
| execute(ctl, filename, ctl->shell, ctl->shell, "-c", ctl->shell_line, 0); |
| } |
| |
| /* Skip n lines in the file f */ |
| static void skip_lines(struct more_control *ctl) |
| { |
| int c; |
| |
| while (ctl->next_jump > 0) { |
| while ((c = more_getc(ctl)) != '\n') |
| if (c == EOF) |
| return; |
| ctl->next_jump--; |
| ctl->current_line++; |
| } |
| } |
| |
| /* Clear the screen */ |
| static void more_clear_screen(struct more_control *ctl) |
| { |
| if (ctl->clear && !ctl->hard_tty) { |
| putp(ctl->clear); |
| /* Put out carriage return so that system doesn't get |
| * confused by escape sequences when expanding tabs */ |
| putchar('\r'); |
| ctl->prompt_len = 0; |
| } |
| } |
| |
| static void read_line(struct more_control *ctl) |
| { |
| int c; |
| char *p; |
| |
| p = ctl->line_buf; |
| while ((c = more_getc(ctl)) != '\n' && c != EOF |
| && (ptrdiff_t)p != (ptrdiff_t)(ctl->line_buf + ctl->line_sz - 1)) |
| *p++ = c; |
| if (c == '\n') |
| ctl->current_line++; |
| *p = '\0'; |
| } |
| |
| static int more_poll(struct more_control *ctl, int timeout) |
| { |
| struct pollfd pfd[2]; |
| |
| pfd[0].fd = ctl->sigfd; |
| pfd[0].events = POLLIN | POLLERR | POLLHUP; |
| pfd[1].fd = STDIN_FILENO; |
| pfd[1].events = POLLIN; |
| |
| if (poll(pfd, 2, timeout) < 0) { |
| if (errno == EAGAIN) |
| return 1; |
| more_error(ctl, _("poll failed")); |
| return 1; |
| } |
| if (pfd[0].revents != 0) { |
| struct signalfd_siginfo info; |
| ssize_t sz; |
| |
| sz = read(pfd[0].fd, &info, sizeof(info)); |
| assert(sz == sizeof(info)); |
| switch (info.ssi_signo) { |
| case SIGINT: |
| more_exit(ctl); |
| break; |
| case SIGQUIT: |
| sigquit_handler(ctl); |
| break; |
| case SIGTSTP: |
| sigtstp_handler(ctl); |
| break; |
| case SIGCONT: |
| sigcont_handler(ctl); |
| break; |
| case SIGWINCH: |
| sigwinch_handler(ctl); |
| break; |
| default: |
| abort(); |
| } |
| } |
| if (pfd[1].revents == 0) |
| return 1; |
| return 0; |
| } |
| |
| /* Search for nth occurrence of regular expression contained in buf in |
| * the file */ |
| static void search(struct more_control *ctl, char buf[], int n) |
| { |
| off_t startline = ctl->file_position; |
| off_t line1 = startline; |
| off_t line2 = startline; |
| off_t line3; |
| int lncount; |
| int saveln, rc; |
| regex_t re; |
| |
| if (buf != ctl->previous_search) { |
| free(ctl->previous_search); |
| ctl->previous_search = buf; |
| } |
| |
| ctl->search_called = 1; |
| ctl->context.line_num = saveln = ctl->current_line; |
| ctl->context.row_num = startline; |
| lncount = 0; |
| if (!buf) |
| goto notfound; |
| if ((rc = regcomp(&re, buf, REG_NOSUB)) != 0) { |
| char s[REGERR_BUF]; |
| regerror(rc, &re, s, sizeof s); |
| more_error(ctl, s); |
| return; |
| } |
| while (!feof(ctl->current_file)) { |
| line3 = line2; |
| line2 = line1; |
| line1 = ctl->file_position; |
| read_line(ctl); |
| lncount++; |
| if (regexec(&re, ctl->line_buf, 0, NULL, 0) == 0 && --n == 0) { |
| if ((1 < lncount && ctl->no_tty_in) || 3 < lncount) { |
| putchar('\n'); |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| fputs(_("...skipping\n"), stdout); |
| } |
| if (!ctl->no_tty_in) { |
| ctl->current_line -= (lncount < 3 ? lncount : 3); |
| more_fseek(ctl, line3); |
| if (ctl->no_scroll) { |
| if (ctl->clear_line_ends) { |
| putp(ctl->go_home); |
| putp(ctl->erase_line); |
| } else |
| more_clear_screen(ctl); |
| } |
| } else { |
| erase_to_col(ctl, 0); |
| if (ctl->no_scroll) { |
| if (ctl->clear_line_ends) { |
| putp(ctl->go_home); |
| putp(ctl->erase_line); |
| } else |
| more_clear_screen(ctl); |
| } |
| puts(ctl->line_buf); |
| } |
| break; |
| } |
| more_poll(ctl, 1); |
| } |
| /* Move ctrl+c signal handling back to more_key_command(). */ |
| signal(SIGINT, SIG_DFL); |
| sigaddset(&ctl->sigset, SIGINT); |
| sigprocmask(SIG_BLOCK, &ctl->sigset, NULL); |
| regfree(&re); |
| if (feof(ctl->current_file)) { |
| if (!ctl->no_tty_in) { |
| ctl->current_line = saveln; |
| more_fseek(ctl, startline); |
| } else { |
| fputs(_("\nPattern not found\n"), stdout); |
| more_exit(ctl); |
| } |
| notfound: |
| more_error(ctl, _("Pattern not found")); |
| } |
| } |
| |
| static char *find_editor(void) |
| { |
| static char *editor; |
| |
| editor = getenv("VISUAL"); |
| if (editor == NULL || *editor == '\0') |
| editor = getenv("EDITOR"); |
| if (editor == NULL || *editor == '\0') |
| editor = _PATH_VI; |
| return editor; |
| } |
| |
| static void runtime_usage(void) |
| { |
| fputs(_("Most commands optionally preceded by integer argument k. " |
| "Defaults in brackets.\n" |
| "Star (*) indicates argument becomes new default.\n"), stdout); |
| print_separator('-', 79); |
| fprintf(stdout, |
| _ |
| ("<space> Display next k lines of text [current screen size]\n" |
| "z Display next k lines of text [current screen size]*\n" |
| "<return> Display next k lines of text [1]*\n" |
| "d or ctrl-D Scroll k lines [current scroll size, initially 11]*\n" |
| "q or Q or <interrupt> Exit from more\n" |
| "s Skip forward k lines of text [1]\n" |
| "f Skip forward k screenfuls of text [1]\n" |
| "b or ctrl-B Skip backwards k screenfuls of text [1]\n" |
| "' Go to place where previous search started\n" |
| "= Display current line number\n" |
| "/<regular expression> Search for kth occurrence of regular expression [1]\n" |
| "n Search for kth occurrence of last r.e [1]\n" |
| "!<cmd> or :!<cmd> Execute <cmd> in a subshell\n" |
| "v Start up '%s' at current line\n" |
| "ctrl-L Redraw screen\n" |
| ":n Go to kth next file [1]\n" |
| ":p Go to kth previous file [1]\n" |
| ":f Display current file name and line number\n" |
| ". Repeat previous command\n"), |
| find_editor()); |
| print_separator('-', 79); |
| } |
| |
| static void execute_editor(struct more_control *ctl, char *cmdbuf, char *filename) |
| { |
| char *editor, *p; |
| int split = 0; |
| int n; |
| |
| if ((ctl->current_line - ctl->lines_per_screen) < 1) |
| n = 1; |
| else |
| n = ctl->current_line - (ctl->lines_per_screen + 1) / 2; |
| editor = find_editor(); |
| p = strrchr(editor, '/'); |
| if (p) |
| p++; |
| else |
| p = editor; |
| /* |
| * Earlier: call vi +n file. This also works for emacs. |
| * POSIX: call vi -c n file (when editor is vi or ex). |
| */ |
| if (!strcmp(p, "vi") || !strcmp(p, "ex")) { |
| sprintf(cmdbuf, "-c %d", n); |
| split = 1; |
| } else |
| sprintf(cmdbuf, "+%d", n); |
| |
| erase_to_col(ctl, 0); |
| printf("%s %s %s", editor, cmdbuf, ctl->file_names[ctl->argv_position]); |
| if (split) { |
| cmdbuf[2] = 0; |
| execute(ctl, filename, editor, editor, |
| cmdbuf, cmdbuf + 3, |
| ctl->file_names[ctl->argv_position], (char *)0); |
| } else |
| execute(ctl, filename, editor, editor, |
| cmdbuf, ctl->file_names[ctl->argv_position], (char *)0); |
| } |
| |
| static int skip_backwards(struct more_control *ctl, int nlines) |
| { |
| if (nlines == 0) |
| nlines++; |
| erase_to_col(ctl, 0); |
| printf(P_("...back %d page", "...back %d pages", nlines), nlines); |
| putchar('\n'); |
| ctl->next_jump = ctl->current_line - (ctl->lines_per_screen * (nlines + 1)) - 1; |
| if (ctl->next_jump < 0) |
| ctl->next_jump = 0; |
| more_fseek(ctl, 0); |
| ctl->current_line = 0; |
| skip_lines(ctl); |
| return ctl->lines_per_screen; |
| } |
| |
| static int skip_forwards(struct more_control *ctl, int nlines, cc_t comchar) |
| { |
| int c; |
| |
| if (nlines == 0) |
| nlines++; |
| if (comchar == 'f') |
| nlines *= ctl->lines_per_screen; |
| putchar('\r'); |
| erase_to_col(ctl, 0); |
| putchar('\n'); |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| printf(P_("...skipping %d line", |
| "...skipping %d lines", nlines), nlines); |
| |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| putchar('\n'); |
| |
| while (nlines > 0) { |
| while ((c = more_getc(ctl)) != '\n') |
| if (c == EOF) |
| return 0; |
| ctl->current_line++; |
| nlines--; |
| } |
| return 1; |
| } |
| |
| /* Read a command and do it. A command consists of an optional integer |
| * argument followed by the command character. Return the number of |
| * lines to display in the next screenful. If there is nothing more to |
| * display in the current file, zero is returned. */ |
| static int more_key_command(struct more_control *ctl, char *filename) |
| { |
| int retval = 0; |
| int done = 0, search_again = 0; |
| char cmdbuf[INIT_BUF]; |
| struct number_command cmd; |
| |
| if (!ctl->report_errors) |
| output_prompt(ctl, filename); |
| else |
| ctl->report_errors = 0; |
| ctl->search_called = 0; |
| for (;;) { |
| if (more_poll(ctl, -1) != 0) |
| continue; |
| cmd = read_command(ctl); |
| if (cmd.key == more_kc_unknown_command) |
| continue; |
| if (cmd.key == more_kc_repeat_previous) |
| cmd = ctl->previous_command; |
| switch (cmd.key) { |
| case more_kc_backwards: |
| if (ctl->no_tty_in) { |
| fprintf(stderr, "\a"); |
| return -1; |
| } |
| retval = skip_backwards(ctl, cmd.number); |
| done = 1; |
| break; |
| case more_kc_jump_lines_per_screen: |
| case more_kc_set_lines_per_screen: |
| if (cmd.number == 0) |
| cmd.number = ctl->lines_per_screen; |
| else if (cmd.key == more_kc_set_lines_per_screen) |
| ctl->lines_per_screen = cmd.number; |
| retval = cmd.number; |
| done = 1; |
| break; |
| case more_kc_set_scroll_len: |
| if (cmd.number != 0) |
| ctl->d_scroll_len = cmd.number; |
| retval = ctl->d_scroll_len; |
| done = 1; |
| break; |
| case more_kc_quit: |
| more_exit(ctl); |
| case more_kc_skip_forward_screen: |
| if (skip_forwards(ctl, cmd.number, 'f')) |
| retval = ctl->lines_per_screen; |
| done = 1; |
| break; |
| case more_kc_skip_forward_line: |
| if (skip_forwards(ctl, cmd.number, 's')) |
| retval = ctl->lines_per_screen; |
| done = 1; |
| break; |
| case more_kc_next_line: |
| if (cmd.number != 0) |
| ctl->lines_per_screen = cmd.number; |
| else |
| cmd.number = 1; |
| retval = cmd.number; |
| done = 1; |
| break; |
| case more_kc_clear_screen: |
| if (!ctl->no_tty_in) { |
| more_clear_screen(ctl); |
| more_fseek(ctl, ctl->screen_start.row_num); |
| ctl->current_line = ctl->screen_start.line_num; |
| retval = ctl->lines_per_screen; |
| done = 1; |
| break; |
| } else { |
| fprintf(stderr, "\a"); |
| break; |
| } |
| case more_kc_previous_search_match: |
| if (!ctl->no_tty_in) { |
| erase_to_col(ctl, 0); |
| fputs(_("\n***Back***\n\n"), stdout); |
| more_fseek(ctl, ctl->context.row_num); |
| ctl->current_line = ctl->context.line_num; |
| retval = ctl->lines_per_screen; |
| done = 1; |
| break; |
| } else { |
| fprintf(stderr, "\a"); |
| break; |
| } |
| case more_kc_display_line: |
| erase_to_col(ctl, 0); |
| ctl->prompt_len = printf("%d", ctl->current_line); |
| fflush(NULL); |
| break; |
| case more_kc_display_file_and_line: |
| erase_to_col(ctl, 0); |
| if (!ctl->no_tty_in) |
| ctl->prompt_len = |
| printf(_("\"%s\" line %d"), |
| ctl->file_names[ctl->argv_position], ctl->current_line); |
| else |
| ctl->prompt_len = printf(_("[Not a file] line %d"), |
| ctl->current_line); |
| fflush(NULL); |
| break; |
| case more_kc_repeat_search: |
| if (!ctl->previous_search) { |
| more_error(ctl, _("No previous regular expression")); |
| break; |
| } |
| search_again = 1; |
| /* fallthrough */ |
| case more_kc_search: |
| if (cmd.number == 0) |
| cmd.number++; |
| erase_to_col(ctl, 0); |
| putchar('/'); |
| ctl->prompt_len = 1; |
| fflush(NULL); |
| if (search_again) { |
| fputc('\r', stderr); |
| search(ctl, ctl->previous_search, cmd.number); |
| search_again = 0; |
| } else { |
| ttyin(ctl, cmdbuf, sizeof(cmdbuf) - 2, '/'); |
| fputc('\r', stderr); |
| ctl->next_search = xstrdup(cmdbuf); |
| search(ctl, ctl->next_search, cmd.number); |
| } |
| retval = ctl->lines_per_screen - 1; |
| done = 1; |
| break; |
| case more_kc_run_shell: |
| run_shell(ctl, filename); |
| break; |
| case more_kc_help: |
| if (ctl->no_scroll) |
| more_clear_screen(ctl); |
| erase_to_col(ctl, 0); |
| runtime_usage(); |
| output_prompt(ctl, filename); |
| break; |
| case more_kc_next_file: |
| putchar('\r'); |
| erase_to_col(ctl, 0); |
| if (cmd.number == 0) |
| cmd.number = 1; |
| if (ctl->argv_position + cmd.number >= (unsigned int)ctl->num_files) |
| more_exit(ctl); |
| change_file(ctl, cmd.number); |
| done = 1; |
| break; |
| case more_kc_previous_file: |
| if (ctl->no_tty_in) { |
| fprintf(stderr, "\a"); |
| break; |
| } |
| putchar('\r'); |
| erase_to_col(ctl, 0); |
| if (cmd.number == 0) |
| cmd.number = 1; |
| change_file(ctl, -cmd.number); |
| done = 1; |
| break; |
| case more_kc_run_editor: /* This case should go right before default */ |
| if (!ctl->no_tty_in) { |
| execute_editor(ctl, cmdbuf, filename); |
| break; |
| } |
| /* fallthrough */ |
| default: |
| if (ctl->suppress_bell) { |
| erase_to_col(ctl, 0); |
| if (ctl->enter_std) |
| putp(ctl->enter_std); |
| ctl->prompt_len = |
| printf(_("[Press 'h' for instructions.]")) |
| + 2 * ctl->stdout_glitch; |
| if (ctl->exit_std) |
| putp(ctl->exit_std); |
| } else |
| fprintf(stderr, "\a"); |
| fflush(NULL); |
| break; |
| } |
| ctl->previous_command = cmd; |
| if (done) { |
| cmd.key = more_kc_unknown_command; |
| break; |
| } |
| } |
| putchar('\r'); |
| ctl->no_quit_dialog = 1; |
| return retval; |
| } |
| |
| /* Print out the contents of the file f, one screenful at a time. */ |
| static void screen(struct more_control *ctl, int num_lines) |
| { |
| int c; |
| int nchars; |
| int length; /* length of current line */ |
| static int prev_len = 1; /* length of previous line */ |
| |
| for (;;) { |
| while (num_lines > 0 && !ctl->is_paused) { |
| if ((nchars = get_line(ctl, &length)) == EOF) { |
| if (ctl->clear_line_ends) |
| putp(ctl->clear_rest); |
| return; |
| } |
| if (ctl->squeeze_spaces && length == 0 && prev_len == 0) |
| continue; |
| prev_len = length; |
| if (ctl->bad_stdout |
| || ((ctl->enter_std && *ctl->enter_std == ' ') && (ctl->prompt_len > 0))) |
| erase_to_col(ctl, 0); |
| /* must clear before drawing line since tabs on |
| * some terminals do not erase what they tab |
| * over. */ |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| fwrite(ctl->line_buf, length, 1, stdout); |
| if (nchars < ctl->prompt_len) |
| erase_to_col(ctl, nchars); |
| ctl->prompt_len = 0; |
| if (nchars < ctl->num_columns || !ctl->fold_long_lines) |
| putchar('\n'); |
| num_lines--; |
| } |
| fflush(NULL); |
| if ((c = more_getc(ctl)) == EOF) { |
| if (ctl->clear_line_ends) |
| putp(ctl->clear_rest); |
| return; |
| } |
| |
| if (ctl->is_paused && ctl->clear_line_ends) |
| putp(ctl->clear_rest); |
| more_ungetc(ctl, c); |
| ctl->is_paused = 0; |
| do { |
| if ((num_lines = more_key_command(ctl, NULL)) == 0) |
| return; |
| } while (ctl->search_called && !ctl->previous_search); |
| if (ctl->hard_tty && ctl->prompt_len > 0) |
| erase_to_col(ctl, 0); |
| if (ctl->no_scroll && num_lines >= ctl->lines_per_screen) { |
| if (ctl->clear_line_ends) |
| putp(ctl->go_home); |
| else |
| more_clear_screen(ctl); |
| } |
| ctl->screen_start.line_num = ctl->current_line; |
| ctl->screen_start.row_num = ctl->file_position; |
| } |
| } |
| |
| static void copy_file(FILE *f) |
| { |
| char buf[BUFSIZ]; |
| size_t sz; |
| |
| while ((sz = fread(&buf, sizeof(char), sizeof(buf), f)) > 0) |
| fwrite(&buf, sizeof(char), sz, stdout); |
| } |
| |
| |
| static void display_file(struct more_control *ctl, int left) |
| { |
| if (!ctl->current_file) |
| return; |
| ctl->context.line_num = ctl->context.row_num = 0; |
| ctl->current_line = 0; |
| if (ctl->first_file) { |
| ctl->first_file = 0; |
| if (ctl->next_jump) |
| skip_lines(ctl); |
| if (ctl->search_at_start) { |
| search(ctl, ctl->next_search, 1); |
| if (ctl->no_scroll) |
| left--; |
| } |
| } else if (ctl->argv_position < ctl->num_files && !ctl->no_tty_out) |
| left = |
| more_key_command(ctl, ctl->file_names[ctl->argv_position]); |
| if (left != 0) { |
| if ((ctl->no_scroll || ctl->clear_first) |
| && ctl->file_size != ~((off_t)0)) { |
| if (ctl->clear_line_ends) |
| putp(ctl->go_home); |
| else |
| more_clear_screen(ctl); |
| } |
| if (ctl->print_banner) { |
| if (ctl->bad_stdout) |
| erase_to_col(ctl, 0); |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| if (ctl->prompt_len > 14) |
| erase_to_col(ctl, 14); |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| print_separator(':', 14); |
| puts(ctl->file_names[ctl->argv_position]); |
| if (ctl->clear_line_ends) |
| putp(ctl->erase_line); |
| print_separator(':', 14); |
| if (left > ctl->lines_per_page - 4) |
| left = ctl->lines_per_page - 4; |
| } |
| if (ctl->no_tty_out) |
| copy_file(ctl->current_file); |
| else |
| screen(ctl, left); |
| } |
| fflush(NULL); |
| fclose(ctl->current_file); |
| ctl->current_file = NULL; |
| ctl->screen_start.line_num = ctl->screen_start.row_num = 0; |
| ctl->context.line_num = ctl->context.row_num = 0L; |
| } |
| |
| static void initterm(struct more_control *ctl) |
| { |
| int ret; |
| char *term; |
| struct winsize win; |
| char *cursor_addr; |
| |
| #ifndef NON_INTERACTIVE_MORE |
| ctl->no_tty_out = tcgetattr(STDOUT_FILENO, &ctl->output_tty); |
| #endif |
| ctl->no_tty_in = tcgetattr(STDIN_FILENO, &ctl->output_tty); |
| tcgetattr(STDERR_FILENO, &ctl->output_tty); |
| ctl->original_tty = ctl->output_tty; |
| ctl->hard_tabs = (ctl->output_tty.c_oflag & TABDLY) != TAB3; |
| if (ctl->no_tty_out) |
| return; |
| |
| ctl->output_tty.c_lflag &= ~(ICANON | ECHO); |
| ctl->output_tty.c_cc[VMIN] = 1; |
| ctl->output_tty.c_cc[VTIME] = 0; |
| ctl->erase_previous_ok = (ctl->output_tty.c_cc[VERASE] != 255); |
| ctl->erase_input_ok = (ctl->output_tty.c_cc[VKILL] != 255); |
| if ((term = getenv("TERM")) == NULL) { |
| ctl->dumb_tty = 1; |
| } |
| setupterm(term, 1, &ret); |
| if (ret <= 0) { |
| ctl->dumb_tty = 1; |
| return; |
| } |
| if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &win) < 0) { |
| ctl->lines_per_page = tigetnum(TERM_LINES); |
| ctl->num_columns = tigetnum(TERM_COLS); |
| } else { |
| if ((ctl->lines_per_page = win.ws_row) == 0) |
| ctl->lines_per_page = tigetnum(TERM_LINES); |
| if ((ctl->num_columns = win.ws_col) == 0) |
| ctl->num_columns = tigetnum(TERM_COLS); |
| } |
| if ((ctl->lines_per_page <= 0) || tigetflag(TERM_HARD_COPY)) { |
| ctl->hard_tty = 1; |
| ctl->lines_per_page = LINES_PER_PAGE; |
| } |
| |
| if (tigetflag(TERM_EAT_NEW_LINE)) |
| /* Eat newline at last column + 1; dec, concept */ |
| ctl->eat_newline++; |
| if (ctl->num_columns <= 0) |
| ctl->num_columns = NUM_COLUMNS; |
| |
| ctl->wrap_margin = tigetflag(TERM_AUTO_RIGHT_MARGIN); |
| ctl->bad_stdout = tigetflag(TERM_CEOL); |
| ctl->erase_line = tigetstr(TERM_CLEAR_TO_LINE_END); |
| ctl->clear = tigetstr(TERM_CLEAR); |
| if ((ctl->enter_std = tigetstr(TERM_STANDARD_MODE)) != NULL) { |
| ctl->exit_std = tigetstr(TERM_EXIT_STANDARD_MODE); |
| if (0 < tigetnum(TERM_STD_MODE_GLITCH)) |
| ctl->stdout_glitch = 1; |
| } |
| |
| cursor_addr = tigetstr(TERM_HOME); |
| if (cursor_addr == NULL || *cursor_addr == '\0') { |
| cursor_addr = tigetstr(TERM_CURSOR_ADDRESS); |
| if (cursor_addr) |
| cursor_addr = tparm(cursor_addr, 0, 0); |
| } |
| if (cursor_addr) |
| ctl->go_home = xstrdup(cursor_addr); |
| |
| if ((ctl->move_line_down = tigetstr(TERM_LINE_DOWN)) == NULL) |
| ctl->move_line_down = BACKSPACE; |
| ctl->clear_rest = tigetstr(TERM_CLEAR_TO_SCREEN_END); |
| if ((ctl->backspace_ch = tigetstr(TERM_BACKSPACE)) == NULL) |
| ctl->backspace_ch = BACKSPACE; |
| |
| if ((ctl->shell = getenv("SHELL")) == NULL) |
| ctl->shell = _PATH_BSHELL; |
| } |
| |
| int main(int argc, char **argv) |
| { |
| char *s; |
| int left; |
| struct more_control ctl = { |
| .first_file = 1, |
| .fold_long_lines = 1, |
| .no_quit_dialog = 1, |
| .stop_after_formfeed = 1, |
| .wrap_margin = 1, |
| .lines_per_page = LINES_PER_PAGE, |
| .num_columns = NUM_COLUMNS, |
| .d_scroll_len = SCROLL_LEN, |
| 0 |
| }; |
| |
| setlocale(LC_ALL, ""); |
| bindtextdomain(PACKAGE, LOCALEDIR); |
| textdomain(PACKAGE); |
| close_stdout_atexit(); |
| setlocale(LC_ALL, ""); |
| |
| /* Auto set no scroll on when binary is called page */ |
| if (!(strcmp(program_invocation_short_name, "page"))) |
| ctl.no_scroll++; |
| |
| if ((s = getenv("MORE")) != NULL) |
| env_argscan(&ctl, s); |
| argscan(&ctl, argc, argv); |
| |
| initterm(&ctl); |
| |
| #ifdef HAVE_MAGIC |
| ctl.magic = magic_open(MAGIC_MIME_ENCODING | MAGIC_SYMLINK); |
| magic_load(ctl.magic, NULL); |
| #endif |
| prepare_line_buffer(&ctl); |
| |
| ctl.d_scroll_len = ctl.lines_per_page / 2 - 1; |
| if (ctl.d_scroll_len <= 0) |
| ctl.d_scroll_len = 1; |
| |
| /* allow clear_line_ends only if go_home and erase_line and clear_rest strings are |
| * defined, and in that case, make sure we are in no_scroll mode */ |
| if (ctl.clear_line_ends) { |
| if ((ctl.go_home == NULL) || (*ctl.go_home == '\0') || |
| (ctl.erase_line == NULL) || (*ctl.erase_line == '\0') || |
| (ctl.clear_rest == NULL) || (*ctl.clear_rest == '\0')) |
| ctl.clear_line_ends = 0; |
| else |
| ctl.no_scroll = 1; |
| } |
| if (ctl.lines_per_screen == 0) |
| ctl.lines_per_screen = ctl.lines_per_page - 1; |
| left = ctl.lines_per_screen; |
| if (ctl.num_files > 1) |
| ctl.print_banner = 1; |
| if (!ctl.no_tty_in && ctl.num_files == 0) { |
| warnx(_("bad usage")); |
| errtryhelp(EXIT_FAILURE); |
| } else |
| ctl.current_file = stdin; |
| if (!ctl.no_tty_out) { |
| if (signal(SIGTSTP, SIG_IGN) == SIG_DFL) { |
| ctl.catch_suspend++; |
| } |
| tcsetattr(STDERR_FILENO, TCSANOW, &ctl.output_tty); |
| } |
| sigemptyset(&ctl.sigset); |
| sigaddset(&ctl.sigset, SIGINT); |
| sigaddset(&ctl.sigset, SIGQUIT); |
| sigaddset(&ctl.sigset, SIGTSTP); |
| sigaddset(&ctl.sigset, SIGCONT); |
| sigaddset(&ctl.sigset, SIGWINCH); |
| sigprocmask(SIG_BLOCK, &ctl.sigset, NULL); |
| ctl.sigfd = signalfd(-1, &ctl.sigset, SFD_CLOEXEC); |
| if (ctl.no_tty_in) { |
| if (ctl.no_tty_out) |
| copy_file(stdin); |
| else { |
| ctl.current_file = stdin; |
| display_file(&ctl, left); |
| } |
| ctl.no_tty_in = 0; |
| ctl.print_banner = 1; |
| ctl.first_file = 0; |
| } |
| |
| while (ctl.argv_position < ctl.num_files) { |
| checkf(&ctl, ctl.file_names[ctl.argv_position]); |
| display_file(&ctl, left); |
| ctl.first_file = 0; |
| ctl.argv_position++; |
| } |
| ctl.clear_line_ends = 0; |
| ctl.prompt_len = 0; |
| more_exit(&ctl); |
| } |