[net.sources] Bcsh -- Bourne Shell Cshell Emulator, Part 1 of 1

chris@globetek.UUCP (chris) (01/06/86)

Okay, folks, I got enough requests for my Bourne shell script cshell-emulator
that I decided to post it.  It's grown a bit (well, actually, it's got
more than twice as big) since I first described it, and has quite a lot
more functionality.  Do NOT expect it to be as fast as the cshell, though.
After all, it *is* a shell script.  The primary intent of this program was
to provide cshell-like history for people familliar with the cshell who must
use a Bourne-shell-only system.  Hence, it also attempts to cope with
the problem of incompatible control-flow syntax between the two shells
(no more frustration from using "foreach i (...)" instead of "for i in ..."
and vice-versa).

I have not written a manual page for it 'cause I'm lazy -- er, I mean,
because it's pretty well covered by the regular shell manual pages.
I will actually do a man page and post it reasonably soon.
The features, bugs, etc. are all described in the voluminous comments at
the beginning of the script, which should really be peeled off as a
README file.

Enjoy it!  I'm still amazed how much of the cshell's functionality
can be done via shell.  It should work on a vanilla Bourne shell (and hence
has a lot of code duplication -- I'll do a cleaner version using shell
functions soon).

--chris

This is not a shar file.  Just cut on the line below and peel my signature
off the end.
------------- cut here --------------- cut here ----------------
:	Bcsh -- A Simple Cshell-Like Command Pre-Processor For The Bourne Shell
:
:	"Copyright (c) Chris Robertson, December 1985"
:
:	This software may be used for any purpose provided the original
:	copyright notice and this notice are affixed thereto.  No warranties of
:	any kind whatsoever are provided with this software, and it is hereby
:	understood that the author is not liable for any damagages arising
:	from the use of this software.
:
:	Features Which the Cshell Does Not Have:
:	----------------------------------------
:
:	+  command history persists across bcsh sessions
: 	+  global last-command editing via 'g^string1^string2^' syntax
:	+  edit any command via $EDITOR or $VISUAL editors
:	+  history file name, .bcshrc file name, alias file name, and number
:	   of commands saved on termination can be set by environment variables
:	+  prompt may evaluate commands, such as `pwd`, `date`, etc.
:	+  the whole text of interactive 'for' and 'while' loops and 'if'
:	   statements goes into the history list and may be re-run or edited
:	+  multiple copies of commands and requests to see command history
:	   are not added to the history list
:	+  the history mechanism actually stores all commands entered in a
:	   current session, not just $history of them.  This means that you
:	   can increase $history on the fly and at once have a larger history.
:
:
:	Synonyms:
:	---------
:
:	logout, exit, bye	write out history file and exit
:	h, history		show current history list
:	
:	
:	Aliases:
:	--------
:
:	alias NAME CMND		create an alias called NAME to run CMND
:	unalias NAME		remove the alias NAME
:
:	There are no 'current-session only' aliases -- all alias and unalias
:	commands are permanent, and stored in the $aliasfile.
:
:	If an alias contains positional variables -- $1, $2, $*, etc. -- any
:	arguments following the alias name are considered to be values for
:	those variables, and the alias is turned into a command of the form
:	'set - arguments;alias'.  Otherwise, a simple substitution is performed
:	for the alias and the rest of the command preserved.  The cshell
:	convention of using '\!:n' in an alias to get bits of the current
:	command is mercifully abandoned.
:
:	Quotes are not necessary around the commands comprising an alias;
:	in fact, any enclosing quotes are stripped when the alias is added
:	to the file.
:
:	A couple of typical aliases might be:
:
:		goto	cd $1;pwd
:		l	ls -F
:
:	Note that aliasing something to "commands;logout" will not work -- if
:	you want something to happen routinely on logout put it in the file
:	specified by $logoutfile, default = $HOME/.blogout.
:
:
:	Command Substitutions:
:	----------------------
:
:	!!			substitute last command from history list
:	!!:N			substitute Nth element of last command from
:				history list -- 0 = command name, 1 = 1st arg
: 	!!:$			substitute last element of last command from
:				history list
: 	!!:*			substitute all arguments to last command
:				from history list
:	!NUMBER			substitute command NUMBER from the history list
:	!NUMBER:N		as above, but substitute Nth element, where
:				0 = command name, 1 = 1st arg, etc.
: 	!NUMBER:$		as above, but substitute last element
: 	!NUMBER:*		as above, but substitute all arguments
:	!?STRING		substitute most-recent command from history list
:				containing STRING -- STRING must be enclosed in
:				braces if followed by any other characters
:	!?STRING:N		as above, but substitute Nth element, where
:				0 = command name, 1 = 1st arg, etc.
: 	!?STRING:$		as above, but substitute last element	
: 	!?STRING:*		as above, but substitute all arguments
:
:
:	Command Editing:
:	----------------
:
:	CMND~e			edit CMND using $EDITOR, where CMND may be found
:				using a history substitution
:	CMND~v			edit CMND using $VISUAL, where CMND may be found
:				using a history substitution
: "	^string1^string2^	substitute string2 for string1 in last command"
:				command and run it
: "	g^string1^string2^	globally substitute string2 for string1 in  "
:				last command and run it
: 	!NUMBER:s/string1/string2/
:				substitute string2 for string1 in
:				command NUMBER and run it
: 	!NUMBER:gs/string1/string2/
:				globally substitute string2 for string1 in
:				command NUMBER and run it
: 	!?STRING:s/string1/string2/
:				substitute string2 for string1 in last command
:				containing STRING and run it
: 	!?STRING:gs/string1/string2/
:				globally substitute string2 for string1 in last
:				command containing STRING and run it
:	
:	Any command which ends in the string ":p" is treated as a normal
:	command until all substitutions have been completed.  The trailing
:	":p" is then stripped, and the command is simply echoed and added to
:	the history list instead of being executed.
:
:	None of the other colon extensions of the cshell are supported.
:
:
:	Shell Environment Variables:
:	----------------------------
:
:	EDITOR		editor used by ~e command, default = "ed"
:	VISUAL		editor used by ~v command, default = "vi"
:	MAIL		your system mailbox
:	PAGER		paging program used by history command, default = "more"
:	PS1		primary prompt
:	PS2		secondary prompt
:	history		number of commands in history list, default = 22
:	histfile	file history list is saved in, default = $HOME/.bhistory
:	savehist	number of commands remembered from last bcsh session
:	aliasfile	file of aliased commands, default = $HOME/.baliases
:	logoutfile	file of commands to be executed before termination
:
:
:	Regular Shell Variables:
:	------------------------
:
:	Shell variables may be set via Bourne or cshell syntax, e.g., both
:	"set foo=bar" and "foo=bar" set a variable called "foo" with the value
:	"bar".  However, all variables are automatically set as environment
:	variables, so there is no need to export them.  Conversely, there
:	are NO local variables.  Sorry, folks.
:
:	A cshell-style "setenv" command is turned into a regular "set" command.
:
:
:	The Prompt:
:	----------
:
:	You may, if you wish, have a command executed in your prompt.  If
:	the variable PS1 contains a dollar sign or a backquote, it is
:	evaluated and the result used as the prompt, provided the evaluation
:	did not produce a "not found" error message.  The two special cases
:	of PS1 consisting solely of "$" or "$ " are handled correctly.  For
:	example, to have the prompt contain the current directory followed
:	by a space, enter:
:
:		PS1=\'echo "`pwd` "\'
:
:	You need the backslashed single quotes to prevent the command being
:	evaluated by the variable-setting mechanism and the shell before it
:	is assigned to PS1.
:
:
:	Shell Control-Flow Syntax:
:	--------------------------
:
:	'While', 'for', 'case', and 'if' commands entered in Bourne shell
:	syntax are executed as normal.
:
:	A valiant attempt is made to convert 'foreach' loops into 'for' loops,
:	cshell-syntax 'while' loops into Bourne shell syntax, and 'switch'
:	statements into 'case' statements.  I cannot guarantee to always get it
:	right.  If you forget the 'do' in a 'while' or 'for' loop, or finish
:	them with 'end' instead of 'done', this will be corrected.
:
:	The simple-case cshell "if (condition) command" is turned into Bourne
:	syntax.  Other 'if' statements are left alone apart from making the
:	'then' a separate statement, because constructing a valid interactive
:	cshell 'if' statement is essentially an exercise in frustration anyway.
:	The cshell and Bourne shell have sufficiently different ideas about
:	conditions that if is probably best to resign yourself to learning
:	the Bourne shell conventions.
:
:	Note that since most of the testing built-ins of the cshell are
:	not available in the Bourne shell, a complex condition in a 'while'
:	loop or an 'if' statement will probably fail.
:	
:
:	Bugs, Caveats, etc.:
:	--------------------
:
:	This is not a super-speedy program.  Be patient, especially on startup.
:
:	To the best of my knowledge this program should work on ANY Bourne
:	shell -- except that if your shell does not understand 'echo -n' you
:	will have to change the 10 or so places where this occurs.
:
:	This program may run out of stack space on a 16-bit machine where
:	/bin/sh is not split-space.
:
:	Mail checking is done every 10 commands if $MAIL is set in your
:	environment.  For anything fancier, you will have to hack the code.
:
:	Because commands are stuffed in a file before sh is invoked on them,
:	error messages from failed commands are ugly.
:
:	Failed history substitutions either give nothing at all, or a
:	"not found" style of error message.
:
:	A command history is kept whether you want it or not.  This may be
:	perceived as a bug or a feature, depending on which side of bed you
:	got out on.
:
:	If you want a real backslash in a command, you will have to type two
: 	of them  because the shell swallows the first backslash in the initial
: 	command pickup.  This means that to include a non-history '!' in a
:	command you need '\\!' -- a real wart, especially for net mail,
:	but unavoidable.
:
:	Commands containing an '@' will break all sorts of things.
:
:	Very complex history substitutions may fail.
:
:	File names containing numbers may break numeric history sustitutions.
:
:	Commands containing bizzare sequences of characters may conflict
:	with internal kludges.
:
:	Aliasing something to "commands;logout" will not work -- if you
:	want something to happen routinely on logout, put it in the file
:	specified by $logoutfile, default = $HOME/.blogout.
:
:	This current version is excessively verbose because it was written to
:	run on a vanilla V7 Bourne shell if necessary.  A much more pleasing
:	version using shell functions will appear soon, but it will of course
:	only work on V8 or System V.2 shells.
:	
:	Please send all bug reports to ihnp4!utzoo!globetek!chris.
:	Flames will be posted to net.general with 'Reply-to' set to your
: '	mail path...  :-)						'
:
:
:
:		************* VERY IMPORTANT NOTICE *************
:
: If your shell supports # comments, then REPLACE all the colon 'comments'
: with # comments.  If it does not, then REMOVE all the 'comment' lines from the
: working copy of the file, as it will run MUCH faster -- the shell evaluates
: lines starting with a colon but does not actually execute them, so you will
: save the read-and-evaluate time by removing them.


