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 bb_xasprintf(&flines[i],"%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) ? fopen(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] = (char *) 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] = (char *) 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] = (char *) 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] = (char *) 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] = (char *) 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] = (char *) 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] = (char *) bb_xstrdup(flines[linenum + i]);
488 for (i = 0; i < (height - 1); i++) {
490 if (linenum + i < num_flines + 2)
491 buffer[i] = (char *) bb_xstrdup(flines[linenum + i]);
493 buffer[i] = (char *) 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_xstrndup(filename, (strlen(filename) + 1) * sizeof(char));
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) {
623 bb_xasprintf(&new_line, "%.*s%s%.*s%s%s", start, line, HIGHLIGHT,
624 end - start, line + start, NORMAL, line + end);
628 static char *process_regex_on_line(char *line, regex_t *pattern) {
629 /* This function takes the regex and applies it to the line.
630 Each part of the line that matches has the HIGHLIGHT
631 and NORMAL escape sequences placed around it by
632 insert_highlights, and then the line is returned. */
635 char *line2 = (char *) malloc((sizeof(char) * (strlen(line) + 1)) + 64);
638 regmatch_t match_structs;
640 memset(sub_line, 0, 256);
644 match_status = regexec(pattern, line2, 1, &match_structs, 0);
646 while (match_status == 0) {
648 memset(sub_line, 0, 256);
650 if (match_found == 0)
653 line2 = insert_highlights(line2, match_structs.rm_so + prev_eo, match_structs.rm_eo + prev_eo);
654 if (match_structs.rm_eo + 11 + prev_eo < strlen(line2))
655 strcat(sub_line, line2 + match_structs.rm_eo + 11 + prev_eo);
657 prev_eo += match_structs.rm_eo + 11;
658 match_status = regexec(pattern, sub_line, 1, &match_structs, REG_NOTBOL);
664 static void regex_process(void) {
666 char uncomp_regex[100];
667 char current_line[256];
672 /* Reset variables */
678 pattern = (regex_t *) malloc(sizeof(regex_t));
679 memset(pattern, 0, sizeof(regex_t));
681 /* Get the uncompiled regular expression from the user */
683 putchar((match_backwards) ? '?' : '/');
684 scanf("%s", uncomp_regex);
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] = (char *) bb_xstrndup(current_line, sizeof(char) * (strlen(current_line)+1));
701 if ((match_lines[0] != -1) && (num_flines > height - 2))
702 buffer_line(match_lines[0]);
707 static void goto_match(int match) {
709 /* This goes to a specific match - all line positions of matches are
710 stored within the match_lines[] array. */
711 if ((match < num_matches) && (match >= 0)) {
712 buffer_line(match_lines[match]);
717 static void search_backwards(void) {
719 int current_linepos = line_pos;
725 for (i = 0; i < num_matches; i++) {
726 if (match_lines[i] > current_linepos) {
727 buffer_line(match_lines[i - num_back_match]);
732 /* Reset variables */
739 static void number_process(int first_digit) {
747 num_input[0] = first_digit;
749 /* Clear the current line, print a prompt, and then print the digit */
751 printf(":%c", first_digit);
753 /* Receive input until a letter is given (max 80 chars)*/
754 while((i < 80) && (num_input[i] = tless_getch()) && isdigit(num_input[i])) {
755 printf("%c", num_input[i]);
759 /* Take the final letter out of the digits string */
760 keypress = num_input[i];
762 num = strtol(num_input, &endptr, 10);
763 if (endptr==num_input || *endptr!='\0' || num < 1 || num > MAXLINES)
766 /* We now know the number and the letter entered, so we process them */
768 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
771 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
774 case 'g': case '<': case 'G': case '>':
775 if (num_flines >= height - 2)
776 buffer_line(num - 1);
779 buffer_line(reverse_percent(num));
781 #ifdef CONFIG_FEATURE_LESS_REGEXP
783 goto_match(match_pos + num - 1);
790 num_back_match = num;
801 #ifdef CONFIG_FEATURE_LESS_FLAGCS
802 static void flag_change(void) {
808 keypress = tless_getch();
821 flags &= ~FLAG_TILDE;
828 static void show_flag_status(void) {
835 keypress = tless_getch();
839 flag_val = flags & FLAG_M;
842 flag_val = flags & FLAG_m;
845 flag_val = flags & FLAG_TILDE;
848 flag_val = flags & FLAG_N;
851 flag_val = flags & FLAG_E;
859 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
863 static void full_repaint(void) {
865 int temp_line_pos = line_pos;
868 buffer_line(temp_line_pos);
873 static void save_input_to_file(void) {
875 char current_line[256];
880 printf("Log file: ");
881 fgets(current_line, 256, inp);
882 current_line[strlen(current_line) - 1] = '\0';
883 if (strlen(current_line)) {
884 fp = bb_xfopen(current_line, "w");
885 for (i = 0; i < num_flines; i++)
886 fprintf(fp, "%s", flines[i]);
891 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
894 #ifdef CONFIG_FEATURE_LESS_MARKS
895 static void add_mark(void) {
902 letter = tless_getch();
904 if (isalpha(letter)) {
905 mark_line = line_pos;
907 /* If we exceed 15 marks, start overwriting previous ones */
911 mark_lines[num_marks][0] = letter;
912 mark_lines[num_marks][1] = line_pos;
917 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
921 static void goto_mark(void) {
927 printf("Go to mark: ");
928 letter = tless_getch();
931 if (isalpha(letter)) {
932 for (i = 0; i <= num_marks; i++)
933 if (letter == mark_lines[i][0]) {
934 buffer_line(mark_lines[i][1]);
937 if ((num_marks == 14) && (letter != mark_lines[14][0]))
938 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
941 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
946 #ifdef CONFIG_FEATURE_LESS_BRACKETS
948 static char opp_bracket (char bracket) {
969 static void match_right_bracket(char bracket) {
971 int bracket_line = -1;
976 if (strchr(flines[line_pos], bracket) == NULL)
977 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
979 for (i = line_pos + 1; i < num_flines; i++) {
980 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
986 if (bracket_line == -1)
987 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
989 buffer_line(bracket_line - height + 2);
994 static void match_left_bracket (char bracket) {
996 int bracket_line = -1;
1001 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
1002 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
1003 printf("%s", flines[line_pos + height]);
1007 for (i = line_pos + height - 2; i >= 0; i--) {
1008 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1014 if (bracket_line == -1)
1015 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1017 buffer_line(bracket_line);
1022 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1024 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);