/*
 * hist.c - history expansion
 *
 * This file is part of zsh, the Z shell.
 *
 * Copyright (c) 1992-1996 Paul Falstad
 * All rights reserved.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and to distribute modified versions of this software for any
 * purpose, provided that the above copyright notice and the following
 * two paragraphs appear in all copies of this software.
 *
 * In no event shall Paul Falstad or the Zsh Development Group be liable
 * to any party for direct, indirect, special, incidental, or consequential
 * damages arising out of the use of this software and its documentation,
 * even if Paul Falstad and the Zsh Development Group have been advised of
 * the possibility of such damage.
 *
 * Paul Falstad and the Zsh Development Group specifically disclaim any
 * warranties, including, but not limited to, the implied warranties of
 * merchantability and fitness for a particular purpose.  The software
 * provided hereunder is on an "as is" basis, and Paul Falstad and the
 * Zsh Development Group have no obligation to provide maintenance,
 * support, updates, enhancements, or modifications.
 *
 */

#include "zsh.h"

/*
 * Note on curhist: with history inactive, this points to the
 * last line actually added to the history list.  With history active,
 * the line does not get added to the list until hend(), if at all.
 * However, curhist is incremented to reflect the current line anyway.
 * If the resulting line was not added to the list, a flag is set so
 * that curhist will be decremented in hbegin().
 */

/* Bits of histactive variable */
#define HA_ACTIVE	(1<<0)	/* History mechanism is active */
#define HA_NOSTORE	(1<<1)	/* Don't store the line when finished */
#define HA_JUNKED	(1<<2)	/* Last history line was already junked */
#define HA_NOINC	(1<<3)	/* Don't store, curhist not incremented */

extern int cs, ll;

/* Array of word beginnings and endings in current history line. */
short *chwords;
/* Max, actual position in chwords.
 * nwords = chwordpos/2 because we record beginning and end of words.
 */
int chwordlen, chwordpos;

/* add a character to the current history word */

/**/
void
hwaddc(int c)
{
    /* Only if history line exists and lexing has not finished. */
    if (chline && !(errflag || lexstop)) {
	/* Quote un-expanded bangs in the history line. */
	if (c == bangchar && stophist < 2 && qbang)
	    /* If qbang is not set, we do not escape this bangchar as it's *
	     * not mecessary (e.g. it's a bang in !=, or it is followed    *
	     * by a space). Roughly speaking, qbang is zero only if the    *
	     * history interpreter has already digested this bang and      *
	     * found that it is not necessary to escape it.                */
	    hwaddc('\\');
	*hptr++ = c;

	/* Resize history line if necessary */
	if (hptr - chline >= hlinesz) {
	    int oldsiz = hlinesz;

	    chline = realloc(chline, hlinesz = oldsiz + 16);
	    hptr = chline + oldsiz;
	}
    }
}

/* This function adds a character to the zle input line. It is used when *
 * zsh expands history (see doexpandhist() in zle_tricky.c). It also     *
 * calculates the new cursor position after the expansion. It is called  *
 * from hgetc() and from gettok() in lex.c for characters in comments.   */
 
/**/
void
addtoline(int c)
{
    if (! expanding || lexstop)
	return;
    if (qbang && c == bangchar && stophist < 2) {
	exlast--;
	spaceinline(1);
	line[cs++] = '\\';
    }
    if (excs > cs) {
	excs += 1 + inbufct - exlast;
	if (excs < cs)
	    /* this case could be handled better but it is    *
	     * so rare that it does not worth it              */
	    excs = cs;
    }
    exlast = inbufct;
    spaceinline(1);
    line[cs++] = itok(c) ? ztokens[c - Pound] : c;
}

/**/
int
hgetc(void)
{
    int c = ingetc();

    qbang = 0;
    if (!stophist && !(inbufflags & INP_ALIAS)) {
	/* If necessary, expand history characters. */
	c = histsubchar(c);
	if (c < 0) {
	    /* bad expansion */
	    errflag = lexstop = 1;
	    return ' ';
	}
    }
    if ((inbufflags & INP_HIST) && !stophist) {
	/* the current character c came from a history expansion          *
	 * (inbufflags && INP_HIST) and history is not disabled           *
	 * (e.g. we are not inside single quotes). In that case, \!       *
	 * should be treated as ! (since this \! came from a previous     *
	 * history line where \ was used to escape the bang). So if       *
	 * c == '\\' we fetch one more character to see if it's a bang,   *
	 * and if it is not, we unget it and reset c back to '\\'         */
	qbang = 0;
	if (c == '\\' && !(qbang = (c = ingetc()) == bangchar))
	    safeinungetc(c), c = '\\';
    } else if (stophist || (inbufflags & INP_ALIAS))
	/* If the result is a bangchar which came from history or alias  *
	 * expansion, we treat it as an escaped bangchar, unless history *
	 * is disabled. If stophist == 1 it only means that history is   *
	 * temporarily disabled by a !" which won't appear in in the     *
	 * history, so we still have an escaped bang. stophist > 1 if    *
	 * history is disabled with NOBANGHIST or by someone else (e.g.  *
	 * when the lexer scans single quoted text).                     */
	qbang = c == bangchar && (stophist < 2);
    hwaddc(c);
    addtoline(c);

    return c;
}

