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.
11 * This program needs a lot of development, so consider it in a beta stage
15 * - Add more regular expression support - search modifiers, certain matches, etc.
16 * - Add more complex bracket searching - currently, nested brackets are
18 * - Add support for "F" as an input. This causes less to act in
19 * a similar way to tail -f.
20 * - Check for binary files, and prompt the user if a binary file
22 * - Allow horizontal scrolling. Currently, lines simply continue onto
23 * the next line, per the terminal's discretion
26 * - filename is an array and not a pointer because that avoids all sorts
27 * of complications involving the fact that something that is pointed to
28 * will be changed if the pointer is changed.
29 * - the inp file pointer is used so that keyboard input works after
30 * redirected input has been read from stdin
35 #ifdef CONFIG_FEATURE_LESS_REGEXP
40 /* These are the escape sequences corresponding to special keys */
41 #define REAL_KEY_UP 'A'
42 #define REAL_KEY_DOWN 'B'
43 #define REAL_KEY_RIGHT 'C'
44 #define REAL_KEY_LEFT 'D'
45 #define REAL_PAGE_UP '5'
46 #define REAL_PAGE_DOWN '6'
48 /* These are the special codes assigned by this program to the special keys */
56 /* The escape codes for highlighted and normal text */
57 #define HIGHLIGHT "\033[7m"
58 #define NORMAL "\033[0m"
60 /* The escape code to clear the screen */
61 #define CLEAR "\033[H\033[J"
63 /* Maximum number of lines in a file */
64 #define MAXLINES 10000
69 static char filename[256];
72 static int current_file = 1;
74 static int num_flines;
75 static int num_files = 1;
78 /* Command line options */
79 static unsigned flags;
84 #define FLAG_TILDE (1<<4)
86 /* This is needed so that program behaviour changes when input comes from
90 #ifdef CONFIG_FEATURE_LESS_MARKS
91 static int mark_lines[15][2];
95 #ifdef CONFIG_FEATURE_LESS_REGEXP
96 static int match_found;
97 static int *match_lines;
99 static int num_matches;
100 static int match_backwards;
101 static regex_t old_pattern;
104 /* Needed termios structures */
105 static struct termios term_orig, term_vi;
107 /* File pointer to get input from */
110 /* Reset terminal input to normal */
111 static void set_tty_cooked(void)
114 tcsetattr(fileno(inp), TCSANOW, &term_orig);
117 /* Set terminal input to raw mode (taken from vi.c) */
118 static void set_tty_raw(void)
120 tcsetattr(fileno(inp), TCSANOW, &term_vi);
123 /* Exit the program gracefully */
124 static void tless_exit(int code)
126 /* TODO: We really should save the terminal state when we start,
127 and restore it when we exit. Less does this with the
128 "ti" and "te" termcap commands; can this be done with
135 /* Grab a character from input without requiring the return key. If the
136 character is ASCII \033, get more characters and assign certain sequences
137 special return codes. Note that this function works best with raw input. */
138 static int tless_getch(void)
145 /* Detect escape sequences (i.e. arrow keys) and handle
148 if (input == '\033' && getc(inp) == '[') {
151 if (input == REAL_KEY_UP)
153 else if (input == REAL_KEY_DOWN)
155 else if (input == REAL_KEY_RIGHT)
157 else if (input == REAL_KEY_LEFT)
159 else if (input == REAL_PAGE_UP)
161 else if (input == REAL_PAGE_DOWN)
164 /* The input is a normal ASCII value */
172 /* Move the cursor to a position (x,y), where (0,0) is the
173 top-left corner of the console */
174 static void move_cursor(int x, int y)
176 printf("\033[%i;%iH", x, y);
179 static void clear_line(void)
181 move_cursor(height, 0);
185 /* This adds line numbers to every line, as the -N flag necessitates */
186 static void add_linenumbers(void)
190 for (i = 0; i <= num_flines; i++) {
191 char *new = xasprintf("%5d %s", i + 1, flines[i]);
197 static void data_readlines(void)
200 char current_line[256];
203 fp = (inp_stdin) ? stdin : xfopen(filename, "r");
205 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
206 strcpy(current_line, "");
207 fgets(current_line, 256, fp);
209 die_if_ferror(fp, filename);
210 flines = xrealloc(flines, (i+1) * sizeof(char *));
211 flines[i] = xstrdup(current_line);
215 /* Reset variables for a new file */
223 inp = (inp_stdin) ? xfopen(CURRENT_TTY, "r") : stdin;
229 #ifdef CONFIG_FEATURE_LESS_FLAGS
231 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
233 static int calc_percent(void)
235 return ((100 * (line_pos + height - 2) / num_flines) + 1);
238 /* Print a status line if -M was specified */
239 static void m_status_print(void)
246 printf("%s%s %s%i%s%i%s%i-%i/%i ", HIGHLIGHT,
247 filename, "(file ", current_file, " of ", num_files, ") lines ",
248 line_pos + 1, line_pos + height - 1, num_flines + 1);
250 printf("%s%s lines %i-%i/%i ", HIGHLIGHT,
251 filename, line_pos + 1, line_pos + height - 1,
256 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename,
257 line_pos + 1, line_pos + height - 1, num_flines + 1);
260 if (line_pos == num_flines - height + 2) {
261 printf("(END) %s", NORMAL);
262 if ((num_files > 1) && (current_file != num_files))
263 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
266 percentage = calc_percent();
267 printf("%i%% %s", percentage, NORMAL);
271 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename,
272 line_pos + 1, num_flines + 1, num_flines + 1);
273 if ((num_files > 1) && (current_file != num_files))
274 printf("- Next: %s", files[current_file]);
275 printf("%s", NORMAL);
279 /* Print a status line if -m was specified */
280 static void medium_status_print(void)
283 percentage = calc_percent();
286 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
287 else if (line_pos == num_flines - height + 2)
288 printf("%s(END)%s", HIGHLIGHT, NORMAL);
290 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
294 /* Print the status line */
295 static void status_print(void)
297 /* Change the status if flags have been set */
298 #ifdef CONFIG_FEATURE_LESS_FLAGS
301 else if (flags & FLAG_m)
302 medium_status_print();
307 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
309 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ",
310 current_file, " of ", num_files, ")", NORMAL);
312 else if (line_pos == num_flines - height + 2) {
313 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
314 if ((num_files > 1) && (current_file != num_files))
315 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
320 #ifdef CONFIG_FEATURE_LESS_FLAGS
325 /* Print the buffer */
326 static void buffer_print(void)
331 if (num_flines >= height - 2) {
332 for (i = 0; i < height - 1; i++)
333 printf("%s", buffer[i]);
336 for (i = 1; i < (height - 1 - num_flines); i++)
338 for (i = 0; i < height - 1; i++)
339 printf("%s", buffer[i]);
345 /* Initialise the buffer */
346 static void buffer_init(void)
350 if (buffer == NULL) {
351 /* malloc the number of lines needed for the buffer */
352 buffer = xrealloc(buffer, height * sizeof(char *));
354 for (i = 0; i < (height - 1); i++)
358 /* Fill the buffer until the end of the file or the
359 end of the buffer is reached */
360 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
361 buffer[i] = xstrdup(flines[i]);
364 /* If the buffer still isn't full, fill it with blank lines */
365 for (; i < (height - 1); i++) {
366 buffer[i] = xstrdup("");
370 /* Move the buffer up and down in the file in order to scroll */
371 static void buffer_down(int nlines)
376 if (line_pos + (height - 3) + nlines < num_flines) {
378 for (i = 0; i < (height - 1); i++) {
380 buffer[i] = xstrdup(flines[line_pos + i]);
384 /* As the number of lines requested was too large, we just move
385 to the end of the file */
386 while (line_pos + (height - 3) + 1 < num_flines) {
388 for (i = 0; i < (height - 1); i++) {
390 buffer[i] = xstrdup(flines[line_pos + i]);
395 /* We exit if the -E flag has been set */
396 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
401 static void buffer_up(int nlines)
407 if (line_pos - nlines >= 0) {
409 for (i = 0; i < (height - 1); i++) {
411 buffer[i] = xstrdup(flines[line_pos + i]);
415 /* As the requested number of lines to move was too large, we
416 move one line up at a time until we can't. */
417 while (line_pos != 0) {
419 for (i = 0; i < (height - 1); i++) {
421 buffer[i] = xstrdup(flines[line_pos + i]);
427 /* Work out where the tildes start */
428 tilde_line = num_flines - line_pos + 3;
431 /* Going backwards nlines lines has taken us to a point where
432 nothing is past the EOF, so we revert to normal. */
433 if (line_pos < num_flines - height + 3) {
438 /* We only move part of the buffer, as the rest
440 for (i = 0; i < (height - 1); i++) {
442 if (i < tilde_line - nlines + 1)
443 buffer[i] = xstrdup(flines[line_pos + i]);
445 if (line_pos >= num_flines - height + 2)
446 buffer[i] = xstrdup("~\n");
453 static void buffer_line(int linenum)
458 if (linenum < 0 || linenum > num_flines) {
460 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum + 1, NORMAL);
462 else if (linenum < (num_flines - height - 2)) {
463 for (i = 0; i < (height - 1); i++) {
465 buffer[i] = xstrdup(flines[linenum + i]);
471 for (i = 0; i < (height - 1); i++) {
473 if (linenum + i < num_flines + 2)
474 buffer[i] = xstrdup(flines[linenum + i]);
476 buffer[i] = xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
479 /* Set past_eof so buffer_down and buffer_up act differently */
485 /* Reinitialise everything for a new file - free the memory and start over */
486 static void reinitialise(void)
490 for (i = 0; i <= num_flines; i++)
499 static void examine_file(void)
505 fgets(filename, 256, inp);
507 /* As fgets adds a newline to the end of an input string, we
509 newline_offset = strlen(filename) - 1;
510 filename[newline_offset] = '\0';
512 files[num_files] = xstrdup(filename);
513 current_file = num_files + 1;
520 /* This function changes the file currently being paged. direction can be one of the following:
521 * -1: go back one file
522 * 0: go to the first file
523 * 1: go forward one file
525 static void change_file(int direction)
527 if (current_file != ((direction > 0) ? num_files : 1)) {
528 current_file = direction ? current_file + direction : 1;
529 strcpy(filename, files[current_file - 1]);
534 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
538 static void remove_current_file(void)
542 if (current_file != 1) {
544 for (i = 3; i <= num_files; i++)
545 files[i - 2] = files[i - 1];
551 for (i = 2; i <= num_files; i++)
552 files[i - 2] = files[i - 1];
559 static void colon_process(void)
563 /* Clear the current line and print a prompt */
567 keypress = tless_getch();
570 remove_current_file();
575 #ifdef CONFIG_FEATURE_LESS_FLAGS
598 #ifdef CONFIG_FEATURE_LESS_REGEXP
599 /* The below two regular expression handler functions NEED development. */
601 /* Get a regular expression from the user, and then go through the current
602 file line by line, running a processing regex function on each one. */
604 static char *process_regex_on_line(char *line, regex_t *pattern, int action)
606 /* This function takes the regex and applies it to the line.
607 Each part of the line that matches has the HIGHLIGHT
608 and NORMAL escape sequences placed around it by
609 insert_highlights if action = 1, or has the escape sequences
610 removed if action = 0, and then the line is returned. */
612 char *line2 = xmalloc((sizeof(char) * (strlen(line) + 1)) + 64);
614 regmatch_t match_structs;
616 line2 = xstrdup(line);
619 match_status = regexec(pattern, line2, 1, &match_structs, 0);
621 while (match_status == 0) {
622 if (match_found == 0)
626 growline = xasprintf("%s%.*s%s%.*s%s", growline,
627 match_structs.rm_so, line2, HIGHLIGHT,
628 match_structs.rm_eo - match_structs.rm_so,
629 line2 + match_structs.rm_so, NORMAL);
632 growline = xasprintf("%s%.*s%.*s", growline,
633 match_structs.rm_so - 4, line2,
634 match_structs.rm_eo - match_structs.rm_so,
635 line2 + match_structs.rm_so);
638 line2 += match_structs.rm_eo;
639 match_status = regexec(pattern, line2, 1, &match_structs, REG_NOTBOL);
642 growline = xasprintf("%s%s", growline, line2);
644 return (match_found ? growline : line);
650 static void goto_match(int match)
652 /* This goes to a specific match - all line positions of matches are
653 stored within the match_lines[] array. */
654 if ((match < num_matches) && (match >= 0)) {
655 buffer_line(match_lines[match]);
660 static void regex_process(void)
662 char uncomp_regex[100];
667 /* Get the uncompiled regular expression from the user */
669 putchar((match_backwards) ? '?' : '/');
671 fgets(uncomp_regex, sizeof(uncomp_regex), inp);
673 if (strlen(uncomp_regex) == 1) {
675 goto_match(match_backwards ? match_pos - 1 : match_pos + 1);
680 uncomp_regex[strlen(uncomp_regex) - 1] = '\0';
682 /* Compile the regex and check for errors */
683 xregcomp(&pattern, uncomp_regex, 0);
686 /* Get rid of all the highlights we added previously */
687 for (i = 0; i <= num_flines; i++) {
688 current_line = process_regex_on_line(flines[i], &old_pattern, 0);
689 flines[i] = xstrdup(current_line);
692 old_pattern = pattern;
694 /* Reset variables */
695 match_lines = xrealloc(match_lines, sizeof(int));
700 /* Run the regex on each line of the current file here */
701 for (i = 0; i <= num_flines; i++) {
702 current_line = process_regex_on_line(flines[i], &pattern, 1);
703 flines[i] = xstrdup(current_line);
705 match_lines = xrealloc(match_lines, (j + 1) * sizeof(int));
712 if ((match_lines[0] != -1) && (num_flines > height - 2)) {
713 if (match_backwards) {
714 for (i = 0; i < num_matches; i++) {
715 if (match_lines[i] > line_pos) {
717 buffer_line(match_lines[match_pos]);
723 buffer_line(match_lines[0]);
730 static void number_process(int first_digit)
738 num_input[0] = first_digit;
740 /* Clear the current line, print a prompt, and then print the digit */
742 printf(":%c", first_digit);
744 /* Receive input until a letter is given (max 80 chars)*/
745 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
746 putchar(num_input[i]);
750 /* Take the final letter out of the digits string */
751 keypress = num_input[i];
753 num = strtol(num_input, &endptr, 10);
754 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES) {
759 /* We now know the number and the letter entered, so we process them */
761 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
764 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
767 case 'g': case '<': case 'G': case '>':
768 if (num_flines >= height - 2)
769 buffer_line(num - 1);
772 buffer_line(((num / 100) * num_flines) - 1);
774 #ifdef CONFIG_FEATURE_LESS_REGEXP
776 goto_match(match_pos + num);
792 #ifdef CONFIG_FEATURE_LESS_FLAGCS
793 static void flag_change(void)
799 keypress = tless_getch();
819 static void show_flag_status(void)
826 keypress = tless_getch();
830 flag_val = flags & FLAG_M;
833 flag_val = flags & FLAG_m;
836 flag_val = flags & FLAG_TILDE;
839 flag_val = flags & FLAG_N;
842 flag_val = flags & FLAG_E;
850 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
854 static void full_repaint(void)
856 int temp_line_pos = line_pos;
859 buffer_line(temp_line_pos);
863 static void save_input_to_file(void)
865 char current_line[256];
870 printf("Log file: ");
871 fgets(current_line, 256, inp);
872 current_line[strlen(current_line) - 1] = '\0';
873 if (strlen(current_line) > 1) {
874 fp = xfopen(current_line, "w");
875 for (i = 0; i < num_flines; i++)
876 fprintf(fp, "%s", flines[i]);
881 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
884 #ifdef CONFIG_FEATURE_LESS_MARKS
885 static void add_mark(void)
891 letter = tless_getch();
893 if (isalpha(letter)) {
895 /* If we exceed 15 marks, start overwriting previous ones */
899 mark_lines[num_marks][0] = letter;
900 mark_lines[num_marks][1] = line_pos;
905 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
909 static void goto_mark(void)
915 printf("Go to mark: ");
916 letter = tless_getch();
919 if (isalpha(letter)) {
920 for (i = 0; i <= num_marks; i++)
921 if (letter == mark_lines[i][0]) {
922 buffer_line(mark_lines[i][1]);
925 if ((num_marks == 14) && (letter != mark_lines[14][0]))
926 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
929 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
934 #ifdef CONFIG_FEATURE_LESS_BRACKETS
936 static char opp_bracket(char bracket)
952 static void match_right_bracket(char bracket)
954 int bracket_line = -1;
959 if (strchr(flines[line_pos], bracket) == NULL)
960 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
962 for (i = line_pos + 1; i < num_flines; i++) {
963 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
969 if (bracket_line == -1)
970 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
972 buffer_line(bracket_line - height + 2);
976 static void match_left_bracket(char bracket)
978 int bracket_line = -1;
983 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
984 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
985 printf("%s", flines[line_pos + height]);
989 for (i = line_pos + height - 2; i >= 0; i--) {
990 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
996 if (bracket_line == -1)
997 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
999 buffer_line(bracket_line);
1003 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1005 static void keypress_process(int keypress)
1008 case KEY_DOWN: case 'e': case 'j': case '\015':
1012 case KEY_UP: case 'y': case 'k':
1016 case PAGE_DOWN: case ' ': case 'z':
1017 buffer_down(height - 1);
1020 case PAGE_UP: case 'w': case 'b':
1021 buffer_up(height - 1);
1025 buffer_down((height - 1) / 2);
1029 buffer_up((height - 1) / 2);
1032 case 'g': case 'p': case '<': case '%':
1036 buffer_line(num_flines - height + 2);
1041 #ifdef CONFIG_FEATURE_LESS_MARKS
1059 save_input_to_file();
1064 #ifdef CONFIG_FEATURE_LESS_FLAGS
1070 #ifdef CONFIG_FEATURE_LESS_REGEXP
1072 match_backwards = 0;
1076 goto_match(match_pos + 1);
1079 goto_match(match_pos - 1);
1082 match_backwards = 1;
1086 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1095 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1096 case '{': case '(': case '[':
1097 match_right_bracket(keypress);
1099 case '}': case ')': case ']':
1100 match_left_bracket(keypress);
1110 if (isdigit(keypress))
1111 number_process(keypress);
1114 int less_main(int argc, char **argv) {
1118 flags = getopt32(argc, argv, "EMmN~");
1126 if (ttyname(STDIN_FILENO) == NULL)
1129 bb_error_msg("missing filename");
1134 strcpy(filename, (inp_stdin) ? bb_msg_standard_input : files[0]);
1135 get_terminal_width_height(0, &width, &height);
1137 tcgetattr(fileno(inp), &term_orig);
1138 term_vi = term_orig;
1139 term_vi.c_lflag &= (~ICANON & ~ECHO);
1140 term_vi.c_iflag &= (~IXON & ~ICRNL);
1141 term_vi.c_oflag &= (~ONLCR);
1142 term_vi.c_cc[VMIN] = 1;
1143 term_vi.c_cc[VTIME] = 0;
1148 keypress = tless_getch();
1149 keypress_process(keypress);