[comp.sources.amiga] v89i046: find - file system searcher v1.2

page@swan.ulowell.edu (Bob Page) (03/15/89)

Submitted-by: munnari!bhpese.oz.au!rodney@uunet.UU.NET  (Rodney Lewis)
Posting-number: Volume 89, Issue 46
Archive-name: unix/find12.1

#	This is a shell archive.
#	Remove everything above and including the cut line.
#	Then run the rest of the file through sh.
#----cut here-----cut here-----cut here-----cut here----#
#!/bin/sh
# shar:    Shell Archiver
#	Run the following text with /bin/sh to create:
#	find.c
#	makefile
# This archive created: Wed Mar 15 10:21:11 1989
cat << \SHAR_EOF > find.c
/**********************************************************************/
/*                                                                    */
/* find - Amiga Version 1.2                                           */
/*                                                                    */
/* Copyright (c) 1988,89 - Rodney Lewis                               */
/*                                                                    */
/* This program is freely copyable and distributable. All copyrights  */
/* are reserved by the author. You may give copies of this program to */
/* anyone you wish but you may not sell it.                           */
/*                                                                    */
/* Pattern matching routine taken from Matt Dillon's csh program;     */
/* reproduced by permission.                                          */
/*                                                                    */
/* Changes from Version 1.0:                                          */
/*                                                                    */
/*   - dynamic allocation of file name space when recursing down      */
/*     the directory tree and when executing commands.                */
/*                                                                    */
/*   - changed comparison to assignment on line 652 of find.c.        */
/*     Though this makes no difference to the program, (i.e the       */
/*     line doesn't really need to be there in the first place),      */
/*     it was incorrect code. Thanks to Hume Smith, Acadia U.         */
/*                                                                    */
/*   - added support for the 1.3 hidden, script, pure and archive     */
/*     bits in "-perm" primary.                                       */
/*                                                                    */
/*   - added extra primary "-prot" to test for indiviual protection   */
/*     bits. "-perm" tests only for an exact permissions match. This  */
/*     allows the you (for example) to search for all files that have */
/*     not been archived using "! -prot a".                           */
/*                                                                    */
/*   - changed find so the path name used for each file is relative   */
/*     to the current directory; which is as it should be. In V1.0,   */
/*     the full path name of each file from the root of the device    */
/*     was always used.                                               */
/*                                                                    */
/**********************************************************************/

/**********************************************************************/
/*                                                                    */
/* find    - searches the directory hierachy looking for files that   */
/*           match a given boolean expression. Based  on  the  U**X   */
/*           find command.                                            */
/*                                                                    */
/**********************************************************************/

#include <exec/types.h>
#include <exec/memory.h>
#include <libraries/dosextens.h>
#include <stdio.h>
#include <functions.h>

/* define new 1.3 protection bits */

#ifndef FIBB_HIDDEN
#define FIBB_HIDDEN	7L
#define FIBB_SCRIPT	6L
#define FIBB_PURE	5L

#define FIBF_HIDDEN	(1L << FIBB_HIDDEN)
#define FIBF_SCRIPT	(1L << FIBB_SCRIPT)
#define FIBF_PURE	(1L << FIBB_PURE)
#endif

#define MAXARGS		50
#define NULL_PRIM	(struct primary *) NULL
#define EQ(x,y)		(strcmp(x, y) == 0)
#define PROTECTION	(FIBF_READ | FIBF_WRITE | FIBF_EXECUTE | FIBF_DELETE)
#define STATUS		(FIBF_HIDDEN | FIBF_SCRIPT | FIBF_PURE | FIBF_ARCHIVE | PROTECTION)

/* boolean expression node structure */

struct node {
	unsigned long		type;
	struct node	*first;
	struct node	*second;
};

/* Node types - must be different from primary node types (below) */

#define OR	0x000000ff
#define AND	0x0000ff00
#define NOT	0x00ff0000

/* structure to hold interpreted primary information */

struct primary {
	unsigned long		type;
	unsigned long		size;
	char				*data[MAXARGS];
};

/* start of compiled expression tree */

struct node *node_head;

/* Primary types */

#define PRINT	0x00000001
#define NAME	0x00000002
#define SIZE	0x00000004
#define TYPE	0x00000008
#define EXEC	0x00000010
#define NEWER	0x00000020
#define MTIME	0x00000040
#define PERM	0x00000080
#define PROT	0x00000100
#define PRIMS	0x0000ffff

