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