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*                  Common Public License, Version 1.0                  *
7*                    by AT&T Intellectual Property                     *
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
40NoN(hexpand)
41
42#else
43
44static char *modifiers = "htrepqxs&";
45static 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
49struct 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
65static 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
136int 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		case '0': /* reference by number */
254		case '1':
255		case '2':
256		case '3':
257		case '4':
258		case '5':
259		case '6':
260		case '7':
261		case '8':
262		case '9':
263			n = 0;
264			while(isdigit(*cp))
265				n = n * 10 + (*cp++) - '0';
266			if(c == '-')
267				n = -n;
268			break;
269		case '$':
270			n = -1;
271		case ':':
272			break;
273		case '?':
274			cp++;
275			flag |= HIST_QUESTION;
276		string_event:
277		default:
278			/* read until end of string or word designator/modifier */
279			str = cp;
280			while(*cp)
281			{
282				cp++;
283				if((!(flag&HIST_QUESTION) &&
284				   (*cp == ':' || isspace(*cp)
285				    || *cp == '^' || *cp == '$'
286				    || *cp == '*' || *cp == '-'
287				    || *cp == '%')
288				   )
289				   || ((flag&HIST_QUESTION) && (*cp == '?' || *cp == '\n')))
290				{
291					c = *cp;
292					*cp = '\0';
293				}
294			}
295			break;
296		}
297
298getline:
299		flag |= HIST_EVENT;
300		if(str)	/* !string or !?string? event designator */
301		{
302
303			/* search history for string */
304			hl = hist_find(shgd->hist_ptr, str,
305				       shgd->hist_ptr->histind,
306				       flag&HIST_QUESTION, -1);
307			if((n = hl.hist_command) == -1)
308				n = 0;	/* not found */
309		}
310		if(n)
311		{
312			if(n < 0) /* determine index for backref */
313				n = shgd->hist_ptr->histind + n;
314			/* search and use history file if found */
315			if(n > 0 && hist_seek(shgd->hist_ptr, n) != -1)
316				ref = shgd->hist_ptr->histfp;
317
318		}
319		if(!ref)
320		{
321			/* string not found or command # out of range */
322			c = *cp;
323			*cp = '\0';
324			errormsg(SH_DICT, ERROR_ERROR, "%s: event not found", evp);
325			*cp = c;
326			DONE();
327		}
328
329		if(str) /* string search: restore orig. line */
330		{
331			if(flag&HIST_QUESTION)
332				*cp++ = c; /* skip second question mark */
333			else
334				*cp = c;
335		}
336
337		/* colon introduces either word designators or modifiers */
338		if(*(evp = cp) == ':')
339			cp++;
340
341		w[0] = 0; /* -1 means last word, -2 means match from !?string? */
342		w[1] = -1; /* -1 means last word, -2 means suppress last word */
343
344		if(flag & HIST_QUICKSUBST) /* shortcut substitution */
345			goto getsel;
346
347		n = 0;
348		while(n < 2)
349		{
350			switch(c = *cp++) {
351			case '^': /* first word */
352				if(n == 0)
353				{
354					w[0] = w[1] = 1;
355					goto skip;
356				}
357				else
358					goto skip2;
359			case '$': /* last word */
360				w[n] = -1;
361				goto skip;
362			case '%': /* match from !?string? event designator */
363				if(n == 0)
364				{
365					if(!str)
366					{
367						w[0] = 0;
368						w[1] = -1;
369						ref = wm;
370					}
371					else
372					{
373						w[0] = -2;
374						w[1] = sftell(ref) + hl.hist_char;
375					}
376					sfseek(wm, 0, SEEK_SET);
377					goto skip;
378				}
379			default:
380			skip2:
381				cp--;
382				n = 2;
383				break;
384			case '*': /* until last word */
385				if(n == 0)
386					w[0] = 1;
387				w[1] = -1;
388			skip:
389				flag |= HIST_WORDDSGN;
390				n = 2;
391				break;
392			case '-': /* until last word or specified index */
393				w[1] = -2;
394				flag |= HIST_WORDDSGN;
395				n = 1;
396				break;
397			case '0':
398			case '1':
399			case '2':
400			case '3':
401			case '4':
402			case '5':
403			case '6':
404			case '7':
405			case '8':
406			case '9': /* specify index */
407				if((*evp == ':') || w[1] == -2)
408				{
409					w[n] = c - '0';
410					while(isdigit(c=*cp++))
411						w[n] = w[n] * 10 + c - '0';
412					flag |= HIST_WORDDSGN;
413					if(n == 0)
414						w[1] = w[0];
415					n++;
416				}
417				else
418					n = 2;
419				cp--;
420				break;
421			}
422		}
423
424		if(w[0] != -2 && w[1] > 0 && w[0] > w[1])
425		{
426			c = *cp;
427			*cp = '\0';
428			errormsg(SH_DICT, ERROR_ERROR, "%s: bad word specifier", evp);
429			*cp = c;
430			DONE();
431		}
432
433		/* no valid word designator after colon, rewind */
434		if(!(flag & HIST_WORDDSGN) && (*evp == ':'))
435			cp = evp;
436
437getsel:
438		/* open temp buffer, let sfio do the (re)allocation */
439		tmp = sfopen(NULL, NULL, "swr");
440
441		/* push selected words into buffer, squash
442		   whitespace into single blank or a newline */
443		n = i = q = 0;
444
445		while((c = sfgetc(ref)) > 0)
446		{
447			if(isspace(c))
448			{
449				flag |= (c == '\n' ? HIST_NEWLINE : 0);
450				continue;
451			}
452
453			if(n >= w[0] && ((w[0] != -2) ? (w[1] < 0 || n <= w[1]) : 1))
454			{
455				if(w[0] < 0)
456					sfseek(tmp, 0, SEEK_SET);
457				else
458					i = sftell(tmp);
459
460				if(i > 0)
461					sfputc(tmp, flag & HIST_NEWLINE ? '\n' : ' ');
462
463				flag &= ~HIST_NEWLINE;
464				p = 1;
465			}
466			else
467				p = 0;
468
469			do
470			{
471				cc = strchr(qc, c);
472				q ^= cc ? 1<<(int)(cc - qc) : 0;
473				if(p)
474					sfputc(tmp, c);
475			}
476			while((c = sfgetc(ref)) > 0  && (!isspace(c) || q));
477
478			if(w[0] == -2 && sftell(ref) > w[1])
479				break;
480
481			flag |= (c == '\n' ? HIST_NEWLINE : 0);
482			n++;
483		}
484		if(w[0] != -2 && w[1] >= 0 && w[1] >= n)
485		{
486			c = *cp;
487			*cp = '\0';
488			errormsg(SH_DICT, ERROR_ERROR, "%s: bad word specifier", evp);
489			*cp = c;
490			DONE();
491		}
492		else if(w[1] == -2)	/* skip last word */
493			sfseek(tmp, i, SEEK_SET);
494
495		/* remove trailing newline */
496		if(sftell(tmp))
497		{
498			sfseek(tmp, -1, SEEK_CUR);
499			if(sfgetc(tmp) == '\n')
500				sfungetc(tmp, '\n');
501		}
502
503		sfputc(tmp, '\0');
504
505		if(str)
506		{
507			if(wm)
508				sfclose(wm);
509			wm = tmp;
510		}
511
512		if(cc && (flag&HIST_HASH))
513		{
514			/* close !# temp file */
515			sfclose(ref);
516			flag &= ~HIST_HASH;
517			free(cc);
518			cc = 0;
519		}
520
521		evp = cp;
522
523		/* selected line/words are now in buffer, now go for the modifiers */
524		while(*cp == ':' || (flag & HIST_QUICKSUBST))
525		{
526			if(flag & HIST_QUICKSUBST)
527			{
528				flag &= ~HIST_QUICKSUBST;
529				c = 's';
530				cp--;
531			}
532			else
533				c = *++cp;
534
535			sfseek(tmp, 0, SEEK_SET);
536			tmp2 = sfopen(tmp2, NULL, "swr");
537
538			if(c == 'g') /* global substitution */
539			{
540				flag |= HIST_GLOBALSUBST;
541				c = *++cp;
542			}
543
544			if(cc = strchr(modifiers, c))
545				flag |= mod_flags[cc - modifiers];
546			else
547			{
548				errormsg(SH_DICT, ERROR_ERROR, "%c: unrecognized history modifier", c);
549				DONE();
550			}
551
552			if(c == 'h' || c == 'r') /* head or base */
553			{
554				n = -1;
555				while((c = sfgetc(tmp)) > 0)
556				{	/* remember position of / or . */
557					if((c == '/' && *cp == 'h') || (c == '.' && *cp == 'r'))
558						n = sftell(tmp2);
559					sfputc(tmp2, c);
560				}
561				if(n > 0)
562				{	 /* rewind to last / or . */
563					sfseek(tmp2, n, SEEK_SET);
564					/* end string there */
565					sfputc(tmp2, '\0');
566				}
567			}
568			else if(c == 't' || c == 'e') /* tail or suffix */
569			{
570				n = 0;
571				while((c = sfgetc(tmp)) > 0)
572				{	/* remember position of / or . */
573					if((c == '/' && *cp == 't') || (c == '.' && *cp == 'e'))
574						n = sftell(tmp);
575				}
576				/* rewind to last / or . */
577				sfseek(tmp, n, SEEK_SET);
578				/* copy from there on */
579				while((c = sfgetc(tmp)) > 0)
580					sfputc(tmp2, c);
581			}
582			else if(c == 's' || c == '&')
583			{
584				cp++;
585
586				if(c == 's')
587				{
588					/* preset old with match from !?string? */
589					if(!sb.str[0] && wm)
590						sb.str[0] = strdup(sfsetbuf(wm, (Void_t*)1, 0));
591					cp = parse_subst(cp, &sb);
592				}
593
594				if(!sb.str[0] || !sb.str[1])
595				{
596					c = *cp;
597					*cp = '\0';
598					errormsg(SH_DICT, ERROR_ERROR,
599						 "%s%s: no previous substitution",
600						(flag & HIST_QUICKSUBST) ? ":s" : "",
601						evp);
602					*cp = c;
603					DONE();
604				}
605
606				/* need pointer for strstr() */
607				str = sfsetbuf(tmp, (Void_t*)1, 0);
608
609				flag |= HIST_SUBSTITUTE;
610				while(flag & HIST_SUBSTITUTE)
611				{
612					/* find string */
613					if(cc = strstr(str, sb.str[0]))
614					{	/* replace it */
615						c = *cc;
616						*cc = '\0';
617						sfputr(tmp2, str, -1);
618						sfputr(tmp2, sb.str[1], -1);
619						*cc = c;
620						str = cc + strlen(sb.str[0]);
621					}
622					else if(!sftell(tmp2))
623					{	/* not successfull */
624						c = *cp;
625						*cp = '\0';
626						errormsg(SH_DICT, ERROR_ERROR,
627							 "%s%s: substitution failed",
628							(flag & HIST_QUICKSUBST) ? ":s" : "",
629							evp);
630						*cp = c;
631						DONE();
632					}
633					/* loop if g modifier specified */
634					if(!cc || !(flag & HIST_GLOBALSUBST))
635						flag &= ~HIST_SUBSTITUTE;
636				}
637				/* output rest of line */
638				sfputr(tmp2, str, -1);
639				if(*cp)
640					cp--;
641			}
642
643			if(sftell(tmp2))
644			{ /* if any substitions done, swap buffers */
645				if(wm != tmp)
646					sfclose(tmp);
647				tmp = tmp2;
648				tmp2 = 0;
649			}
650			cc = 0;
651			if(*cp)
652				cp++;
653		}
654
655		/* flush temporary buffer to stack */
656		if(tmp)
657		{
658			sfseek(tmp, 0, SEEK_SET);
659
660			if(flag & HIST_QUOTE)
661				stakputc('\'');
662
663			while((c = sfgetc(tmp)) > 0)
664			{
665				if(isspace(c))
666				{
667					flag = flag & ~HIST_NEWLINE;
668
669					/* squash white space to either a
670					   blank or a newline */
671					do
672						flag |= (c == '\n' ? HIST_NEWLINE : 0);
673					while((c = sfgetc(tmp)) > 0 && isspace(c));
674
675					sfungetc(tmp, c);
676
677					c = (flag & HIST_NEWLINE) ? '\n' : ' ';
678
679					if(flag & HIST_QUOTE_BR)
680					{
681						stakputc('\'');
682						stakputc(c);
683						stakputc('\'');
684					}
685					else
686						stakputc(c);
687				}
688				else if((c == '\'') && (flag & HIST_QUOTE))
689				{
690					stakputc('\'');
691					stakputc('\\');
692					stakputc(c);
693					stakputc('\'');
694				}
695				else
696					stakputc(c);
697			}
698			if(flag & HIST_QUOTE)
699				stakputc('\'');
700		}
701	}
702
703	stakputc('\0');
704
705done:
706	if(cc && (flag&HIST_HASH))
707	{
708		/* close !# temp file */
709		sfclose(ref);
710		free(cc);
711		cc = 0;
712	}
713
714	/* error? */
715	if(staktell() && !(flag & HIST_ERROR))
716		*xp = strdup(stakfreeze(1));
717
718	/* restore shell stack */
719	if(off)
720		stakset(sp,off);
721	else
722		stakseek(0);
723
724	/* drop temporary files */
725
726	if(tmp && tmp != wm)
727		sfclose(tmp);
728	if(tmp2)
729		sfclose(tmp2);
730
731	return (flag & HIST_ERROR ? HIST_ERROR : flag & HIST_FLAG_RETURN_MASK);
732}
733
734#endif
735