history=${history-22}
savehist=${savehist-22}
histfile=${histfile-$HOME/.bhistory}
EDITOR=${EDITOR-ed}
VISUAL=${VISUAL-vi}
PAGER=${PAGER-more}

aliasfile=${aliasfile-$HOME/.baliases}

: the alias file may contain 1 blank line, so a test -s will not work

case "`cat $aliasfile 2> /dev/null`" in
	"")
		doalias=no
		;;
	*)
		doalias=yes
		;;
esac

if test -s "${sourcefile-$HOME/.bcshrc}"
	then
	. ${sourcefile-$HOME/.bcshrc}
fi

if test -s "$histfile"
	then
	cmdno="`set - \`wc -l $histfile\`;echo $1`"
	cmdno="`expr \"$cmdno\" + 1`"
	lastcmd="`tail -1 $histfile`"
else
	cat /dev/null > $histfile
	cmdno=1
	lastcmd=
fi

: default prompts -- PS1 and PS2 may be SET but EMPTY, so '${PS1-% }' syntax
: is not used here

case "$PS1" in
	"")					
		PS1="% "
		;;				
esac
case "$PS2" in
	"")					
		PS2="> "
		;;				
esac

export histfile savehist history aliasfile EDITOR VISUAL PAGER cmdno PS1 PS2

