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.
12 * - Add more regular expression support - search modifiers, certain matches, etc.
13 * - Add more complex bracket searching - currently, nested brackets are
15 * - Add support for "F" as an input. This causes less to act in
16 * a similar way to tail -f.
17 * - Allow horizontal scrolling.
20 * - the inp file pointer is used so that keyboard input works after
21 * redirected input has been read from stdin
25 #if ENABLE_FEATURE_LESS_REGEXP
30 /* These are the escape sequences corresponding to special keys */
31 #define REAL_KEY_UP 'A'
32 #define REAL_KEY_DOWN 'B'
33 #define REAL_KEY_RIGHT 'C'
34 #define REAL_KEY_LEFT 'D'
35 #define REAL_PAGE_UP '5'
36 #define REAL_PAGE_DOWN '6'
37 #define REAL_KEY_HOME '7'
38 #define REAL_KEY_END '8'
40 /* These are the special codes assigned by this program to the special keys */
50 /* The escape codes for highlighted and normal text */
51 #define HIGHLIGHT "\033[7m"
52 #define NORMAL "\033[0m"
53 /* The escape code to clear the screen */
54 #define CLEAR "\033[H\033[J"
55 /* The escape code to clear to end of line */
56 #define CLEAR_2_EOL "\033[K"
58 #define MAXLINES CONFIG_FEATURE_LESS_MAXLINES
63 static char *filename;
64 static const char **buffer;
66 static int current_file = 1;
68 static int num_flines;
69 static int num_files = 1;
70 static const char *empty_line_marker = "~";
72 /* Command line options */
77 #define FLAG_TILDE (1<<4)
78 /* hijack command line options variable for internal state vars */
79 #define LESS_STATE_MATCH_BACKWARDS (1<<6)
81 #if ENABLE_FEATURE_LESS_MARKS
82 static int mark_lines[15][2];
86 #if ENABLE_FEATURE_LESS_REGEXP
87 static int *match_lines;
89 static int num_matches;
90 static regex_t pattern;
91 static int pattern_valid;
94 /* Needed termios structures */
95 static struct termios term_orig, term_vi;
97 /* File pointer to get input from */
100 /* Reset terminal input to normal */
101 static void set_tty_cooked(void)
104 tcsetattr(fileno(inp), TCSANOW, &term_orig);
107 /* Exit the program gracefully */
108 static void tless_exit(int code)
110 /* TODO: We really should save the terminal state when we start,
111 and restore it when we exit. Less does this with the
112 "ti" and "te" termcap commands; can this be done with
116 fflush_stdout_and_exit(code);
119 /* Grab a character from input without requiring the return key. If the
120 character is ASCII \033, get more characters and assign certain sequences
121 special return codes. Note that this function works best with raw input. */
122 static int tless_getch(void)
125 /* Set terminal input to raw mode (taken from vi.c) */
126 tcsetattr(fileno(inp), TCSANOW, &term_vi);
129 /* Detect escape sequences (i.e. arrow keys) and handle
132 if (input == '\033' && getc(inp) == '[') {
137 i = input - REAL_KEY_UP;
140 i = input - REAL_PAGE_UP;
145 /* Reject almost all control chars */
146 if (input < ' ' && input != 0x0d && input != 8) goto again;
151 static char* tless_gets(void)
155 char *result = xzalloc(1);
160 if (c == 0x7f) c = 8;
169 result = xrealloc(result, i+1);
171 if (i >= width-1) return result;
175 /* Move the cursor to a position (x,y), where (0,0) is the
176 top-left corner of the console */
177 static void move_cursor(int line, int row)
179 printf("\033[%u;%uH", line, row);
182 static void clear_line(void)
184 printf("\033[%u;0H" CLEAR_2_EOL, height);
187 static void print_hilite(const char *str)
189 printf(HIGHLIGHT"%s"NORMAL, str);
192 static void data_readlines(void)
197 /* "remained unused space" in a line (0 if line fills entire width) */
198 int rem, last_rem = 1; /* "not 0" */
199 char *current_line, *p;
202 fp = filename ? xfopen(filename, "r") : stdin;
204 if (option_mask32 & FLAG_N) {
207 for (i = 0; !feof(fp) && i <= MAXLINES; i++) {
208 flines = xrealloc(flines, (i+1) * sizeof(char *));
209 current_line = xmalloc(w);
216 if (c == '\n') break;
217 /* NUL is substituted by '\n'! */
218 if (c == '\0') c = '\n';
223 die_if_ferror(fp, filename);
225 /* Corner case: linewrap with only "" wrapping to next line */
226 /* Looks ugly on screen, so we do not store this empty line */
227 if (!last_rem && !current_line[0]) {
228 last_rem = 1; /* "not 0" */
233 if (option_mask32 & FLAG_N) {
234 flines[i] = xasprintf((n <= 99999) ? "%5u %s" : "%05u %s",
235 n % 100000, current_line);
240 flines[i] = xrealloc(current_line, strlen(current_line)+1);
243 num_flines = i - 1; /* buggie: 'num_flines' must be 'max_fline' */
245 /* Reset variables for a new file */
252 #if ENABLE_FEATURE_LESS_FLAGS
254 /* Interestingly, writing calc_percent as a function and not a prototype saves around 32 bytes
256 static int calc_percent(void)
258 return ((100 * (line_pos + height - 2) / num_flines) + 1);
261 /* Print a status line if -M was specified */
262 static void m_status_print(void)
268 printf(HIGHLIGHT"%s (file %i of %i) lines %i-%i/%i ",
269 filename, current_file, num_files,
270 line_pos + 1, line_pos + height - 1, num_flines + 1);
272 printf(HIGHLIGHT"%s lines %i-%i/%i ",
273 filename, line_pos + 1, line_pos + height - 1,
277 printf(HIGHLIGHT" %s lines %i-%i/%i ", filename,
278 line_pos + 1, line_pos + height - 1, num_flines + 1);
281 if (line_pos >= num_flines - height + 2) {
282 printf("(END) "NORMAL);
283 if (num_files > 1 && current_file != num_files)
284 printf(HIGHLIGHT"- Next: %s"NORMAL, files[current_file]);
286 percentage = calc_percent();
287 printf("%i%% "NORMAL, percentage);
291 /* Print a status line if -m was specified */
292 static void medium_status_print(void)
295 percentage = calc_percent();
298 printf(HIGHLIGHT"%s %i%%"NORMAL, filename, percentage);
299 else if (line_pos == num_flines - height + 2)
300 print_hilite("(END)");
302 printf(HIGHLIGHT"%i%%"NORMAL, percentage);
306 /* Print the status line */
307 static void status_print(void)
309 /* Change the status if flags have been set */
310 #if ENABLE_FEATURE_LESS_FLAGS
311 if (option_mask32 & FLAG_M)
313 else if (option_mask32 & FLAG_m)
314 medium_status_print();
319 print_hilite(filename);
321 printf(HIGHLIGHT"(file %i of %i)"NORMAL,
322 current_file, num_files);
323 } else if (line_pos == num_flines - height + 2) {
324 print_hilite("(END) ");
325 if (num_files > 1 && current_file != num_files)
326 printf(HIGHLIGHT"- Next: %s"NORMAL, files[current_file]);
330 #if ENABLE_FEATURE_LESS_FLAGS
335 static char controls[] =
336 /**/"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
337 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
338 "\x7f\x9b"; /* DEL and infamous Meta-ESC :( */
339 static char ctrlconv[] =
340 /* Note that on input NUL is converted to '\n' ('\x0a') */
341 /* Therefore we subst '\n' with '@', not 'J' */
342 "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x40\x4b\x4c\x4d\x4e\x4f"
343 "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f";
345 static void print_found(const char *line)
350 regmatch_t match_structs;
353 const char *str = line;
358 n = strcspn(str, controls);
365 n = strspn(str, controls);
372 /* buf[] holds quarantined version of str */
374 /* Each part of the line that matches has the HIGHLIGHT
375 and NORMAL escape sequences placed around it.
376 NB: we regex against line, but insert text
377 from quarantined copy (buf[]) */
383 while (match_status == 0) {
384 char *new = xasprintf("%s%.*s"HIGHLIGHT"%.*s"NORMAL,
386 match_structs.rm_so, str,
387 match_structs.rm_eo - match_structs.rm_so,
388 str + match_structs.rm_so);
389 free(growline); growline = new;
390 str += match_structs.rm_eo;
391 line += match_structs.rm_eo;
394 /* Most of the time doesn't find the regex, optimize for that */
395 match_status = regexec(&pattern, line, 1, &match_structs, eflags);
399 printf("%s"CLEAR_2_EOL"\n", str);
402 printf("%s%s"CLEAR_2_EOL"\n", growline, str);
406 static void print_ascii(const char *str)
413 n = strcspn(str, controls);
416 printf("%.*s", n, str);
419 n = strspn(str, controls);
424 else if (*str == 0x9b)
425 /* VT100's CSI, aka Meta-ESC. Who's inventor? */
426 /* I want to know who committed this sin */
429 *p++ = ctrlconv[(unsigned char)*str];
435 printf("%s"CLEAR_2_EOL"\n", str);
438 /* Print the buffer */
439 static void buffer_print(void)
444 for (i = 0; i < height - 1; i++)
446 print_found(buffer[i]);
448 print_ascii(buffer[i]);
449 fputs(CLEAR_2_EOL, stdout); /* clears status line */
453 /* Initialise the buffer */
454 static void buffer_init(void)
458 /* Fill the buffer until the end of the file or the
459 end of the buffer is reached */
460 for (i = 0; i < height - 1 && i <= num_flines; i++) {
461 buffer[i] = flines[i];
464 /* If the buffer still isn't full, fill it with blank lines */
465 for (; i < height - 1; i++) {
466 buffer[i] = empty_line_marker;
470 /* Move the buffer up and down in the file in order to scroll */
471 static void buffer_down(int nlines)
475 if (line_pos + (height - 3) + nlines < num_flines) {
477 for (i = 0; i < (height - 1); i++) {
478 buffer[i] = flines[line_pos + i];
481 /* As the number of lines requested was too large, we just move
482 to the end of the file */
483 while (line_pos + (height - 3) + 1 < num_flines) {
485 for (i = 0; i < (height - 1); i++) {
486 buffer[i] = flines[line_pos + i];
491 /* We exit if the -E flag has been set */
492 if ((option_mask32 & FLAG_E) && (line_pos + (height - 2) == num_flines))
496 static void buffer_up(int nlines)
501 if (line_pos < 0) line_pos = 0;
502 for (i = 0; i < height - 1; i++) {
503 if (line_pos + i <= num_flines) {
504 buffer[i] = flines[line_pos + i];
506 buffer[i] = empty_line_marker;
511 static void buffer_line(int linenum)
515 if (linenum < 0 || linenum > num_flines) {
517 printf(HIGHLIGHT"%s%u"NORMAL, "Cannot seek to line ", linenum + 1);
521 for (i = 0; i < height - 1; i++) {
522 if (linenum + i <= num_flines)
523 buffer[i] = flines[linenum + i];
525 buffer[i] = empty_line_marker;
532 /* Reinitialise everything for a new file - free the memory and start over */
533 static void reinitialise(void)
537 for (i = 0; i <= num_flines; i++)
546 static void examine_file(void)
551 filename = tless_gets();
552 /* files start by = argv. why we assume that argv is infinitely long??
553 files[num_files] = filename;
554 current_file = num_files + 1;
561 /* This function changes the file currently being paged. direction can be one of the following:
562 * -1: go back one file
563 * 0: go to the first file
564 * 1: go forward one file
566 static void change_file(int direction)
568 if (current_file != ((direction > 0) ? num_files : 1)) {
569 current_file = direction ? current_file + direction : 1;
571 filename = xstrdup(files[current_file - 1]);
575 print_hilite((direction > 0) ? "No next file" : "No previous file");
579 static void remove_current_file(void)
583 if (current_file != 1) {
585 for (i = 3; i <= num_files; i++)
586 files[i - 2] = files[i - 1];
591 for (i = 2; i <= num_files; i++)
592 files[i - 2] = files[i - 1];
599 static void colon_process(void)
603 /* Clear the current line and print a prompt */
607 keypress = tless_getch();
610 remove_current_file();
615 #if ENABLE_FEATURE_LESS_FLAGS
638 static int normalize_match_pos(int match)
642 return (match_pos = 0);
643 if (match >= num_matches)
645 match_pos = num_matches - 1;
650 #if ENABLE_FEATURE_LESS_REGEXP
651 static void goto_match(int match)
653 buffer_line(match_lines[normalize_match_pos(match)]);
656 static void regex_process(void)
658 char *uncomp_regex, *err;
660 /* Reset variables */
661 match_lines = xrealloc(match_lines, sizeof(int));
670 /* Get the uncompiled regular expression from the user */
672 putchar((option_mask32 & LESS_STATE_MATCH_BACKWARDS) ? '?' : '/');
673 uncomp_regex = tless_gets();
674 if (/*!uncomp_regex ||*/ !uncomp_regex[0]) {
680 /* Compile the regex and check for errors */
681 err = regcomp_or_errmsg(&pattern, uncomp_regex, 0);
691 /* Run the regex on each line of the current file */
692 for (match_pos = 0; match_pos <= num_flines; match_pos++) {
693 if (regexec(&pattern, flines[match_pos], 0, NULL, 0) == 0) {
694 match_lines = xrealloc(match_lines, (num_matches+1) * sizeof(int));
695 match_lines[num_matches++] = match_pos;
699 if (num_matches == 0 || num_flines <= height - 2) {
703 for (match_pos = 0; match_pos < num_matches; match_pos++) {
704 if (match_lines[match_pos] > line_pos)
707 if (option_mask32 & LESS_STATE_MATCH_BACKWARDS) match_pos--;
708 buffer_line(match_lines[normalize_match_pos(match_pos)]);
712 static void number_process(int first_digit)
716 char num_input[sizeof(int)*4]; /* more than enough */
719 num_input[0] = first_digit;
721 /* Clear the current line, print a prompt, and then print the digit */
723 printf(":%c", first_digit);
725 /* Receive input until a letter is given */
726 while (i < sizeof(num_input)-1) {
727 num_input[i] = tless_getch();
728 if (!num_input[i] || !isdigit(num_input[i]))
730 putchar(num_input[i]);
734 /* Take the final letter out of the digits string */
735 keypress = num_input[i];
737 num = bb_strtou(num_input, NULL, 10);
738 /* on format error, num == -1 */
739 if (num < 1 || num > MAXLINES) {
744 /* We now know the number and the letter entered, so we process them */
746 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
749 case KEY_UP: case 'b': case 'w': case 'y': case 'u':
752 case 'g': case '<': case 'G': case '>':
753 if (num_flines >= height - 2)
754 buffer_line(num - 1);
757 buffer_line(((num / 100) * num_flines) - 1);
759 #if ENABLE_FEATURE_LESS_REGEXP
761 goto_match(match_pos + num);
764 option_mask32 &= ~LESS_STATE_MATCH_BACKWARDS;
768 option_mask32 |= LESS_STATE_MATCH_BACKWARDS;
777 #if ENABLE_FEATURE_LESS_FLAGCS
778 static void flag_change(void)
784 keypress = tless_getch();
788 option_mask32 ^= FLAG_M;
791 option_mask32 ^= FLAG_m;
794 option_mask32 ^= FLAG_E;
797 option_mask32 ^= FLAG_TILDE;
804 static void show_flag_status(void)
811 keypress = tless_getch();
815 flag_val = option_mask32 & FLAG_M;
818 flag_val = option_mask32 & FLAG_m;
821 flag_val = option_mask32 & FLAG_TILDE;
824 flag_val = option_mask32 & FLAG_N;
827 flag_val = option_mask32 & FLAG_E;
835 printf(HIGHLIGHT"%s%u"NORMAL, "The status of the flag is: ", flag_val != 0);
839 static void full_repaint(void)
841 int temp_line_pos = line_pos;
844 buffer_line(temp_line_pos);
848 static void save_input_to_file(void)
855 printf("Log file: ");
856 current_line = tless_gets();
857 if (strlen(current_line) > 0) {
858 fp = fopen(current_line, "w");
861 print_hilite("Error opening log file");
864 for (i = 0; i < num_flines; i++)
865 fprintf(fp, "%s\n", flines[i]);
871 print_hilite("No log file");
874 #if ENABLE_FEATURE_LESS_MARKS
875 static void add_mark(void)
881 letter = tless_getch();
883 if (isalpha(letter)) {
885 /* If we exceed 15 marks, start overwriting previous ones */
889 mark_lines[num_marks][0] = letter;
890 mark_lines[num_marks][1] = line_pos;
894 print_hilite("Invalid mark letter");
898 static void goto_mark(void)
904 printf("Go to mark: ");
905 letter = tless_getch();
908 if (isalpha(letter)) {
909 for (i = 0; i <= num_marks; i++)
910 if (letter == mark_lines[i][0]) {
911 buffer_line(mark_lines[i][1]);
914 if (num_marks == 14 && letter != mark_lines[14][0])
915 print_hilite("Mark not set");
917 print_hilite("Invalid mark letter");
922 #if ENABLE_FEATURE_LESS_BRACKETS
924 static char opp_bracket(char bracket)
940 static void match_right_bracket(char bracket)
942 int bracket_line = -1;
947 if (strchr(flines[line_pos], bracket) == NULL) {
948 print_hilite("No bracket in top line");
951 for (i = line_pos + 1; i < num_flines; i++) {
952 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
957 if (bracket_line == -1)
958 print_hilite("No matching bracket found");
959 buffer_line(bracket_line - height + 2);
962 static void match_left_bracket(char bracket)
964 int bracket_line = -1;
969 if (strchr(flines[line_pos + height - 2], bracket) == NULL) {
970 print_hilite("No bracket in bottom line");
972 /*printf("%s", flines[line_pos + height]);*/
977 for (i = line_pos + height - 2; i >= 0; i--) {
978 if (strchr(flines[i], opp_bracket(bracket)) != NULL) {
983 if (bracket_line == -1)
984 print_hilite("No matching bracket found");
985 buffer_line(bracket_line);
988 #endif /* FEATURE_LESS_BRACKETS */
990 static void keypress_process(int keypress)
993 case KEY_DOWN: case 'e': case 'j': case 0x0d:
997 case KEY_UP: case 'y': case 'k':
1001 case PAGE_DOWN: case ' ': case 'z':
1002 buffer_down(height - 1);
1005 case PAGE_UP: case 'w': case 'b':
1006 buffer_up(height - 1);
1010 buffer_down((height - 1) / 2);
1014 buffer_up((height - 1) / 2);
1017 case KEY_HOME: case 'g': case 'p': case '<': case '%':
1020 case KEY_END: case 'G': case '>':
1021 buffer_line(num_flines - height + 2);
1026 #if ENABLE_FEATURE_LESS_MARKS
1043 save_input_to_file();
1048 #if ENABLE_FEATURE_LESS_FLAGS
1054 #if ENABLE_FEATURE_LESS_REGEXP
1056 option_mask32 &= ~LESS_STATE_MATCH_BACKWARDS;
1060 goto_match(match_pos + 1);
1063 goto_match(match_pos - 1);
1066 option_mask32 |= LESS_STATE_MATCH_BACKWARDS;
1070 #if ENABLE_FEATURE_LESS_FLAGCS
1079 #if ENABLE_FEATURE_LESS_BRACKETS
1080 case '{': case '(': case '[':
1081 match_right_bracket(keypress);
1083 case '}': case ')': case ']':
1084 match_left_bracket(keypress);
1094 if (isdigit(keypress))
1095 number_process(keypress);
1098 static void sig_catcher(int sig ATTRIBUTE_UNUSED)
1104 int less_main(int argc, char **argv)
1108 getopt32(argc, argv, "EMmN~");
1114 /* Another popular pager, most, detects when stdout
1115 * is not a tty and turns into cat. This makes sense. */
1116 if (!isatty(STDOUT_FILENO))
1117 return bb_cat(argv);
1120 if (isatty(STDIN_FILENO)) {
1121 /* Just "less"? No args and no redirection? */
1122 bb_error_msg("missing filename");
1126 filename = xstrdup(files[0]);
1128 inp = xfopen(CURRENT_TTY, "r");
1130 get_terminal_width_height(fileno(inp), &width, &height);
1131 if (width < 10 || height < 3)
1132 bb_error_msg_and_die("too narrow here");
1134 buffer = xmalloc(height * sizeof(char *));
1135 if (option_mask32 & FLAG_TILDE)
1136 empty_line_marker = "";
1140 tcgetattr(fileno(inp), &term_orig);
1141 signal(SIGTERM, sig_catcher);
1142 signal(SIGINT, sig_catcher);
1143 term_vi = term_orig;
1144 term_vi.c_lflag &= (~ICANON & ~ECHO);
1145 term_vi.c_iflag &= (~IXON & ~ICRNL);
1146 term_vi.c_oflag &= (~ONLCR);
1147 term_vi.c_cc[VMIN] = 1;
1148 term_vi.c_cc[VTIME] = 0;
1154 keypress = tless_getch();
1155 keypress_process(keypress);