/* type qualifiers */

#define DIRECT	0x00010000		/* directory for -type */
#define PLAIN	0x00020000		/* plain file for -type */
#define PROMPT	0x00040000		/* prompt for EXEC */
#define LT		0x00080000		/* greater than */
#define GT		0x00100000		/* less than */
#define CHAR	0x00200000		/* use characters in -size check */
#define QUALS	0xffff0000

int breakflag = FALSE;

char *path = NULL;				/* memory to hold full path name */
int p_alloc = 500;

struct DateStamp date;

/* manx releases the memory allocated by calloc when you call exit() */

extern char		*calloc();
extern char		*malloc();

main(argc, argv)
int argc;
char *argv[];
{
	register struct FileLock *start;
	register i;
	extern struct node *compile();

	DateStamp(&date);

	/* must be at least three arguments */

	if (argc < 3) {
		fprintf(stderr, "Usage: find <path-list> <expression>\n");
		exit(1);
	}

	/* find the start of the boolean expression */

	for (i = 1 ; argv[i][0] != '-' && argv[i][0] != '!' && argv[i][0] != '(' ; i++);
	if (i == 1) {
		/* no path name list */
		fprintf(stderr, "Usage: find <path-list> <expression>\n");
		exit(1);
	}

	/* compile the boolean expression */

	if (node_head = compile(argc - i, &argv[i])) {

		/* search each path-name specified */

		for (i = 1 ; argv[i][0] != '-' && argv[i][0] != '!' && argv[i][0] != '(' ; ++i) {
			start = Lock(argv[i], ACCESS_READ);
			if (start == NULL) {
				fprintf(stderr, "can't access '%s'\n", argv[i]);
				continue;
			}

			search(start, argv[i]);
			UnLock(start);

			if (path) {
				free(path);
				path = (char *) NULL;
				p_alloc = 500;
			}
		}
	}

	exit(0);
}

/* search the given directory and for each file
 * execute the boolean expression.
 */

search(lock, name)
register struct FileLock *lock;
register char *name;
{
	register struct FileInfoBlock *fib;
	register struct FileLock *nlock;
	char *prev = NULL, *file;
	int ret;

	fib = (struct FileInfoBlock *) AllocMem((long) sizeof(struct FileInfoBlock), MEMF_CLEAR);
	if (fib == NULL) {
		fprintf(stderr, "can't allocate file info block\n");
		return(0);
	}

	/* save current position in full path name */

	if (path)
		prev = path + strlen(path);

	/* examine initial path name */

	if (Examine(lock, fib)) {

		/* execute the expression on the inital path */

		execute(node_head, fib, name);

		if (name)
			ret = add_pname(name);
		else
			ret = add_pname(fib->fib_FileName);

		if (ret == 0) {
			fprintf(stderr, "out of memory\n");
			FreeMem(fib, (long) sizeof(struct FileInfoBlock));
			return(0);
		}

		if (fib->fib_DirEntryType <= 0) {

			/* if initial path name is not a directory then we just return */

			if (prev)
				*prev = '\0';
			FreeMem(fib, (long) sizeof(struct FileInfoBlock));
			return(0);
		}

		/* examine directory contents */

		while(ExNext(lock, fib)) {
			if (breakflag) break;

			/* recurse if we have found a directory */

			if (fib->fib_DirEntryType > 0) {
				file = malloc(strlen(path) + strlen(fib->fib_FileName) + 1);
				if (file == NULL) {
					fprintf(stderr, "out of memory\n");
					breakflag = TRUE;
					break;
				}
				strcpy(file, path);
				strcat(file, fib->fib_FileName);
				nlock = Lock(file, ACCESS_READ);
				if (nlock == NULL)
					fprintf(stderr, "locking error - %s\n", file);
				else {
					search(nlock, NULL);
					UnLock(nlock);
				}
				free(file);
			}
			else
				execute(node_head, fib, NULL);

			if (SetSignal(0L, 0L) & SIGBREAKF_CTRL_C) {
				breakflag = TRUE;
				break;
			}
		}
	}

	if (prev)
		*prev = '\0';
	FreeMem(fib, (long) sizeof(struct FileInfoBlock));
}

