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
35 #ifdef CONFIG_FEATURE_LESS_REGEXP
40 /* These are the escape sequences corresponding to special keys */
41 #define REAL_KEY_UP 'A'
42 #define REAL_KEY_DOWN 'B'
43 #define REAL_KEY_RIGHT 'C'
44 #define REAL_KEY_LEFT 'D'
45 #define REAL_PAGE_UP '5'
46 #define REAL_PAGE_DOWN '6'
48 /* These are the special codes assigned by this program to the special keys */
56 /* The escape codes for highlighted and normal text */
57 #define HIGHLIGHT "\033[7m"
58 #define NORMAL "\033[0m"
60 /* The escape code to clear the screen */
61 #define CLEAR "\033[H\033[J"
63 /* Maximum number of lines in a file */
64 #define MAXLINES 10000
69 static char filename[256];
72 static int current_file = 1;
74 static int num_flines;
75 static int num_files = 1;
78 /* Command line options */
79 static unsigned long flags;
84 #define FLAG_TILDE (1<<4)
86 /* This is needed so that program behaviour changes when input comes from
90 #ifdef CONFIG_FEATURE_LESS_MARKS
91 static int mark_lines[15][2];
95 #ifdef CONFIG_FEATURE_LESS_REGEXP
96 static int match_found;
97 static int *match_lines;
99 static int num_matches;
100 static int match_backwards;
101 static regex_t old_pattern;
104 /* Needed termios structures */
105 static struct termios term_orig, term_vi;
107 /* File pointer to get input from */
110 /* Reset terminal input to normal */
111 static void set_tty_cooked(void)
114 tcsetattr(fileno(inp), TCSANOW, &term_orig);
117 /* Set terminal input to raw mode (taken from vi.c) */
118 static void set_tty_raw(void)
120 tcsetattr(fileno(inp), TCSANOW, &term_vi);
123 /* Exit the program gracefully */
124 static void tless_exit(int code)
126 /* TODO: We really should save the terminal state when we start,
127 and restore it when we exit. Less does this with the
128 "ti" and "te" termcap commands; can this be done with
135 /* Grab a character from input without requiring the return key. If the
136 character is ASCII \033, get more characters and assign certain sequences
137 special return codes. Note that this function works best with raw input. */
138 static int tless_getch(void)
145 /* Detect escape sequences (i.e. arrow keys) and handle
148 if (input == '\033' && getc(inp) == '[') {
151 if (input == REAL_KEY_UP)
153 else if (input == REAL_KEY_DOWN)
155 else if (input == REAL_KEY_RIGHT)
157 else if (input == REAL_KEY_LEFT)
159 else if (input == REAL_PAGE_UP)
161 else if (input == REAL_PAGE_DOWN)
164 /* The input is a normal ASCII value */
172 /* Move the cursor to a position (x,y), where (0,0) is the
173 top-left corner of the console */
174 static void move_cursor(int x, int y)
176 printf("\033[%i;%iH", x, y);
179 static void clear_line(void)
181 move_cursor(height, 0);
185 /* This adds line numbers to every line, as the -N flag necessitates */
186 static void add_linenumbers(void)
188 char current_line[256];
191 for (i = 0; i <= num_flines; i++) {
192 safe_strncpy(current_line, flines[i], 256);
193 flines[i] = xasprintf("%5d %s", i + 1, current_line);
197 static void data_readlines(void)
200 char current_line[256];
203 fp = (inp_stdin) ? stdin : xfopen(filename, "r");
205 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
206 strcpy(current_line, "");
207 fgets(current_line, 256, fp);
209 xferror(fp, filename);
210 flines = xrealloc(flines, (i+1) * sizeof(char *));
211 flines[i] = xstrdup(current_line);
215 /* Reset variables for a new file */
223 inp = (inp_stdin) ? xfopen(CURRENT_TTY, "r") : stdin;
229 #ifdef CONFIG_FEATURE_LESS_FLAGS
231 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
233 static int calc_percent(void)
235 return ((100 * (line_pos + height - 2) / num_flines) + 1);
238 /* Print a status line if -M was specified */
239 static void m_status_print(void)
246 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);
248 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
252 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
255 if (line_pos == num_flines - height + 2) {
256 printf("(END) %s", NORMAL);
257 if ((num_files > 1) && (current_file != num_files))
258 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
261 percentage = calc_percent();
262 printf("%i%% %s", percentage, NORMAL);
266 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
267 if ((num_files > 1) && (current_file != num_files))
268 printf("- Next: %s", files[current_file]);
269 printf("%s", NORMAL);
273 /* Print a status line if -m was specified */
274 static void medium_status_print(void)
277 percentage = calc_percent();
280 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
281 else if (line_pos == num_flines - height + 2)
282 printf("%s(END)%s", HIGHLIGHT, NORMAL);
284 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
288 /* Print the status line */
289 static void status_print(void)
291 /* Change the status if flags have been set */
292 #ifdef CONFIG_FEATURE_LESS_FLAGS
295 else if (flags & FLAG_m)
296 medium_status_print();
301 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
303 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", current_file, " of ", num_files, ")", NORMAL);
305 else if (line_pos == num_flines - height + 2) {
306 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
307 if ((num_files > 1) && (current_file != num_files))
308 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
313 #ifdef CONFIG_FEATURE_LESS_FLAGS
318 /* Print the buffer */
319 static void buffer_print(void)
324 if (num_flines >= height - 2) {
325 for (i = 0; i < height - 1; i++)
326 printf("%s", buffer[i]);
329 for (i = 1; i < (height - 1 - num_flines); i++)
331 for (i = 0; i < height - 1; i++)
332 printf("%s", buffer[i]);
338 /* Initialise the buffer */
339 static void buffer_init(void)
343 if (buffer == NULL) {
344 /* malloc the number of lines needed for the buffer */
345 buffer = xrealloc(buffer, height * sizeof(char *));
347 for (i = 0; i < (height - 1); i++)
351 /* Fill the buffer until the end of the file or the
352 end of the buffer is reached */
353 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
354 buffer[i] = xstrdup(flines[i]);
357 /* If the buffer still isn't full, fill it with blank lines */
358 for (; i < (height - 1); i++) {
359 buffer[i] = xstrdup("");
363 /* Move the buffer up and down in the file in order to scroll */
364 static void buffer_down(int nlines)
369 if (line_pos + (height - 3) + nlines < num_flines) {
371 for (i = 0; i < (height - 1); i++) {
373 buffer[i] = xstrdup(flines[line_pos + i]);
377 /* As the number of lines requested was too large, we just move
378 to the end of the file */
379 while (line_pos + (height - 3) + 1 < num_flines) {
381 for (i = 0; i < (height - 1); i++) {
383 buffer[i] = xstrdup(flines[line_pos + i]);
388 /* We exit if the -E flag has been set */
389 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
394 static void buffer_up(int nlines)
400 if (line_pos - nlines >= 0) {
402 for (i = 0; i < (height - 1); i++) {
404 buffer[i] = xstrdup(flines[line_pos + i]);
408 /* As the requested number of lines to move was too large, we
409 move one line up at a time until we can't. */
410 while (line_pos != 0) {
412 for (i = 0; i < (height - 1); i++) {
414 buffer[i] = xstrdup(flines[line_pos + i]);
420 /* Work out where the tildes start */
421 tilde_line = num_flines - line_pos + 3;
424 /* Going backwards nlines lines has taken us to a point where
425 nothing is past the EOF, so we revert to normal. */
426 if (line_pos < num_flines - height + 3) {
431 /* We only move part of the buffer, as the rest
433 for (i = 0; i < (height - 1); i++) {
435 if (i < tilde_line - nlines + 1)
436 buffer[i] = xstrdup(flines[line_pos + i]);
438 if (line_pos >= num_flines - height + 2)
439 buffer[i] = xstrdup("~\n");
446 static void buffer_line(int linenum)
451 if (linenum < 0 || linenum > num_flines) {
453 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum + 1, NORMAL);
455 else if (linenum < (num_flines - height - 2)) {
456 for (i = 0; i < (height - 1); i++) {
458 buffer[i] = xstrdup(flines[linenum + i]);
464 for (i = 0; i < (height - 1); i++) {
466 if (linenum + i < num_flines + 2)
467 buffer[i] = xstrdup(flines[linenum + i]);
469 buffer[i] = xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
472 /* Set past_eof so buffer_down and buffer_up act differently */
478 /* Reinitialise everything for a new file - free the memory and start over */
479 static void reinitialise(void)
483 for (i = 0; i <= num_flines; i++)
492 static void examine_file(void)
498 fgets(filename, 256, inp);
500 /* As fgets adds a newline to the end of an input string, we
502 newline_offset = strlen(filename) - 1;
503 filename[newline_offset] = '\0';
505 files[num_files] = xstrdup(filename);
506 current_file = num_files + 1;
513 /* This function changes the file currently being paged. direction can be one of the following:
514 * -1: go back one file
515 * 0: go to the first file
516 * 1: go forward one file
518 static void change_file(int direction)
520 if (current_file != ((direction > 0) ? num_files : 1)) {
521 current_file = direction ? current_file + direction : 1;
522 strcpy(filename, files[current_file - 1]);
527 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
531 static void remove_current_file(void)
535 if (current_file != 1) {
537 for (i = 3; i <= num_files; i++)
538 files[i - 2] = files[i - 1];
544 for (i = 2; i <= num_files; i++)
545 files[i - 2] = files[i - 1];
552 static void colon_process(void)
556 /* Clear the current line and print a prompt */
560 keypress = tless_getch();
563 remove_current_file();
568 #ifdef CONFIG_FEATURE_LESS_FLAGS
591 #ifdef CONFIG_FEATURE_LESS_REGEXP
592 /* The below two regular expression handler functions NEED development. */
594 /* Get a regular expression from the user, and then go through the current
595 file line by line, running a processing regex function on each one. */
597 static char *process_regex_on_line(char *line, regex_t *pattern, int action)
599 /* This function takes the regex and applies it to the line.
600 Each part of the line that matches has the HIGHLIGHT
601 and NORMAL escape sequences placed around it by
602 insert_highlights if action = 1, or has the escape sequences
603 removed if action = 0, and then the line is returned. */
605 char *line2 = (char *) xmalloc((sizeof(char) * (strlen(line) + 1)) + 64);
607 regmatch_t match_structs;
609 line2 = xstrdup(line);
612 match_status = regexec(pattern, line2, 1, &match_structs, 0);
614 while (match_status == 0) {
615 if (match_found == 0)
619 growline = 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);
622 growline = xasprintf("%s%.*s%.*s", growline, match_structs.rm_so - 4, line2, match_structs.rm_eo - match_structs.rm_so, line2 + match_structs.rm_so);
625 line2 += match_structs.rm_eo;
626 match_status = regexec(pattern, line2, 1, &match_structs, REG_NOTBOL);
629 growline = xasprintf("%s%s", growline, line2);
631 return (match_found ? growline : line);
637 static void goto_match(int match)
639 /* This goes to a specific match - all line positions of matches are
640 stored within the match_lines[] array. */
641 if ((match < num_matches) && (match >= 0)) {
642 buffer_line(match_lines[match]);
647 static void regex_process(void)
649 char uncomp_regex[100];
654 /* Get the uncompiled regular expression from the user */
656 putchar((match_backwards) ? '?' : '/');
658 fgets(uncomp_regex, sizeof(uncomp_regex), inp);
660 if (strlen(uncomp_regex) == 1) {
662 goto_match(match_backwards ? match_pos - 1 : match_pos + 1);
667 uncomp_regex[strlen(uncomp_regex) - 1] = '\0';
669 /* Compile the regex and check for errors */
670 xregcomp(&pattern, uncomp_regex, 0);
673 /* Get rid of all the highlights we added previously */
674 for (i = 0; i <= num_flines; i++) {
675 current_line = process_regex_on_line(flines[i], &old_pattern, 0);
676 flines[i] = xstrdup(current_line);
679 old_pattern = pattern;
681 /* Reset variables */
682 match_lines = xrealloc(match_lines, sizeof(int));
687 /* Run the regex on each line of the current file here */
688 for (i = 0; i <= num_flines; i++) {
689 current_line = process_regex_on_line(flines[i], &pattern, 1);
690 flines[i] = xstrdup(current_line);
692 match_lines = xrealloc(match_lines, (j + 1) * sizeof(int));
699 if ((match_lines[0] != -1) && (num_flines > height - 2)) {
700 if (match_backwards) {
701 for (i = 0; i < num_matches; i++) {
702 if (match_lines[i] > line_pos) {
704 buffer_line(match_lines[match_pos]);
710 buffer_line(match_lines[0]);
717 static void number_process(int first_digit)
725 num_input[0] = first_digit;
727 /* Clear the current line, print a prompt, and then print the digit */
729 printf(":%c", first_digit);
731 /* Receive input until a letter is given (max 80 chars)*/
732 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
733 putchar(num_input[i]);
737 /* Take the final letter out of the digits string */
738 keypress = num_input[i];
740 num = strtol(num_input, &endptr, 10);
741 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES) {
746 /* We now know the number and the letter entered, so we process them */
748 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
751 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
754 case 'g': case '<': case 'G': case '>':
755 if (num_flines >= height - 2)
756 buffer_line(num - 1);
759 buffer_line(((num / 100) * num_flines) - 1);
761 #ifdef CONFIG_FEATURE_LESS_REGEXP
763 goto_match(match_pos + num);
779 #ifdef CONFIG_FEATURE_LESS_FLAGCS
780 static void flag_change(void)
786 keypress = tless_getch();
806 static void show_flag_status(void)
813 keypress = tless_getch();
817 flag_val = flags & FLAG_M;
820 flag_val = flags & FLAG_m;
823 flag_val = flags & FLAG_TILDE;
826 flag_val = flags & FLAG_N;
829 flag_val = flags & FLAG_E;
837 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
841 static void full_repaint(void)
843 int temp_line_pos = line_pos;
846 buffer_line(temp_line_pos);
850 static void save_input_to_file(void)
852 char current_line[256];
857 printf("Log file: ");
858 fgets(current_line, 256, inp);
859 current_line[strlen(current_line) - 1] = '\0';
860 if (strlen(current_line) > 1) {
861 fp = xfopen(current_line, "w");
862 for (i = 0; i < num_flines; i++)
863 fprintf(fp, "%s", flines[i]);
868 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
871 #ifdef CONFIG_FEATURE_LESS_MARKS
872 static void add_mark(void)
878 letter = tless_getch();
880 if (isalpha(letter)) {
882 /* If we exceed 15 marks, start overwriting previous ones */
886 mark_lines[num_marks][0] = letter;
887 mark_lines[num_marks][1] = line_pos;
892 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
896 static void goto_mark(void)
902 printf("Go to mark: ");
903 letter = tless_getch();
906 if (isalpha(letter)) {
907 for (i = 0; i <= num_marks; i++)
908 if (letter == mark_lines[i][0]) {
909 buffer_line(mark_lines[i][1]);
912 if ((num_marks == 14) && (letter != mark_lines[14][0]))
913 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
916 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
921 #ifdef CONFIG_FEATURE_LESS_BRACKETS
923 static char opp_bracket(char bracket)
939 static void match_right_bracket(char bracket)
941 int bracket_line = -1;
946 if (strchr(flines[line_pos], bracket) == NULL)
947 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
949 for (i = line_pos + 1; i < num_flines; i++) {
950 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
956 if (bracket_line == -1)
957 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
959 buffer_line(bracket_line - height + 2);
963 static void match_left_bracket(char bracket)
965 int bracket_line = -1;
970 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
971 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
972 printf("%s", flines[line_pos + height]);
976 for (i = line_pos + height - 2; i >= 0; i--) {
977 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
983 if (bracket_line == -1)
984 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
986 buffer_line(bracket_line);
990 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
992 static void keypress_process(int keypress)
995 case KEY_DOWN: case 'e': case 'j': case '\015':
999 case KEY_UP: case 'y': case 'k':
1003 case PAGE_DOWN: case ' ': case 'z':
1004 buffer_down(height - 1);
1007 case PAGE_UP: case 'w': case 'b':
1008 buffer_up(height - 1);
1012 buffer_down((height - 1) / 2);
1016 buffer_up((height - 1) / 2);
1019 case 'g': case 'p': case '<': case '%':
1023 buffer_line(num_flines - height + 2);
1028 #ifdef CONFIG_FEATURE_LESS_MARKS
1046 save_input_to_file();
1051 #ifdef CONFIG_FEATURE_LESS_FLAGS
1057 #ifdef CONFIG_FEATURE_LESS_REGEXP
1059 match_backwards = 0;
1063 goto_match(match_pos + 1);
1066 goto_match(match_pos - 1);
1069 match_backwards = 1;
1073 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1082 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1083 case '{': case '(': case '[':
1084 match_right_bracket(keypress);
1086 case '}': case ')': case ']':
1087 match_left_bracket(keypress);
1097 if (isdigit(keypress))
1098 number_process(keypress);
1101 int less_main(int argc, char **argv) {
1105 flags = bb_getopt_ulflags(argc, argv, "EMmN~");
1113 if (ttyname(STDIN_FILENO) == NULL)
1116 bb_error_msg("Missing filename");
1121 strcpy(filename, (inp_stdin) ? bb_msg_standard_input : files[0]);
1122 get_terminal_width_height(0, &width, &height);
1124 tcgetattr(fileno(inp), &term_orig);
1125 term_vi = term_orig;
1126 term_vi.c_lflag &= (~ICANON & ~ECHO);
1127 term_vi.c_iflag &= (~IXON & ~ICRNL);
1128 term_vi.c_oflag &= (~ONLCR);
1129 term_vi.c_cc[VMIN] = 1;
1130 term_vi.c_cc[VTIME] = 0;
1135 keypress = tless_getch();
1136 keypress_process(keypress);