/**/
void
safeinungetc(int c)
{
    if (lexstop)
	lexstop = 0;
    else
	inungetc(c);
}

/**/
void
herrflush(void)
{
    while (!lexstop && inbufct && !strin)
	hwaddc(ingetc());
}

/* extract :s/foo/bar/ delimiters and arguments */

/**/
int
getsubsargs(char *subline)
{
    int del;
    char *ptr1, *ptr2;

    del = ingetc();
    ptr1 = hdynread2(del);
    if (!ptr1)
	return 1;
    ptr2 = hdynread2(del);
    if (strlen(ptr1)) {
	zsfree(hsubl);
	hsubl = ptr1;
    }
    zsfree(hsubr);
    hsubr = ptr2;
    if (hsubl && !strstr(subline, hsubl)) {
	herrflush();
	zerr("substitution failed", NULL, 0);
	return 1;
    }
    return 0;
}

/* Get the maximum no. of words for a history entry. */

/**/
int
getargc(Histent ehist)
{
    return ehist->nwords ? ehist->nwords-1 : 0;
}

/* Perform history substitution, returning the next character afterwards. */

/**/
int
histsubchar(int c)
{
    int ev, farg, evset = -1, larg, argc, cflag = 0, bflag = 0;
    static int mev = -1, marg = -1;
    char buf[256], *ptr;
    char *sline;
    Histent ehist;

    /* look, no goto's */
    if (isfirstch && c == hatchar) {
	/* Line begins ^foo^bar */
	isfirstch = 0;
	inungetc(hatchar);
	if (!(ehist = gethist(defev))
	    || !(sline = getargs(ehist, 0, getargc(ehist)))
	    || getsubsargs(sline) || !hsubl)
	    return -1;
	subst(&sline, hsubl, hsubr, 0);
    } else {
	/* Line doesn't begin ^foo^bar */
	if (c != ' ')
	    isfirstch = 0;
	if (c == '\\') {
	    int g = ingetc();

	    if (g != bangchar)
		safeinungetc(g);
	    else {
		qbang = 1;
		return bangchar;
	    }
	}
	if (c != bangchar)
	    return c;
	*hptr = '\0';
	if ((c = ingetc()) == '{') {
	    bflag = cflag = 1;
	    c = ingetc();
	}
	if (c == '\"') {
	    stophist = 1;
	    return ingetc();
	}
	if ((!cflag && inblank(c)) || c == '=' || c == '(' || lexstop) {
	    safeinungetc(c);
	    return bangchar;
	}
	cflag = 0;
	ptr = buf;

	/* get event number */

	if (c == '?') {
	    for (;;) {
		c = ingetc();
		if (c == '?' || c == '\n' || lexstop)
		    break;
		else
		    *ptr++ = c;
	    }
	    if (c != '\n' && !lexstop)
		c = ingetc();
	    *ptr = '\0';
	    mev = ev = hconsearch(hsubl = ztrdup(buf), &marg);
	    evset = 0;
	    if (ev == -1) {
		herrflush();
		zerr("no such event: %s", buf, 0);
		return -1;
	    }
	} else {
	    int t0;

	    for (;;) {
		if (inblank(c) || c == ';' || c == ':' || c == '^' ||
		    c == '$' || c == '*' || c == '%' || c == '}' ||
		    c == '\'' || c == '"' || c == '`' || lexstop)
		    break;
		if (ptr != buf) {
		    if (c == '-')
			break;
		    if ((idigit(buf[0]) || buf[0] == '-') && !idigit(c))
			break;
		}
		*ptr++ = c;
		if (c == '#' || c == bangchar) {
		    c = ingetc();
		    break;
		}
		c = ingetc();
	    }
	    *ptr = 0;
	    if (!*buf) {
		if (c != '%') {
		    if (isset(CSHJUNKIEHISTORY))
			ev = curhist - 1;
		    else
			ev = defev;
		    if (c == ':' && evset == -1)
			evset = 0;
		    else
			evset = 1;
		} else {
		    if (marg != -1)
			ev = mev;
		    else
			ev = defev;
		    evset = 0;
		}
	    } else if ((t0 = atoi(buf))) {
		ev = (t0 < 0) ? curhist + t0 : t0;
		evset = 1;
	    } else if ((unsigned)*buf == bangchar) {
		ev = curhist - 1;
		evset = 1;
	    } else if (*buf == '#') {
		ev = curhist;
		evset = 1;
	    } else if ((ev = hcomsearch(buf)) == -1) {
		herrflush();
		zerr("event not found: %s", buf, 0);
		return -1;
	    } else
		evset = 1;
	}

	/* get the event */

	if (!(ehist = gethist(defev = ev)))
	    return -1;

	/* extract the relevant arguments */

	argc = getargc(ehist);
	if (c == ':') {
	    cflag = 1;
	    c = ingetc();
	    if (c == '%' && marg != -1) {
		if (!evset) {
		    ehist = gethist(defev = mev);
		    argc = getargc(ehist);
		} else {
		    herrflush();
		    zerr("Ambiguous history reference", NULL, 0);
		    return -1;
		}

	    }
	}
	if (c == '*') {
	    farg = 1;
	    larg = argc;
	    cflag = 0;
	} else {
	    inungetc(c);
	    larg = farg = getargspec(argc, marg, evset);
	    if (larg == -2)
		return -1;
	    if (farg != -1)
		cflag = 0;
	    c = ingetc();
	    if (c == '*') {
		cflag = 0;
		larg = argc;
	    } else if (c == '-') {
		cflag = 0;
		larg = getargspec(argc, marg, evset);
		if (larg == -2)
		    return -1;
		if (larg == -1)
		    larg = argc - 1;
	    } else
		inungetc(c);
	}
	if (farg == -1)
	    farg = 0;
	if (larg == -1)
	    larg = argc;
	if (!(sline = getargs(ehist, farg, larg)))
	    return -1;
    }

    /* do the modifiers */

    for (;;) {
	c = (cflag) ? ':' : ingetc();
	cflag = 0;
	if (c == ':') {
	    int gbal = 0;

	    if ((c = ingetc()) == 'g') {
		gbal = 1;
		c = ingetc();
	    }
	    switch (c) {
	    case 'p':
		histdone = HISTFLAG_DONE | HISTFLAG_NOEXEC;
		break;
	    case 'h':
		if (!remtpath(&sline)) {
		    herrflush();
		    zerr("modifier failed: h", NULL, 0);
		    return -1;
		}
		break;
	    case 'e':
		if (!rembutext(&sline)) {
		    herrflush();
		    zerr("modifier failed: e", NULL, 0);
		    return -1;
		}
		break;
	    case 'r':
		if (!remtext(&sline)) {
		    herrflush();
		    zerr("modifier failed: r", NULL, 0);
		    return -1;
		}
		break;
	    case 't':
		if (!remlpaths(&sline)) {
		    herrflush();
		    zerr("modifier failed: t", NULL, 0);
		    return -1;
		}
		break;
	    case 's':
		if (getsubsargs(sline))
		    return -1; /* fall through */
	    case '&':
		if (hsubl && hsubr)
		    subst(&sline, hsubl, hsubr, gbal);
		else {
		    herrflush();
		    zerr("no previous substitution", NULL, 0);
		    return -1;
		}
		break;
	    case 'q':
		quote(&sline);
		break;
	    case 'x':
		quotebreak(&sline);
		break;
	    case 'l':
		downcase(&sline);
		break;
	    case 'u':
		upcase(&sline);
		break;
	    default:
		herrflush();
		zerr("illegal modifier: %c", NULL, c);
		return -1;
	    }
	} else {
	    if (c != '}' || !bflag)
		inungetc(c);
	    if (c != '}' && bflag) {
		zerr("'}' expected", NULL, 0);
		return -1;
	    }
	    break;
	}
    }

    /*
     * Push the expanded value onto the input stack,
     * marking this as a history word for purposes of the alias stack.
     */

    lexstop = 0;
    /* this function is called only called from hgetc and only if      *
     * !(inbufflags & INP_ALIAS). History expansion should never be    *
     * done with INP_ALIAS (to prevent recursive history expansion and *
     * histoty expansion of aliases). Escapes are not removed here.    *
     * This is now handled in hgetc.                                   */
    inpush(sline, INP_HIST, NULL); /* sline from heap, don't free */
    histdone |= HISTFLAG_DONE;
    if (isset(HISTVERIFY))
	histdone |= HISTFLAG_NOEXEC | HISTFLAG_RECALL;

    /* Don't try and re-expand line. */
    return ingetc();
}