add_pname(name)
register char *name;
{
	register char *tmp;
	register char c;

	if (path == NULL) {
		path = calloc(1, p_alloc);
		if (path == NULL)
			return(0);
	}

	while (strlen(name) + strlen(path) + 1 > p_alloc) {
		p_alloc *= 2;
		tmp = calloc(1, p_alloc);
		if (tmp == NULL)
			return(0);

		strcpy(tmp, path);
		free(path);
		path = tmp;
	}

	if (strlen(path) && (c = path[strlen(path) - 1]) != ':' && c != '/')
		strcat(path, "/");

	strcat(path, name);
	if (strlen(path) && (c = path[strlen(path) - 1]) != ':' && c != '/')
		strcat(path, "/");

	return(1);
}

/* execute the boolean expression on the given file */

execute(cnode, fib, name)
register struct node *cnode;
register struct FileInfoBlock *fib;
char *name;
{
	register struct primary *prim;
	register long checksize;
	register j;
	register struct DateStamp *ds;
	char *file, ok[10];
	char *av[MAXARGS + 1];

	/* check node type */

	if (cnode->type == AND)
		if  (execute(cnode->first, fib, name))
			return(execute(cnode->second, fib, name));
		else
			return(0);

	else if (cnode->type == OR)
		if  (execute(cnode->first, fib, name))
			return(1);
		else
			return(execute(cnode->second, fib, name));

	else if (cnode->type == NOT)
		return(!execute(cnode->first, fib, name));

	else {

		/* we have an actual primary */

		if (name == NULL)
			name = fib->fib_FileName;

		prim = (struct primary *) cnode;
		switch (prim->type & PRIMS) {

		case PRINT:

			if (*path)
				printf("%s%s\n", path, name);
			else
				printf("%s\n", name);
			return(1);

		case NAME:

			if (compare_ok(prim->data[0], name))
				return(1);
			else
				return(0);

		case SIZE:

			if (prim->type & CHAR)
				checksize = fib->fib_Size;
			else
				checksize = fib->fib_NumBlocks;

			if (((prim->type & GT) && (checksize > prim->size) ) ||
				((prim->type & LT) && (checksize < prim->size) ) ||
				((prim->type & (GT | LT)) == 0 && (checksize == prim->size)))
				return(1);
			else
				return(0);

		case TYPE:

			switch (prim->type & QUALS | (fib->fib_DirEntryType > 0)) {

			case DIRECT:
			case (PLAIN | 1):
				return(0);

			default:
				return(1);
			}

		case EXEC:

			for (j = 0 ; prim->data[j] ; j++)
				if (EQ("{}", prim->data[j])) {
					file = malloc(strlen(path) + strlen(name) + 1);
					if (file == NULL) {
						fprintf(stderr, "out of memory\n");
						return(0);
					}
					strcpy(file, path);
					strcat(file, name);
					av[j] = file;
				}
				else
					av[j] = prim->data[j];
			av[j] = NULL;

			if (!(prim->type & PROMPT) || (pr_cmd(av) && gets(ok) &&
				 ((ok[0] == 'y') || (ok[0] == 'Y')))) {
				if (fexecv(av[0], av) == -1)
					return(0);
				else if (wait())
					return(0);
				else
					return(1);
			}

			return(0);

		case NEWER:

			ds = (struct DateStamp *) prim->data[0];
			if (fib->fib_Date.ds_Days > ds->ds_Days)
				return(1);
			else if (fib->fib_Date.ds_Days == ds->ds_Days &&
					 fib->fib_Date.ds_Minute > ds->ds_Minute)
				return(1);
			else if (fib->fib_Date.ds_Days == ds->ds_Days &&
					 fib->fib_Date.ds_Minute == ds->ds_Minute &&
					 fib->fib_Date.ds_Tick > ds->ds_Tick)
				return(1);
			else
				return(0);

		case MTIME:

			checksize = date.ds_Days - fib->fib_Date.ds_Days;

			if (((prim->type & GT) && (checksize > prim->size) ) ||
				((prim->type & LT) && (checksize < prim->size) ) ||
				((prim->type & (GT | LT)) == 0 && (checksize == prim->size)))
				return(1);
			else
				return(0);

		case PERM:

			if ((fib->fib_Protection & STATUS) == prim->size)
				return(1);
			else
				return(0);

		case PROT:

			if ((fib->fib_Protection & prim->size) == prim->size)
				return(1);
			else
				return(0);

		}
		return(0);
	}
}

