summaryrefslogtreecommitdiff
path: root/util-linux/more.c
blob: 934b30f8a8384e178ff00ce3c7cc8c70d68b6d27 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/* vi: set sw=4 ts=4: */
/*
 * Mini more implementation for busybox
 *
 * Copyright (C) 1995, 1996 by Bruce Perens <bruce@pixar.com>.
 * Copyright (C) 1999-2004 by Erik Andersen <andersen@codepoet.org>
 *
 * Latest version blended together by Erik Andersen <andersen@codepoet.org>,
 * based on the original more implementation by Bruce, and code from the
 * Debian boot-floppies team.
 *
 * Termios corrects by Vladimir Oleynik <dzo@simtreas.ru>
 *
 * Licensed under GPLv2 or later, see file LICENSE in this source tree.
 */
//config:config MORE
//config:	bool "more"
//config:	default y
//config:	help
//config:	  more is a simple utility which allows you to read text one screen
//config:	  sized page at a time. If you want to read text that is larger than
//config:	  the screen, and you are using anything faster than a 300 baud modem,
//config:	  you will probably find this utility very helpful. If you don't have
//config:	  any need to reading text files, you can leave this disabled.

//applet:IF_MORE(APPLET(more, BB_DIR_BIN, BB_SUID_DROP))

//kbuild:lib-$(CONFIG_MORE) += more.o

//usage:#define more_trivial_usage
//usage:       "[FILE]..."
//usage:#define more_full_usage "\n\n"
//usage:       "View FILE (or stdin) one screenful at a time"
//usage:
//usage:#define more_example_usage
//usage:       "$ dmesg | more\n"

#include "libbb.h"
#include "common_bufsiz.h"

/* Support for FEATURE_USE_TERMIOS */

struct globals {
	int cin_fileno;
	struct termios initial_settings;
	struct termios new_settings;
} FIX_ALIASING;
#define G (*(struct globals*)bb_common_bufsiz1)
#define initial_settings (G.initial_settings)
#define new_settings     (G.new_settings    )
#define cin_fileno       (G.cin_fileno      )
#define INIT_G() do { setup_common_bufsiz(); } while (0)

#define setTermSettings(fd, argp) \
do { \
	if (ENABLE_FEATURE_USE_TERMIOS) \
		tcsetattr(fd, TCSANOW, argp); \
} while (0)
#define getTermSettings(fd, argp) tcgetattr(fd, argp)

static void gotsig(int sig UNUSED_PARAM)
{
	/* bb_putchar_stderr doesn't use stdio buffering,
	 * therefore it is safe in signal handler */
	bb_putchar_stderr('\n');
	setTermSettings(cin_fileno, &initial_settings);
	_exit(EXIT_FAILURE);
}

#define CONVERTED_TAB_SIZE 8