/* unget a char and remove it from chline. It can only be used *
 * to unget a character returned by hgetc.                     */

/**/
void
hungetc(int c)
{
    int doit = 1;

    while (!lexstop) {
	if (hptr[-1] != (char) c && stophist < 4 &&
	    hptr > chline + 1 && hptr[-1] == '\n' && hptr[-2] == '\\')
	    hungetc('\n'), hungetc('\\');

	if (expanding) {
	    cs--;
	    ll--;
	    exlast++;
	}
	DPUTS(hptr <= chline, "BUG: hungetc attempted at buffer start");
	hptr--;
	DPUTS(*hptr != (char) c, "BUG: wrong character in hungetc() ");
	qbang = (c == bangchar && stophist < 2 &&
		 hptr > chline && hptr[-1] == '\\');
	if (doit)
	    inungetc(c);
	if (!qbang)
	    return;
	doit = !stophist && ((inbufflags & INP_HIST) ||
				 !(inbufflags & INP_ALIAS));
	c = '\\';
    }
}

/* begin reading a string */

/**/
void
strinbeg(void)
{
    strin++;
    hbegin();
    lexinit();
}

/* done reading a string */

/**/
void
strinend(void)
{
    hend();
    DPUTS(!strin, "BUG: strinend() called without strinbeg()");
    strin--;
    isfirstch = 1;
    histdone = 0;
}

/* initialize the history mechanism */

/**/
void
hbegin(void)
{
    Histent curhistent;

    isfirstln = isfirstch = 1;
    errflag = histdone = spaceflag = 0;
    stophist = (!interact || unset(BANGHIST) || unset(SHINSTDIN)) << 1;
    chline = hptr = zcalloc(hlinesz = 16);
    chwords = zalloc((chwordlen = 16)*sizeof(short));
    chwordpos = 0;

    if (histactive & HA_JUNKED)
	curhist--;
    curhistent = gethistent(curhist);
    if (!curhistent->ftim)
	curhistent->ftim = time(NULL);
    histactive = HA_ACTIVE;
    if (interact && isset(SHINSTDIN) && !strin) {
	attachtty(mypgrp);
	defev = curhist;
	curhist++;
    } else
	histactive |= HA_NOINC;
}

