[net.sources] Release 2 Cshell w/completion, autologout, history-save/restore

kg@hplabs.UUCP (Ken Greer) (10/06/83)

Attached is a second release of the C shell with

1.  Command and file name recognition.
2.  Autologout timer
3.  Saving history across logouts.

This release differs from the one I posted a couple weeks ago in:

1.  Speed improvements in tenex.c command searching.  [More still are
    needed -- like prefix oriented hashing.  Any takers?]
2.  It correctly handles -ctlecho mode (presuming your terminal itself
    doesn't wierd out when it gets an ESC.)
3.  Corrections to INSTALL.  [flush() needed at the end of printprompt
    routine was major omission]
4.  Missed routine name "exit" in makebuiltins.
5.  Inclusion of autologout timer and save/restore of history across
    logouts.

For simplicity, a complete distribution is attached.

				Ken Greer

: "Run this file as a shell script to extract contents."
echo x - INSTALL
cat << '//E*O*F INSTALL' > INSTALL
Installing the new C shell...

1. Adjust the define BUILTINS in tenex.c, if you wish, to whatever
   directory you want to place the builtins.  We use /usr/local/lib/builtins.

2. If you change it, adjust DIR in the shell script "makebuiltins".

3. Run "makebuiltins".

4. In sh.lex.c replace the bgetc routine with the contents
   of newbgetc.c

5. Replace sh.c and sh.hist.c with the ones in this distribution.

6. To makefile:

   a. Add tenex.o to OBJS.

   b. If you have not yet installed the new UCB directory code
      add "-I." to the CFLAGS and dir14.o to OBJS.
      The -I. will let cc find <dir.h> in the current directory.
      4.2 Folks should change <dir.h> to <sys/dir.h>

   c. Add -ltermcap to LIBES.

   d. Xstr does not understand static initialized arrays.  So you must
      either have a special make rule for tenex.c -> tenex.o *without*
      the xstr processing or remove the xstr stuff entirly.
      (I've removed it entirly from ours -- "make clean" the first time,
      of course, if you choose to do this.)

7. make

8. Move ./csh wherever you like.

9. Enjoy.

Notes:

   If you rename csh to something else (like newcsh) and use it as your
   login shell you'll have to fix login.c to know that newcsh wants the
   newtty driver.  If you don't, you'll just get a dumb message
   "Switching to new tty driver".  Alternately, You can just delete this
   message (from sh.c).  We (HP Labs) have simply replaced /bin/csh with
   this one as everyones standard login shell.

   The files dir.h and dir14.c support the old 14 character UNIX
   directories.  Change the makefile if you have the Berkeley arbitrary
   length file names.

See newshell.1 man file for changes made.

If you make any fixes or changes, please mail them back to

		kg@HP-Labs		(CSNET)
		kg.HP-Labs@Rand-Relay	(ARPANET)
		ucbvax!hplabs!kg	(UUCP)

Comments and suggestions always welcome.

				-Ken Greer
//E*O*F INSTALL
echo x - makebuiltins
cat << '//E*O*F makebuiltins' > makebuiltins
#! /bin/sh -x
# Make a fake builtins directory.
DIR=/usr/local/lib/builtins
if [ ! -d $DIR ]
then
	mkdir $DIR
fi
cd $DIR
tee @ alias alloc bg case cd chdir dirs eval exec exit fg foreach glob \
  hashstat history if jobs kill limit login logout newgrp nice nohup \
  notify onintr popd pushd rehash repeat set setenv source stop suspend \
  switch time umask unalias unhash unlimit unset unsetenv wait while < /dev/null
chmod 555 *
//E*O*F makebuiltins
echo x - newshell.1
cat << '//E*O*F newshell.1' > newshell.1
.TH NEWSHELL 1 local
.SH NAME
newshell \- New C shell with tenex-like file name and command completion
.SH DESCRIPTION
We have installed
an enhanced version of the Berkeley UNIX C shell
.I csh (1).
It behaves exactly like the C shell,
except for the added utilities of:
.PP
.in +6
.ti -3
1) Interactive file name, command, and user name recognition and
completion.
.sp
.ti -3
2) Command/File/Directory/User list in the middle of a typed command.
.sp
.ti -3
3) Optional timer for automatic logout after selected idle time.
.sp
.ti -3
4) Terminal mode sanity checking.
.sp
.ti -3
5) Saving history between logouts.
.in -6
.PP
A description of these features follows.
For information on the other standard 
.I csh
features, please see "man csh".
.SH "1. FILE NAME COMPLETION"
In typing file names as arguments to commands,
it is no longer necessary to type a complete name,
only a unique abbreviation is necessary.
When you type an ESCAPE to
.I csh
it will complete the file name for you,
echoing the full name on the terminal.
.PP
If the file name prefix you typed matches no file name, the terminal
bell is enunciated.
The file name may be partially completed if the prefix matches several
longer file names.  If this is the case, the name is extended up to
the ambiguous deviation, and the bell is enunciated.
.PP
.I Example
.PP
In the following example, assume the plus character ``+''
is where the user typed the ESCAPE key.
Assume the current directory contained the files:
.sp
.nf
   DSC.OLD    bin        cmd       lib        memos
   DSC.NEW    chaosnet   cmtest    mail       netnews
   bench      class      dev       mbox       new
.fi
.sp
The command:
.sp
		% vi ch+
.sp
would cause 
.I csh
to complete the command with the name \fIchaosnet\fR.  If instead, the
user had typed:
.sp
		% vi D+
.sp
.I csh
would have extended the name to \fIDSC.\fR and enunciated the terminal bell, 
indicating partial completion.
.PP
File name completion works equally well when other directories are addressed.
In addition,
the tilde (~) convention for home directories is understood in this context.
Thus,
.sp
		% cd ~speech/data/fr+
.sp
does what one might expect.  This may also be used to expand login names only.
Thus,
.sp
		% cd ~sy+
.sp
does a 
.I cd
to the 
.I synthesis
directory.
.SH "2. FILE/DIRECTORY LIST"
At any point in typing a command, you may request "what files are available"
or "what files match my current specification".
Thus, when you have typed, perhaps:
.sp
		% cd ~speech/data/bench/fritz/
.sp
you may wish to know what files or subdirectories exist
(in ~speech/data/bench/fritz),
without, of course, aborting the command you are typing.
Typing the character Control-D at this point, will list the files available.
The files are listed in multicolumn format, sorted column-wise.
Directories and executable files are indicated with a trailing `/' and `*',
respectively.
Once printed, the command is re-echoed for you to complete.
.PP
Additionally, one may want to know which files match a prefix, the current file
specification so far.
If the user had typed:
.sp
		% cd ~speech/data/bench/fr
.sp
followed by a control-D, all files and subdirectories whose prefix was
``fr'' in the directory ~speech/data/bench would be printed.
Notice that the example before was simply
a degenerate case of this with a null trailing file name. 
(The null string is a prefix of all strings.)
Notice also, that
a trailing slash is required to pass to a \fInew\fR sub-directory for 
both file name completion and listing.
.PP
Notice, the degenerate case
.sp
		% ~^D
.sp
will print a full list of login names on the current system.
.SH "3. COMMAND NAME RECOGNITION"
Command name recognition and completion
works in the same manner as file name recognition
and completion above.
The current value of the environment variable \fBPATH\fR is used
in searching for the command.
For example
.sp
		% newa+
.sp
might expand to
.sp
		% newaliases
.sp
Also,
.sp
		% new^D
.sp
would list all commands (along PATH) that begin with "new".
.PP
As an option, if the shell variable \fIlistpathnum\fR is set, then
a number indicating the
index in PATH is printed next to each command on a ^D listing.
.SH "4. AUTO-LOGOUT"
A new shell variable has been added called \fIautologout\fB.
If the terminal remains idle (no character input) at the shell's
top level for the number of minutes greater than the autologout
value, you are automatically logged off.
This feature was added primarily for security reasons as people
often forget to log off when they leave, permitting anyone to walk by
and peruse your mail, or private files.
.PP
The autologout feature is temporarily disabled while a command is executing.
The initial value of \fIautologout\fB is 60.
If unset or set to 0, autologout is entirely disabled.
.PP
.SH "5. SANITY"
The shell will now restore your terminal to a sane mode if it appears to
return from some command in raw, cbreak, or noecho mode.
.PP
.SH "6. SAVEING HISTORY"
The shell now has the facility to save your history list between login session.
If the shell variable \fBsavehist\fR is set to a number
then that number of commands from your history list is saved.
For example, placing the line
.PP
	set history=50 savehist=50
