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 regmatch_t match_structs;
675 memset(sub_line, 0, 256);
679 match_status = regexec(pattern, line2, 1, &match_structs, 0);
681 while (match_status == 0) {
683 memset(sub_line, 0, 256);
685 if (match_found == 0)
688 line2 = insert_highlights(line2, match_structs.rm_so + prev_eo, match_structs.rm_eo + prev_eo);
689 if (match_structs.rm_eo + 11 + prev_eo < strlen(line2))
690 strcat(sub_line, line2 + match_structs.rm_eo + 11 + prev_eo);
692 prev_eo += match_structs.rm_eo + 11;
693 match_status = regexec(pattern, sub_line, 1, &match_structs, REG_NOTBOL);
699 static void regex_process(void) {
701 char uncomp_regex[100];
702 char current_line[256];
707 /* Reset variables */
713 pattern = (regex_t *) malloc(sizeof(regex_t));
714 memset(pattern, 0, sizeof(regex_t));
716 /* Get the uncompiled regular expression from the user */
722 scanf("%s", uncomp_regex);
724 /* Compile the regex and check for errors */
725 xregcomp(pattern, uncomp_regex, 0);
727 /* Run the regex on each line of the current file here */
728 for (i = 0; i <= num_flines; i++) {
729 strcpy(current_line, process_regex_on_line(flines[i], pattern));
730 flines[i] = (char *) bb_xstrndup(current_line, sizeof(char) * (strlen(current_line)+1));
740 if ((match_lines[0] != -1) && (num_flines > height - 2))
741 buffer_line(match_lines[0]);
746 static void goto_match(int match) {
748 /* This goes to a specific match - all line positions of matches are
749 stored within the match_lines[] array. */
750 if ((match < num_matches) && (match >= 0)) {
751 buffer_line(match_lines[match]);
756 static void search_backwards(void) {
758 int current_linepos = line_pos;
764 for (i = 0; i < num_matches; i++) {
765 if (match_lines[i] > current_linepos) {
766 buffer_line(match_lines[i - num_back_match]);
771 /* Reset variables */
778 static void number_process(int first_digit) {
784 num_input[0] = first_digit;
786 /* Clear the current line, print a prompt, and then print the digit */
788 printf(":%c", first_digit);
790 /* Receive input until a letter is given */
791 while((num_input[i] = tless_getch()) && isdigit(num_input[i])) {
792 printf("%c",num_input[i]);
796 /* Take the final letter out of the digits string */
797 keypress = num_input[i];
800 num = atoi(num_input);
802 /* We now know the number and the letter entered, so we process them */
804 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
808 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
812 case 'g': case '<': case 'G': case '>':
813 if (num_flines >= height - 2)
814 buffer_line(num - 1);
818 buffer_line(reverse_percent(num));
821 #ifdef CONFIG_FEATURE_LESS_REGEXP
823 goto_match(match_pos + num - 1);
832 num_back_match = num;
842 #ifdef CONFIG_FEATURE_LESS_FLAGCS
843 static void flag_change(void) {
849 keypress = tless_getch();
862 TILDE_FLAG = !TILDE_FLAG;
869 static void show_flag_status(void) {
876 keypress = tless_getch();
886 flag_val = TILDE_FLAG;
900 printf("%s%s%i%s", HIGHLIGHT, "The status of the flag is: ", flag_val != 0, NORMAL);
904 static void full_repaint(void) {
906 int temp_line_pos = line_pos;
909 buffer_line(temp_line_pos);
914 static void save_input_to_file(void) {
916 char current_line[256];
921 printf("Log file: ");
922 fgets(current_line, 256, inp);
923 current_line[strlen(current_line) - 1] = '\0';
924 if (strlen(current_line)) {
925 fp = bb_xfopen(current_line, "w");
926 for (i = 0; i < num_flines; i++)
927 fprintf(fp, "%s", flines[i]);
932 printf("%sNo log file%s", HIGHLIGHT, NORMAL);
935 #ifdef CONFIG_FEATURE_LESS_MARKS
936 static void add_mark(void) {
943 letter = tless_getch();
945 if (isalpha(letter)) {
946 mark_line = line_pos;
948 /* If we exceed 15 marks, start overwriting previous ones */
952 mark_lines[num_marks][0] = letter;
953 mark_lines[num_marks][1] = line_pos;
958 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
962 static void goto_mark(void) {
968 printf("Go to mark: ");
969 letter = tless_getch();
970 if (isalpha(letter)) {
971 for (i = 0; i <= num_marks; i++)
972 if (letter == mark_lines[i][0]) {
973 buffer_line(mark_lines[i][1]);
976 if ((num_marks == 14) && (letter != mark_lines[14][0])) {
978 printf("%s%s%s", HIGHLIGHT, "Mark not set", NORMAL);
983 printf("%s%s%s", HIGHLIGHT, "Invalid mark letter", NORMAL);
989 #ifdef CONFIG_FEATURE_LESS_BRACKETS
991 static char opp_bracket (char bracket) {
1012 static void match_right_bracket(char bracket) {
1014 int bracket_line = -1;
1017 if (strchr(flines[line_pos], bracket) == NULL) {
1019 printf("%s%s%s", HIGHLIGHT, "No bracket in top line", NORMAL);
1022 for (i = line_pos + 1; i < num_flines; i++) {
1023 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1029 if (bracket_line == -1) {
1031 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1034 buffer_line(bracket_line - height + 2);
1039 static void match_left_bracket (char bracket) {
1041 int bracket_line = -1;
1044 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
1046 printf("%s%s%s", HIGHLIGHT, "No bracket in bottom line", NORMAL);
1047 printf("%s", flines[line_pos + height]);
1051 for (i = line_pos + height - 2; i >= 0; i--) {
1052 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
1058 if (bracket_line == -1) {
1060 printf("%s%s%s", HIGHLIGHT, "No matching bracket found", NORMAL);
1063 buffer_line(bracket_line);
1068 #endif /* CONFIG_FEATURE_LESS_BRACKETS */
1070 static void keypress_process(int keypress) {
1072 case KEY_DOWN: case 'e': case 'j': case '\015':
1076 case KEY_UP: case 'y': case 'k':
1080 case PAGE_DOWN: case ' ': case 'z':
1081 buffer_down(height - 1);
1084 case PAGE_UP: case 'w': case 'b':
1085 buffer_up(height - 1);
1089 buffer_down((height - 1) / 2);
1093 buffer_up((height - 1) / 2);
1096 case 'g': case 'p': case '<': case '%':
1097 buffer_up(num_flines + 1);
1101 buffer_down(num_flines + 1);
1107 #ifdef CONFIG_FEATURE_LESS_MARKS
1125 save_input_to_file();
1130 #ifdef CONFIG_FEATURE_LESS_FLAGS
1136 #ifdef CONFIG_FEATURE_LESS_REGEXP
1142 goto_match(match_pos + 1);
1146 goto_match(match_pos - 1);
1154 #ifdef CONFIG_FEATURE_LESS_FLAGCS
1163 #ifdef CONFIG_FEATURE_LESS_BRACKETS
1164 case '{': case '(': case '[':
1165 match_right_bracket(keypress);
1167 case '}': case ')': case ']':
1168 match_left_bracket(keypress);
1177 if (isdigit(keypress))
1178 number_process(keypress);
1181 int less_main(int argc, char **argv) {
1183 unsigned long flags;
1186 flags = bb_getopt_ulflags(argc, argv, "EMNm~");
1187 E_FLAG = (flags & 1);
1188 M_FLAG = (flags & 2);
1189 N_FLAG = (flags & 4);
1190 m_FLAG = (flags & 8);
1191 TILDE_FLAG = (flags & 16);
1199 if (ttyname(STDIN_FILENO) == NULL)
1202 bb_error_msg("Missing filename");
1207 strcpy(filename, (inp_stdin) ? "stdin" : files[0]);
1214 keypress = tless_getch();
1215 keypress_process(keypress);