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[100];
108 static int match_pos;
109 static int num_matches;
110 static int match_backwards;
113 /* Needed termios structures */
114 static struct termios term_orig, term_vi;
116 /* File pointer to get input from */
119 /* Reset terminal input to normal */
120 static void set_tty_cooked(void)
123 tcsetattr(fileno(inp), TCSANOW, &term_orig);
126 /* Set terminal input to raw mode (taken from vi.c) */
127 static void set_tty_raw(void)
129 tcsetattr(fileno(inp), TCSANOW, &term_vi);
132 /* Exit the program gracefully */
133 static void tless_exit(int code)
135 /* TODO: We really should save the terminal state when we start,
136 and restore it when we exit. Less does this with the
137 "ti" and "te" termcap commands; can this be done with
144 /* Grab a character from input without requiring the return key. If the
145 character is ASCII \033, get more characters and assign certain sequences
146 special return codes. Note that this function works best with raw input. */
147 static int tless_getch(void)
154 /* Detect escape sequences (i.e. arrow keys) and handle
157 if (input == '\033' && getc(inp) == '[') {
160 if (input == REAL_KEY_UP)
162 else if (input == REAL_KEY_DOWN)
164 else if (input == REAL_KEY_RIGHT)
166 else if (input == REAL_KEY_LEFT)
168 else if (input == REAL_PAGE_UP)
170 else if (input == REAL_PAGE_DOWN)
173 /* The input is a normal ASCII value */
181 /* Move the cursor to a position (x,y), where (0,0) is the
182 top-left corner of the console */
183 static void move_cursor(int x, int y)
185 printf("\033[%i;%iH", x, y);
188 static void clear_line(void)
190 move_cursor(height, 0);
194 /* This adds line numbers to every line, as the -N flag necessitates */
195 static void add_linenumbers(void)
197 char current_line[256];
200 for (i = 0; i <= num_flines; i++) {
201 safe_strncpy(current_line, flines[i], 256);
202 flines[i] = bb_xasprintf("%5d %s", i + 1, current_line);
206 static void data_readlines(void)
209 char current_line[256];
212 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "rt");
214 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
215 strcpy(current_line, "");
216 fgets(current_line, 256, fp);
218 bb_xferror(fp, filename);
219 flines = xrealloc(flines, (i+1) * sizeof(char *));
220 flines[i] = bb_xstrdup(current_line);
224 /* Reset variables for a new file */
232 inp = (inp_stdin) ? bb_xfopen(CURRENT_TTY, "r") : stdin;
238 /* Turn a percentage into a line number */
239 static int reverse_percent(int percentage)
241 double linenum = percentage;
242 linenum = ((linenum / 100) * num_flines) - 1;
246 #ifdef CONFIG_FEATURE_LESS_FLAGS
248 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
250 static int calc_percent(void)
252 return ((100 * (line_pos + height - 2) / num_flines) + 1);
255 /* Print a status line if -M was specified */
256 static void m_status_print(void)
263 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);
265 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
269 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
272 if (line_pos == num_flines - height + 2) {
273 printf("(END) %s", NORMAL);
274 if ((num_files > 1) && (current_file != num_files))
275 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
278 percentage = calc_percent();
279 printf("%i%% %s", percentage, NORMAL);
283 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
284 if ((num_files > 1) && (current_file != num_files))
285 printf("- Next: %s", files[current_file]);
286 printf("%s", NORMAL);
290 /* Print a status line if -m was specified */
291 static void medium_status_print(void)
294 percentage = calc_percent();
297 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
298 else if (line_pos == num_flines - height + 2)
299 printf("%s(END)%s", HIGHLIGHT, NORMAL);
301 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
305 /* Print the status line */
306 static void status_print(void)
308 /* Change the status if flags have been set */
309 #ifdef CONFIG_FEATURE_LESS_FLAGS
312 else if (flags & FLAG_m)
313 medium_status_print();
318 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
320 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", current_file, " of ", num_files, ")", NORMAL);
322 else if (line_pos == num_flines - height + 2) {
323 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
324 if ((num_files > 1) && (current_file != num_files))
325 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
330 #ifdef CONFIG_FEATURE_LESS_FLAGS
335 /* Print the buffer */
336 static void buffer_print(void)
341 if (num_flines >= height - 2) {
342 for (i = 0; i < height - 1; i++)
343 printf("%s", buffer[i]);
346 for (i = 1; i < (height - 1 - num_flines); i++)
348 for (i = 0; i < height - 1; i++)
349 printf("%s", buffer[i]);
355 /* Initialise the buffer */
356 static void buffer_init(void)
360 if (buffer == NULL) {
361 /* malloc the number of lines needed for the buffer */
362 buffer = xrealloc(buffer, height * sizeof(char *));
364 for (i = 0; i < (height - 1); i++)
368 /* Fill the buffer until the end of the file or the
369 end of the buffer is reached */
370 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
371 buffer[i] = bb_xstrdup(flines[i]);
374 /* If the buffer still isn't full, fill it with blank lines */
375 for (; i < (height - 1); i++) {
376 buffer[i] = bb_xstrdup("");
380 /* Move the buffer up and down in the file in order to scroll */
381 static void buffer_down(int nlines)
386 if (line_pos + (height - 3) + nlines < num_flines) {
388 for (i = 0; i < (height - 1); i++) {
390 buffer[i] = bb_xstrdup(flines[line_pos + i]);
394 /* As the number of lines requested was too large, we just move
395 to the end of the file */
396 while (line_pos + (height - 3) + 1 < num_flines) {
398 for (i = 0; i < (height - 1); i++) {
400 buffer[i] = bb_xstrdup(flines[line_pos + i]);
405 /* We exit if the -E flag has been set */
406 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
411 static void buffer_up(int nlines)
417 if (line_pos - nlines >= 0) {
419 for (i = 0; i < (height - 1); i++) {
421 buffer[i] = bb_xstrdup(flines[line_pos + i]);
425 /* As the requested number of lines to move was too large, we
426 move one line up at a time until we can't. */
427 while (line_pos != 0) {
429 for (i = 0; i < (height - 1); i++) {
431 buffer[i] = bb_xstrdup(flines[line_pos + i]);
437 /* Work out where the tildes start */
438 tilde_line = num_flines - line_pos + 3;
441 /* Going backwards nlines lines has taken us to a point where
442 nothing is past the EOF, so we revert to normal. */
443 if (line_pos < num_flines - height + 3) {
448 /* We only move part of the buffer, as the rest
450 for (i = 0; i < (height - 1); i++) {
452 if (i < tilde_line - nlines + 1)
453 buffer[i] = bb_xstrdup(flines[line_pos + i]);
455 if (line_pos >= num_flines - height + 2)
456 buffer[i] = bb_xstrdup("~\n");
463 static void buffer_line(int linenum)
469 if (linenum < 1 || linenum > num_flines) {
471 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum, NORMAL);
473 else if (linenum < (num_flines - height - 2)) {
474 for (i = 0; i < (height - 1); i++) {
476 buffer[i] = bb_xstrdup(flines[linenum + i]);
481 for (i = 0; i < (height - 1); i++) {
483 if (linenum + i < num_flines + 2)
484 buffer[i] = bb_xstrdup(flines[linenum + i]);
486 buffer[i] = bb_xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
489 /* Set past_eof so buffer_down and buffer_up act differently */
494 /* Reinitialise everything for a new file - free the memory and start over */
495 static void reinitialise(void)
499 for (i = 0; i <= num_flines; i++)
508 static void examine_file(void)
514 fgets(filename, 256, inp);
516 /* As fgets adds a newline to the end of an input string, we
518 newline_offset = strlen(filename) - 1;
519 filename[newline_offset] = '\0';
521 files[num_files] = bb_xstrdup(filename);
522 current_file = num_files + 1;
529 /* This function changes the file currently being paged. direction can be one of the following:
530 * -1: go back one file
531 * 0: go to the first file
532 * 1: go forward one file
534 static void change_file(int direction)
536 if (current_file != ((direction > 0) ? num_files : 1)) {
537 current_file = direction ? current_file + direction : 1;
538 strcpy(filename, files[current_file - 1]);
543 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
547 static void remove_current_file(void)
551 if (current_file != 1) {
553 for (i = 3; i <= num_files; i++)
554 files[i - 2] = files[i - 1];
560 for (i = 2; i <= num_files; i++)
561 files[i - 2] = files[i - 1];
568 static void colon_process(void)
572 /* Clear the current line and print a prompt */
576 keypress = tless_getch();
579 remove_current_file();
584 #ifdef CONFIG_FEATURE_LESS_FLAGS
607 #ifdef CONFIG_FEATURE_LESS_REGEXP
608 /* The below two regular expression handler functions NEED development. */
610 /* Get a regular expression from the user, and then go through the current
611 file line by line, running a processing regex function on each one. */
613 static char *insert_highlights(char *line, int start, int end)
615 return bb_xasprintf("%.*s%s%.*s%s%s", start, line, HIGHLIGHT,
616 end - start, line + start, NORMAL, line + end);
619 static char *process_regex_on_line(char *line, regex_t *pattern)
621 /* This function takes the regex and applies it to the line.
622 Each part of the line that matches has the HIGHLIGHT
623 and NORMAL escape sequences placed around it by
624 insert_highlights, and then the line is returned. */
627 char *line2 = (char *) xmalloc((sizeof(char) * (strlen(line) + 1)) + 64);
630 regmatch_t match_structs;
635 match_status = regexec(pattern, line2, 1, &match_structs, 0);
637 while (match_status == 0) {
639 memset(sub_line, 0, sizeof(sub_line));
641 if (match_found == 0)
644 line2 = insert_highlights(line2, match_structs.rm_so + prev_eo, match_structs.rm_eo + prev_eo);
645 if ((size_t)match_structs.rm_eo + 11 + prev_eo < strlen(line2))
646 strcat(sub_line, line2 + match_structs.rm_eo + 11 + prev_eo);
648 prev_eo += match_structs.rm_eo + 11;
649 match_status = regexec(pattern, sub_line, 1, &match_structs, REG_NOTBOL);
655 static void goto_match(int match)
657 /* This goes to a specific match - all line positions of matches are
658 stored within the match_lines[] array. */
659 if ((match < num_matches) && (match >= 0)) {
660 buffer_line(match_lines[match]);
665 static void regex_process(void)
667 char uncomp_regex[100];
668 char current_line[256];
673 /* Get the uncompiled regular expression from the user */
675 putchar((match_backwards) ? '?' : '/');
677 fgets(uncomp_regex, sizeof(uncomp_regex), inp);
679 if (strlen(uncomp_regex) == 1) {
680 goto_match(match_backwards ? match_pos - 1 : match_pos + 1);
685 uncomp_regex[strlen(uncomp_regex) - 1] = '\0';
687 /* Compile the regex and check for errors */
688 xregcomp(&pattern, uncomp_regex, 0);
690 /* Reset variables */
696 /* Run the regex on each line of the current file here */
697 for (i = 0; i <= num_flines; i++) {
698 strcpy(current_line, process_regex_on_line(flines[i], &pattern));
699 flines[i] = bb_xstrdup(current_line);
707 if ((match_lines[0] != -1) && (num_flines > height - 2)) {
708 if (match_backwards) {
709 for (i = 0; i < num_matches; i++) {
710 if (match_lines[i] > line_pos) {
712 buffer_line(match_lines[match_pos]);
718 buffer_line(match_lines[0]);
725 static void number_process(int first_digit)
733 num_input[0] = first_digit;
735 /* Clear the current line, print a prompt, and then print the digit */
737 printf(":%c", first_digit);
739 /* Receive input until a letter is given (max 80 chars)*/
740 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
741 putchar(num_input[i]);
745 /* Take the final letter out of the digits string */
746 keypress = num_input[i];
748 num = strtol(num_input, &endptr, 10);
749 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES) {
754 /* We now know the number and the letter entered, so we process them */
756 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
759 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
762 case 'g': case '<': case 'G': case '>':
763 if (num_flines >= height - 2)
764 buffer_line(num - 1);
767 buffer_line(reverse_percent(num));
769 #ifdef CONFIG_FEATURE_LESS_REGEXP
771 goto_match(match_pos + num);
787 #ifdef CONFIG_FEATURE_LESS_FLAGCS
788 static void flag_change(void)
794 keypress = tless_getch();
814 static void show_flag_status(void)
821 keypress = tless_getch();
825 flag_val = flags & FLAG_M;
828 flag_val = flags & FLAG_m;
831 flag_val = flags & FLAG_TILDE;
834 flag_val = flags & FLAG_N;
837 flag_val = flags & FLAG_E;
845 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
849 static void full_repaint(void)
851 int temp_line_pos = line_pos;
854 buffer_line(temp_line_pos);
859 static void save_input_to_file(void)
861 char current_line[256];
866 printf("Log file: ");
867 fgets(current_line, 256, inp);
868 current_line[strlen(current_line) - 1] = '\0';
869 if (strlen(current_line)) {
870 fp = bb_xfopen(current_line, "w");
871 for (i = 0; i < num_flines; i++)
872 fprintf(fp, "%s", flines[i]);
877 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
880 #ifdef CONFIG_FEATURE_LESS_MARKS
881 static void add_mark(void)
888 letter = tless_getch();
890 if (isalpha(letter)) {
891 mark_line = line_pos;
893 /* If we exceed 15 marks, start overwriting previous ones */
897 mark_lines[num_marks][0] = letter;
898 mark_lines[num_marks][1] = line_pos;
903 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
907 static void goto_mark(void)
913 printf("Go to mark: ");
914 letter = tless_getch();
917 if (isalpha(letter)) {
918 for (i = 0; i <= num_marks; i++)
919 if (letter == mark_lines[i][0]) {
920 buffer_line(mark_lines[i][1]);
923 if ((num_marks == 14) && (letter != mark_lines[14][0]))
924 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
927 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
932 #ifdef CONFIG_FEATURE_LESS_BRACKETS
934 static char opp_bracket(char bracket)
955 static void match_right_bracket(char bracket)
957 int bracket_line = -1;
962 if (strchr(flines[line_pos], bracket) == NULL)
963 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
965 for (i = line_pos + 1; i < num_flines; i++) {
966 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
972 if (bracket_line == -1)
973 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
975 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);
1008 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1010 static void keypress_process(int keypress)
1013 case KEY_DOWN: case 'e': case 'j': case '\015':
1017 case KEY_UP: case 'y': case 'k':
1021 case PAGE_DOWN: case ' ': case 'z':
1022 buffer_down(height - 1);
1025 case PAGE_UP: case 'w': case 'b':
1026 buffer_up(height - 1);
1030 buffer_down((height - 1) / 2);
1034 buffer_up((height - 1) / 2);
1037 case 'g': case 'p': case '<': case '%':
1038 buffer_up(num_flines + 1);
1042 buffer_down(num_flines + 1);
1048 #ifdef CONFIG_FEATURE_LESS_MARKS
1066 save_input_to_file();
1071 #ifdef CONFIG_FEATURE_LESS_FLAGS
1077 #ifdef CONFIG_FEATURE_LESS_REGEXP
1079 match_backwards = 0;
1084 goto_match(match_pos + 1);
1088 goto_match(match_pos - 1);
1092 match_backwards = 1;
1097 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1106 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1107 case '{': case '(': case '[':
1108 match_right_bracket(keypress);
1110 case '}': case ')': case ']':
1111 match_left_bracket(keypress);
1121 if (isdigit(keypress))
1122 number_process(keypress);
1125 int less_main(int argc, char **argv) {
1129 flags = bb_getopt_ulflags(argc, argv, "EMmN~");
1137 if (ttyname(STDIN_FILENO) == NULL)
1140 bb_error_msg("Missing filename");
1145 strcpy(filename, (inp_stdin) ? bb_msg_standard_input : files[0]);
1148 tcgetattr(fileno(inp), &term_orig);
1149 term_vi = term_orig;
1150 term_vi.c_lflag &= (~ICANON & ~ECHO);
1151 term_vi.c_iflag &= (~IXON & ~ICRNL);
1152 term_vi.c_oflag &= (~ONLCR);
1153 term_vi.c_cc[VMIN] = 1;
1154 term_vi.c_cc[VTIME] = 0;
1159 keypress = tless_getch();
1160 keypress_process(keypress);