.PP
in your .cshrc file maintains a history list of length 50 and 
saves the entire list when
you logout, to be retored when you login next.
The commands are saved in the file \fB.history\fR in your login directory.
.SH "F.Y.I."
This shell uses neither raw or cbreak mode.
It works by (temporarily) setting the "additional" tty break character to ESC.
There is no overhead usually associated by programs which run in
raw or cbreak mode.
.PP
If you select ESC as your default additonal break character, then
you will be able to do recognition on typeahead. 
.PP
The "vb" (visible bell) is used in place of the terminal bell if
it exists in the termcap entry for your terminal.
.SH FILES
/usr/local/lib/builtins/* - fake list of builtin commands
.sp
/etc/termcap - to discover vb (visible bell)
.SH SEE ALSO
csh (1)
.SH BUGS
A control-D on a blank line (a degenerate case of the List command)
logs you out, of course.  (But try <space><control-d>, which lists all
commands on the system along PATH).
.PP
It would be nice if you could change the command characters to something else
and not just be stuck with ESC and ^D.
.PP
Typing \fIimmediately\fR after hitting ESC
before recognition expansion completes
will result in character juxtaposition or loss.
.PP
Your terminal type is examined the first time you attempt recognition only.
.SH AUTHORS
Ken Greer, HP Labs; 
Mike Ellis, Fairchild, added command name recognition/completion.
//E*O*F newshell.1
echo x - newbgetc.c
cat << '//E*O*F newbgetc.c' > newbgetc.c
bgetc()
{
	register int buf, off, c;
	char ttyline[BUFSIZ];
	register int numleft = 0, roomleft;

#ifdef TELL
	if (cantell) {
		if (fseekp < fbobp || fseekp > feobp) {
			fbobp = feobp = fseekp;
			lseek(SHIN, fseekp, 0);
		}
		if (fseekp == feobp) {
			fbobp = feobp;
			do
			    c = read(SHIN, fbuf[0], BUFSIZ);
			while (c < 0 && errno == EINTR);
			if (c <= 0)
				return (-1);
			feobp += c;
		}
		c = fbuf[0][fseekp - fbobp];
		fseekp++;
		return (c);
	}
#endif
again:
	buf = (int) fseekp / BUFSIZ;
	if (buf >= fblocks) {
		register char **nfbuf = (char **) calloc(fblocks+2, sizeof (char **));

		if (fbuf) {
			blkcpy(nfbuf, fbuf);
			xfree((char *)fbuf);
		}
		fbuf = nfbuf;
		fbuf[fblocks] = calloc(BUFSIZ, sizeof (char));
		fblocks++;
		goto again;
	}
	if (fseekp >= feobp) {
		buf = (int) feobp / BUFSIZ;
		off = (int) feobp % BUFSIZ;
		roomleft = BUFSIZ - off;
		do
		    if (intty)			/* then use tenex routine */
		    {
			c = numleft ? numleft : tenex(ttyline, BUFSIZ);
			if (c > roomleft)	/* No room in this buffer? */
			{
			    /* start with fresh buffer */
			    feobp = fseekp = fblocks * BUFSIZ;
			    numleft = c;
			    goto again;
			}
			if (c > 0)
			    copy (fbuf[buf] + off, ttyline, c);
			numleft = 0;
		    }
		    else
			c = read(SHIN, fbuf[buf] + off, roomleft);
		while (c < 0 && errno == EINTR);
		if (c <= 0)
			return (-1);
		feobp += c;
		if (!intty)
		    goto again;
	}
	c = fbuf[buf][(int) fseekp % BUFSIZ];
	fseekp++;
	return (c);
}
//E*O*F newbgetc.c
echo x - sh.c
cat << '//E*O*F sh.c' > sh.c
static char *RCSid =
"$Header: /usr/local/src/cmd/tcsh/sh.c,v 1.4 83/10/05 22:26:25 kg Exp $";
static	char *sccsid = "@(#)sh.c 4.9 6/6/82";

#include "sh.h"
#include <sys/ioctl.h>
/*
 * C Shell
 *
 * Bill Joy, UC Berkeley, California, USA
 * October 1978, May 1980
 *
 * Jim Kulp, IIASA, Laxenburg, Austria
 * April 1980
 *
 * Filename recognition added:
 * Ken Greer, Ind. Consultant, Palo Alto CA
 * October 1983.
 */

char	*pathlist[] =	{ ".", "/usr/ucb", "/bin", "/usr/bin", 0 };
char	*dumphist[] =	{ "history", "-h", 0, 0 };
char	*loadhist[] =	{ "source", "-h", "~/.history", 0 };
char	HIST = '!';
char	HISTSUB = '^';
bool	nofile;
bool	reenter;
bool	nverbose;
bool	nexececho;
bool	quitit;
bool	fast;
bool	prompt = 1;
bool	enterhist = 0;

#define DEFAULT_AUTOLOGOUT	"60"	/* 1 Hour Alarm default */

auto_logout ()
{
    printf ("auto-logout\n");
    close (SHIN);
    child++;
    goodbye ();
}

