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;
112 #ifdef CONFIG_FEATURE_LESS_MARKS
113 static int mark_lines[15][2];
114 static int num_marks;
117 #ifdef CONFIG_FEATURE_LESS_REGEXP
118 static int match_found;
119 static int match_lines[100];
120 static int match_pos;
121 static int num_matches;
122 static int match_backwards;
123 static int num_back_match = 1;
126 /* Needed termios structures */
127 static struct termios term_orig, term_vi;
129 /* File pointer to get input from */
132 /* Reset terminal input to normal */
133 static void set_tty_cooked(void)
136 tcsetattr(fileno(inp), TCSANOW, &term_orig);
139 /* Set terminal input to raw mode (taken from vi.c) */
140 static void set_tty_raw(void)
142 tcsetattr(fileno(inp), TCSANOW, &term_vi);
145 /* Exit the program gracefully */
146 static void tless_exit(int code)
148 /* TODO: We really should save the terminal state when we start,
149 and restore it when we exit. Less does this with the
150 "ti" and "te" termcap commands; can this be done with
157 /* Grab a character from input without requiring the return key. If the
158 character is ASCII \033, get more characters and assign certain sequences
159 special return codes. Note that this function works best with raw input. */
160 static int tless_getch(void)
167 /* Detect escape sequences (i.e. arrow keys) and handle
170 if (input == '\033' && getc(inp) == '[') {
173 if (input == REAL_KEY_UP)
175 else if (input == REAL_KEY_DOWN)
177 else if (input == REAL_KEY_RIGHT)
179 else if (input == REAL_KEY_LEFT)
181 else if (input == REAL_PAGE_UP)
183 else if (input == REAL_PAGE_DOWN)
186 /* The input is a normal ASCII value */
194 /* Move the cursor to a position (x,y), where (0,0) is the
195 top-left corner of the console */
196 static void move_cursor(int x, int y)
198 printf("\033[%i;%iH", x, y);
201 static void clear_line(void)
203 move_cursor(height, 0);
207 /* This adds line numbers to every line, as the -N flag necessitates */
208 static void add_linenumbers(void)
210 char current_line[256];
213 for (i = 0; i <= num_flines; i++) {
214 safe_strncpy(current_line, flines[i], 256);
215 flines[i] = bb_xasprintf("%5d %s", i + 1, current_line);
219 static void data_readlines(void)
222 char current_line[256];
225 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "rt");
227 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
228 strcpy(current_line, "");
229 fgets(current_line, 256, fp);
231 bb_xferror(fp, filename);
232 flines = xrealloc(flines, (i+1) * sizeof(char *));
233 flines[i] = bb_xstrdup(current_line);
237 /* Reset variables for a new file */
245 inp = (inp_stdin) ? bb_xfopen(CURRENT_TTY, "r") : stdin;
251 /* Turn a percentage into a line number */
252 static int reverse_percent(int percentage)
254 double linenum = percentage;
255 linenum = ((linenum / 100) * num_flines) - 1;
259 #ifdef CONFIG_FEATURE_LESS_FLAGS
261 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
263 static int calc_percent(void)
265 return ((100 * (line_pos + height - 2) / num_flines) + 1);
268 /* Print a status line if -M was specified */
269 static void m_status_print(void)
276 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);
278 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
282 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
285 if (line_pos == num_flines - height + 2) {
286 printf("(END) %s", NORMAL);
287 if ((num_files > 1) && (current_file != num_files))
288 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
291 percentage = calc_percent();
292 printf("%i%% %s", percentage, NORMAL);
296 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
297 if ((num_files > 1) && (current_file != num_files))
298 printf("- Next: %s", files[current_file]);
299 printf("%s", NORMAL);
303 /* Print a status line if -m was specified */
304 static void medium_status_print(void)
307 percentage = calc_percent();
310 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
311 else if (line_pos == num_flines - height + 2)
312 printf("%s(END)%s", HIGHLIGHT, NORMAL);
314 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
318 /* Print the status line */
319 static void status_print(void)
321 /* Change the status if flags have been set */
322 #ifdef CONFIG_FEATURE_LESS_FLAGS
325 else if (flags & FLAG_m)
326 medium_status_print();
331 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
333 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", current_file, " of ", num_files, ")", NORMAL);
335 else if (line_pos == num_flines - height + 2) {
336 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
337 if ((num_files > 1) && (current_file != num_files))
338 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
343 #ifdef CONFIG_FEATURE_LESS_FLAGS
348 /* Print the buffer */
349 static void buffer_print(void)
354 if (num_flines >= height - 2) {
355 for (i = 0; i < height - 1; i++)
356 printf("%s", buffer[i]);
359 for (i = 1; i < (height - 1 - num_flines); i++)
361 for (i = 0; i < height - 1; i++)
362 printf("%s", buffer[i]);
368 /* Initialise the buffer */
369 static void buffer_init(void)
374 /* malloc the number of lines needed for the buffer */
375 buffer = xrealloc(buffer, height * sizeof(char *));
377 for (i = 0; i < (height - 1); i++)
381 /* Fill the buffer until the end of the file or the
382 end of the buffer is reached */
383 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
384 buffer[i] = bb_xstrdup(flines[i]);
387 /* If the buffer still isn't full, fill it with blank lines */
388 for (; i < (height - 1); i++) {
389 buffer[i] = bb_xstrdup("");
393 /* Move the buffer up and down in the file in order to scroll */
394 static void buffer_down(int nlines)
399 if (line_pos + (height - 3) + nlines < num_flines) {
401 for (i = 0; i < (height - 1); i++) {
403 buffer[i] = bb_xstrdup(flines[line_pos + i]);
407 /* As the number of lines requested was too large, we just move
408 to the end of the file */
409 while (line_pos + (height - 3) + 1 < num_flines) {
411 for (i = 0; i < (height - 1); i++) {
413 buffer[i] = bb_xstrdup(flines[line_pos + i]);
418 /* We exit if the -E flag has been set */
419 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
424 static void buffer_up(int nlines)
430 if (line_pos - nlines >= 0) {
432 for (i = 0; i < (height - 1); i++) {
434 buffer[i] = bb_xstrdup(flines[line_pos + i]);
438 /* As the requested number of lines to move was too large, we
439 move one line up at a time until we can't. */
440 while (line_pos != 0) {
442 for (i = 0; i < (height - 1); i++) {
444 buffer[i] = bb_xstrdup(flines[line_pos + i]);
450 /* Work out where the tildes start */
451 tilde_line = num_flines - line_pos + 3;
454 /* Going backwards nlines lines has taken us to a point where
455 nothing is past the EOF, so we revert to normal. */
456 if (line_pos < num_flines - height + 3) {
461 /* We only move part of the buffer, as the rest
463 for (i = 0; i < (height - 1); i++) {
465 if (i < tilde_line - nlines + 1)
466 buffer[i] = bb_xstrdup(flines[line_pos + i]);
468 if (line_pos >= num_flines - height + 2)
469 buffer[i] = bb_xstrdup("~\n");
476 static void buffer_line(int linenum)
482 if (linenum < 1 || linenum > num_flines) {
484 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum, NORMAL);
486 else if (linenum < (num_flines - height - 2)) {
487 for (i = 0; i < (height - 1); i++) {
489 buffer[i] = bb_xstrdup(flines[linenum + i]);
494 for (i = 0; i < (height - 1); i++) {
496 if (linenum + i < num_flines + 2)
497 buffer[i] = bb_xstrdup(flines[linenum + i]);
499 buffer[i] = bb_xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
502 /* Set past_eof so buffer_down and buffer_up act differently */
507 /* Reinitialise everything for a new file - free the memory and start over */
508 static void reinitialise(void)
512 for (i = 0; i <= num_flines; i++)
521 static void examine_file(void)
527 fgets(filename, 256, inp);
529 /* As fgets adds a newline to the end of an input string, we
531 newline_offset = strlen(filename) - 1;
532 filename[newline_offset] = '\0';
534 files[num_files] = bb_xstrdup(filename);
535 current_file = num_files + 1;
542 /* This function changes the file currently being paged. direction can be one of the following:
543 * -1: go back one file
544 * 0: go to the first file
545 * 1: go forward one file
547 static void change_file(int direction)
549 if (current_file != ((direction > 0) ? num_files : 1)) {
550 current_file = direction ? current_file + direction : 1;
551 strcpy(filename, files[current_file - 1]);
556 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
560 static void remove_current_file(void)
564 if (current_file != 1) {
566 for (i = 3; i <= num_files; i++)
567 files[i - 2] = files[i - 1];
573 for (i = 2; i <= num_files; i++)
574 files[i - 2] = files[i - 1];
581 static void colon_process(void)
585 /* Clear the current line and print a prompt */
589 keypress = tless_getch();
592 remove_current_file();
597 #ifdef CONFIG_FEATURE_LESS_FLAGS
620 #ifdef CONFIG_FEATURE_LESS_REGEXP
621 /* The below two regular expression handler functions NEED development. */
623 /* Get a regular expression from the user, and then go through the current
624 file line by line, running a processing regex function on each one. */
626 static char *insert_highlights(char *line, int start, int end)
628 return bb_xasprintf("%.*s%s%.*s%s%s", start, line, HIGHLIGHT,
629 end - start, line + start, NORMAL, line + end);
632 static char *process_regex_on_line(char *line, regex_t *pattern)
634 /* This function takes the regex and applies it to the line.
635 Each part of the line that matches has the HIGHLIGHT
636 and NORMAL escape sequences placed around it by
637 insert_highlights, and then the line is returned. */
640 char *line2 = (char *) malloc((sizeof(char) * (strlen(line) + 1)) + 64);
643 regmatch_t match_structs;
645 memset(sub_line, 0, 256);
649 match_status = regexec(pattern, line2, 1, &match_structs, 0);
651 while (match_status == 0) {
653 memset(sub_line, 0, 256);
655 if (match_found == 0)
658 line2 = insert_highlights(line2, match_structs.rm_so + prev_eo, match_structs.rm_eo + prev_eo);
659 if ((size_t)match_structs.rm_eo + 11 + prev_eo < strlen(line2))
660 strcat(sub_line, line2 + match_structs.rm_eo + 11 + prev_eo);
662 prev_eo += match_structs.rm_eo + 11;
663 match_status = regexec(pattern, sub_line, 1, &match_structs, REG_NOTBOL);
669 static void regex_process(void)
671 char uncomp_regex[100];
672 char current_line[256];
677 /* Reset variables */
683 pattern = (regex_t *) malloc(sizeof(regex_t));
684 memset(pattern, 0, sizeof(regex_t));
686 /* Get the uncompiled regular expression from the user */
688 putchar((match_backwards) ? '?' : '/');
690 fgets(uncomp_regex, sizeof(uncomp_regex), stdin);
691 i = strlen(uncomp_regex);
693 if(uncomp_regex[i-1] == '\n')
694 uncomp_regex[i-1] = '\0';
696 while((i = getchar()) != '\n' && i != EOF);
699 /* Compile the regex and check for errors */
700 xregcomp(pattern, uncomp_regex, 0);
702 /* Run the regex on each line of the current file here */
703 for (i = 0; i <= num_flines; i++) {
704 strcpy(current_line, process_regex_on_line(flines[i], pattern));
705 flines[i] = bb_xstrdup(current_line);
713 if ((match_lines[0] != -1) && (num_flines > height - 2))
714 buffer_line(match_lines[0]);
719 static void goto_match(int match)
721 /* This goes to a specific match - all line positions of matches are
722 stored within the match_lines[] array. */
723 if ((match < num_matches) && (match >= 0)) {
724 buffer_line(match_lines[match]);
729 static void search_backwards(void)
731 int current_linepos = line_pos;
737 for (i = 0; i < num_matches; i++) {
738 if (match_lines[i] > current_linepos) {
739 buffer_line(match_lines[i - num_back_match]);
744 /* Reset variables */
751 static void number_process(int first_digit)
759 num_input[0] = first_digit;
761 /* Clear the current line, print a prompt, and then print the digit */
763 printf(":%c", first_digit);
765 /* Receive input until a letter is given (max 80 chars)*/
766 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
767 putchar(num_input[i]);
771 /* Take the final letter out of the digits string */
772 keypress = num_input[i];
774 num = strtol(num_input, &endptr, 10);
775 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES)
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();
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)
1039 case KEY_DOWN: case 'e': case 'j': case '\015':
1043 case KEY_UP: case 'y': case 'k':
1047 case PAGE_DOWN: case ' ': case 'z':
1048 buffer_down(height - 1);
1051 case PAGE_UP: case 'w': case 'b':
1052 buffer_up(height - 1);
1056 buffer_down((height - 1) / 2);
1060 buffer_up((height - 1) / 2);
1063 case 'g': case 'p': case '<': case '%':
1064 buffer_up(num_flines + 1);
1068 buffer_down(num_flines + 1);
1074 #ifdef CONFIG_FEATURE_LESS_MARKS
1092 save_input_to_file();
1097 #ifdef CONFIG_FEATURE_LESS_FLAGS
1103 #ifdef CONFIG_FEATURE_LESS_REGEXP
1109 goto_match(match_pos + 1);
1113 goto_match(match_pos - 1);
1121 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1130 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1131 case '{': case '(': case '[':
1132 match_right_bracket(keypress);
1134 case '}': case ')': case ']':
1135 match_left_bracket(keypress);
1145 if (isdigit(keypress))
1146 number_process(keypress);
1149 int less_main(int argc, char **argv) {
1153 flags = bb_getopt_ulflags(argc, argv, "EMmN~");
1161 if (ttyname(STDIN_FILENO) == NULL)
1164 bb_error_msg("Missing filename");
1169 strcpy(filename, (inp_stdin) ? bb_msg_standard_input : files[0]);
1172 tcgetattr(fileno(inp), &term_orig);
1173 term_vi = term_orig;
1174 term_vi.c_lflag &= (~ICANON & ~ECHO);
1175 term_vi.c_iflag &= (~IXON & ~ICRNL);
1176 term_vi.c_oflag &= (~ONLCR);
1177 term_vi.c_cc[VMIN] = 1;
1178 term_vi.c_cc[VTIME] = 0;
1183 keypress = tless_getch();
1184 keypress_process(keypress);