[net.sources] C shell with command and filename recognition/completion

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

The following code enhances the Berkeley C shell to do command and
file name recognition and completion.  The code is limited to a new module
and a hook into sh.lex.c.  Also included is a manual update "newshell".

I've had the file name completion code running for years.  Thanx go
to Mike Ellis of Fairchild A.I. Labs for adding command recogition/completion.

This version is for 4.1 BSD.  If the 4.2 shell is any different and 
this requires any change I'll repost.

				Ken Greer
				Palo Alto, CA
				ucbvax!hplabs!kg
				kg.hplabs@rand-relay (ARPANET)

Run the rest through "sh" in an empty directory...

: "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. In sh.c, move the prompt printing code to a routine "printprompt".
   The result is...
		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();
		}


6. Add tenex.o to OBJS in the makefile.  Add "-I." to the CFLAGS
   if you have not yet installed the new UCB directory code.
   This will let cc find <dir.h> in the current directory.
   Add -ltermcap to LIBES.

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 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 - 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.8 83/09/25 09:53:10 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;

	last_char = inputline[num_read - 1] & 0177;

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

	if (last_char == ESC)		/* RECOGNIZE */
	{
	    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? */
	{
	    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