main(c, av)
	int c;
	char **av;
{
	register char **v, *cp;
	register int f;

	settimes();			/* Immed. estab. timing base */
	v = av;
	if (eq(v[0], "a.out"))		/* A.out's are quittable */
		quitit = 1;
	uid = getuid();
	loginsh = **v == '-';
	if (loginsh)
		time(&chktim);

	/*
	 * Move the descriptors to safe places.
	 * The variable didfds is 0 while we have only FSH* to work with.
	 * When didfds is true, we have 0,1,2 and prefer to use these.
	 */
	initdesc();

	/*
	 * Initialize the shell variables.
	 * ARGV and PROMPT are initialized later.
	 * STATUS is also munged in several places.
	 * CHILD is munged when forking/waiting
	 */

	set("autologout", DEFAULT_AUTOLOGOUT);
	sigset (SIGALRM, auto_logout);
	set("status", "0");
	dinit(cp = getenv("HOME"));	/* dinit thinks that HOME == cwd in a
					 * login shell */
	if (cp == NOSTR)
		fast++;			/* No home -> can't read scripts */
	else
		set("home", savestr(cp));
	/*
	 * Grab other useful things from the environment.
	 * Should we grab everything??
	 */
	if ((cp = getenv("USER")) != NOSTR)
		set("user", savestr(cp));
	if ((cp = getenv("TERM")) != NOSTR)
		set("term", savestr(cp));
	/*
	 * Re-initialize path if set in environment
	 */
	if ((cp = getenv("PATH")) == NOSTR)
		set1("path", saveblk(pathlist), &shvhed);
	else {
		register unsigned i = 0;
		register char *dp;
		register char **pv;

		for (dp = cp; *dp; dp++)
			if (*dp == ':')
				i++;
		pv = (char **)calloc(i+2, sizeof (char **));
		for (dp = cp, i = 0; ;)
			if (*dp == ':') {
				*dp = 0;
				pv[i++] = savestr(*cp ? cp : ".");
				*dp++ = ':';
				cp = dp;
			} else if (*dp++ == 0) {
				pv[i++] = savestr(*cp ? cp : ".");
				break;
			}
		pv[i] = 0;
		set1("path", pv, &shvhed);
	}
	set("shell", SHELLPATH);

	doldol = putn(getpid());		/* For $$ */
	shtemp = strspl("/tmp/sh", doldol);	/* For << */

	/*
	 * Record the interrupt states from the parent process.
	 * If the parent is non-interruptible our hand must be forced
	 * or we (and our children) won't be either.
	 * Our children inherit termination from our parent.
	 * We catch it only if we are the login shell.
	 */
	parintr = signal(SIGINT, SIG_IGN);	/* parents interruptibility */
	sigset(SIGINT, parintr);			/* ... restore */
	parterm = signal(SIGTERM, SIG_IGN);	/* parents terminability */
	signal(SIGTERM, parterm);			/* ... restore */
	if (loginsh) {
		signal(SIGHUP, phup);		/* exit processing on HUP */
		signal(SIGXCPU, phup);		/* ...and on XCPU */
		signal(SIGXFSZ, phup);		/* ...and on XFSZ */
	}

	/*
	 * Process the arguments.
	 *
	 * Note that processing of -v/-x is actually delayed till after
	 * script processing.
	 *
	 * We set the first character of our name to be '-' if we are
	 * a shell running interruptible commands.  Many programs which
	 * examine ps'es use this to filter such shells out.
	 */
	c--, v++;
	while (c > 0 && (cp = v[0])[0] == '-') {
		do switch (*cp++) {

		case 0:			/* -	Interruptible, no prompt */
			prompt = 0;
			setintr++;
			nofile++;
			break;

		case 'c':		/* -c	Command input from arg */
			if (c == 1)
				exit(0);
			c--, v++;
			arginp = v[0];
			prompt = 0;
			nofile++;
			break;

		case 'e':		/* -e	Exit on any error */
			exiterr++;
			break;

		case 'f':		/* -f	Fast start */
			fast++;
			break;

		case 'i':		/* -i	Interactive, even if !intty */
			intact++;
			nofile++;
			break;

		case 'n':		/* -n	Don't execute */
			noexec++;
			break;

		case 'q':		/* -q	(Undoc'd) ... die on quit */
			quitit = 1;
			break;

		case 's':		/* -s	Read from std input */
			nofile++;
			break;

		case 't':		/* -t	Read one line from input */
			onelflg = 2;
			prompt = 0;
			nofile++;
			break;

		case 'v':		/* -v	Echo hist expanded input */
			nverbose = 1;			/* ... later */
			break;

		case 'x':		/* -x	Echo just before execution */
			nexececho = 1;			/* ... later */
			break;

		case 'V':		/* -V	Echo hist expanded input */
			setNS("verbose");		/* NOW! */
			break;

		case 'X':		/* -X	Echo just before execution */
			setNS("echo");			/* NOW! */
			break;

		} while (*cp);
		v++, c--;
	}

	if (quitit)			/* With all due haste, for debugging */
		signal(SIGQUIT, SIG_DFL);

	/*
	 * Unless prevented by -, -c, -i, -s, or -t, if there
	 * are remaining arguments the first of them is the name
	 * of a shell file from which to read commands.
	 */
	if (nofile == 0 && c > 0) {
		nofile = open(v[0], 0);
		if (nofile < 0) {
			child++;		/* So this ... */
			Perror(v[0]);		/* ... doesn't return */
		}
		file = v[0];
		SHIN = dmove(nofile, FSHIN);	/* Replace FSHIN */
		prompt = 0;
		c--, v++;
	}
	/*
	 * Consider input a tty if it really is or we are interactive.
	 */
	intty = intact || isatty(SHIN);
	/*
	 * Decide whether we should play with signals or not.
	 * If we are explicitly told (via -i, or -) or we are a login
	 * shell (arg0 starts with -) or the input and output are both
	 * the ttys("csh", or "csh</dev/ttyx>/dev/ttyx")
	 * Note that in only the login shell is it likely that parent
	 * may have set signals to be ignored
	 */
	if (loginsh || intact || intty && isatty(SHOUT))
		setintr = 1;
#ifdef TELL
	settell();
#endif
	/*
	 * Save the remaining arguments in argv.
	 */
	setq("argv", v, &shvhed);

	/*
	 * Set up the prompt.
	 */
	if (prompt)
		set("prompt", uid == 0 ? "# " : "% ");

	/*
	 * If we are an interactive shell, then start fiddling
	 * with the signals; this is a tricky game.
	 */
	shpgrp = getpgrp(0);
	opgrp = tpgrp = -1;
	oldisc = -1;
	if (setintr) {
		**av = '-';
		if (!quitit)		/* Wary! */
			signal(SIGQUIT, SIG_IGN);
		sigset(SIGINT, pintr);
		sighold(SIGINT);
		signal(SIGTERM, SIG_IGN);
		if (quitit == 0 && arginp == 0) {
			signal(SIGTSTP, SIG_IGN);
			signal(SIGTTIN, SIG_IGN);
			signal(SIGTTOU, SIG_IGN);
			/*
			 * Wait till in foreground, in case someone
			 * stupidly runs
			 *	csh &
			 * dont want to try to grab away the tty.
			 */
			if (isatty(FSHDIAG))
				f = FSHDIAG;
			else if (isatty(FSHOUT))
				f = FSHOUT;
			else if (isatty(OLDSTD))
				f = OLDSTD;
			else
				f = -1;
retry:
			if (ioctl(f, TIOCGPGRP, &tpgrp) == 0 && tpgrp != -1) {
				int ldisc;
				if (tpgrp != shpgrp) {
					int old = sigsys(SIGTTIN, SIG_DFL);
					kill(0, SIGTTIN);
					sigsys(SIGTTIN, old);
					goto retry;
				}
				if (ioctl(f, TIOCGETD, &oldisc) != 0) 
					goto notty;
				if (oldisc != NTTYDISC) {
#ifdef DEBUG
					printf("Switching to new tty driver...\n");
#endif DEBUG
					ldisc = NTTYDISC;
					ioctl(f, TIOCSETD, &ldisc);
				} else
					oldisc = -1;
				opgrp = shpgrp;
				shpgrp = getpid();
				tpgrp = shpgrp;
				ioctl(f, TIOCSPGRP, &shpgrp);
				setpgrp(0, shpgrp);
				dcopy(f, FSHTTY);
				ioctl(FSHTTY, FIOCLEX, 0);
			} else {
notty:
  printf("Warning: no access to tty; thus no job control in this shell...\n");
				tpgrp = -1;
			}
		}
	}
	if (setintr == 0 && parintr == SIG_DFL)
		setintr++;
	sigset(SIGCHLD, pchild);		/* while signals not ready */

	/*
	 * Set an exit here in case of an interrupt or error reading
	 * the shell start-up scripts.
	 */
	setexit();
	haderr = 0;		/* In case second time through */
	if (!fast && reenter == 0) {
		reenter++;
		/* Will have value("home") here because set fast if don't */
		srccat(value("home"), "/.cshrc");
		if (!fast && !arginp && !onelflg)
			dohash();
		dosource(loadhist);
		if (loginsh) {
			srccat(value("home"), "/.login");
		}
	}

	/*
	 * Now are ready for the -v and -x flags
	 */
	if (nverbose)
		setNS("verbose");
	if (nexececho)
		setNS("echo");

	/*
	 * All the rest of the world is inside this call.
	 * The argument to process indicates whether it should
	 * catch "error unwinds".  Thus if we are a interactive shell
	 * our call here will never return by being blown past on an error.
	 */
	process(setintr);

	/*
	 * Mop-up.
	 */
	if (loginsh) {
		printf("logout\n");
		close(SHIN);
		child++;
		goodbye();
	}
	rechist();
	exitstat();
}

untty()
{

	if (tpgrp > 0) {
		setpgrp(0, opgrp);
		ioctl(FSHTTY, TIOCSPGRP, &opgrp);
		if (oldisc != -1 && oldisc != NTTYDISC) {
#ifdef DEBUG
			printf("\nReverting to old tty driver...\n");
#endif DEBUG
			ioctl(FSHTTY, TIOCSETD, &oldisc);
		}
	}
}

importpath(cp)
char *cp;
{
	register int i = 0;
	register char *dp;
	register char **pv;
	int c;
	static char dot[2] = {'.', 0};

	for (dp = cp; *dp; dp++)
		if (*dp == ':')
			i++;
	/*
	 * i+2 where i is the number of colons in the path.
	 * There are i+1 directories in the path plus we need
	 * room for a zero terminator.
	 */
	pv = (char **) calloc(i+2, sizeof (char **));
	dp = cp;
	i = 0;
	if (*dp)
	for (;;) {
		if ((c = *dp) == ':' || c == 0) {
			*dp = 0;
			pv[i++] = savestr(*cp ? cp : dot);
			if (c) {
				cp = dp + 1;
				*dp = ':';
			} else
				break;
		}
		dp++;
	}
	pv[i] = 0;
	set1("path", pv, &shvhed);
}

/*
 * Source to the file which is the catenation of the argument names.
 */
srccat(cp, dp)
	char *cp, *dp;
{
	register char *ep = strspl(cp, dp);
	register int unit = dmove(open(ep, 0), -1);

	/* ioctl(unit, FIOCLEX, NULL); */
	xfree(ep);
#ifdef DONT_SOURCE_IF_I_DONT_OWN_IT
	srcunit(unit, 1, 0);
#else
	srcunit(unit, 0, 0);
#endif
}

/*
 * Source to a unit.  If onlyown it must be our file or our group or
 * we don't chance it.	This occurs on ".cshrc"s and the like.
 */
