lesskey.c revision 191930
1/*
2 * Copyright (C) 1984-2008  Mark Nudelman
3 *
4 * You may distribute under the terms of either the GNU General Public
5 * License or the Less License, as specified in the README file.
6 *
7 * For more information about less, or for information on how to
8 * contact the author, see the README file.
9 */
10
11
12/*
13 *	lesskey [-o output] [input]
14 *
15 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
16 *
17 *	Make a .less file.
18 *	If no input file is specified, standard input is used.
19 *	If no output file is specified, $HOME/.less is used.
20 *
21 *	The .less file is used to specify (to "less") user-defined
22 *	key bindings.  Basically any sequence of 1 to MAX_CMDLEN
23 *	keystrokes may be bound to an existing less function.
24 *
25 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
26 *
27 *	The input file is an ascii file consisting of a
28 *	sequence of lines of the form:
29 *		string <whitespace> action [chars] <newline>
30 *
31 *	"string" is a sequence of command characters which form
32 *		the new user-defined command.  The command
33 *		characters may be:
34 *		1. The actual character itself.
35 *		2. A character preceded by ^ to specify a
36 *		   control character (e.g. ^X means control-X).
37 *		3. A backslash followed by one to three octal digits
38 *		   to specify a character by its octal value.
39 *		4. A backslash followed by b, e, n, r or t
40 *		   to specify \b, ESC, \n, \r or \t, respectively.
41 *		5. Any character (other than those mentioned above) preceded
42 *		   by a \ to specify the character itself (characters which
43 *		   must be preceded by \ include ^, \, and whitespace.
44 *	"action" is the name of a "less" action, from the table below.
45 *	"chars" is an optional sequence of characters which is treated
46 *		as keyboard input after the command is executed.
47 *
48 *	Blank lines and lines which start with # are ignored,
49 *	except for the special control lines:
50 *		#command	Signals the beginning of the command
51 *				keys section.
52 *		#line-edit	Signals the beginning of the line-editing
53 *				keys section.
54 *		#env		Signals the beginning of the environment
55 *				variable section.
56 *		#stop		Stops command parsing in less;
57 *				causes all default keys to be disabled.
58 *
59 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
60 *
61 *	The output file is a non-ascii file, consisting of a header,
62 *	one or more sections, and a trailer.
63 *	Each section begins with a section header, a section length word
64 *	and the section data.  Normally there are three sections:
65 *		CMD_SECTION	Definition of command keys.
66 *		EDIT_SECTION	Definition of editing keys.
67 *		END_SECTION	A special section header, with no
68 *				length word or section data.
69 *
70 *	Section data consists of zero or more byte sequences of the form:
71 *		string <0> <action>
72 *	or
73 *		string <0> <action|A_EXTRA> chars <0>
74 *
75 *	"string" is the command string.
76 *	"<0>" is one null byte.
77 *	"<action>" is one byte containing the action code (the A_xxx value).
78 *	If action is ORed with A_EXTRA, the action byte is followed
79 *		by the null-terminated "chars" string.
80 *
81 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
82 */
83
84#include "less.h"
85#include "lesskey.h"
86#include "cmd.h"
87
88struct cmdname
89{
90	char *cn_name;
91	int cn_action;
92};
93
94struct cmdname cmdnames[] =
95{
96	{ "back-bracket",         A_B_BRACKET },
97	{ "back-line",            A_B_LINE },
98	{ "back-line-force",      A_BF_LINE },
99	{ "back-screen",          A_B_SCREEN },
100	{ "back-scroll",          A_B_SCROLL },
101	{ "back-search",          A_B_SEARCH },
102	{ "back-window",          A_B_WINDOW },
103	{ "debug",                A_DEBUG },
104	{ "digit",                A_DIGIT },
105	{ "display-flag",         A_DISP_OPTION },
106	{ "display-option",       A_DISP_OPTION },
107	{ "end",                  A_GOEND },
108	{ "examine",              A_EXAMINE },
109	{ "filter",               A_FILTER },
110	{ "first-cmd",            A_FIRSTCMD },
111	{ "firstcmd",             A_FIRSTCMD },
112	{ "flush-repaint",        A_FREPAINT },
113	{ "forw-bracket",         A_F_BRACKET },
114	{ "forw-forever",         A_F_FOREVER },
115	{ "forw-line",            A_F_LINE },
116	{ "forw-line-force",      A_FF_LINE },
117	{ "forw-screen",          A_F_SCREEN },
118	{ "forw-screen-force",    A_FF_SCREEN },
119	{ "forw-scroll",          A_F_SCROLL },
120	{ "forw-search",          A_F_SEARCH },
121	{ "forw-window",          A_F_WINDOW },
122	{ "goto-end",             A_GOEND },
123	{ "goto-line",            A_GOLINE },
124	{ "goto-mark",            A_GOMARK },
125	{ "help",                 A_HELP },
126	{ "index-file",           A_INDEX_FILE },
127	{ "invalid",              A_UINVALID },
128	{ "left-scroll",          A_LSHIFT },
129	{ "next-file",            A_NEXT_FILE },
130	{ "next-tag",             A_NEXT_TAG },
131	{ "noaction",             A_NOACTION },
132	{ "percent",              A_PERCENT },
133	{ "pipe",                 A_PIPE },
134	{ "prev-file",            A_PREV_FILE },
135	{ "prev-tag",             A_PREV_TAG },
136	{ "quit",                 A_QUIT },
137	{ "remove-file",          A_REMOVE_FILE },
138	{ "repaint",              A_REPAINT },
139	{ "repaint-flush",        A_FREPAINT },
140	{ "repeat-search",        A_AGAIN_SEARCH },
141	{ "repeat-search-all",    A_T_AGAIN_SEARCH },
142	{ "reverse-search",       A_REVERSE_SEARCH },
143	{ "reverse-search-all",   A_T_REVERSE_SEARCH },
144	{ "right-scroll",         A_RSHIFT },
145	{ "set-mark",             A_SETMARK },
146	{ "shell",                A_SHELL },
147	{ "status",               A_STAT },
148	{ "toggle-flag",          A_OPT_TOGGLE },
149	{ "toggle-option",        A_OPT_TOGGLE },
150	{ "undo-hilite",          A_UNDO_SEARCH },
151	{ "version",              A_VERSION },
152	{ "visual",               A_VISUAL },
153	{ NULL,   0 }
154};
155
156struct cmdname editnames[] =
157{
158	{ "back-complete",	EC_B_COMPLETE },
159	{ "backspace",		EC_BACKSPACE },
160	{ "delete",		EC_DELETE },
161	{ "down",		EC_DOWN },
162	{ "end",		EC_END },
163	{ "expand",		EC_EXPAND },
164	{ "forw-complete",	EC_F_COMPLETE },
165	{ "home",		EC_HOME },
166	{ "insert",		EC_INSERT },
167	{ "invalid",		EC_UINVALID },
168	{ "kill-line",		EC_LINEKILL },
169	{ "left",		EC_LEFT },
170	{ "literal",		EC_LITERAL },
171	{ "right",		EC_RIGHT },
172	{ "up",			EC_UP },
173	{ "word-backspace",	EC_W_BACKSPACE },
174	{ "word-delete",	EC_W_DELETE },
175	{ "word-left",		EC_W_LEFT },
176	{ "word-right",		EC_W_RIGHT },
177	{ NULL, 0 }
178};
179
180struct table
181{
182	struct cmdname *names;
183	char *pbuffer;
184	char buffer[MAX_USERCMD];
185};
186
187struct table cmdtable;
188struct table edittable;
189struct table vartable;
190struct table *currtable = &cmdtable;
191
192char fileheader[] = {
193	C0_LESSKEY_MAGIC,
194	C1_LESSKEY_MAGIC,
195	C2_LESSKEY_MAGIC,
196	C3_LESSKEY_MAGIC
197};
198char filetrailer[] = {
199	C0_END_LESSKEY_MAGIC,
200	C1_END_LESSKEY_MAGIC,
201	C2_END_LESSKEY_MAGIC
202};
203char cmdsection[1] =	{ CMD_SECTION };
204char editsection[1] =	{ EDIT_SECTION };
205char varsection[1] =	{ VAR_SECTION };
206char endsection[1] =	{ END_SECTION };
207
208char *infile = NULL;
209char *outfile = NULL ;
210
211int linenum;
212int errors;
213
214extern char version[];
215
216	void
217usage()
218{
219	fprintf(stderr, "usage: lesskey [-o output] [input]\n");
220	exit(1);
221}
222
223	char *
224mkpathname(dirname, filename)
225	char *dirname;
226	char *filename;
227{
228	char *pathname;
229
230	pathname = calloc(strlen(dirname) + strlen(filename) + 2, sizeof(char));
231	strcpy(pathname, dirname);
232	strcat(pathname, PATHNAME_SEP);
233	strcat(pathname, filename);
234	return (pathname);
235}
236
237/*
238 * Figure out the name of a default file (in the user's HOME directory).
239 */
240	char *
241homefile(filename)
242	char *filename;
243{
244	char *p;
245	char *pathname;
246
247	if ((p = getenv("HOME")) != NULL && *p != '\0')
248		pathname = mkpathname(p, filename);
249#if OS2
250	else if ((p = getenv("INIT")) != NULL && *p != '\0')
251		pathname = mkpathname(p, filename);
252#endif
253	else
254	{
255		fprintf(stderr, "cannot find $HOME - using current directory\n");
256		pathname = mkpathname(".", filename);
257	}
258	return (pathname);
259}
260
261/*
262 * Parse command line arguments.
263 */
264	void
265parse_args(argc, argv)
266	int argc;
267	char **argv;
268{
269	char *arg;
270
271	outfile = NULL;
272	while (--argc > 0)
273	{
274		arg = *++argv;
275		if (arg[0] != '-')
276			/* Arg does not start with "-"; it's not an option. */
277			break;
278		if (arg[1] == '\0')
279			/* "-" means standard input. */
280			break;
281		if (arg[1] == '-' && arg[2] == '\0')
282		{
283			/* "--" means end of options. */
284			argc--;
285			argv++;
286			break;
287		}
288		switch (arg[1])
289		{
290		case '-':
291			if (strncmp(arg, "--output", 8) == 0)
292			{
293				if (arg[8] == '\0')
294					outfile = &arg[8];
295				else if (arg[8] == '=')
296					outfile = &arg[9];
297				else
298					usage();
299				goto opt_o;
300			}
301			if (strcmp(arg, "--version") == 0)
302			{
303				goto opt_V;
304			}
305			usage();
306			break;
307		case 'o':
308			outfile = &argv[0][2];
309		opt_o:
310			if (*outfile == '\0')
311			{
312				if (--argc <= 0)
313					usage();
314				outfile = *(++argv);
315			}
316			break;
317		case 'V':
318		opt_V:
319			printf("lesskey  version %s\n", version);
320			exit(0);
321		default:
322			usage();
323		}
324	}
325	if (argc > 1)
326		usage();
327	/*
328	 * Open the input file, or use DEF_LESSKEYINFILE if none specified.
329	 */
330	if (argc > 0)
331		infile = *argv;
332	else
333		infile = homefile(DEF_LESSKEYINFILE);
334}
335
336/*
337 * Initialize data structures.
338 */
339	void
340init_tables()
341{
342	cmdtable.names = cmdnames;
343	cmdtable.pbuffer = cmdtable.buffer;
344
345	edittable.names = editnames;
346	edittable.pbuffer = edittable.buffer;
347
348	vartable.names = NULL;
349	vartable.pbuffer = vartable.buffer;
350}
351
352/*
353 * Parse one character of a string.
354 */
355	char *
356tstr(pp, xlate)
357	char **pp;
358	int xlate;
359{
360	register char *p;
361	register char ch;
362	register int i;
363	static char buf[10];
364	static char tstr_control_k[] =
365		{ SK_SPECIAL_KEY, SK_CONTROL_K, 6, 1, 1, 1, '\0' };
366
367	p = *pp;
368	switch (*p)
369	{
370	case '\\':
371		++p;
372		switch (*p)
373		{
374		case '0': case '1': case '2': case '3':
375		case '4': case '5': case '6': case '7':
376			/*
377			 * Parse an octal number.
378			 */
379			ch = 0;
380			i = 0;
381			do
382				ch = 8*ch + (*p - '0');
383			while (*++p >= '0' && *p <= '7' && ++i < 3);
384			*pp = p;
385			if (xlate && ch == CONTROL('K'))
386				return tstr_control_k;
387			buf[0] = ch;
388			buf[1] = '\0';
389			return (buf);
390		case 'b':
391			*pp = p+1;
392			return ("\b");
393		case 'e':
394			*pp = p+1;
395			buf[0] = ESC;
396			buf[1] = '\0';
397			return (buf);
398		case 'n':
399			*pp = p+1;
400			return ("\n");
401		case 'r':
402			*pp = p+1;
403			return ("\r");
404		case 't':
405			*pp = p+1;
406			return ("\t");
407		case 'k':
408			if (xlate)
409			{
410				switch (*++p)
411				{
412				case 'u': ch = SK_UP_ARROW; break;
413				case 'd': ch = SK_DOWN_ARROW; break;
414				case 'r': ch = SK_RIGHT_ARROW; break;
415				case 'l': ch = SK_LEFT_ARROW; break;
416				case 'U': ch = SK_PAGE_UP; break;
417				case 'D': ch = SK_PAGE_DOWN; break;
418				case 'h': ch = SK_HOME; break;
419				case 'e': ch = SK_END; break;
420				case 'x': ch = SK_DELETE; break;
421				default:
422					error("illegal char after \\k");
423					*pp = p+1;
424					return ("");
425				}
426				*pp = p+1;
427				buf[0] = SK_SPECIAL_KEY;
428				buf[1] = ch;
429				buf[2] = 6;
430				buf[3] = 1;
431				buf[4] = 1;
432				buf[5] = 1;
433				buf[6] = '\0';
434				return (buf);
435			}
436			/* FALLTHRU */
437		default:
438			/*
439			 * Backslash followed by any other char
440			 * just means that char.
441			 */
442			*pp = p+1;
443			buf[0] = *p;
444			buf[1] = '\0';
445			if (xlate && buf[0] == CONTROL('K'))
446				return tstr_control_k;
447			return (buf);
448		}
449	case '^':
450		/*
451		 * Carat means CONTROL.
452		 */
453		*pp = p+2;
454		buf[0] = CONTROL(p[1]);
455		buf[1] = '\0';
456		if (buf[0] == CONTROL('K'))
457			return tstr_control_k;
458		return (buf);
459	}
460	*pp = p+1;
461	buf[0] = *p;
462	buf[1] = '\0';
463	if (xlate && buf[0] == CONTROL('K'))
464		return tstr_control_k;
465	return (buf);
466}
467
468/*
469 * Skip leading spaces in a string.
470 */
471	public char *
472skipsp(s)
473	register char *s;
474{
475	while (*s == ' ' || *s == '\t')
476		s++;
477	return (s);
478}
479
480/*
481 * Skip non-space characters in a string.
482 */
483	public char *
484skipnsp(s)
485	register char *s;
486{
487	while (*s != '\0' && *s != ' ' && *s != '\t')
488		s++;
489	return (s);
490}
491
492/*
493 * Clean up an input line:
494 * strip off the trailing newline & any trailing # comment.
495 */
496	char *
497clean_line(s)
498	char *s;
499{
500	register int i;
501
502	s = skipsp(s);
503	for (i = 0;  s[i] != '\n' && s[i] != '\r' && s[i] != '\0';  i++)
504		if (s[i] == '#' && (i == 0 || s[i-1] != '\\'))
505			break;
506	s[i] = '\0';
507	return (s);
508}
509
510/*
511 * Add a byte to the output command table.
512 */
513	void
514add_cmd_char(c)
515	int c;
516{
517	if (currtable->pbuffer >= currtable->buffer + MAX_USERCMD)
518	{
519		error("too many commands");
520		exit(1);
521	}
522	*(currtable->pbuffer)++ = c;
523}
524
525/*
526 * Add a string to the output command table.
527 */
528	void
529add_cmd_str(s)
530	char *s;
531{
532	for ( ;  *s != '\0';  s++)
533		add_cmd_char(*s);
534}
535
536/*
537 * See if we have a special "control" line.
538 */
539	int
540control_line(s)
541	char *s;
542{
543#define	PREFIX(str,pat)	(strncmp(str,pat,strlen(pat)) == 0)
544
545	if (PREFIX(s, "#line-edit"))
546	{
547		currtable = &edittable;
548		return (1);
549	}
550	if (PREFIX(s, "#command"))
551	{
552		currtable = &cmdtable;
553		return (1);
554	}
555	if (PREFIX(s, "#env"))
556	{
557		currtable = &vartable;
558		return (1);
559	}
560	if (PREFIX(s, "#stop"))
561	{
562		add_cmd_char('\0');
563		add_cmd_char(A_END_LIST);
564		return (1);
565	}
566	return (0);
567}
568
569/*
570 * Output some bytes.
571 */
572	void
573fputbytes(fd, buf, len)
574	FILE *fd;
575	char *buf;
576	int len;
577{
578	while (len-- > 0)
579	{
580		fwrite(buf, sizeof(char), 1, fd);
581		buf++;
582	}
583}
584
585/*
586 * Output an integer, in special KRADIX form.
587 */
588	void
589fputint(fd, val)
590	FILE *fd;
591	unsigned int val;
592{
593	char c;
594
595	if (val >= KRADIX*KRADIX)
596	{
597		fprintf(stderr, "error: integer too big (%d > %d)\n",
598			val, KRADIX*KRADIX);
599		exit(1);
600	}
601	c = val % KRADIX;
602	fwrite(&c, sizeof(char), 1, fd);
603	c = val / KRADIX;
604	fwrite(&c, sizeof(char), 1, fd);
605}
606
607/*
608 * Find an action, given the name of the action.
609 */
610	int
611findaction(actname)
612	char *actname;
613{
614	int i;
615
616	for (i = 0;  currtable->names[i].cn_name != NULL;  i++)
617		if (strcmp(currtable->names[i].cn_name, actname) == 0)
618			return (currtable->names[i].cn_action);
619	error("unknown action");
620	return (A_INVALID);
621}
622
623	void
624error(s)
625	char *s;
626{
627	fprintf(stderr, "line %d: %s\n", linenum, s);
628	errors++;
629}
630
631
632	void
633parse_cmdline(p)
634	char *p;
635{
636	int cmdlen;
637	char *actname;
638	int action;
639	char *s;
640	char c;
641
642	/*
643	 * Parse the command string and store it in the current table.
644	 */
645	cmdlen = 0;
646	do
647	{
648		s = tstr(&p, 1);
649		cmdlen += strlen(s);
650		if (cmdlen > MAX_CMDLEN)
651			error("command too long");
652		else
653			add_cmd_str(s);
654	} while (*p != ' ' && *p != '\t' && *p != '\0');
655	/*
656	 * Terminate the command string with a null byte.
657	 */
658	add_cmd_char('\0');
659
660	/*
661	 * Skip white space between the command string
662	 * and the action name.
663	 * Terminate the action name with a null byte.
664	 */
665	p = skipsp(p);
666	if (*p == '\0')
667	{
668		error("missing action");
669		return;
670	}
671	actname = p;
672	p = skipnsp(p);
673	c = *p;
674	*p = '\0';
675
676	/*
677	 * Parse the action name and store it in the current table.
678	 */
679	action = findaction(actname);
680
681	/*
682	 * See if an extra string follows the action name.
683	 */
684	*p = c;
685	p = skipsp(p);
686	if (*p == '\0')
687	{
688		add_cmd_char(action);
689	} else
690	{
691		/*
692		 * OR the special value A_EXTRA into the action byte.
693		 * Put the extra string after the action byte.
694		 */
695		add_cmd_char(action | A_EXTRA);
696		while (*p != '\0')
697			add_cmd_str(tstr(&p, 0));
698		add_cmd_char('\0');
699	}
700}
701
702	void
703parse_varline(p)
704	char *p;
705{
706	char *s;
707
708	do
709	{
710		s = tstr(&p, 0);
711		add_cmd_str(s);
712	} while (*p != ' ' && *p != '\t' && *p != '=' && *p != '\0');
713	/*
714	 * Terminate the variable name with a null byte.
715	 */
716	add_cmd_char('\0');
717
718	p = skipsp(p);
719	if (*p++ != '=')
720	{
721		error("missing =");
722		return;
723	}
724
725	add_cmd_char(EV_OK|A_EXTRA);
726
727	p = skipsp(p);
728	while (*p != '\0')
729	{
730		s = tstr(&p, 0);
731		add_cmd_str(s);
732	}
733	add_cmd_char('\0');
734}
735
736/*
737 * Parse a line from the lesskey file.
738 */
739	void
740parse_line(line)
741	char *line;
742{
743	char *p;
744
745	/*
746	 * See if it is a control line.
747	 */
748	if (control_line(line))
749		return;
750	/*
751	 * Skip leading white space.
752	 * Replace the final newline with a null byte.
753	 * Ignore blank lines and comments.
754	 */
755	p = clean_line(line);
756	if (*p == '\0')
757		return;
758
759	if (currtable == &vartable)
760		parse_varline(p);
761	else
762		parse_cmdline(p);
763}
764
765	int
766main(argc, argv)
767	int argc;
768	char *argv[];
769{
770	FILE *desc;
771	FILE *out;
772	char line[1024];
773
774#ifdef WIN32
775	if (getenv("HOME") == NULL)
776	{
777		/*
778		 * If there is no HOME environment variable,
779		 * try the concatenation of HOMEDRIVE + HOMEPATH.
780		 */
781		char *drive = getenv("HOMEDRIVE");
782		char *path  = getenv("HOMEPATH");
783		if (drive != NULL && path != NULL)
784		{
785			char *env = (char *) calloc(strlen(drive) +
786					strlen(path) + 6, sizeof(char));
787			strcpy(env, "HOME=");
788			strcat(env, drive);
789			strcat(env, path);
790			putenv(env);
791		}
792	}
793#endif /* WIN32 */
794
795	/*
796	 * Process command line arguments.
797	 */
798	parse_args(argc, argv);
799	init_tables();
800
801	/*
802	 * Open the input file.
803	 */
804	if (strcmp(infile, "-") == 0)
805		desc = stdin;
806	else if ((desc = fopen(infile, "r")) == NULL)
807	{
808#if HAVE_PERROR
809		perror(infile);
810#else
811		fprintf(stderr, "Cannot open %s\n", infile);
812#endif
813		usage();
814	}
815
816	/*
817	 * Read and parse the input file, one line at a time.
818	 */
819	errors = 0;
820	linenum = 0;
821	while (fgets(line, sizeof(line), desc) != NULL)
822	{
823		++linenum;
824		parse_line(line);
825	}
826
827	/*
828	 * Write the output file.
829	 * If no output file was specified, use "$HOME/.less"
830	 */
831	if (errors > 0)
832	{
833		fprintf(stderr, "%d errors; no output produced\n", errors);
834		exit(1);
835	}
836
837	if (outfile == NULL)
838		outfile = getenv("LESSKEY");
839	if (outfile == NULL)
840		outfile = homefile(LESSKEYFILE);
841	if ((out = fopen(outfile, "wb")) == NULL)
842	{
843#if HAVE_PERROR
844		perror(outfile);
845#else
846		fprintf(stderr, "Cannot open %s\n", outfile);
847#endif
848		exit(1);
849	}
850
851	/* File header */
852	fputbytes(out, fileheader, sizeof(fileheader));
853
854	/* Command key section */
855	fputbytes(out, cmdsection, sizeof(cmdsection));
856	fputint(out, cmdtable.pbuffer - cmdtable.buffer);
857	fputbytes(out, (char *)cmdtable.buffer, cmdtable.pbuffer-cmdtable.buffer);
858	/* Edit key section */
859	fputbytes(out, editsection, sizeof(editsection));
860	fputint(out, edittable.pbuffer - edittable.buffer);
861	fputbytes(out, (char *)edittable.buffer, edittable.pbuffer-edittable.buffer);
862
863	/* Environment variable section */
864	fputbytes(out, varsection, sizeof(varsection));
865	fputint(out, vartable.pbuffer - vartable.buffer);
866	fputbytes(out, (char *)vartable.buffer, vartable.pbuffer-vartable.buffer);
867
868	/* File trailer */
869	fputbytes(out, endsection, sizeof(endsection));
870	fputbytes(out, filetrailer, sizeof(filetrailer));
871	return (0);
872}
873