1 /* vi: set sw=4 ts=4: */
3 * Mini less implementation for busybox
5 * Copyright (C) 2005 by Rob Sullivan <cogito.ergo.cogito@gmail.com>
7 * Licensed under the GPL v2 or later, see the file LICENSE in this tarball.
12 * - Add more regular expression support - search modifiers, certain matches, etc.
13 * - Add more complex bracket searching - currently, nested brackets are
15 * - Add support for "F" as an input. This causes less to act in
16 * a similar way to tail -f.
17 * - Allow horizontal scrolling.
20 * - the inp file pointer is used so that keyboard input works after
21 * redirected input has been read from stdin
25 #if ENABLE_FEATURE_LESS_REGEXP
30 /* These are the escape sequences corresponding to special keys */
31 #define REAL_KEY_UP 'A'
32 #define REAL_KEY_DOWN 'B'
33 #define REAL_KEY_RIGHT 'C'
34 #define REAL_KEY_LEFT 'D'
35 #define REAL_PAGE_UP '5'
36 #define REAL_PAGE_DOWN '6'
37 #define REAL_KEY_HOME '7'
38 #define REAL_KEY_END '8'
40 /* These are the special codes assigned by this program to the special keys */
50 /* The escape codes for highlighted and normal text */
51 #define HIGHLIGHT "\033[7m"
52 #define NORMAL "\033[0m"
54 /* The escape code to clear the screen */
55 #define CLEAR "\033[H\033[J"
57 #define MAXLINES CONFIG_FEATURE_LESS_MAXLINES
62 static char *filename;
63 static const char **buffer;
65 static int current_file = 1;
67 static int num_flines;
68 static int num_files = 1;
69 static const char *empty_line_marker = "~";
71 /* Command line options */
76 #define FLAG_TILDE (1<<4)
77 /* hijack command line options variable for internal state vars */
78 #define LESS_STATE_MATCH_BACKWARDS (1<<6)
80 #if ENABLE_FEATURE_LESS_MARKS
81 static int mark_lines[15][2];
85 #if ENABLE_FEATURE_LESS_REGEXP
86 static int *match_lines;
88 static int num_matches;
89 static regex_t pattern;
90 static int pattern_valid;
93 /* Needed termios structures */
94 static struct termios term_orig, term_vi;
96 /* File pointer to get input from */
99 /* Reset terminal input to normal */
100 static void set_tty_cooked(void)
103 tcsetattr(fileno(inp), TCSANOW, &term_orig);
106 /* Exit the program gracefully */
107 static void tless_exit(int code)
109 /* TODO: We really should save the terminal state when we start,
110 and restore it when we exit. Less does this with the
111 "ti" and "te" termcap commands; can this be done with
115 fflush_stdout_and_exit(code);
118 /* Grab a character from input without requiring the return key. If the
119 character is ASCII \033, get more characters and assign certain sequences
120 special return codes. Note that this function works best with raw input. */
121 static int tless_getch(void)
124 /* Set terminal input to raw mode (taken from vi.c) */
125 tcsetattr(fileno(inp), TCSANOW, &term_vi);
128 /* Detect escape sequences (i.e. arrow keys) and handle
131 if (input == '\033' && getc(inp) == '[') {
136 i = input - REAL_KEY_UP;
139 else if ((i = input - REAL_PAGE_UP) < 4)
144 /* The input is a normal ASCII value */
149 /* Move the cursor to a position (x,y), where (0,0) is the
150 top-left corner of the console */
151 static void move_cursor(int x, int y)
153 printf("\033[%i;%iH", x, y);
156 static void clear_line(void)
158 move_cursor(height, 0);
162 static void data_readlines(void)
167 /* "remained unused space" in a line (0 if line fills entire width) */
168 int rem, last_rem = 1; /* "not 0" */
169 char *current_line, *p;
172 fp = filename ? xfopen(filename, "r") : stdin;
174 if (option_mask32 & FLAG_N) {
177 for (i = 0; !feof(fp) && i <= MAXLINES; i++) {
178 flines = xrealloc(flines, (i+1) * sizeof(char *));
179 current_line = xmalloc(w);
186 if (c == '\n') break;
187 /* NUL is substituted by '\n'! */
188 if (c == '\0') c = '\n';
193 die_if_ferror(fp, filename);
195 /* Corner case: linewrap with only "" wrapping to next line */
196 /* Looks ugly on screen, so we do not store this empty line */
197 if (!last_rem && !current_line[0]) {
198 last_rem = 1; /* "not 0" */
203 if (option_mask32 & FLAG_N) {
204 flines[i] = xasprintf((n <= 99999) ? "%5u %s" : "%05u %s",
205 n % 100000, current_line);
210 flines[i] = xrealloc(current_line, strlen(current_line)+1);
213 num_flines = i - 1; /* buggie: 'num_flines' must be 'max_fline' */
215 /* Reset variables for a new file */
222 #if ENABLE_FEATURE_LESS_FLAGS
224 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
226 static int calc_percent(void)
228 return ((100 * (line_pos + height - 2) / num_flines) + 1);
231 /* Print a status line if -M was specified */
232 static void m_status_print(void)
238 printf("%s%s %s%i%s%i%s%i-%i/%i ", HIGHLIGHT,
239 filename, "(file ", current_file, " of ", num_files, ") lines ",
240 line_pos + 1, line_pos + height - 1, num_flines + 1);
242 printf("%s%s lines %i-%i/%i ", HIGHLIGHT,
243 filename, line_pos + 1, line_pos + height - 1,
247 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename,
248 line_pos + 1, line_pos + height - 1, num_flines + 1);
251 if (line_pos >= num_flines - height + 2) {
252 printf("(END) %s", NORMAL);
253 if ((num_files > 1) && (current_file != num_files))
254 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
256 percentage = calc_percent();
257 printf("%i%% %s", percentage, NORMAL);
261 /* Print a status line if -m was specified */
262 static void medium_status_print(void)
265 percentage = calc_percent();
268 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
269 else if (line_pos == num_flines - height + 2)
270 printf("%s(END)%s", HIGHLIGHT, NORMAL);
272 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
276 /* Print the status line */
277 static void status_print(void)
279 /* Change the status if flags have been set */
280 #if ENABLE_FEATURE_LESS_FLAGS
281 if (option_mask32 & FLAG_M)
283 else if (option_mask32 & FLAG_m)
284 medium_status_print();
289 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
291 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ",
292 current_file, " of ", num_files, ")", NORMAL);
293 } else if (line_pos == num_flines - height + 2) {
294 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
295 if ((num_files > 1) && (current_file != num_files))
296 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
300 #if ENABLE_FEATURE_LESS_FLAGS
305 static char controls[] =
306 /**/"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
307 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
308 "\x7f\x9b"; /* DEL and infamous Meta-ESC :( */
309 static char ctrlconv[] =
310 /* Note that on input NUL is converted to '\n' ('\x0a') */
311 /* Therefore we subst '\n' with '@', not 'J' */
312 "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x40\x4b\x4c\x4d\x4e\x4f"
313 "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f";
315 static void print_found(const char *line)
320 regmatch_t match_structs;
323 const char *str = line;
328 n = strcspn(str, controls);
335 n = strspn(str, controls);
341 if (*str == '\x7f') { *p++ = '?'; str++; }
342 else if (*str == '\x9b') { *p++ = '{'; str++; }
343 else *p++ = ctrlconv[(unsigned char)*str++];
349 /* buf[] holds quarantined version of str */
351 /* Each part of the line that matches has the HIGHLIGHT
352 and NORMAL escape sequences placed around it.
353 NB: we regex against line, but insert text
354 from quarantined copy (buf[]) */
360 while (match_status == 0) {
361 char *new = xasprintf("%s" "%.*s" "%s" "%.*s" "%s",
363 match_structs.rm_so, str,
365 match_structs.rm_eo - match_structs.rm_so,
366 str + match_structs.rm_so,
368 free(growline); growline = new;
369 str += match_structs.rm_eo;
370 line += match_structs.rm_eo;
373 /* Most of the time doesn't find the regex, optimize for that */
374 match_status = regexec(&pattern, line, 1, &match_structs, eflags);
381 printf("%s%s\n", growline, str);
385 static void print_ascii(const char *str)
392 n = strcspn(str, controls);
395 printf("%.*s", n, str);
398 n = strspn(str, controls);
401 if (*str == '\x7f') { *p++ = '?'; str++; }
402 else if (*str == '\x9b') { *p++ = '{'; str++; }
403 else *p++ = ctrlconv[(unsigned char)*str++];
406 printf("%s%s%s", HIGHLIGHT, buf, NORMAL);
411 /* Print the buffer */
412 static void buffer_print(void)
417 for (i = 0; i < height - 1; i++)
419 print_found(buffer[i]);
421 print_ascii(buffer[i]);
425 /* Initialise the buffer */
426 static void buffer_init(void)
431 /* malloc the number of lines needed for the buffer */
432 buffer = xmalloc(height * sizeof(char *));
435 /* Fill the buffer until the end of the file or the
436 end of the buffer is reached */
437 for (i = 0; i < height - 1 && i <= num_flines; i++) {
438 buffer[i] = flines[i];
441 /* If the buffer still isn't full, fill it with blank lines */
442 for (; i < height - 1; i++) {
443 buffer[i] = empty_line_marker;
447 /* Move the buffer up and down in the file in order to scroll */
448 static void buffer_down(int nlines)
452 if (line_pos + (height - 3) + nlines < num_flines) {
454 for (i = 0; i < (height - 1); i++) {
455 buffer[i] = flines[line_pos + i];
458 /* As the number of lines requested was too large, we just move
459 to the end of the file */
460 while (line_pos + (height - 3) + 1 < num_flines) {
462 for (i = 0; i < (height - 1); i++) {
463 buffer[i] = flines[line_pos + i];
468 /* We exit if the -E flag has been set */
469 if ((option_mask32 & FLAG_E) && (line_pos + (height - 2) == num_flines))
473 static void buffer_up(int nlines)
478 if (line_pos < 0) line_pos = 0;
479 for (i = 0; i < height - 1; i++) {
480 if (line_pos + i <= num_flines) {
481 buffer[i] = flines[line_pos + i];
483 buffer[i] = empty_line_marker;
488 static void buffer_line(int linenum)
492 if (linenum < 0 || linenum > num_flines) {
494 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum + 1, NORMAL);
496 for (i = 0; i < height - 1; i++) {
497 if (linenum + i <= num_flines)
498 buffer[i] = flines[linenum + i];
500 buffer[i] = empty_line_marker;
508 /* Reinitialise everything for a new file - free the memory and start over */
509 static void reinitialise(void)
513 for (i = 0; i <= num_flines; i++)
522 static void examine_file(void)
527 filename = xmalloc_getline(inp);
528 files[num_files] = filename;
529 current_file = num_files + 1;
534 /* This function changes the file currently being paged. direction can be one of the following:
535 * -1: go back one file
536 * 0: go to the first file
537 * 1: go forward one file
539 static void change_file(int direction)
541 if (current_file != ((direction > 0) ? num_files : 1)) {
542 current_file = direction ? current_file + direction : 1;
544 filename = xstrdup(files[current_file - 1]);
548 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
552 static void remove_current_file(void)
556 if (current_file != 1) {
558 for (i = 3; i <= num_files; i++)
559 files[i - 2] = files[i - 1];
564 for (i = 2; i <= num_files; i++)
565 files[i - 2] = files[i - 1];
572 static void colon_process(void)
576 /* Clear the current line and print a prompt */
580 keypress = tless_getch();
583 remove_current_file();
588 #if ENABLE_FEATURE_LESS_FLAGS
611 static int normalize_match_pos(int match)
615 return (match_pos = 0);
616 if (match >= num_matches)
618 match_pos = num_matches - 1;
623 #if ENABLE_FEATURE_LESS_REGEXP
624 static void goto_match(int match)
626 buffer_line(match_lines[normalize_match_pos(match)]);
629 static void regex_process(void)
633 /* Reset variables */
634 match_lines = xrealloc(match_lines, sizeof(int));
643 /* Get the uncompiled regular expression from the user */
645 putchar((option_mask32 & LESS_STATE_MATCH_BACKWARDS) ? '?' : '/');
646 uncomp_regex = xmalloc_getline(inp);
647 if (!uncomp_regex || !uncomp_regex[0]) {
653 /* Compile the regex and check for errors */
654 xregcomp(&pattern, uncomp_regex, 0);
658 /* Run the regex on each line of the current file */
659 for (match_pos = 0; match_pos <= num_flines; match_pos++) {
660 if (regexec(&pattern, flines[match_pos], 0, NULL, 0) == 0) {
661 match_lines = xrealloc(match_lines, (num_matches+1) * sizeof(int));
662 match_lines[num_matches++] = match_pos;
666 if (num_matches == 0 || num_flines <= height - 2) {
670 for (match_pos = 0; match_pos < num_matches; match_pos++) {
671 if (match_lines[match_pos] > line_pos)
674 if (option_mask32 & LESS_STATE_MATCH_BACKWARDS) match_pos--;
675 buffer_line(match_lines[normalize_match_pos(match_pos)]);
679 static void number_process(int first_digit)
683 char num_input[sizeof(int)*4]; /* more than enough */
686 num_input[0] = first_digit;
688 /* Clear the current line, print a prompt, and then print the digit */
690 printf(":%c", first_digit);
692 /* Receive input until a letter is given */
693 while (i < sizeof(num_input)-1) {
694 num_input[i] = tless_getch();
695 if (!num_input[i] || !isdigit(num_input[i]))
697 putchar(num_input[i]);
701 /* Take the final letter out of the digits string */
702 keypress = num_input[i];
704 num = bb_strtou(num_input, NULL, 10);
705 /* on format error, num == -1 */
706 if (num < 1 || num > MAXLINES) {
711 /* We now know the number and the letter entered, so we process them */
713 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
716 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
719 case 'g': case '<': case 'G': case '>':
720 if (num_flines >= height - 2)
721 buffer_line(num - 1);
724 buffer_line(((num / 100) * num_flines) - 1);
726 #if ENABLE_FEATURE_LESS_REGEXP
728 goto_match(match_pos + num);
731 option_mask32 &= ~LESS_STATE_MATCH_BACKWARDS;
735 option_mask32 |= LESS_STATE_MATCH_BACKWARDS;
744 #if ENABLE_FEATURE_LESS_FLAGCS
745 static void flag_change(void)
751 keypress = tless_getch();
755 option_mask32 ^= FLAG_M;
758 option_mask32 ^= FLAG_m;
761 option_mask32 ^= FLAG_E;
764 option_mask32 ^= FLAG_TILDE;
771 static void show_flag_status(void)
778 keypress = tless_getch();
782 flag_val = option_mask32 & FLAG_M;
785 flag_val = option_mask32 & FLAG_m;
788 flag_val = option_mask32 & FLAG_TILDE;
791 flag_val = option_mask32 & FLAG_N;
794 flag_val = option_mask32 & FLAG_E;
802 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
806 static void full_repaint(void)
808 int temp_line_pos = line_pos;
811 buffer_line(temp_line_pos);
815 static void save_input_to_file(void)
822 printf("Log file: ");
823 current_line = xmalloc_getline(inp);
824 if (strlen(current_line) > 0) {
825 fp = fopen(current_line, "w");
828 printf("%s%s%s", HIGHLIGHT, "Error opening log file", NORMAL);
831 for (i = 0; i < num_flines; i++)
832 fprintf(fp, "%s\n", flines[i]);
838 printf("%s%s%s", HIGHLIGHT, "No log file", NORMAL);
841 #if ENABLE_FEATURE_LESS_MARKS
842 static void add_mark(void)
848 letter = tless_getch();
850 if (isalpha(letter)) {
852 /* If we exceed 15 marks, start overwriting previous ones */
856 mark_lines[num_marks][0] = letter;
857 mark_lines[num_marks][1] = line_pos;
861 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
865 static void goto_mark(void)
871 printf("Go to mark: ");
872 letter = tless_getch();
875 if (isalpha(letter)) {
876 for (i = 0; i <= num_marks; i++)
877 if (letter == mark_lines[i][0]) {
878 buffer_line(mark_lines[i][1]);
881 if ((num_marks == 14) && (letter != mark_lines[14][0]))
882 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
884 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
889 #if ENABLE_FEATURE_LESS_BRACKETS
891 static char opp_bracket(char bracket)
907 static void match_right_bracket(char bracket)
909 int bracket_line = -1;
914 if (strchr(flines[line_pos], bracket) == NULL)
915 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
917 for (i = line_pos + 1; i < num_flines; i++) {
918 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
924 if (bracket_line == -1)
925 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
927 buffer_line(bracket_line - height + 2);
931 static void match_left_bracket(char bracket)
933 int bracket_line = -1;
938 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
939 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
940 printf("%s", flines[line_pos + height]);
943 for (i = line_pos + height - 2; i >= 0; i--) {
944 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
950 if (bracket_line == -1)
951 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
953 buffer_line(bracket_line);
957 #endif /* FEATURE_LESS_BRACKETS */
959 static void keypress_process(int keypress)
962 case KEY_DOWN: case 'e': case 'j': case '\015':
966 case KEY_UP: case 'y': case 'k':
970 case PAGE_DOWN: case ' ': case 'z':
971 buffer_down(height - 1);
974 case PAGE_UP: case 'w': case 'b':
975 buffer_up(height - 1);
979 buffer_down((height - 1) / 2);
983 buffer_up((height - 1) / 2);
986 case KEY_HOME: case 'g': case 'p': case '<': case '%':
989 case KEY_END: case 'G': case '>':
990 buffer_line(num_flines - height + 2);
995 #if ENABLE_FEATURE_LESS_MARKS
1012 save_input_to_file();
1017 #if ENABLE_FEATURE_LESS_FLAGS
1023 #if ENABLE_FEATURE_LESS_REGEXP
1025 option_mask32 &= ~LESS_STATE_MATCH_BACKWARDS;
1029 goto_match(match_pos + 1);
1032 goto_match(match_pos - 1);
1035 option_mask32 |= LESS_STATE_MATCH_BACKWARDS;
1039 #if ENABLE_FEATURE_LESS_FLAGCS
1048 #if ENABLE_FEATURE_LESS_BRACKETS
1049 case '{': case '(': case '[':
1050 match_right_bracket(keypress);
1052 case '}': case ')': case ']':
1053 match_left_bracket(keypress);
1063 if (isdigit(keypress))
1064 number_process(keypress);
1067 static void sig_catcher(int sig ATTRIBUTE_UNUSED)
1073 int less_main(int argc, char **argv)
1077 getopt32(argc, argv, "EMmN~");
1084 if (isatty(STDIN_FILENO)) {
1085 /* Just "less"? No file and no redirection? */
1086 bb_error_msg("missing filename");
1090 filename = xstrdup(files[0]);
1092 /* FIXME: another popular pager, most, detects when stdout
1093 * is not a tty and turns into cat */
1094 inp = xfopen(CURRENT_TTY, "r");
1096 get_terminal_width_height(fileno(inp), &width, &height);
1097 if (width < 10 || height < 3)
1098 bb_error_msg_and_die("too narrow here");
1100 if (option_mask32 & FLAG_TILDE) empty_line_marker = "";
1104 tcgetattr(fileno(inp), &term_orig);
1105 signal(SIGTERM, sig_catcher);
1106 signal(SIGINT, sig_catcher);
1108 term_vi = term_orig;
1109 term_vi.c_lflag &= (~ICANON & ~ECHO);
1110 term_vi.c_iflag &= (~IXON & ~ICRNL);
1111 term_vi.c_oflag &= (~ONLCR);
1112 term_vi.c_cc[VMIN] = 1;
1113 term_vi.c_cc[VTIME] = 0;
1118 keypress = tless_getch();
1119 keypress_process(keypress);