1 /* vi: set sw=4 ts=4: */
3 * Mini less implementation for busybox
6 * Copyright (C) 2005 by Rob Sullivan <cogito.ergo.cogito@gmail.com>
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
23 * This program needs a lot of development, so consider it in a beta stage
27 * - Add more regular expression support - search modifiers, certain matches, etc.
28 * - Add more complex bracket searching - currently, nested brackets are
30 * - Add support for "F" as an input. This causes less to act in
31 * a similar way to tail -f.
32 * - Check for binary files, and prompt the user if a binary file
34 * - Allow horizontal scrolling. Currently, lines simply continue onto
35 * the next line, per the terminal's discretion
38 * - filename is an array and not a pointer because that avoids all sorts
39 * of complications involving the fact that something that is pointed to
40 * will be changed if the pointer is changed.
41 * - the inp file pointer is used so that keyboard input works after
42 * redirected input has been read from stdin
54 #ifdef CONFIG_FEATURE_LESS_REGEXP
59 /* These are the escape sequences corresponding to special keys */
60 #define REAL_KEY_UP 'A'
61 #define REAL_KEY_DOWN 'B'
62 #define REAL_KEY_RIGHT 'C'
63 #define REAL_KEY_LEFT 'D'
64 #define REAL_PAGE_UP '5'
65 #define REAL_PAGE_DOWN '6'
67 /* These are the special codes assigned by this program to the special keys */
75 /* The escape codes for highlighted and normal text */
76 #define HIGHLIGHT "\033[7m"
77 #define NORMAL "\033[0m"
79 /* The escape code to clear the screen */
80 #define CLEAR "\033[H\033[J"
82 /* Maximum number of lines in a file */
83 #define MAXLINES 10000
85 /* Get height and width of terminal */
86 #define tty_width_height() get_terminal_width_height(0, &width, &height)
91 static char filename[256];
94 static int current_file = 1;
96 static int num_flines;
97 static int num_files = 1;
100 /* Command line options */
101 static unsigned long flags;
103 #define FLAG_M (1<<1)
104 #define FLAG_m (1<<2)
105 #define FLAG_N (1<<3)
106 #define FLAG_TILDE (1<<4)
108 /* This is needed so that program behaviour changes when input comes from
110 static int inp_stdin;
111 /* This is required so that when a file is requested to be examined after
112 input has come from stdin (e.g. dmesg | less), the input stream from
113 the keyboard still stays the same. If it switched back to stdin, keyboard
114 input wouldn't work. */
115 static int ea_inp_stdin;
117 #ifdef CONFIG_FEATURE_LESS_MARKS
118 static int mark_lines[15][2];
119 static int num_marks;
122 #ifdef CONFIG_FEATURE_LESS_REGEXP
123 static int match_found;
124 static int match_lines[100];
125 static int match_pos;
126 static int num_matches;
127 static int match_backwards;
128 static int num_back_match = 1;
131 /* Needed termios structures */
132 static struct termios term_orig, term_vi;
134 /* File pointer to get input from */
137 /* Reset terminal input to normal */
138 static void set_tty_cooked(void) {
140 tcsetattr(0, TCSANOW, &term_orig);
143 /* Set terminal input to raw mode (taken from vi.c) */
144 static void set_tty_raw(void) {
145 tcsetattr(0, TCSANOW, &term_vi);
148 /* Exit the program gracefully */
149 static void tless_exit(int code) {
151 /* TODO: We really should save the terminal state when we start,
152 and restore it when we exit. Less does this with the
153 "ti" and "te" termcap commands; can this be done with
160 /* Grab a character from input without requiring the return key. If the
161 character is ASCII \033, get more characters and assign certain sequences
162 special return codes. Note that this function works best with raw input. */
163 static int tless_getch(void) {
170 /* Detect escape sequences (i.e. arrow keys) and handle
173 if (input == '\033' && getc(inp) == '[') {
176 if (input == REAL_KEY_UP)
178 else if (input == REAL_KEY_DOWN)
180 else if (input == REAL_KEY_RIGHT)
182 else if (input == REAL_KEY_LEFT)
184 else if (input == REAL_PAGE_UP)
186 else if (input == REAL_PAGE_DOWN)
189 /* The input is a normal ASCII value */
197 /* Move the cursor to a position (x,y), where (0,0) is the
198 top-left corner of the console */
199 static void move_cursor(int x, int y) {
200 printf("\033[%i;%iH", x, y);
203 static void clear_line(void) {
204 move_cursor(height, 0);
208 /* This adds line numbers to every line, as the -N flag necessitates */
209 static void add_linenumbers(void) {
211 char current_line[256];
214 for (i = 0; i <= num_flines; i++) {
215 safe_strncpy(current_line, flines[i], 256);
216 bb_xasprintf(&flines[i],"%5d %s", i + 1, current_line);
220 static void data_readlines(void) {
223 char current_line[256];
226 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "rt");
228 /* First of all, we need to know the number of lines so that flines can be initialised. */
229 for (i = 0; (!feof(fp)) && (i <= MAXLINES); i++)
230 fgets(current_line, 256, fp);
233 flines = malloc(i * sizeof(char *));
235 for (i = 0; (!feof(fp)) && (i <= MAXLINES); i++) {
236 strcpy(current_line, "");
237 fgets(current_line, 256, fp);
238 bb_xferror(fp, filename);
239 flines[i] = bb_xstrdup(current_line);
243 /* Reset variables for a new file */
250 inp = (inp_stdin) ? fopen(CURRENT_TTY, "r") : stdin;
254 inp = fopen(CURRENT_TTY, "r");
261 /* Turn a percentage into a line number */
262 static int reverse_percent(int percentage) {
263 double linenum = percentage;
264 linenum = ((linenum / 100) * num_flines) - 1;
268 #ifdef CONFIG_FEATURE_LESS_FLAGS
270 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
272 static int calc_percent(void) {
273 return ((100 * (line_pos + height - 2) / num_flines) + 1);
276 /* Print a status line if -M was specified */
277 static void m_status_print(void) {
284 printf("%s%s %s%i%s%i%s%i-%i/%i ", HIGHLIGHT, filename, "(file ", current_file, " of ", num_files, ") lines ", line_pos + 1, line_pos + height - 1, num_flines + 1);
286 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
290 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
293 if (line_pos == num_flines - height + 2) {
294 printf("(END) %s", NORMAL);
295 if ((num_files > 1) && (current_file != num_files))
296 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
299 percentage = calc_percent();
300 printf("%i%% %s", percentage, NORMAL);
304 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
305 if ((num_files > 1) && (current_file != num_files))
306 printf("- Next: %s", files[current_file]);
307 printf("%s", NORMAL);
311 /* Print a status line if -m was specified */
312 static void medium_status_print(void) {
315 percentage = calc_percent();
318 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
319 else if (line_pos == num_flines - height + 2)
320 printf("%s(END)%s", HIGHLIGHT, NORMAL);
322 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
326 /* Print the status line */
327 static void status_print(void) {
329 /* Change the status if flags have been set */
330 #ifdef CONFIG_FEATURE_LESS_FLAGS
333 else if (flags & FLAG_m)
334 medium_status_print();
339 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
341 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", current_file, " of ", num_files, ")", NORMAL);
343 else if (line_pos == num_flines - height + 2) {
344 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
345 if ((num_files > 1) && (current_file != num_files))
346 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
351 #ifdef CONFIG_FEATURE_LESS_FLAGS
356 /* Print the buffer */
357 static void buffer_print(void) {
362 if (num_flines >= height - 2) {
363 for (i = 0; i < height - 1; i++)
364 printf("%s", buffer[i]);
367 for (i = 1; i < (height - 1 - num_flines); i++)
369 for (i = 0; i < height - 1; i++)
370 printf("%s", buffer[i]);
376 /* Initialise the buffer */
377 static void buffer_init(void) {
382 /* malloc the number of lines needed for the buffer */
383 buffer = xrealloc(buffer, height * sizeof(char *));
385 for (i = 0; i < (height - 1); i++)
389 /* Fill the buffer until the end of the file or the
390 end of the buffer is reached */
391 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
392 buffer[i] = (char *) bb_xstrdup(flines[i]);
395 /* If the buffer still isn't full, fill it with blank lines */
396 for (; i < (height - 1); i++) {
397 buffer[i] = bb_xstrdup("");
401 /* Move the buffer up and down in the file in order to scroll */
402 static void buffer_down(int nlines) {
407 if (line_pos + (height - 3) + nlines < num_flines) {
409 for (i = 0; i < (height - 1); i++) {
411 buffer[i] = (char *) bb_xstrdup(flines[line_pos + i]);
415 /* As the number of lines requested was too large, we just move
416 to the end of the file */
417 while (line_pos + (height - 3) + 1 < num_flines) {
419 for (i = 0; i < (height - 1); i++) {
421 buffer[i] = (char *) bb_xstrdup(flines[line_pos + i]);
426 /* We exit if the -E flag has been set */
427 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
432 static void buffer_up(int nlines) {
438 if (line_pos - nlines >= 0) {
440 for (i = 0; i < (height - 1); i++) {
442 buffer[i] = (char *) bb_xstrdup(flines[line_pos + i]);
446 /* As the requested number of lines to move was too large, we
447 move one line up at a time until we can't. */
448 while (line_pos != 0) {
450 for (i = 0; i < (height - 1); i++) {
452 buffer[i] = (char *) bb_xstrdup(flines[line_pos + i]);
458 /* Work out where the tildes start */
459 tilde_line = num_flines - line_pos + 3;
462 /* Going backwards nlines lines has taken us to a point where
463 nothing is past the EOF, so we revert to normal. */
464 if (line_pos < num_flines - height + 3) {
469 /* We only move part of the buffer, as the rest
471 for (i = 0; i < (height - 1); i++) {
473 if (i < tilde_line - nlines + 1)
474 buffer[i] = (char *) bb_xstrdup(flines[line_pos + i]);
476 if (line_pos >= num_flines - height + 2)
477 buffer[i] = bb_xstrdup("~\n");
484 static void buffer_line(int linenum) {
490 if (linenum < 1 || linenum > num_flines) {
492 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum, NORMAL);
494 else if (linenum < (num_flines - height - 2)) {
495 for (i = 0; i < (height - 1); i++) {
497 buffer[i] = (char *) bb_xstrdup(flines[linenum + i]);
502 for (i = 0; i < (height - 1); i++) {
504 if (linenum + i < num_flines + 2)
505 buffer[i] = (char *) bb_xstrdup(flines[linenum + i]);
507 buffer[i] = (char *) bb_xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
510 /* Set past_eof so buffer_down and buffer_up act differently */
515 /* Reinitialise everything for a new file - free the memory and start over */
516 static void reinitialise(void) {
520 for (i = 0; i <= num_flines; i++)
529 static void examine_file(void) {
535 fgets(filename, 256, inp);
537 /* As fgets adds a newline to the end of an input string, we
539 newline_offset = strlen(filename) - 1;
540 filename[newline_offset] = '\0';
542 files[num_files] = bb_xstrndup(filename, (strlen(filename) + 1) * sizeof(char));
543 current_file = num_files + 1;
551 /* This function changes the file currently being paged. direction can be one of the following:
552 * -1: go back one file
553 * 0: go to the first file
554 * 1: go forward one file
556 static void change_file (int direction) {
557 if (current_file != ((direction > 0) ? num_files : 1)) {
558 current_file = direction ? current_file + direction : 1;
559 strcpy(filename, files[current_file - 1]);
564 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
568 static void remove_current_file(void) {
572 if (current_file != 1) {
574 for (i = 3; i <= num_files; i++)
575 files[i - 2] = files[i - 1];
581 for (i = 2; i <= num_files; i++)
582 files[i - 2] = files[i - 1];
589 static void colon_process(void) {
593 /* Clear the current line and print a prompt */
597 keypress = tless_getch();
600 remove_current_file();
605 #ifdef CONFIG_FEATURE_LESS_FLAGS
628 #ifdef CONFIG_FEATURE_LESS_REGEXP
629 /* The below two regular expression handler functions NEED development. */
631 /* Get a regular expression from the user, and then go through the current
632 file line by line, running a processing regex function on each one. */
634 static char *insert_highlights (char *line, int start, int end) {
638 bb_xasprintf(&new_line, "%.*s%s%.*s%s%s", start, line, HIGHLIGHT,
639 end - start, line + start, NORMAL, line + end);
643 static char *process_regex_on_line(char *line, regex_t *pattern) {
644 /* This function takes the regex and applies it to the line.
645 Each part of the line that matches has the HIGHLIGHT
646 and NORMAL escape sequences placed around it by
647 insert_highlights, and then the line is returned. */
650 char *line2 = (char *) malloc((sizeof(char) * (strlen(line) + 1)) + 64);
653 regmatch_t match_structs;
655 memset(sub_line, 0, 256);
659 match_status = regexec(pattern, line2, 1, &match_structs, 0);
661 while (match_status == 0) {
663 memset(sub_line, 0, 256);
665 if (match_found == 0)
668 line2 = insert_highlights(line2, match_structs.rm_so + prev_eo, match_structs.rm_eo + prev_eo);
669 if (match_structs.rm_eo + 11 + prev_eo < strlen(line2))
670 strcat(sub_line, line2 + match_structs.rm_eo + 11 + prev_eo);
672 prev_eo += match_structs.rm_eo + 11;
673 match_status = regexec(pattern, sub_line, 1, &match_structs, REG_NOTBOL);
679 static void regex_process(void) {
681 char uncomp_regex[100];
682 char current_line[256];
687 /* Reset variables */
693 pattern = (regex_t *) malloc(sizeof(regex_t));
694 memset(pattern, 0, sizeof(regex_t));
696 /* Get the uncompiled regular expression from the user */
698 putchar((match_backwards) ? '?' : '/');
699 scanf("%s", uncomp_regex);
701 /* Compile the regex and check for errors */
702 xregcomp(pattern, uncomp_regex, 0);
704 /* Run the regex on each line of the current file here */
705 for (i = 0; i <= num_flines; i++) {
706 strcpy(current_line, process_regex_on_line(flines[i], pattern));
707 flines[i] = (char *) bb_xstrndup(current_line, sizeof(char) * (strlen(current_line)+1));
716 if ((match_lines[0] != -1) && (num_flines > height - 2))
717 buffer_line(match_lines[0]);
722 static void goto_match(int match) {
724 /* This goes to a specific match - all line positions of matches are
725 stored within the match_lines[] array. */
726 if ((match < num_matches) && (match >= 0)) {
727 buffer_line(match_lines[match]);
732 static void search_backwards(void) {
734 int current_linepos = line_pos;
740 for (i = 0; i < num_matches; i++) {
741 if (match_lines[i] > current_linepos) {
742 buffer_line(match_lines[i - num_back_match]);
747 /* Reset variables */
754 static void number_process(int first_digit) {
760 num_input[0] = first_digit;
762 /* Clear the current line, print a prompt, and then print the digit */
764 printf(":%c", first_digit);
766 /* Receive input until a letter is given */
767 while((num_input[i] = tless_getch()) && isdigit(num_input[i])) {
768 printf("%c", num_input[i]);
772 /* Take the final letter out of the digits string */
773 keypress = num_input[i];
776 num = atoi(num_input);
778 /* We now know the number and the letter entered, so we process them */
780 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
783 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
786 case 'g': case '<': case 'G': case '>':
787 if (num_flines >= height - 2)
788 buffer_line(num - 1);
791 buffer_line(reverse_percent(num));
793 #ifdef CONFIG_FEATURE_LESS_REGEXP
795 goto_match(match_pos + num - 1);
802 num_back_match = num;
813 #ifdef CONFIG_FEATURE_LESS_FLAGCS
814 static void flag_change(void) {
820 keypress = tless_getch();
833 flags &= ~FLAG_TILDE;
840 static void show_flag_status(void) {
847 keypress = tless_getch();
851 flag_val = flags & FLAG_M;
854 flag_val = flags & FLAG_m;
857 flag_val = flags & FLAG_TILDE;
860 flag_val = flags & FLAG_N;
863 flag_val = flags & FLAG_E;
871 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
875 static void full_repaint(void) {
877 int temp_line_pos = line_pos;
880 buffer_line(temp_line_pos);
885 static void save_input_to_file(void) {
887 char current_line[256];
892 printf("Log file: ");
893 fgets(current_line, 256, inp);
894 current_line[strlen(current_line) - 1] = '\0';
895 if (strlen(current_line)) {
896 fp = bb_xfopen(current_line, "w");
897 for (i = 0; i < num_flines; i++)
898 fprintf(fp, "%s", flines[i]);
903 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
906 #ifdef CONFIG_FEATURE_LESS_MARKS
907 static void add_mark(void) {
914 letter = tless_getch();
916 if (isalpha(letter)) {
917 mark_line = line_pos;
919 /* If we exceed 15 marks, start overwriting previous ones */
923 mark_lines[num_marks][0] = letter;
924 mark_lines[num_marks][1] = line_pos;
929 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
933 static void goto_mark(void) {
939 printf("Go to mark: ");
940 letter = tless_getch();
943 if (isalpha(letter)) {
944 for (i = 0; i <= num_marks; i++)
945 if (letter == mark_lines[i][0]) {
946 buffer_line(mark_lines[i][1]);
949 if ((num_marks == 14) && (letter != mark_lines[14][0]))
950 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
953 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
958 #ifdef CONFIG_FEATURE_LESS_BRACKETS
960 static char opp_bracket (char bracket) {
981 static void match_right_bracket(char bracket) {
983 int bracket_line = -1;
988 if (strchr(flines[line_pos], bracket) == NULL)
989 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
991 for (i = line_pos + 1; i < num_flines; i++) {
992 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
998 if (bracket_line == -1)
999 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1001 buffer_line(bracket_line - height + 2);
1006 static void match_left_bracket (char bracket) {
1008 int bracket_line = -1;
1013 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
1014 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
1015 printf("%s", flines[line_pos + height]);
1019 for (i = line_pos + height - 2; i >= 0; i--) {
1020 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1026 if (bracket_line == -1)
1027 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1029 buffer_line(bracket_line);
1034 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1036 static void keypress_process(int keypress) {
1038 case KEY_DOWN: case 'e': case 'j': case '\015':
1042 case KEY_UP: case 'y': case 'k':
1046 case PAGE_DOWN: case ' ': case 'z':
1047 buffer_down(height - 1);
1050 case PAGE_UP: case 'w': case 'b':
1051 buffer_up(height - 1);
1055 buffer_down((height - 1) / 2);
1059 buffer_up((height - 1) / 2);
1062 case 'g': case 'p': case '<': case '%':
1063 buffer_up(num_flines + 1);
1067 buffer_down(num_flines + 1);
1073 #ifdef CONFIG_FEATURE_LESS_MARKS
1091 save_input_to_file();
1096 #ifdef CONFIG_FEATURE_LESS_FLAGS
1102 #ifdef CONFIG_FEATURE_LESS_REGEXP
1108 goto_match(match_pos + 1);
1112 goto_match(match_pos - 1);
1120 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1129 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1130 case '{': case '(': case '[':
1131 match_right_bracket(keypress);
1133 case '}': case ')': case ']':
1134 match_left_bracket(keypress);
1144 if (isdigit(keypress))
1145 number_process(keypress);
1148 int less_main(int argc, char **argv) {
1152 flags = bb_getopt_ulflags(argc, argv, "EMmN~");
1160 if (ttyname(STDIN_FILENO) == NULL)
1163 bb_error_msg("Missing filename");
1168 strcpy(filename, (inp_stdin) ? "stdin" : files[0]);
1170 tcgetattr(0, &term_orig);
1171 term_vi = term_orig;
1172 term_vi.c_lflag &= (~ICANON & ~ECHO);
1173 term_vi.c_iflag &= (~IXON & ~ICRNL);
1174 term_vi.c_oflag &= (~ONLCR);
1175 term_vi.c_cc[VMIN] = 1;
1176 term_vi.c_cc[VTIME] = 0;
1183 keypress = tless_getch();
1184 keypress_process(keypress);