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;
111 static int num_back_match = 1;
114 /* Needed termios structures */
115 static struct termios term_orig, term_vi;
117 /* File pointer to get input from */
120 /* Reset terminal input to normal */
121 static void set_tty_cooked(void)
124 tcsetattr(fileno(inp), TCSANOW, &term_orig);
127 /* Set terminal input to raw mode (taken from vi.c) */
128 static void set_tty_raw(void)
130 tcsetattr(fileno(inp), TCSANOW, &term_vi);
133 /* Exit the program gracefully */
134 static void tless_exit(int code)
136 /* TODO: We really should save the terminal state when we start,
137 and restore it when we exit. Less does this with the
138 "ti" and "te" termcap commands; can this be done with
145 /* Grab a character from input without requiring the return key. If the
146 character is ASCII \033, get more characters and assign certain sequences
147 special return codes. Note that this function works best with raw input. */
148 static int tless_getch(void)
155 /* Detect escape sequences (i.e. arrow keys) and handle
158 if (input == '\033' && getc(inp) == '[') {
161 if (input == REAL_KEY_UP)
163 else if (input == REAL_KEY_DOWN)
165 else if (input == REAL_KEY_RIGHT)
167 else if (input == REAL_KEY_LEFT)
169 else if (input == REAL_PAGE_UP)
171 else if (input == REAL_PAGE_DOWN)
174 /* The input is a normal ASCII value */
182 /* Move the cursor to a position (x,y), where (0,0) is the
183 top-left corner of the console */
184 static void move_cursor(int x, int y)
186 printf("\033[%i;%iH", x, y);
189 static void clear_line(void)
191 move_cursor(height, 0);
195 /* This adds line numbers to every line, as the -N flag necessitates */
196 static void add_linenumbers(void)
198 char current_line[256];
201 for (i = 0; i <= num_flines; i++) {
202 safe_strncpy(current_line, flines[i], 256);
203 flines[i] = bb_xasprintf("%5d %s", i + 1, current_line);
207 static void data_readlines(void)
210 char current_line[256];
213 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "rt");
215 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
216 strcpy(current_line, "");
217 fgets(current_line, 256, fp);
219 bb_xferror(fp, filename);
220 flines = xrealloc(flines, (i+1) * sizeof(char *));
221 flines[i] = bb_xstrdup(current_line);
225 /* Reset variables for a new file */
233 inp = (inp_stdin) ? bb_xfopen(CURRENT_TTY, "r") : stdin;
239 /* Turn a percentage into a line number */
240 static int reverse_percent(int percentage)
242 double linenum = percentage;
243 linenum = ((linenum / 100) * num_flines) - 1;
247 #ifdef CONFIG_FEATURE_LESS_FLAGS
249 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
251 static int calc_percent(void)
253 return ((100 * (line_pos + height - 2) / num_flines) + 1);
256 /* Print a status line if -M was specified */
257 static void m_status_print(void)
264 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);
266 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
270 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
273 if (line_pos == num_flines - height + 2) {
274 printf("(END) %s", NORMAL);
275 if ((num_files > 1) && (current_file != num_files))
276 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
279 percentage = calc_percent();
280 printf("%i%% %s", percentage, NORMAL);
284 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
285 if ((num_files > 1) && (current_file != num_files))
286 printf("- Next: %s", files[current_file]);
287 printf("%s", NORMAL);
291 /* Print a status line if -m was specified */
292 static void medium_status_print(void)
295 percentage = calc_percent();
298 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
299 else if (line_pos == num_flines - height + 2)
300 printf("%s(END)%s", HIGHLIGHT, NORMAL);
302 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
306 /* Print the status line */
307 static void status_print(void)
309 /* Change the status if flags have been set */
310 #ifdef CONFIG_FEATURE_LESS_FLAGS
313 else if (flags & FLAG_m)
314 medium_status_print();
319 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
321 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", current_file, " of ", num_files, ")", NORMAL);
323 else if (line_pos == num_flines - height + 2) {
324 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
325 if ((num_files > 1) && (current_file != num_files))
326 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
331 #ifdef CONFIG_FEATURE_LESS_FLAGS
336 /* Print the buffer */
337 static void buffer_print(void)
342 if (num_flines >= height - 2) {
343 for (i = 0; i < height - 1; i++)
344 printf("%s", buffer[i]);
347 for (i = 1; i < (height - 1 - num_flines); i++)
349 for (i = 0; i < height - 1; i++)
350 printf("%s", buffer[i]);
356 /* Initialise the buffer */
357 static void buffer_init(void)
361 if (buffer == NULL) {
362 /* malloc the number of lines needed for the buffer */
363 buffer = xrealloc(buffer, height * sizeof(char *));
365 for (i = 0; i < (height - 1); i++)
369 /* Fill the buffer until the end of the file or the
370 end of the buffer is reached */
371 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
372 buffer[i] = bb_xstrdup(flines[i]);
375 /* If the buffer still isn't full, fill it with blank lines */
376 for (; i < (height - 1); i++) {
377 buffer[i] = bb_xstrdup("");
381 /* Move the buffer up and down in the file in order to scroll */
382 static void buffer_down(int nlines)
387 if (line_pos + (height - 3) + nlines < num_flines) {
389 for (i = 0; i < (height - 1); i++) {
391 buffer[i] = bb_xstrdup(flines[line_pos + i]);
395 /* As the number of lines requested was too large, we just move
396 to the end of the file */
397 while (line_pos + (height - 3) + 1 < num_flines) {
399 for (i = 0; i < (height - 1); i++) {
401 buffer[i] = bb_xstrdup(flines[line_pos + i]);
406 /* We exit if the -E flag has been set */
407 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
412 static void buffer_up(int nlines)
418 if (line_pos - nlines >= 0) {
420 for (i = 0; i < (height - 1); i++) {
422 buffer[i] = bb_xstrdup(flines[line_pos + i]);
426 /* As the requested number of lines to move was too large, we
427 move one line up at a time until we can't. */
428 while (line_pos != 0) {
430 for (i = 0; i < (height - 1); i++) {
432 buffer[i] = bb_xstrdup(flines[line_pos + i]);
438 /* Work out where the tildes start */
439 tilde_line = num_flines - line_pos + 3;
442 /* Going backwards nlines lines has taken us to a point where
443 nothing is past the EOF, so we revert to normal. */
444 if (line_pos < num_flines - height + 3) {
449 /* We only move part of the buffer, as the rest
451 for (i = 0; i < (height - 1); i++) {
453 if (i < tilde_line - nlines + 1)
454 buffer[i] = bb_xstrdup(flines[line_pos + i]);
456 if (line_pos >= num_flines - height + 2)
457 buffer[i] = bb_xstrdup("~\n");
464 static void buffer_line(int linenum)
470 if (linenum < 1 || linenum > num_flines) {
472 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum, NORMAL);
474 else if (linenum < (num_flines - height - 2)) {
475 for (i = 0; i < (height - 1); i++) {
477 buffer[i] = bb_xstrdup(flines[linenum + i]);
482 for (i = 0; i < (height - 1); i++) {
484 if (linenum + i < num_flines + 2)
485 buffer[i] = bb_xstrdup(flines[linenum + i]);
487 buffer[i] = bb_xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
490 /* Set past_eof so buffer_down and buffer_up act differently */
495 /* Reinitialise everything for a new file - free the memory and start over */
496 static void reinitialise(void)
500 for (i = 0; i <= num_flines; i++)
509 static void examine_file(void)
515 fgets(filename, 256, inp);
517 /* As fgets adds a newline to the end of an input string, we
519 newline_offset = strlen(filename) - 1;
520 filename[newline_offset] = '\0';
522 files[num_files] = bb_xstrdup(filename);
523 current_file = num_files + 1;
530 /* This function changes the file currently being paged. direction can be one of the following:
531 * -1: go back one file
532 * 0: go to the first file
533 * 1: go forward one file
535 static void change_file(int direction)
537 if (current_file != ((direction > 0) ? num_files : 1)) {
538 current_file = direction ? current_file + direction : 1;
539 strcpy(filename, files[current_file - 1]);
544 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
548 static void remove_current_file(void)
552 if (current_file != 1) {
554 for (i = 3; i <= num_files; i++)
555 files[i - 2] = files[i - 1];
561 for (i = 2; i <= num_files; i++)
562 files[i - 2] = files[i - 1];
569 static void colon_process(void)
573 /* Clear the current line and print a prompt */
577 keypress = tless_getch();
580 remove_current_file();
585 #ifdef CONFIG_FEATURE_LESS_FLAGS
608 #ifdef CONFIG_FEATURE_LESS_REGEXP
609 /* The below two regular expression handler functions NEED development. */
611 /* Get a regular expression from the user, and then go through the current
612 file line by line, running a processing regex function on each one. */
614 static char *insert_highlights(char *line, int start, int end)
616 return bb_xasprintf("%.*s%s%.*s%s%s", start, line, HIGHLIGHT,
617 end - start, line + start, NORMAL, line + end);
620 static char *process_regex_on_line(char *line, regex_t *pattern)
622 /* This function takes the regex and applies it to the line.
623 Each part of the line that matches has the HIGHLIGHT
624 and NORMAL escape sequences placed around it by
625 insert_highlights, and then the line is returned. */
628 char *line2 = (char *) xmalloc((sizeof(char) * (strlen(line) + 1)) + 64);
631 regmatch_t match_structs;
636 match_status = regexec(pattern, line2, 1, &match_structs, 0);
638 while (match_status == 0) {
640 memset(sub_line, 0, sizeof(sub_line));
642 if (match_found == 0)
645 line2 = insert_highlights(line2, match_structs.rm_so + prev_eo, match_structs.rm_eo + prev_eo);
646 if ((size_t)match_structs.rm_eo + 11 + prev_eo < strlen(line2))
647 strcat(sub_line, line2 + match_structs.rm_eo + 11 + prev_eo);
649 prev_eo += match_structs.rm_eo + 11;
650 match_status = regexec(pattern, sub_line, 1, &match_structs, REG_NOTBOL);
656 static void regex_process(void)
658 char uncomp_regex[100];
659 char current_line[256];
664 /* Reset variables */
670 pattern = (regex_t *) malloc(sizeof(regex_t));
671 memset(pattern, 0, sizeof(regex_t));
673 /* Get the uncompiled regular expression from the user */
675 putchar((match_backwards) ? '?' : '/');
677 fgets(uncomp_regex, sizeof(uncomp_regex), stdin);
678 i = strlen(uncomp_regex);
680 if (uncomp_regex[i-1] == '\n')
681 uncomp_regex[i-1] = '\0';
683 while((i = getchar()) != '\n' && i != EOF);
686 /* Compile the regex and check for errors */
687 xregcomp(pattern, uncomp_regex, 0);
689 /* Run the regex on each line of the current file here */
690 for (i = 0; i <= num_flines; i++) {
691 strcpy(current_line, process_regex_on_line(flines[i], pattern));
692 flines[i] = bb_xstrdup(current_line);
700 if ((match_lines[0] != -1) && (num_flines > height - 2))
701 buffer_line(match_lines[0]);
706 static void goto_match(int match)
708 /* This goes to a specific match - all line positions of matches are
709 stored within the match_lines[] array. */
710 if ((match < num_matches) && (match >= 0)) {
711 buffer_line(match_lines[match]);
716 static void search_backwards(void)
718 int current_linepos = line_pos;
724 for (i = 0; i < num_matches; i++) {
725 if (match_lines[i] > current_linepos) {
726 buffer_line(match_lines[i - num_back_match]);
731 /* Reset variables */
738 static void number_process(int first_digit)
746 num_input[0] = first_digit;
748 /* Clear the current line, print a prompt, and then print the digit */
750 printf(":%c", first_digit);
752 /* Receive input until a letter is given (max 80 chars)*/
753 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
754 putchar(num_input[i]);
758 /* Take the final letter out of the digits string */
759 keypress = num_input[i];
761 num = strtol(num_input, &endptr, 10);
762 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES)
765 /* We now know the number and the letter entered, so we process them */
767 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
770 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
773 case 'g': case '<': case 'G': case '>':
774 if (num_flines >= height - 2)
775 buffer_line(num - 1);
778 buffer_line(reverse_percent(num));
780 #ifdef CONFIG_FEATURE_LESS_REGEXP
782 goto_match(match_pos + num - 1);
789 num_back_match = num;
800 #ifdef CONFIG_FEATURE_LESS_FLAGCS
801 static void flag_change(void)
807 keypress = tless_getch();
827 static void show_flag_status(void)
834 keypress = tless_getch();
838 flag_val = flags & FLAG_M;
841 flag_val = flags & FLAG_m;
844 flag_val = flags & FLAG_TILDE;
847 flag_val = flags & FLAG_N;
850 flag_val = flags & FLAG_E;
858 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
862 static void full_repaint(void)
864 int temp_line_pos = line_pos;
867 buffer_line(temp_line_pos);
872 static void save_input_to_file(void)
874 char current_line[256];
879 printf("Log file: ");
880 fgets(current_line, 256, inp);
881 current_line[strlen(current_line) - 1] = '\0';
882 if (strlen(current_line)) {
883 fp = bb_xfopen(current_line, "w");
884 for (i = 0; i < num_flines; i++)
885 fprintf(fp, "%s", flines[i]);
890 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
893 #ifdef CONFIG_FEATURE_LESS_MARKS
894 static void add_mark(void)
901 letter = tless_getch();
903 if (isalpha(letter)) {
904 mark_line = line_pos;
906 /* If we exceed 15 marks, start overwriting previous ones */
910 mark_lines[num_marks][0] = letter;
911 mark_lines[num_marks][1] = line_pos;
916 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
920 static void goto_mark(void)
926 printf("Go to mark: ");
927 letter = tless_getch();
930 if (isalpha(letter)) {
931 for (i = 0; i <= num_marks; i++)
932 if (letter == mark_lines[i][0]) {
933 buffer_line(mark_lines[i][1]);
936 if ((num_marks == 14) && (letter != mark_lines[14][0]))
937 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
940 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
945 #ifdef CONFIG_FEATURE_LESS_BRACKETS
947 static char opp_bracket(char bracket)
968 static void match_right_bracket(char bracket)
970 int bracket_line = -1;
975 if (strchr(flines[line_pos], bracket) == NULL)
976 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
978 for (i = line_pos + 1; i < num_flines; i++) {
979 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
985 if (bracket_line == -1)
986 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
988 buffer_line(bracket_line - height + 2);
993 static void match_left_bracket(char bracket)
995 int bracket_line = -1;
1000 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
1001 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
1002 printf("%s", flines[line_pos + height]);
1006 for (i = line_pos + height - 2; i >= 0; i--) {
1007 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1013 if (bracket_line == -1)
1014 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1016 buffer_line(bracket_line);
1021 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1023 static void keypress_process(int keypress)
1026 case KEY_DOWN: case 'e': case 'j': case '\015':
1030 case KEY_UP: case 'y': case 'k':
1034 case PAGE_DOWN: case ' ': case 'z':
1035 buffer_down(height - 1);
1038 case PAGE_UP: case 'w': case 'b':
1039 buffer_up(height - 1);
1043 buffer_down((height - 1) / 2);
1047 buffer_up((height - 1) / 2);
1050 case 'g': case 'p': case '<': case '%':
1051 buffer_up(num_flines + 1);
1055 buffer_down(num_flines + 1);
1061 #ifdef CONFIG_FEATURE_LESS_MARKS
1079 save_input_to_file();
1084 #ifdef CONFIG_FEATURE_LESS_FLAGS
1090 #ifdef CONFIG_FEATURE_LESS_REGEXP
1096 goto_match(match_pos + 1);
1100 goto_match(match_pos - 1);
1108 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1117 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1118 case '{': case '(': case '[':
1119 match_right_bracket(keypress);
1121 case '}': case ')': case ']':
1122 match_left_bracket(keypress);
1132 if (isdigit(keypress))
1133 number_process(keypress);
1136 int less_main(int argc, char **argv) {
1140 flags = bb_getopt_ulflags(argc, argv, "EMmN~");
1148 if (ttyname(STDIN_FILENO) == NULL)
1151 bb_error_msg("Missing filename");
1156 strcpy(filename, (inp_stdin) ? bb_msg_standard_input : files[0]);
1159 tcgetattr(fileno(inp), &term_orig);
1160 term_vi = term_orig;
1161 term_vi.c_lflag &= (~ICANON & ~ECHO);
1162 term_vi.c_iflag &= (~IXON & ~ICRNL);
1163 term_vi.c_oflag &= (~ONLCR);
1164 term_vi.c_cc[VMIN] = 1;
1165 term_vi.c_cc[VTIME] = 0;
1170 keypress = tless_getch();
1171 keypress_process(keypress);