srcunit(unit, onlyown, hflg)
	register int unit;
	bool onlyown;
	bool hflg;
{
	/* We have to push down a lot of state here */
	/* All this could go into a structure */
	int oSHIN = -1, oldintty = intty;
	struct whyle *oldwhyl = whyles;
	char *ogointr = gointr, *oarginp = arginp;
	char *oevalp = evalp, **oevalvec = evalvec;
	int oonelflg = onelflg;
	bool oenterhist = enterhist;
	char OHIST = HIST;
#ifdef TELL
	bool otell = cantell;
#endif
	struct Bin saveB;

	/* The (few) real local variables */
	jmp_buf oldexit;
	int reenter;

	if (unit < 0)
		return;
	if (didfds)
		donefds();
	if (onlyown) {
		struct stat stb;

		if (fstat(unit, &stb) < 0 || (stb.st_uid != uid && stb.st_gid != getgid())) {
			close(unit);
			return;
		}
	}

	/*
	 * There is a critical section here while we are pushing down the
	 * input stream since we have stuff in different structures.
	 * If we weren't careful an interrupt could corrupt SHIN's Bin
	 * structure and kill the shell.
	 *
	 * We could avoid the critical region by grouping all the stuff
	 * in a single structure and pointing at it to move it all at
	 * once.  This is less efficient globally on many variable references
	 * however.
	 */
	getexit(oldexit);
	reenter = 0;
	if (setintr)
		sighold(SIGINT);
	setexit();
	reenter++;
	if (reenter == 1) {
		/* Setup the new values of the state stuff saved above */
		copy((char *)&saveB, (char *)&B, sizeof saveB);
		fbuf = (char **) 0;
		fseekp = feobp = fblocks = 0;
		oSHIN = SHIN, SHIN = unit, arginp = 0, onelflg = 0;
		intty = isatty(SHIN), whyles = 0, gointr = 0;
		evalvec = 0; evalp = 0;
		enterhist = hflg;
		if (enterhist)
			HIST = '\0';
		/*
		 * Now if we are allowing commands to be interrupted,
		 * we let ourselves be interrupted.
		 */
		if (setintr)
			sigrelse(SIGINT);
#ifdef TELL
		settell();
#endif
		process(0);		/* 0 -> blow away on errors */
	}
	if (setintr)
		sigrelse(SIGINT);
	if (oSHIN >= 0) {
		register int i;

		/* We made it to the new state... free up its storage */
		/* This code could get run twice but xfree doesn't care */
		for (i = 0; i < fblocks; i++)
			xfree(fbuf[i]);
		xfree((char *)fbuf);

		/* Reset input arena */
		copy((char *)&B, (char *)&saveB, sizeof B);

		close(SHIN), SHIN = oSHIN;
		arginp = oarginp, onelflg = oonelflg;
		evalp = oevalp, evalvec = oevalvec;
		intty = oldintty, whyles = oldwhyl, gointr = ogointr;
		if (enterhist)
			HIST = OHIST;
		enterhist = oenterhist;
#ifdef TELL
		cantell = otell;
#endif
	}

	resexit(oldexit);
	/*
	 * If process reset() (effectively an unwind) then
	 * we must also unwind.
	 */
	if (reenter >= 2)
		error(NOSTR);
}

rechist()
{
	char buf[BUFSIZ];
	int fp, ftmp, oldidfds;

	if (!fast) {
		if (value("savehist")[0] == '\0')
			return;
		strcpy(buf, value("home"));
		strcat(buf, "/.history");
		fp = creat(buf, 0777);
		if (fp == -1)
			return;
		oldidfds = didfds;
		didfds = 0;
		ftmp = SHOUT;
		SHOUT = fp;
		strcpy(buf, value("savehist"));
		dumphist[2] = buf;
		dohist(dumphist);
		close(fp);
		SHOUT = ftmp;
		didfds = oldidfds;
	}
}

goodbye()
{
	if (loginsh) {
		signal(SIGQUIT, SIG_IGN);
		sigset(SIGINT, SIG_IGN);
		signal(SIGTERM, SIG_IGN);
		setintr = 0;		/* No interrupts after "logout" */
		if (adrof("home"))
			srccat(value("home"), "/.logout");
	}
	rechist();
	exitstat();
}

exitstat()
{

	/*
	 * Note that if STATUS is corrupted (i.e. getn bombs)
	 * then error will exit directly because we poke child here.
	 * Otherwise we might continue unwarrantedly (sic).
	 */
	child++;
	exit(getn(value("status")));
}

/*
 * in the event of a HUP we want to save the history
 */
phup()
{
	rechist();
	exit(1);
}

char	*jobargv[2] = { "jobs", 0 };
/*
 * Catch an interrupt, e.g. during lexical input.
 * If we are an interactive shell, we reset the interrupt catch
 * immediately.  In any case we drain the shell output,
 * and finally go through the normal error mechanism, which
 * gets a chance to make the shell go away.
 */
pintr()
{
	pintr1(1);
}

pintr1(wantnl)
	bool wantnl;
{
	register char **v;

	if (setintr) {
		sigrelse(SIGINT);
		if (pjobs) {
			pjobs = 0;
			printf("\n");
			dojobs(jobargv);
			bferr("Interrupted");
		}
	}
	if (setintr)
		sighold(SIGINT);
	sigrelse(SIGCHLD);
	draino();

	/*
	 * If we have an active "onintr" then we search for the label.
	 * Note that if one does "onintr -" then we shan't be interruptible
	 * so we needn't worry about that here.
	 */
	if (gointr) {
		search(ZGOTO, 0, gointr);
		timflg = 0;
		if (v = pargv)
			pargv = 0, blkfree(v);
		if (v = gargv)
			gargv = 0, blkfree(v);
		reset();
	} else if (intty && wantnl)
		printf("\n");		/* Some like this, others don't */
	error(NOSTR);
}

/*
 * Process is the main driving routine for the shell.
 * It runs all command processing, except for those within { ... }
 * in expressions (which is run by a routine evalav in sh.exp.c which
 * is a stripped down process), and `...` evaluation which is run
 * also by a subset of this code in sh.glob.c in the routine backeval.
 *
 * The code here is a little strange because part of it is interruptible
 * and hence freeing of structures appears to occur when none is necessary
 * if this is ignored.
 *
 * Note that if catch is not set then we will unwind on any error.
 * If an end-of-file occurs, we return.
 */
process(catch)
	bool catch;
{
	register char *cp;
	jmp_buf osetexit;
	struct command *t;

	getexit(osetexit);
	for (;;) {
		pendjob();
		paraml.next = paraml.prev = &paraml;
		paraml.word = "";
		t = 0;
		setexit();
		justpr = enterhist;	/* execute if not entering history */

		/*
		 * Interruptible during interactive reads
		 */
		if (setintr)
			sigrelse(SIGINT);

		/*
		 * For the sake of reset()
		 */
		freelex(&paraml), freesyn(t), t = 0;

		if (haderr) {
			if (!catch) {
				/* unwind */
				doneinp = 0;
				resexit(osetexit);
				reset();
			}
			haderr = 0;
			/*
			 * Every error is eventually caught here or
			 * the shell dies.  It is at this
			 * point that we clean up any left-over open
			 * files, by closing all but a fixed number
			 * of pre-defined files.  Thus routines don't
			 * have to worry about leaving files open due
			 * to deeper errors... they will get closed here.
			 */
			closem();
			continue;
		}
		if (doneinp) {
			doneinp = 0;
			break;
		}
		if (chkstop)
			chkstop--;
		if (neednote)
			pnote();
		if (intty && evalvec == 0) {
			mailchk();
			/*
			 * If we are at the end of the input buffer
			 * then we are going to read fresh stuff.
			 * Otherwise, we are rereading input and don't
			 * need or want to prompt.
			 */
			if (fseekp == feobp)
				printprompt ();
			flush();
			if (cp = value("autologout"))
			    alarm (atoi (cp) * 60);	/* Autologout ON */
		}
		err = 0;

		/*
		 * Echo not only on VERBOSE, but also with history expansion.
		 * If there is a lexical error then we forego history echo.
		 */
		if (lex(&paraml) && !err && intty ||
		    adrof("verbose")) {
			haderr = 1;
			prlex(&paraml);
			haderr = 0;
		}
		alarm (0);				/* Autologout OFF */

		/*
		 * The parser may lose space if interrupted.
		 */
		if (setintr)
			sighold(SIGINT);

		/*
		 * Save input text on the history list if 
		 * reading in old history, or it
		 * is from the terminal at the top level and not
		 * in a loop.
		 */
		if (enterhist || catch && intty && !whyles)
			savehist(&paraml);

		/*
		 * Print lexical error messages, except when sourcing
		 * history lists.
		 */
		if (!enterhist && err)
			error(err);

		/*
		 * If had a history command :p modifier then
		 * this is as far as we should go
		 */
		if (justpr)
			reset();

		alias(&paraml);

		/*
		 * Parse the words of the input into a parse tree.
		 */
		t = syntax(paraml.next, &paraml, 0);
		if (err)
			error(err);

		/*
		 * Execute the parse tree
		 */
		execute(t, tpgrp);

		/*
		 * Made it!
		 */
		freelex(&paraml), freesyn(t);
	}
	resexit(osetexit);
}