case "$MAIL" in
	"")
		;;
	*)
		mailsize=`set - \`wc -c $MAIL\`;echo $1`
		;;
esac

trap ':' 2 3
trap "tail -$savehist $histfile>/tmp/hist$$;uniq /tmp/hist$$ > $histfile;\
rm -f /tmp/*$$;exit 0" 15

getcmd=yes
mailcheck=
while :
	do
	run=yes
	case "$mailprompt" in
		"")
			;;
		*)
			echo "$mailprompt"
			;;
	esac
	case "$getcmd" in
		yes)
			: guess if the prompt should be evaluated or not

			case "$PS1" in
				\$|\$\ )
					echo -n "$PS1"
					;;
				*\`*|*\$*)
					tmp="`eval $PS1 2>&1`"
					case "$tmp" in
						*not\ found)			
							echo -n "$PS1"
							;;			
						*)				
							echo -n "$tmp"
							;;			
					esac
					;;
				*)
					echo -n "$PS1"
					;;
			esac

			read cmd
			;;
	esac
	case "$MAIL" in
		"")
			;;
		*)
			: check for mail every 10 commands

			case "$mailcheck" in
				1111111111)
					mailcheck=
					newsize="`set - \`wc -c $MAIL\`;echo $1`"
					if test "$newsize" -gt "$mailsize"
						then
						mailprompt="You have new mail"
					else
						mailprompt=
					fi
					mailsize=$newsize
					;;
				*)
					mailcheck=1$mailcheck
					;;
			esac
			;;
	esac
	case "$cmd" in
		"")
			continue
			;;
		sh)
			sh
			run=no
			;;
		!!)
			cmd=$lastcmd
			echoit=yes
			getcmd=no
			continue
			;;
		*:p)
			cmd="`expr \"$cmd\" : '\(.*\):p'` +~+p"
			getcmd=no
			continue
			;;
		foreach[\ \	]*)
			while test "$line" != "end"
				do
				echo -n "$PS2"
				read line
				cmd="${cmd};$line"
			done
			echo "$cmd" > /tmp/doit$$
			ed - /tmp/doit$$ << ++++
			s/end/done/
			s/foreach[ 	]\(.*\)(/for \1 in /
			s/)//
			s/;/;do /
			w