/* print the command to be executed on the screen */

pr_cmd(av)
char *av[];
{
	register j;

	printf("< ");
	for (j = 0 ; av[j] ; j++) printf("%s ", av[j]);
	printf("> ? ");
	return(1);
}

/* compile the boolean expression: returns a pointer to the start
 * of the compiled expression tree, or NULL if a failure occurs.
 */

struct node *
compile(argc, argv)
int argc;
char *argv[];
{
	register i, j, scan;
	register struct primary *prim;
	register struct node *node_head = (struct node *) NULL, *tmp_node;

	for(i = 0 ; i < argc ; i++) {

		prim = (struct primary *) calloc(1, sizeof(struct primary));
		if (prim == NULL_PRIM) {
			fprintf(stderr, "out memory in primary interpretation\n");
			exit(5);
		}


		if (EQ("-o", argv[i])) {
			free(prim);

			/* -o cannot be the first argument */

			if (node_head == NULL_PRIM) {
				fprintf(stderr, "misplaced 'or' operator ... ignored\n");
				continue;
			}

			else {

				/* create OR node */

				tmp_node = (struct node *) calloc(1, sizeof(struct node));
				if (tmp_node == NULL) {
					fprintf(stderr, "out of memory in expression compilation");
					exit(5);
				}
				tmp_node->type = OR;
				tmp_node->first = node_head;

				/* compile rest of expression and attach it to OR node */

				if ((tmp_node->second = compile(argc - i - 1, argv + i + 1)) == NULL) {
					free(tmp_node);
					return(node_head);
				}
				else
					return(tmp_node);
			}
		}

		else if (EQ("(", argv[i])) {
			free(prim);

			/* scan to matching brackets */

			for (j = 0, scan = 0 ; ++i < argc && (!EQ(")", argv[i]) || scan != 0) ; j++) {
				if (EQ("(", argv[i])) scan++;
				if (EQ(")", argv[i])) scan--;
			}

			if (i >= argc) {
				fprintf(stderr, "unmatched bracket\n");
				exit(5);
			}

			if (j == 0) {
				fprintf(stderr, "empty brackets ... ignored\n");
				continue;
			}

			/* compile what is in the brackets */

			if ((prim = (struct primary *) compile(j, argv + i - j)) == NULL)
				continue;
		}

		else if (EQ("!", argv[i])) {
			if (++i >= argc) {
				fprintf(stderr, "trailing '!' ignored\n");
				continue;
			}
			if (EQ("-o", argv[i])) {
				fprintf(stderr, "illegal 'or' operator placement\n");
				exit(5);
			}

			tmp_node = (struct node *) calloc(1, sizeof(struct node));
			if (tmp_node == NULL) {
				fprintf(stderr, "out of memory in expression compilation\n");
				exit(5);
			}
			tmp_node->type = NOT;

			if (EQ("(", argv[i])) {

				/* scan to matching bracket */

				for (j = 0, scan = 0 ; ++i < argc && (!EQ(")", argv[i]) || scan != 0) ; j++) {
					if (EQ("(", argv[i])) scan++;
					if (EQ(")", argv[i])) scan--;
				}

				if (i >= argc) {
					fprintf(stderr, "unmatched bracket\n");
					exit(5);
				}

				if (j == 0) {
					fprintf(stderr, "empty brackets ... ignored\n");
					free(tmp_node);
					continue;
				}

				/* compile what is in the brackets */

				if ((tmp_node->first = compile(j, argv + i - j)) == NULL)
					continue;
			}
			else {
				tmp_node->first = (struct node *) prim;
				i += interpret(prim, argc - i, argv + i);
			}
			prim = (struct primary *) tmp_node;
		}

		else
			i += interpret(prim, argc - i, argv + i);

		/* attach interpreted primary to expression tree */

		if (node_head == NULL)
			node_head = (struct node *) prim;
		else {
			tmp_node = (struct node *) calloc(1, sizeof(struct node));
			if (tmp_node == NULL) {
				fprintf(stderr, "out of memory in expression compilation\n");
				exit(5);
			}
			tmp_node->type   = AND;
			tmp_node->first  = node_head;
			tmp_node->second = (struct node *) prim;
			node_head        = tmp_node;
		}
	}

	return(node_head);
}