dosource(t)
	register char **t;
{
	register char *f;
	register int u;
	bool hflg = 0;
	char buf[BUFSIZ];

	t++;
	if (*t && eq(*t, "-h")) {
		t++;
		hflg++;
	}
	strcpy(buf, *t);
	f = globone(buf);
	u = dmove(open(f, 0), -1);
	xfree(f);
	if (u < 0 && !hflg)
		Perror(f);
	srcunit(u, 0, hflg);
}

/*
 * Check for mail.
 * If we are a login shell, then we don't want to tell
 * about any mail file unless its been modified
 * after the time we started.
 * This prevents us from telling the user things he already
 * knows, since the login program insists on saying
 * "You have mail."
 */
mailchk()
{
	register struct varent *v;
	register char **vp;
	time_t t;
	int intvl, cnt;
	struct stat stb;
	bool new;

	v = adrof("mail");
	if (v == 0)
		return;
	time(&t);
	vp = v->vec;
	cnt = blklen(vp);
	intvl = (cnt && number(*vp)) ? (--cnt, getn(*vp++)) : MAILINTVL;
	if (intvl < 1)
		intvl = 1;
	if (chktim + intvl > t)
		return;
	for (; *vp; vp++) {
		if (stat(*vp, &stb) < 0)
			continue;
		new = stb.st_mtime > time0;
		if (stb.st_size == 0 || stb.st_atime > stb.st_mtime ||
		    (stb.st_atime < chktim && stb.st_mtime < chktim) ||
		    loginsh && !new)
			continue;
		if (cnt == 1)
			printf("You have %smail.\n", new ? "new " : "");
		else
			printf("%s in %s.\n", new ? "New mail" : "Mail", *vp);
	}
	chktim = t;
}

#include <pwd.h>
/*
 * Extract a home directory from the password file
 * The argument points to a buffer where the name of the
 * user whose home directory is sought is currently.
 * We write the home directory of the user back there.
 */
gethdir(home)
	char *home;
{
	register struct passwd *pp = getpwnam(home);

	if (pp == 0)
		return (1);
	strcpy(home, pp->pw_dir);
	return (0);
}

/*
 * Move the initial descriptors to their eventual
 * resting places, closin all other units.
 */
initdesc()
{

	didcch = 0;			/* Havent closed for child */
	didfds = 0;			/* 0, 1, 2 aren't set up */
	SHIN = dcopy(0, FSHIN);
	SHOUT = dcopy(1, FSHOUT);
	SHDIAG = dcopy(2, FSHDIAG);
	OLDSTD = dcopy(SHIN, FOLDSTD);
	closem();
}

exit(i)
	int i;
{

	untty();
#ifdef PROF
	IEH3exit(i);
#else
	_exit(i);
#endif
}

printprompt ()
{
    register char *cp;
    if (!whyles)
	for (cp = value ("prompt"); *cp; cp++)
	    if (*cp == HIST)
		printf ("%d", eventno + 1);
	    else
	    {
		if (*cp == '\\' && cp[1] == HIST)
		    cp++;
		putchar (*cp | QUOTE);
	    }
    else
    /* 
     * Prompt for forward reading loop
     * body content.
     */
	printf ("? ");
    flush ();
}
//E*O*F sh.c
echo x - sh.hist.c
cat << '//E*O*F sh.hist.c' > sh.hist.c
static	char *sccsid = "@(#)sh.hist.c 4.3 11/19/81";

#include "sh.h"

/*
 * C shell
 */

savehist(sp)
	struct wordent *sp;
{
	register struct Hist *hp, *np;
	int histlen;
	register char *cp;

	cp = value("history");
	if (*cp == 0)
		histlen = 0;
	else {
		while (*cp && digit(*cp))
			cp++;
		/* avoid a looping snafu */
		if (*cp)
			set("history", "10");
		histlen = getn(value("history"));
	}
	/* throw away null lines */
	if (sp->next->word[0] == '\n')
		return;
	for (hp = &Histlist; np = hp->Hnext;)
		if (eventno - np->Href >= histlen || histlen == 0)
			hp->Hnext = np->Hnext, hfree(np);
		else
			hp = np;
	enthist(++eventno, sp, 1);
}

struct Hist *
enthist(event, lp, docopy)
	int event;
	register struct wordent *lp;
	bool docopy;
{
	register struct Hist *np;

	np = (struct Hist *) calloc(1, sizeof *np);
	np->Hnum = np->Href = event;
	if (docopy)
		copylex(&np->Hlex, lp);
	else {
		np->Hlex.next = lp->next;
		lp->next->prev = &np->Hlex;
		np->Hlex.prev = lp->prev;
		lp->prev->next = &np->Hlex;
	}
	np->Hnext = Histlist.Hnext;
	Histlist.Hnext = np;
	return (np);
}

hfree(hp)
	register struct Hist *hp;
{

	freelex(&hp->Hlex);
	xfree((char *)hp);
}

dohist(vp)
	char **vp;
{
	int n, rflg = 0, hflg = 0;
	if (getn(value("history")) == 0)
		return;
	if (setintr)
		sigrelse(SIGINT);
	vp++;
	while (*vp[0] == '-') {
		if (*vp && eq(*vp, "-h")) {
			hflg++;
			vp++;
		}
		if (*vp && eq(*vp, "-r")) {
			rflg++;
			vp++;
		}
	}
	if (*vp)
		n = getn(*vp);
	else {
		n = getn(value("history"));
	}
	dohist1(Histlist.Hnext, &n, rflg, hflg);
}

dohist1(hp, np, rflg, hflg)
	struct Hist *hp;
	int *np, rflg, hflg;
{
	bool print = (*np) > 0;
top:
	if (hp == 0)
		return;
	(*np)--;
	hp->Href++;
	if (rflg == 0) {
		dohist1(hp->Hnext, np, rflg, hflg);
		if (print)
			phist(hp, hflg);
		return;
	}
	if (*np >= 0)
		phist(hp, hflg);
	hp = hp->Hnext;
	goto top;
}

phist(hp, hflg)
	register struct Hist *hp;
	int hflg;
{

	if (hflg == 0)
		printf("%6d\t", hp->Hnum);
	prlex(&hp->Hlex);
}

//E*O*F sh.hist.c
echo x - dir.h
cat << '//E*O*F dir.h' > dir.h
/* Copyright (c) 1982 Regents of the University of California */

/* @(#)ndir.h 4.4 3/30/82 */

/*
 * This sets the "page size" for directories.
 * Requirements are DEV_BSIZE <= DIRBLKSIZ <= MINBSIZE with
 * DIRBLKSIZ a power of two.
 * Dennis Ritchie feels that directory pages should be atomic
 * operations to the disk, so we use DEV_BSIZE.
 */
#define DIRBLKSIZ 512

/*
 * This limits the directory name length. Its main constraint
 * is that it appears twice in the user structure. (u. area)
 */
#define MAXNAMLEN 255

struct	direct {
	u_long	d_ino;
	short	d_reclen;
	short	d_namlen;
	char	d_name[MAXNAMLEN + 1];
	/* typically shorter */
};

struct _dirdesc {
	int	dd_fd;
	long	dd_loc;
	long	dd_size;
	char	dd_buf[DIRBLKSIZ];
};

/*
 * useful macros.
 */
#undef DIRSIZ
#define DIRSIZ(dp) \
    ((sizeof(struct direct) - MAXNAMLEN + (dp)->d_namlen + sizeof(ino_t) - 1) &\
    ~(sizeof(ino_t) - 1))