/* compare current line with history entry using only text in words */

/**/
int
histcmp(Histent he)
{
    int kword, lword;
    int nwords = chwordpos/2;

    /* If the history entry came from a file, the words were not divided by
     * the lexer so we have to resort to a whitespace-ignoring compare.
     */
    if (he->flags & HIST_READ) {
	char *str1 = he->text, *str2 = chline;

	while (inblank(*str1)) str1++;
	while (inblank(*str2)) str2++;
	while (*str1 && *str2) {
	    if (inblank(*str1)) {
		if (!inblank(*str2))
		    break;
		do str1++; while (inblank(*str1));
		do str2++; while (inblank(*str2));
	    }
	    else {
		if (*str1 != *str2)
		    break;
		str1++;
		str2++;
	    }
	}
	return *str1 - *str2;
    }

    if (nwords != he->nwords)
	return 1;

    for (kword = 0; kword < 2*nwords; kword += 2)
	if ((lword = chwords[kword+1]-chwords[kword])
	    != he->words[kword+1]-he->words[kword] ||
	    memcmp(he->text+he->words[kword], chline+chwords[kword], lword))
	    return 1;

    return 0;
}

/**/
void
histreduceblanks(void)
{
    int i, len, pos, needblank;

    for (i = 0, len = 0; i < chwordpos; i += 2) {
	len += chwords[i+1] - chwords[i]
	     + (i > 0 && chwords[i] > chwords[i-1]);
    }
    if (chline[len] == '\0')
	return;

    for (i = 0, pos = 0; i < chwordpos; i += 2) {
	len = chwords[i+1] - chwords[i];
	needblank = (i < chwordpos-2 && chwords[i+2] > chwords[i+1]);
	if (pos != chwords[i]) {
	    memcpy(chline + pos, chline + chwords[i], len + needblank);
	    chwords[i] = pos;
	    chwords[i+1] = chwords[i] + len;
	}
	pos += len + needblank;
    }
    chline[pos] = '\0';
}

/* say we're done using the history mechanism */

/**/
int
hend(void)
{
    int flag, save = 1;

    DPUTS(!chline, "BUG: chline is NULL in hend()");
    if (histdone & HISTFLAG_SETTY)
	settyinfo(&shttyinfo);
    if (histactive & (HA_NOSTORE|HA_NOINC)) {
	zfree(chline, hlinesz);
	zfree(chwords, chwordlen*sizeof(short));
	chline = NULL;
	if (!(histactive & HA_NOINC))
	    curhist--;
	histactive = 0;
	return 1;
    }
    flag = histdone;
    histdone = 0;
    if (hptr < chline + 1)
	save = 0;
    else {
	*hptr = '\0';
	if (hptr[-1] == '\n') {
	    if (chline[1]) {
		*--hptr = '\0';
	    } else
		save = 0;
	}
	if (!*chline || !strcmp(chline, "\n") ||
	    (isset(HISTIGNORESPACE) && spaceflag))
	    save = 0;
    }
    if (flag & (HISTFLAG_DONE | HISTFLAG_RECALL)) {
	char *ptr;

	ptr = ztrdup(chline);
	if ((flag & (HISTFLAG_DONE | HISTFLAG_RECALL)) == HISTFLAG_DONE) {
	    zputs(ptr, shout);
	    fputc('\n', shout);
	    fflush(shout);
	}
	if (flag & HISTFLAG_RECALL) {
	    PERMALLOC {
		pushnode(bufstack, ptr);
	    } LASTALLOC;
	    save = 0;
	} else
	    zsfree(ptr);
    }
    if (save) {
	Histent he;

#ifdef DEBUG
	/* debugging only */
	if (chwordpos%2) {
	    hwend();
	    DPUTS(1, "BUG: uncompleted line in history");
	}
#endif
	/* get rid of pesky \n which we've already nulled out */
	if (chwordpos > 1 && !chline[chwords[chwordpos-2]])
	    chwordpos -= 2;
	/* strip superfluous blanks, if desired */
	if (isset(HISTREDUCEBLANKS))
	    histreduceblanks();
	if (isset(HISTIGNOREDUPS) && (he = gethistent(curhist - 1))
	 && he->text && histcmp(he) == 0) {
	    /* This history entry compares the same as the previous.
	     * In case minor changes were made, we overwrite the
	     * previous one with the current one.  This also gets the
	     * timestamp right.  Perhaps, preserve the HIST_OLD flag.
	     */
	    zsfree(he->text);
	    he->text = ztrdup(chline);
	    if (chwordpos)
		memcpy(he->words, chwords, chwordpos * sizeof(short));
	    he->stim = time(NULL);	/* set start time */
	    he->ftim = 0;
	    curhist--;
	}
	else {
	    Histent curhistent = gethistent(curhist);
	    zsfree(curhistent->text);
	    if (curhistent->nwords)
		zfree(curhistent->words, curhistent->nwords*2*sizeof(short));

	    curhistent->text = ztrdup(chline);
	    curhistent->stim = time(NULL);
	    curhistent->ftim = 0L;
	    curhistent->flags = 0;

	    if ((curhistent->nwords = chwordpos/2)) {
		curhistent->words = (short *)zalloc(chwordpos * sizeof(short));
		memcpy(curhistent->words, chwords, chwordpos * sizeof(short));
	    }
	}
    } else
	curhist--;
    zfree(chline, hlinesz);
    zfree(chwords, chwordlen*sizeof(short));
    chline = NULL;
    histactive = 0;
    return !(flag & HISTFLAG_NOEXEC || errflag);
}

