1/* vi: set sw=4 ts=4: */ 2/* 3 * Mini less implementation for busybox 4 * 5 * Copyright (C) 2005 by Rob Sullivan <cogito.ergo.cogito@gmail.com> 6 * 7 * Licensed under the GPL v2 or later, see the file LICENSE in this tarball. 8 */ 9 10/* 11 * TODO: 12 * - Add more regular expression support - search modifiers, certain matches, etc. 13 * - Add more complex bracket searching - currently, nested brackets are 14 * not considered. 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. 18 * 19 * Notes: 20 * - the inp file pointer is used so that keyboard input works after 21 * redirected input has been read from stdin 22 */ 23 24#include <sched.h> /* sched_yield() */ 25 26#include "libbb.h" 27#if ENABLE_FEATURE_LESS_REGEXP 28#include "xregex.h" 29#endif 30 31#undef ENABLE_FEATURE_LESS_FLAGCS 32#define ENABLE_FEATURE_LESS_FLAGCS 0 33 34/* The escape codes for highlighted and normal text */ 35#define HIGHLIGHT "\033[7m" 36#define NORMAL "\033[0m" 37/* The escape code to clear the screen */ 38#define CLEAR "\033[H\033[J" 39/* The escape code to clear to end of line */ 40#define CLEAR_2_EOL "\033[K" 41 42/* These are the escape sequences corresponding to special keys */ 43enum { 44 REAL_KEY_UP = 'A', 45 REAL_KEY_DOWN = 'B', 46 REAL_KEY_RIGHT = 'C', 47 REAL_KEY_LEFT = 'D', 48 REAL_PAGE_UP = '5', 49 REAL_PAGE_DOWN = '6', 50 REAL_KEY_HOME = '7', // vt100? linux vt? or what? 51 REAL_KEY_END = '8', 52 REAL_KEY_HOME_ALT = '1', // ESC [1~ (vt100? linux vt? or what?) 53 REAL_KEY_END_ALT = '4', // ESC [4~ 54 REAL_KEY_HOME_XTERM = 'H', 55 REAL_KEY_END_XTERM = 'F', 56 57/* These are the special codes assigned by this program to the special keys */ 58 KEY_UP = 20, 59 KEY_DOWN = 21, 60 KEY_RIGHT = 22, 61 KEY_LEFT = 23, 62 PAGE_UP = 24, 63 PAGE_DOWN = 25, 64 KEY_HOME = 26, 65 KEY_END = 27, 66 67/* Absolute max of lines eaten */ 68 MAXLINES = CONFIG_FEATURE_LESS_MAXLINES, 69 70/* This many "after the end" lines we will show (at max) */ 71 TILDES = 1, 72}; 73 74/* Command line options */ 75enum { 76 FLAG_E = 1, 77 FLAG_M = 1 << 1, 78 FLAG_m = 1 << 2, 79 FLAG_N = 1 << 3, 80 FLAG_TILDE = 1 << 4, 81/* hijack command line options variable for internal state vars */ 82 LESS_STATE_MATCH_BACKWARDS = 1 << 15, 83}; 84 85#if !ENABLE_FEATURE_LESS_REGEXP 86enum { pattern_valid = 0 }; 87#endif 88 89struct globals { 90 int cur_fline; /* signed */ 91 int kbd_fd; /* fd to get input from */ 92/* last position in last line, taking into account tabs */ 93 size_t linepos; 94 unsigned max_displayed_line; 95 unsigned max_fline; 96 unsigned max_lineno; /* this one tracks linewrap */ 97 unsigned width; 98 ssize_t eof_error; /* eof if 0, error if < 0 */ 99 size_t readpos; 100 size_t readeof; 101 const char **buffer; 102 const char **flines; 103 const char *empty_line_marker; 104 unsigned num_files; 105 unsigned current_file; 106 char *filename; 107 char **files; 108#if ENABLE_FEATURE_LESS_MARKS 109 unsigned num_marks; 110 unsigned mark_lines[15][2]; 111#endif 112#if ENABLE_FEATURE_LESS_REGEXP 113 unsigned *match_lines; 114 int match_pos; /* signed! */ 115 unsigned num_matches; 116 regex_t pattern; 117 smallint pattern_valid; 118#endif 119 smallint terminated; 120 struct termios term_orig, term_less; 121}; 122#define G (*ptr_to_globals) 123#define cur_fline (G.cur_fline ) 124#define kbd_fd (G.kbd_fd ) 125#define linepos (G.linepos ) 126#define max_displayed_line (G.max_displayed_line) 127#define max_fline (G.max_fline ) 128#define max_lineno (G.max_lineno ) 129#define width (G.width ) 130#define eof_error (G.eof_error ) 131#define readpos (G.readpos ) 132#define readeof (G.readeof ) 133#define buffer (G.buffer ) 134#define flines (G.flines ) 135#define empty_line_marker (G.empty_line_marker ) 136#define num_files (G.num_files ) 137#define current_file (G.current_file ) 138#define filename (G.filename ) 139#define files (G.files ) 140#define num_marks (G.num_marks ) 141#define mark_lines (G.mark_lines ) 142#if ENABLE_FEATURE_LESS_REGEXP 143#define match_lines (G.match_lines ) 144#define match_pos (G.match_pos ) 145#define num_matches (G.num_matches ) 146#define pattern (G.pattern ) 147#define pattern_valid (G.pattern_valid ) 148#endif 149#define terminated (G.terminated ) 150#define term_orig (G.term_orig ) 151#define term_less (G.term_less ) 152#define INIT_G() do { \ 153 PTR_TO_GLOBALS = xzalloc(sizeof(G)); \ 154 empty_line_marker = "~"; \ 155 num_files = 1; \ 156 current_file = 1; \ 157 eof_error = 1; \ 158 terminated = 1; \ 159 } while (0) 160 161/* Reset terminal input to normal */ 162static void set_tty_cooked(void) 163{ 164 fflush(stdout); 165 tcsetattr(kbd_fd, TCSANOW, &term_orig); 166} 167 168/* Exit the program gracefully */ 169static void less_exit(int code) 170{ 171 /* TODO: We really should save the terminal state when we start, 172 * and restore it when we exit. Less does this with the 173 * "ti" and "te" termcap commands; can this be done with 174 * only termios.h? */ 175 putchar('\n'); 176 fflush_stdout_and_exit(code); 177} 178 179/* Move the cursor to a position (x,y), where (0,0) is the 180 top-left corner of the console */ 181static void move_cursor(int line, int row) 182{ 183 printf("\033[%u;%uH", line, row); 184} 185 186static void clear_line(void) 187{ 188 printf("\033[%u;0H" CLEAR_2_EOL, max_displayed_line + 2); 189} 190 191static void print_hilite(const char *str) 192{ 193 printf(HIGHLIGHT"%s"NORMAL, str); 194} 195 196static void print_statusline(const char *str) 197{ 198 clear_line(); 199 printf(HIGHLIGHT"%.*s"NORMAL, width - 1, str); 200} 201 202#if ENABLE_FEATURE_LESS_REGEXP 203static void fill_match_lines(unsigned pos); 204#else 205#define fill_match_lines(pos) ((void)0) 206#endif 207 208/* Devilishly complex routine. 209 * 210 * Has to deal with EOF and EPIPE on input, 211 * with line wrapping, with last line not ending in '\n' 212 * (possibly not ending YET!), with backspace and tabs. 213 * It reads input again if last time we got an EOF (thus supporting 214 * growing files) or EPIPE (watching output of slow process like make). 215 * 216 * Variables used: 217 * flines[] - array of lines already read. Linewrap may cause 218 * one source file line to occupy several flines[n]. 219 * flines[max_fline] - last line, possibly incomplete. 220 * terminated - 1 if flines[max_fline] is 'terminated' 221 * (if there was '\n' [which isn't stored itself, we just remember 222 * that it was seen]) 223 * max_lineno - last line's number, this one doesn't increment 224 * on line wrap, only on "real" new lines. 225 * readbuf[0..readeof-1] - small preliminary buffer. 226 * readbuf[readpos] - next character to add to current line. 227 * linepos - screen line position of next char to be read 228 * (takes into account tabs and backspaces) 229 * eof_error - < 0 error, == 0 EOF, > 0 not EOF/error 230 */ 231static void read_lines(void) 232{ 233#define readbuf bb_common_bufsiz1 234 char *current_line, *p; 235 USE_FEATURE_LESS_REGEXP(unsigned old_max_fline = max_fline;) 236 int w = width; 237 char last_terminated = terminated; 238 239 if (option_mask32 & FLAG_N) 240 w -= 8; 241 242 current_line = xmalloc(w); 243 p = current_line; 244 max_fline += last_terminated; 245 if (!last_terminated) { 246 const char *cp = flines[max_fline]; 247 if (option_mask32 & FLAG_N) 248 cp += 8; 249 strcpy(current_line, cp); 250 p += strlen(current_line); 251 /* linepos is still valid from previous read_lines() */ 252 } else { 253 linepos = 0; 254 } 255 256 while (1) { 257 again: 258 *p = '\0'; 259 terminated = 0; 260 while (1) { 261 char c; 262 /* if no unprocessed chars left, eat more */ 263 if (readpos >= readeof) { 264 smallint yielded = 0; 265 266 ndelay_on(0); 267 read_again: 268 eof_error = safe_read(0, readbuf, sizeof(readbuf)); 269 readpos = 0; 270 readeof = eof_error; 271 if (eof_error < 0) { 272 if (errno == EAGAIN && !yielded) { 273 /* We can hit EAGAIN while searching for regexp match. 274 * Yield is not 100% reliable solution in general, 275 * but for less it should be good enough - 276 * we give stdin supplier some CPU time to produce 277 * more input. We do it just once. 278 * Currently, we do not stop when we found the Nth 279 * occurrence we were looking for. We read till end 280 * (or double EAGAIN). TODO? */ 281 sched_yield(); 282 yielded = 1; 283 goto read_again; 284 } 285 readeof = 0; 286 if (errno != EAGAIN) 287 print_statusline("read error"); 288 } 289 ndelay_off(0); 290 291 if (eof_error <= 0) { 292 goto reached_eof; 293 } 294 } 295 c = readbuf[readpos]; 296 /* backspace? [needed for manpages] */ 297 /* <tab><bs> is (a) insane and */ 298 /* (b) harder to do correctly, so we refuse to do it */ 299 if (c == '\x8' && linepos && p[-1] != '\t') { 300 readpos++; /* eat it */ 301 linepos--; 302 /* was buggy (p could end up <= current_line)... */ 303 *--p = '\0'; 304 continue; 305 } 306 { 307 size_t new_linepos = linepos + 1; 308 if (c == '\t') { 309 new_linepos += 7; 310 new_linepos &= (~7); 311 } 312 if (new_linepos >= w) 313 break; 314 linepos = new_linepos; 315 } 316 /* ok, we will eat this char */ 317 readpos++; 318 if (c == '\n') { 319 terminated = 1; 320 linepos = 0; 321 break; 322 } 323 /* NUL is substituted by '\n'! */ 324 if (c == '\0') c = '\n'; 325 *p++ = c; 326 *p = '\0'; 327 } 328 /* Corner case: linewrap with only "" wrapping to next line */ 329 /* Looks ugly on screen, so we do not store this empty line */ 330 if (!last_terminated && !current_line[0]) { 331 last_terminated = 1; 332 max_lineno++; 333 goto again; 334 } 335 reached_eof: 336 last_terminated = terminated; 337 flines = xrealloc(flines, (max_fline+1) * sizeof(char *)); 338 if (option_mask32 & FLAG_N) { 339 /* Width of 7 preserves tab spacing in the text */ 340 flines[max_fline] = xasprintf( 341 (max_lineno <= 9999999) ? "%7u %s" : "%07u %s", 342 max_lineno % 10000000, current_line); 343 free(current_line); 344 if (terminated) 345 max_lineno++; 346 } else { 347 flines[max_fline] = xrealloc(current_line, strlen(current_line)+1); 348 } 349 if (max_fline >= MAXLINES) { 350 eof_error = 0; /* Pretend we saw EOF */ 351 break; 352 } 353 if (max_fline > cur_fline + max_displayed_line) 354 break; 355 if (eof_error <= 0) { 356 if (eof_error < 0 && errno == EAGAIN) { 357 /* not yet eof or error, reset flag (or else 358 * we will hog CPU - select() will return 359 * immediately */ 360 eof_error = 1; 361 } 362 break; 363 } 364 max_fline++; 365 current_line = xmalloc(w); 366 p = current_line; 367 linepos = 0; 368 } 369 fill_match_lines(old_max_fline); 370#undef readbuf 371} 372 373#if ENABLE_FEATURE_LESS_FLAGS 374/* Interestingly, writing calc_percent as a function saves around 32 bytes 375 * on my build. */ 376static int calc_percent(void) 377{ 378 unsigned p = (100 * (cur_fline+max_displayed_line+1) + max_fline/2) / (max_fline+1); 379 return p <= 100 ? p : 100; 380} 381 382/* Print a status line if -M was specified */ 383static void m_status_print(void) 384{ 385 int percentage; 386 387 clear_line(); 388 printf(HIGHLIGHT"%s", filename); 389 if (num_files > 1) 390 printf(" (file %i of %i)", current_file, num_files); 391 printf(" lines %i-%i/%i ", 392 cur_fline + 1, cur_fline + max_displayed_line + 1, 393 max_fline + 1); 394 if (cur_fline >= max_fline - max_displayed_line) { 395 printf("(END)"NORMAL); 396 if (num_files > 1 && current_file != num_files) 397 printf(HIGHLIGHT" - next: %s"NORMAL, files[current_file]); 398 return; 399 } 400 percentage = calc_percent(); 401 printf("%i%%"NORMAL, percentage); 402} 403#endif 404 405/* Print the status line */ 406static void status_print(void) 407{ 408 const char *p; 409 410 /* Change the status if flags have been set */ 411#if ENABLE_FEATURE_LESS_FLAGS 412 if (option_mask32 & (FLAG_M|FLAG_m)) { 413 m_status_print(); 414 return; 415 } 416 /* No flags set */ 417#endif 418 419 clear_line(); 420 if (cur_fline && cur_fline < max_fline - max_displayed_line) { 421 putchar(':'); 422 return; 423 } 424 p = "(END)"; 425 if (!cur_fline) 426 p = filename; 427 if (num_files > 1) { 428 printf(HIGHLIGHT"%s (file %i of %i)"NORMAL, 429 p, current_file, num_files); 430 return; 431 } 432 print_hilite(p); 433} 434 435static void cap_cur_fline(int nlines) 436{ 437 int diff; 438 if (cur_fline < 0) 439 cur_fline = 0; 440 if (cur_fline + max_displayed_line > max_fline + TILDES) { 441 cur_fline -= nlines; 442 if (cur_fline < 0) 443 cur_fline = 0; 444 diff = max_fline - (cur_fline + max_displayed_line) + TILDES; 445 /* As the number of lines requested was too large, we just move 446 to the end of the file */ 447 if (diff > 0) 448 cur_fline += diff; 449 } 450} 451 452static const char controls[] ALIGN1 = 453 /* NUL: never encountered; TAB: not converted */ 454 /**/"\x01\x02\x03\x04\x05\x06\x07\x08" "\x0a\x0b\x0c\x0d\x0e\x0f" 455 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" 456 "\x7f\x9b"; /* DEL and infamous Meta-ESC :( */ 457static const char ctrlconv[] ALIGN1 = 458 /* '\n': it's a former NUL - subst with '@', not 'J' */ 459 "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x40\x4b\x4c\x4d\x4e\x4f" 460 "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"; 461 462#if ENABLE_FEATURE_LESS_REGEXP 463static void print_found(const char *line) 464{ 465 int match_status; 466 int eflags; 467 char *growline; 468 regmatch_t match_structs; 469 470 char buf[width]; 471 const char *str = line; 472 char *p = buf; 473 size_t n; 474 475 while (*str) { 476 n = strcspn(str, controls); 477 if (n) { 478 if (!str[n]) break; 479 memcpy(p, str, n); 480 p += n; 481 str += n; 482 } 483 n = strspn(str, controls); 484 memset(p, '.', n); 485 p += n; 486 str += n; 487 } 488 strcpy(p, str); 489 490 /* buf[] holds quarantined version of str */ 491 492 /* Each part of the line that matches has the HIGHLIGHT 493 and NORMAL escape sequences placed around it. 494 NB: we regex against line, but insert text 495 from quarantined copy (buf[]) */ 496 str = buf; 497 growline = NULL; 498 eflags = 0; 499 goto start; 500 501 while (match_status == 0) { 502 char *new = xasprintf("%s%.*s"HIGHLIGHT"%.*s"NORMAL, 503 growline ? : "", 504 match_structs.rm_so, str, 505 match_structs.rm_eo - match_structs.rm_so, 506 str + match_structs.rm_so); 507 free(growline); growline = new; 508 str += match_structs.rm_eo; 509 line += match_structs.rm_eo; 510 eflags = REG_NOTBOL; 511 start: 512 /* Most of the time doesn't find the regex, optimize for that */ 513 match_status = regexec(&pattern, line, 1, &match_structs, eflags); 514 } 515 516 if (!growline) { 517 printf(CLEAR_2_EOL"%s\n", str); 518 return; 519 } 520 printf(CLEAR_2_EOL"%s%s\n", growline, str); 521 free(growline); 522} 523#else 524void print_found(const char *line); 525#endif 526 527static void print_ascii(const char *str) 528{ 529 char buf[width]; 530 char *p; 531 size_t n; 532 533 printf(CLEAR_2_EOL); 534 while (*str) { 535 n = strcspn(str, controls); 536 if (n) { 537 if (!str[n]) break; 538 printf("%.*s", (int) n, str); 539 str += n; 540 } 541 n = strspn(str, controls); 542 p = buf; 543 do { 544 if (*str == 0x7f) 545 *p++ = '?'; 546 else if (*str == (char)0x9b) 547 /* VT100's CSI, aka Meta-ESC. Who's inventor? */ 548 /* I want to know who committed this sin */ 549 *p++ = '{'; 550 else 551 *p++ = ctrlconv[(unsigned char)*str]; 552 str++; 553 } while (--n); 554 *p = '\0'; 555 print_hilite(buf); 556 } 557 puts(str); 558} 559 560/* Print the buffer */ 561static void buffer_print(void) 562{ 563 int i; 564 565 move_cursor(0, 0); 566 for (i = 0; i <= max_displayed_line; i++) 567 if (pattern_valid) 568 print_found(buffer[i]); 569 else 570 print_ascii(buffer[i]); 571 status_print(); 572} 573 574static void buffer_fill_and_print(void) 575{ 576 int i; 577 for (i = 0; i <= max_displayed_line && cur_fline + i <= max_fline; i++) { 578 buffer[i] = flines[cur_fline + i]; 579 } 580 for (; i <= max_displayed_line; i++) { 581 buffer[i] = empty_line_marker; 582 } 583 buffer_print(); 584} 585 586/* Move the buffer up and down in the file in order to scroll */ 587static void buffer_down(int nlines) 588{ 589 cur_fline += nlines; 590 read_lines(); 591 cap_cur_fline(nlines); 592 buffer_fill_and_print(); 593} 594 595static void buffer_up(int nlines) 596{ 597 cur_fline -= nlines; 598 if (cur_fline < 0) cur_fline = 0; 599 read_lines(); 600 buffer_fill_and_print(); 601} 602 603static void buffer_line(int linenum) 604{ 605 if (linenum < 0) 606 linenum = 0; 607 cur_fline = linenum; 608 read_lines(); 609 if (linenum + max_displayed_line > max_fline) 610 linenum = max_fline - max_displayed_line + TILDES; 611 if (linenum < 0) 612 linenum = 0; 613 cur_fline = linenum; 614 buffer_fill_and_print(); 615} 616 617static void open_file_and_read_lines(void) 618{ 619 if (filename) { 620 int fd = xopen(filename, O_RDONLY); 621 dup2(fd, 0); 622 if (fd) close(fd); 623 } else { 624 /* "less" with no arguments in argv[] */ 625 /* For status line only */ 626 filename = xstrdup(bb_msg_standard_input); 627 } 628 readpos = 0; 629 readeof = 0; 630 linepos = 0; 631 terminated = 1; 632 read_lines(); 633} 634 635/* Reinitialize everything for a new file - free the memory and start over */ 636static void reinitialize(void) 637{ 638 int i; 639 640 if (flines) { 641 for (i = 0; i <= max_fline; i++) 642 free((void*)(flines[i])); 643 free(flines); 644 flines = NULL; 645 } 646 647 max_fline = -1; 648 cur_fline = 0; 649 max_lineno = 0; 650 open_file_and_read_lines(); 651 buffer_fill_and_print(); 652} 653 654static void getch_nowait(char* input, int sz) 655{ 656 ssize_t rd; 657 fd_set readfds; 658 again: 659 fflush(stdout); 660 661 /* NB: select returns whenever read will not block. Therefore: 662 * (a) with O_NONBLOCK'ed fds select will return immediately 663 * (b) if eof is reached, select will also return 664 * because read will immediately return 0 bytes. 665 * Even if select says that input is available, read CAN block 666 * (switch fd into O_NONBLOCK'ed mode to avoid it) 667 */ 668 FD_ZERO(&readfds); 669 if (max_fline <= cur_fline + max_displayed_line 670 && eof_error > 0 /* did NOT reach eof yet */ 671 ) { 672 /* We are interested in stdin */ 673 FD_SET(0, &readfds); 674 } 675 FD_SET(kbd_fd, &readfds); 676 tcsetattr(kbd_fd, TCSANOW, &term_less); 677 select(kbd_fd + 1, &readfds, NULL, NULL, NULL); 678 679 input[0] = '\0'; 680 ndelay_on(kbd_fd); 681 rd = read(kbd_fd, input, sz); 682 ndelay_off(kbd_fd); 683 if (rd < 0) { 684 /* No keyboard input, but we have input on stdin! */ 685 if (errno != EAGAIN) /* Huh?? */ 686 return; 687 read_lines(); 688 buffer_fill_and_print(); 689 goto again; 690 } 691} 692 693/* Grab a character from input without requiring the return key. If the 694 * character is ASCII \033, get more characters and assign certain sequences 695 * special return codes. Note that this function works best with raw input. */ 696static int less_getch(void) 697{ 698 char input[16]; 699 unsigned i; 700 again: 701 memset(input, 0, sizeof(input)); 702 getch_nowait(input, sizeof(input)); 703 704 /* Detect escape sequences (i.e. arrow keys) and handle 705 * them accordingly */ 706 if (input[0] == '\033' && input[1] == '[') { 707 set_tty_cooked(); 708 i = input[2] - REAL_KEY_UP; 709 if (i < 4) 710 return 20 + i; 711 i = input[2] - REAL_PAGE_UP; 712 if (i < 4) 713 return 24 + i; 714 if (input[2] == REAL_KEY_HOME_XTERM) 715 return KEY_HOME; 716 if (input[2] == REAL_KEY_HOME_ALT) 717 return KEY_HOME; 718 if (input[2] == REAL_KEY_END_XTERM) 719 return KEY_END; 720 if (input[2] == REAL_KEY_END_ALT) 721 return KEY_END; 722 return 0; 723 } 724 /* Reject almost all control chars */ 725 i = input[0]; 726 if (i < ' ' && i != 0x0d && i != 8) goto again; 727 set_tty_cooked(); 728 return i; 729} 730 731static char* less_gets(int sz) 732{ 733 char c; 734 int i = 0; 735 char *result = xzalloc(1); 736 while (1) { 737 fflush(stdout); 738 739 /* I be damned if I know why is it needed *repeatedly*, 740 * but it is needed. Is it because of stdio? */ 741 tcsetattr(kbd_fd, TCSANOW, &term_less); 742 743 c = '\0'; 744 read(kbd_fd, &c, 1); 745 if (c == 0x0d) 746 return result; 747 if (c == 0x7f) 748 c = 8; 749 if (c == 8 && i) { 750 printf("\x8 \x8"); 751 i--; 752 } 753 if (c < ' ') 754 continue; 755 if (i >= width - sz - 1) 756 continue; /* len limit */ 757 putchar(c); 758 result[i++] = c; 759 result = xrealloc(result, i+1); 760 result[i] = '\0'; 761 } 762} 763 764static void examine_file(void) 765{ 766 print_statusline("Examine: "); 767 free(filename); 768 filename = less_gets(sizeof("Examine: ")-1); 769 /* files start by = argv. why we assume that argv is infinitely long?? 770 files[num_files] = filename; 771 current_file = num_files + 1; 772 num_files++; */ 773 files[0] = filename; 774 num_files = current_file = 1; 775 reinitialize(); 776} 777 778/* This function changes the file currently being paged. direction can be one of the following: 779 * -1: go back one file 780 * 0: go to the first file 781 * 1: go forward one file */ 782static void change_file(int direction) 783{ 784 if (current_file != ((direction > 0) ? num_files : 1)) { 785 current_file = direction ? current_file + direction : 1; 786 free(filename); 787 filename = xstrdup(files[current_file - 1]); 788 reinitialize(); 789 } else { 790 print_statusline(direction > 0 ? "No next file" : "No previous file"); 791 } 792} 793 794static void remove_current_file(void) 795{ 796 int i; 797 798 if (num_files < 2) 799 return; 800 801 if (current_file != 1) { 802 change_file(-1); 803 for (i = 3; i <= num_files; i++) 804 files[i - 2] = files[i - 1]; 805 num_files--; 806 } else { 807 change_file(1); 808 for (i = 2; i <= num_files; i++) 809 files[i - 2] = files[i - 1]; 810 num_files--; 811 current_file--; 812 } 813} 814 815static void colon_process(void) 816{ 817 int keypress; 818 819 /* Clear the current line and print a prompt */ 820 print_statusline(" :"); 821 822 keypress = less_getch(); 823 switch (keypress) { 824 case 'd': 825 remove_current_file(); 826 break; 827 case 'e': 828 examine_file(); 829 break; 830#if ENABLE_FEATURE_LESS_FLAGS 831 case 'f': 832 m_status_print(); 833 break; 834#endif 835 case 'n': 836 change_file(1); 837 break; 838 case 'p': 839 change_file(-1); 840 break; 841 case 'q': 842 less_exit(0); 843 break; 844 case 'x': 845 change_file(0); 846 break; 847 } 848} 849 850#if ENABLE_FEATURE_LESS_REGEXP 851static void normalize_match_pos(int match) 852{ 853 if (match >= num_matches) 854 match = num_matches - 1; 855 if (match < 0) 856 match = 0; 857 match_pos = match; 858} 859 860static void goto_match(int match) 861{ 862 int sv; 863 864 if (!pattern_valid) 865 return; 866 if (match < 0) 867 match = 0; 868 sv = cur_fline; 869 /* Try to find next match if eof isn't reached yet */ 870 if (match >= num_matches && eof_error > 0) { 871 cur_fline = MAXLINES; /* look as far as needed */ 872 read_lines(); 873 } 874 if (num_matches) { 875 cap_cur_fline(cur_fline); 876 normalize_match_pos(match); 877 buffer_line(match_lines[match_pos]); 878 } else { 879 cur_fline = sv; 880 print_statusline("No matches found"); 881 } 882} 883 884static void fill_match_lines(unsigned pos) 885{ 886 if (!pattern_valid) 887 return; 888 /* Run the regex on each line of the current file */ 889 while (pos <= max_fline) { 890 /* If this line matches */ 891 if (regexec(&pattern, flines[pos], 0, NULL, 0) == 0 892 /* and we didn't match it last time */ 893 && !(num_matches && match_lines[num_matches-1] == pos) 894 ) { 895 match_lines = xrealloc(match_lines, (num_matches+1) * sizeof(int)); 896 match_lines[num_matches++] = pos; 897 } 898 pos++; 899 } 900} 901 902static void regex_process(void) 903{ 904 char *uncomp_regex, *err; 905 906 /* Reset variables */ 907 free(match_lines); 908 match_lines = NULL; 909 match_pos = 0; 910 num_matches = 0; 911 if (pattern_valid) { 912 regfree(&pattern); 913 pattern_valid = 0; 914 } 915 916 /* Get the uncompiled regular expression from the user */ 917 clear_line(); 918 putchar((option_mask32 & LESS_STATE_MATCH_BACKWARDS) ? '?' : '/'); 919 uncomp_regex = less_gets(1); 920 if (!uncomp_regex[0]) { 921 free(uncomp_regex); 922 buffer_print(); 923 return; 924 } 925 926 /* Compile the regex and check for errors */ 927 err = regcomp_or_errmsg(&pattern, uncomp_regex, 0); 928 free(uncomp_regex); 929 if (err) { 930 print_statusline(err); 931 free(err); 932 return; 933 } 934 935 pattern_valid = 1; 936 match_pos = 0; 937 fill_match_lines(0); 938 while (match_pos < num_matches) { 939 if (match_lines[match_pos] > cur_fline) 940 break; 941 match_pos++; 942 } 943 if (option_mask32 & LESS_STATE_MATCH_BACKWARDS) 944 match_pos--; 945 946 /* It's possible that no matches are found yet. 947 * goto_match() will read input looking for match, 948 * if needed */ 949 goto_match(match_pos); 950} 951#endif 952 953static void number_process(int first_digit) 954{ 955 int i = 1; 956 int num; 957 char num_input[sizeof(int)*4]; /* more than enough */ 958 char keypress; 959 960 num_input[0] = first_digit; 961 962 /* Clear the current line, print a prompt, and then print the digit */ 963 clear_line(); 964 printf(":%c", first_digit); 965 966 /* Receive input until a letter is given */ 967 while (i < sizeof(num_input)-1) { 968 num_input[i] = less_getch(); 969 if (!num_input[i] || !isdigit(num_input[i])) 970 break; 971 putchar(num_input[i]); 972 i++; 973 } 974 975 /* Take the final letter out of the digits string */ 976 keypress = num_input[i]; 977 num_input[i] = '\0'; 978 num = bb_strtou(num_input, NULL, 10); 979 /* on format error, num == -1 */ 980 if (num < 1 || num > MAXLINES) { 981 buffer_print(); 982 return; 983 } 984 985 /* We now know the number and the letter entered, so we process them */ 986 switch (keypress) { 987 case KEY_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015': 988 buffer_down(num); 989 break; 990 case KEY_UP: case 'b': case 'w': case 'y': case 'u': 991 buffer_up(num); 992 break; 993 case 'g': case '<': case 'G': case '>': 994 cur_fline = num + max_displayed_line; 995 read_lines(); 996 buffer_line(num - 1); 997 break; 998 case 'p': case '%': 999 num = num * (max_fline / 100); /* + max_fline / 2; */ 1000 cur_fline = num + max_displayed_line; 1001 read_lines(); 1002 buffer_line(num); 1003 break; 1004#if ENABLE_FEATURE_LESS_REGEXP 1005 case 'n': 1006 goto_match(match_pos + num); 1007 break; 1008 case '/': 1009 option_mask32 &= ~LESS_STATE_MATCH_BACKWARDS; 1010 regex_process(); 1011 break; 1012 case '?': 1013 option_mask32 |= LESS_STATE_MATCH_BACKWARDS; 1014 regex_process(); 1015 break; 1016#endif 1017 } 1018} 1019 1020#if ENABLE_FEATURE_LESS_FLAGCS 1021static void flag_change(void) 1022{ 1023 int keypress; 1024 1025 clear_line(); 1026 putchar('-'); 1027 keypress = less_getch(); 1028 1029 switch (keypress) { 1030 case 'M': 1031 option_mask32 ^= FLAG_M; 1032 break; 1033 case 'm': 1034 option_mask32 ^= FLAG_m; 1035 break; 1036 case 'E': 1037 option_mask32 ^= FLAG_E; 1038 break; 1039 case '~': 1040 option_mask32 ^= FLAG_TILDE; 1041 break; 1042 } 1043} 1044 1045static void show_flag_status(void) 1046{ 1047 int keypress; 1048 int flag_val; 1049 1050 clear_line(); 1051 putchar('_'); 1052 keypress = less_getch(); 1053 1054 switch (keypress) { 1055 case 'M': 1056 flag_val = option_mask32 & FLAG_M; 1057 break; 1058 case 'm': 1059 flag_val = option_mask32 & FLAG_m; 1060 break; 1061 case '~': 1062 flag_val = option_mask32 & FLAG_TILDE; 1063 break; 1064 case 'N': 1065 flag_val = option_mask32 & FLAG_N; 1066 break; 1067 case 'E': 1068 flag_val = option_mask32 & FLAG_E; 1069 break; 1070 default: 1071 flag_val = 0; 1072 break; 1073 } 1074 1075 clear_line(); 1076 printf(HIGHLIGHT"The status of the flag is: %u"NORMAL, flag_val != 0); 1077} 1078#endif 1079 1080static void save_input_to_file(void) 1081{ 1082 const char *msg = ""; 1083 char *current_line; 1084 int i; 1085 FILE *fp; 1086 1087 print_statusline("Log file: "); 1088 current_line = less_gets(sizeof("Log file: ")-1); 1089 if (strlen(current_line) > 0) { 1090 fp = fopen(current_line, "w"); 1091 if (!fp) { 1092 msg = "Error opening log file"; 1093 goto ret; 1094 } 1095 for (i = 0; i <= max_fline; i++) 1096 fprintf(fp, "%s\n", flines[i]); 1097 fclose(fp); 1098 msg = "Done"; 1099 } 1100 ret: 1101 print_statusline(msg); 1102 free(current_line); 1103} 1104 1105#if ENABLE_FEATURE_LESS_MARKS 1106static void add_mark(void) 1107{ 1108 int letter; 1109 1110 print_statusline("Mark: "); 1111 letter = less_getch(); 1112 1113 if (isalpha(letter)) { 1114 /* If we exceed 15 marks, start overwriting previous ones */ 1115 if (num_marks == 14) 1116 num_marks = 0; 1117 1118 mark_lines[num_marks][0] = letter; 1119 mark_lines[num_marks][1] = cur_fline; 1120 num_marks++; 1121 } else { 1122 print_statusline("Invalid mark letter"); 1123 } 1124} 1125 1126static void goto_mark(void) 1127{ 1128 int letter; 1129 int i; 1130 1131 print_statusline("Go to mark: "); 1132 letter = less_getch(); 1133 clear_line(); 1134 1135 if (isalpha(letter)) { 1136 for (i = 0; i <= num_marks; i++) 1137 if (letter == mark_lines[i][0]) { 1138 buffer_line(mark_lines[i][1]); 1139 break; 1140 } 1141 if (num_marks == 14 && letter != mark_lines[14][0]) 1142 print_statusline("Mark not set"); 1143 } else 1144 print_statusline("Invalid mark letter"); 1145} 1146#endif 1147 1148#if ENABLE_FEATURE_LESS_BRACKETS 1149static char opp_bracket(char bracket) 1150{ 1151 switch (bracket) { 1152 case '{': case '[': 1153 return bracket + 2; 1154 case '(': 1155 return ')'; 1156 case '}': case ']': 1157 return bracket - 2; 1158 case ')': 1159 return '('; 1160 } 1161 return 0; 1162} 1163 1164static void match_right_bracket(char bracket) 1165{ 1166 int bracket_line = -1; 1167 int i; 1168 1169 if (strchr(flines[cur_fline], bracket) == NULL) { 1170 print_statusline("No bracket in top line"); 1171 return; 1172 } 1173 for (i = cur_fline + 1; i < max_fline; i++) { 1174 if (strchr(flines[i], opp_bracket(bracket)) != NULL) { 1175 bracket_line = i; 1176 break; 1177 } 1178 } 1179 if (bracket_line == -1) 1180 print_statusline("No matching bracket found"); 1181 buffer_line(bracket_line - max_displayed_line); 1182} 1183 1184static void match_left_bracket(char bracket) 1185{ 1186 int bracket_line = -1; 1187 int i; 1188 1189 if (strchr(flines[cur_fline + max_displayed_line], bracket) == NULL) { 1190 print_statusline("No bracket in bottom line"); 1191 return; 1192 } 1193 1194 for (i = cur_fline + max_displayed_line; i >= 0; i--) { 1195 if (strchr(flines[i], opp_bracket(bracket)) != NULL) { 1196 bracket_line = i; 1197 break; 1198 } 1199 } 1200 if (bracket_line == -1) 1201 print_statusline("No matching bracket found"); 1202 buffer_line(bracket_line); 1203} 1204#endif /* FEATURE_LESS_BRACKETS */ 1205 1206static void keypress_process(int keypress) 1207{ 1208 switch (keypress) { 1209 case KEY_DOWN: case 'e': case 'j': case 0x0d: 1210 buffer_down(1); 1211 break; 1212 case KEY_UP: case 'y': case 'k': 1213 buffer_up(1); 1214 break; 1215 case PAGE_DOWN: case ' ': case 'z': 1216 buffer_down(max_displayed_line + 1); 1217 break; 1218 case PAGE_UP: case 'w': case 'b': 1219 buffer_up(max_displayed_line + 1); 1220 break; 1221 case 'd': 1222 buffer_down((max_displayed_line + 1) / 2); 1223 break; 1224 case 'u': 1225 buffer_up((max_displayed_line + 1) / 2); 1226 break; 1227 case KEY_HOME: case 'g': case 'p': case '<': case '%': 1228 buffer_line(0); 1229 break; 1230 case KEY_END: case 'G': case '>': 1231 cur_fline = MAXLINES; 1232 read_lines(); 1233 buffer_line(cur_fline); 1234 break; 1235 case 'q': case 'Q': 1236 less_exit(0); 1237 break; 1238#if ENABLE_FEATURE_LESS_MARKS 1239 case 'm': 1240 add_mark(); 1241 buffer_print(); 1242 break; 1243 case '\'': 1244 goto_mark(); 1245 buffer_print(); 1246 break; 1247#endif 1248 case 'r': case 'R': 1249 buffer_print(); 1250 break; 1251 /*case 'R': 1252 full_repaint(); 1253 break;*/ 1254 case 's': 1255 save_input_to_file(); 1256 break; 1257 case 'E': 1258 examine_file(); 1259 break; 1260#if ENABLE_FEATURE_LESS_FLAGS 1261 case '=': 1262 m_status_print(); 1263 break; 1264#endif 1265#if ENABLE_FEATURE_LESS_REGEXP 1266 case '/': 1267 option_mask32 &= ~LESS_STATE_MATCH_BACKWARDS; 1268 regex_process(); 1269 break; 1270 case 'n': 1271 goto_match(match_pos + 1); 1272 break; 1273 case 'N': 1274 goto_match(match_pos - 1); 1275 break; 1276 case '?': 1277 option_mask32 |= LESS_STATE_MATCH_BACKWARDS; 1278 regex_process(); 1279 break; 1280#endif 1281#if ENABLE_FEATURE_LESS_FLAGCS 1282 case '-': 1283 flag_change(); 1284 buffer_print(); 1285 break; 1286 case '_': 1287 show_flag_status(); 1288 break; 1289#endif 1290#if ENABLE_FEATURE_LESS_BRACKETS 1291 case '{': case '(': case '[': 1292 match_right_bracket(keypress); 1293 break; 1294 case '}': case ')': case ']': 1295 match_left_bracket(keypress); 1296 break; 1297#endif 1298 case ':': 1299 colon_process(); 1300 break; 1301 } 1302 1303 if (isdigit(keypress)) 1304 number_process(keypress); 1305} 1306 1307static void sig_catcher(int sig ATTRIBUTE_UNUSED) 1308{ 1309 set_tty_cooked(); 1310 exit(1); 1311} 1312 1313int less_main(int argc, char **argv); 1314int less_main(int argc, char **argv) 1315{ 1316 int keypress; 1317 1318 INIT_G(); 1319 1320 /* TODO: -x: do not interpret backspace, -xx: tab also */ 1321 getopt32(argv, "EMmN~"); 1322 argc -= optind; 1323 argv += optind; 1324 num_files = argc; 1325 files = argv; 1326 1327 /* Another popular pager, most, detects when stdout 1328 * is not a tty and turns into cat. This makes sense. */ 1329 if (!isatty(STDOUT_FILENO)) 1330 return bb_cat(argv); 1331 kbd_fd = open(CURRENT_TTY, O_RDONLY); 1332 if (kbd_fd < 0) 1333 return bb_cat(argv); 1334 1335 if (!num_files) { 1336 if (isatty(STDIN_FILENO)) { 1337 /* Just "less"? No args and no redirection? */ 1338 bb_error_msg("missing filename"); 1339 bb_show_usage(); 1340 } 1341 } else 1342 filename = xstrdup(files[0]); 1343 1344 get_terminal_width_height(kbd_fd, &width, &max_displayed_line); 1345 /* 20: two tabstops + 4 */ 1346 if (width < 20 || max_displayed_line < 3) 1347 bb_error_msg_and_die("too narrow here"); 1348 max_displayed_line -= 2; 1349 1350 buffer = xmalloc((max_displayed_line+1) * sizeof(char *)); 1351 if (option_mask32 & FLAG_TILDE) 1352 empty_line_marker = ""; 1353 1354 tcgetattr(kbd_fd, &term_orig); 1355 signal(SIGTERM, sig_catcher); 1356 signal(SIGINT, sig_catcher); 1357 term_less = term_orig; 1358 term_less.c_lflag &= ~(ICANON | ECHO); 1359 term_less.c_iflag &= ~(IXON | ICRNL); 1360 /*term_less.c_oflag &= ~ONLCR;*/ 1361 term_less.c_cc[VMIN] = 1; 1362 term_less.c_cc[VTIME] = 0; 1363 1364 /* Want to do it just once, but it doesn't work, */ 1365 /* so we are redoing it (see code above). Mystery... */ 1366 /*tcsetattr(kbd_fd, TCSANOW, &term_less);*/ 1367 1368 reinitialize(); 1369 while (1) { 1370 keypress = less_getch(); 1371 keypress_process(keypress); 1372 } 1373} 1374