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
76 static char filename[256];
79 static int current_file = 1;
81 static int num_flines;
82 static int num_files = 1;
85 /* Command line options */
86 static unsigned long flags;
91 #define FLAG_TILDE (1<<4)
93 /* This is needed so that program behaviour changes when input comes from
97 #ifdef CONFIG_FEATURE_LESS_MARKS
98 static int mark_lines[15][2];
102 #ifdef CONFIG_FEATURE_LESS_REGEXP
103 static int match_found;
104 static int *match_lines;
105 static int match_pos;
106 static int num_matches;
107 static int match_backwards;
108 static regex_t old_pattern;
111 /* Needed termios structures */
112 static struct termios term_orig, term_vi;
114 /* File pointer to get input from */
117 /* Reset terminal input to normal */
118 static void set_tty_cooked(void)
121 tcsetattr(fileno(inp), TCSANOW, &term_orig);
124 /* Set terminal input to raw mode (taken from vi.c) */
125 static void set_tty_raw(void)
127 tcsetattr(fileno(inp), TCSANOW, &term_vi);
130 /* Exit the program gracefully */
131 static void tless_exit(int code)
133 /* TODO: We really should save the terminal state when we start,
134 and restore it when we exit. Less does this with the
135 "ti" and "te" termcap commands; can this be done with
142 /* Grab a character from input without requiring the return key. If the
143 character is ASCII \033, get more characters and assign certain sequences
144 special return codes. Note that this function works best with raw input. */
145 static int tless_getch(void)
152 /* Detect escape sequences (i.e. arrow keys) and handle
155 if (input == '\033' && getc(inp) == '[') {
158 if (input == REAL_KEY_UP)
160 else if (input == REAL_KEY_DOWN)
162 else if (input == REAL_KEY_RIGHT)
164 else if (input == REAL_KEY_LEFT)
166 else if (input == REAL_PAGE_UP)
168 else if (input == REAL_PAGE_DOWN)
171 /* The input is a normal ASCII value */
179 /* Move the cursor to a position (x,y), where (0,0) is the
180 top-left corner of the console */
181 static void move_cursor(int x, int y)
183 printf("\033[%i;%iH", x, y);
186 static void clear_line(void)
188 move_cursor(height, 0);
192 /* This adds line numbers to every line, as the -N flag necessitates */
193 static void add_linenumbers(void)
195 char current_line[256];
198 for (i = 0; i <= num_flines; i++) {
199 safe_strncpy(current_line, flines[i], 256);
200 flines[i] = bb_xasprintf("%5d %s", i + 1, current_line);
204 static void data_readlines(void)
207 char current_line[256];
210 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "r");
212 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
213 strcpy(current_line, "");
214 fgets(current_line, 256, fp);
216 bb_xferror(fp, filename);
217 flines = xrealloc(flines, (i+1) * sizeof(char *));
218 flines[i] = bb_xstrdup(current_line);
222 /* Reset variables for a new file */
230 inp = (inp_stdin) ? bb_xfopen(CURRENT_TTY, "r") : stdin;
236 #ifdef CONFIG_FEATURE_LESS_FLAGS
238 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
240 static int calc_percent(void)
242 return ((100 * (line_pos + height - 2) / num_flines) + 1);
245 /* Print a status line if -M was specified */
246 static void m_status_print(void)
253 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);
255 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
259 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
262 if (line_pos == num_flines - height + 2) {
263 printf("(END) %s", NORMAL);
264 if ((num_files > 1) && (current_file != num_files))
265 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
268 percentage = calc_percent();
269 printf("%i%% %s", percentage, NORMAL);
273 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
274 if ((num_files > 1) && (current_file != num_files))
275 printf("- Next: %s", files[current_file]);
276 printf("%s", NORMAL);
280 /* Print a status line if -m was specified */
281 static void medium_status_print(void)
284 percentage = calc_percent();
287 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
288 else if (line_pos == num_flines - height + 2)
289 printf("%s(END)%s", HIGHLIGHT, NORMAL);
291 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
295 /* Print the status line */
296 static void status_print(void)
298 /* Change the status if flags have been set */
299 #ifdef CONFIG_FEATURE_LESS_FLAGS
302 else if (flags & FLAG_m)
303 medium_status_print();
308 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
310 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", 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] = bb_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] = bb_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] = bb_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] = bb_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] = bb_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] = bb_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] = bb_xstrdup(flines[line_pos + i]);
445 if (line_pos >= num_flines - height + 2)
446 buffer[i] = bb_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] = bb_xstrdup(flines[linenum + i]);
471 for (i = 0; i < (height - 1); i++) {
473 if (linenum + i < num_flines + 2)
474 buffer[i] = bb_xstrdup(flines[linenum + i]);
476 buffer[i] = bb_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] = bb_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 = (char *) xmalloc((sizeof(char) * (strlen(line) + 1)) + 64);
614 regmatch_t match_structs;
616 line2 = bb_xstrdup(line);
619 match_status = regexec(pattern, line2, 1, &match_structs, 0);
621 while (match_status == 0) {
622 if (match_found == 0)
626 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);
629 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);
632 line2 += match_structs.rm_eo;
633 match_status = regexec(pattern, line2, 1, &match_structs, REG_NOTBOL);
636 growline = bb_xasprintf("%s%s", growline, line2);
638 return (match_found ? growline : line);
644 static void goto_match(int match)
646 /* This goes to a specific match - all line positions of matches are
647 stored within the match_lines[] array. */
648 if ((match < num_matches) && (match >= 0)) {
649 buffer_line(match_lines[match]);
654 static void regex_process(void)
656 char uncomp_regex[100];
661 /* Get the uncompiled regular expression from the user */
663 putchar((match_backwards) ? '?' : '/');
665 fgets(uncomp_regex, sizeof(uncomp_regex), inp);
667 if (strlen(uncomp_regex) == 1) {
669 goto_match(match_backwards ? match_pos - 1 : match_pos + 1);
674 uncomp_regex[strlen(uncomp_regex) - 1] = '\0';
676 /* Compile the regex and check for errors */
677 xregcomp(&pattern, uncomp_regex, 0);
680 /* Get rid of all the highlights we added previously */
681 for (i = 0; i <= num_flines; i++) {
682 current_line = process_regex_on_line(flines[i], &old_pattern, 0);
683 flines[i] = bb_xstrdup(current_line);
686 old_pattern = pattern;
688 /* Reset variables */
689 match_lines = xrealloc(match_lines, sizeof(int));
694 /* Run the regex on each line of the current file here */
695 for (i = 0; i <= num_flines; i++) {
696 current_line = process_regex_on_line(flines[i], &pattern, 1);
697 flines[i] = bb_xstrdup(current_line);
699 match_lines = xrealloc(match_lines, (j + 1) * sizeof(int));
706 if ((match_lines[0] != -1) && (num_flines > height - 2)) {
707 if (match_backwards) {
708 for (i = 0; i < num_matches; i++) {
709 if (match_lines[i] > line_pos) {
711 buffer_line(match_lines[match_pos]);
717 buffer_line(match_lines[0]);
724 static void number_process(int first_digit)
732 num_input[0] = first_digit;
734 /* Clear the current line, print a prompt, and then print the digit */
736 printf(":%c", first_digit);
738 /* Receive input until a letter is given (max 80 chars)*/
739 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
740 putchar(num_input[i]);
744 /* Take the final letter out of the digits string */
745 keypress = num_input[i];
747 num = strtol(num_input, &endptr, 10);
748 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES) {
753 /* We now know the number and the letter entered, so we process them */
755 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
758 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
761 case 'g': case '<': case 'G': case '>':
762 if (num_flines >= height - 2)
763 buffer_line(num - 1);
766 buffer_line(((num / 100) * num_flines) - 1);
768 #ifdef CONFIG_FEATURE_LESS_REGEXP
770 goto_match(match_pos + num);
786 #ifdef CONFIG_FEATURE_LESS_FLAGCS
787 static void flag_change(void)
793 keypress = tless_getch();
813 static void show_flag_status(void)
820 keypress = tless_getch();
824 flag_val = flags & FLAG_M;
827 flag_val = flags & FLAG_m;
830 flag_val = flags & FLAG_TILDE;
833 flag_val = flags & FLAG_N;
836 flag_val = flags & FLAG_E;
844 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
848 static void full_repaint(void)
850 int temp_line_pos = line_pos;
853 buffer_line(temp_line_pos);
857 static void save_input_to_file(void)
859 char current_line[256];
864 printf("Log file: ");
865 fgets(current_line, 256, inp);
866 current_line[strlen(current_line) - 1] = '\0';
867 if (strlen(current_line) > 1) {
868 fp = bb_xfopen(current_line, "w");
869 for (i = 0; i < num_flines; i++)
870 fprintf(fp, "%s", flines[i]);
875 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
878 #ifdef CONFIG_FEATURE_LESS_MARKS
879 static void add_mark(void)
886 letter = tless_getch();
888 if (isalpha(letter)) {
889 mark_line = line_pos;
891 /* If we exceed 15 marks, start overwriting previous ones */
895 mark_lines[num_marks][0] = letter;
896 mark_lines[num_marks][1] = line_pos;
901 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
905 static void goto_mark(void)
911 printf("Go to mark: ");
912 letter = tless_getch();
915 if (isalpha(letter)) {
916 for (i = 0; i <= num_marks; i++)
917 if (letter == mark_lines[i][0]) {
918 buffer_line(mark_lines[i][1]);
921 if ((num_marks == 14) && (letter != mark_lines[14][0]))
922 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
925 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
930 #ifdef CONFIG_FEATURE_LESS_BRACKETS
932 static char opp_bracket(char bracket)
953 static void match_right_bracket(char bracket)
955 int bracket_line = -1;
960 if (strchr(flines[line_pos], bracket) == NULL)
961 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
963 for (i = line_pos + 1; i < num_flines; i++) {
964 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
970 if (bracket_line == -1)
971 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
973 buffer_line(bracket_line - height + 2);
977 static void match_left_bracket(char bracket)
979 int bracket_line = -1;
984 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
985 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
986 printf("%s", flines[line_pos + height]);
990 for (i = line_pos + height - 2; i >= 0; i--) {
991 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
997 if (bracket_line == -1)
998 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1000 buffer_line(bracket_line);
1004 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1006 static void keypress_process(int keypress)
1009 case KEY_DOWN: case 'e': case 'j': case '\015':
1013 case KEY_UP: case 'y': case 'k':
1017 case PAGE_DOWN: case ' ': case 'z':
1018 buffer_down(height - 1);
1021 case PAGE_UP: case 'w': case 'b':
1022 buffer_up(height - 1);
1026 buffer_down((height - 1) / 2);
1030 buffer_up((height - 1) / 2);
1033 case 'g': case 'p': case '<': case '%':
1037 buffer_line(num_flines - height + 2);
1042 #ifdef CONFIG_FEATURE_LESS_MARKS
1060 save_input_to_file();
1065 #ifdef CONFIG_FEATURE_LESS_FLAGS
1071 #ifdef CONFIG_FEATURE_LESS_REGEXP
1073 match_backwards = 0;
1077 goto_match(match_pos + 1);
1080 goto_match(match_pos - 1);
1083 match_backwards = 1;
1087 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1096 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1097 case '{': case '(': case '[':
1098 match_right_bracket(keypress);
1100 case '}': case ')': case ']':
1101 match_left_bracket(keypress);
1111 if (isdigit(keypress))
1112 number_process(keypress);
1115 int less_main(int argc, char **argv) {
1119 flags = bb_getopt_ulflags(argc, argv, "EMmN~");
1127 if (ttyname(STDIN_FILENO) == NULL)
1130 bb_error_msg("Missing filename");
1135 strcpy(filename, (inp_stdin) ? bb_msg_standard_input : files[0]);
1136 get_terminal_width_height(0, &width, &height);
1138 tcgetattr(fileno(inp), &term_orig);
1139 term_vi = term_orig;
1140 term_vi.c_lflag &= (~ICANON & ~ECHO);
1141 term_vi.c_iflag &= (~IXON & ~ICRNL);
1142 term_vi.c_oflag &= (~ONLCR);
1143 term_vi.c_cc[VMIN] = 1;
1144 term_vi.c_cc[VTIME] = 0;
1149 keypress = tless_getch();
1150 keypress_process(keypress);