++++
			;;
		for[\ \	]*|while[\ \	]*)

			: try to catch the most common cshell-to-Bourne-shell mistakes

			echo -n "$PS2"
			read line
			case "$line" in
				*do)
					line="do :"
					;;
				*do*)
					;;
				*)
					line="do $line"
					;;
			esac
			cmd="${cmd};$line"
			while test "$line" != "done" -a "$line" != "end"
				do
				echo -n "$PS2"
				read line
				case "$line" in
					end)
						line=done
						;;
				esac
				cmd="${cmd};$line"
			done
			echo "$cmd" > /tmp/doit$$
			;;
		if[\ \	]*)
			while test "$line" != "fi" -a "$line" != "endif"
				do
				echo -n "$PS2"
				read line
				case "$line" in
					*[a-z]*then)
						line="`expr \"$line\" : '\(.*\)then'`;then"
						;;
					endif)
						line=fi
						;;
				esac
				cmd="${cmd};$line"
			done
			echo "$cmd" > /tmp/doit$$
			case "`grep then /tmp/doit$$`" in
				"")
					: fix 'if foo bar' cases

					ed - /tmp/doit$$ << ++++
					s/)/);then/
					s/.*/;fi/
					w
++++
					;;
			esac
			;;
		case[\ \	]*)
			while test "$line" != "esac"
				do
				echo -n "$PS2"
				read line
				cmd="${cmd}@$line"
			done
			cmd="`echo \"$cmd\" | tr '@' ' '`"
			echo "$cmd" > /tmp/doit$$
			;;
		switch[\ \	]*)
			while test "$line" != "endsw"
				do
				echo -n "$PS2"
				read line
				cmd="${cmd}@$line"
			done
			echo "$cmd" > /tmp/doit$$
			ed - /tmp/doit$$ << '++++'
			1,$s/@/\
/g
			g/switch.*(/s//case "/
			s/)/" in/
			1,$s/case[	 ]\(.*\):$/;;\
	\1)/
			2d
			1,$s/endsw/;;\
esac/
			g/breaksw/s///
			1,$s/default.*/;;\
	*)/
			w
