xref: /titanic_50/usr/src/lib/libshell/common/edit/hexpand.c (revision 4bb7efa72ed531c10f097919636e67724ec4c25a)
1 /***********************************************************************
2 *                                                                      *
3 *               This software is part of the ast package               *
4 *           Copyright (c) 1982-2007 AT&T Knowledge Ventures            *
5 *                      and is licensed under the                       *
6 *                  Common Public License, Version 1.0                  *
7 *                      by AT&T Knowledge Ventures                      *
8 *                                                                      *
9 *                A copy of the License is available at                 *
10 *            http://www.opensource.org/licenses/cpl1.0.txt             *
11 *         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         *
12 *                                                                      *
13 *              Information and Software Systems Research               *
14 *                            AT&T Research                             *
15 *                           Florham Park NJ                            *
16 *                                                                      *
17 *                  David Korn <dgk@research.att.com>                   *
18 *                                                                      *
19 ***********************************************************************/
20 #pragma prototyped
21 /*
22  * bash style history expansion
23  *
24  * Author:
25  * Karsten Fleischer
26  * Omnium Software Engineering
27  * An der Luisenburg 7
28  * D-51379 Leverkusen
29  * Germany
30  *
31  * <K.Fleischer@omnium.de>
32  */
33 
34 
35 #include "defs.h"
36 #include "edit.h"
37 
38 #if ! SHOPT_HISTEXPAND
39 
40 NoN(hexpand)
41 
42 #else
43 
44 #include <ctype.h>
45 
46 static char *modifiers = "htrepqxs&";
47 static int mod_flags[] = { 0, 0, 0, 0, HIST_PRINT, HIST_QUOTE, HIST_QUOTE|HIST_QUOTE_BR, 0, 0 };
48 
49 #define	DONE()	{flag |= HIST_ERROR; cp = 0; stakseek(0); goto done;}
50 
51 struct subst
52 {
53 	char *str[2];	/* [0] is "old", [1] is "new" string */
54 };
55 
56 
57 /*
58  * parse an /old/new/ string, delimiter expected as first char.
59  * if "old" not specified, keep sb->str[0]
60  * if "new" not specified, set sb->str[1] to empty string
61  * read up to third delimeter char, \n or \0, whichever comes first.
62  * return adress is one past the last valid char in s:
63  * - the address containing \n or \0 or
64  * - one char beyond the third delimiter
65  */
66 
67 static char *parse_subst(const char *s, struct subst *sb)
68 {
69 	char	*cp,del;
70 	int	off,n = 0;
71 
72 	/* build the strings on the stack, mainly for '&' substition in "new" */
73 	off = staktell();
74 
75 	/* init "new" with empty string */
76 	if(sb->str[1])
77 		free(sb->str[1]);
78 	sb->str[1] = strdup("");
79 
80 	/* get delimiter */
81 	del = *s;
82 
83 	cp = (char*) s + 1;
84 
85 	while(n < 2)
86 	{
87 		if(*cp == del || *cp == '\n' || *cp == '\0')
88 		{
89 			/* delimiter or EOL */
90 			if(staktell() != off)
91 			{
92 				/* dupe string on stack and rewind stack */
93 				stakputc('\0');
94 				if(sb->str[n])
95 					free(sb->str[n]);
96 				sb->str[n] = strdup(stakptr(off));
97 				stakseek(off);
98 			}
99 			n++;
100 
101 			/* if not delimiter, we've reached EOL. Get outta here. */
102 			if(*cp != del)
103 				break;
104 		}
105 		else if(*cp == '\\')
106 		{
107 			if(*(cp+1) == del)	/* quote delimiter */
108 			{
109 				stakputc(del);
110 				cp++;
111 			}
112 			else if(*(cp+1) == '&' && n == 1)
113 			{		/* quote '&' only in "new" */
114 				stakputc('&');
115 				cp++;
116 			}
117 			else
118 				stakputc('\\');
119 		}
120 		else if(*cp == '&' && n == 1 && sb->str[0])
121 			/* substitute '&' with "old" in "new" */
122 			stakputs(sb->str[0]);
123 		else
124 			stakputc(*cp);
125 		cp++;
126 	}
127 
128 	/* rewind stack */
129 	stakseek(off);
130 
131 	return cp;
132 }
133 
134 /*
135  * history expansion main routine
136  */
137 
138 int hist_expand(const char *ln, char **xp)
139 {
140 	int	off,	/* stack offset */
141 		q,	/* quotation flags */
142 		p,	/* flag */
143 		c,	/* current char */
144 		flag=0;	/* HIST_* flags */
145 	Sfoff_t	n,	/* history line number, counter, etc. */
146 		i,	/* counter */
147 		w[2];	/* word range */
148 	char	*sp,	/* stack pointer */
149 		*cp,	/* current char in ln */
150 		*str,	/* search string */
151 		*evp,	/* event/word designator string, for error msgs */
152 		*cc=0,	/* copy of current line up to cp; temp ptr */
153 		hc[3],	/* default histchars */
154 		*qc="\'\"`";	/* quote characters */
155 	Sfio_t	*ref=0,	/* line referenced by event designator */
156 		*tmp=0,	/* temporary line buffer */
157 		*tmp2=0;/* temporary line buffer */
158 	Histloc_t hl;	/* history location */
159 	static Namval_t *np = 0;	/* histchars variable */
160 	static struct subst	sb = {0,0};	/* substition strings */
161 	static Sfio_t	*wm=0;	/* word match from !?string? event designator */
162 
163 	if(!wm)
164 		wm = sfopen(NULL, NULL, "swr");
165 
166 	hc[0] = '!';
167 	hc[1] = '^';
168 	hc[2] = 0;
169 	if((np = nv_open("histchars",sh.var_tree,0)) && (cp = nv_getval(np)))
170 	{
171 		if(cp[0])
172 		{
173 			hc[0] = cp[0];
174 			if(cp[1])
175 			{
176 				hc[1] = cp[1];
177 				if(cp[2])
178 					hc[2] = cp[2];
179 			}
180 		}
181 	}
182 
183 	/* save shell stack */
184 	if(off = staktell())
185 		sp = stakfreeze(0);
186 
187 	cp = (char*)ln;
188 
189 	while(cp && *cp)
190 	{
191 		/* read until event/quick substitution/comment designator */
192 		if((*cp != hc[0] && *cp != hc[1] && *cp != hc[2])
193 		   || (*cp == hc[1] && cp != ln))
194 		{
195 			if(*cp == '\\')	/* skip escaped designators */
196 				stakputc(*cp++);
197 			else if(*cp == '\'') /* skip quoted designators */
198 			{
199 				do
200 					stakputc(*cp);
201 				while(*++cp && *cp != '\'');
202 			}
203 			stakputc(*cp++);
204 			continue;
205 		}
206 
207 		if(hc[2] && *cp == hc[2]) /* history comment designator, skip rest of line */
208 		{
209 			stakputc(*cp++);
210 			stakputs(cp);
211 			DONE();
212 		}
213 
214 		n = -1;
215 		str = 0;
216 		flag &= HIST_EVENT; /* save event flag for returning later */
217 		evp = cp;
218 		ref = 0;
219 
220 		if(*cp == hc[1]) /* shortcut substitution */
221 		{
222 			flag |= HIST_QUICKSUBST;
223 			goto getline;
224 		}
225 
226 		if(*cp == hc[0] && *(cp+1) == hc[0]) /* refer to line -1 */
227 		{
228 			cp += 2;
229 			goto getline;
230 		}
231 
232 		switch(c = *++cp) {
233 		case ' ':
234 		case '\t':
235 		case '\n':
236 		case '\0':
237 		case '=':
238 		case '(':
239 			stakputc(hc[0]);
240 			continue;
241 		case '#': /* the line up to current position */
242 			flag |= HIST_HASH;
243 			cp++;
244 			n = staktell(); /* terminate string and dup */
245 			stakputc('\0');
246 			cc = strdup(stakptr(0));
247 			stakseek(n); /* remove null byte again */
248 			ref = sfopen(ref, cc, "s"); /* open as file */
249 			n = 0; /* skip history file referencing */
250 			break;
251 		case '-': /* back reference by number */
252 			if(!isdigit(*(cp+1)))
253 				goto string_event;
254 			cp++;
255 		case '0': /* reference by number */
256 		case '1':
257 		case '2':
258 		case '3':
259 		case '4':
260 		case '5':
261 		case '6':
262 		case '7':
263 		case '8':
264 		case '9':
265 			n = 0;
266 			while(isdigit(*cp))
267 				n = n * 10 + (*cp++) - '0';
268 			if(c == '-')
269 				n = -n;
270 			break;
271 		case '$':
272 			n = -1;
273 		case ':':
274 			break;
275 		case '?':
276 			cp++;
277 			flag |= HIST_QUESTION;
278 		string_event:
279 		default:
280 			/* read until end of string or word designator/modifier */
281 			str = cp;
282 			while(*cp)
283 			{
284 				cp++;
285 				if((!(flag&HIST_QUESTION) &&
286 				   (*cp == ':' || isspace(*cp)
287 				    || *cp == '^' || *cp == '$'
288 				    || *cp == '*' || *cp == '-'
289 				    || *cp == '%')
290 				   )
291 				   || ((flag&HIST_QUESTION) && (*cp == '?' || *cp == '\n')))
292 				{
293 					c = *cp;
294 					*cp = '\0';
295 				}
296 			}
297 			break;
298 		}
299 
300 getline:
301 		flag |= HIST_EVENT;
302 		if(str)	/* !string or !?string? event designator */
303 		{
304 
305 			/* search history for string */
306 			hl = hist_find(sh.hist_ptr, str,
307 				       sh.hist_ptr->histind,
308 				       flag&HIST_QUESTION, -1);
309 			if((n = hl.hist_command) == -1)
310 				n = 0;	/* not found */
311 		}
312 		if(n)
313 		{
314 			if(n < 0) /* determine index for backref */
315 				n = sh.hist_ptr->histind + n;
316 			/* search and use history file if found */
317 			if(n > 0 && hist_seek(sh.hist_ptr, n) != -1)
318 				ref = sh.hist_ptr->histfp;
319 
320 		}
321 		if(!ref)
322 		{
323 			/* string not found or command # out of range */
324 			c = *cp;
325 			*cp = '\0';
326 			errormsg(SH_DICT, ERROR_ERROR, "%s: event not found", evp);
327 			*cp = c;
328 			DONE();
329 		}
330 
331 		if(str) /* string search: restore orig. line */
332 		{
333 			if(flag&HIST_QUESTION)
334 				*cp++ = c; /* skip second question mark */
335 			else
336 				*cp = c;
337 		}
338 
339 		/* colon introduces either word designators or modifiers */
340 		if(*(evp = cp) == ':')
341 			cp++;
342 
343 		w[0] = 0; /* -1 means last word, -2 means match from !?string? */
344 		w[1] = -1; /* -1 means last word, -2 means suppress last word */
345 
346 		if(flag & HIST_QUICKSUBST) /* shortcut substitution */
347 			goto getsel;
348 
349 		n = 0;
350 		while(n < 2)
351 		{
352 			switch(c = *cp++) {
353 			case '^': /* first word */
354 				if(n == 0)
355 				{
356 					w[0] = w[1] = 1;
357 					goto skip;
358 				}
359 				else
360 					goto skip2;
361 			case '$': /* last word */
362 				w[n] = -1;
363 				goto skip;
364 			case '%': /* match from !?string? event designator */
365 				if(n == 0)
366 				{
367 					if(!str)
368 					{
369 						w[0] = 0;
370 						w[1] = -1;
371 						ref = wm;
372 					}
373 					else
374 					{
375 						w[0] = -2;
376 						w[1] = sftell(ref) + hl.hist_char;
377 					}
378 					sfseek(wm, 0, SEEK_SET);
379 					goto skip;
380 				}
381 			default:
382 			skip2:
383 				cp--;
384 				n = 2;
385 				break;
386 			case '*': /* until last word */
387 				if(n == 0)
388 					w[0] = 1;
389 				w[1] = -1;
390 			skip:
391 				flag |= HIST_WORDDSGN;
392 				n = 2;
393 				break;
394 			case '-': /* until last word or specified index */
395 				w[1] = -2;
396 				flag |= HIST_WORDDSGN;
397 				n = 1;
398 				break;
399 			case '0':
400 			case '1':
401 			case '2':
402 			case '3':
403 			case '4':
404 			case '5':
405 			case '6':
406 			case '7':
407 			case '8':
408 			case '9': /* specify index */
409 				if((*evp == ':') || w[1] == -2)
410 				{
411 					w[n] = c - '0';
412 					while(isdigit(c=*cp++))
413 						w[n] = w[n] * 10 + c - '0';
414 					flag |= HIST_WORDDSGN;
415 					if(n == 0)
416 						w[1] = w[0];
417 					n++;
418 				}
419 				else
420 					n = 2;
421 				cp--;
422 				break;
423 			}
424 		}
425 
426 		if(w[0] != -2 && w[1] > 0 && w[0] > w[1])
427 		{
428 			c = *cp;
429 			*cp = '\0';
430 			errormsg(SH_DICT, ERROR_ERROR, "%s: bad word specifier", evp);
431 			*cp = c;
432 			DONE();
433 		}
434 
435 		/* no valid word designator after colon, rewind */
436 		if(!(flag & HIST_WORDDSGN) && (*evp == ':'))
437 			cp = evp;
438 
439 getsel:
440 		/* open temp buffer, let sfio do the (re)allocation */
441 		tmp = sfopen(NULL, NULL, "swr");
442 
443 		/* push selected words into buffer, squash
444 		   whitespace into single blank or a newline */
445 		n = i = q = 0;
446 
447 		while((c = sfgetc(ref)) > 0)
448 		{
449 			if(isspace(c))
450 			{
451 				flag |= (c == '\n' ? HIST_NEWLINE : 0);
452 				continue;
453 			}
454 
455 			if(n >= w[0] && ((w[0] != -2) ? (w[1] < 0 || n <= w[1]) : 1))
456 			{
457 				if(w[0] < 0)
458 					sfseek(tmp, 0, SEEK_SET);
459 				else
460 					i = sftell(tmp);
461 
462 				if(i > 0)
463 					sfputc(tmp, flag & HIST_NEWLINE ? '\n' : ' ');
464 
465 				flag &= ~HIST_NEWLINE;
466 				p = 1;
467 			}
468 			else
469 				p = 0;
470 
471 			do
472 			{
473 				cc = strchr(qc, c);
474 				q ^= cc ? 1<<(int)(cc - qc) : 0;
475 				if(p)
476 					sfputc(tmp, c);
477 			}
478 			while((c = sfgetc(ref)) > 0  && (!isspace(c) || q));
479 
480 			if(w[0] == -2 && sftell(ref) > w[1])
481 				break;
482 
483 			flag |= (c == '\n' ? HIST_NEWLINE : 0);
484 			n++;
485 		}
486 		if(w[0] != -2 && w[1] >= 0 && w[1] >= n)
487 		{
488 			c = *cp;
489 			*cp = '\0';
490 			errormsg(SH_DICT, ERROR_ERROR, "%s: bad word specifier", evp);
491 			*cp = c;
492 			DONE();
493 		}
494 		else if(w[1] == -2)	/* skip last word */
495 			sfseek(tmp, i, SEEK_SET);
496 
497 		/* remove trailing newline */
498 		if(sftell(tmp))
499 		{
500 			sfseek(tmp, -1, SEEK_CUR);
501 			if(sfgetc(tmp) == '\n')
502 				sfungetc(tmp, '\n');
503 		}
504 
505 		sfputc(tmp, '\0');
506 
507 		if(str)
508 		{
509 			if(wm)
510 				sfclose(wm);
511 			wm = tmp;
512 		}
513 
514 		if(cc && (flag&HIST_HASH))
515 		{
516 			/* close !# temp file */
517 			sfclose(ref);
518 			flag &= ~HIST_HASH;
519 			free(cc);
520 			cc = 0;
521 		}
522 
523 		evp = cp;
524 
525 		/* selected line/words are now in buffer, now go for the modifiers */
526 		while(*cp == ':' || (flag & HIST_QUICKSUBST))
527 		{
528 			if(flag & HIST_QUICKSUBST)
529 			{
530 				flag &= ~HIST_QUICKSUBST;
531 				c = 's';
532 				cp--;
533 			}
534 			else
535 				c = *++cp;
536 
537 			sfseek(tmp, 0, SEEK_SET);
538 			tmp2 = sfopen(tmp2, NULL, "swr");
539 
540 			if(c == 'g') /* global substitution */
541 			{
542 				flag |= HIST_GLOBALSUBST;
543 				c = *++cp;
544 			}
545 
546 			if(cc = strchr(modifiers, c))
547 				flag |= mod_flags[cc - modifiers];
548 			else
549 			{
550 				errormsg(SH_DICT, ERROR_ERROR, "%c: unrecognized history modifier", c);
551 				DONE();
552 			}
553 
554 			if(c == 'h' || c == 'r') /* head or base */
555 			{
556 				n = -1;
557 				while((c = sfgetc(tmp)) > 0)
558 				{	/* remember position of / or . */
559 					if((c == '/' && *cp == 'h') || (c == '.' && *cp == 'r'))
560 						n = sftell(tmp2);
561 					sfputc(tmp2, c);
562 				}
563 				if(n > 0)
564 				{	 /* rewind to last / or . */
565 					sfseek(tmp2, n, SEEK_SET);
566 					/* end string there */
567 					sfputc(tmp2, '\0');
568 				}
569 			}
570 			else if(c == 't' || c == 'e') /* tail or suffix */
571 			{
572 				n = 0;
573 				while((c = sfgetc(tmp)) > 0)
574 				{	/* remember position of / or . */
575 					if((c == '/' && *cp == 't') || (c == '.' && *cp == 'e'))
576 						n = sftell(tmp);
577 				}
578 				/* rewind to last / or . */
579 				sfseek(tmp, n, SEEK_SET);
580 				/* copy from there on */
581 				while((c = sfgetc(tmp)) > 0)
582 					sfputc(tmp2, c);
583 			}
584 			else if(c == 's' || c == '&')
585 			{
586 				cp++;
587 
588 				if(c == 's')
589 				{
590 					/* preset old with match from !?string? */
591 					if(!sb.str[0] && wm)
592 						sb.str[0] = strdup(sfsetbuf(wm, (Void_t*)1, 0));
593 					cp = parse_subst(cp, &sb);
594 				}
595 
596 				if(!sb.str[0] || !sb.str[1])
597 				{
598 					c = *cp;
599 					*cp = '\0';
600 					errormsg(SH_DICT, ERROR_ERROR,
601 						 "%s%s: no previous substitution",
602 						(flag & HIST_QUICKSUBST) ? ":s" : "",
603 						evp);
604 					*cp = c;
605 					DONE();
606 				}
607 
608 				/* need pointer for strstr() */
609 				str = sfsetbuf(tmp, (Void_t*)1, 0);
610 
611 				flag |= HIST_SUBSTITUTE;
612 				while(flag & HIST_SUBSTITUTE)
613 				{
614 					/* find string */
615 					if(cc = strstr(str, sb.str[0]))
616 					{	/* replace it */
617 						c = *cc;
618 						*cc = '\0';
619 						sfputr(tmp2, str, -1);
620 						sfputr(tmp2, sb.str[1], -1);
621 						*cc = c;
622 						str = cc + strlen(sb.str[0]);
623 					}
624 					else if(!sftell(tmp2))
625 					{	/* not successfull */
626 						c = *cp;
627 						*cp = '\0';
628 						errormsg(SH_DICT, ERROR_ERROR,
629 							 "%s%s: substitution failed",
630 							(flag & HIST_QUICKSUBST) ? ":s" : "",
631 							evp);
632 						*cp = c;
633 						DONE();
634 					}
635 					/* loop if g modifier specified */
636 					if(!cc || !(flag & HIST_GLOBALSUBST))
637 						flag &= ~HIST_SUBSTITUTE;
638 				}
639 				/* output rest of line */
640 				sfputr(tmp2, str, -1);
641 				if(*cp)
642 					cp--;
643 			}
644 
645 			if(sftell(tmp2))
646 			{ /* if any substitions done, swap buffers */
647 				if(wm != tmp)
648 					sfclose(tmp);
649 				tmp = tmp2;
650 				tmp2 = 0;
651 			}
652 			cc = 0;
653 			if(*cp)
654 				cp++;
655 		}
656 
657 		/* flush temporary buffer to stack */
658 		if(tmp)
659 		{
660 			sfseek(tmp, 0, SEEK_SET);
661 
662 			if(flag & HIST_QUOTE)
663 				stakputc('\'');
664 
665 			while((c = sfgetc(tmp)) > 0)
666 			{
667 				if(isspace(c))
668 				{
669 					flag = flag & ~HIST_NEWLINE;
670 
671 					/* squash white space to either a
672 					   blank or a newline */
673 					do
674 						flag |= (c == '\n' ? HIST_NEWLINE : 0);
675 					while((c = sfgetc(tmp)) > 0 && isspace(c));
676 
677 					sfungetc(tmp, c);
678 
679 					c = (flag & HIST_NEWLINE) ? '\n' : ' ';
680 
681 					if(flag & HIST_QUOTE_BR)
682 					{
683 						stakputc('\'');
684 						stakputc(c);
685 						stakputc('\'');
686 					}
687 					else
688 						stakputc(c);
689 				}
690 				else if((c == '\'') && (flag & HIST_QUOTE))
691 				{
692 					stakputc('\'');
693 					stakputc('\\');
694 					stakputc(c);
695 					stakputc('\'');
696 				}
697 				else
698 					stakputc(c);
699 			}
700 			if(flag & HIST_QUOTE)
701 				stakputc('\'');
702 		}
703 	}
704 
705 	stakputc('\0');
706 
707 done:
708 	if(cc && (flag&HIST_HASH))
709 	{
710 		/* close !# temp file */
711 		sfclose(ref);
712 		free(cc);
713 		cc = 0;
714 	}
715 
716 	/* error? */
717 	if(staktell() && !(flag & HIST_ERROR))
718 		*xp = strdup(stakfreeze(1));
719 
720 	/* restore shell stack */
721 	if(off)
722 		stakset(sp,off);
723 	else
724 		stakseek(0);
725 
726 	/* drop temporary files */
727 
728 	if(tmp && tmp != wm)
729 		sfclose(tmp);
730 	if(tmp2)
731 		sfclose(tmp2);
732 
733 	return (flag & HIST_ERROR ? HIST_ERROR : flag & HIST_FLAG_RETURN_MASK);
734 }
735 
736 #endif
737