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