++++
			cmd="`cat /tmp/doit$$`"
			;;
		*![0-9]*:s*|*![0-9]*:gs*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*![0-9]*:gs/*/*/*|*![0-9]*:s/*/*/*)
						;;
					*![0-9]*:gs/*/*|*![0-9]*:s/*/*)
						i="${i}/"
						;;
				esac
				case "$i" in
					*![0-9]*:gs/*)

						: get right command to parse

						frontstuff="`expr \"$i\" : '\(.*\)!.*:gs\/.*'`"
						wanted="`expr \"$i\" : '.*!\(.*\):gs\/.*'`"
						rest="`expr \"$i\" : '.*!.*:gs.*\/\(.*\)'`"
						cmd="`grep -n . $histfile | grep \"^$wanted\"`"
						cmd="`expr \"$cmd\" : \"${wanted}.\(.*\)\"`"

						: find what substitution is wanted

						tmp2="`expr \"$i\" : '.*:gs\/\(.*\)\/.*\/.*'`"
						wanted="`expr \"$i\" : '.*:gs/.*/\(.*\)/.*'`"
						tmp3="$frontstuff`echo \"$cmd\" | sed -e \"s@$tmp2@$wanted@g\"`$rest"
						;;
					*![0-9]*:s/*)

						: get right command to parse

						frontstuff="`expr \"$i\" : '\(.*\)!.*:s\/.*'`"
						wanted="`expr \"$i\" : '.*!\(.*\):s\/.*'`"
						rest="`expr \"$i\" : '.*!.*:s.*\/\(.*\)'`"
						cmd="`grep -n . $histfile | grep \"^$wanted\"`"
						cmd="`expr \"$cmd\" : \"${wanted}.\(.*\)\"`"

						: find what substitution is wanted

						tmp2="`expr \"$i\" : '.*:s\/\(.*\)\/.*\/.*'`"
						wanted="`expr \"$i\" : '.*:s/.*/\(.*\)/.*'`"
						tmp3="$frontstuff`echo \"$cmd\" | sed -e \"s@$tmp2@$wanted@\"`$rest"
						;;
					*)
						tmp3="$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		*![0-9]*:*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*![0-9]*:*)

						: get right command to parse

						frontstuff="`expr \"$i\" : '\(.*\)!.*:.*'`"
						wanted="`expr \"$i\" : '.*!\(.*\):.*'`"
						tmp3="`grep -n . $histfile | grep \"^$wanted\"`"
						tmp3="`expr \"$tmp3\" : \"${wanted}.\(.*\)\"`"

						: get right part of the command

						wanted="`expr \"$i\" : '.*!.*:\(.*\)'`"
						case "$wanted" in
							[0-9]*)
								rest="`expr \"$wanted\" : '.*[0-9]\(.*\)'`"
								case "$rest" in
									"")
										;;
									*)
										wanted="`expr \"$wanted\" : '\(.*[0-9]\).*'`"
										;;
								esac
								wanted="`expr \"$wanted\" + 1`"
								tmp2=1
								for j in $tmp3
									do
									case "$wanted" in
										$tmp2)
											tmp3="$frontstuff$j$rest"
											break
											;;
										*)
											tmp2="`expr \"$tmp2\" + 1`"
											;;
									esac
								done
								;;
							\$*)
								rest="`expr \"$wanted\" : '\$\(.*\)'`"
								for j in $tmp3
									do
									:
								done
								tmp3="$frontstuff$j$rest"
								;;
							\**)
								rest="`expr \"$i\" : '.*:\*\(.*\)'`"
								set - $tmp3
								shift
								tmp3="$frontstuff$*$rest"
								set -
								;;
						esac
						;;
					*)
						tmp3="$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		*![0-9]*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*![0-9]*)
						frontstuff="`expr \"$i\" : '\(.*\)!.*:.*'`"
						wanted="`expr \"$i\" : '.*!\([0-9].*\)'`"
						rest="`expr \"$wanted\" : '.*[0-9]\(.*\)'`"
						case "$rest" in
							"")
								;;
							*)
								wanted="`expr \"$wanted\" : '\(.*[0-9]\).*'`"
								;;
						esac
						tmp3="`grep -n . $histfile | grep \"^$wanted\"`"
						tmp3="$frontstuff`expr \"$tmp3\" : \"${wanted}.\(.*\)\"`$rest"
						;;
					*)
						tmp3="$frontstuff$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		*!!:*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*!!:*)
						frontstuff="`expr \"$i\" : '\(.*\)!!:.*'`"
						wanted="`expr \"$i\" : '.*!!:\(.*\)'`"
						case "$wanted" in
							[0-9]*)
								rest="`expr \"$wanted\" : '.*[0-9]\(.*\)'`"
								case "$rest" in
									"")
										;;
									*)
										wanted="`expr \"$wanted\" : '\(.*[0-9]\).*'`"
										;;
								esac
								wanted="`expr \"$wanted\" + 1`"
								tmp2=1
								for j in $lastcmd
									do
									case "$tmp2" in
										$wanted)
											tmp3="$frontstuff$j$rest"
											break
											;;
										*)
											tmp2="`expr \"$tmp2\" + 1`"
											;;
									esac
								done
								;;
							\$*)
								rest="`expr \"$wanted\" : '\$\(.*\)'`"
								for j in $lastcmd
									do
									:
								done
								tmp3="$frontstuff$j$rest"
								;;
							\**)
								rest="`expr \"$i\" : '.*:\*\(.*\)'`"
								set - $lastcmd
								shift
								tmp3="$frontstuff$*rest"
								set -
								;;
						esac
						;;
					*)
						tmp3="$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		*!!*)
			cmd="`echo \"$cmd\" | sed -e \"s@!!@$lastcmd@g\"`"
			echoit=yes
			getcmd=no
			continue
			;;
		*!\?*:s/*|*!\?*:gs/*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*!\?*:gs/*/*/*|*!?*:s/*/*/*)
						;;
					*!\?*:gs/*/*|*!?*:s/*/*)
						i="${i}/"
						;;
				esac
				case "$i" in
					*!\?*:gs/*)

						: get right command to parse

						frontstuff="`expr \"$i\" : '\(.*\)!?.*:gs\/.*'`"
						wanted="`expr \"$i\" : '.*!?\(.*\):gs\/.*'`"
						rest="`expr \"$i\" : '.*!?.*:gs.*\/\(.*\)'`"
						cmd="`ed - $histfile << ++++
						1,\\$-${history}d
						a

.
						?$wanted?
++++
`"

						: find what substitution is wanted

						tmp2="`expr \"$i\" : '.*:gs\/\(.*\)\/.*\/.*'`"
						wanted="`expr \"$i\" : '.*:gs/.*/\(.*\)/.*'`"
						tmp3="$frontstuff`echo \"$cmd\" | sed -e \"s@$tmp2@$wanted@g\"`$rest"
						;;
					*!\?*:s/*)

						: get right command to parse

						frontstuff="`expr \"$i\" : '\(.*\)!?.*:s\/.*'`"
						wanted="`expr \"$i\" : '.*!?\(.*\):s\/.*'`"
						rest="`expr \"$i\" : '.*!?.*:s.*\/\(.*\)'`"
						cmd="`ed - $histfile << ++++
						1,\\$-${history}d
						a

.
						?$wanted?
++++
`"

						: find what substitution is wanted

						tmp2="`expr \"$i\" : '.*:s\/\(.*\)\/.*\/.*'`"
						wanted="`expr \"$i\" : '.*:s/.*/\(.*\)/.*'`"
						tmp3="$frontstuff`echo \"$cmd\" | sed -e \"s@$tmp2@$wanted@\"`$rest"
						;;
					*)
						tmp3="$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done;
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		*!\?*:*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*!\?*:*)
						: get right command to parse

						frontstuff="`expr \"$i\" : '\(.*\)!?.*:.*'`"
						wanted="`expr \"$i\" : '.*!?\(.*\):.*'`"
						cmd="`ed - $histfile << ++++
						1,\\$-${history}d
						a