typedef	struct _dirdesc DIR;
#ifndef	NULL
#define	NULL	0
#endif

/*
 * functions defined on directories
 */
extern DIR *opendir();
extern struct direct *readdir();
extern long telldir();
extern void seekdir();
#define rewinddir(dirp)	seekdir((dirp), 0)
extern void closedir();
//E*O*F dir.h
echo x - dir14.c
cat << '//E*O*F dir14.c' > dir14.c
/* Copyright (c) 1982 Regents of the University of California */

/* @(#)opendir.c 4.2.1.1 7/1/82 - HPL */

#include <sys/types.h>
#include <sys/stat.h>
#include <dir.h>

/*
 * open a directory.
 */
DIR *
opendir(name)
	char *name;
{
	register DIR *dirp;
	register int fd;
	struct stat sbuf;

	if ((fd = open(name, 0)) == -1)
		return NULL;
	fstat(fd, &sbuf);
	if (((sbuf.st_mode & S_IFDIR) == 0) ||
	    ((dirp = (DIR *)malloc(sizeof(DIR))) == NULL)) {
		close (fd);
		return NULL;
	}
	dirp->dd_fd = fd;
	dirp->dd_loc = 0;
	return dirp;
}
/* Copyright (c) 1982 Regents of the University of California */

/* @(#)closedir.c 4.2 3/10/82 */

/*
 * close a directory.
 */
void
closedir(dirp)
	register DIR *dirp;
{
	close(dirp->dd_fd);
	dirp->dd_fd = -1;
	dirp->dd_loc = 0;
	free(dirp);
}
/* Copyright (c) 1982 Regents of the University of California */

/* @(#)readdir.c 4.2 3/12/82 */

/*
 * read an old stlye directory entry and present it as a new one
 */
#define	ODIRSIZ	14

struct	olddirect {
	ino_t	d_ino;
	char	d_name[ODIRSIZ];
};

/*
 * get next entry in a directory.
 */
struct direct *
readdir(dirp)
	register DIR *dirp;
{
	register struct olddirect *dp;
	static struct direct dir;

	for (;;) {
		if (dirp->dd_loc == 0) {
			dirp->dd_size = read(dirp->dd_fd, dirp->dd_buf, 
			    DIRBLKSIZ);
			if (dirp->dd_size <= 0)
				return NULL;
		}
		if (dirp->dd_loc >= dirp->dd_size) {
			dirp->dd_loc = 0;
			continue;
		}
		dp = (struct olddirect *)(dirp->dd_buf + dirp->dd_loc);
		dirp->dd_loc += sizeof(struct olddirect);
		if (dp->d_ino == 0)
			continue;
		dir.d_ino = dp->d_ino;
		strncpy(dir.d_name, dp->d_name, ODIRSIZ);
		dir.d_name[ODIRSIZ] = '\0'; /* insure null termination */
		dir.d_namlen = strlen(dir.d_name);
		dir.d_reclen = DIRSIZ(&dir);
		return (&dir);
	}
}
//E*O*F dir14.c
echo x - tenex.c
cat << '//E*O*F tenex.c' > tenex.c
static char *RCSid = 
"$Header: /usr/local/src/cmd/tcsh/tenex.c,v 1.9 83/10/05 21:56:27 kg Exp $";

/*
 * Tenex style file name recognition, .. and more.
 * History:
 *	Author: Ken Greer, Sept. 1975, CMU.
 *	Finally got around to adding to the Cshell., Ken Greer, Dec. 1981.
 *
 *	Search and recognition of command names (in addition to file names)
 *	by Mike Ellis, Fairchild A.I. Labs, Sept 1983.
 *
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <sgtty.h>
#include <dir.h>
#include <signal.h>
#include <pwd.h>
/* Don't include stdio.h!  Csh doesn't like it!! */
#ifdef TEST
#include <stdio.h>
#include "dir.h"
#define flush()		fflush(stdout)
#endif

#define TRUE		1
#define FALSE		0
#define ON		1
#define OFF		0
#define FILSIZ		512		/* Max reasonable file name length */
#define ESC		'\033'
#define equal(a, b)	(strcmp(a, b) == 0)
#define is_set(var)	adrof(var)
#define BUILTINS	"/usr/local/lib/builtins/" /* fake builtin bin */

extern short SHIN, SHOUT;
extern char *getenv ();
extern putchar ();

typedef enum {LIST, RECOGNIZE} COMMAND;

static char
    *BELL = "\07";

static
setup_tty (on)
{
    static struct tchars  tchars;	/* INT, QUIT, XON, XOFF, EOF, BRK */
    static char save_t_brkc = -1;	/* Save user's break character */

    sigignore (SIGINT);
    if (on)
    {
	struct sgttyb sgtty;

	ioctl (SHIN, TIOCGETC, &tchars);	/* Get current break character*/
	save_t_brkc = tchars.t_brkc;		/* Current break char, if any */
	if (save_t_brkc != ESC)			/* If it's not already ESCAPE */
	{
	    tchars.t_brkc = ESC;		/* Set break char to ESCAPE */
	    ioctl (SHIN, TIOCSETC, &tchars);
	}

	/*
	 * This is a useful feature in it's own right...
	 * The shell makes sure that the tty is not in some weird state
	 * and fixes it if it is.  But it should be noted that the
	 * tenex routine will not work correctly in CBREAK or RAW mode
	 * so this code below is, therefore, mandatory.
	 */
	ioctl (SHIN, TIOCGETP, &sgtty);
	if ((sgtty.sg_flags & (RAW | CBREAK)) ||
	   ((sgtty.sg_flags & ECHO) == 0))	/* not manditory, but nice */
	{
	    sgtty.sg_flags &= ~(RAW | CBREAK);
	    sgtty.sg_flags |= ECHO;
	    ioctl (SHIN, TIOCSETP, &sgtty);
	}
    }
    else
    {
	/*
	 * Reset break character to what user had when invoked
	 * (providing it is different from current one)
	 */
	if (save_t_brkc != tchars.t_brkc)
	{
	    tchars.t_brkc = save_t_brkc;
	    ioctl (SHIN, TIOCSETC, &tchars);
	}
    }
    sigrelse (SIGINT);
}

static
termchars ()
{
    extern char *tgetstr ();
    char bp[1024];
    static char area[256];
    static int been_here = 0;
    char *ap = area;
    register char *s;

    if (been_here)
	return;
    been_here = TRUE;

    if (tgetent (bp, getenv ("TERM")) != 1)
        return;
    if (s = tgetstr ("vb", &ap))		/* Visible Bell */
	BELL = s;
    return;
}

/*
 * Move back to beginning of current line
 */
static
back_to_col_1 ()
{
    struct sgttyb tty, tty_normal;
    sigignore (SIGINT);
    ioctl (SHIN, TIOCGETP, &tty);
    tty_normal = tty;
    tty.sg_flags &= ~CRMOD;
    ioctl (SHIN, TIOCSETN, &tty);
    (void) write (SHOUT, "\r", 1);
    ioctl (SHIN, TIOCSETN, &tty_normal);
    sigrelse (SIGINT);
}

/*
 * Push string contents back into tty queue
 */
static
pushback (string)
char  *string;
{
    register char  *p;
    struct sgttyb   tty, tty_normal;

    sigignore (SIGINT);
    ioctl (SHOUT, TIOCGETP, &tty);
    tty_normal = tty;
    tty.sg_flags &= ~ECHO;
    ioctl (SHOUT, TIOCSETN, &tty);

    for (p = string; *p; p++)
	ioctl (SHOUT, TIOCSTI, p);
    ioctl (SHOUT, TIOCSETN, &tty_normal);
    sigrelse (SIGINT);
}

/*
 * Concatonate src onto tail of des.
 * Des is a string whose maximum length is count.
 * Always null terminate.
 */
catn (des, src, count)
register char *des, *src;
register count;
{
    while (--count >= 0 && *des)
	des++;
    while (--count >= 0)
	if ((*des++ = *src++) == 0)
	    return;
    *des = '\0';
}

static
max (a, b)
{
    if (a > b)
	return (a);
    return (b);
}

/*
 * like strncpy but always leave room for trailing \0
 * and always null terminate.
 */
copyn (des, src, count)
register char *des, *src;
register count;
{
    while (--count >= 0)
	if ((*des++ = *src++) == 0)
	    return;
    *des = '\0';
}

/*
 * For qsort()
 */
