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