/* remove the current line from the history List */

/**/
void
remhist(void)
{
    if (!(histactive & HA_ACTIVE)) {
	if (!(histactive & HA_JUNKED)) {
	    /* make sure this doesn't show up when we do firsthist() */
	    Histent he = gethistent(curhist);
	    zsfree(he->text);
	    he->text = NULL;
	    histactive |= HA_JUNKED;
	    /* curhist-- is delayed until the next hbegin() */
	}
    } else
	histactive |= HA_NOSTORE;
}

/* Gives current expansion word if not last word before chwordpos. */
int hwgetword = -1;

/* begin a word */

/**/
void
hwbegin(int offset)
{
    if (chwordpos%2)
	chwordpos--;	/* make sure we're on a word start, not end */
    /* If we're expanding an alias, we should overwrite the expansion
     * in the history.
     */
    if ((inbufflags & INP_ALIAS) && !(inbufflags & INP_HIST))
	hwgetword = chwordpos;
    else
	hwgetword = -1;
    chwords[chwordpos++] = hptr - chline + offset;
}

/* add a word to the history List */

/**/
void
hwend(void)
{
    if (chwordpos%2 && chline) {
	/* end of word reached and we've already begun a word */
	if (hptr > chline + chwords[chwordpos-1]) {
	    chwords[chwordpos++] = hptr - chline;
	    if (chwordpos >= chwordlen) {
		chwords = (short *) realloc(chwords,
					    (chwordlen += 16)*sizeof(short));
	    }
	    if (hwgetword > -1) {
		/* We want to reuse the current word position */
		chwordpos = hwgetword;
		/* Start from where previous word ended, if possible */
		hptr = chline + chwords[chwordpos ? chwordpos - 1 : 0];
	    }
	} else {
	    /* scrub that last word, it doesn't exist */
	    chwordpos--;
	}
    }
}

/* Go back to immediately after the last word, skipping space. */

/**/
void
histbackword(void)
{
    if (!(chwordpos%2) && chwordpos)
	hptr = chline + chwords[chwordpos-1];
}

/* Get the start and end point of the current history word */

/**/
void
hwget(char **startptr)
{
    int pos = hwgetword > -1 ? hwgetword : chwordpos - 2;

#ifdef DEBUG
    /* debugging only */
    if (hwgetword == -1 && !chwordpos) {
	/* no words available */
	DPUTS(1, "BUG: hwget() called with no words");
	*startptr = "";
	return;
    } 
    else if (hwgetword == -1 && chwordpos%2) {
	DPUTS(1, "BUG: hwget() called in middle of word");
	*startptr = "";
	return;
    }
#endif

    *startptr = chline + chwords[pos];
    chline[chwords[++pos]] = '\0';
}

/* Replace the current history word with rep, if different */

/**/
void
hwrep(char *rep)
{
    char *start;
    hwget(&start);

    if (!strcmp(rep, start))
	return;
    
    hptr = start;
    chwordpos = (hwgetword > -1) ? hwgetword : chwordpos - 2;
    hwbegin(0);
    qbang = 1;
    while (*rep)
	hwaddc(*rep++);
    hwend();
}

/* Get the entire current line, deleting it in the history. */

/**/
char *
hgetline(void)
{
    /* Currently only used by pushlineoredit().
     * It's necessary to prevent that from getting too pally with
     * the history code.
     */
    char *ret;

    if (!chline || hptr == chline)
	return NULL;
    *hptr = '\0';
    ret = dupstring(chline);

    /* reset line */
    hptr = chline;
    chwordpos = 0;
    hwgetword = -1;

    return ret;
}

/* get an argument specification */

/**/
int
getargspec(int argc, int marg, int evset)
{
    int c, ret = -1;

    if ((c = ingetc()) == '0')
	return 0;
    if (idigit(c)) {
	ret = 0;
	while (idigit(c)) {
	    ret = ret * 10 + c - '0';
	    c = ingetc();
	}
	inungetc(c);
    } else if (c == '^')
	ret = 1;
    else if (c == '$')
	ret = argc;
    else if (c == '%') {
	if (evset) {
	    herrflush();
	    zerr("Ambiguous history reference", NULL, 0);
	    return -2;
	}
	if (marg == -1) {
	    herrflush();
	    zerr("%% with no previous word matched", NULL, 0);
	    return -2;
	}
	ret = marg;
    } else
	inungetc(c);
    return ret;
}

/* do ?foo? search */

/**/
int
hconsearch(char *str, int *marg)
{
    int t0, t1 = 0;
    char *s;
    Histent he;

    for (t0 = curhist - 1; (he = quietgethist(t0)); t0--)
	if ((s = strstr(he->text, str))) {
	    int pos = s - he->text;
	    while (t1 < he->nwords && he->words[2*t1] <= pos)
		t1++;
	    *marg = t1 - 1;
	    return t0;
	}
    return -1;
}

/* do !foo search */

