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