1 /* vi: set sw=4 ts=4: */
3 * Mini less implementation for busybox
6 * Copyright (C) 2005 by Rob Sullivan <cogito.ergo.cogito@gmail.com>
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
23 * This program needs a lot of development, so consider it in a beta stage
27 * - Add more regular expression support - search modifiers, certain matches, etc.
28 * - Add more complex bracket searching - currently, nested brackets are
30 * - Add support for "F" as an input. This causes less to act in
31 * a similar way to tail -f.
32 * - Check for binary files, and prompt the user if a binary file
34 * - Allow horizontal scrolling. Currently, lines simply continue onto
35 * the next line, per the terminal's discretion
38 * - filename is an array and not a pointer because that avoids all sorts
39 * of complications involving the fact that something that is pointed to
40 * will be changed if the pointer is changed.
41 * - the inp file pointer is used so that keyboard input works after
42 * redirected input has been read from stdin
54 #ifdef CONFIG_FEATURE_LESS_REGEXP
59 /* These are the escape sequences corresponding to special keys */
60 #define REAL_KEY_UP 'A'
61 #define REAL_KEY_DOWN 'B'
62 #define REAL_KEY_RIGHT 'C'
63 #define REAL_KEY_LEFT 'D'
64 #define REAL_PAGE_UP '5'
65 #define REAL_PAGE_DOWN '6'
67 /* These are the special codes assigned by this program to the special keys */
75 /* The escape codes for highlighted and normal text */
76 #define HIGHLIGHT "\033[7m"
77 #define NORMAL "\033[0m"
79 /* The escape code to clear the screen */
80 #define CLEAR "\033[H\033[J"
82 /* Maximum number of lines in a file */
83 #define MAXLINES 10000
85 /* Get height and width of terminal */
86 #define tty_width_height() get_terminal_width_height(0, &width, &height)
91 static char filename[256];
94 static int current_file = 1;
96 static int num_flines;
97 static int num_files = 1;
100 /* Command line options */
101 static unsigned long flags;
103 #define FLAG_M (1<<1)
104 #define FLAG_m (1<<2)
105 #define FLAG_N (1<<3)
106 #define FLAG_TILDE (1<<4)
108 /* This is needed so that program behaviour changes when input comes from
110 static int inp_stdin;
112 #ifdef CONFIG_FEATURE_LESS_MARKS
113 static int mark_lines[15][2];
114 static int num_marks;
117 #ifdef CONFIG_FEATURE_LESS_REGEXP
118 static int match_found;
119 static int match_lines[100];
120 static int match_pos;
121 static int num_matches;
122 static int match_backwards;
123 static int num_back_match = 1;
126 /* Needed termios structures */
127 static struct termios term_orig, term_vi;
129 /* File pointer to get input from */
132 /* Reset terminal input to normal */
133 static void set_tty_cooked(void) {
135 tcsetattr(fileno(inp), TCSANOW, &term_orig);
138 /* Set terminal input to raw mode (taken from vi.c) */
139 static void set_tty_raw(void) {
140 tcsetattr(fileno(inp), TCSANOW, &term_vi);
143 /* Exit the program gracefully */
144 static void tless_exit(int code) {
146 /* TODO: We really should save the terminal state when we start,
147 and restore it when we exit. Less does this with the
148 "ti" and "te" termcap commands; can this be done with
155 /* Grab a character from input without requiring the return key. If the
156 character is ASCII \033, get more characters and assign certain sequences
157 special return codes. Note that this function works best with raw input. */
158 static int tless_getch(void) {
165 /* Detect escape sequences (i.e. arrow keys) and handle
168 if (input == '\033' && getc(inp) == '[') {
171 if (input == REAL_KEY_UP)
173 else if (input == REAL_KEY_DOWN)
175 else if (input == REAL_KEY_RIGHT)
177 else if (input == REAL_KEY_LEFT)
179 else if (input == REAL_PAGE_UP)
181 else if (input == REAL_PAGE_DOWN)
184 /* The input is a normal ASCII value */
192 /* Move the cursor to a position (x,y), where (0,0) is the
193 top-left corner of the console */
194 static void move_cursor(int x, int y) {
195 printf("\033[%i;%iH", x, y);
198 static void clear_line(void) {
199 move_cursor(height, 0);
203 /* This adds line numbers to every line, as the -N flag necessitates */
204 static void add_linenumbers(void) {
206 char current_line[256];
209 for (i = 0; i <= num_flines; i++) {
210 safe_strncpy(current_line, flines[i], 256);
211 flines[i] = bb_xasprintf("%5d %s", i + 1, current_line);
215 static void data_readlines(void) {
218 char current_line[256];
221 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "rt");
223 for (i = 0; (feof(fp)==0) && (i <= MAXLINES); i++) {
224 strcpy(current_line, "");
225 fgets(current_line, 256, fp);
227 bb_xferror(fp, filename);
228 flines = xrealloc(flines, (i+1) * sizeof(char *));
229 flines[i] = bb_xstrdup(current_line);
233 /* Reset variables for a new file */
241 inp = (inp_stdin) ? bb_xfopen(CURRENT_TTY, "r") : stdin;
247 /* Turn a percentage into a line number */
248 static int reverse_percent(int percentage) {
249 double linenum = percentage;
250 linenum = ((linenum / 100) * num_flines) - 1;
254 #ifdef CONFIG_FEATURE_LESS_FLAGS
256 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
258 static int calc_percent(void) {
259 return ((100 * (line_pos + height - 2) / num_flines) + 1);
262 /* Print a status line if -M was specified */
263 static void m_status_print(void) {
270 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);
272 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
276 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
279 if (line_pos == num_flines - height + 2) {
280 printf("(END) %s", NORMAL);
281 if ((num_files > 1) && (current_file != num_files))
282 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
285 percentage = calc_percent();
286 printf("%i%% %s", percentage, NORMAL);
290 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
291 if ((num_files > 1) && (current_file != num_files))
292 printf("- Next: %s", files[current_file]);
293 printf("%s", NORMAL);
297 /* Print a status line if -m was specified */
298 static void medium_status_print(void) {
301 percentage = calc_percent();
304 printf("%s%s %i%%%s", HIGHLIGHT, filename, percentage, NORMAL);
305 else if (line_pos == num_flines - height + 2)
306 printf("%s(END)%s", HIGHLIGHT, NORMAL);
308 printf("%s%i%%%s", HIGHLIGHT, percentage, NORMAL);
312 /* Print the status line */
313 static void status_print(void) {
315 /* Change the status if flags have been set */
316 #ifdef CONFIG_FEATURE_LESS_FLAGS
319 else if (flags & FLAG_m)
320 medium_status_print();
325 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
327 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", current_file, " of ", num_files, ")", NORMAL);
329 else if (line_pos == num_flines - height + 2) {
330 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
331 if ((num_files > 1) && (current_file != num_files))
332 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
337 #ifdef CONFIG_FEATURE_LESS_FLAGS
342 /* Print the buffer */
343 static void buffer_print(void) {
348 if (num_flines >= height - 2) {
349 for (i = 0; i < height - 1; i++)
350 printf("%s", buffer[i]);
353 for (i = 1; i < (height - 1 - num_flines); i++)
355 for (i = 0; i < height - 1; i++)
356 printf("%s", buffer[i]);
362 /* Initialise the buffer */
363 static void buffer_init(void) {
368 /* malloc the number of lines needed for the buffer */
369 buffer = xrealloc(buffer, height * sizeof(char *));
371 for (i = 0; i < (height - 1); i++)
375 /* Fill the buffer until the end of the file or the
376 end of the buffer is reached */
377 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
378 buffer[i] = bb_xstrdup(flines[i]);
381 /* If the buffer still isn't full, fill it with blank lines */
382 for (; i < (height - 1); i++) {
383 buffer[i] = bb_xstrdup("");
387 /* Move the buffer up and down in the file in order to scroll */
388 static void buffer_down(int nlines) {
393 if (line_pos + (height - 3) + nlines < num_flines) {
395 for (i = 0; i < (height - 1); i++) {
397 buffer[i] = bb_xstrdup(flines[line_pos + i]);
401 /* As the number of lines requested was too large, we just move
402 to the end of the file */
403 while (line_pos + (height - 3) + 1 < num_flines) {
405 for (i = 0; i < (height - 1); i++) {
407 buffer[i] = bb_xstrdup(flines[line_pos + i]);
412 /* We exit if the -E flag has been set */
413 if ((flags & FLAG_E) && (line_pos + (height - 2) == num_flines))
418 static void buffer_up(int nlines) {
424 if (line_pos - nlines >= 0) {
426 for (i = 0; i < (height - 1); i++) {
428 buffer[i] = bb_xstrdup(flines[line_pos + i]);
432 /* As the requested number of lines to move was too large, we
433 move one line up at a time until we can't. */
434 while (line_pos != 0) {
436 for (i = 0; i < (height - 1); i++) {
438 buffer[i] = bb_xstrdup(flines[line_pos + i]);
444 /* Work out where the tildes start */
445 tilde_line = num_flines - line_pos + 3;
448 /* Going backwards nlines lines has taken us to a point where
449 nothing is past the EOF, so we revert to normal. */
450 if (line_pos < num_flines - height + 3) {
455 /* We only move part of the buffer, as the rest
457 for (i = 0; i < (height - 1); i++) {
459 if (i < tilde_line - nlines + 1)
460 buffer[i] = bb_xstrdup(flines[line_pos + i]);
462 if (line_pos >= num_flines - height + 2)
463 buffer[i] = bb_xstrdup("~\n");
470 static void buffer_line(int linenum) {
476 if (linenum < 1 || linenum > num_flines) {
478 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum, NORMAL);
480 else if (linenum < (num_flines - height - 2)) {
481 for (i = 0; i < (height - 1); i++) {
483 buffer[i] = bb_xstrdup(flines[linenum + i]);
488 for (i = 0; i < (height - 1); i++) {
490 if (linenum + i < num_flines + 2)
491 buffer[i] = bb_xstrdup(flines[linenum + i]);
493 buffer[i] = bb_xstrdup((flags & FLAG_TILDE) ? "\n" : "~\n");
496 /* Set past_eof so buffer_down and buffer_up act differently */
501 /* Reinitialise everything for a new file - free the memory and start over */
502 static void reinitialise(void) {
506 for (i = 0; i <= num_flines; i++)
515 static void examine_file(void) {
521 fgets(filename, 256, inp);
523 /* As fgets adds a newline to the end of an input string, we
525 newline_offset = strlen(filename) - 1;
526 filename[newline_offset] = '\0';
528 files[num_files] = bb_xstrdup(filename);
529 current_file = num_files + 1;
536 /* This function changes the file currently being paged. direction can be one of the following:
537 * -1: go back one file
538 * 0: go to the first file
539 * 1: go forward one file
541 static void change_file (int direction) {
542 if (current_file != ((direction > 0) ? num_files : 1)) {
543 current_file = direction ? current_file + direction : 1;
544 strcpy(filename, files[current_file - 1]);
549 printf("%s%s%s", HIGHLIGHT, (direction > 0) ? "No next file" : "No previous file", NORMAL);
553 static void remove_current_file(void) {
557 if (current_file != 1) {
559 for (i = 3; i <= num_files; i++)
560 files[i - 2] = files[i - 1];
566 for (i = 2; i <= num_files; i++)
567 files[i - 2] = files[i - 1];
574 static void colon_process(void) {
578 /* Clear the current line and print a prompt */
582 keypress = tless_getch();
585 remove_current_file();
590 #ifdef CONFIG_FEATURE_LESS_FLAGS
613 #ifdef CONFIG_FEATURE_LESS_REGEXP
614 /* The below two regular expression handler functions NEED development. */
616 /* Get a regular expression from the user, and then go through the current
617 file line by line, running a processing regex function on each one. */
619 static char *insert_highlights (char *line, int start, int end) {
621 return bb_xasprintf("%.*s%s%.*s%s%s", start, line, HIGHLIGHT,
622 end - start, line + start, NORMAL, line + end);
625 static char *process_regex_on_line(char *line, regex_t *pattern) {
626 /* This function takes the regex and applies it to the line.
627 Each part of the line that matches has the HIGHLIGHT
628 and NORMAL escape sequences placed around it by
629 insert_highlights, and then the line is returned. */
632 char *line2 = (char *) malloc((sizeof(char) * (strlen(line) + 1)) + 64);
635 regmatch_t match_structs;
637 memset(sub_line, 0, 256);
641 match_status = regexec(pattern, line2, 1, &match_structs, 0);
643 while (match_status == 0) {
645 memset(sub_line, 0, 256);
647 if (match_found == 0)
650 line2 = insert_highlights(line2, match_structs.rm_so + prev_eo, match_structs.rm_eo + prev_eo);
651 if ((size_t)match_structs.rm_eo + 11 + prev_eo < strlen(line2))
652 strcat(sub_line, line2 + match_structs.rm_eo + 11 + prev_eo);
654 prev_eo += match_structs.rm_eo + 11;
655 match_status = regexec(pattern, sub_line, 1, &match_structs, REG_NOTBOL);
661 static void regex_process(void) {
663 char uncomp_regex[100];
664 char current_line[256];
669 /* Reset variables */
675 pattern = (regex_t *) malloc(sizeof(regex_t));
676 memset(pattern, 0, sizeof(regex_t));
678 /* Get the uncompiled regular expression from the user */
680 putchar((match_backwards) ? '?' : '/');
682 fgets(uncomp_regex, sizeof(uncomp_regex), stdin);
683 i = strlen(uncomp_regex);
685 if(uncomp_regex[i-1] == '\n')
686 uncomp_regex[i-1] = '\0';
688 while((i = getchar()) != '\n' && i != EOF);
691 /* Compile the regex and check for errors */
692 xregcomp(pattern, uncomp_regex, 0);
694 /* Run the regex on each line of the current file here */
695 for (i = 0; i <= num_flines; i++) {
696 strcpy(current_line, process_regex_on_line(flines[i], pattern));
697 flines[i] = bb_xstrdup(current_line);
705 if ((match_lines[0] != -1) && (num_flines > height - 2))
706 buffer_line(match_lines[0]);
711 static void goto_match(int match) {
713 /* This goes to a specific match - all line positions of matches are
714 stored within the match_lines[] array. */
715 if ((match < num_matches) && (match >= 0)) {
716 buffer_line(match_lines[match]);
721 static void search_backwards(void) {
723 int current_linepos = line_pos;
729 for (i = 0; i < num_matches; i++) {
730 if (match_lines[i] > current_linepos) {
731 buffer_line(match_lines[i - num_back_match]);
736 /* Reset variables */
743 static void number_process(int first_digit) {
751 num_input[0] = first_digit;
753 /* Clear the current line, print a prompt, and then print the digit */
755 printf(":%c", first_digit);
757 /* Receive input until a letter is given (max 80 chars)*/
758 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
759 putchar(num_input[i]);
763 /* Take the final letter out of the digits string */
764 keypress = num_input[i];
766 num = strtol(num_input, &endptr, 10);
767 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES)
770 /* We now know the number and the letter entered, so we process them */
772 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
775 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
778 case 'g': case '<': case 'G': case '>':
779 if (num_flines >= height - 2)
780 buffer_line(num - 1);
783 buffer_line(reverse_percent(num));
785 #ifdef CONFIG_FEATURE_LESS_REGEXP
787 goto_match(match_pos + num - 1);
794 num_back_match = num;
805 #ifdef CONFIG_FEATURE_LESS_FLAGCS
806 static void flag_change(void) {
812 keypress = tless_getch();
832 static void show_flag_status(void) {
839 keypress = tless_getch();
843 flag_val = flags & FLAG_M;
846 flag_val = flags & FLAG_m;
849 flag_val = flags & FLAG_TILDE;
852 flag_val = flags & FLAG_N;
855 flag_val = flags & FLAG_E;
863 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
867 static void full_repaint(void) {
869 int temp_line_pos = line_pos;
872 buffer_line(temp_line_pos);
877 static void save_input_to_file(void) {
879 char current_line[256];
884 printf("Log file: ");
885 fgets(current_line, 256, inp);
886 current_line[strlen(current_line) - 1] = '\0';
887 if (strlen(current_line)) {
888 fp = bb_xfopen(current_line, "w");
889 for (i = 0; i < num_flines; i++)
890 fprintf(fp, "%s", flines[i]);
895 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
898 #ifdef CONFIG_FEATURE_LESS_MARKS
899 static void add_mark(void) {
906 letter = tless_getch();
908 if (isalpha(letter)) {
909 mark_line = line_pos;
911 /* If we exceed 15 marks, start overwriting previous ones */
915 mark_lines[num_marks][0] = letter;
916 mark_lines[num_marks][1] = line_pos;
921 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
925 static void goto_mark(void) {
931 printf("Go to mark: ");
932 letter = tless_getch();
935 if (isalpha(letter)) {
936 for (i = 0; i <= num_marks; i++)
937 if (letter == mark_lines[i][0]) {
938 buffer_line(mark_lines[i][1]);
941 if ((num_marks == 14) && (letter != mark_lines[14][0]))
942 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
945 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
950 #ifdef CONFIG_FEATURE_LESS_BRACKETS
952 static char opp_bracket (char bracket) {
973 static void match_right_bracket(char bracket) {
975 int bracket_line = -1;
980 if (strchr(flines[line_pos], bracket) == NULL)
981 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
983 for (i = line_pos + 1; i < num_flines; i++) {
984 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
990 if (bracket_line == -1)
991 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
993 buffer_line(bracket_line - height + 2);
998 static void match_left_bracket (char bracket) {
1000 int bracket_line = -1;
1005 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
1006 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
1007 printf("%s", flines[line_pos + height]);
1011 for (i = line_pos + height - 2; i >= 0; i--) {
1012 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1018 if (bracket_line == -1)
1019 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1021 buffer_line(bracket_line);
1026 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1028 static void keypress_process(int keypress) {
1030 case KEY_DOWN: case 'e': case 'j': case '\015':
1034 case KEY_UP: case 'y': case 'k':
1038 case PAGE_DOWN: case ' ': case 'z':
1039 buffer_down(height - 1);
1042 case PAGE_UP: case 'w': case 'b':
1043 buffer_up(height - 1);
1047 buffer_down((height - 1) / 2);
1051 buffer_up((height - 1) / 2);
1054 case 'g': case 'p': case '<': case '%':
1055 buffer_up(num_flines + 1);
1059 buffer_down(num_flines + 1);
1065 #ifdef CONFIG_FEATURE_LESS_MARKS
1083 save_input_to_file();
1088 #ifdef CONFIG_FEATURE_LESS_FLAGS
1094 #ifdef CONFIG_FEATURE_LESS_REGEXP
1100 goto_match(match_pos + 1);
1104 goto_match(match_pos - 1);
1112 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1121 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1122 case '{': case '(': case '[':
1123 match_right_bracket(keypress);
1125 case '}': case ')': case ']':
1126 match_left_bracket(keypress);
1136 if (isdigit(keypress))
1137 number_process(keypress);
1140 int less_main(int argc, char **argv) {
1144 flags = bb_getopt_ulflags(argc, argv, "EMmN~");
1152 if (ttyname(STDIN_FILENO) == NULL)
1155 bb_error_msg("Missing filename");
1160 strcpy(filename, (inp_stdin) ? bb_msg_standard_input : files[0]);
1163 tcgetattr(fileno(inp), &term_orig);
1164 term_vi = term_orig;
1165 term_vi.c_lflag &= (~ICANON & ~ECHO);
1166 term_vi.c_iflag &= (~IXON & ~ICRNL);
1167 term_vi.c_oflag &= (~ONLCR);
1168 term_vi.c_cc[VMIN] = 1;
1169 term_vi.c_cc[VTIME] = 0;
1174 keypress = tless_getch();
1175 keypress_process(keypress);