/* interpret a primary */

interpret(prim, argc, argv)
struct primary *prim;
char *argv[];
{
	register i, j;
	register struct FileLock *lock;
	register struct FileInfoBlock *fib;
	register struct DateStamp *ds;
	char *numstr;
	extern unsigned long atol();

	for (i = 0 ; i < argc ; i++) {

		if (EQ("-print", argv[i]))
			prim->type = PRINT;

		else if (EQ("-name", argv[i])) {
			prim->type = NAME;
			prim->data[0] = argv[++i];
		}

		else if (EQ("-size", argv[i])) {
			prim->type = SIZE;

			/* get required size */

			numstr = argv[++i];

			if (*numstr == '+') {
				prim->type |= GT;
				numstr++;
			}

			else if (*numstr == '-') {
				prim->type |= LT;
				numstr++;
			}

			if (numstr[strlen(numstr) - 1] == 'c') {
				prim->type |= CHAR;
				numstr[strlen(numstr) - 1] = '\0';
			}

			prim->size = atol(numstr);
		}

		else if (EQ("-type", argv[i])) {
			prim->type = TYPE;
			if (EQ(argv[++i], "d"))
				prim->type |= DIRECT;
			else if (EQ(argv[i], "f"))
				prim->type |= PLAIN;
			else {
				fprintf(stderr, "illegal file type specified\n");
				exit(5);
			}
		}

		else if (EQ("-exec", argv[i])) {
			prim->type = EXEC;

			/* scan to ending ';', saving pointers to arguments */

			for (j=0 ; (j<MAXARGS) && (++i < argc) && !EQ(";",argv[i]) ; j++)
				prim->data[j] = argv[i];

			if (i >= argc) {
				fprintf(stderr, "no ending ';' on command\n");
				exit(5);
			}

			else if (j >= MAXARGS) {
				fprintf(stderr, "command too long\n");
				exit(5);
			}

			else
				argv[j] = NULL;
		}

		else if (EQ("-ok", argv[i])) {
			prim->type = EXEC | PROMPT;

			/* scan to ending ';', saving pointers to arguments */

			for (j=0 ; (j<MAXARGS) && (++i < argc) && !EQ(";",argv[i]) ; j++)
				prim->data[j] = argv[i];

			if (i >= argc) {
				fprintf(stderr, "no ending ';' on command\n");
				exit(5);
			}

			else if (j >= MAXARGS) {
				fprintf(stderr, "command too long\n");
				exit(5);
			}

			else
				argv[j] = NULL;
		}

		else if (EQ("-newer", argv[i])) {
			prim->type = NEWER;

			if (lock = Lock(argv[++i])) {
				fib = (struct FileInfoBlock *) AllocMem((long) sizeof(struct FileInfoBlock), MEMF_CLEAR);
				if (fib == NULL) {
					fprintf(stderr, "no mem for -newer test\n");
					UnLock(lock);
					exit(5);
				}

				if (Examine(lock, fib) == 0) {
					fprintf(stderr, "could not examine %s\n", argv[i]);
					FreeMem(fib, (long) sizeof(struct FileInfoBlock));
					UnLock(lock);
					exit(5);
				}

				/* save date stamp of given file */

				ds = (struct DateStamp *) calloc(1, sizeof(struct DateStamp));
				if (ds == NULL) {
					fprintf(stderr, "no mem for DateStamp on %s\n", argv[i]);
					FreeMem(fib, (long) sizeof(struct FileInfoBlock));
					UnLock(lock);
					exit(5);
				}

				prim->data[0] = (char *) ds;
				*ds = fib->fib_Date;

				FreeMem(fib, (long) sizeof(struct FileInfoBlock));
				UnLock(lock);
			}

			else {
				fprintf(stderr, "unable to access %s\n", argv[i]);
				exit(5);
			}
		}

		else if (EQ("-mtime", argv[i])) {
			prim->type = MTIME;

			/* get required number of days */

			numstr = argv[++i];

			if (*numstr == '+') {
				prim->type |= GT;
				numstr++;
			}
			else if (*numstr == '-') {
				prim->type |= LT;
				numstr++;
			}

			prim->size = atol(numstr);
		}

		else if (EQ("-perm", argv[i])) {
			prim->type = PERM;
			prim->size = PROTECTION;

			/* assemble desired protection bits */

			for(i++, j = 0 ; argv[i][j] ; j++) {
				switch(argv[i][j]) {

				case 'n':
					if (prim->size != PROTECTION)
						fprintf(stderr, "'n' permission code overriding other given codes \n");

					prim->size = PROTECTION;
					return(i);

				case 'h':
					prim->size |= FIBF_HIDDEN;
					break;

				case 's':
					prim->size |= FIBF_SCRIPT;
					break;

				case 'p':
					prim->size |= FIBF_PURE;
					break;

				case 'a':
					prim->size |= FIBF_ARCHIVE;
					break;

				case 'r':
					prim->size &= ~FIBF_READ;
					break;

				case 'w':
					prim->size &= ~FIBF_WRITE;
					break;

				case 'e':
					prim->size &= ~FIBF_EXECUTE;
					break;

				case 'd':
					prim->size &= ~FIBF_DELETE;
					break;

				default:
					fprintf(stderr, "unknown permission code '%c' ... ignored\n", argv[i][j]);
					break;
				}
			}
		}

		else if (EQ("-prot", argv[i])) {
			prim->type = PROT;
			prim->size = 0;

			/* assemble desired protection bits */

			for(i++, j = 0 ; argv[i][j] ; j++) {
				switch(argv[i][j]) {

				case 'h':
					prim->size |= FIBF_HIDDEN;
					break;

				case 's':
					prim->size |= FIBF_SCRIPT;
					break;

				case 'p':
					prim->size |= FIBF_PURE;
					break;

				case 'a':
					prim->size |= FIBF_ARCHIVE;
					break;

				case 'r':
					prim->size |= FIBF_READ;
					break;

				case 'w':
					prim->size |= FIBF_WRITE;
					break;

				case 'e':
					prim->size |= FIBF_EXECUTE;
					break;

				case 'd':
					prim->size |= FIBF_DELETE;
					break;

				default:
					fprintf(stderr, "unknown protection code '%c' ... ignored\n", argv[i][j]);
					break;
				}
			}
		}

		else {
			fprintf(stderr, "unknown primary: %s\n", argv[i]);
			exit(5);
		}

		return(i);
	}
}