.
						?$wanted?
++++
`"
						: get right part of the command

						wanted="`expr \"$i\" : '.*!?.*:\(.*\)'`"
						case "$wanted" in
							[0-9]*)
								rest="`expr \"$wanted\" : '.*[0-9]\(.*\)'`"
								case "$rest" in
									"")
										;;
									*)
										wanted="`expr \"$wanted\" : '\(.*[0-9]\).*'`"
										;;
								esac
								wanted="`expr \"$wanted\" + 1`"
								tmp2=1
								for j in $cmd
									do
									case "$tmp2" in
										$wanted)
											tmp3="$frontstuff$j$rest"
											break
											;;
										*)
											tmp2="`expr \"$tmp2\" + 1`"
											;;
									esac
								done
								;;
							\$*)
								rest="`expr \"$wanted\" : '\$\(.*\)'`"
								for j in $cmd
									do
									:
								done
								tmp3="$frontstuff$j$rest"
								;;
							\**)
								rest="`expr \"$i\" : '.*:\*\(.*\)'`"
								set - $cmd
								shift
								tmp3="$frontstuff$*$rest"
								set -
								;;
						esac
						;;
					*)
						tmp3="$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		*!\?*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*!\?{*}*)
						frontstuff="`expr \"$i\" : '\(.*\)!?.*:.*'`"
						wanted="`expr \"$i\" : '.*!?{\(.*\)}.*'`"
						rest="`expr \"$i\" : '.*!?{.*}\(.*\)'`"
						tmp3="`ed - $histfile << ++++
						1,\\$-${history}d
						a

.
						?$wanted?
++++
`"
						tmp3="$frontstuff$tmp3$rest"
						;;
					*!\?*)
						wanted="`expr \"$i\" : '.*!?\(.*\)'`"
						tmp3="$frontstuff`ed - $histfile << ++++
						1,\\$-${history}d
						a

.
						?$wanted?
