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[2J"
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];
92 static char buffer[100][256];
93 static char *flines[MAXLINES];
94 static int current_file = 1;
96 static int num_flines;
97 static int num_files = 1;
100 /* Command line options */
105 static int TILDE_FLAG;
107 /* This is needed so that program behaviour changes when input comes from
109 static int inp_stdin;
110 /* This is required so that when a file is requested to be examined after
111 input has come from stdin (e.g. dmesg | less), the input stream from
112 the keyboard still stays the same. If it switched back to stdin, keyboard
113 input wouldn't work. */
114 static int ea_inp_stdin;
116 #ifdef CONFIG_FEATURE_LESS_MARKS
117 static int mark_lines[15][2];
118 static int num_marks;
121 #ifdef CONFIG_FEATURE_LESS_REGEXP
122 static int match_found;
123 static int match_lines[100];
124 static int match_pos;
125 static int num_matches;
126 static int match_backwards;
127 static int num_back_match = 1;
130 /* Needed termios structures */
131 static struct termios term_orig, term_vi;
133 /* File pointer to get input from */
136 /* Reset terminal input to normal */
137 static void set_tty_cooked(void) {
139 tcsetattr(0, TCSANOW, &term_orig);
142 /* Set terminal input to raw mode */
143 static void set_tty_raw(void) {
144 tcgetattr(0, &term_orig);
146 term_vi.c_lflag &= (~ICANON & ~ECHO);
147 term_vi.c_iflag &= (~IXON & ~ICRNL);
148 term_vi.c_oflag &= (~ONLCR);
149 term_vi.c_cc[VMIN] = 1;
150 term_vi.c_cc[VTIME] = 0;
151 tcsetattr(0, TCSANOW, &term_vi);
154 /* Exit the program gracefully */
155 static void tless_exit(int code) {
157 /* TODO: We really should save the terminal state when we start,
158 and restore it when we exit. Less does this with the
159 "ti" and "te" termcap commands; can this be done with
166 /* Grab a character from input without requiring the return key. If the
167 character is ASCII \033, get more characters and assign certain sequences
168 special return codes. Note that this function works best with raw input. */
169 static int tless_getch(void) {
174 input_key[0] = getc(inp);
175 /* Detect escape sequences (i.e. arrow keys) and handle
178 if (input_key[0] == '\033') {
179 input_key[1] = getc(inp);
180 input_key[2] = getc(inp);
182 if (input_key[1] == '[') {
183 if (input_key[2] == REAL_KEY_UP)
185 else if (input_key[2] == REAL_KEY_DOWN)
187 else if (input_key[2] == REAL_KEY_RIGHT)
189 else if (input_key[2] == REAL_KEY_LEFT)
191 else if (input_key[2] == REAL_PAGE_UP)
193 else if (input_key[2] == REAL_PAGE_DOWN)
197 /* The input is a normal ASCII value */
205 /* Move the cursor to a position (x,y), where (0,0) is the
206 top-left corner of the console */
207 static void move_cursor(int x, int y) {
208 printf("\033[%i;%iH", x, y);
211 static void clear_line(void) {
212 move_cursor(height, 0);
216 /* This adds line numbers to every line, as the -N flag necessitates */
217 static void add_linenumbers(void) {
219 char current_line[256];
222 for (i = 0; i <= num_flines; i++) {
223 safe_strncpy(current_line, flines[i], 256);
224 flines[i] = xrealloc(flines[i], strlen(current_line) + 7 );
225 sprintf(flines[i],"%5d %s", i+1, current_line);
229 static void data_readlines(void) {
232 char current_line[256];
235 fp = (inp_stdin) ? stdin : bb_xfopen(filename, "rt");
237 for (i = 0; (!feof(fp)) && (i <= MAXLINES); i++) {
238 strcpy(current_line, "");
239 fgets(current_line, 256, fp);
240 bb_xferror(fp, filename);
241 flines[i] = (char *) bb_xstrndup(current_line, (strlen(current_line) + 1) * sizeof(char));
245 /* Reset variables for a new file */
253 inp = fopen(CURRENT_TTY, "r");
259 inp = fopen(CURRENT_TTY, "r");
266 /* Free the file data */
267 static void free_flines(void) {
271 for (i = 0; i <= num_flines; i++)
275 #ifdef CONFIG_FEATURE_LESS_FLAGS
276 /* Calculate the percentage the current line position is through the file */
277 static int calc_percent(void) {
278 return ((100 * (line_pos + height - 2) / num_flines) + 1);
282 /* Turn a percentage into a line number */
283 static int reverse_percent(int percentage) {
284 double linenum = percentage;
285 linenum = ((linenum / 100) * num_flines) - 1;
289 #ifdef CONFIG_FEATURE_LESS_FLAGS
290 /* Print a status line if -M was specified */
291 static void m_status_print(void) {
298 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);
300 printf("%s%s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
304 printf("%s %s lines %i-%i/%i ", HIGHLIGHT, filename, line_pos + 1, line_pos + height - 1, num_flines + 1);
307 if (line_pos == num_flines - height + 2) {
308 printf("(END) %s", NORMAL);
309 if ((num_files > 1) && (current_file != num_files))
310 printf("%s- Next: %s%s", HIGHLIGHT, files[current_file], NORMAL);
313 percentage = calc_percent();
314 printf("%i%s %s", percentage, "%", NORMAL);
318 printf("%s%s lines %i-%i/%i (END) ", HIGHLIGHT, filename, line_pos + 1, num_flines + 1, num_flines + 1);
319 if ((num_files > 1) && (current_file != num_files))
320 printf("- Next: %s", files[current_file]);
321 printf("%s", NORMAL);
325 /* Print a status line if -m was specified */
326 static void medium_status_print(void) {
329 percentage = calc_percent();
332 printf("%s%s %i%s%s", HIGHLIGHT, filename, percentage, "%", NORMAL);
333 else if (line_pos == num_flines - height + 2)
334 printf("%s(END)%s", HIGHLIGHT, NORMAL);
336 printf("%s%i%s%s", HIGHLIGHT, percentage, "%", NORMAL);
340 /* Print the status line */
341 static void status_print(void) {
343 /* Change the status if flags have been set */
344 #ifdef CONFIG_FEATURE_LESS_FLAGS
348 medium_status_print();
353 printf("%s%s %s", HIGHLIGHT, filename, NORMAL);
355 printf("%s%s%i%s%i%s%s", HIGHLIGHT, "(file ", current_file, " of ", num_files, ")", NORMAL);
357 else if (line_pos == num_flines - height + 2) {
358 printf("%s%s %s", HIGHLIGHT, "(END)", NORMAL);
359 if ((num_files > 1) && (current_file != num_files))
360 printf("%s%s%s%s", HIGHLIGHT, "- Next: ", files[current_file], NORMAL);
365 #ifdef CONFIG_FEATURE_LESS_FLAGS
370 /* Print the buffer */
371 static void buffer_print(void) {
375 if (num_flines >= height - 2) {
378 for (i = 0; i < height - 1; i++)
379 printf("%s", buffer[i]);
385 for (i = 1; i < (height - 1 - num_flines); i++)
387 for (i = 0; i < height - 1; i++)
388 printf("%s", buffer[i]);
393 /* Initialise the buffer */
394 static void buffer_init(void) {
398 for (i = 0; i < (height - 1); i++)
399 memset(buffer[i], '\0', 256);
401 /* Fill the buffer until the end of the file or the
402 end of the buffer is reached */
403 for (i = 0; (i < (height - 1)) && (i <= num_flines); i++) {
404 strcpy(buffer[i], flines[i]);
407 /* If the buffer still isn't full, fill it with blank lines */
408 for (; i < (height - 1); i++) {
409 strcpy(buffer[i], "");
413 /* Move the buffer up and down in the file in order to scroll */
414 static void buffer_down(int nlines) {
419 if (line_pos + (height - 3) + nlines < num_flines) {
421 for (i = 0; i < (height - 1); i++)
422 strcpy(buffer[i], flines[line_pos + i]);
425 /* As the number of lines requested was too large, we just move
426 to the end of the file */
427 while (line_pos + (height - 3) + 1 < num_flines) {
429 for (i = 0; i < (height - 1); i++)
430 strcpy(buffer[i], flines[line_pos + i]);
434 /* We exit if the -E flag has been set */
435 if (E_FLAG && (line_pos + (height - 2) == num_flines))
440 static void buffer_up(int nlines) {
446 if (line_pos - nlines >= 0) {
448 for (i = 0; i < (height - 1); i++)
449 strcpy(buffer[i], flines[line_pos + i]);
452 /* As the requested number of lines to move was too large, we
453 move one line up at a time until we can't. */
454 while (line_pos != 0) {
456 for (i = 0; i < (height - 1); i++)
457 strcpy(buffer[i], flines[line_pos + i]);
462 /* Work out where the tildes start */
463 tilde_line = num_flines - line_pos + 3;
466 /* Going backwards nlines lines has taken us to a point where
467 nothing is past the EOF, so we revert to normal. */
468 if (line_pos < num_flines - height + 3) {
473 /* We only move part of the buffer, as the rest
475 for (i = 0; i < (height - 1); i++) {
476 if (i < tilde_line - nlines + 1)
477 strcpy(buffer[i], flines[line_pos + i]);
479 if (line_pos >= num_flines - height + 2)
480 strcpy(buffer[i], "~\n");
487 static void buffer_line(int linenum) {
493 if (linenum < 1 || linenum > num_flines) {
495 printf("%s%s%i%s", HIGHLIGHT, "Cannot seek to line number ", linenum, NORMAL);
497 else if (linenum < (num_flines - height - 2)) {
498 for (i = 0; i < (height - 1); i++)
499 strcpy(buffer[i], flines[linenum + i]);
503 for (i = 0; i < (height - 1); i++) {
504 if (linenum + i < num_flines + 2)
505 strcpy(buffer[i], flines[linenum + i]);
507 strcpy(buffer[i], (TILDE_FLAG) ? "\n" : "~\n");
510 /* Set past_eof so buffer_down and buffer_up act differently */
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;
541 static void next_file(void) {
542 if (current_file != num_files) {
544 strcpy(filename, files[current_file - 1]);
552 printf("%s%s%s", HIGHLIGHT, "No next file", NORMAL);
556 static void previous_file(void) {
557 if (current_file != 1) {
559 strcpy(filename, files[current_file - 1]);
568 printf("%s%s%s", HIGHLIGHT, "No previous file", NORMAL);
572 static void first_file(void) {
573 if (current_file != 1) {
575 strcpy(filename, files[current_file - 1]);
583 static void remove_current_file(void) {
587 if (current_file != 1) {
589 for (i = 3; i <= num_files; i++)
590 files[i - 2] = files[i - 1];
596 for (i = 2; i <= num_files; i++)
597 files[i - 2] = files[i - 1];
604 static void colon_process(void) {
608 /* Clear the current line and print a prompt */
612 keypress = tless_getch();
615 remove_current_file();
620 #ifdef CONFIG_FEATURE_LESS_FLAGS
643 #ifdef CONFIG_FEATURE_LESS_REGEXP
644 /* The below two regular expression handler functions NEED development. */
646 /* Get a regular expression from the user, and then go through the current
647 file line by line, running a processing regex function on each one. */
649 static char *insert_highlights (char *line, int start, int end) {
651 char *new_line = (char *) malloc((sizeof(char) * (strlen(line) + 1)) + 10);
653 memset(new_line, 0, ((sizeof(char) * (strlen(line) + 1)) + 10));
654 strncat(new_line, line, start);
655 strcat(new_line, HIGHLIGHT);
656 strncat(new_line, line + start, end - start);
657 strcat(new_line, NORMAL);
658 strncat(new_line, line + end, strlen(line) - end);
663 static char *process_regex_on_line(char *line, regex_t *pattern) {
664 /* This function takes the regex and applies it to the line.
665 Each part of the line that matches has the HIGHLIGHT
666 and NORMAL escape sequences placed around it by
667 insert_highlights, and then the line is returned. */
670 char *line2 = (char *) malloc((sizeof(char) * (strlen(line) + 1)) + 64);
673 memset(sub_line, 0, 256);
675 regmatch_t match_structs;
678 match_status = regexec(pattern, line2, 1, &match_structs, 0);
680 while (match_status == 0) {
682 memset(sub_line, 0, 256);
684 if (match_found == 0)
687 line2 = insert_highlights(line2, match_structs.rm_so + prev_eo, match_structs.rm_eo + prev_eo);
688 if (match_structs.rm_eo + 11 + prev_eo < strlen(line2))
689 strcat(sub_line, line2 + match_structs.rm_eo + 11 + prev_eo);
691 prev_eo += match_structs.rm_eo + 11;
692 match_status = regexec(pattern, sub_line, 1, &match_structs, REG_NOTBOL);
698 static void regex_process(void) {
700 char uncomp_regex[100];
701 char current_line[256];
706 /* Reset variables */
712 pattern = (regex_t *) malloc(sizeof(regex_t));
713 memset(pattern, 0, sizeof(regex_t));
715 /* Get the uncompiled regular expression from the user */
721 scanf("%s", uncomp_regex);
723 /* Compile the regex and check for errors */
724 xregcomp(pattern, uncomp_regex, 0);
726 /* Run the regex on each line of the current file here */
727 for (i = 0; i <= num_flines; i++) {
728 strcpy(current_line, process_regex_on_line(flines[i], pattern));
729 flines[i] = (char *) bb_xstrndup(current_line, sizeof(char) * (strlen(current_line)+1));
739 if ((match_lines[0] != -1) && (num_flines > height - 2))
740 buffer_line(match_lines[0]);
745 static void goto_match(int match) {
747 /* This goes to a specific match - all line positions of matches are
748 stored within the match_lines[] array. */
749 if ((match < num_matches) && (match >= 0)) {
750 buffer_line(match_lines[match]);
755 static void search_backwards(void) {
757 int current_linepos = line_pos;
763 for (i = 0; i < num_matches; i++) {
764 if (match_lines[i] > current_linepos) {
765 buffer_line(match_lines[i - num_back_match]);
770 /* Reset variables */
777 static void number_process(int first_digit) {
783 num_input[0] = first_digit;
785 /* Clear the current line, print a prompt, and then print the digit */
787 printf(":%c", first_digit);
789 /* Receive input until a letter is given */
790 while((num_input[i] = tless_getch()) && isdigit(num_input[i])) {
791 printf("%c",num_input[i]);
795 /* Take the final letter out of the digits string */
796 keypress = num_input[i];
799 num = atoi(num_input);
801 /* We now know the number and the letter entered, so we process them */
803 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
807 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
811 case 'g': case '<': case 'G': case '>':
812 if (num_flines >= height - 2)
813 buffer_line(num - 1);
817 buffer_line(reverse_percent(num));
820 #ifdef CONFIG_FEATURE_LESS_REGEXP
822 goto_match(match_pos + num - 1);
831 num_back_match = num;
841 #ifdef CONFIG_FEATURE_LESS_FLAGCS
842 static void flag_change(void) {
848 keypress = tless_getch();
861 TILDE_FLAG = !TILDE_FLAG;
868 static void show_flag_status(void) {
875 keypress = tless_getch();
885 flag_val = TILDE_FLAG;
899 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
903 static void full_repaint(void) {
905 int temp_line_pos = line_pos;
908 buffer_line(temp_line_pos);
913 static void save_input_to_file(void) {
915 char current_line[256];
920 printf("Log file: ");
921 fgets(current_line, 256, inp);
922 current_line[strlen(current_line) - 1] = '\0';
923 if (strlen(current_line)) {
924 fp = bb_xfopen(current_line, "w");
925 for (i = 0; i < num_flines; i++)
926 fprintf(fp, "%s", flines[i]);
931 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
934 #ifdef CONFIG_FEATURE_LESS_MARKS
935 static void add_mark(void) {
942 letter = tless_getch();
944 if (isalpha(letter)) {
945 mark_line = line_pos;
947 /* If we exceed 15 marks, start overwriting previous ones */
951 mark_lines[num_marks][0] = letter;
952 mark_lines[num_marks][1] = line_pos;
957 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
961 static void goto_mark(void) {
967 printf("Go to mark: ");
968 letter = tless_getch();
969 if (isalpha(letter)) {
970 for (i = 0; i <= num_marks; i++)
971 if (letter == mark_lines[i][0]) {
972 buffer_line(mark_lines[i][1]);
975 if ((num_marks == 14) && (letter != mark_lines[14][0])) {
977 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
982 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
988 #ifdef CONFIG_FEATURE_LESS_BRACKETS
990 static char opp_bracket (char bracket) {
1011 static void match_right_bracket(char bracket) {
1013 int bracket_line = -1;
1016 if (strchr(flines[line_pos], bracket) == NULL) {
1018 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
1021 for (i = line_pos + 1; i < num_flines; i++) {
1022 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1028 if (bracket_line == -1) {
1030 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1033 buffer_line(bracket_line - height + 2);
1038 static void match_left_bracket (char bracket) {
1040 int bracket_line = -1;
1043 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
1045 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
1046 printf("%s", flines[line_pos + height]);
1050 for (i = line_pos + height - 2; i >= 0; i--) {
1051 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1057 if (bracket_line == -1) {
1059 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1062 buffer_line(bracket_line);
1067 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1069 static void keypress_process(int keypress) {
1071 case KEY_DOWN: case 'e': case 'j': case '\015':
1075 case KEY_UP: case 'y': case 'k':
1079 case PAGE_DOWN: case ' ': case 'z':
1080 buffer_down(height - 1);
1083 case PAGE_UP: case 'w': case 'b':
1084 buffer_up(height - 1);
1088 buffer_down((height - 1) / 2);
1092 buffer_up((height - 1) / 2);
1095 case 'g': case 'p': case '<': case '%':
1096 buffer_up(num_flines + 1);
1100 buffer_down(num_flines + 1);
1106 #ifdef CONFIG_FEATURE_LESS_MARKS
1124 save_input_to_file();
1129 #ifdef CONFIG_FEATURE_LESS_FLAGS
1135 #ifdef CONFIG_FEATURE_LESS_REGEXP
1141 goto_match(match_pos + 1);
1145 goto_match(match_pos - 1);
1153 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1162 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1163 case '{': case '(': case '[':
1164 match_right_bracket(keypress);
1166 case '}': case ')': case ']':
1167 match_left_bracket(keypress);
1176 if (isdigit(keypress))
1177 number_process(keypress);
1180 int less_main(int argc, char **argv) {
1182 unsigned long flags;
1185 flags = bb_getopt_ulflags(argc, argv, "EMNm~");
1186 E_FLAG = (flags & 1);
1187 M_FLAG = (flags & 2);
1188 N_FLAG = (flags & 4);
1189 m_FLAG = (flags & 8);
1190 TILDE_FLAG = (flags & 16);
1198 if (ttyname(STDIN_FILENO) == NULL)
1201 bb_error_msg("Missing filename");
1206 strcpy(filename, (inp_stdin) ? "stdin" : files[0]);
1213 keypress = tless_getch();
1214 keypress_process(keypress);