hush/miscutils/less.c
Ron Yorston 193ba408a4 less: improvements to verbose status messages
Make verbose status messages (-m/-M flags) behave more like the
real `less` command:

- fix display of line numbers so they're correct whether lines are
  being truncated (-S flag) or wrapped.
- don't display total lines or percentage when lines are read from
  stdin:  we don't have that information until we reach EOF.  When
  we do reach EOF the additional information is displayed.
- when lines are read from a file count the total number of lines
  so that we can display percentages.  Counting lines is avoided
  until the information is actually needed.  If the user pages to
  EOF the separate read pass can be avoided entirely.

Fixes Bug 7586

function                                             old     new   delta
m_status_print                                       195     382    +187
safe_lineno                                            -      35     +35
reinitialize                                         172     182     +10
read_lines                                           675     685     +10
buffer_fill_and_print                                178     169      -9
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 3/1 up/down: 242/-9)            Total: 233 bytes

Signed-off-by: Ron Yorston <rmy@pobox.com>
Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
2015-07-21 22:28:09 +02:00

2018 lines
53 KiB
C

/* vi: set sw=4 ts=4: */
/*
* Mini less implementation for busybox
*
* Copyright (C) 2005 by Rob Sullivan <cogito.ergo.cogito@gmail.com>
*
* Licensed under GPLv2 or later, see file LICENSE in this source tree.
*/
/*
* TODO:
* - Add more regular expression support - search modifiers, certain matches, etc.
* - Add more complex bracket searching - currently, nested brackets are
* not considered.
* - Add support for "F" as an input. This causes less to act in
* a similar way to tail -f.
* - Allow horizontal scrolling.
*
* Notes:
* - the inp file pointer is used so that keyboard input works after
* redirected input has been read from stdin
*/
//config:config LESS
//config: bool "less"
//config: default y
//config: help
//config: 'less' is a pager, meaning that it displays text files. It possesses
//config: a wide array of features, and is an improvement over 'more'.
//config:
//config:config FEATURE_LESS_MAXLINES
//config: int "Max number of input lines less will try to eat"
//config: default 9999999
//config: depends on LESS
//config:
//config:config FEATURE_LESS_BRACKETS
//config: bool "Enable bracket searching"
//config: default y
//config: depends on LESS
//config: help
//config: This option adds the capability to search for matching left and right
//config: brackets, facilitating programming.
//config:
//config:config FEATURE_LESS_FLAGS
//config: bool "Enable -m/-M"
//config: default y
//config: depends on LESS
//config: help
//config: The -M/-m flag enables a more sophisticated status line.
//config:
//config:config FEATURE_LESS_TRUNCATE
//config: bool "Enable -S"
//config: default y
//config: depends on LESS
//config: help
//config: The -S flag causes long lines to be truncated rather than
//config: wrapped.
//config:
//config:config FEATURE_LESS_MARKS
//config: bool "Enable marks"
//config: default y
//config: depends on LESS
//config: help
//config: Marks enable positions in a file to be stored for easy reference.
//config:
//config:config FEATURE_LESS_REGEXP
//config: bool "Enable regular expressions"
//config: default y
//config: depends on LESS
//config: help
//config: Enable regular expressions, allowing complex file searches.
//config:
//config:config FEATURE_LESS_WINCH
//config: bool "Enable automatic resizing on window size changes"
//config: default y
//config: depends on LESS
//config: help
//config: Makes less track window size changes.
//config:
//config:config FEATURE_LESS_ASK_TERMINAL
//config: bool "Use 'tell me cursor position' ESC sequence to measure window"
//config: default y
//config: depends on FEATURE_LESS_WINCH
//config: help
//config: Makes less track window size changes.
//config: If terminal size can't be retrieved and $LINES/$COLUMNS are not set,
//config: this option makes less perform a last-ditch effort to find it:
//config: position cursor to 999,999 and ask terminal to report real
//config: cursor position using "ESC [ 6 n" escape sequence, then read stdin.
//config:
//config: This is not clean but helps a lot on serial lines and such.
//config:
//config:config FEATURE_LESS_DASHCMD
//config: bool "Enable flag changes ('-' command)"
//config: default y
//config: depends on LESS
//config: help
//config: This enables the ability to change command-line flags within
//config: less itself ('-' keyboard command).
//config:
//config:config FEATURE_LESS_LINENUMS
//config: bool "Enable dynamic switching of line numbers"
//config: default y
//config: depends on FEATURE_LESS_DASHCMD
//config: help
//config: Enables "-N" command.
//usage:#define less_trivial_usage
//usage: "[-E" IF_FEATURE_LESS_REGEXP("I")IF_FEATURE_LESS_FLAGS("Mm")
//usage: "N" IF_FEATURE_LESS_TRUNCATE("S") "h~] [FILE]..."
//usage:#define less_full_usage "\n\n"
//usage: "View FILE (or stdin) one screenful at a time\n"
//usage: "\n -E Quit once the end of a file is reached"
//usage: IF_FEATURE_LESS_REGEXP(
//usage: "\n -I Ignore case in all searches"
//usage: )
//usage: IF_FEATURE_LESS_FLAGS(
//usage: "\n -M,-m Display status line with line numbers"
//usage: "\n and percentage through the file"
//usage: )
//usage: "\n -N Prefix line number to each line"
//usage: IF_FEATURE_LESS_TRUNCATE(
//usage: "\n -S Truncate long lines"
//usage: )
//usage: "\n -~ Suppress ~s displayed past EOF"
#include <sched.h> /* sched_yield() */
#include "libbb.h"
#if ENABLE_FEATURE_LESS_REGEXP
#include "xregex.h"
#endif
#define ESC "\033"
/* The escape codes for highlighted and normal text */
#define HIGHLIGHT ESC"[7m"
#define NORMAL ESC"[0m"
/* The escape code to home and clear to the end of screen */
#define CLEAR ESC"[H\033[J"
/* The escape code to clear to the end of line */
#define CLEAR_2_EOL ESC"[K"
enum {
/* Absolute max of lines eaten */
MAXLINES = CONFIG_FEATURE_LESS_MAXLINES,
/* This many "after the end" lines we will show (at max) */
TILDES = 1,
};
/* Command line options */
enum {
FLAG_E = 1 << 0,
FLAG_M = 1 << 1,
FLAG_m = 1 << 2,
FLAG_N = 1 << 3,
FLAG_TILDE = 1 << 4,
FLAG_I = 1 << 5,
FLAG_S = (1 << 6) * ENABLE_FEATURE_LESS_TRUNCATE,
/* hijack command line options variable for internal state vars */
LESS_STATE_MATCH_BACKWARDS = 1 << 15,
};
#if !ENABLE_FEATURE_LESS_REGEXP
enum { pattern_valid = 0 };
#endif
enum {
READING_FILE = -1,
READING_STDIN = -2
};
struct globals {
int cur_fline; /* signed */
int kbd_fd; /* fd to get input from */
int less_gets_pos;
/* last position in last line, taking into account tabs */
size_t last_line_pos;
unsigned max_fline;
unsigned max_lineno; /* this one tracks linewrap */
unsigned max_displayed_line;
unsigned width;
#if ENABLE_FEATURE_LESS_WINCH
unsigned winch_counter;
#endif
ssize_t eof_error; /* eof if 0, error if < 0 */
ssize_t readpos;
ssize_t readeof; /* must be signed */
const char **buffer;
const char **flines;
const char *empty_line_marker;
unsigned num_files;
unsigned current_file;
char *filename;
char **files;
#if ENABLE_FEATURE_LESS_FLAGS
int num_lines; /* input source if < 0, line count if >= 0 */
#endif
#if ENABLE_FEATURE_LESS_MARKS
unsigned num_marks;
unsigned mark_lines[15][2];
#endif
#if ENABLE_FEATURE_LESS_REGEXP
unsigned *match_lines;
int match_pos; /* signed! */
int wanted_match; /* signed! */
int num_matches;
regex_t pattern;
smallint pattern_valid;
#endif
#if ENABLE_FEATURE_LESS_ASK_TERMINAL
smallint winsize_err;
#endif
smallint terminated;
struct termios term_orig, term_less;
char kbd_input[KEYCODE_BUFFER_SIZE];
};
#define G (*ptr_to_globals)
#define cur_fline (G.cur_fline )
#define kbd_fd (G.kbd_fd )
#define less_gets_pos (G.less_gets_pos )
#define last_line_pos (G.last_line_pos )
#define max_fline (G.max_fline )
#define max_lineno (G.max_lineno )
#define max_displayed_line (G.max_displayed_line)
#define width (G.width )
#define winch_counter (G.winch_counter )
/* This one is 100% not cached by compiler on read access */
#define WINCH_COUNTER (*(volatile unsigned *)&winch_counter)
#define eof_error (G.eof_error )
#define readpos (G.readpos )
#define readeof (G.readeof )
#define buffer (G.buffer )
#define flines (G.flines )
#define empty_line_marker (G.empty_line_marker )
#define num_files (G.num_files )
#define current_file (G.current_file )
#define filename (G.filename )
#define files (G.files )
#define num_lines (G.num_lines )
#define num_marks (G.num_marks )
#define mark_lines (G.mark_lines )
#if ENABLE_FEATURE_LESS_REGEXP
#define match_lines (G.match_lines )
#define match_pos (G.match_pos )
#define num_matches (G.num_matches )
#define wanted_match (G.wanted_match )
#define pattern (G.pattern )
#define pattern_valid (G.pattern_valid )
#endif
#define terminated (G.terminated )
#define term_orig (G.term_orig )
#define term_less (G.term_less )
#define kbd_input (G.kbd_input )
#define INIT_G() do { \
SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
less_gets_pos = -1; \
empty_line_marker = "~"; \
num_files = 1; \
current_file = 1; \
eof_error = 1; \
terminated = 1; \
IF_FEATURE_LESS_REGEXP(wanted_match = -1;) \
} while (0)
/* flines[] are lines read from stdin, each in malloc'ed buffer.
* Line numbers are stored as uint32_t prepended to each line.
* Pointer is adjusted so that flines[i] points directly past
* line number. Accesor: */
#define MEMPTR(p) ((char*)(p) - 4)
#define LINENO(p) (*(uint32_t*)((p) - 4))
/* Reset terminal input to normal */
static void set_tty_cooked(void)
{
fflush_all();
tcsetattr(kbd_fd, TCSANOW, &term_orig);
}
/* Move the cursor to a position (x,y), where (0,0) is the
top-left corner of the console */
static void move_cursor(int line, int row)
{
printf(ESC"[%u;%uH", line, row);
}
static void clear_line(void)
{
printf(ESC"[%u;0H" CLEAR_2_EOL, max_displayed_line + 2);
}
static void print_hilite(const char *str)
{
printf(HIGHLIGHT"%s"NORMAL, str);
}
static void print_statusline(const char *str)
{
clear_line();
printf(HIGHLIGHT"%.*s"NORMAL, width - 1, str);
}
/* Exit the program gracefully */
static void less_exit(int code)
{
set_tty_cooked();
clear_line();
if (code < 0)
kill_myself_with_sig(- code); /* does not return */
exit(code);
}
#if (ENABLE_FEATURE_LESS_DASHCMD && ENABLE_FEATURE_LESS_LINENUMS) \
|| ENABLE_FEATURE_LESS_WINCH
static void re_wrap(void)
{
int w = width;
int new_line_pos;
int src_idx;
int dst_idx;
int new_cur_fline = 0;
uint32_t lineno;
char linebuf[w + 1];
const char **old_flines = flines;
const char *s;
char **new_flines = NULL;
char *d;
if (option_mask32 & FLAG_N)
w -= 8;
src_idx = 0;
dst_idx = 0;
s = old_flines[0];
lineno = LINENO(s);
d = linebuf;
new_line_pos = 0;
while (1) {
*d = *s;
if (*d != '\0') {
new_line_pos++;
if (*d == '\t') { /* tab */
new_line_pos += 7;
new_line_pos &= (~7);
}
s++;
d++;
if (new_line_pos >= w) {
int sz;
/* new line is full, create next one */
*d = '\0';
next_new:
sz = (d - linebuf) + 1; /* + 1: NUL */
d = ((char*)xmalloc(sz + 4)) + 4;
LINENO(d) = lineno;
memcpy(d, linebuf, sz);
new_flines = xrealloc_vector(new_flines, 8, dst_idx);
new_flines[dst_idx] = d;
dst_idx++;
if (new_line_pos < w) {
/* if we came here thru "goto next_new" */
if (src_idx > max_fline)
break;
lineno = LINENO(s);
}
d = linebuf;
new_line_pos = 0;
}
continue;
}
/* *d == NUL: old line ended, go to next old one */
free(MEMPTR(old_flines[src_idx]));
/* btw, convert cur_fline... */
if (cur_fline == src_idx)
new_cur_fline = dst_idx;
src_idx++;
/* no more lines? finish last new line (and exit the loop) */
if (src_idx > max_fline)
goto next_new;
s = old_flines[src_idx];
if (lineno != LINENO(s)) {
/* this is not a continuation line!
* create next _new_ line too */
goto next_new;
}
}
free(old_flines);
flines = (const char **)new_flines;
max_fline = dst_idx - 1;
last_line_pos = new_line_pos;
cur_fline = new_cur_fline;
/* max_lineno is screen-size independent */
#if ENABLE_FEATURE_LESS_REGEXP
pattern_valid = 0;
#endif
}
#endif
#if ENABLE_FEATURE_LESS_REGEXP
static void fill_match_lines(unsigned pos);
#else
#define fill_match_lines(pos) ((void)0)
#endif
/* Devilishly complex routine.
*
* Has to deal with EOF and EPIPE on input,
* with line wrapping, with last line not ending in '\n'
* (possibly not ending YET!), with backspace and tabs.
* It reads input again if last time we got an EOF (thus supporting
* growing files) or EPIPE (watching output of slow process like make).
*
* Variables used:
* flines[] - array of lines already read. Linewrap may cause
* one source file line to occupy several flines[n].
* flines[max_fline] - last line, possibly incomplete.
* terminated - 1 if flines[max_fline] is 'terminated'
* (if there was '\n' [which isn't stored itself, we just remember
* that it was seen])
* max_lineno - last line's number, this one doesn't increment
* on line wrap, only on "real" new lines.
* readbuf[0..readeof-1] - small preliminary buffer.
* readbuf[readpos] - next character to add to current line.
* last_line_pos - screen line position of next char to be read
* (takes into account tabs and backspaces)
* eof_error - < 0 error, == 0 EOF, > 0 not EOF/error
*
* "git log -p | less -m" on the kernel git tree is a good test for EAGAINs,
* "/search on very long input" and "reaching max line count" corner cases.
*/
static void read_lines(void)
{
#define readbuf bb_common_bufsiz1
char *current_line, *p;
int w = width;
char last_terminated = terminated;
time_t last_time = 0;
int retry_EAGAIN = 2;
#if ENABLE_FEATURE_LESS_REGEXP
unsigned old_max_fline = max_fline;
#endif
/* (careful: max_fline can be -1) */
if (max_fline + 1 > MAXLINES)
return;
if (option_mask32 & FLAG_N)
w -= 8;
p = current_line = ((char*)xmalloc(w + 4)) + 4;
if (!last_terminated) {
const char *cp = flines[max_fline];
p = stpcpy(p, cp);
free(MEMPTR(cp));
/* last_line_pos is still valid from previous read_lines() */
} else {
max_fline++;
last_line_pos = 0;
}
while (1) { /* read lines until we reach cur_fline or wanted_match */
*p = '\0';
terminated = 0;
while (1) { /* read chars until we have a line */
char c;
/* if no unprocessed chars left, eat more */
if (readpos >= readeof) {
int flags = ndelay_on(0);
while (1) {
time_t t;
errno = 0;
eof_error = safe_read(STDIN_FILENO, readbuf, sizeof(readbuf));
if (errno != EAGAIN)
break;
t = time(NULL);
if (t != last_time) {
last_time = t;
if (--retry_EAGAIN < 0)
break;
}
sched_yield();
}
fcntl(0, F_SETFL, flags); /* ndelay_off(0) */
readpos = 0;
readeof = eof_error;
if (eof_error <= 0)
goto reached_eof;
retry_EAGAIN = 1;
}
c = readbuf[readpos];
/* backspace? [needed for manpages] */
/* <tab><bs> is (a) insane and */
/* (b) harder to do correctly, so we refuse to do it */
if (c == '\x8' && last_line_pos && p[-1] != '\t') {
readpos++; /* eat it */
last_line_pos--;
/* was buggy (p could end up <= current_line)... */
*--p = '\0';
continue;
}
{
size_t new_last_line_pos = last_line_pos + 1;
if (c == '\t') {
new_last_line_pos += 7;
new_last_line_pos &= (~7);
}
if ((int)new_last_line_pos >= w)
break;
last_line_pos = new_last_line_pos;
}
/* ok, we will eat this char */
readpos++;
if (c == '\n') {
terminated = 1;
last_line_pos = 0;
break;
}
/* NUL is substituted by '\n'! */
if (c == '\0') c = '\n';
*p++ = c;
*p = '\0';
} /* end of "read chars until we have a line" loop */
#if 0
//BUG: also triggers on this:
// { printf "\nfoo\n"; sleep 1; printf "\nbar\n"; } | less
// (resulting in lost empty line between "foo" and "bar" lines)
// the "terminated" logic needs fixing (or explaining)
/* Corner case: linewrap with only "" wrapping to next line */
/* Looks ugly on screen, so we do not store this empty line */
if (!last_terminated && !current_line[0]) {
last_terminated = 1;
max_lineno++;
continue;
}
#endif
reached_eof:
last_terminated = terminated;
flines = xrealloc_vector(flines, 8, max_fline);
flines[max_fline] = (char*)xrealloc(MEMPTR(current_line), strlen(current_line) + 1 + 4) + 4;
LINENO(flines[max_fline]) = max_lineno;
if (terminated)
max_lineno++;
if (max_fline >= MAXLINES) {
eof_error = 0; /* Pretend we saw EOF */
break;
}
if (!(option_mask32 & FLAG_S)
? (max_fline > cur_fline + max_displayed_line)
: (max_fline >= cur_fline
&& max_lineno > LINENO(flines[cur_fline]) + max_displayed_line)
) {
#if !ENABLE_FEATURE_LESS_REGEXP
break;
#else
if (wanted_match >= num_matches) { /* goto_match called us */
fill_match_lines(old_max_fline);
old_max_fline = max_fline;
}
if (wanted_match < num_matches)
break;
#endif
}
if (eof_error <= 0) {
break;
}
max_fline++;
current_line = ((char*)xmalloc(w + 4)) + 4;
p = current_line;
last_line_pos = 0;
} /* end of "read lines until we reach cur_fline" loop */
if (eof_error < 0) {
if (errno == EAGAIN) {
eof_error = 1;
} else {
print_statusline(bb_msg_read_error);
}
}
#if ENABLE_FEATURE_LESS_FLAGS
else if (eof_error == 0)
num_lines = max_lineno;
#endif
fill_match_lines(old_max_fline);
#if ENABLE_FEATURE_LESS_REGEXP
/* prevent us from being stuck in search for a match */
wanted_match = -1;
#endif
#undef readbuf
}
#if ENABLE_FEATURE_LESS_FLAGS
static int safe_lineno(int fline)
{
if (fline >= max_fline)
fline = max_fline - 1;
/* also catches empty file (max_fline == 0) */
if (fline < 0)
return 0;
return LINENO(flines[fline]) + 1;
}
/* Print a status line if -M was specified */
static void m_status_print(void)
{
int first, last;
unsigned percent;
if (less_gets_pos >= 0) /* don't touch statusline while input is done! */
return;
clear_line();
printf(HIGHLIGHT"%s", filename);
if (num_files > 1)
printf(" (file %i of %i)", current_file, num_files);
first = safe_lineno(cur_fline);
last = (option_mask32 & FLAG_S)
? MIN(first + max_displayed_line, max_lineno)
: safe_lineno(cur_fline + max_displayed_line);
printf(" lines %i-%i", first, last);
if (num_lines == READING_FILE) {
int count, fd;
ssize_t len, i;
char buf[4096];
struct stat stbuf;
/* count number of lines in file */
count = 0;
fd = open(filename, O_RDONLY);
if (fd < 0)
goto skip;
if (fstat(fd, &stbuf) != 0 || !S_ISREG(stbuf.st_mode))
goto do_close;
while ((len = safe_read(fd, buf, sizeof(buf))) > 0) {
for (i = 0; i < len; ++i) {
if (buf[i] == '\n' && ++count == MAXLINES)
goto done;
}
}
done:
num_lines = count;
do_close:
close(fd);
skip: ;
}
if (num_lines >= 0)
printf("/%i", num_lines);
if (cur_fline >= (int)(max_fline - max_displayed_line)) {
printf(" (END)");
if (num_files > 1 && current_file != num_files)
printf(" - next: %s", files[current_file]);
} else if (num_lines > 0) {
percent = (100 * last + num_lines/2) / num_lines;
printf(" %i%%", percent <= 100 ? percent : 100);
}
printf(NORMAL);
}
#endif
/* Print the status line */
static void status_print(void)
{
const char *p;
if (less_gets_pos >= 0) /* don't touch statusline while input is done! */
return;
/* Change the status if flags have been set */
#if ENABLE_FEATURE_LESS_FLAGS
if (option_mask32 & (FLAG_M|FLAG_m)) {
m_status_print();
return;
}
/* No flags set */
#endif
clear_line();
if (cur_fline && cur_fline < (int)(max_fline - max_displayed_line)) {
bb_putchar(':');
return;
}
p = "(END)";
if (!cur_fline)
p = filename;
if (num_files > 1) {
printf(HIGHLIGHT"%s (file %i of %i)"NORMAL,
p, current_file, num_files);
return;
}
print_hilite(p);
}
static void cap_cur_fline(int nlines)
{
int diff;
if (cur_fline < 0)
cur_fline = 0;
if (cur_fline + max_displayed_line > max_fline + TILDES) {
cur_fline -= nlines;
if (cur_fline < 0)
cur_fline = 0;
diff = max_fline - (cur_fline + max_displayed_line) + TILDES;
/* As the number of lines requested was too large, we just move
* to the end of the file */
if (diff > 0)
cur_fline += diff;
}
}
static const char controls[] ALIGN1 =
/* NUL: never encountered; TAB: not converted */
/**/"\x01\x02\x03\x04\x05\x06\x07\x08" "\x0a\x0b\x0c\x0d\x0e\x0f"
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
"\x7f\x9b"; /* DEL and infamous Meta-ESC :( */
static const char ctrlconv[] ALIGN1 =
/* why 40 instead of 4a below? - it is a replacement for '\n'.
* '\n' is a former NUL - we subst it with @, not J */
"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x40\x4b\x4c\x4d\x4e\x4f"
"\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f";
static void print_lineno(const char *line)
{
const char *fmt = " ";
unsigned n = n; /* for compiler */
if (line != empty_line_marker) {
/* Width of 7 preserves tab spacing in the text */
fmt = "%7u ";
n = LINENO(line) + 1;
if (n > 9999999 && MAXLINES > 9999999) {
n %= 10000000;
fmt = "%07u ";
}
}
printf(fmt, n);
}
#if ENABLE_FEATURE_LESS_REGEXP
static void print_found(const char *line)
{
int match_status;
int eflags;
char *growline;
regmatch_t match_structs;
char buf[width];
const char *str = line;
char *p = buf;
size_t n;
while (*str) {
n = strcspn(str, controls);
if (n) {
if (!str[n]) break;
memcpy(p, str, n);
p += n;
str += n;
}
n = strspn(str, controls);
memset(p, '.', n);
p += n;
str += n;
}
strcpy(p, str);
/* buf[] holds quarantined version of str */
/* Each part of the line that matches has the HIGHLIGHT
* and NORMAL escape sequences placed around it.
* NB: we regex against line, but insert text
* from quarantined copy (buf[]) */
str = buf;
growline = NULL;
eflags = 0;
goto start;
while (match_status == 0) {
char *new = xasprintf("%s%.*s"HIGHLIGHT"%.*s"NORMAL,
growline ? growline : "",
(int)match_structs.rm_so, str,
(int)(match_structs.rm_eo - match_structs.rm_so),
str + match_structs.rm_so);
free(growline);
growline = new;
str += match_structs.rm_eo;
line += match_structs.rm_eo;
eflags = REG_NOTBOL;
start:
/* Most of the time doesn't find the regex, optimize for that */
match_status = regexec(&pattern, line, 1, &match_structs, eflags);
/* if even "" matches, treat it as "not a match" */
if (match_structs.rm_so >= match_structs.rm_eo)
match_status = 1;
}
printf("%s%s\n", growline ? growline : "", str);
free(growline);
}
#else
void print_found(const char *line);
#endif
static void print_ascii(const char *str)
{
char buf[width];
char *p;
size_t n;
while (*str) {
n = strcspn(str, controls);
if (n) {
if (!str[n]) break;
printf("%.*s", (int) n, str);
str += n;
}
n = strspn(str, controls);
p = buf;
do {
if (*str == 0x7f)
*p++ = '?';
else if (*str == (char)0x9b)
/* VT100's CSI, aka Meta-ESC. Who's inventor? */
/* I want to know who committed this sin */
*p++ = '{';
else
*p++ = ctrlconv[(unsigned char)*str];
str++;
} while (--n);
*p = '\0';
print_hilite(buf);
}
puts(str);
}
/* Print the buffer */
static void buffer_print(void)
{
unsigned i;
move_cursor(0, 0);
for (i = 0; i <= max_displayed_line; i++) {
printf(CLEAR_2_EOL);
if (option_mask32 & FLAG_N)
print_lineno(buffer[i]);
if (pattern_valid)
print_found(buffer[i]);
else
print_ascii(buffer[i]);
}
if ((option_mask32 & FLAG_E)
&& eof_error <= 0
&& (max_fline - cur_fline) <= max_displayed_line
) {
less_exit(EXIT_SUCCESS);
}
status_print();
}
static void buffer_fill_and_print(void)
{
unsigned i;
#if ENABLE_FEATURE_LESS_TRUNCATE
int fpos = cur_fline;
if (option_mask32 & FLAG_S) {
/* Go back to the beginning of this line */
while (fpos && LINENO(flines[fpos]) == LINENO(flines[fpos-1]))
fpos--;
}
i = 0;
while (i <= max_displayed_line && fpos <= max_fline) {
int lineno = LINENO(flines[fpos]);
buffer[i] = flines[fpos];
i++;
do {
fpos++;
} while ((fpos <= max_fline)
&& (option_mask32 & FLAG_S)
&& lineno == LINENO(flines[fpos])
);
}
#else
for (i = 0; i <= max_displayed_line && cur_fline + i <= max_fline; i++) {
buffer[i] = flines[cur_fline + i];
}
#endif
for (; i <= max_displayed_line; i++) {
buffer[i] = empty_line_marker;
}
buffer_print();
}
/* Move the buffer up and down in the file in order to scroll */
static void buffer_down(int nlines)
{
cur_fline += nlines;
read_lines();
cap_cur_fline(nlines);
buffer_fill_and_print();
}
static void buffer_up(int nlines)
{
cur_fline -= nlines;
if (cur_fline < 0) cur_fline = 0;
read_lines();
buffer_fill_and_print();
}
static void buffer_line(int linenum)
{
if (linenum < 0)
linenum = 0;
cur_fline = linenum;
read_lines();
if (linenum + max_displayed_line > max_fline)
linenum = max_fline - max_displayed_line + TILDES;
if (linenum < 0)
linenum = 0;
cur_fline = linenum;
buffer_fill_and_print();
}
static void open_file_and_read_lines(void)
{
if (filename) {
xmove_fd(xopen(filename, O_RDONLY), STDIN_FILENO);
} else {
/* "less" with no arguments in argv[] */
/* For status line only */
filename = xstrdup(bb_msg_standard_input);
}
readpos = 0;
readeof = 0;
last_line_pos = 0;
terminated = 1;
read_lines();
}
/* Reinitialize everything for a new file - free the memory and start over */
static void reinitialize(void)
{
unsigned i;
if (flines) {
for (i = 0; i <= max_fline; i++)
free(MEMPTR(flines[i]));
free(flines);
flines = NULL;
}
max_fline = -1;
cur_fline = 0;
max_lineno = 0;
#if ENABLE_FEATURE_LESS_FLAGS
num_lines = filename ? READING_FILE : READING_STDIN;
#endif
open_file_and_read_lines();
#if ENABLE_FEATURE_LESS_ASK_TERMINAL
if (G.winsize_err)
printf("\033[999;999H" "\033[6n");
#endif
buffer_fill_and_print();
}
static int64_t getch_nowait(void)
{
int rd;
int64_t key64;
struct pollfd pfd[2];
pfd[0].fd = STDIN_FILENO;
pfd[0].events = POLLIN;
pfd[1].fd = kbd_fd;
pfd[1].events = POLLIN;
again:
tcsetattr(kbd_fd, TCSANOW, &term_less);
/* NB: select/poll returns whenever read will not block. Therefore:
* if eof is reached, select/poll will return immediately
* because read will immediately return 0 bytes.
* Even if select/poll says that input is available, read CAN block
* (switch fd into O_NONBLOCK'ed mode to avoid it)
*/
rd = 1;
/* Are we interested in stdin? */
//TODO: reuse code for determining this
if (!(option_mask32 & FLAG_S)
? !(max_fline > cur_fline + max_displayed_line)
: !(max_fline >= cur_fline
&& max_lineno > LINENO(flines[cur_fline]) + max_displayed_line)
) {
if (eof_error > 0) /* did NOT reach eof yet */
rd = 0; /* yes, we are interested in stdin */
}
/* Position cursor if line input is done */
if (less_gets_pos >= 0)
move_cursor(max_displayed_line + 2, less_gets_pos + 1);
fflush_all();
if (kbd_input[0] == 0) { /* if nothing is buffered */
#if ENABLE_FEATURE_LESS_WINCH
while (1) {
int r;
/* NB: SIGWINCH interrupts poll() */
r = poll(pfd + rd, 2 - rd, -1);
if (/*r < 0 && errno == EINTR &&*/ winch_counter)
return '\\'; /* anything which has no defined function */
if (r) break;
}
#else
safe_poll(pfd + rd, 2 - rd, -1);
#endif
}
/* We have kbd_fd in O_NONBLOCK mode, read inside read_key()
* would not block even if there is no input available */
key64 = read_key(kbd_fd, kbd_input, /*timeout off:*/ -2);
if ((int)key64 == -1) {
if (errno == EAGAIN) {
/* No keyboard input available. Since poll() did return,
* we should have input on stdin */
read_lines();
buffer_fill_and_print();
goto again;
}
/* EOF/error (ssh session got killed etc) */
less_exit(0);
}
set_tty_cooked();
return key64;
}
/* Grab a character from input without requiring the return key.
* May return KEYCODE_xxx values.
* Note that this function works best with raw input. */
static int64_t less_getch(int pos)
{
int64_t key64;
int key;
again:
less_gets_pos = pos;
key = key64 = getch_nowait();
less_gets_pos = -1;
/* Discard Ctrl-something chars.
* (checking only lower 32 bits is a size optimization:
* upper 32 bits are used only by KEYCODE_CURSOR_POS)
*/
if (key >= 0 && key < ' ' && key != 0x0d && key != 8)
goto again;
return key64;
}
static char* less_gets(int sz)
{
int c;
unsigned i = 0;
char *result = xzalloc(1);
while (1) {
c = '\0';
less_gets_pos = sz + i;
c = getch_nowait();
if (c == 0x0d) {
result[i] = '\0';
less_gets_pos = -1;
return result;
}
if (c == 0x7f)
c = 8;
if (c == 8 && i) {
printf("\x8 \x8");
i--;
}
if (c < ' ') /* filters out KEYCODE_xxx too (<0) */
continue;
if (i >= width - sz - 1)
continue; /* len limit */
bb_putchar(c);
result[i++] = c;
result = xrealloc(result, i+1);
}
}
static void examine_file(void)
{
char *new_fname;
print_statusline("Examine: ");
new_fname = less_gets(sizeof("Examine: ") - 1);
if (!new_fname[0]) {
status_print();
err:
free(new_fname);
return;
}
if (access(new_fname, R_OK) != 0) {
print_statusline("Cannot read this file");
goto err;
}
free(filename);
filename = new_fname;
/* files start by = argv. why we assume that argv is infinitely long??
files[num_files] = filename;
current_file = num_files + 1;
num_files++; */
files[0] = filename;
num_files = current_file = 1;
reinitialize();
}
/* This function changes the file currently being paged. direction can be one of the following:
* -1: go back one file
* 0: go to the first file
* 1: go forward one file */
static void change_file(int direction)
{
if (current_file != ((direction > 0) ? num_files : 1)) {
current_file = direction ? current_file + direction : 1;
free(filename);
filename = xstrdup(files[current_file - 1]);
reinitialize();
} else {
print_statusline(direction > 0 ? "No next file" : "No previous file");
}
}
static void remove_current_file(void)
{
unsigned i;
if (num_files < 2)
return;
if (current_file != 1) {
change_file(-1);
for (i = 3; i <= num_files; i++)
files[i - 2] = files[i - 1];
num_files--;
} else {
change_file(1);
for (i = 2; i <= num_files; i++)
files[i - 2] = files[i - 1];
num_files--;
current_file--;
}
}
static void colon_process(void)
{
int keypress;
/* Clear the current line and print a prompt */
print_statusline(" :");
keypress = less_getch(2);
switch (keypress) {
case 'd':
remove_current_file();
break;
case 'e':
examine_file();
break;
#if ENABLE_FEATURE_LESS_FLAGS
case 'f':
m_status_print();
break;
#endif
case 'n':
change_file(1);
break;
case 'p':
change_file(-1);
break;
case 'q':
less_exit(EXIT_SUCCESS);
break;
case 'x':
change_file(0);
break;
}
}
#if ENABLE_FEATURE_LESS_REGEXP
static void normalize_match_pos(int match)
{
if (match >= num_matches)
match = num_matches - 1;
if (match < 0)
match = 0;
match_pos = match;
}
static void goto_match(int match)
{
if (!pattern_valid)
return;
if (match < 0)
match = 0;
/* Try to find next match if eof isn't reached yet */
if (match >= num_matches && eof_error > 0) {
wanted_match = match; /* "I want to read until I see N'th match" */
read_lines();
}
if (num_matches) {
normalize_match_pos(match);
buffer_line(match_lines[match_pos]);
} else {
print_statusline("No matches found");
}
}
static void fill_match_lines(unsigned pos)
{
if (!pattern_valid)
return;
/* Run the regex on each line of the current file */
while (pos <= max_fline) {
/* If this line matches */
if (regexec(&pattern, flines[pos], 0, NULL, 0) == 0
/* and we didn't match it last time */
&& !(num_matches && match_lines[num_matches-1] == pos)
) {
match_lines = xrealloc_vector(match_lines, 4, num_matches);
match_lines[num_matches++] = pos;
}
pos++;
}
}
static void regex_process(void)
{
char *uncomp_regex, *err;
/* Reset variables */
free(match_lines);
match_lines = NULL;
match_pos = 0;
num_matches = 0;
if (pattern_valid) {
regfree(&pattern);
pattern_valid = 0;
}
/* Get the uncompiled regular expression from the user */
clear_line();
bb_putchar((option_mask32 & LESS_STATE_MATCH_BACKWARDS) ? '?' : '/');
uncomp_regex = less_gets(1);
if (!uncomp_regex[0]) {
free(uncomp_regex);
buffer_print();
return;
}
/* Compile the regex and check for errors */
err = regcomp_or_errmsg(&pattern, uncomp_regex,
(option_mask32 & FLAG_I) ? REG_ICASE : 0);
free(uncomp_regex);
if (err) {
print_statusline(err);
free(err);
return;
}
pattern_valid = 1;
match_pos = 0;
fill_match_lines(0);
while (match_pos < num_matches) {
if ((int)match_lines[match_pos] > cur_fline)
break;
match_pos++;
}
if (option_mask32 & LESS_STATE_MATCH_BACKWARDS)
match_pos--;
/* It's possible that no matches are found yet.
* goto_match() will read input looking for match,
* if needed */
goto_match(match_pos);
}
#endif
static void number_process(int first_digit)
{
unsigned i;
int num;
int keypress;
char num_input[sizeof(int)*4]; /* more than enough */
num_input[0] = first_digit;
/* Clear the current line, print a prompt, and then print the digit */
clear_line();
printf(":%c", first_digit);
/* Receive input until a letter is given */
i = 1;
while (i < sizeof(num_input)-1) {
keypress = less_getch(i + 1);
if ((unsigned)keypress > 255 || !isdigit(num_input[i]))
break;
num_input[i] = keypress;
bb_putchar(keypress);
i++;
}
num_input[i] = '\0';
num = bb_strtou(num_input, NULL, 10);
/* on format error, num == -1 */
if (num < 1 || num > MAXLINES) {
buffer_print();
return;
}
/* We now know the number and the letter entered, so we process them */
switch (keypress) {
case KEYCODE_DOWN: case 'z': case 'd': case 'e': case ' ': case '\015':
buffer_down(num);
break;
case KEYCODE_UP: case 'b': case 'w': case 'y': case 'u':
buffer_up(num);
break;
case 'g': case '<': case 'G': case '>':
cur_fline = num + max_displayed_line;
read_lines();
buffer_line(num - 1);
break;
case 'p': case '%':
num = num * (max_fline / 100); /* + max_fline / 2; */
cur_fline = num + max_displayed_line;
read_lines();
buffer_line(num);
break;
#if ENABLE_FEATURE_LESS_REGEXP
case 'n':
goto_match(match_pos + num);
break;
case '/':
option_mask32 &= ~LESS_STATE_MATCH_BACKWARDS;
regex_process();
break;
case '?':
option_mask32 |= LESS_STATE_MATCH_BACKWARDS;
regex_process();
break;
#endif
}
}
#if ENABLE_FEATURE_LESS_DASHCMD
static void flag_change(void)
{
int keypress;
clear_line();
bb_putchar('-');
keypress = less_getch(1);
switch (keypress) {
case 'M':
option_mask32 ^= FLAG_M;
break;
case 'm':
option_mask32 ^= FLAG_m;
break;
case 'E':
option_mask32 ^= FLAG_E;
break;
case '~':
option_mask32 ^= FLAG_TILDE;
break;
#if ENABLE_FEATURE_LESS_TRUNCATE
case 'S':
option_mask32 ^= FLAG_S;
buffer_fill_and_print();
break;
#endif
#if ENABLE_FEATURE_LESS_LINENUMS
case 'N':
option_mask32 ^= FLAG_N;
re_wrap();
buffer_fill_and_print();
break;
#endif
}
}
#ifdef BLOAT
static void show_flag_status(void)
{
int keypress;
int flag_val;
clear_line();
bb_putchar('_');
keypress = less_getch(1);
switch (keypress) {
case 'M':
flag_val = option_mask32 & FLAG_M;
break;
case 'm':
flag_val = option_mask32 & FLAG_m;
break;
case '~':
flag_val = option_mask32 & FLAG_TILDE;
break;
case 'N':
flag_val = option_mask32 & FLAG_N;
break;
case 'E':
flag_val = option_mask32 & FLAG_E;
break;
default:
flag_val = 0;
break;
}
clear_line();
printf(HIGHLIGHT"The status of the flag is: %u"NORMAL, flag_val != 0);
}
#endif
#endif /* ENABLE_FEATURE_LESS_DASHCMD */
static void save_input_to_file(void)
{
const char *msg = "";
char *current_line;
unsigned i;
FILE *fp;
print_statusline("Log file: ");
current_line = less_gets(sizeof("Log file: ")-1);
if (current_line[0]) {
fp = fopen_for_write(current_line);
if (!fp) {
msg = "Error opening log file";
goto ret;
}
for (i = 0; i <= max_fline; i++)
fprintf(fp, "%s\n", flines[i]);
fclose(fp);
msg = "Done";
}
ret:
print_statusline(msg);
free(current_line);
}
#if ENABLE_FEATURE_LESS_MARKS
static void add_mark(void)
{
int letter;
print_statusline("Mark: ");
letter = less_getch(sizeof("Mark: ") - 1);
if (isalpha(letter)) {
/* If we exceed 15 marks, start overwriting previous ones */
if (num_marks == 14)
num_marks = 0;
mark_lines[num_marks][0] = letter;
mark_lines[num_marks][1] = cur_fline;
num_marks++;
} else {
print_statusline("Invalid mark letter");
}
}
static void goto_mark(void)
{
int letter;
int i;
print_statusline("Go to mark: ");
letter = less_getch(sizeof("Go to mark: ") - 1);
clear_line();
if (isalpha(letter)) {
for (i = 0; i <= num_marks; i++)
if (letter == mark_lines[i][0]) {
buffer_line(mark_lines[i][1]);
break;
}
if (num_marks == 14 && letter != mark_lines[14][0])
print_statusline("Mark not set");
} else
print_statusline("Invalid mark letter");
}
#endif
#if ENABLE_FEATURE_LESS_BRACKETS
static char opp_bracket(char bracket)
{
switch (bracket) {
case '{': case '[': /* '}' == '{' + 2. Same for '[' */
bracket++;
case '(': /* ')' == '(' + 1 */
bracket++;
break;
case '}': case ']':
bracket--;
case ')':
bracket--;
break;
};
return bracket;
}
static void match_right_bracket(char bracket)
{
unsigned i;
if (strchr(flines[cur_fline], bracket) == NULL) {
print_statusline("No bracket in top line");
return;
}
bracket = opp_bracket(bracket);
for (i = cur_fline + 1; i < max_fline; i++) {
if (strchr(flines[i], bracket) != NULL) {
buffer_line(i);
return;
}
}
print_statusline("No matching bracket found");
}
static void match_left_bracket(char bracket)
{
int i;
if (strchr(flines[cur_fline + max_displayed_line], bracket) == NULL) {
print_statusline("No bracket in bottom line");
return;
}
bracket = opp_bracket(bracket);
for (i = cur_fline + max_displayed_line; i >= 0; i--) {
if (strchr(flines[i], bracket) != NULL) {
buffer_line(i);
return;
}
}
print_statusline("No matching bracket found");
}
#endif /* FEATURE_LESS_BRACKETS */
static void keypress_process(int keypress)
{
switch (keypress) {
case KEYCODE_DOWN: case 'e': case 'j': case 0x0d:
buffer_down(1);
break;
case KEYCODE_UP: case 'y': case 'k':
buffer_up(1);
break;
case KEYCODE_PAGEDOWN: case ' ': case 'z': case 'f':
buffer_down(max_displayed_line + 1);
break;
case KEYCODE_PAGEUP: case 'w': case 'b':
buffer_up(max_displayed_line + 1);
break;
case 'd':
buffer_down((max_displayed_line + 1) / 2);
break;
case 'u':
buffer_up((max_displayed_line + 1) / 2);
break;
case KEYCODE_HOME: case 'g': case 'p': case '<': case '%':
buffer_line(0);
break;
case KEYCODE_END: case 'G': case '>':
cur_fline = MAXLINES;
read_lines();
buffer_line(cur_fline);
break;
case 'q': case 'Q':
less_exit(EXIT_SUCCESS);
break;
#if ENABLE_FEATURE_LESS_MARKS
case 'm':
add_mark();
buffer_print();
break;
case '\'':
goto_mark();
buffer_print();
break;
#endif
case 'r': case 'R':
/* TODO: (1) also bind ^R, ^L to this?
* (2) re-measure window size?
*/
buffer_print();
break;
/*case 'R':
full_repaint();
break;*/
case 's':
save_input_to_file();
break;
case 'E':
examine_file();
break;
#if ENABLE_FEATURE_LESS_FLAGS
case '=':
m_status_print();
break;
#endif
#if ENABLE_FEATURE_LESS_REGEXP
case '/':
option_mask32 &= ~LESS_STATE_MATCH_BACKWARDS;
regex_process();
break;
case 'n':
goto_match(match_pos + 1);
break;
case 'N':
goto_match(match_pos - 1);
break;
case '?':
option_mask32 |= LESS_STATE_MATCH_BACKWARDS;
regex_process();
break;
#endif
#if ENABLE_FEATURE_LESS_DASHCMD
case '-':
flag_change();
buffer_print();
break;
#ifdef BLOAT
case '_':
show_flag_status();
break;
#endif
#endif
#if ENABLE_FEATURE_LESS_BRACKETS
case '{': case '(': case '[':
match_right_bracket(keypress);
break;
case '}': case ')': case ']':
match_left_bracket(keypress);
break;
#endif
case ':':
colon_process();
break;
}
if (isdigit(keypress))
number_process(keypress);
}
static void sig_catcher(int sig)
{
less_exit(- sig);
}
#if ENABLE_FEATURE_LESS_WINCH
static void sigwinch_handler(int sig UNUSED_PARAM)
{
winch_counter++;
}
#endif
int less_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int less_main(int argc, char **argv)
{
char *tty_name;
int tty_fd;
INIT_G();
/* TODO: -x: do not interpret backspace, -xx: tab also
* -xxx: newline also
* -w N: assume width N (-xxx -w 32: hex viewer of sorts)
* -s: condense many empty lines to one
* (used by some setups for manpage display)
*/
getopt32(argv, "EMmN~I" IF_FEATURE_LESS_TRUNCATE("S") /*ignored:*/"s");
argc -= optind;
argv += optind;
num_files = argc;
files = argv;
/* 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);
if (!num_files) {
if (isatty(STDIN_FILENO)) {
/* Just "less"? No args and no redirection? */
bb_error_msg("missing filename");
bb_show_usage();
}
} else {
filename = xstrdup(files[0]);
}
if (option_mask32 & FLAG_TILDE)
empty_line_marker = "";
/* Some versions of less can survive w/o controlling tty,
* try to do the same. This also allows to specify an alternative
* tty via "less 1<>TTY".
* We don't try to use STDOUT_FILENO directly,
* since we want to set this fd to non-blocking mode,
* and not bother with restoring it on exit.
*/
tty_name = xmalloc_ttyname(STDOUT_FILENO);
if (tty_name) {
tty_fd = open(tty_name, O_RDONLY);
free(tty_name);
if (tty_fd < 0)
goto try_ctty;
} else {
/* Try controlling tty */
try_ctty:
tty_fd = open(CURRENT_TTY, O_RDONLY);
if (tty_fd < 0)
return bb_cat(argv);
}
ndelay_on(tty_fd);
kbd_fd = tty_fd; /* save in a global */
tcgetattr(kbd_fd, &term_orig);
term_less = term_orig;
term_less.c_lflag &= ~(ICANON | ECHO);
term_less.c_iflag &= ~(IXON | ICRNL);
/*term_less.c_oflag &= ~ONLCR;*/
term_less.c_cc[VMIN] = 1;
term_less.c_cc[VTIME] = 0;
IF_FEATURE_LESS_ASK_TERMINAL(G.winsize_err =) get_terminal_width_height(kbd_fd, &width, &max_displayed_line);
/* 20: two tabstops + 4 */
if (width < 20 || max_displayed_line < 3)
return bb_cat(argv);
max_displayed_line -= 2;
/* We want to restore term_orig on exit */
bb_signals(BB_FATAL_SIGS, sig_catcher);
#if ENABLE_FEATURE_LESS_WINCH
signal(SIGWINCH, sigwinch_handler);
#endif
buffer = xmalloc((max_displayed_line+1) * sizeof(char *));
reinitialize();
while (1) {
int64_t keypress;
#if ENABLE_FEATURE_LESS_WINCH
while (WINCH_COUNTER) {
again:
winch_counter--;
IF_FEATURE_LESS_ASK_TERMINAL(G.winsize_err =) get_terminal_width_height(kbd_fd, &width, &max_displayed_line);
IF_FEATURE_LESS_ASK_TERMINAL(got_size:)
/* 20: two tabstops + 4 */
if (width < 20)
width = 20;
if (max_displayed_line < 3)
max_displayed_line = 3;
max_displayed_line -= 2;
free(buffer);
buffer = xmalloc((max_displayed_line+1) * sizeof(char *));
/* Avoid re-wrap and/or redraw if we already know
* we need to do it again. These ops are expensive */
if (WINCH_COUNTER)
goto again;
re_wrap();
if (WINCH_COUNTER)
goto again;
buffer_fill_and_print();
/* This took some time. Loop back and check,
* were there another SIGWINCH? */
}
keypress = less_getch(-1); /* -1: do not position cursor */
# if ENABLE_FEATURE_LESS_ASK_TERMINAL
if ((int32_t)keypress == KEYCODE_CURSOR_POS) {
uint32_t rc = (keypress >> 32);
width = (rc & 0x7fff);
max_displayed_line = ((rc >> 16) & 0x7fff);
goto got_size;
}
# endif
#else
keypress = less_getch(-1); /* -1: do not position cursor */
#endif
keypress_process(keypress);
}
}
/*
Help text of less version 418 is below.
If you are implementing something, keeping
key and/or command line switch compatibility is a good idea:
SUMMARY OF LESS COMMANDS
Commands marked with * may be preceded by a number, N.
Notes in parentheses indicate the behavior if N is given.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MOVING
e ^E j ^N CR * Forward one line (or N lines).
y ^Y k ^K ^P * Backward one line (or N lines).
f ^F ^V SPACE * Forward one window (or N lines).
b ^B ESC-v * Backward one window (or N lines).
z * Forward one window (and set window to N).
w * Backward one window (and set window to N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to N).
u ^U * Backward one half-window (and set half-window to N).
ESC-) RightArrow * Left one half screen width (or N positions).
ESC-( LeftArrow * Right one half screen width (or N positions).
F Forward forever; like "tail -f".
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SEARCHING
/pattern * Search forward for (N-th) matching line.
?pattern * Search backward for (N-th) matching line.
n * Repeat previous search (for N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
---------------------------------------------------
Search patterns may be modified by one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
---------------------------------------------------------------------------
JUMPING
g < ESC-< * Go to first line in file (or line N).
G > ESC-> * Go to last line in file (or line N).
p % * Go to beginning of file (or N percent into file).
t * Go to the (N-th) next tag.
T * Go to the (N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F <c1> <c2> * Find close bracket <c2>.
ESC-^B <c1> <c2> * Find open bracket <c1>
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (N-th) close bracket in the bottom line.
m<letter> Mark the current position with <letter>.
'<letter> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CHANGING FILES
:e [file] Examine a new file.
^X^V Same as :e.
:n * Examine the (N-th) next file from the command line.
:p * Examine the (N-th) previous file from the command line.
:x * Examine the first (or N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MISCELLANEOUS COMMANDS
-<flag> Toggle a command line option [see OPTIONS below].
--<name> Toggle a command line option, by name.
_<flag> Display the setting of a command line option.
__<name> Display the setting of an option, by name.
+cmd Execute the less cmd each time a new file is examined.
!command Execute the shell command with $SHELL.
|Xcommand Pipe file between current pos & mark X to shell command.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OPTIONS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Forward search skips current screen.
-b [N] .... --buffers=[N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D [xn.n] . --color=xn.n
Set screen colors. (MS-DOS only)
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [N] .... --max-back-scroll=[N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [N] .... --jump-target=[N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [file] . --lesskey-file=[file]
Use a lesskey file.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n -N .... --line-numbers --LINE-NUMBERS
Don't use line numbers.
-o [file] . --log-file=[file]
Copy to log file (standard input only).
-O [file] . --LOG-FILE=[file]
Copy to log file (unconditionally overwrite).
-p [pattern] --pattern=[pattern]
Start at pattern (from command line).
-P [prompt] --prompt=[prompt]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop long lines.
-t [tag] .. --tag=[tag]
Find a tag.
-T [tagsfile] --tag-file=[tagsfile]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [N[,...]] --tabs=[N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
--no-keypad
Don't use termcap keypad init/deinit strings.
-y [N] .... --max-forw-scroll=[N]
Forward scroll limit.
-z [N] .... --window=[N]
Set size of window.
-" [c[c]] . --quotes=[c[c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [N] .... --shift=[N]
Horizontal scroll amount (0 = one half screen width)
---------------------------------------------------------------------------
LINE EDITING
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ESC-l Move cursor right one character.
LeftArrow ESC-h Move cursor left one character.
CNTL-RightArrow ESC-RightArrow ESC-w Move cursor right one word.
CNTL-LeftArrow ESC-LeftArrow ESC-b Move cursor left one word.
HOME ESC-0 Move cursor to start of line.
END ESC-$ Move cursor to end of line.
BACKSPACE Delete char to left of cursor.
DELETE ESC-x Delete char under cursor.
CNTL-BACKSPACE ESC-BACKSPACE Delete word to left of cursor.
CNTL-DELETE ESC-DELETE ESC-X Delete word under cursor.
CNTL-U ESC (MS-DOS only) Delete entire line.
UpArrow ESC-k Retrieve previous command line.
DownArrow ESC-j Retrieve next command line.
TAB Complete filename & cycle.
SHIFT-TAB ESC-TAB Complete filename & reverse cycle.
CNTL-L Complete filename, list all.
*/