xref: /openbsd/usr.bin/mg/interpreter.c (revision 274d7c50)
1 /*      $OpenBSD: interpreter.c,v 1.5 2019/07/20 11:06:33 lum Exp $	*/
2 /*
3  * This file is in the public domain.
4  *
5  * Author: Mark Lumsden <mark@showcomplex.com>
6  */
7 
8 /*
9  * This file attempts to add some 'scripting' functionality into mg.
10  *
11  * The initial goal is to give mg the ability to use it's existing functions
12  * and structures in a linked-up way. Hopefully resulting in user definable
13  * functions. The syntax is 'scheme' like but currently it is not a scheme
14  * interpreter.
15  *
16  * At the moment there is no manual page reference to this file. The code below
17  * is liable to change, so use at your own risk!
18  *
19  * If you do want to do some testing, you can add some lines to your .mg file
20  * like:
21  *
22  * 1. Give multiple arguments to a function that usually would accept only one:
23  * (find-file a.txt b.txt. c.txt)
24  *
25  * 2. Define a list:
26  * (define myfiles(list d.txt e.txt))
27  *
28  * 3. Use the previously defined list:
29  * (find-file myfiles)
30  *
31  * To do:
32  * 1. multiline parsing - currently only single lines supported.
33  * 2. parsing for '(' and ')' throughout whole string and evaluate correctly.
34  * 3. conditional execution.
35  * 4. define single value variables (define i 0)
36  * 5. deal with quotes around a string: "x x"
37  * 6. oh so many things....
38  * [...]
39  * n. implement user definable functions.
40  */
41 #include <sys/queue.h>
42 #include <regex.h>
43 #include <signal.h>
44 #include <stdio.h>
45 #include <stdlib.h>
46 #include <string.h>
47 
48 #include "def.h"
49 #include "funmap.h"
50 
51 #ifdef  MGLOG
52 #include "kbd.h"
53 #include "log.h"
54 #endif
55 
56 static int	 multiarg(char *);
57 static int	 isvar(char **, char **, int);
58 static int	 foundvar(char *);
59 static int	 foundlist(char *);
60 
61 
62 /*
63  * Structure for variables during buffer evaluation.
64  */
65 struct varentry {
66 	SLIST_ENTRY(varentry) entry;
67 	char	*name;
68 	char	*vals;
69 	int	 count;
70 };
71 SLIST_HEAD(vlisthead, varentry) varhead = SLIST_HEAD_INITIALIZER(varhead);
72 
73 /*
74  * Pass a list of arguments to a function.
75  */
76 static int
77 multiarg(char *funstr)
78 {
79 	regex_t  regex_buff;
80 	PF	 funcp;
81 	char	 excbuf[BUFSIZE], argbuf[BUFSIZE], *contbuf, tmpbuf[BUFSIZE];
82 	char	*cmdp, *argp, *fendp, *endp, *p, *t, *s = " ";
83 	int	 singlecmd = 0, spc, numparams, numspc;
84 	int	 inlist, foundlst = 0, eolst, rpar, sizof, fin;
85 
86 	contbuf = NULL;
87 	endp = strrchr(funstr, ')');
88 	if (endp == NULL) {
89 		ewprintf("No closing parenthesis found");
90 		return(FALSE);
91 	}
92 	p = endp + 1;
93 	if (*p != '\0')
94 		*p = '\0';
95 	/* we now know that string starts with '(' and ends with ')' */
96 	if (regcomp(&regex_buff, "^[(][\t ]*[)]$", REG_EXTENDED)) {
97 		regfree(&regex_buff);
98 		return (dobeep_msg("Could not compile regex"));
99 	}
100 	if (!regexec(&regex_buff, funstr, 0, NULL, 0)) {
101 		regfree(&regex_buff);
102 		return (dobeep_msg("No command found"));
103 	}
104 	/* currently there are no mg commands that don't have a letter */
105 	if (regcomp(&regex_buff, "^[(][\t ]*[A-Za-z-]+[\t ]*[)]$",
106 	    REG_EXTENDED)) {
107 		regfree(&regex_buff);
108 		return (dobeep_msg("Could not compile regex"));
109 	}
110 	if (!regexec(&regex_buff, funstr, 0, NULL, 0))
111 		singlecmd = 1;
112 
113 	regfree(&regex_buff);
114 	p = funstr + 1;		/* move past first '(' char.	*/
115 	cmdp = skipwhite(p);	/* find first char of command.	*/
116 
117 	if (singlecmd) {
118 		/* remove ')', then check for spaces at the end */
119 		cmdp[strlen(cmdp) - 1] = '\0';
120 		if ((fendp = strchr(cmdp, ' ')) != NULL)
121 			*fendp = '\0';
122 		else if ((fendp = strchr(cmdp, '\t')) != NULL)
123 			*fendp = '\0';
124 		return(excline(cmdp));
125 	}
126 	if ((fendp = strchr(cmdp, ' ')) == NULL)
127 		fendp = strchr(cmdp, '\t');
128 
129 	*fendp = '\0';
130 	/*
131 	 * If no extant mg command found, just return.
132 	 */
133 	if ((funcp = name_function(cmdp)) == NULL)
134 		return (dobeep_msgs("Unknown command: ", cmdp));
135 
136 	numparams = numparams_function(funcp);
137 	if (numparams == 0)
138 		return (dobeep_msgs("Command takes no arguments: ", cmdp));
139 
140 	/* now find the first argument */
141 	p = fendp + 1;
142 	p = skipwhite(p);
143 	if (strlcpy(argbuf, p, sizeof(argbuf)) >= sizeof(argbuf))
144 		return (dobeep_msg("strlcpy error"));
145 	argp = argbuf;
146 	numspc = spc = 1; /* initially fake a space so we find first argument */
147 	inlist = eolst = fin = rpar = 0;
148 
149 	for (p = argp; fin == 0; p++) {
150 #ifdef  MGLOG
151 		mglog_execbuf("", excbuf, argbuf, argp, eolst, inlist, cmdp,
152 		    p, contbuf);
153 #endif
154 		if (foundlst) {
155 			foundlst = 0;
156 			p--;	/* otherwise 1st arg is missed from list. */
157 		}
158 		if (*p == ')') {
159 			rpar = 1;
160 			*p = '\0';
161 		}
162 		if (*p == ' ' || *p == '\t' || *p == '\0') {
163 			if (spc == 1)
164 				continue;
165 			if (spc == 0 && (numspc % numparams == 0)) {
166 				if (*p == '\0')
167 					eolst = 1;
168 				else
169 					eolst = 0;
170 				*p = '\0'; 	/* terminate arg string */
171 				endp = p + 1;
172 				excbuf[0] = '\0';
173 				/* Is arg a var? */
174 				if (!inlist) {
175 					sizof = sizeof(tmpbuf);
176 					t = tmpbuf;
177 					if (isvar(&argp, &t, sizof)) {
178 						if ((contbuf = strndup(endp,
179 						    BUFSIZE)) == NULL)
180 							return(FALSE);
181 						*p = ' ';
182 						(void)(strlcpy(argbuf, tmpbuf,
183 						    sizof) >= sizof);
184 						p = argp = argbuf;
185 						spc = 1;
186 						foundlst = inlist = 1;
187 						continue;
188 					}
189 				}
190 				if (strlcpy(excbuf, cmdp, sizeof(excbuf))
191 				     >= sizeof(excbuf))
192 					return (dobeep_msg("strlcpy error"));
193 				if (strlcat(excbuf, s, sizeof(excbuf))
194 				    >= sizeof(excbuf))
195 					return (dobeep_msg("strlcat error"));
196 				if (strlcat(excbuf, argp, sizeof(excbuf))
197 				    >= sizeof(excbuf))
198 					return (dobeep_msg("strlcat error"));
199 
200 				excline(excbuf);
201 #ifdef  MGLOG
202 				mglog_execbuf("  ", excbuf, argbuf, argp,
203 				    eolst, inlist, cmdp, p, contbuf);
204 #endif
205 				*p = ' ';	/* so 'for' loop can continue */
206 				if (eolst) {
207 					if (contbuf != NULL) {
208 						(void)strlcpy(argbuf, contbuf,
209 						    sizeof(argbuf));
210 						free(contbuf);
211 						contbuf = NULL;
212 						p = argp = argbuf;
213 						foundlst = 1;
214 						inlist = 0;
215 						if (rpar)
216 							fin = 1;
217 						continue;
218 					}
219 					spc = 1;
220 					inlist = 0;
221 				}
222 				if (eolst && rpar)
223 					fin = 1;
224 			}
225 			numspc++;
226 			spc = 1;
227 		} else {
228 			if (spc == 1)
229 				if ((numparams == 1) ||
230 				    ((numspc + 1) % numparams) == 0)
231 					argp = p;
232 			spc = 0;
233 		}
234 	}
235 	return (TRUE);
236 }
237 
238 
239 /*
240  * Is an item a value or a variable?
241  */
242 static int
243 isvar(char **argp, char **tmpbuf, int sizof)
244 {
245 	struct varentry *v1 = NULL;
246 
247 	if (SLIST_EMPTY(&varhead))
248 		return (FALSE);
249 #ifdef  MGLOG
250 	mglog_isvar(*tmpbuf, *argp, sizof);
251 #endif
252 	SLIST_FOREACH(v1, &varhead, entry) {
253 		if (strcmp(*argp, v1->name) == 0) {
254 			(void)(strlcpy(*tmpbuf, v1->vals, sizof) >= sizof);
255 			return (TRUE);
256 		}
257 	}
258 	return (FALSE);
259 }
260 
261 
262 /*
263  * The (define string _must_ adhere to the regex in foundparen.
264  * This is not the correct way to do parsing but it does highlight
265  * the issues.
266  */
267 static int
268 foundlist(char *defstr)
269 {
270 	struct varentry *vt, *v1 = NULL;
271 	const char	 e[1] = "e", t[1] = "t";
272 	char		*p, *vnamep, *vendp = NULL, *valp, *o;
273 	int		 spc;
274 
275 
276 	p = defstr + 1;         /* move past first '(' char.    */
277 	p = skipwhite(p);    	/* find first char of 'define'. */
278 	p = strstr(p, e);	/* find first 'e' in 'define'.	*/
279 	p = strstr(++p, e);	/* find second 'e' in 'define'.	*/
280 	p++;			/* move past second 'e'.	*/
281 	vnamep = skipwhite(p);  /* find first char of var name. */
282 	vendp = vnamep;
283 
284 	/* now find the end of the list name */
285 	while (1) {
286 		++vendp;
287 		if (*vendp == '(' || *vendp == ' ' || *vendp == '\t')
288 			break;
289 	}
290 	*vendp = '\0';
291 	/*
292 	 * Check list name is not an existing function.
293 	 * Although could this be allowed? Shouldn't context dictate?
294 	 */
295 	if (name_function(vnamep) != NULL)
296 		return(dobeep_msgs("Variable/function name clash:", vnamep));
297 
298 	p = ++vendp;
299 	p = strstr(p, t);	/* find 't' in 'list'.	*/
300 	valp = skipwhite(++p);	/* find first value	*/
301 	/*
302 	 * Now we have the name of the list starting at 'vnamep',
303 	 * and the first value is at 'valp', record the details
304 	 * in a linked list. But first remove variable, if existing already.
305 	 */
306 	if (!SLIST_EMPTY(&varhead)) {
307 		SLIST_FOREACH_SAFE(v1, &varhead, entry, vt) {
308 			if (strcmp(vnamep, v1->name) == 0)
309 				SLIST_REMOVE(&varhead, v1, varentry, entry);
310 		}
311 	}
312 	if ((v1 = malloc(sizeof(struct varentry))) == NULL)
313 		return (ABORT);
314 	SLIST_INSERT_HEAD(&varhead, v1, entry);
315 	if ((v1->name = strndup(vnamep, BUFSIZE)) == NULL)
316 		return(dobeep_msg("strndup error"));
317 	v1->count = 0;
318 	vendp = NULL;
319 
320 	/* initially fake a space so we find first value */
321 	spc = 1;
322 	/* now loop through values in list value string while counting them */
323 	for (p = valp; *p != '\0'; p++) {
324 		if (*p == ' ' || *p == '\t') {
325 			if (spc == 0)
326 				vendp = p;
327 			spc = 1;
328 		} else if (*p == ')') {
329 			o = p - 1;
330 			if (*o != ' ' && *o != '\t')
331 				vendp = p;
332 			break;
333 		} else {
334 			if (spc == 1)
335 				v1->count++;
336 			spc = 0;
337 		}
338 	}
339 	*vendp = '\0';
340 	if ((v1->vals = strndup(valp, BUFSIZE)) == NULL)
341 		return(dobeep_msg("strndup error"));
342 
343 	return (TRUE);
344 }
345 
346 
347 /*
348  * to do
349  */
350 static int
351 foundvar(char *funstr)
352 {
353 	ewprintf("to do");
354 	return (TRUE);
355 }
356 
357 /*
358  * Finished with evaluation, so clean up any vars.
359  */
360 int
361 clearvars(void)
362 {
363 	struct varentry	*v1 = NULL;
364 
365 	while (!SLIST_EMPTY(&varhead)) {
366 		v1 = SLIST_FIRST(&varhead);
367 		SLIST_REMOVE_HEAD(&varhead, entry);
368 		free(v1->vals);
369 		free(v1->name);
370 		free(v1);
371 	}
372 	return (FALSE);
373 }
374 
375 /*
376  * Line has a '(' as the first non-white char.
377  * Do some very basic parsing of line with '(' as the first character.
378  * Multi-line not supported at the moment, To do.
379  */
380 int
381 foundparen(char *funstr)
382 {
383 	regex_t  regex_buff;
384 	char	*regs;
385 
386 	/* Does the line have a list 'define' like: */
387 	/* (define alist(list 1 2 3 4)) */
388 	regs = "^[(][\t ]*define[\t ]+[^\t (]+[\t ]*[(][\t ]*list[\t ]+"\
389 		"[^\t ]+.*[)][\t ]*[)]";
390 	if (regcomp(&regex_buff, regs, REG_EXTENDED)) {
391 		regfree(&regex_buff);
392 		return(dobeep_msg("Could not compile regex"));
393 	}
394 	if (!regexec(&regex_buff, funstr, 0, NULL, 0)) {
395 		regfree(&regex_buff);
396 		return(foundlist(funstr));
397 	}
398 	/* Does the line have a single variable 'define' like: */
399 	/* (define i 0) */
400 	regs = "^[(][\t ]*define[\t ]+[^\t (]+[\t ]*[^\t (]+[\t ]*[)]";
401 	if (regcomp(&regex_buff, regs, REG_EXTENDED)) {
402 		regfree(&regex_buff);
403 		return(dobeep_msg("Could not compile regex"));
404 	}
405 	if (!regexec(&regex_buff, funstr, 0, NULL, 0)) {
406 		regfree(&regex_buff);
407 		return(foundvar(funstr));
408 	}
409 	/* Does the line have an unrecognised 'define' */
410 	regs = "^[(][\t ]*define[\t ]+";
411 	if (regcomp(&regex_buff, regs, REG_EXTENDED)) {
412 		regfree(&regex_buff);
413 		return(dobeep_msg("Could not compile regex"));
414 	}
415 	if (!regexec(&regex_buff, funstr, 0, NULL, 0)) {
416 		regfree(&regex_buff);
417 		return(dobeep_msg("Invalid use of define"));
418 	}
419 	regfree(&regex_buff);
420 	return(multiarg(funstr));
421 }
422