/**/
int
hcomsearch(char *str)
{
    int t0;
    char *hs;

    for (t0 = curhist - 1; (hs = quietgetevent(t0)); t0--)
	if (!strncmp(hs, str, strlen(str)))
	    return t0;
    return -1;
}

/* various utilities for : modifiers */

/**/
int
remtpath(char **junkptr)
{
    char *str = *junkptr, *remcut;

    if ((remcut = strrchr(str, '/'))) {
	if (str != remcut)
	    *remcut = '\0';
	else
	    str[1] = '\0';
	return 1;
    }
    return 0;
}

/**/
int
remtext(char **junkptr)
{
    char *str = *junkptr, *remcut;

    if ((remcut = strrchr(str, '.')) && remcut != str) {
	*remcut = '\0';
	return 1;
    }
    return 0;
}

/**/
int
rembutext(char **junkptr)
{
    char *str = *junkptr, *remcut;

    if ((remcut = strrchr(str, '.')) && remcut != str) {
	*junkptr = dupstring(remcut + 1);	/* .xx or xx? */
	return 1;
    }
    return 0;
}

/**/
int
remlpaths(char **junkptr)
{
    char *str = *junkptr, *remcut;

    if ((remcut = strrchr(str, '/'))) {
	*remcut = '\0';
	*junkptr = dupstring(remcut + 1);
	return 1;
    }
    return 0;
}

/**/
int
makeuppercase(char **junkptr)
{
    char *str = *junkptr;

    for (; *str; str++)
	*str = tuupper(*str);
    return 1;
}

/**/
int
makelowercase(char **junkptr)
{
    char *str = *junkptr;

    for (; *str; str++)
	*str = tulower(*str);
    return 1;
}

/**/
int
makecapitals(char **junkptr)
{
    char *str = *junkptr;

    for (; *str;) {
	for (; *str && !ialnum(*str); str++);
	if (*str)
	    *str = tuupper(*str), str++;
	for (; *str && ialnum(*str); str++)
	    *str = tulower(*str);
    }
    return 1;
}

/**/
void
subst(char **strptr, char *in, char *out, int gbal)
{
    char *str = *strptr, *instr = *strptr, *substcut, *sptr, *oldstr;
    int off, inlen, outlen;

    if (!*in)
	in = str, gbal = 0;
    if (!(substcut = (char *)strstr(str, in)))
	return;
    inlen = strlen(in);
    sptr = convamps(out, in, inlen);
    outlen = strlen(sptr);

    do {
	*substcut = '\0';
	off = substcut - *strptr + outlen;
	substcut += inlen;
	*strptr = tricat(oldstr = *strptr, sptr, substcut);
	if (oldstr != instr)
	    zsfree(oldstr);
	str = (char *)*strptr + off;
    } while (gbal && (substcut = (char *)strstr(str, in)));
}

/**/
char *
convamps(char *out, char *in, int inlen)
{
    char *ptr, *ret, *pp;
    int slen, sdup = 0;

    for (ptr = out, slen = 0; *ptr; ptr++, slen++)
	if (*ptr == '\\')
	    ptr++, sdup = 1;
	else if (*ptr == '&')
	    slen += inlen - 1, sdup = 1;
    if (!sdup)
	return out;
    ret = pp = (char *)alloc(slen + 1);
    for (ptr = out; *ptr; ptr++)
	if (*ptr == '\\')
	    *pp++ = *++ptr;
	else if (*ptr == '&') {
	    strcpy(pp, in);
	    pp += inlen;
	} else
	    *pp++ = *ptr;
    *pp = '\0';
    return ret;
}

/**/
struct histent *
quietgethist(int ev)
{
    static struct histent storehist;

    if (ev < firsthist() || ev > curhist)
	return NULL;
    if (ev == curhist && (histactive & HA_ACTIVE)) {
	/* The current history line has not been stored.  Build it up
	 * from other variables.
	 */
	storehist.text = chline;
	storehist.nwords = chwordpos/2;
	storehist.words = chwords;

	return &storehist;
    } else
	return gethistent(ev);
}

/**/
char *
quietgetevent(int ev)
{
    Histent ent = quietgethist(ev);

    return ent ? ent->text : NULL;
}

/**/
Histent
gethist(int ev)
{
    Histent ret;

    ret = quietgethist(ev);
    if (!ret) {
	herrflush();
	zerr("no such event: %d", NULL, ev);
    }
    return ret;
}

/**/
char *
getargs(Histent elist, int arg1, int arg2)
{
    short *words = elist->words;
    int pos1, nwords = elist->nwords;

    if (arg2 < arg1 || arg1 >= nwords || arg2 >= nwords) {
	/* remember, argN is indexed from 0, nwords is total no. of words */
	herrflush();
	zerr("no such word in event", NULL, 0);
	return NULL;
    }

    pos1 = words[2*arg1];
    return dupstrpfx(elist->text + pos1, words[2*arg2+1] - pos1);
}

/**/
void
upcase(char **x)
{
    char *pp = *(char **)x;

    for (; *pp; pp++)
	*pp = tuupper(*pp);
}

/**/
void
downcase(char **x)
{
    char *pp = *(char **)x;

    for (; *pp; pp++)
	*pp = tulower(*pp);
}