++++
`"
						;;
					*)
						tmp3="$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		*!*:*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*!*:*)

						: get right command to parse

						frontstuff="`expr \"$i\" : '\(.*\)!.*:.*'`"
						wanted="`expr \"$i\" : '.*!\(.*\):.*'`"
						cmd="`grep \"^$wanted\" $histfile | tail -1`"

						: get right part of the command

						wanted="`expr \"$i\" : '.*!.*:\(.*\)'`"
						case "$wanted" in
							[0-9]*)
								rest="`expr \"$wanted\" : '.*[0-9]\(.*\)'`"
								case "$rest" in
									"")
										;;
									*)
										wanted="`expr \"$wanted\" : '\(.*[0-9]\).*'`"
										;;
								esac
								wanted="`expr \"$wanted\" + 1`"
								tmp2=1
								for j in $cmd
									do
									case "$tmp2" in
										$wanted)
											tmp3="$frontstuff$j$rest"
											break
											;;
										*)
											tmp2="`expr \"$tmp2\" + 1`"
											;;
									esac
								done
								;;
							\$*)
								rest="`expr \"$wanted\" : '\$\(.*\)'`"
								for j in $cmd
									do
									:
								done
								tmp3="$frontstuff$j$rest"
								;;
							\**)
								rest="`expr \"$i\" : '.*:\*\(.*\)'`"
								set - $cmd
								shift
								tmp3="$frontstuff$*$rest"
								set -
								;;
						esac
						;;
					*)
						tmp3="$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		*\\!*)
			cmd="`echo \"$cmd\" | sed -e 's@\\!@REAL EXCLAMATION MARK@g'`"
			exclaim=yes
			getcmd=no
			continue
			;;
		*!*)
			tmp=
			for i in $cmd
				do
				case "$i" in
					*!{*}*)
						frontstuff="`expr \"$i\" : '\(.*\)!.*:.*'`"
						wanted="`expr \"$i\" : '.*!{\(.*\)}.*'`"
						rest="`expr \"$i\" : '!{.*}\(.*\)'`"
						tmp3="$frontstuff`grep \"^$wanted\" $histfile | tail -1`$rest"
						;;
					*!*)
						frontstuff="`expr \"$i\" : '\(.*\)!.*:.*'`"
						wanted="`expr \"$i\" : '.*!\(.*\)'`"
						tmp3="$frontstuff`grep \"^$wanted\" $histfile | tail -1`"
						;;
					*)
						tmp3="$i"
						;;
				esac
				tmp="$tmp $tmp3"
			done
			cmd="$tmp"
			echoit=yes
			getcmd=no
			continue
			;;
		g\^*\^*\^*)
			tmp="`expr \"$cmd\" : 'g\^\(.*\)\^.*\^.*'`"
			wanted="`expr \"$cmd\" : 'g\^.*\^\(.*\)\^.*'`"
			rest="`expr \"$cmd\" : 'g\^.*\^.*\^\(.*\)'`"
			cmd="`echo \"$lastcmd\" | sed -e \"s@$tmp@$wanted@g\"`\"$rest\""
			echoit=yes
			getcmd=no
			continue
			;;
		\^*\^*\^*)
			tmp="`expr \"$cmd\" : '\^\(.*\)\^.*\^.*'`"
			wanted="`expr \"$cmd\" : '\^.*\^\(.*\)\^.*'`"
			rest="`expr \"$cmd\" : '\^.*\^.*\^\(.*\)'`"
			cmd="`echo \"$lastcmd\" | sed -e \"s@$tmp@$wanted@\"`$rest"
			echoit=yes
			getcmd=no
			continue
			;;
		g\^*\^*)
			tmp="`expr \"$cmd\" : 'g\^\(.*\)\^.*'`"
			wanted="`expr \"$cmd\" : 'g\^.*\^\(.*\)'`"
			cmd="`echo \"$lastcmd\" | sed -e \"s@$tmp@$wanted@g\"`"
			echoit=yes
			getcmd=no
			continue
			;;
		\^*\^*)
			tmp="`expr \"$cmd\" : '\^\(.*\)\^.*'`"
			wanted="`expr \"$cmd\" : '\^.*\^\(.*\)'`"
			cmd="`echo \"$lastcmd\" | sed -e \"s@$tmp@$wanted@\"`"
			echoit=yes
			getcmd=no
			continue
			;;
		*~e)
			echo "$cmd" | sed -e "s@~e@@" > /tmp/doit$$
			$EDITOR /tmp/doit$$
			cmd="`cat /tmp/doit$$`"
			getcmd=no
			continue
			;;
		*~v)
			echo "$cmd" | sed -e "s@~v@@" > /tmp/doit$$
			echo "$lastcmd" > /tmp/doit$$
			$VISUAL /tmp/doit$$
			cmd="`cat /tmp/doit$$`"
			getcmd=no
			continue
			;;
		wait)
			$cms
			;;
		exec[\ \	]*)
			tail -$savehist $histfile>/tmp/hist$$
			uniq /tmp/hist$$ > $histfile
			rm -f /tmp/*$$
			$cmd
			;;
		umask[\ \	]*|login[\ \	]*|newgrp[\ \	]*)
			tail -$savehist $histfile>/tmp/hist$$
			uniq /tmp/hist$$ > $histfile
			rm -f /tmp/*$$
			exec $cmd
			;;
		logout|exit|bye)
			if test -s "${logoutfile-$HOME/.blogout}"
				then
				sh $logoutfile
			fi
			tail -$savehist $histfile > /tmp/hist$$
			uniq /tmp/hist$$ > $histfile
			rm -f /tmp/*$$
			exit 0
			;;
		h|history)
			grep -n . $histfile | tail -$history | sed -e 's@:@	@' | $PAGER
			continue
			;;
		h\ \|*|h\ \>*|h\|*|h\>*)
			cmd="`echo \"$cmd\" | sed -e \"s@h@grep -n . $histfile | tail -$history | sed -e 's@:@	@'@\"`"
			getcmd=no
			continue
			;;
		history*\|*|history*\>*)
			cmd="`echo \"$cmd\" | sed -e \"s@history@grep -n . $histfile | tail -$history | sed -e 's@:@ @'@\"`"
			getcmd=no
			continue
			;;
		.[\ \	]*)
			$cmd
			;;
		cd|cd[\ \	]*)
			
			: check if it will work first, or else this shell will terminate
			: if the cd dies.  If you have a built-in test, you might want
			: to replace the try-it-and-see below with a couple of tests,
			: but it is probably just as fast like this.

			if ($cmd)
				then
				$cmd
			fi
			run=no
			;;
		awk[\ \	]*|dd[\ \	]*|cc[\ \	]*|make[\ \	]*)
			: these are the only commands I can think of whose syntax
			: includes an equals sign.  Add others as you find them.

			echo "$cmd" > /tmp/doit$$
			;;
		setenv*|*=*)

			: handle setting shell variables, turning cshell syntax to Bourne
			: syntax -- note all variables must be exported or they will not
			: be usable in other commands

			echo "$cmd" > /tmp/cmd$$
			ed - /tmp/cmd$$ << ++++
			g/^setenv[ 	]/s/[ 	]/@/
			g/^setenv@/s/[ 	]/=/
			g/^setenv@/s///
			g/^set/s///
			.t.
			\$s/=.*//
			s/^/export /
			w