static
fcompare (file1, file2)
char  **file1, **file2;
{
    return (strcmp (*file1, *file2));
}

static char
filetype (dir, file)
char *dir, *file;
{
    if (dir)
    {
	char path[512];
	struct stat statb;
	strcpy (path, dir);
	catn (path, file, sizeof path);
	if (stat (path, &statb) >= 0)
	{
	    if (statb.st_mode & S_IFDIR)
		return ('/');
	    if (statb.st_mode & 0111)
		return ('*');
	}
    }
    return (' ');
}

/*
 * Print sorted down columns
 */
static
print_by_column (dir, items, count, looking_for_command)
register char *dir, *items[];
{
    register int i, rows, r, c, maxwidth = 0, columns;
    for (i = 0; i < count; i++)
	maxwidth = max (maxwidth, strlen (items[i]));
    maxwidth += looking_for_command ? 1:2;	/* for the file tag and space */
    columns = 80 / maxwidth;
    rows = (count + (columns - 1)) / columns;
    for (r = 0; r < rows; r++)
    {
	for (c = 0; c < columns; c++)
	{
	    i = c * rows + r;
	    if (i < count)
	    {
		register int w;
		printf("%s", items[i]);
		w = strlen (items[i]);
		/* Print filename followed by '/' or '*' or ' ' */
		if (!looking_for_command)
			putchar (filetype (dir, items[i])), w++;
		if (c < (columns - 1))			/* Not last column? */
		    for (; w < maxwidth; w++)
			putchar (' ');
	    }
	}
	printf ("\n");
    }
}

/*
 * expand "old" file name with possible tilde usage
 *		~person/mumble
 * expands to
 *		home_directory_of_person/mumble
 * into string "new".
 */

char *
tilde (new, old)
char *new, *old;
{
    extern struct passwd *getpwuid (), *getpwnam ();

    register char *o, *p;
    register struct passwd *pw;
    static char person[40] = {0};

    if (old[0] != '~')
    {
	strcpy (new, old);
	return (new);
    }

    for (p = person, o = &old[1]; *o && *o != '/'; *p++ = *o++);
    *p = '\0';

    if (person[0] == '\0')			/* then use current uid */
	pw = getpwuid (getuid ());
    else
	pw = getpwnam (person);

    if (pw == NULL)
	return (NULL);

    strcpy (new, pw -> pw_dir);
    (void) strcat (new, o);
    return (new);
}

/*
 * Cause pending line to be printed
 */
static
retype ()
{
    int     pending_input = LPENDIN;
    ioctl (SHOUT, TIOCLBIS, &pending_input);
}

static
beep ()
{
    (void) write (SHOUT, BELL, strlen(BELL));
}


/*
 * parse full path in file into 2 parts: directory and file names
 * Should leave final slash (/) at end of dir.
 */
static
extract_dir_and_name (path, dir, name)
char   *path, *dir, *name;
{
    extern char *rindex ();
    register char  *p;
    p = rindex (path, '/');
    if (p == NULL)
    {
	copyn (name, path, MAXNAMLEN);
	dir[0] = '\0';
    }
    else
    {
	p++;
	copyn (name, p, MAXNAMLEN);
	copyn (dir, path, p - path);
    }
}


char *
getentry (dir_fd, looking_for_lognames)
DIR *dir_fd;
{
    if (looking_for_lognames)			/* Is it login names we want? */
    {
	extern struct passwd *getpwent ();
	register struct passwd *pw;
	if ((pw = getpwent ()) == NULL)
	    return (NULL);
	return (pw -> pw_name);
    }
    else					/* It's a dir entry we want */
    {
	register struct direct *dirp;
	if (dirp = readdir (dir_fd))
	    return (dirp -> d_name);
	return (NULL);
    }
}

static
free_items (items)
register char **items;
{
    register int i;
    for (i = 0; items[i]; i++)
	free (items[i]);
    free (items);
}

#define FREE_ITEMS(items)\
{\
    sighold (SIGINT);\
    free_items (items);\
    items = NULL;\
    sigrelse (SIGINT);\
}

#define FREE_DIR(fd)\
{\
    sighold (SIGINT);\
    closedir (fd);\
    fd = NULL;\
    sigrelse (SIGINT);\
}

static int  dirctr;		/* -1 0 1 2 ... */
static char dirflag[5];		/*  ' nn\0' - dir #s -  . 1 2 ... */

/*
 * Strip next directory from path; return ptr to next unstripped directory.
 */
 
char *extract_dir_from_path (path, dir)
char *path, dir[];
{
    register char *d = dir;

    while (*path && (*path == ' ' || *path == ':')) path++;
    while (*path && (*path != ' ' && *path != ':')) *(d++) = *(path++);
    while (*path && (*path == ' ' || *path == ':')) path++;

    ++dirctr;
    if (*dir == '.')
        strcpy (dirflag, " .");
    else
    {
        dirflag[0] = ' ';
	if (dirctr <= 9)
	{
		dirflag[1] = '0' + dirctr;
		dirflag[2] = '\0';
	}
	else
	{
		dirflag[1] = '0' + dirctr / 10;
		dirflag[2] = '0' + dirctr % 10;
		dirflag[3] = '\0';
	}
    }
    *(d++) = '/';
    *d = 0;

    return path;
}

/*
 * Perform a RECOGNIZE or LIST command on string "word".
 */
static
search (word, wp, command, routine, max_word_length, looking_for_command)
char   *word,
       *wp;			/* original end-of-word */
COMMAND command;
int (*routine) ();
{
#   define MAXITEMS 2048
    register numitems,
	    name_length,		/* Length of prefix (file name) */
	    looking_for_lognames;	/* True if looking for login names */
    int	    showpathn;			/* True if we want path number */
    struct stat
	    dot_statb,			/* Stat buffer for "." */
	    curdir_statb;		/* Stat buffer for current directory */
    int	    dot_scan,			/* True if scanning "." */
	    dot_got;			/* True if have scanned dot already */
    char    tilded_dir[FILSIZ + 1],	/* dir after ~ expansion */
	    dir[FILSIZ + 1],		/* /x/y/z/ part in /x/y/z/f */
            name[MAXNAMLEN + 1],	/* f part in /d/d/d/f */
            extended_name[MAXNAMLEN+1],	/* the recognized (extended) name */
            *entry,			/* single directory entry or logname */
	    *path;			/* hacked PATH environment variable */
    static DIR 
	    *dir_fd = NULL;
    static char
           **items = NULL;		/* file names when doing a LIST */

    if (items != NULL)
	FREE_ITEMS (items);
    if (dir_fd != NULL)
	FREE_DIR (dir_fd);

    looking_for_lognames = (*word == '~') && (index (word, '/') == NULL);
    looking_for_command &= (*word != '~') && (index (word, '/') == NULL);

    if (looking_for_command)
    {
        copyn (name, word, MAXNAMLEN);
        if ((path = getenv ("PATH")) == NULL)
	    path = "";
	/* setup builtins as 1st to search before PATH */
	copyn (dir, BUILTINS, sizeof dir);

	dirctr = -1;		/* BUILTINS -1 */
	dirflag[0] = 0;
    }
    numitems = 0;

    dot_got = FALSE;
    stat (".", &dot_statb);

cmdloop:	/* One loop per directory in PATH, if looking_for_command */

    if (looking_for_lognames)			/* Looking for login names? */
    {
	setpwent ();				/* Open passwd file */
	copyn (name, &word[1], MAXNAMLEN);	/* name sans ~ */
    }
    else
    {						/* Open directory */
        if (!looking_for_command)
	    extract_dir_and_name (word, dir, name);
	if ((tilde (tilded_dir, dir) == 0) ||	/* expand ~user/... stuff */
	    
	   ((dir_fd = opendir (*tilded_dir ? tilded_dir : ".")) == NULL))
	{
	    if (looking_for_command)
	        goto try_next_path;
	    else
		return (0);
	}
	dot_scan = FALSE;
	if (looking_for_command)
	{
	    /*
	     * Are we searching "."?
	     */
	    fstat (dir_fd->dd_fd, &curdir_statb);
	    if (curdir_statb.st_dev == dot_statb.st_dev &&
	        curdir_statb.st_ino == dot_statb.st_ino)
	    {
	        if (dot_got)			/* Second time in PATH? */
			goto try_next_path;
		dot_scan = TRUE;
		dot_got = TRUE;
	    }
	}
    }

    name_length = strlen (name);
    showpathn = looking_for_command && is_set("listpathnum");

    while (entry = getentry (dir_fd, looking_for_lognames))
    {
	if (!is_prefix (name, entry))
	    continue;

	/*
	 * Don't match . files on null prefix match
	 */
	if (name_length == 0 && entry[0] == '.' && !looking_for_lognames)
	    continue;

	/*
	 * Skip non-executables if looking for commands:
	 * Only done for directory "." for speed.
	 * (Benchmarked with and without:
	 * With filetype check, a full search took 10 seconds.
	 * Without filetype check, a full search took 1 second.)
	 *                                   -Ken Greer
         */
	if (looking_for_command && dot_scan && filetype (dir, entry) != '*')
	    continue;

	if (command == LIST)		/* LIST command */
	{
	    extern char *malloc ();
	    register int length;
	    if (numitems >= MAXITEMS)
	    {
		printf ("\nYikes!! Too many %s!!\n",
		    looking_for_lognames ? "names in password file":"files");
		break;
	    }
	    if (items == NULL)
	    {
		items = (char **) calloc (sizeof (items[1]), MAXITEMS + 1);
		if (items == NULL)
		    break;
	    }
	    length = strlen(entry) + 1;
	    if (showpathn)
		length += strlen(dirflag);
	    if ((items[numitems] = malloc (length)) == NULL)
	    {
		printf ("out of mem\n");
		break;
	    }
	    copyn (items[numitems], entry, MAXNAMLEN);
	    if (showpathn)
	        catn (items[numitems], dirflag, MAXNAMLEN);
	    numitems++;
	}
	else					/* RECOGNIZE command */
	    if (recognize (extended_name, entry, name_length, ++numitems))
		break;
    }

    if (looking_for_lognames)
	endpwent ();
    else
	FREE_DIR (dir_fd);

try_next_path:
    if (looking_for_command && *path &&
    	(path = extract_dir_from_path (path, dir), dir)) 
    	goto cmdloop;
    
    if (command == RECOGNIZE && numitems > 0)
    {
	if (looking_for_lognames)
	    copyn (word, "~", 1);
	else if (looking_for_command)
	    word[0] = 0;
	else
	    copyn (word, dir, max_word_length);		/* put back dir part */
	catn (word, extended_name, max_word_length);	/* add extended name */
	while (*wp) (*routine) (*wp++);
	return (numitems);
    }

    if (command == LIST)
    {
	qsort (items, numitems, sizeof (items[1]), fcompare);
	print_by_column (looking_for_lognames ? NULL:tilded_dir, items,
			 numitems, looking_for_command);
	if (items != NULL)
	    FREE_ITEMS (items);
    }
    return (0);
}