/**/
int
quote(char **tr)
{
    char *ptr, *rptr, **str = (char **)tr;
    int len = 3;
    int inquotes = 0;

    for (ptr = *str; *ptr; ptr++, len++)
	if (*ptr == '\'') {
	    len += 3;
	    if (!inquotes)
		inquotes = 1;
	    else
		inquotes = 0;
	} else if (inblank(*ptr) && !inquotes && ptr[-1] != '\\')
	    len += 2;
    ptr = *str;
    *str = rptr = (char *)alloc(len);
    *rptr++ = '\'';
    for (; *ptr; ptr++)
	if (*ptr == '\'') {
	    if (!inquotes)
		inquotes = 1;
	    else
		inquotes = 0;
	    *rptr++ = '\'';
	    *rptr++ = '\\';
	    *rptr++ = '\'';
	    *rptr++ = '\'';
	} else if (inblank(*ptr) && !inquotes && ptr[-1] != '\\') {
	    *rptr++ = '\'';
	    *rptr++ = *ptr;
	    *rptr++ = '\'';
	} else
	    *rptr++ = *ptr;
    *rptr++ = '\'';
    *rptr++ = 0;
    str[1] = NULL;
    return 0;
}

/**/
int
quotebreak(char **tr)
{
    char *ptr, *rptr, **str = (char **)tr;
    int len = 3;

    for (ptr = *str; *ptr; ptr++, len++)
	if (*ptr == '\'')
	    len += 3;
	else if (inblank(*ptr))
	    len += 2;
    ptr = *str;
    *str = rptr = (char *)alloc(len);
    *rptr++ = '\'';
    for (; *ptr;)
	if (*ptr == '\'') {
	    *rptr++ = '\'';
	    *rptr++ = '\\';
	    *rptr++ = '\'';
	    *rptr++ = '\'';
	    ptr++;
	} else if (inblank(*ptr)) {
	    *rptr++ = '\'';
	    *rptr++ = *ptr++;
	    *rptr++ = '\'';
	} else
	    *rptr++ = *ptr++;
    *rptr++ = '\'';
    *rptr++ = '\0';
    return 0;
}

/* read an arbitrary amount of data into a buffer until stop is found */

#if 0 /**/
char *
hdynread(int stop)
{
    int bsiz = 256, ct = 0, c;
    char *buf = (char *)zalloc(bsiz), *ptr;

    ptr = buf;
    while ((c = ingetc()) != stop && c != '\n' && !lexstop) {
	if (c == '\\')
	    c = ingetc();
	*ptr++ = c;
	if (++ct == bsiz) {
	    buf = realloc(buf, bsiz *= 2);
	    ptr = buf + ct;
	}
    }
    *ptr = 0;
    if (c == '\n') {
	inungetc('\n');
	zerr("delimiter expected", NULL, 0);
	zfree(buf, bsiz);
	return NULL;
    }
    return buf;
}
#endif

/**/
char *
hdynread2(int stop)
{
    int bsiz = 256, ct = 0, c;
    char *buf = (char *)zalloc(bsiz), *ptr;

    ptr = buf;
    while ((c = ingetc()) != stop && c != '\n' && !lexstop) {
	if (c == '\n') {
	    inungetc(c);
	    break;
	}
	if (c == '\\')
	    c = ingetc();
	*ptr++ = c;
	if (++ct == bsiz) {
	    buf = realloc(buf, bsiz *= 2);
	    ptr = buf + ct;
	}
    }
    *ptr = 0;
    if (c == '\n')
	inungetc('\n');
    return buf;
}

/**/
void
inithist(void)
{
    histentct = histsiz;
    histentarr = (Histent) zcalloc(histentct * sizeof *histentarr);
}

/**/
void
resizehistents(void)
{
    int newentct, t0, t1, firstlex;
    Histent newarr;

    newentct = histsiz;
    newarr = (Histent) zcalloc(newentct * sizeof *newarr);
    firstlex = curhist - histsiz + 1;
    t0 = firsthist();
    if (t0 < curhist - newentct)
	t0 = curhist - newentct;
    t1 = t0 % newentct;
    for (; t0 <= curhist; t0++) {
	newarr[t1] = *gethistent(t0);
	if (t0 < firstlex) {
	    zsfree(newarr[t1].text);
	    newarr[t1].text = NULL;
	}
	t1++;
	if (t1 == newentct)
	    t1 = 0;
    }
    free(histentarr);
    histentarr = newarr;
    histentct = newentct;
}