++++
			. /tmp/cmd$$
			rm -f /tmp/cmd$$
			run=no
			;;
		export[\ \	]*|set[\ \	]*)

			: handle commands which twiddle current environment

			$cmd
			run=no
			;;
		alias)
			$PAGER $aliasfile
			lastcmd=$cmd
			run=no
			continue
			;;
		alias[\ \	]*)
			case "$cmd" in
				alias[\ \	]\|*|alias[\ \	]\>*)
					cmd="`echo \"$cmd\" | sed -e \"s@alias@cat $aliasfile@\"`"
					getcmd=no
					continue
					;;
				alias[\ \	]*)
					;;
				*)
					echo "Syntax: alias name command"
					cmd=
					continue
					;;
			esac
			set - $cmd
			shift
			cmd=$*

			: make sure there is always 1 blank line in file so
			: unaliasing will always work -- ed normally refuses
			: to write an empty file

			echo "" >> $aliasfile
			cat << ++++ >> $aliasfile
$cmd
++++
			ed - $aliasfile << '++++'
			g/alias[ 	]/s///
			g/^['"]\(.*\)['"]$/s//\1/
			g/^/s//alias	/
			w
++++
			sort -u -o $aliasfile $aliasfile
			doalias=yes
			cmd="alias $cmd"
			run=no
			;;
		unalias*)
			set - $cmd
			case "$#" in
				2)
					cmd=$2
					;;
				*)
					echo "Syntax: unalias alias_name"
					continue
					;;
			esac
			ed - $aliasfile << ++++
			/^$cmd[ 	]/d
			w
++++
			case "`set - \`wc -l $aliasfile\`;echo $1``" in
				1)
					: just removed last alias

					doalias=no
					;;
			esac
			run=no
			;;
		*)
			case "$doalias" in
				yes)
					set - $cmd
					tmp="`grep \"^$1 \" $aliasfile`"
					case "$tmp" in
						$1[\ \	]*)
							shift
							cmd=$*
							set - $tmp
							shift
							tmp=$*
							case "$tmp" in
								*\$*)
									: uses positional variables

									cmd="set - $cmd ; $tmp"
									getcmd=no
									continue
									;;
								*)
									cmd="$tmp $cmd"
									getcmd=no
									continue
									;;
							esac
							;;
						*)
							echo "$cmd" > /tmp/doit$$
							;;
					esac
					;;
				no)
					echo "$cmd" > /tmp/doit$$
					;;
			esac
			;;
	esac
	case "$cmd" in
		*+~+p)
			cmd="`expr \"$cmd\" : '\(.*\)+~+p'`"
			echoit=yes
			run=no
			;;
	esac
	case "$cmd" in
		"")
			continue
			;;
		*)
			case "$exclaim" in
				yes)
					cmd="`echo \"$cmd\" | sed -e 's@REAL EXCLAMATION MARK@!@g'`"
					echo "$cmd" > /tmp/doit$$
					;;
			esac
			case "$echoit" in
				yes)
					echo $cmd
					;;
			esac
			case "$run" in
				yes)
					echo "trap 'exit 1' 2 3" > /tmp/bcsh$$
					cat /tmp/doit$$ >> /tmp/bcsh$$
					sh /tmp/bcsh$$
					;;
			esac
			case "$cmd" in
				$lastcmd)
					;;
				*)
					case "$exclaim" in
						yes)
							cmd="`echo \"$cmd\" | sed -e 's@!@\\\\!@g'`"
							;;
					esac
					cat << ++++ >> $histfile
$cmd
++++
					lastcmd=$cmd

					cmdno="`expr \"$cmdno\" + 1`"
					;;
			esac
			;;
	esac

	: The next commented-out line sets the prompt to include the command
	: number -- you should only un-comment this if it is the ONLY thing
	: you ever want as your prompt, because it will override attempts
	: to set PS1 from the command level.  If you want the command number
	: in your prompt without sacrificing the ability to change the prompt
	: later, replace the default setting for PS1 before the beginning of
	: the main loop with the following:  PS1='echo -n "${cmdno}% "'
	: Doing it this way is, however, slower than the simple version below.
	 
	# PS1="${cmdno}% "

	getcmd=yes
	echoit=no
	exclaim=no
	set -
done
exit 0
-- 

Christine Robertson  {linus, ihnp4, decvax}!utzoo!globetek!chris

Money may not buy happiness, but misery in luxury has its compensations...