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
41 #ifdef CONFIG_FEATURE_LESS_REGEXP
46 /* These are the escape sequences corresponding to special keys */
47 #define REAL_KEY_UP 'A'
48 #define REAL_KEY_DOWN 'B'
49 #define REAL_KEY_RIGHT 'C'
50 #define REAL_KEY_LEFT 'D'
51 #define REAL_PAGE_UP '5'
52 #define REAL_PAGE_DOWN '6'
54 /* These are the special codes assigned by this program to the special keys */
62 /* The escape codes for highlighted and normal text */
63 #define HIGHLIGHT "\033[7m"
64 #define NORMAL "\033[0m"
66 /* The escape code to clear the screen */
67 #define CLEAR "\033[H\033[J"
69 /* Maximum number of lines in a file */
70 #define MAXLINES 10000
75 static char filename[256];
78 static int current_file = 1;
80 static int num_flines;
81 static int num_files = 1;
84 /* Command line options */
85 static unsigned long flags;
90 #define FLAG_TILDE (1<<4)
92 /* This is needed so that program behaviour changes when input comes from
96 #ifdef CONFIG_FEATURE_LESS_MARKS
97 static int mark_lines[15][2];
101 #ifdef CONFIG_FEATURE_LESS_REGEXP
102 static int match_found;
103 static int *match_lines;
104 static int match_pos;
105 static int num_matches;
106 static int match_backwards;
107 static regex_t old_pattern;
110 /* Needed termios structures */
111 static struct termios term_orig, term_vi;
113 /* File pointer to get input from */
116 /* Reset terminal input to normal */
117 static void set_tty_cooked(void)
120 tcsetattr(fileno(inp), TCSANOW, &term_orig);
123 /* Set terminal input to raw mode (taken from vi.c) */
124 static void set_tty_raw(void)
126 tcsetattr(fileno(inp), TCSANOW, &term_vi);
129 /* Exit the program gracefully */
130 static void tless_exit(int code)
132 /* TODO: We really should save the terminal state when we start,
133 and restore it when we exit. Less does this with the
134 "ti" and "te" termcap commands; can this be done with
141 /* Grab a character from input without requiring the return key. If the
142 character is ASCII \033, get more characters and assign certain sequences
143 special return codes. Note that this function works best with raw input. */
144 static int tless_getch(void)
151 /* Detect escape sequences (i.e. arrow keys) and handle
154 if (input == '\033' && getc(inp) == '[') {
157 if (input == REAL_KEY_UP)
159 else if (input == REAL_KEY_DOWN)
161 else if (input == REAL_KEY_RIGHT)
163 else if (input == REAL_KEY_LEFT)
165 else if (input == REAL_PAGE_UP)
167 else if (input == REAL_PAGE_DOWN)
170 /* The input is a normal ASCII value */
178 /* Move the cursor to a position (x,y), where (0,0) is the
179 top-left corner of the console */
180 static void move_cursor(int x, int y)
182 printf("\033[%i;%iH", x, y);
185 static void clear_line(void)
187 move_cursor(height, 0);
191 /* This adds line numbers to every line, as the -N flag necessitates */
192 static void add_linenumbers(void)
194 char current_line[256];
197 for (i = 0; i <= num_flines; i++) {
198 safe_strncpy(current_line, flines[i], 256);
199 flines[i] = bb_xasprintf("%5d %s", i + 1, current_line);
203 static void data_readlines(void)
206 char current_line[256];
209 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "r");
211 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
212 strcpy(current_line, "");
213 fgets(current_line, 256, fp);
215 bb_xferror(fp, filename);
216 flines = xrealloc(flines, (i+1) * sizeof(char *));
217 flines[i] = bb_xstrdup(current_line);
221 /* Reset variables for a new file */
229 inp = (inp_stdin) ? bb_xfopen(CURRENT_TTY, "r") : stdin;
235 #ifdef CONFIG_FEATURE_LESS_FLAGS
237 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
239 static int calc_percent(void)
241 return ((100 * (line_pos + height - 2) / num_flines) + 1);
244 /* Print a status line if -M was specified */
245 static void m_status_print(void)
252 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);
254 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, 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);
261 if (line_pos == num_flines - height + 2) {
262 printf("(END) %s", NORMAL);
263 if ((num_files > 1) && (current_file != num_files))
264 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
267 percentage = calc_percent();
268 printf("%i%% %s", percentage, NORMAL);
272 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, 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 ", current_file, " of ", num_files, ")", NORMAL);
311 else if (line_pos == num_flines - height + 2) {
312 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
313 if ((num_files > 1) && (current_file != num_files))
314 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
319 #ifdef CONFIG_FEATURE_LESS_FLAGS
324 /* Print the buffer */
325 static void buffer_print(void)
330 if (num_flines >= height - 2) {
331 for (i = 0; i < height - 1; i++)
332 printf("%s", buffer[i]);
335 for (i = 1; i < (height - 1 - num_flines); i++)
337 for (i = 0; i < height - 1; i++)
338 printf("%s", buffer[i]);
344 /* Initialise the buffer */
345 static void buffer_init(void)
349 if (buffer == NULL) {
350 /* malloc the number of lines needed for the buffer */
351 buffer = xrealloc(buffer, height * sizeof(char *));
353 for (i = 0; i < (height - 1); i++)
357 /* Fill the buffer until the end of the file or the
358 end of the buffer is reached */
359 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
360 buffer[i] = bb_xstrdup(flines[i]);
363 /* If the buffer still isn't full, fill it with blank lines */
364 for (; i < (height - 1); i++) {
365 buffer[i] = bb_xstrdup("");
369 /* Move the buffer up and down in the file in order to scroll */
370 static void buffer_down(int nlines)
375 if (line_pos + (height - 3) + nlines < num_flines) {
377 for (i = 0; i < (height - 1); i++) {
379 buffer[i] = bb_xstrdup(flines[line_pos + i]);
383 /* As the number of lines requested was too large, we just move
384 to the end of the file */
385 while (line_pos + (height - 3) + 1 < num_flines) {
387 for (i = 0; i < (height - 1); i++) {
389 buffer[i] = bb_xstrdup(flines[line_pos + i]);
394 /* We exit if the -E flag has been set */
395 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
400 static void buffer_up(int nlines)
406 if (line_pos - nlines >= 0) {
408 for (i = 0; i < (height - 1); i++) {
410 buffer[i] = bb_xstrdup(flines[line_pos + i]);
414 /* As the requested number of lines to move was too large, we
415 move one line up at a time until we can't. */
416 while (line_pos != 0) {
418 for (i = 0; i < (height - 1); i++) {
420 buffer[i] = bb_xstrdup(flines[line_pos + i]);
426 /* Work out where the tildes start */
427 tilde_line = num_flines - line_pos + 3;
430 /* Going backwards nlines lines has taken us to a point where
431 nothing is past the EOF, so we revert to normal. */
432 if (line_pos < num_flines - height + 3) {
437 /* We only move part of the buffer, as the rest
439 for (i = 0; i < (height - 1); i++) {
441 if (i < tilde_line - nlines + 1)
442 buffer[i] = bb_xstrdup(flines[line_pos + i]);
444 if (line_pos >= num_flines - height + 2)
445 buffer[i] = bb_xstrdup("~\n");
452 static void buffer_line(int linenum)
457 if (linenum < 0 || linenum > num_flines) {
459 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum + 1, NORMAL);
461 else if (linenum < (num_flines - height - 2)) {
462 for (i = 0; i < (height - 1); i++) {
464 buffer[i] = bb_xstrdup(flines[linenum + i]);
470 for (i = 0; i < (height - 1); i++) {
472 if (linenum + i < num_flines + 2)
473 buffer[i] = bb_xstrdup(flines[linenum + i]);
475 buffer[i] = bb_xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
478 /* Set past_eof so buffer_down and buffer_up act differently */
484 /* Reinitialise everything for a new file - free the memory and start over */
485 static void reinitialise(void)
489 for (i = 0; i <= num_flines; i++)
498 static void examine_file(void)
504 fgets(filename, 256, inp);
506 /* As fgets adds a newline to the end of an input string, we
508 newline_offset = strlen(filename) - 1;
509 filename[newline_offset] = '\0';
511 files[num_files] = bb_xstrdup(filename);
512 current_file = num_files + 1;
519 /* This function changes the file currently being paged. direction can be one of the following:
520 * -1: go back one file
521 * 0: go to the first file
522 * 1: go forward one file
524 static void change_file(int direction)
526 if (current_file != ((direction > 0) ? num_files : 1)) {
527 current_file = direction ? current_file + direction : 1;
528 strcpy(filename, files[current_file - 1]);
533 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
537 static void remove_current_file(void)
541 if (current_file != 1) {
543 for (i = 3; i <= num_files; i++)
544 files[i - 2] = files[i - 1];
550 for (i = 2; i <= num_files; i++)
551 files[i - 2] = files[i - 1];
558 static void colon_process(void)
562 /* Clear the current line and print a prompt */
566 keypress = tless_getch();
569 remove_current_file();
574 #ifdef CONFIG_FEATURE_LESS_FLAGS
597 #ifdef CONFIG_FEATURE_LESS_REGEXP
598 /* The below two regular expression handler functions NEED development. */
600 /* Get a regular expression from the user, and then go through the current
601 file line by line, running a processing regex function on each one. */
603 static char *process_regex_on_line(char *line, regex_t *pattern, int action)
605 /* This function takes the regex and applies it to the line.
606 Each part of the line that matches has the HIGHLIGHT
607 and NORMAL escape sequences placed around it by
608 insert_highlights if action = 1, or has the escape sequences
609 removed if action = 0, and then the line is returned. */
611 char *line2 = (char *) xmalloc((sizeof(char) * (strlen(line) + 1)) + 64);
613 regmatch_t match_structs;
615 line2 = bb_xstrdup(line);
618 match_status = regexec(pattern, line2, 1, &match_structs, 0);
620 while (match_status == 0) {
621 if (match_found == 0)
625 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);
628 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);
631 line2 += match_structs.rm_eo;
632 match_status = regexec(pattern, line2, 1, &match_structs, REG_NOTBOL);
635 growline = bb_xasprintf("%s%s", growline, line2);
637 return (match_found ? growline : line);
643 static void goto_match(int match)
645 /* This goes to a specific match - all line positions of matches are
646 stored within the match_lines[] array. */
647 if ((match < num_matches) && (match >= 0)) {
648 buffer_line(match_lines[match]);
653 static void regex_process(void)
655 char uncomp_regex[100];
660 /* Get the uncompiled regular expression from the user */
662 putchar((match_backwards) ? '?' : '/');
664 fgets(uncomp_regex, sizeof(uncomp_regex), inp);
666 if (strlen(uncomp_regex) == 1) {
668 goto_match(match_backwards ? match_pos - 1 : match_pos + 1);
673 uncomp_regex[strlen(uncomp_regex) - 1] = '\0';
675 /* Compile the regex and check for errors */
676 xregcomp(&pattern, uncomp_regex, 0);
679 /* Get rid of all the highlights we added previously */
680 for (i = 0; i <= num_flines; i++) {
681 current_line = process_regex_on_line(flines[i], &old_pattern, 0);
682 flines[i] = bb_xstrdup(current_line);
685 old_pattern = pattern;
687 /* Reset variables */
688 match_lines = xrealloc(match_lines, sizeof(int));
693 /* Run the regex on each line of the current file here */
694 for (i = 0; i <= num_flines; i++) {
695 current_line = process_regex_on_line(flines[i], &pattern, 1);
696 flines[i] = bb_xstrdup(current_line);
698 match_lines = xrealloc(match_lines, (j + 1) * sizeof(int));
705 if ((match_lines[0] != -1) && (num_flines > height - 2)) {
706 if (match_backwards) {
707 for (i = 0; i < num_matches; i++) {
708 if (match_lines[i] > line_pos) {
710 buffer_line(match_lines[match_pos]);
716 buffer_line(match_lines[0]);
723 static void number_process(int first_digit)
731 num_input[0] = first_digit;
733 /* Clear the current line, print a prompt, and then print the digit */
735 printf(":%c", first_digit);
737 /* Receive input until a letter is given (max 80 chars)*/
738 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
739 putchar(num_input[i]);
743 /* Take the final letter out of the digits string */
744 keypress = num_input[i];
746 num = strtol(num_input, &endptr, 10);
747 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES) {
752 /* We now know the number and the letter entered, so we process them */
754 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
757 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
760 case 'g': case '<': case 'G': case '>':
761 if (num_flines >= height - 2)
762 buffer_line(num - 1);
765 buffer_line(((num / 100) * num_flines) - 1);
767 #ifdef CONFIG_FEATURE_LESS_REGEXP
769 goto_match(match_pos + num);
785 #ifdef CONFIG_FEATURE_LESS_FLAGCS
786 static void flag_change(void)
792 keypress = tless_getch();
812 static void show_flag_status(void)
819 keypress = tless_getch();
823 flag_val = flags & FLAG_M;
826 flag_val = flags & FLAG_m;
829 flag_val = flags & FLAG_TILDE;
832 flag_val = flags & FLAG_N;
835 flag_val = flags & FLAG_E;
843 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
847 static void full_repaint(void)
849 int temp_line_pos = line_pos;
852 buffer_line(temp_line_pos);
856 static void save_input_to_file(void)
858 char current_line[256];
863 printf("Log file: ");
864 fgets(current_line, 256, inp);
865 current_line[strlen(current_line) - 1] = '\0';
866 if (strlen(current_line) > 1) {
867 fp = bb_xfopen(current_line, "w");
868 for (i = 0; i < num_flines; i++)
869 fprintf(fp, "%s", flines[i]);
874 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
877 #ifdef CONFIG_FEATURE_LESS_MARKS
878 static void add_mark(void)
885 letter = tless_getch();
887 if (isalpha(letter)) {
888 mark_line = line_pos;
890 /* If we exceed 15 marks, start overwriting previous ones */
894 mark_lines[num_marks][0] = letter;
895 mark_lines[num_marks][1] = line_pos;
900 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
904 static void goto_mark(void)
910 printf("Go to mark: ");
911 letter = tless_getch();
914 if (isalpha(letter)) {
915 for (i = 0; i <= num_marks; i++)
916 if (letter == mark_lines[i][0]) {
917 buffer_line(mark_lines[i][1]);
920 if ((num_marks == 14) && (letter != mark_lines[14][0]))
921 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
924 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
929 #ifdef CONFIG_FEATURE_LESS_BRACKETS
931 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 = bb_getopt_ulflags(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);