int more_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int more_main(int argc UNUSED_PARAM, char **argv)
{
	int c = c; /* for compiler */
	int lines;
	int input = 0;
	int spaces = 0;
	int please_display_more_prompt;
	struct stat st;
	FILE *file;
	FILE *cin;
	int len;
	unsigned terminal_width;
	unsigned terminal_height;

	INIT_G();

	/* Parse options */
	/* Accepted but ignored: */
	/* -d	Display help instead of ringing bell is pressed */
	/* -f	Count logical lines (IOW: long lines are not folded) */
	/* -l	Do not pause after any line containing a ^L (form feed) */
	/* -s	Squeeze blank lines into one */
	/* -u	Suppress underlining */
	getopt32(argv, "dflsu");
	argv += optind;

	/* Another popular pager, most, detects when stdout
	 * is not a tty and turns into cat. This makes sense. */
	if (!isatty(STDOUT_FILENO))
		return bb_cat(argv);
	cin = fopen_for_read(CURRENT_TTY);
	if (!cin)
		return bb_cat(argv);

	if (ENABLE_FEATURE_USE_TERMIOS) {
		cin_fileno = fileno(cin);
		getTermSettings(cin_fileno, &initial_settings);
		new_settings = initial_settings;
		new_settings.c_lflag &= ~(ICANON | ECHO);
		new_settings.c_cc[VMIN] = 1;
		new_settings.c_cc[VTIME] = 0;
		setTermSettings(cin_fileno, &new_settings);
		bb_signals(0
			+ (1 << SIGINT)
			+ (1 << SIGQUIT)
			+ (1 << SIGTERM)
			, gotsig);
	}

	do {
		file = stdin;
		if (*argv) {
			file = fopen_or_warn(*argv, "r");
			if (!file)
				continue;
		}
		st.st_size = 0;
		fstat(fileno(file), &st);

		please_display_more_prompt = 0;
		/* never returns w, h <= 1 */
		get_terminal_width_height(fileno(cin), &terminal_width, &terminal_height);
		terminal_height -= 1;

		len = 0;
		lines = 0;
		while (spaces || (c = getc(file)) != EOF) {
			int wrap;
			if (spaces)
				spaces--;
 loop_top:
			if (input != 'r' && please_display_more_prompt) {
				len = printf("--More-- ");
				if (st.st_size != 0) {
					uoff_t d = (uoff_t)st.st_size / 100;
					if (d == 0)
						d = 1;
					len += printf("(%u%% of %"OFF_FMT"u bytes)",
						(int) ((uoff_t)ftello(file) / d),
						st.st_size);
				}
				fflush_all();

				/*
				 * We've just displayed the "--More--" prompt, so now we need
				 * to get input from the user.
				 */
				for (;;) {
					input = getc(cin);
					input = tolower(input);
					if (!ENABLE_FEATURE_USE_TERMIOS)
						printf("\033[A"); /* cursor up */
					/* Erase the last message */
					printf("\r%*s\r", len, "");

					/* Due to various multibyte escape
					 * sequences, it's not ok to accept
					 * any input as a command to scroll
					 * the screen. We only allow known
					 * commands, else we show help msg. */
					if (input == ' ' || input == '\n' || input == 'q' || input == 'r')
						break;
					len = printf("(Enter:next line Space:next page Q:quit R:show the rest)");
				}
				len = 0;
				lines = 0;
				please_display_more_prompt = 0;

				if (input == 'q')
					goto end;

				/* The user may have resized the terminal.
				 * Re-read the dimensions. */
				if (ENABLE_FEATURE_USE_TERMIOS) {
					get_terminal_width_height(cin_fileno, &terminal_width, &terminal_height);
					terminal_height -= 1;
				}
			}

			/* Crudely convert tabs into spaces, which are
			 * a bajillion times easier to deal with. */
			if (c == '\t') {
				spaces = ((unsigned)~len) % CONVERTED_TAB_SIZE;
				c = ' ';
			}

			/*
			 * There are two input streams to worry about here:
			 *
			 * c    : the character we are reading from the file being "mored"
			 * input: a character received from the keyboard
			 *
			 * If we hit a newline in the _file_ stream, we want to test and
			 * see if any characters have been hit in the _input_ stream. This
			 * allows the user to quit while in the middle of a file.
			 */
			wrap = (++len > terminal_width);
			if (c == '\n' || wrap) {
				/* Then outputting this character
				 * will move us to a new line. */
				if (++lines >= terminal_height || input == '\n')
					please_display_more_prompt = 1;
				len = 0;
			}
			if (c != '\n' && wrap) {
				/* Then outputting this will also put a character on
				 * the beginning of that new line. Thus we first want to
				 * display the prompt (if any), so we skip the putchar()
				 * and go back to the top of the loop, without reading
				 * a new character. */
				goto loop_top;
			}
			/* My small mind cannot fathom backspaces and UTF-8 */
			putchar(c);
			die_if_ferror_stdout(); /* if tty was destroyed (closed xterm, etc) */
		}
		fclose(file);
		fflush_all();
	} while (*argv && *++argv);
 end:
	setTermSettings(cin_fileno, &initial_settings);
	return 0;
}