/*
 * Object: extend what user typed up to an ambiguity.
 * Algorithm:
 * On first match, copy full entry (assume it'll be the only match) 
 * On subsequent matches, shorten extended_name to the first
 * character mismatch between extended_name and entry.
 * If we shorten it back to the prefix length, stop searching.
 */
recognize (extended_name, entry, name_length, numitems)
char *extended_name, *entry;
{
    if (numitems == 1)				/* 1st match */
	copyn (extended_name, entry, MAXNAMLEN);
    else					/* 2nd and subsequent matches */
    {
	register char *x, *ent;
	register int len = 0;
	for (x = extended_name, ent = entry; *x && *x == *ent++; x++, len++);
	*x = '\0';				/* Shorten at 1st char diff */
	if (len == name_length)			/* Ambiguous to prefix? */
	    return (-1);			/* So stop now and save time */
    }
    return (0);
}

/*
 * return true if check items initial chars in template
 * This differs from PWB imatch in that if check is null
 * it items anything
 */
static
is_prefix (check, template)
char   *check,
       *template;
{
    register char  *check_char,
                   *template_char;

    check_char = check;
    template_char = template;
    do
	if (*check_char == 0)
	    return (TRUE);
    while (*check_char++ == *template_char++);
    return (FALSE);
}

starting_a_command (wordstart, inputline)
register char *wordstart, *inputline;
{
    static char
	    cmdstart[] = ";&(|`",
	    cmdalive[] = " \t'\"";
    while (--wordstart >= inputline)
    {
	if (index (cmdstart, *wordstart))
	    break;
	if (!index (cmdalive, *wordstart))
	    return (FALSE);
    }
    if (wordstart > inputline && *wordstart == '&')	/* Look for >& */
    {
	while (wordstart > inputline &&
			(*--wordstart == ' ' || *wordstart == '\t'));
	if (*wordstart == '>')
		return (FALSE);
    }
    return (TRUE);
}

tenematch (inputline, inputline_size, num_read, command, command_routine)
char   *inputline;		/* match string prefix */
int     inputline_size;		/* max size of string */
int	num_read;		/* # actually in inputline */
COMMAND command;		/* LIST or RECOGNIZE */
int	(*command_routine) ();	/* either append char or display char */

{
    static char 
	    delims[] = " '\"\t;&<>()|^%";
    char word [FILSIZ + 1];
    register char *str_end, *word_start, *cmd_start, *wp;
    int space_left;
    int is_a_cmd;		/* UNIX command rather than filename */

    str_end = &inputline[num_read];

   /*
    * Find LAST occurence of a delimiter in the inputline.
    * The word start is one character past it.
    */
    for (word_start = str_end; word_start > inputline; --word_start)
	if (index (delims, word_start[-1]))
	    break;

    space_left = inputline_size - (word_start - inputline) - 1;

    is_a_cmd = starting_a_command (word_start, inputline);

    for (cmd_start = word_start, wp = word; cmd_start < str_end;
    	 *wp++ = *cmd_start++);
    *wp = 0;
   
    return search (word, wp, command, command_routine, space_left, is_a_cmd);
}

char *CharPtr;
static
CharAppend (c)
{
    putchar (c);
    *CharPtr++ = c;
    *CharPtr   = 0;
}
    
tenex (inputline, inputline_size)
char   *inputline;
int     inputline_size;
{
    register int numitems, num_read;

    setup_tty (ON);
    termchars ();
    while((num_read = read (SHIN, inputline, inputline_size)) > 0)
    {
	register char *str_end, last_char, should_retype;
	COMMAND command;
	int tty_local = 0;			/* tty "local mode" bits */

	last_char = inputline[num_read - 1] & 0177;

	if (last_char == '\n' || num_read == inputline_size)
	    break;

	ioctl (SHIN, TIOCLGET, &tty_local);

	if (last_char == ESC)		/* RECOGNIZE */
	{
	    if (tty_local & LCTLECH)
		printf ("\210\210  \210\210");	/* Erase ^[ */
	    /*
	    if (num_read == 1)
	    {
	        num_read = tenedit (inputline, inputline_size, "");
		break;
	    }
	    else	
	    */
		command = RECOGNIZE;
		num_read--;
	}
	else				/* LIST */
	    command = LIST,
	    putchar ('\n');

	CharPtr = str_end = &inputline[num_read];
	*str_end = '\0';

	numitems = tenematch (inputline, inputline_size, num_read, command,
			  command == LIST ? putchar : CharAppend);
	flush ();
			  
	if (command == RECOGNIZE)
	    if (numitems != 1) 			/* Beep = No match/ambiguous */
		beep ();

	/*
	 * Tabs in the input line cause trouble after a pushback.
	 * tty driver won't backspace over them because column positions
	 * are now incorrect. This is solved by retyping over current line.
	 */
	should_retype = FALSE;
	if (index (inputline, '\t')		/* tab in input line? */
	    || (tty_local & LCTLECH) == 0)	/* Control chars don't echo? */
	{
	    back_to_col_1 ();
	    should_retype = TRUE;
	}
	if (command == LIST)			/* Always retype after LIST */
	    should_retype = TRUE;

	if (should_retype)
	    printprompt ();

	pushback (inputline);

	if (should_retype)
	    retype ();
    }

    setup_tty (OFF);

    return (num_read);
}

#ifdef TEST

short SHIN = 0, SHOUT = 1;

printprompt ()
{
    (void) write (SHOUT, "-> ", 3);
    return (1);
}

main (argc, argv)
char **argv;
{
    char    string[128];
    int n;
    while (printprompt () && (n = tenex (string, 127)) > 0)
    {
	string[n] = '\0';
	printf ("Tenex returns \"%s\"\n", string);
    }
}
#endif


//E*O*F tenex.c
-- 
Ken Greer