/*
 * Compare a wild card name with a normal name.
 * Taken from Matt Dillon's csh program.
 */

#define MAXB   8

compare_ok(wild, name)
char *wild, *name;
{
   register char *w = wild;
   register char *n = name;
   char *back[MAXB][2];
   register char s1, s2;
   int	bi = 0;

   while (*n || *w) {
      switch (*w) {
      case '*':
	 if (bi == MAXB) {
	    fprintf(stderr,"Too many levels of '*'\n");
	    return (0);
	 }
	 back[bi][0] = w;
	 back[bi][1] = n;
	 ++bi;
	 ++w;
	 continue;
goback:
	 --bi;
	 while (bi >= 0 && *back[bi][1] == '\0')
	    --bi;
	 if (bi < 0)
	    return (0);
	 w = back[bi][0] + 1;
	 n = ++back[bi][1];
	 ++bi;
	 continue;
      case '?':
	 if (!*n) {
	    if (bi)
	       goto goback;
	    return (0);
	 }
	 break;
      default:
	 s1 = (*n >= 'A' && *n <= 'Z') ? *n - 'A' + 'a' : *n;
	 s2 = (*w >= 'A' && *w <= 'Z') ? *w - 'A' + 'a' : *w;
	 if (s1 != s2) {
	    if (bi)
	       goto goback;
	    return (0);
	 }
	 break;
      }
      if (*n)  ++n;
      if (*w)  ++w;
   }
   return (1);
}
SHAR_EOF
cat << \SHAR_EOF > makefile
OBJ = find.o

.c.o:
	cc -E200 $*.c

find:		Makefile $(OBJ)
	ln -o find $(OBJ) -lc

find.o:		find.c

find.uue:	find
	uuencode >find.uue find find

shar:		shar1 shar2 shar3

shar1:
	shar -a find.c makefile > find.shar1

shar2:
	shar -a find.doc README > find.shar2

shar3:		find.uue
	shar -a find.uue > find.shar3
SHAR_EOF
#	End of shell archive
exit 0
-- 
Bob Page, U of Lowell CS Dept.  page@swan.ulowell.edu  ulowell!page
Have five nice days.