/**/
void
readhistfile(char *s, int err)
{
    char *buf;
    FILE *in;
    Histent ent;
    time_t tim = time(NULL);
    short *wordlist;
    int nwordpos, nwordlist, bufsiz;

    if (!s)
	return;
    if ((in = fopen(unmeta(s), "r"))) {
	nwordlist = 16;
	wordlist = (short *)zalloc(nwordlist*sizeof(short));
	bufsiz = 1024;
	buf = zalloc(bufsiz);

	while (fgets(buf, bufsiz, in)) {
	    int l = strlen(buf);
	    char *pt, *start;

	    while (l) {
		while (buf[l - 1] != '\n') {
		    buf = zrealloc(buf, 2 * bufsiz);
		    bufsiz = 2 * bufsiz;
		    if (!fgets(buf + l, bufsiz - l, in)) {
			l++;
			break;
		    }
		    l = strlen(buf);
		}
		buf[l - 1] = '\0';
		if (l > 1 && buf[l - 2] == '\\') {
		    buf[l - 2] = '\n';
		    fgets(buf + l - 1, bufsiz - (l - 1), in);
		    l = strlen(buf);
		} else
		    break;
	    }

	    ent = gethistent(++curhist);
	    pt = buf;
	    if (*pt == ':') {
		pt++;
		ent->stim = zstrtol(pt, NULL, 0);
		for (; *pt != ':' && *pt; pt++);
		if (*pt) {
		    pt++;
		    ent->ftim = zstrtol(pt, NULL, 0);
		    for (; *pt != ';' && *pt; pt++);
		    if (*pt)
			pt++;
		} else {
		    ent->ftim = tim;
		}
		if (ent->stim == 0)
		    ent->stim = tim;
		if (ent->ftim == 0)
		    ent->ftim = tim;
	    } else {
		ent->ftim = ent->stim = tim;
	    }

	    if (ent->ftim < ent->stim)	/* 3.1.6 history file compatibility */
		ent->ftim += ent->stim;

	    zsfree(ent->text);
	    ent->text = ztrdup(pt);
	    ent->flags = HIST_OLD|HIST_READ;
	    if (ent->nwords)
		zfree(ent->words, ent->nwords*2*sizeof(short));

	    /* Divide up the words.  We don't know how it lexes,
	       so just look for white-space.
	       */
	    nwordpos = 0;
	    start = pt;
	    do {
		while (inblank(*pt))
		    pt++;
		if (*pt) {
		    if (nwordpos >= nwordlist)
			wordlist = (short *) realloc(wordlist,
					(nwordlist += 16)*sizeof(short));
		    wordlist[nwordpos++] = pt - start;
		    while (*pt && !inblank(*pt))
			pt++;
		    wordlist[nwordpos++] = pt - start;
		}
	    } while (*pt);

	    ent->nwords = nwordpos/2;
	    if (ent->nwords) {
		ent->words = (short *)zalloc(nwordpos*sizeof(short));
		memcpy(ent->words, wordlist, nwordpos*sizeof(short));
	    } else
		ent->words = (short *)NULL;
	}
	fclose(in);

	zfree(wordlist, nwordlist*sizeof(short));
	zfree(buf, bufsiz);
    } else if (err)
	zerr("can't read history file", s, 0);
}

/**/
void
savehistfile(char *s, int err, int app)
{
    char *t;
    FILE *out;
    int ev;
    Histent ent;
    int savehist = getiparam("SAVEHIST");

    if (!s || !interact || savehist <= 0)
	return;
    ev = curhist - savehist + 1;
    if (ev < firsthist())
	ev = firsthist();
    if (app & 1)
	out = fdopen(open(unmeta(s), O_CREAT | O_WRONLY | O_APPEND, 0600), "a");
    else
	out = fdopen(open(unmeta(s), O_CREAT | O_WRONLY | O_TRUNC, 0600), "w");
    if (out) {
	for (; ev <= curhist - !!histactive; ev++) {
	    ent = gethistent(ev);
	    if (app & 2) {
		if (ent->flags & HIST_OLD)
		    continue;
		ent->flags |= HIST_OLD;
	    }
	    t = ent->text;
	    if (isset(EXTENDEDHISTORY)) {
		fprintf(out, ": %ld:%ld;",
			(long)ent->stim,
			(long)ent->ftim);
	    } else if (*t == ':')
		fputc('\\', out);

	    for (; *t; t++) {
		if (*t == '\n')
		    fputc('\\', out);
		fputc(*t, out);
	    }
	    fputc('\n', out);
	}
	fclose(out);

	if (app & 2 && (out = fopen(unmeta(s), "r"))) {
	    char **store, buf[1024], **ptr;
	    int i, l, histnum = 0;

	    store = (char **)zcalloc((savehist + 1) * sizeof *store);
	    while (fgets(buf, sizeof(buf), out)) {
		char *t;

		if (store[i = histnum % savehist])
		    free(store[i]);
		store[i] = ztrdup(buf);
		l = strlen(buf);
		if (l > 1) {
		    t = store[i] + l;
		    while ((t[-1] != '\n' ||
			    (t[-1] == '\n' && t[-2] == '\\')) &&
			   fgets(buf, sizeof(buf), out)) {
			l += strlen(buf);
			store[i] = zrealloc(store[i], l + 1);
			t = store[i] + l;
			strcat(store[i], buf);
		    }
		}
		histnum++;
	    }
	    fclose(out);
	    if ((out = fdopen(open(unmeta(s), O_WRONLY | O_TRUNC, 0600), "w"))) {
		if (histnum < savehist)
		    for (i = 0; i < histnum; i++)
			fprintf(out, "%s", store[i]);
		else
		    for (i = histnum; i < histnum + savehist; i++)
			fprintf(out, "%s", store[i % savehist]);
		fclose(out);
	    }
	    for (ptr = store; *ptr; ptr++)
		zsfree(*ptr);
	    free(store);
	}
    } else if (err)
	zerr("can't write history file %s", s, 0);
}

/**/
int
firsthist(void)
{
    int ev;
    Histent ent;

    ev = curhist - histentct + 1;
    if (ev < 1)
	ev = 1;
    do {
	ent = gethistent(ev);
	if (ent->text)
	    break;
	ev++;
    }
    while (ev < curhist);
    return ev;
}

