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
42 #ifdef CONFIG_FEATURE_LESS_REGEXP
47 /* These are the escape sequences corresponding to special keys */
48 #define REAL_KEY_UP 'A'
49 #define REAL_KEY_DOWN 'B'
50 #define REAL_KEY_RIGHT 'C'
51 #define REAL_KEY_LEFT 'D'
52 #define REAL_PAGE_UP '5'
53 #define REAL_PAGE_DOWN '6'
55 /* These are the special codes assigned by this program to the special keys */
63 /* The escape codes for highlighted and normal text */
64 #define HIGHLIGHT "\033[7m"
65 #define NORMAL "\033[0m"
67 /* The escape code to clear the screen */
68 #define CLEAR "\033[H\033[J"
70 /* Maximum number of lines in a file */
71 #define MAXLINES 10000
73 /* Get height and width of terminal */
74 #define tty_width_height() get_terminal_width_height(0, &width, &height)
79 static char filename[256];
82 static int current_file = 1;
84 static int num_flines;
85 static int num_files = 1;
88 /* Command line options */
89 static unsigned long flags;
94 #define FLAG_TILDE (1<<4)
96 /* This is needed so that program behaviour changes when input comes from
100 #ifdef CONFIG_FEATURE_LESS_MARKS
101 static int mark_lines[15][2];
102 static int num_marks;
105 #ifdef CONFIG_FEATURE_LESS_REGEXP
106 static int match_found;
107 static int *match_lines;
108 static int match_pos;
109 static int num_matches;
110 static int match_backwards;
111 static regex_t old_pattern;
114 /* Needed termios structures */
115 static struct termios term_orig, term_vi;
117 /* File pointer to get input from */
120 /* Reset terminal input to normal */
121 static void set_tty_cooked(void)
124 tcsetattr(fileno(inp), TCSANOW, &term_orig);
127 /* Set terminal input to raw mode (taken from vi.c) */
128 static void set_tty_raw(void)
130 tcsetattr(fileno(inp), TCSANOW, &term_vi);
133 /* Exit the program gracefully */
134 static void tless_exit(int code)
136 /* TODO: We really should save the terminal state when we start,
137 and restore it when we exit. Less does this with the
138 "ti" and "te" termcap commands; can this be done with
145 /* Grab a character from input without requiring the return key. If the
146 character is ASCII \033, get more characters and assign certain sequences
147 special return codes. Note that this function works best with raw input. */
148 static int tless_getch(void)
155 /* Detect escape sequences (i.e. arrow keys) and handle
158 if (input == '\033' && getc(inp) == '[') {
161 if (input == REAL_KEY_UP)
163 else if (input == REAL_KEY_DOWN)
165 else if (input == REAL_KEY_RIGHT)
167 else if (input == REAL_KEY_LEFT)
169 else if (input == REAL_PAGE_UP)
171 else if (input == REAL_PAGE_DOWN)
174 /* The input is a normal ASCII value */
182 /* Move the cursor to a position (x,y), where (0,0) is the
183 top-left corner of the console */
184 static void move_cursor(int x, int y)
186 printf("\033[%i;%iH", x, y);
189 static void clear_line(void)
191 move_cursor(height, 0);
195 /* This adds line numbers to every line, as the -N flag necessitates */
196 static void add_linenumbers(void)
198 char current_line[256];
201 for (i = 0; i <= num_flines; i++) {
202 safe_strncpy(current_line, flines[i], 256);
203 flines[i] = bb_xasprintf("%5d %s", i + 1, current_line);
207 static void data_readlines(void)
210 char current_line[256];
213 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "rt");
215 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
216 strcpy(current_line, "");
217 fgets(current_line, 256, fp);
219 bb_xferror(fp, filename);
220 flines = xrealloc(flines, (i+1) * sizeof(char *));
221 flines[i] = bb_xstrdup(current_line);
225 /* Reset variables for a new file */
233 inp = (inp_stdin) ? bb_xfopen(CURRENT_TTY, "r") : stdin;
239 #ifdef CONFIG_FEATURE_LESS_FLAGS
241 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
243 static int calc_percent(void)
245 return ((100 * (line_pos + height - 2) / num_flines) + 1);
248 /* Print a status line if -M was specified */
249 static void m_status_print(void)
256 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);
258 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
262 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
265 if (line_pos == num_flines - height + 2) {
266 printf("(END) %s", NORMAL);
267 if ((num_files > 1) && (current_file != num_files))
268 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
271 percentage = calc_percent();
272 printf("%i%% %s", percentage, NORMAL);
276 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
277 if ((num_files > 1) && (current_file != num_files))
278 printf("- Next: %s", files[current_file]);
279 printf("%s", NORMAL);
283 /* Print a status line if -m was specified */
284 static void medium_status_print(void)
287 percentage = calc_percent();
290 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
291 else if (line_pos == num_flines - height + 2)
292 printf("%s(END)%s", HIGHLIGHT, NORMAL);
294 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
298 /* Print the status line */
299 static void status_print(void)
301 /* Change the status if flags have been set */
302 #ifdef CONFIG_FEATURE_LESS_FLAGS
305 else if (flags & FLAG_m)
306 medium_status_print();
311 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
313 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", current_file, " of ", num_files, ")", NORMAL);
315 else if (line_pos == num_flines - height + 2) {
316 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
317 if ((num_files > 1) && (current_file != num_files))
318 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
323 #ifdef CONFIG_FEATURE_LESS_FLAGS
328 /* Print the buffer */
329 static void buffer_print(void)
334 if (num_flines >= height - 2) {
335 for (i = 0; i < height - 1; i++)
336 printf("%s", buffer[i]);
339 for (i = 1; i < (height - 1 - num_flines); i++)
341 for (i = 0; i < height - 1; i++)
342 printf("%s", buffer[i]);
348 /* Initialise the buffer */
349 static void buffer_init(void)
353 if (buffer == NULL) {
354 /* malloc the number of lines needed for the buffer */
355 buffer = xrealloc(buffer, height * sizeof(char *));
357 for (i = 0; i < (height - 1); i++)
361 /* Fill the buffer until the end of the file or the
362 end of the buffer is reached */
363 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
364 buffer[i] = bb_xstrdup(flines[i]);
367 /* If the buffer still isn't full, fill it with blank lines */
368 for (; i < (height - 1); i++) {
369 buffer[i] = bb_xstrdup("");
373 /* Move the buffer up and down in the file in order to scroll */
374 static void buffer_down(int nlines)
379 if (line_pos + (height - 3) + nlines < num_flines) {
381 for (i = 0; i < (height - 1); i++) {
383 buffer[i] = bb_xstrdup(flines[line_pos + i]);
387 /* As the number of lines requested was too large, we just move
388 to the end of the file */
389 while (line_pos + (height - 3) + 1 < num_flines) {
391 for (i = 0; i < (height - 1); i++) {
393 buffer[i] = bb_xstrdup(flines[line_pos + i]);
398 /* We exit if the -E flag has been set */
399 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
404 static void buffer_up(int nlines)
410 if (line_pos - nlines >= 0) {
412 for (i = 0; i < (height - 1); i++) {
414 buffer[i] = bb_xstrdup(flines[line_pos + i]);
418 /* As the requested number of lines to move was too large, we
419 move one line up at a time until we can't. */
420 while (line_pos != 0) {
422 for (i = 0; i < (height - 1); i++) {
424 buffer[i] = bb_xstrdup(flines[line_pos + i]);
430 /* Work out where the tildes start */
431 tilde_line = num_flines - line_pos + 3;
434 /* Going backwards nlines lines has taken us to a point where
435 nothing is past the EOF, so we revert to normal. */
436 if (line_pos < num_flines - height + 3) {
441 /* We only move part of the buffer, as the rest
443 for (i = 0; i < (height - 1); i++) {
445 if (i < tilde_line - nlines + 1)
446 buffer[i] = bb_xstrdup(flines[line_pos + i]);
448 if (line_pos >= num_flines - height + 2)
449 buffer[i] = bb_xstrdup("~\n");
456 static void buffer_line(int linenum)
461 if (linenum < 0 || linenum > num_flines) {
463 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum + 1, NORMAL);
465 else if (linenum < (num_flines - height - 2)) {
466 for (i = 0; i < (height - 1); i++) {
468 buffer[i] = bb_xstrdup(flines[linenum + i]);
474 for (i = 0; i < (height - 1); i++) {
476 if (linenum + i < num_flines + 2)
477 buffer[i] = bb_xstrdup(flines[linenum + i]);
479 buffer[i] = bb_xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
482 /* Set past_eof so buffer_down and buffer_up act differently */
488 /* Reinitialise everything for a new file - free the memory and start over */
489 static void reinitialise(void)
493 for (i = 0; i <= num_flines; i++)
502 static void examine_file(void)
508 fgets(filename, 256, inp);
510 /* As fgets adds a newline to the end of an input string, we
512 newline_offset = strlen(filename) - 1;
513 filename[newline_offset] = '\0';
515 files[num_files] = bb_xstrdup(filename);
516 current_file = num_files + 1;
523 /* This function changes the file currently being paged. direction can be one of the following:
524 * -1: go back one file
525 * 0: go to the first file
526 * 1: go forward one file
528 static void change_file(int direction)
530 if (current_file != ((direction > 0) ? num_files : 1)) {
531 current_file = direction ? current_file + direction : 1;
532 strcpy(filename, files[current_file - 1]);
537 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
541 static void remove_current_file(void)
545 if (current_file != 1) {
547 for (i = 3; i <= num_files; i++)
548 files[i - 2] = files[i - 1];
554 for (i = 2; i <= num_files; i++)
555 files[i - 2] = files[i - 1];
562 static void colon_process(void)
566 /* Clear the current line and print a prompt */
570 keypress = tless_getch();
573 remove_current_file();
578 #ifdef CONFIG_FEATURE_LESS_FLAGS
601 #ifdef CONFIG_FEATURE_LESS_REGEXP
602 /* The below two regular expression handler functions NEED development. */
604 /* Get a regular expression from the user, and then go through the current
605 file line by line, running a processing regex function on each one. */
607 static char *process_regex_on_line(char *line, regex_t *pattern, int action)
609 /* This function takes the regex and applies it to the line.
610 Each part of the line that matches has the HIGHLIGHT
611 and NORMAL escape sequences placed around it by
612 insert_highlights if action = 1, or has the escape sequences
613 removed if action = 0, and then the line is returned. */
615 char *line2 = (char *) xmalloc((sizeof(char) * (strlen(line) + 1)) + 64);
617 regmatch_t match_structs;
619 line2 = bb_xstrdup(line);
622 match_status = regexec(pattern, line2, 1, &match_structs, 0);
624 while (match_status == 0) {
625 if (match_found == 0)
629 growline = bb_xasprintf("%s%.*s%s%.*s%s", growline, match_structs.rm_so, line2, HIGHLIGHT, match_structs.rm_eo - match_structs.rm_so, line2 + match_structs.rm_so, NORMAL);
632 growline = bb_xasprintf("%s%.*s%.*s", growline, match_structs.rm_so - 4, line2, match_structs.rm_eo - match_structs.rm_so, line2 + match_structs.rm_so);
635 line2 += match_structs.rm_eo;
636 match_status = regexec(pattern, line2, 1, &match_structs, REG_NOTBOL);
639 growline = bb_xasprintf("%s%s", growline, line2);
641 return (match_found ? growline : line);
647 static void goto_match(int match)
649 /* This goes to a specific match - all line positions of matches are
650 stored within the match_lines[] array. */
651 if ((match < num_matches) && (match >= 0)) {
652 buffer_line(match_lines[match]);
657 static void regex_process(void)
659 char uncomp_regex[100];
664 /* Get the uncompiled regular expression from the user */
666 putchar((match_backwards) ? '?' : '/');
668 fgets(uncomp_regex, sizeof(uncomp_regex), inp);
670 if (strlen(uncomp_regex) == 1) {
672 goto_match(match_backwards ? match_pos - 1 : match_pos + 1);
677 uncomp_regex[strlen(uncomp_regex) - 1] = '\0';
679 /* Compile the regex and check for errors */
680 xregcomp(&pattern, uncomp_regex, 0);
683 /* Get rid of all the highlights we added previously */
684 for (i = 0; i <= num_flines; i++) {
685 current_line = process_regex_on_line(flines[i], &old_pattern, 0);
686 flines[i] = bb_xstrdup(current_line);
689 old_pattern = pattern;
691 /* Reset variables */
692 match_lines = xrealloc(match_lines, sizeof(int));
697 /* Run the regex on each line of the current file here */
698 for (i = 0; i <= num_flines; i++) {
699 current_line = process_regex_on_line(flines[i], &pattern, 1);
700 flines[i] = bb_xstrdup(current_line);
702 match_lines = xrealloc(match_lines, (j + 1) * sizeof(int));
709 if ((match_lines[0] != -1) && (num_flines > height - 2)) {
710 if (match_backwards) {
711 for (i = 0; i < num_matches; i++) {
712 if (match_lines[i] > line_pos) {
714 buffer_line(match_lines[match_pos]);
720 buffer_line(match_lines[0]);
727 static void number_process(int first_digit)
735 num_input[0] = first_digit;
737 /* Clear the current line, print a prompt, and then print the digit */
739 printf(":%c", first_digit);
741 /* Receive input until a letter is given (max 80 chars)*/
742 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
743 putchar(num_input[i]);
747 /* Take the final letter out of the digits string */
748 keypress = num_input[i];
750 num = strtol(num_input, &endptr, 10);
751 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES) {
756 /* We now know the number and the letter entered, so we process them */
758 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
761 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
764 case 'g': case '<': case 'G': case '>':
765 if (num_flines >= height - 2)
766 buffer_line(num - 1);
769 buffer_line(((num / 100) * num_flines) - 1);
771 #ifdef CONFIG_FEATURE_LESS_REGEXP
773 goto_match(match_pos + num);
789 #ifdef CONFIG_FEATURE_LESS_FLAGCS
790 static void flag_change(void)
796 keypress = tless_getch();
816 static void show_flag_status(void)
823 keypress = tless_getch();
827 flag_val = flags & FLAG_M;
830 flag_val = flags & FLAG_m;
833 flag_val = flags & FLAG_TILDE;
836 flag_val = flags & FLAG_N;
839 flag_val = flags & FLAG_E;
847 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
851 static void full_repaint(void)
853 int temp_line_pos = line_pos;
856 buffer_line(temp_line_pos);
860 static void save_input_to_file(void)
862 char current_line[256];
867 printf("Log file: ");
868 fgets(current_line, 256, inp);
869 current_line[strlen(current_line) - 1] = '\0';
870 if (strlen(current_line) > 1) {
871 fp = bb_xfopen(current_line, "w");
872 for (i = 0; i < num_flines; i++)
873 fprintf(fp, "%s", flines[i]);
878 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
881 #ifdef CONFIG_FEATURE_LESS_MARKS
882 static void add_mark(void)
889 letter = tless_getch();
891 if (isalpha(letter)) {
892 mark_line = line_pos;
894 /* If we exceed 15 marks, start overwriting previous ones */
898 mark_lines[num_marks][0] = letter;
899 mark_lines[num_marks][1] = line_pos;
904 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
908 static void goto_mark(void)
914 printf("Go to mark: ");
915 letter = tless_getch();
918 if (isalpha(letter)) {
919 for (i = 0; i <= num_marks; i++)
920 if (letter == mark_lines[i][0]) {
921 buffer_line(mark_lines[i][1]);
924 if ((num_marks == 14) && (letter != mark_lines[14][0]))
925 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
928 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
933 #ifdef CONFIG_FEATURE_LESS_BRACKETS
935 static char opp_bracket(char bracket)
956 static void match_right_bracket(char bracket)
958 int bracket_line = -1;
963 if (strchr(flines[line_pos], bracket) == NULL)
964 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
966 for (i = line_pos + 1; i < num_flines; i++) {
967 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
973 if (bracket_line == -1)
974 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
976 buffer_line(bracket_line - height + 2);
980 static void match_left_bracket(char bracket)
982 int bracket_line = -1;
987 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
988 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
989 printf("%s", flines[line_pos + height]);
993 for (i = line_pos + height - 2; i >= 0; i--) {
994 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1000 if (bracket_line == -1)
1001 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1003 buffer_line(bracket_line);
1007 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1009 static void keypress_process(int keypress)
1012 case KEY_DOWN: case 'e': case 'j': case '\015':
1016 case KEY_UP: case 'y': case 'k':
1020 case PAGE_DOWN: case ' ': case 'z':
1021 buffer_down(height - 1);
1024 case PAGE_UP: case 'w': case 'b':
1025 buffer_up(height - 1);
1029 buffer_down((height - 1) / 2);
1033 buffer_up((height - 1) / 2);
1036 case 'g': case 'p': case '<': case '%':
1040 buffer_line(num_flines - height + 2);
1045 #ifdef CONFIG_FEATURE_LESS_MARKS
1063 save_input_to_file();
1068 #ifdef CONFIG_FEATURE_LESS_FLAGS
1074 #ifdef CONFIG_FEATURE_LESS_REGEXP
1076 match_backwards = 0;
1080 goto_match(match_pos + 1);
1083 goto_match(match_pos - 1);
1086 match_backwards = 1;
1090 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1099 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1100 case '{': case '(': case '[':
1101 match_right_bracket(keypress);
1103 case '}': case ')': case ']':
1104 match_left_bracket(keypress);
1114 if (isdigit(keypress))
1115 number_process(keypress);
1118 int less_main(int argc, char **argv) {
1122 flags = bb_getopt_ulflags(argc, argv, "EMmN~");
1130 if (ttyname(STDIN_FILENO) == NULL)
1133 bb_error_msg("Missing filename");
1138 strcpy(filename, (inp_stdin) ? bb_msg_standard_input : files[0]);
1141 tcgetattr(fileno(inp), &term_orig);
1142 term_vi = term_orig;
1143 term_vi.c_lflag &= (~ICANON & ~ECHO);
1144 term_vi.c_iflag &= (~IXON & ~ICRNL);
1145 term_vi.c_oflag &= (~ONLCR);
1146 term_vi.c_cc[VMIN] = 1;
1147 term_vi.c_cc[VTIME] = 0;
1152 keypress = tless_getch();
1153 keypress_process(keypress);