Initial revision
diff --git a/src/cmd/mk/LICENSE b/src/cmd/mk/LICENSE
new file mode 100644
index 0000000..a5d7d87
--- /dev/null
+++ b/src/cmd/mk/LICENSE
@@ -0,0 +1,258 @@
+The Plan 9 software is provided under the terms of the
+Lucent Public License, Version 1.02, reproduced below,
+with the following exceptions:
+
+1. No right is granted to create derivative works of or
+   to redistribute (other than with the Plan 9 Operating System)
+   the screen imprinter fonts identified in subdirectory
+   /lib/font/bit/lucida and printer fonts (Lucida Sans Unicode, Lucida
+   Sans Italic, Lucida Sans Demibold, Lucida Typewriter, Lucida Sans
+   Typewriter83), identified in subdirectory /sys/lib/postscript/font.
+   These directories contain material copyrights by B&H Inc. and Y&Y Inc.
+
+2. The printer fonts identified in subdirectory /sys/lib/ghostscript/font
+   are subject to the GNU GPL, reproduced in the file /LICENSE.gpl.
+
+3. The ghostscript program in the subdirectory /sys/src/cmd/gs is
+   covered by the Aladdin Free Public License, reproduced in the file
+   /LICENSE.afpl.
+
+===================================================================
+
+Lucent Public License Version 1.02
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE
+PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+  a. in the case of Lucent Technologies Inc. ("LUCENT"), the Original
+     Program, and
+  b. in the case of each Contributor,
+
+     i. changes to the Program, and
+    ii. additions to the Program;
+
+    where such changes and/or additions to the Program were added to the
+    Program by such Contributor itself or anyone acting on such
+    Contributor's behalf, and the Contributor explicitly consents, in
+    accordance with Section 3C, to characterization of the changes and/or
+    additions as Contributions.
+
+"Contributor" means LUCENT and any other entity that has Contributed a
+Contribution to the Program.
+
+"Distributor" means a Recipient that distributes the Program,
+modifications to the Program, or any part thereof.
+
+"Licensed Patents" mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its Contribution
+alone or when combined with the Program.
+
+"Original Program" means the original version of the software
+accompanying this Agreement as released by LUCENT, including source
+code, object code and documentation, if any.
+
+"Program" means the Original Program and Contributions or any part
+thereof
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+ a. Subject to the terms of this Agreement, each Contributor hereby
+    grants Recipient a non-exclusive, worldwide, royalty-free copyright
+    license to reproduce, prepare derivative works of, publicly display,
+    publicly perform, distribute and sublicense the Contribution of such
+    Contributor, if any, and such derivative works, in source code and
+    object code form.
+    
+ b. Subject to the terms of this Agreement, each Contributor hereby
+    grants Recipient a non-exclusive, worldwide, royalty-free patent
+    license under Licensed Patents to make, use, sell, offer to sell,
+    import and otherwise transfer the Contribution of such Contributor, if
+    any, in source code and object code form. The patent license granted
+    by a Contributor shall also apply to the combination of the
+    Contribution of that Contributor and the Program if, at the time the
+    Contribution is added by the Contributor, such addition of the
+    Contribution causes such combination to be covered by the Licensed
+    Patents. The patent license granted by a Contributor shall not apply
+    to (i) any other combinations which include the Contribution, nor to
+    (ii) Contributions of other Contributors. No hardware per se is
+    licensed hereunder.
+    
+ c. Recipient understands that although each Contributor grants the
+    licenses to its Contributions set forth herein, no assurances are
+    provided by any Contributor that the Program does not infringe the
+    patent or other intellectual property rights of any other entity. Each
+    Contributor disclaims any liability to Recipient for claims brought by
+    any other entity based on infringement of intellectual property rights
+    or otherwise. As a condition to exercising the rights and licenses
+    granted hereunder, each Recipient hereby assumes sole responsibility
+    to secure any other intellectual property rights needed, if any. For
+    example, if a third party patent license is required to allow
+    Recipient to distribute the Program, it is Recipient's responsibility
+    to acquire that license before distributing the Program.
+
+ d. Each Contributor represents that to its knowledge it has sufficient
+    copyright rights in its Contribution, if any, to grant the copyright
+    license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A. Distributor may choose to distribute the Program in any form under
+this Agreement or under its own license agreement, provided that:
+
+ a. it complies with the terms and conditions of this Agreement;
+
+ b. if the Program is distributed in source code or other tangible
+    form, a copy of this Agreement or Distributor's own license agreement
+    is included with each copy of the Program; and
+
+ c. if distributed under Distributor's own license agreement, such
+    license agreement:
+
+      i. effectively disclaims on behalf of all Contributors all warranties
+         and conditions, express and implied, including warranties or
+         conditions of title and non-infringement, and implied warranties or
+         conditions of merchantability and fitness for a particular purpose;
+     ii. effectively excludes on behalf of all Contributors all liability
+         for damages, including direct, indirect, special, incidental and
+         consequential damages, such as lost profits; and
+    iii. states that any provisions which differ from this Agreement are
+         offered by that Contributor alone and not by any other party.
+
+B. Each Distributor must include the following in a conspicuous
+   location in the Program:
+
+   Copyright (C) 2003, Lucent Technologies Inc. and others. All Rights
+   Reserved.
+
+C. In addition, each Contributor must identify itself as the
+originator of its Contribution in a manner that reasonably allows
+subsequent Recipients to identify the originator of the Contribution.
+Also, each Contributor must agree that the additions and/or changes
+are intended to be a Contribution. Once a Contribution is contributed,
+it may not thereafter be revoked.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial use
+of the Program, the Distributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for Contributors. Therefore, if a
+Distributor includes the Program in a commercial product offering,
+such Distributor ("Commercial Distributor") hereby agrees to defend
+and indemnify every Contributor ("Indemnified Contributor") against
+any losses, damages and costs (collectively"Losses") arising from
+claims, lawsuits and other legal actions brought by a third party
+against the Indemnified Contributor to the extent caused by the acts
+or omissions of such Commercial Distributor in connection with its
+distribution of the Program in a commercial product offering. The
+obligations in this section do not apply to any claims or Losses
+relating to any actual or alleged intellectual property infringement.
+In order to qualify, an Indemnified Contributor must: a) promptly
+notify the Commercial Distributor in writing of such claim, and b)
+allow the Commercial Distributor to control, and cooperate with the
+Commercial Distributor in, the defense and any related settlement
+negotiations. The Indemnified Contributor may participate in any such
+claim at its own expense.
+
+For example, a Distributor might include the Program in a commercial
+product offering, Product X. That Distributor is then a Commercial
+Distributor. If that Commercial Distributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Distributor's responsibility
+alone. Under this section, the Commercial Distributor would have to
+defend claims against the Contributors related to those performance
+claims and warranties, and if a court requires any Contributor to pay
+any damages as a result, the Commercial Distributor must pay those
+damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with its
+exercise of rights under this Agreement, including but not limited to
+the risks and costs of program errors, compliance with applicable
+laws, damage to or loss of data, programs or equipment, and
+unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR
+ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
+WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
+DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
+HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. EXPORT CONTROL
+
+Recipient agrees that Recipient alone is responsible for compliance
+with the United States export administration regulations (and the
+export control laws and regulation of any other countries).
+
+8. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed to the
+minimum extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against a Contributor with
+respect to a patent applicable to software (including a cross-claim or
+counterclaim in a lawsuit), then any patent licenses granted by that
+Contributor to such Recipient under this Agreement shall terminate as
+of the date such litigation is filed. In addition, if Recipient
+institutes patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Program
+itself (excluding combinations of the Program with other software or
+hardware) infringes such Recipient's patent(s), then such Recipient's
+rights granted under Section 2(b) shall terminate as of the date such
+litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it
+fails to comply with any of the material terms or conditions of this
+Agreement and does not cure such failure in a reasonable period of
+time after becoming aware of such noncompliance. If all Recipient's
+rights under this Agreement terminate, Recipient agrees to cease use
+and distribution of the Program as soon as reasonably practicable.
+However, Recipient's obligations under this Agreement and any licenses
+granted by Recipient relating to the Program shall continue and
+survive.
+
+LUCENT may publish new versions (including revisions) of this
+Agreement from time to time. Each new version of the Agreement will be
+given a distinguishing version number. The Program (including
+Contributions) may always be distributed subject to the version of the
+Agreement under which it was received. In addition, after a new
+version of the Agreement is published, Contributor may elect to
+distribute the Program (including its Contributions) under the new
+version. No one other than LUCENT has the right to modify this
+Agreement. Except as expressly stated in Sections 2(a) and 2(b) above,
+Recipient receives no rights or licenses to the intellectual property
+of any Contributor under this Agreement, whether expressly, by
+implication, estoppel or otherwise. All rights in the Program not
+expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this Agreement
+more than one year after the cause of action arose. Each party waives
+its rights to a jury trial in any resulting litigation.
+
diff --git a/src/cmd/mk/Make.FreeBSD-386 b/src/cmd/mk/Make.FreeBSD-386
new file mode 100644
index 0000000..087ed3a
--- /dev/null
+++ b/src/cmd/mk/Make.FreeBSD-386
@@ -0,0 +1,7 @@
+CC=gcc
+CFLAGS+=-Wall -Wno-missing-braces -Wno-parentheses -Wno-switch -O2 -g -c -I. -I$(PREFIX)/include
+O=o
+AR=ar
+ARFLAGS=rvc
+NAN=nan64.$O	# default, can be overriden by Make.$(SYSNAME)
+NAN=nan64.$O
diff --git a/src/cmd/mk/Make.HP-UX-9000 b/src/cmd/mk/Make.HP-UX-9000
new file mode 100644
index 0000000..edbdc11
--- /dev/null
+++ b/src/cmd/mk/Make.HP-UX-9000
@@ -0,0 +1,6 @@
+CC=cc
+CFLAGS=-O -c -Ae -I.
+O=o
+AR=ar
+ARFLAGS=rvc
+NAN=nan64.$O
diff --git a/src/cmd/mk/Make.Linux-386 b/src/cmd/mk/Make.Linux-386
new file mode 100644
index 0000000..74b0252
--- /dev/null
+++ b/src/cmd/mk/Make.Linux-386
@@ -0,0 +1,7 @@
+CC=gcc
+CFLAGS+=-Wall -Wno-missing-braces -Wno-parentheses -Wno-switch -O2 -g -c -I.
+O=o
+AR=ar
+ARFLAGS=rvc
+NAN=nan64.$O	# default, can be overriden by Make.$(SYSNAME)
+NAN=nan64.$O
diff --git a/src/cmd/mk/Make.OSF1-alpha b/src/cmd/mk/Make.OSF1-alpha
new file mode 100644
index 0000000..3d45279
--- /dev/null
+++ b/src/cmd/mk/Make.OSF1-alpha
@@ -0,0 +1,6 @@
+CC=cc
+CFLAGS+=-g -c -I.
+O=o
+AR=ar
+ARFLAGS=rvc
+NAN=nan64.$O
diff --git a/src/cmd/mk/Make.SunOS-sun4u b/src/cmd/mk/Make.SunOS-sun4u
new file mode 100644
index 0000000..c5fe67b
--- /dev/null
+++ b/src/cmd/mk/Make.SunOS-sun4u
@@ -0,0 +1,2 @@
+include Make.SunOS-sun4u-$(CC)
+NAN=nan64.$O
diff --git a/src/cmd/mk/Make.SunOS-sun4u-cc b/src/cmd/mk/Make.SunOS-sun4u-cc
new file mode 100644
index 0000000..829301d
--- /dev/null
+++ b/src/cmd/mk/Make.SunOS-sun4u-cc
@@ -0,0 +1,6 @@
+CC=cc
+CFLAGS+=-g -c -I. -O
+O=o
+AR=ar
+ARFLAGS=rvc
+NAN=nan64.$O
diff --git a/src/cmd/mk/Make.SunOS-sun4u-gcc b/src/cmd/mk/Make.SunOS-sun4u-gcc
new file mode 100644
index 0000000..5c41594
--- /dev/null
+++ b/src/cmd/mk/Make.SunOS-sun4u-gcc
@@ -0,0 +1,6 @@
+CC=gcc
+CFLAGS+=-Wall -Wno-missing-braces -Wno-parentheses -Wno-switch -O2 -g -c
+O=o
+AR=ar
+ARFLAGS=rvc
+NAN=nan64.$O
diff --git a/src/cmd/mk/Makefile b/src/cmd/mk/Makefile
new file mode 100644
index 0000000..df21b60
--- /dev/null
+++ b/src/cmd/mk/Makefile
@@ -0,0 +1,117 @@
+
+# this works in gnu make
+SYSNAME:=${shell uname}
+OBJTYPE:=${shell uname -m | sed 's;i.86;386;; s;/.*;;; s; ;;g'}
+
+# this works in bsd make
+SYSNAME!=uname
+OBJTYPE!=uname -m | sed 's;i.86;386;; s;/.*;;; s; ;;g'
+
+# the gnu rules will mess up bsd but not vice versa,
+# hence the gnu rules come first.
+
+include Make.$(SYSNAME)-$(OBJTYPE)
+
+PREFIX=/usr/local
+
+NUKEFILES=
+
+TGZFILES=
+
+TARG=mk
+VERSION=2.0
+PORTPLACE=devel/mk
+NAME=mk
+
+OFILES=\
+	arc.$O\
+	archive.$O\
+	bufblock.$O\
+	env.$O\
+	file.$O\
+	graph.$O\
+	job.$O\
+	lex.$O\
+	main.$O\
+	match.$O\
+	mk.$O\
+	parse.$O\
+	recipe.$O\
+	rule.$O\
+	run.$O\
+	sh.$O\
+	shprint.$O\
+	symtab.$O\
+	var.$O\
+	varsub.$O\
+	word.$O\
+	unix.$O\
+
+HFILES=\
+	mk.h\
+	fns.h\
+
+all: $(TARG)
+
+TGZFILES+=mk.pdf
+
+install: $(LIB)
+	test -d $(PREFIX)/man/man1 || mkdir $(PREFIX)/man/man1
+	test -d $(PREFIX)/doc || mkdir $(PREFIX)/doc
+	install -m 0755 mk $(PREFIX)/bin/mk
+	cat mk.1 | sed 's;DOCPREFIX;$(PREFIX);g' >mk.1a
+	install -m 0644 mk.1a $(PREFIX)/man/man1/mk.1
+	install -m 0644 mk.pdf $(PREFIX)/doc/mk.pdf
+
+
+$(TARG): $(OFILES)
+	$(CC) -o $(TARG) $(OFILES) -L$(PREFIX)/lib -lregexp9 -lbio -lfmt -lutf
+
+
+.c.$O:
+	$(CC) $(CFLAGS) -I$(PREFIX)/include $*.c
+
+%.$O: %.c
+	$(CC) $(CFLAGS) -I$(PREFIX)/include $*.c
+
+
+$(OFILES): $(HFILES)
+
+tgz:
+	rm -rf $(NAME)-$(VERSION)
+	mkdir $(NAME)-$(VERSION)
+	cp Makefile Make.* README LICENSE NOTICE *.[ch137] rpm.spec bundle.ports $(TGZFILES) $(NAME)-$(VERSION)
+	tar cf - $(NAME)-$(VERSION) | gzip >$(NAME)-$(VERSION).tgz
+	rm -rf $(NAME)-$(VERSION)
+
+clean:
+	rm -f $(OFILES) $(LIB)
+
+nuke:
+	rm -f $(OFILES) *.tgz *.rpm $(NUKEFILES)
+
+rpm:
+	make tgz
+	cp $(NAME)-$(VERSION).tgz /usr/src/RPM/SOURCES
+	rpm -ba rpm.spec
+	cp /usr/src/RPM/SRPMS/$(NAME)-$(VERSION)-1.src.rpm .
+	cp /usr/src/RPM/RPMS/i586/$(NAME)-$(VERSION)-1.i586.rpm .
+	scp *.rpm rsc@amsterdam.lcs.mit.edu:public_html/software
+
+PORTDIR=/usr/ports/$(PORTPLACE)
+
+ports:
+	make tgz
+	rm -rf $(PORTDIR)
+	mkdir $(PORTDIR)
+	cp $(NAME)-$(VERSION).tgz /usr/ports/distfiles
+	cat bundle.ports | (cd $(PORTDIR) && awk '$$1=="---" && $$3=="---" { ofile=$$2; next} {if(ofile) print >ofile}')
+	(cd $(PORTDIR); make makesum)
+	(cd $(PORTDIR); make)
+	(cd $(PORTDIR); /usr/local/bin/portlint)
+	rm -rf $(PORTDIR)/work
+	shar `find $(PORTDIR)` > ports.shar
+	(cd $(PORTDIR); tar cf - *) | gzip >$(NAME)-$(VERSION)-ports.tgz
+	scp *.tgz rsc@amsterdam.lcs.mit.edu:public_html/software
+
+.phony: all clean nuke install tgz rpm ports
diff --git a/src/cmd/mk/Makefile.MID b/src/cmd/mk/Makefile.MID
new file mode 100644
index 0000000..f840b18
--- /dev/null
+++ b/src/cmd/mk/Makefile.MID
@@ -0,0 +1,45 @@
+TARG=mk
+VERSION=2.0
+PORTPLACE=devel/mk
+NAME=mk
+
+OFILES=\
+	arc.$O\
+	archive.$O\
+	bufblock.$O\
+	env.$O\
+	file.$O\
+	graph.$O\
+	job.$O\
+	lex.$O\
+	main.$O\
+	match.$O\
+	mk.$O\
+	parse.$O\
+	recipe.$O\
+	rule.$O\
+	run.$O\
+	sh.$O\
+	shprint.$O\
+	symtab.$O\
+	var.$O\
+	varsub.$O\
+	word.$O\
+	unix.$O\
+
+HFILES=\
+	mk.h\
+	fns.h\
+
+all: $(TARG)
+
+TGZFILES+=mk.pdf
+
+install: $(LIB)
+	test -d $(PREFIX)/man/man1 || mkdir $(PREFIX)/man/man1
+	test -d $(PREFIX)/doc || mkdir $(PREFIX)/doc
+	install -m 0755 mk $(PREFIX)/bin/mk
+	cat mk.1 | sed 's;DOCPREFIX;$(PREFIX);g' >mk.1a
+	install -m 0644 mk.1a $(PREFIX)/man/man1/mk.1
+	install -m 0644 mk.pdf $(PREFIX)/doc/mk.pdf
+
diff --git a/src/cmd/mk/arc.c b/src/cmd/mk/arc.c
new file mode 100644
index 0000000..521ef7a
--- /dev/null
+++ b/src/cmd/mk/arc.c
@@ -0,0 +1,52 @@
+#include	"mk.h"
+
+Arc *
+newarc(Node *n, Rule *r, char *stem, Resub *match)
+{
+	Arc *a;
+
+	a = (Arc *)Malloc(sizeof(Arc));
+	a->n = n;
+	a->r = r;
+	a->stem = strdup(stem);
+	rcopy(a->match, match, NREGEXP);
+	a->next = 0;
+	a->flag = 0;
+	a->prog = r->prog;
+	return(a);
+}
+
+void
+dumpa(char *s, Arc *a)
+{
+	char buf[1024];
+
+	Bprint(&bout, "%sArc@%p: n=%p r=%p flag=0x%x stem='%s'",
+		s, a, a->n, a->r, a->flag, a->stem);
+	if(a->prog)
+		Bprint(&bout, " prog='%s'", a->prog);
+	Bprint(&bout, "\n");
+
+	if(a->n){
+		snprint(buf, sizeof(buf), "%s    ", (*s == ' ')? s:"");
+		dumpn(buf, a->n);
+	}
+}
+
+void
+nrep(void)
+{
+	Symtab *sym;
+	Word *w;
+
+	sym = symlook("NREP", S_VAR, 0);
+	if(sym){
+		w = (Word *) sym->value;
+		if (w && w->s && *w->s)
+			nreps = atoi(w->s);
+	}
+	if(nreps < 1)
+		nreps = 1;
+	if(DEBUG(D_GRAPH))
+		Bprint(&bout, "nreps = %d\n", nreps);
+}
diff --git a/src/cmd/mk/archive.c b/src/cmd/mk/archive.c
new file mode 100644
index 0000000..9ba7a12
--- /dev/null
+++ b/src/cmd/mk/archive.c
@@ -0,0 +1,180 @@
+#include	"mk.h"
+#define	ARMAG	"!<arch>\n"
+#define	SARMAG	8
+
+#define	ARFMAG	"`\n"
+#define SARNAME	16
+
+struct	ar_hdr
+{
+	char	name[SARNAME];
+	char	date[12];
+	char	uid[6];
+	char	gid[6];
+	char	mode[8];
+	char	size[10];
+	char	fmag[2];
+};
+#define	SAR_HDR	(SARNAME+44)
+
+static int dolong;
+
+static void atimes(char *);
+static char *split(char*, char**);
+
+long
+atimeof(int force, char *name)
+{
+	Symtab *sym;
+	long t;
+	char *archive, *member, buf[512];
+
+	archive = split(name, &member);
+	if(archive == 0)
+		Exit();
+
+	t = mtime(archive);
+	sym = symlook(archive, S_AGG, 0);
+	if(sym){
+		if(force || (t > (long)sym->value)){
+			atimes(archive);
+			sym->value = (void *)t;
+		}
+	}
+	else{
+		atimes(archive);
+		/* mark the aggegate as having been done */
+		symlook(strdup(archive), S_AGG, "")->value = (void *)t;
+	}
+		/* truncate long member name to sizeof of name field in archive header */
+	if(dolong)
+		snprint(buf, sizeof(buf), "%s(%s)", archive, member);
+	else
+		snprint(buf, sizeof(buf), "%s(%.*s)", archive, SARNAME, member);
+	sym = symlook(buf, S_TIME, 0);
+	if (sym)
+		return (long)sym->value;	/* uggh */
+	return 0;
+}
+
+void
+atouch(char *name)
+{
+	char *archive, *member;
+	int fd, i;
+	struct ar_hdr h;
+	long t;
+
+	archive = split(name, &member);
+	if(archive == 0)
+		Exit();
+
+	fd = open(archive, ORDWR);
+	if(fd < 0){
+		fd = create(archive, OWRITE, 0666);
+		if(fd < 0){
+			fprint(2, "create %s: %r\n", archive);
+			Exit();
+		}
+		write(fd, ARMAG, SARMAG);
+	}
+	if(symlook(name, S_TIME, 0)){
+		/* hoon off and change it in situ */
+		LSEEK(fd, SARMAG, 0);
+		while(read(fd, (char *)&h, sizeof(h)) == sizeof(h)){
+			for(i = SARNAME-1; i > 0 && h.name[i] == ' '; i--)
+					;
+			h.name[i+1]=0;
+			if(strcmp(member, h.name) == 0){
+				t = SARNAME-sizeof(h);	/* ughgghh */
+				LSEEK(fd, t, 1);
+				fprint(fd, "%-12ld", time(0));
+				break;
+			}
+			t = atol(h.size);
+			if(t&01) t++;
+			LSEEK(fd, t, 1);
+		}
+	}
+	close(fd);
+}
+
+static void
+atimes(char *ar)
+{
+	struct ar_hdr h;
+	long t;
+	int fd, i;
+	char buf[BIGBLOCK];
+	char name[sizeof(h.name)+1];
+
+	fd = open(ar, OREAD);
+	if(fd < 0)
+		return;
+
+	if(read(fd, buf, SARMAG) != SARMAG){
+		close(fd);
+		return;
+	}
+	while(read(fd, (char *)&h, sizeof(h)) == sizeof(h)){
+		t = atol(h.date);
+		if(t == 0)	/* as it sometimes happens; thanks ken */
+			t = 1;
+		strncpy(name, h.name, sizeof(h.name));
+		for(i = sizeof(h.name)-1; i > 0 && name[i] == ' '; i--)
+				;
+		if(name[i] == '/')		/* system V bug */
+			i--;
+		name[i+1]=0;
+		sprint(buf, "%s(%s)", ar, h.size);
+		symlook(strdup(buf), S_TIME, (void *)t)->value = (void *)t;
+		t = atol(h.size);
+		if(t&01) t++;
+		LSEEK(fd, t, 1);
+	}
+	close(fd);
+}
+
+static int
+type(char *file)
+{
+	int fd;
+	char buf[SARMAG];
+
+	fd = open(file, OREAD);
+	if(fd < 0){
+		if(symlook(file, S_BITCH, 0) == 0){
+			Bprint(&bout, "%s doesn't exist: assuming it will be an archive\n", file);
+			symlook(file, S_BITCH, (void *)file);
+		}
+		return 1;
+	}
+	if(read(fd, buf, SARMAG) != SARMAG){
+		close(fd);
+		return 0;
+	}
+	close(fd);
+	return !strncmp(ARMAG, buf, SARMAG);
+}
+
+static char*
+split(char *name, char **member)
+{
+	char *p, *q;
+
+	p = strdup(name);
+	q = utfrune(p, '(');
+	if(q){
+		*q++ = 0;
+		if(member)
+			*member = q;
+		q = utfrune(q, ')');
+		if (q)
+			*q = 0;
+		if(type(p))
+			return p;
+		free(p);
+		fprint(2, "mk: '%s' is not an archive\n", name);
+	}
+	return 0;
+}
diff --git a/src/cmd/mk/bundle.ports b/src/cmd/mk/bundle.ports
new file mode 100644
index 0000000..4649831
--- /dev/null
+++ b/src/cmd/mk/bundle.ports
@@ -0,0 +1,46 @@
+--- Makefile ---
+# New ports collection makefile for: mk
+# Date Created:		11 Feb 2003
+# Whom:			rsc
+#
+# THIS LINE NEEDS REPLACING.  IT'S HERE TO GET BY PORTLINT
+# $FreeBSD: ports/devel/mk/Makefile,v 1.1 2003/02/12 00:51:22 rsc Exp $
+
+PORTNAME=	mk
+PORTVERSION=	2.0
+CATEGORIES=	devel
+MASTER_SITES=	http://pdos.lcs.mit.edu/~rsc/software/
+EXTRACT_SUFX=	.tgz
+
+MAINTAINER=	rsc@post.harvard.edu
+
+DEPENDS=	${PORTSDIR}/devel/libutf \
+	${PORTSDIR}/devel/libfmt \
+	${PORTSDIR}/devel/libbio \
+	${PORTSDIR}/devel/libregexp9
+
+MAN1=		mk.1
+USE_REINPLACE=	yes
+
+.include <bsd.port.pre.mk>
+
+post-patch:
+	${REINPLACE_CMD} -e 's,$$(PREFIX),${PREFIX},g' ${WRKSRC}/Makefile
+
+.include <bsd.port.post.mk>
+--- pkg-comment ---
+Streamlined replacement for make
+--- pkg-descr ---
+Mk is a streamlined replacement for make, written for 
+Tenth Edition Research Unix by Andrew Hume.
+
+WWW: http://pdos.lcs.mit.edu/~rsc/software/#mk
+
+Russ Cox
+rsc@post.harvard.edu
+--- pkg-plist ---
+bin/mk
+doc/mk.pdf
+--- /dev/null ---
+This is just a way to make sure blank lines don't
+creep into pkg-plist.
diff --git a/src/cmd/mk/env.c b/src/cmd/mk/env.c
new file mode 100644
index 0000000..c040db5
--- /dev/null
+++ b/src/cmd/mk/env.c
@@ -0,0 +1,149 @@
+#include	"mk.h"
+
+enum {
+	ENVQUANTA=10
+};
+
+Envy	*envy;
+static int nextv;
+
+static char	*myenv[] =
+{
+	"target",
+	"stem",
+	"prereq",
+	"pid",
+	"nproc",
+	"newprereq",
+	"alltarget",
+	"newmember",
+	"stem0",		/* must be in order from here */
+	"stem1",
+	"stem2",
+	"stem3",
+	"stem4",
+	"stem5",
+	"stem6",
+	"stem7",
+	"stem8",
+	"stem9",
+	0,
+};
+
+void
+initenv(void)
+{
+	char **p;
+
+	for(p = myenv; *p; p++)
+		symlook(*p, S_INTERNAL, (void *)"");
+	readenv();				/* o.s. dependent */
+}
+
+static void
+envinsert(char *name, Word *value)
+{
+	static int envsize;
+
+	if (nextv >= envsize) {
+		envsize += ENVQUANTA;
+		envy = (Envy *) Realloc((char *) envy, envsize*sizeof(Envy));
+	}
+	envy[nextv].name = name;
+	envy[nextv++].values = value;
+}
+
+static void
+envupd(char *name, Word *value)
+{
+	Envy *e;
+
+	for(e = envy; e->name; e++)
+		if(strcmp(name, e->name) == 0){
+			delword(e->values);
+			e->values = value;
+			return;
+		}
+	e->name = name;
+	e->values = value;
+	envinsert(0,0);
+}
+
+static void
+ecopy(Symtab *s)
+{
+	char **p;
+
+	if(symlook(s->name, S_NOEXPORT, 0))
+		return;
+	for(p = myenv; *p; p++)
+		if(strcmp(*p, s->name) == 0)
+			return;
+	envinsert(s->name, (Word *) s->value);
+}
+
+void
+execinit(void)
+{
+	char **p;
+
+	nextv = 0;
+	for(p = myenv; *p; p++)
+		envinsert(*p, stow(""));
+
+	symtraverse(S_VAR, ecopy);
+	envinsert(0, 0);
+}
+
+Envy*
+buildenv(Job *j, int slot)
+{
+	char **p, *cp, *qp;
+	Word *w, *v, **l;
+	int i;
+	char buf[256];
+
+	envupd("target", wdup(j->t));
+	if(j->r->attr&REGEXP)
+		envupd("stem",newword(""));
+	else
+		envupd("stem", newword(j->stem));
+	envupd("prereq", wdup(j->p));
+	sprint(buf, "%d", getpid());
+	envupd("pid", newword(buf));
+	sprint(buf, "%d", slot);
+	envupd("nproc", newword(buf));
+	envupd("newprereq", wdup(j->np));
+	envupd("alltarget", wdup(j->at));
+	l = &v;
+	v = w = wdup(j->np);
+	while(w){
+		cp = strchr(w->s, '(');
+		if(cp){
+			qp = strchr(cp+1, ')');
+			if(qp){
+				*qp = 0;
+				strcpy(w->s, cp+1);
+				l = &w->next;
+				w = w->next;
+				continue;
+			}
+		}
+		*l = w->next;
+		free(w->s);
+		free(w);
+		w = *l;
+	}
+	envupd("newmember", v);
+		/* update stem0 -> stem9 */
+	for(p = myenv; *p; p++)
+		if(strcmp(*p, "stem0") == 0)
+			break;
+	for(i = 0; *p; i++, p++){
+		if((j->r->attr&REGEXP) && j->match[i])
+			envupd(*p, newword(j->match[i]));
+		else 
+			envupd(*p, newword(""));
+	}
+	return envy;
+}
diff --git a/src/cmd/mk/file.c b/src/cmd/mk/file.c
new file mode 100644
index 0000000..2533343
--- /dev/null
+++ b/src/cmd/mk/file.c
@@ -0,0 +1,90 @@
+#include	"mk.h"
+
+/* table-driven version in bootes dump of 12/31/96 */
+
+long
+mtime(char *name)
+{
+	return mkmtime(name);
+}
+
+long
+timeof(char *name, int force)
+{
+	Symtab *sym;
+	long t;
+
+	if(utfrune(name, '('))
+		return atimeof(force, name);	/* archive */
+
+	if(force)
+		return mtime(name);
+
+
+	sym = symlook(name, S_TIME, 0);
+	if (sym)
+		return (long) sym->value;		/* uggh */
+
+	t = mtime(name);
+	if(t == 0)
+		return 0;
+
+	symlook(name, S_TIME, (void*)t);		/* install time in cache */
+	return t;
+}
+
+void
+touch(char *name)
+{
+	Bprint(&bout, "touch(%s)\n", name);
+	if(nflag)
+		return;
+
+	if(utfrune(name, '('))
+		atouch(name);		/* archive */
+	else if(chgtime(name) < 0) {
+		fprint(2, "%s: %r\n", name);
+		Exit();
+	}
+}
+
+void
+delete(char *name)
+{
+	if(utfrune(name, '(') == 0) {		/* file */
+		if(remove(name) < 0)
+			fprint(2, "remove %s: %r\n", name);
+	} else
+		fprint(2, "hoon off; mk can'tdelete archive members\n");
+}
+
+void
+timeinit(char *s)
+{
+	long t;
+	char *cp;
+	Rune r;
+	int c, n;
+
+	t = time(0);
+	while (*s) {
+		cp = s;
+		do{
+			n = chartorune(&r, s);
+			if (r == ' ' || r == ',' || r == '\n')
+				break;
+			s += n;
+		} while(*s);
+		c = *s;
+		*s = 0;
+		symlook(strdup(cp), S_TIME, (void *)t)->value = (void *)t;
+		if (c)
+			*s++ = c;
+		while(*s){
+			n = chartorune(&r, s);
+			if(r != ' ' && r != ',' && r != '\n')
+				break;
+			s += n;
+		}
+	}
+}
diff --git a/src/cmd/mk/fns.h b/src/cmd/mk/fns.h
new file mode 100644
index 0000000..2c9f46a
--- /dev/null
+++ b/src/cmd/mk/fns.h
@@ -0,0 +1,84 @@
+void	addrule(char*, Word*, char*, Word*, int, int, char*);
+void	addrules(Word*, Word*, char*, int, int, char*);
+void	addw(Word*, char*);
+void	assert(char*, int);
+int	assline(Biobuf *, Bufblock *);
+long	atimeof(int,char*);
+void	atouch(char*);
+void	bufcpy(Bufblock *, char *, int);
+Envy	*buildenv(Job*, int);
+void	catchnotes(void);
+char 	*charin(char *, char *);
+int	chgtime(char*);
+void	clrmade(Node*);
+char	*copyq(char*, Rune, Bufblock*);
+void	delete(char*);
+void	delword(Word*);
+int	dorecipe(Node*);
+void	dumpa(char*, Arc*);
+void	dumpj(char*, Job*, int);
+void	dumpn(char*, Node*);
+void	dumpr(char*, Rule*);
+void	dumpv(char*);
+void	dumpw(char*, Word*);
+int	escapetoken(Biobuf*, Bufblock*, int, int);
+void	execinit(void);
+int	execsh(char*, char*, Bufblock*, Envy*);
+void	Exit(void);
+char	*expandquote(char*, Rune, Bufblock*);
+void	expunge(int, char*);
+void	freebuf(Bufblock*);
+void	front(char*);
+Node	*graph(char*);
+void	growbuf(Bufblock *);
+void	initenv(void);
+void	insert(Bufblock *, int);
+void	ipop(void);
+void	ipush(void);
+void	killchildren(char*);
+void	*Malloc(int);
+char	*maketmp(int*);
+int	match(char*, char*, char*);
+char *membername(char*, int, char*);
+void	mk(char*);
+ulong	mkmtime(char*);
+long	mtime(char*);
+Arc	*newarc(Node*, Rule*, char*, Resub*);
+Bufblock *newbuf(void);
+Job	*newjob(Rule*, Node*, char*, char**, Word*, Word*, Word*, Word*);
+Word	*newword(char*);
+int	nextrune(Biobuf*, int);
+int	nextslot(void);
+void	nproc(void);
+void	nrep(void);
+int	outofdate(Node*, Arc*, int);
+void	parse(char*, int, int);
+int	pipecmd(char*, Envy*, int*);
+void	prusage(void);
+void	rcopy(char**, Resub*, int);
+void	readenv(void);
+void	*Realloc(void*, int);
+void	rinsert(Bufblock *, Rune);
+char	*rulecnt(void);
+void	run(Job*);
+void	setvar(char*, void*);
+char	*shname(char*);
+void	shprint(char*, Envy*, Bufblock*);
+Word	*stow(char*);
+void	subst(char*, char*, char*);
+void	symdel(char*, int);
+void	syminit(void);
+Symtab	*symlook(char*, int, void*);
+void	symstat(void);
+void	symtraverse(int, void(*)(Symtab*));
+void	timeinit(char*);
+long	timeof(char*, int);
+void	touch(char*);
+void	update(int, Node*);
+void	usage(void);
+Word	*varsub(char**);
+int	waitfor(char*);
+int	waitup(int, int*);
+Word	*wdup(Word*);
+int	work(Node*, Node*, Arc*);
+char	*wtos(Word*, int);
diff --git a/src/cmd/mk/graph.c b/src/cmd/mk/graph.c
new file mode 100644
index 0000000..5852925
--- /dev/null
+++ b/src/cmd/mk/graph.c
@@ -0,0 +1,279 @@
+#include	"mk.h"
+
+static Node *applyrules(char *, char *);
+static void togo(Node *);
+static int vacuous(Node *);
+static Node *newnode(char *);
+static void trace(char *, Arc *);
+static void cyclechk(Node *);
+static void ambiguous(Node *);
+static void attribute(Node *);
+
+Node *
+graph(char *target)
+{
+	Node *node;
+	char *cnt;
+
+	cnt = rulecnt();
+	node = applyrules(target, cnt);
+	free(cnt);
+	cyclechk(node);
+	node->flags |= PROBABLE;	/* make sure it doesn't get deleted */
+	vacuous(node);
+	ambiguous(node);
+	attribute(node);
+	return(node);
+}
+
+static Node *
+applyrules(char *target, char *cnt)
+{
+	Symtab *sym;
+	Node *node;
+	Rule *r;
+	Arc head, *a = &head;
+	Word *w;
+	char stem[NAMEBLOCK], buf[NAMEBLOCK];
+	Resub rmatch[NREGEXP];
+
+/*	print("applyrules(%lux='%s')\n", target, target);*//**/
+	sym = symlook(target, S_NODE, 0);
+	if(sym)
+		return (Node *)(sym->value);
+	target = strdup(target);
+	node = newnode(target);
+	head.n = 0;
+	head.next = 0;
+	sym = symlook(target, S_TARGET, 0);
+	memset((char*)rmatch, 0, sizeof(rmatch));
+	for(r = sym? (Rule *)(sym->value):0; r; r = r->chain){
+		if(r->attr&META) continue;
+		if(strcmp(target, r->target)) continue;
+		if((!r->recipe || !*r->recipe) && (!r->tail || !r->tail->s || !*r->tail->s)) continue;	/* no effect; ignore */
+		if(cnt[r->rule] >= nreps) continue;
+		cnt[r->rule]++;
+		node->flags |= PROBABLE;
+
+/*		if(r->attr&VIR)
+ *			node->flags |= VIRTUAL;
+ *		if(r->attr&NOREC)
+ *			node->flags |= NORECIPE;
+ *		if(r->attr&DEL)
+ *			node->flags |= DELETE;
+ */
+		if(!r->tail || !r->tail->s || !*r->tail->s) {
+			a->next = newarc((Node *)0, r, "", rmatch);
+			a = a->next;
+		} else
+			for(w = r->tail; w; w = w->next){
+				a->next = newarc(applyrules(w->s, cnt), r, "", rmatch);
+				a = a->next;
+		}
+		cnt[r->rule]--;
+		head.n = node;
+	}
+	for(r = metarules; r; r = r->next){
+		if((!r->recipe || !*r->recipe) && (!r->tail || !r->tail->s || !*r->tail->s)) continue;	/* no effect; ignore */
+		if ((r->attr&NOVIRT) && a != &head && (a->r->attr&VIR))
+			continue;
+		if(r->attr&REGEXP){
+			stem[0] = 0;
+			patrule = r;
+			memset((char*)rmatch, 0, sizeof(rmatch));
+			if(regexec(r->pat, node->name, rmatch, NREGEXP) == 0)
+				continue;
+		} else {
+			if(!match(node->name, r->target, stem)) continue;
+		}
+		if(cnt[r->rule] >= nreps) continue;
+		cnt[r->rule]++;
+
+/*		if(r->attr&VIR)
+ *			node->flags |= VIRTUAL;
+ *		if(r->attr&NOREC)
+ *			node->flags |= NORECIPE;
+ *		if(r->attr&DEL)
+ *			node->flags |= DELETE;
+ */
+
+		if(!r->tail || !r->tail->s || !*r->tail->s) {
+			a->next = newarc((Node *)0, r, stem, rmatch);
+			a = a->next;
+		} else
+			for(w = r->tail; w; w = w->next){
+				if(r->attr&REGEXP)
+					regsub(w->s, buf, sizeof buf, rmatch, NREGEXP);
+				else
+					subst(stem, w->s, buf);
+				a->next = newarc(applyrules(buf, cnt), r, stem, rmatch);
+				a = a->next;
+			}
+		cnt[r->rule]--;
+	}
+	a->next = node->prereqs;
+	node->prereqs = head.next;
+	return(node);
+}
+
+static void
+togo(Node *node)
+{
+	Arc *la, *a;
+
+	/* delete them now */
+	la = 0;
+	for(a = node->prereqs; a; la = a, a = a->next)
+		if(a->flag&TOGO){
+			if(a == node->prereqs)
+				node->prereqs = a->next;
+			else
+				la->next = a->next, a = la;
+		}
+}
+
+static int
+vacuous(Node *node)
+{
+	Arc *la, *a;
+	int vac = !(node->flags&PROBABLE);
+
+	if(node->flags&READY)
+		return(node->flags&VACUOUS);
+	node->flags |= READY;
+	for(a = node->prereqs; a; a = a->next)
+		if(a->n && vacuous(a->n) && (a->r->attr&META))
+			a->flag |= TOGO;
+		else
+			vac = 0;
+	/* if a rule generated arcs that DON'T go; no others from that rule go */
+	for(a = node->prereqs; a; a = a->next)
+		if((a->flag&TOGO) == 0)
+			for(la = node->prereqs; la; la = la->next)
+				if((la->flag&TOGO) && (la->r == a->r)){
+					la->flag &= ~TOGO;
+				}
+	togo(node);
+	if(vac)
+		node->flags |= VACUOUS;
+	return(vac);
+}
+
+static Node *
+newnode(char *name)
+{
+	register Node *node;
+
+	node = (Node *)Malloc(sizeof(Node));
+	symlook(name, S_NODE, (void *)node);
+	node->name = name;
+	node->time = timeof(name, 0);
+	node->prereqs = 0;
+	node->flags = node->time? PROBABLE : 0;
+	node->next = 0;
+	return(node);
+}
+
+void
+dumpn(char *s, Node *n)
+{
+	char buf[1024];
+	Arc *a;
+
+	sprint(buf, "%s   ", (*s == ' ')? s:"");
+	Bprint(&bout, "%s%s@%ld: time=%ld flags=0x%x next=%ld\n",
+		s, n->name, n, n->time, n->flags, n->next);
+	for(a = n->prereqs; a; a = a->next)
+		dumpa(buf, a);
+}
+
+static void
+trace(char *s, Arc *a)
+{
+	fprint(2, "\t%s", s);
+	while(a){
+		fprint(2, " <-(%s:%d)- %s", a->r->file, a->r->line,
+			a->n? a->n->name:"");
+		if(a->n){
+			for(a = a->n->prereqs; a; a = a->next)
+				if(*a->r->recipe) break;
+		} else
+			a = 0;
+	}
+	fprint(2, "\n");
+}
+
+static void
+cyclechk(Node *n)
+{
+	Arc *a;
+
+	if((n->flags&CYCLE) && n->prereqs){
+		fprint(2, "mk: cycle in graph detected at target %s\n", n->name);
+		Exit();
+	}
+	n->flags |= CYCLE;
+	for(a = n->prereqs; a; a = a->next)
+		if(a->n)
+			cyclechk(a->n);
+	n->flags &= ~CYCLE;
+}
+
+static void
+ambiguous(Node *n)
+{
+	Arc *a;
+	Rule *r = 0;
+	Arc *la;
+	int bad = 0;
+
+	la = 0;
+	for(a = n->prereqs; a; a = a->next){
+		if(a->n)
+			ambiguous(a->n);
+		if(*a->r->recipe == 0) continue;
+		if(r == 0)
+			r = a->r, la = a;
+		else{
+			if(r->recipe != a->r->recipe){
+				if((r->attr&META) && !(a->r->attr&META)){
+					la->flag |= TOGO;
+					r = a->r, la = a;
+				} else if(!(r->attr&META) && (a->r->attr&META)){
+					a->flag |= TOGO;
+					continue;
+				}
+			}
+			if(r->recipe != a->r->recipe){
+				if(bad == 0){
+					fprint(2, "mk: ambiguous recipes for %s:\n", n->name);
+					bad = 1;
+					trace(n->name, la);
+				}
+				trace(n->name, a);
+			}
+		}
+	}
+	if(bad)
+		Exit();
+	togo(n);
+}
+
+static void
+attribute(Node *n)
+{
+	register Arc *a;
+
+	for(a = n->prereqs; a; a = a->next){
+		if(a->r->attr&VIR)
+			n->flags |= VIRTUAL;
+		if(a->r->attr&NOREC)
+			n->flags |= NORECIPE;
+		if(a->r->attr&DEL)
+			n->flags |= DELETE;
+		if(a->n)
+			attribute(a->n);
+	}
+	if(n->flags&VIRTUAL)
+		n->time = 0;
+}
diff --git a/src/cmd/mk/lex.c b/src/cmd/mk/lex.c
new file mode 100644
index 0000000..3ee244f
--- /dev/null
+++ b/src/cmd/mk/lex.c
@@ -0,0 +1,147 @@
+#include	"mk.h"
+
+static	int	bquote(Biobuf*, Bufblock*);
+
+/*
+ *	Assemble a line skipping blank lines, comments, and eliding
+ *	escaped newlines
+ */
+int
+assline(Biobuf *bp, Bufblock *buf)
+{
+	int c;
+	int lastc;
+
+	buf->current=buf->start;
+	while ((c = nextrune(bp, 1)) >= 0){
+		switch(c)
+		{
+		case '\r':		/* consumes CRs for Win95 */
+			continue;
+		case '\n':
+			if (buf->current != buf->start) {
+				insert(buf, 0);
+				return 1;
+			}
+			break;		/* skip empty lines */
+		case '\\':
+		case '\'':
+		case '"':
+			rinsert(buf, c);
+			if (escapetoken(bp, buf, 1, c) == 0)
+				Exit();
+			break;
+		case '`':
+			if (bquote(bp, buf) == 0)
+				Exit();
+			break;
+		case '#':
+			lastc = '#';
+			while ((c = Bgetc(bp)) != '\n') {
+				if (c < 0)
+					goto eof;
+				if(c != '\r')
+					lastc = c;
+			}
+			mkinline++;
+			if (lastc == '\\')
+				break;		/* propagate escaped newlines??*/
+			if (buf->current != buf->start) {
+				insert(buf, 0);
+				return 1;
+			}
+			break;
+		default:
+			rinsert(buf, c);
+			break;
+		}
+	}
+eof:
+	insert(buf, 0);
+	return *buf->start != 0;
+}
+
+/*
+ *	assemble a back-quoted shell command into a buffer
+ */
+static int
+bquote(Biobuf *bp, Bufblock *buf)
+{
+	int c, line, term;
+	int start;
+
+	line = mkinline;
+	while((c = Bgetrune(bp)) == ' ' || c == '\t')
+			;
+	if(c == '{'){
+		term = '}';		/* rc style */
+		while((c = Bgetrune(bp)) == ' ' || c == '\t')
+			;
+	} else
+		term = '`';		/* sh style */
+
+	start = buf->current-buf->start;
+	for(;c > 0; c = nextrune(bp, 0)){
+		if(c == term){
+			insert(buf, '\n');
+			insert(buf,0);
+			buf->current = buf->start+start;
+			execinit();
+			execsh(0, buf->current, buf, envy);
+			return 1;
+		}
+		if(c == '\n')
+			break;
+		if(c == '\'' || c == '"' || c == '\\'){
+			insert(buf, c);
+			if(!escapetoken(bp, buf, 1, c))
+				return 0;
+			continue;
+		}
+		rinsert(buf, c);
+	}
+	SYNERR(line);
+	fprint(2, "missing closing %c after `\n", term);
+	return 0;
+}
+
+/*
+ *	get next character stripping escaped newlines
+ *	the flag specifies whether escaped newlines are to be elided or
+ *	replaced with a blank.
+ */
+int
+nextrune(Biobuf *bp, int elide)
+{
+	int c, c2;
+	static int savec;
+
+	if(savec){
+		c = savec;
+		savec = 0;
+		return c;
+	}
+
+	for (;;) {
+		c = Bgetrune(bp);
+		if (c == '\\') {
+			c2 = Bgetrune(bp);
+			if(c2 == '\r'){
+				savec = c2;
+				c2 = Bgetrune(bp);
+			}
+			if (c2 == '\n') {
+				savec = 0;
+				mkinline++;
+				if (elide)
+					continue;
+				return ' ';
+			}
+			Bungetrune(bp);
+		}
+		if (c == '\n')
+			mkinline++;
+		return c;
+	}
+	return 0;
+}
diff --git a/src/cmd/mk/main.c b/src/cmd/mk/main.c
new file mode 100644
index 0000000..267ed73
--- /dev/null
+++ b/src/cmd/mk/main.c
@@ -0,0 +1,285 @@
+#include	"mk.h"
+
+#define		MKFILE		"mkfile"
+
+int debug;
+Rule *rules, *metarules;
+int nflag = 0;
+int tflag = 0;
+int iflag = 0;
+int kflag = 0;
+int aflag = 0;
+int uflag = 0;
+char *explain = 0;
+Word *target1;
+int nreps = 1;
+Job *jobs;
+Biobuf bout;
+Rule *patrule;
+void badusage(void);
+#ifdef	PROF
+short buf[10000];
+#endif
+
+int
+main(int argc, char **argv)
+{
+	Word *w;
+	char *s, *temp;
+	char *files[256], **f = files, **ff;
+	int sflag = 0;
+	int i;
+	int tfd = -1;
+	Biobuf tb;
+	Bufblock *buf;
+	Bufblock *whatif;
+
+	/*
+	 *  start with a copy of the current environment variables
+	 *  instead of sharing them
+	 */
+
+	Binit(&bout, 1, OWRITE);
+	buf = newbuf();
+	whatif = 0;
+	USED(argc);
+	for(argv++; *argv && (**argv == '-'); argv++)
+	{
+		bufcpy(buf, argv[0], strlen(argv[0]));
+		insert(buf, ' ');
+		switch(argv[0][1])
+		{
+		case 'a':
+			aflag = 1;
+			break;
+		case 'd':
+			if(*(s = &argv[0][2]))
+				while(*s) switch(*s++)
+				{
+				case 'p':	debug |= D_PARSE; break;
+				case 'g':	debug |= D_GRAPH; break;
+				case 'e':	debug |= D_EXEC; break;
+				}
+			else
+				debug = 0xFFFF;
+			break;
+		case 'e':
+			explain = &argv[0][2];
+			break;
+		case 'f':
+			if(*++argv == 0)
+				badusage();
+			*f++ = *argv;
+			bufcpy(buf, argv[0], strlen(argv[0]));
+			insert(buf, ' ');
+			break;
+		case 'i':
+			iflag = 1;
+			break;
+		case 'k':
+			kflag = 1;
+			break;
+		case 'n':
+			nflag = 1;
+			break;
+		case 's':
+			sflag = 1;
+			break;
+		case 't':
+			tflag = 1;
+			break;
+		case 'u':
+			uflag = 1;
+			break;
+		case 'w':
+			if(whatif == 0)
+				whatif = newbuf();
+			else
+				insert(whatif, ' ');
+			if(argv[0][2])
+				bufcpy(whatif, &argv[0][2], strlen(&argv[0][2]));
+			else {
+				if(*++argv == 0)
+					badusage();
+				bufcpy(whatif, &argv[0][0], strlen(&argv[0][0]));
+			}
+			break;
+		default:
+			badusage();
+		}
+	}
+#ifdef	PROF
+	{
+		extern etext();
+		monitor(main, etext, buf, sizeof buf, 300);
+	}
+#endif
+
+	if(aflag)
+		iflag = 1;
+	usage();
+	syminit();
+	initenv();
+	usage();
+
+	/*
+		assignment args become null strings
+	*/
+	temp = 0;
+	for(i = 0; argv[i]; i++) if(utfrune(argv[i], '=')){
+		bufcpy(buf, argv[i], strlen(argv[i]));
+		insert(buf, ' ');
+		if(tfd < 0){
+			temp = maketmp(&tfd);
+			if(temp == 0) {
+				fprint(2, "temp file: %r\n");
+				Exit();
+			}
+			Binit(&tb, tfd, OWRITE);
+		}
+		Bprint(&tb, "%s\n", argv[i]);
+		*argv[i] = 0;
+	}
+	if(tfd >= 0){
+		Bflush(&tb);
+		LSEEK(tfd, 0L, 0);
+		parse("command line args", tfd, 1);
+		remove(temp);
+	}
+
+	if (buf->current != buf->start) {
+		buf->current--;
+		insert(buf, 0);
+	}
+	symlook("MKFLAGS", S_VAR, (void *) stow(buf->start));
+	buf->current = buf->start;
+	for(i = 0; argv[i]; i++){
+		if(*argv[i] == 0) continue;
+		if(i)
+			insert(buf, ' ');
+		bufcpy(buf, argv[i], strlen(argv[i]));
+	}
+	insert(buf, 0);
+	symlook("MKARGS", S_VAR, (void *) stow(buf->start));
+	freebuf(buf);
+
+	if(f == files){
+		if(access(MKFILE, 4) == 0)
+			parse(MKFILE, open(MKFILE, 0), 0);
+	} else
+		for(ff = files; ff < f; ff++)
+			parse(*ff, open(*ff, 0), 0);
+	if(DEBUG(D_PARSE)){
+		dumpw("default targets", target1);
+		dumpr("rules", rules);
+		dumpr("metarules", metarules);
+		dumpv("variables");
+	}
+	if(whatif){
+		insert(whatif, 0);
+		timeinit(whatif->start);
+		freebuf(whatif);
+	}
+	execinit();
+	/* skip assignment args */
+	while(*argv && (**argv == 0))
+		argv++;
+
+	catchnotes();
+	if(*argv == 0){
+		if(target1)
+			for(w = target1; w; w = w->next)
+				mk(w->s);
+		else {
+			fprint(2, "mk: nothing to mk\n");
+			Exit();
+		}
+	} else {
+		if(sflag){
+			for(; *argv; argv++)
+				if(**argv)
+					mk(*argv);
+		} else {
+			Word *head, *tail, *t;
+
+			/* fake a new rule with all the args as prereqs */
+			tail = 0;
+			t = 0;
+			for(; *argv; argv++)
+				if(**argv){
+					if(tail == 0)
+						tail = t = newword(*argv);
+					else {
+						t->next = newword(*argv);
+						t = t->next;
+					}
+				}
+			if(tail->next == 0)
+				mk(tail->s);
+			else {
+				head = newword("command line arguments");
+				addrules(head, tail, strdup(""), VIR, mkinline, 0);
+				mk(head->s);
+			}
+		}
+	}
+	if(uflag)
+		prusage();
+	exits(0);
+}
+
+void
+badusage(void)
+{
+
+	fprint(2, "Usage: mk [-f file] [-n] [-a] [-e] [-t] [-k] [-i] [-d[egp]] [targets ...]\n");
+	Exit();
+}
+
+void *
+Malloc(int n)
+{
+	register void *s;
+
+	s = malloc(n);
+	if(!s) {
+		fprint(2, "mk: cannot alloc %d bytes\n", n);
+		Exit();
+	}
+	return(s);
+}
+
+void *
+Realloc(void *s, int n)
+{
+	if(s)
+		s = realloc(s, n);
+	else
+		s = malloc(n);
+	if(!s) {
+		fprint(2, "mk: cannot alloc %d bytes\n", n);
+		Exit();
+	}
+	return(s);
+}
+
+void
+assert(char *s, int n)
+{
+	if(!n){
+		fprint(2, "mk: Assertion ``%s'' failed.\n", s);
+		Exit();
+	}
+}
+
+void
+regerror(char *s)
+{
+	if(patrule)
+		fprint(2, "mk: %s:%d: regular expression error; %s\n",
+			patrule->file, patrule->line, s);
+	else
+		fprint(2, "mk: %s:%d: regular expression error; %s\n",
+			infile, mkinline, s);
+	Exit();
+}
diff --git a/src/cmd/mk/match.c b/src/cmd/mk/match.c
new file mode 100644
index 0000000..2a96394
--- /dev/null
+++ b/src/cmd/mk/match.c
@@ -0,0 +1,49 @@
+#include	"mk.h"
+
+int
+match(char *name, char *template, char *stem)
+{
+	Rune r;
+	int n;
+
+	while(*name && *template){
+		n = chartorune(&r, template);
+		if (PERCENT(r))
+			break;
+		while (n--)
+			if(*name++ != *template++)
+				return 0;
+	}
+	if(!PERCENT(*template))
+		return 0;
+	n = strlen(name)-strlen(template+1);
+	if (n < 0)
+		return 0;
+	if (strcmp(template+1, name+n))
+		return 0;
+	strncpy(stem, name, n);
+	stem[n] = 0;
+	if(*template == '&')
+		return !charin(stem, "./");
+	return 1;
+}
+
+void
+subst(char *stem, char *template, char *dest)
+{
+	Rune r;
+	char *s;
+	int n;
+
+	while(*template){
+		n = chartorune(&r, template);
+		if (PERCENT(r)) {
+			template += n;
+			for (s = stem; *s; s++)
+				*dest++ = *s;
+		} else
+			while (n--)
+				*dest++ = *template++;
+	}
+	*dest = 0;
+}
diff --git a/src/cmd/mk/mk.1 b/src/cmd/mk/mk.1
new file mode 100644
index 0000000..c58a6df
--- /dev/null
+++ b/src/cmd/mk/mk.1
@@ -0,0 +1,665 @@
+.TH MK 1
+.de EX
+.nf
+.ft B
+..
+.de EE
+.fi
+.ft R
+..
+.de LR
+.if t .BR \\$1 \\$2
+.if n .RB ` \\$1 '\\$2
+..
+.de L
+.nh
+.if t .B \\$1
+.if n .RB ` \\$1 '
+..
+.SH NAME
+mk \- maintain (make) related files
+.SH SYNOPSIS
+.B mk
+[
+.B -f
+.I mkfile
+] ...
+[
+.I option ...
+]
+[
+.I target ...
+]
+.SH DESCRIPTION
+.I Mk
+uses the dependency rules specified in
+.I mkfile
+to control the update (usually by compilation) of
+.I targets
+(usually files)
+from the source files upon which they depend.
+The
+.I mkfile
+(default
+.LR mkfile )
+contains a
+.I rule
+for each target that identifies the files and other
+targets upon which it depends and an
+.IR sh (1)
+script, a
+.IR recipe ,
+to update the target.
+The script is run if the target does not exist
+or if it is older than any of the files it depends on.
+.I Mkfile
+may also contain
+.I meta-rules
+that define actions for updating implicit targets.
+If no
+.I target
+is specified, the target of the first rule (not meta-rule) in
+.I mkfile
+is updated.
+.PP
+The environment variable
+.B $NPROC
+determines how many targets may be updated simultaneously;
+Some operating systems, e.g., Plan 9, set
+.B $NPROC
+automatically to the number of CPUs on the current machine.
+.PP
+Options are:
+.TP \w'\fL-d[egp]\ 'u
+.B -a
+Assume all targets to be out of date.
+Thus, everything is updated.
+.PD 0
+.TP
+.BR -d [ egp ]
+Produce debugging output
+.RB ( p
+is for parsing,
+.B g
+for graph building,
+.B e
+for execution).
+.TP
+.B -e
+Explain why each target is made.
+.TP
+.B -i
+Force any missing intermediate targets to be made.
+.TP
+.B -k
+Do as much work as possible in the face of errors.
+.TP
+.B -n
+Print, but do not execute, the commands
+needed to update the targets.
+.TP
+.B -s
+Make the command line arguments sequentially rather than in parallel.
+.TP
+.B -t
+Touch (update the modified date of) file targets, without
+executing any recipes.
+.TP
+.BI -w target1 , target2,...
+Pretend the modify time for each
+.I target
+is the current time; useful in conjunction with
+.B -n
+to learn what updates would be triggered by
+modifying the
+.IR targets .
+.PD
+.SS The \fLmkfile\fP
+A
+.I mkfile
+consists of
+.I assignments
+(described under `Environment') and
+.IR rules .
+A rule contains
+.I targets
+and a
+.IR tail .
+A target is a literal string
+and is normally a file name.
+The tail contains zero or more 
+.I prerequisites
+and an optional
+.IR recipe ,
+which is an
+.B shell
+script.
+Each line of the recipe must begin with white space.
+A rule takes the form
+.IP
+.EX
+target: prereq1 prereq2
+        \f2recipe using\fP prereq1, prereq2 \f2to build\fP target
+.EE
+.PP
+When the recipe is executed,
+the first character on every line is elided.
+.PP
+After the colon on the target line, a rule may specify
+.IR attributes ,
+described below.
+.PP
+A
+.I meta-rule 
+has a target of the form
+.IB A % B
+where
+.I A
+and
+.I B
+are (possibly empty) strings.
+A meta-rule acts as a rule for any potential target whose
+name matches
+.IB A % B
+with
+.B %
+replaced by an arbitrary string, called the
+.IR stem .
+In interpreting a meta-rule,
+the stem is substituted for all occurrences of
+.B %
+in the prerequisite names.
+In the recipe of a meta-rule, the environment variable
+.B $stem
+contains the string matched by the
+.BR % .
+For example, a meta-rule to compile a C program using
+.IR cc (1)
+might be:
+.IP
+.EX
+%:    %.c
+        cc -c $stem.c
+        cc -o $stem $stem.o
+.EE
+.PP
+Meta-rules may contain an ampersand
+.B &
+rather than a percent sign
+.BR % .
+A
+.B %
+matches a maximal length string of any characters;
+an
+.B &
+matches a maximal length string of any characters except period
+or slash.
+.PP
+The text of the
+.I mkfile
+is processed as follows.
+Lines beginning with
+.B <
+followed by a file name are replaced by the contents of the named
+file.
+Lines beginning with
+.B "<|"
+followed by a file name are replaced by the output
+of the execution of the named
+file.
+Blank lines and comments, which run from unquoted
+.B #
+characters to the following newline, are deleted.
+The character sequence backslash-newline is deleted,
+so long lines in
+.I mkfile
+may be folded.
+Non-recipe lines are processed by substituting for
+.BI `{ command }
+the output of the
+.I command
+when run by
+.IR sh .
+References to variables are replaced by the variables' values.
+Special characters may be quoted using single quotes
+.BR \&''
+as in
+.IR sh (1).
+.PP
+Assignments and rules are distinguished by
+the first unquoted occurrence of
+.B :
+(rule)
+or
+.B =
+(assignment).
+.PP
+A later rule may modify or override an existing rule under the
+following conditions:
+.TP
+\-
+If the targets of the rules exactly match and one rule
+contains only a prerequisite clause and no recipe, the
+clause is added to the prerequisites of the other rule.
+If either or both targets are virtual, the recipe is
+always executed.
+.TP
+\-
+If the targets of the rules match exactly and the
+prerequisites do not match and both rules
+contain recipes,
+.I mk
+reports an ``ambiguous recipe'' error.
+.TP
+\-
+If the target and prerequisites of both rules match exactly,
+the second rule overrides the first.
+.SS Environment
+Rules may make use of
+shell
+environment variables.
+A legal reference of the form
+.B $OBJ
+or
+.B ${name}
+is expanded as in
+.IR sh (1).
+A reference of the form
+.BI ${name: A % B = C\fL%\fID\fL}\fR,
+where
+.I A, B, C, D
+are (possibly empty) strings,
+has the value formed by expanding
+.B $name
+and substituting
+.I C
+for
+.I A
+and
+.I D
+for
+.I B
+in each word in
+.B $name
+that matches pattern
+.IB A % B\f1.
+.PP
+Variables can be set by
+assignments of the form
+.I
+        var\fL=\fR[\fIattr\fL=\fR]\fIvalue\fR
+.br
+Blanks in the
+.I value
+break it into words.
+Such variables are exported
+to the environment of
+recipes as they are executed, unless
+.BR U ,
+the only legal attribute
+.IR attr ,
+is present.
+The initial value of a variable is
+taken from (in increasing order of precedence)
+the default values below,
+.I mk's
+environment, the
+.IR mkfiles ,
+and any command line assignment as an argument to
+.IR mk .
+A variable assignment argument overrides the first (but not any subsequent)
+assignment to that variable.
+The variable
+.B MKFLAGS
+contains all the option arguments (arguments starting with
+.L -
+or containing
+.LR = )
+and
+.B MKARGS
+contains all the targets in the call to
+.IR mk .
+.PP
+Dynamic information may be included in the mkfile by using a line of the form
+.IP
+\fR<|\fIcommand\fR \fIargs\fR
+.LP
+This runs the command 
+.I command
+with the given arguments
+.I args
+and pipes its standard output to
+.I mk
+to be included as part of the mkfile. For instance, the Inferno kernels
+use this technique
+to run a shell command with an awk script and a configuration
+file as arguments in order for
+the
+.I awk
+script to process the file and output a set of variables and their values.
+.SS Execution
+.PP
+During execution,
+.I mk
+determines which targets must be updated, and in what order,
+to build the
+.I names
+specified on the command line.
+It then runs the associated recipes.
+.PP
+A target is considered up to date if it has no prerequisites or
+if all its prerequisites are up to date and it is newer
+than all its prerequisites.
+Once the recipe for a target has executed, the target is
+considered up to date.
+.PP
+The date stamp
+used to determine if a target is up to date is computed
+differently for different types of targets.
+If a target is
+.I virtual
+(the target of a rule with the
+.B V
+attribute),
+its date stamp is initially zero; when the target is
+updated the date stamp is set to
+the most recent date stamp of its prerequisites.
+Otherwise, if a target does not exist as a file,
+its date stamp is set to the most recent date stamp of its prerequisites,
+or zero if it has no prerequisites.
+Otherwise, the target is the name of a file and
+the target's date stamp is always that file's modification date.
+The date stamp is computed when the target is needed in
+the execution of a rule; it is not a static value.
+.PP
+Nonexistent targets that have prerequisites
+and are themselves prerequisites are treated specially.
+Such a target
+.I t
+is given the date stamp of its most recent prerequisite
+and if this causes all the targets which have
+.I t
+as a prerequisite to be up to date,
+.I t
+is considered up to date.
+Otherwise,
+.I t
+is made in the normal fashion.
+The
+.B -i
+flag overrides this special treatment.
+.PP
+Files may be made in any order that respects
+the preceding restrictions.
+.PP
+A recipe is executed by supplying the recipe as standard input to
+the command
+.BR /bin/sh .
+(Note that unlike
+.IR make ,
+.I mk
+feeds the entire recipe to the shell rather than running each line
+of the recipe separately.)
+The environment is augmented by the following variables:
+.TP 14
+.B $alltarget
+all the targets of this rule.
+.TP
+.B $newprereq
+the prerequisites that caused this rule to execute.
+.TP
+.B $newmember
+the prerequisites that are members of an aggregate
+that caused this rule to execute.
+When the prerequisites of a rule are members of an
+aggregate,
+.B $newprereq
+contains the name of the aggregate and out of date
+members, while
+.B $newmember
+contains only the name of the members.
+.TP
+.B $nproc
+the process slot for this recipe.
+It satisfies
+.RB 0≤ $nproc < $NPROC .
+.TP
+.B $pid
+the process id for the
+.I mk
+executing the recipe.
+.TP
+.B $prereq
+all the prerequisites for this rule.
+.TP
+.B $stem
+if this is a meta-rule,
+.B $stem
+is the string that matched
+.B %
+or
+.BR & .
+Otherwise, it is empty.
+For regular expression meta-rules (see below), the variables
+.LR stem0 ", ...,"
+.L stem9
+are set to the corresponding subexpressions.
+.TP
+.B $target
+the targets for this rule that need to be remade.
+.PP
+These variables are available only during the execution of a recipe,
+not while evaluating the
+.IR mkfile .
+.PP
+Unless the rule has the
+.B Q
+attribute,
+the recipe is printed prior to execution
+with recognizable environment variables expanded.
+Commands returning error status
+cause
+.I mk
+to terminate.
+.PP
+Recipes and backquoted
+.B rc
+commands in places such as assignments
+execute in a copy of
+.I mk's
+environment; changes they make to
+environment variables are not visible from
+.IR mk .
+.PP
+Variable substitution in a rule is done when
+the rule is read; variable substitution in the recipe is done
+when the recipe is executed.  For example:
+.IP
+.EX
+bar=a.c
+foo:	$bar
+        $CC -o foo $bar
+bar=b.c
+.EE
+.PP
+will compile
+.B b.c
+into
+.BR foo ,
+if
+.B a.c
+is newer than
+.BR foo .
+.SS Aggregates
+Names of the form
+.IR a ( b )
+refer to member
+.I b
+of the aggregate
+.IR a .
+Currently, the only aggregates supported are
+.IR ar (1)
+archives.
+.SS Attributes
+The colon separating the target from the prerequisites
+may be
+immediately followed by
+.I attributes
+and another colon.
+The attributes are:
+.TP
+.B D
+If the recipe exits with a non-null status, the target is deleted.
+.TP
+.B E
+Continue execution if the recipe draws errors.
+.TP
+.B N
+If there is no recipe, the target has its time updated.
+.TP
+.B n
+The rule is a meta-rule that cannot be a target of a virtual rule.
+Only files match the pattern in the target.
+.TP
+.B P
+The characters after the
+.B P
+until the terminating
+.B :
+are taken as a program name.
+It will be invoked as
+.B "sh -c prog 'arg1' 'arg2'"
+and should return a zero exit status
+if and only if arg1 is up to date with respect to arg2.
+Date stamps are still propagated in the normal way.
+.TP
+.B Q
+The recipe is not printed prior to execution.
+.TP
+.B R
+The rule is a meta-rule using regular expressions.
+In the rule,
+.B %
+has no special meaning.
+The target is interpreted as a regular expression as defined in
+.IR regexp (6).
+The prerequisites may contain references
+to subexpressions in form
+.BI \e n\f1,
+as in the substitute command of
+.IR sed (1).
+.TP
+.B U
+The targets are considered to have been updated
+even if the recipe did not do so.
+.TP
+.B V
+The targets of this rule are marked as virtual.
+They are distinct from files of the same name.
+.PD
+.SH EXAMPLES
+A simple mkfile to compile a program:
+.IP
+.EX
+.ta 8n +8n +8n +8n +8n +8n +8n
+</$objtype/mkfile
+
+prog:	a.$O b.$O c.$O
+	$LD $LDFLAGS -o $target $prereq
+
+%.$O:	%.c
+	$CC $CFLAGS $stem.c
+.EE
+.PP
+Override flag settings in the mkfile:
+.IP
+.EX
+% mk target 'CFLAGS=-S -w'
+.EE
+.PP
+Maintain a library:
+.IP
+.EX
+libc.a(%.$O):N:	%.$O
+libc.a:	libc.a(abs.$O) libc.a(access.$O) libc.a(alarm.$O) ...
+	ar r libc.a $newmember
+.EE
+.PP
+String expression variables to derive names from a master list:
+.IP
+.EX
+NAMES=alloc arc bquote builtins expand main match mk var word
+OBJ=${NAMES:%=%.$O}
+.EE
+.PP
+Regular expression meta-rules:
+.IP
+.EX
+([^/]*)/(.*)\e.$O:R:  \e1/\e2.c
+	cd $stem1; $CC $CFLAGS $stem2.c
+.EE
+.PP
+A correct way to deal with
+.IR yacc (1)
+grammars.
+The file
+.B lex.c
+includes the file
+.B x.tab.h
+rather than
+.B y.tab.h
+in order to reflect changes in content, not just modification time.
+.IP
+.EX
+lex.$O:	x.tab.h
+x.tab.h:	y.tab.h
+	cmp -s x.tab.h y.tab.h || cp y.tab.h x.tab.h
+y.tab.c y.tab.h:	gram.y
+	$YACC -d gram.y
+.EE
+.PP
+The above example could also use the
+.B P
+attribute for the
+.B x.tab.h
+rule:
+.IP
+.EX
+x.tab.h:Pcmp -s:	y.tab.h
+	cp y.tab.h x.tab.h
+.EE
+.SH SEE ALSO
+.IR sh (1),
+.IR regexp9 (7)
+.PP
+A. Hume,
+``Mk: a Successor to Make''
+(Tenth Edition Research Unix Manuals).
+.PP
+Andrew G. Hume and Bob Flandrena,
+``Maintaining Files on Plan 9 with Mk''.
+DOCPREFIX/doc/mk.pdf
+.SH HISTORY
+Andrew Hume wrote
+.I mk
+for Tenth Edition Research Unix.
+It was later ported to Plan 9.
+This software is a port of the Plan 9 version back to Unix.
+.SH BUGS
+Identical recipes for regular expression meta-rules only have one target.
+.br
+Seemingly appropriate input like
+.B CFLAGS=-DHZ=60
+is parsed as an erroneous attribute; correct it by inserting
+a space after the first 
+.LR = .
+.br
+The recipes printed by
+.I mk
+before being passed to
+.I sh
+for execution are sometimes erroneously expanded
+for printing.  Don't trust what's printed; rely
+on what
+.I sh
+does.
diff --git a/src/cmd/mk/mk.c b/src/cmd/mk/mk.c
new file mode 100644
index 0000000..26b8a97
--- /dev/null
+++ b/src/cmd/mk/mk.c
@@ -0,0 +1,226 @@
+#include	"mk.h"
+
+int runerrs;
+
+void
+mk(char *target)
+{
+	Node *node;
+	int did = 0;
+
+	nproc();		/* it can be updated dynamically */
+	nrep();			/* it can be updated dynamically */
+	runerrs = 0;
+	node = graph(target);
+	if(DEBUG(D_GRAPH)){
+		dumpn("new target\n", node);
+		Bflush(&bout);
+	}
+	clrmade(node);
+	while(node->flags&NOTMADE){
+		if(work(node, (Node *)0, (Arc *)0))
+			did = 1;	/* found something to do */
+		else {
+			if(waitup(1, (int *)0) > 0){
+				if(node->flags&(NOTMADE|BEINGMADE)){
+					assert("must be run errors", runerrs);
+					break;	/* nothing more waiting */
+				}
+			}
+		}
+	}
+	if(node->flags&BEINGMADE)
+		waitup(-1, (int *)0);
+	while(jobs)
+		waitup(-2, (int *)0);
+	assert("target didn't get done", runerrs || (node->flags&MADE));
+	if(did == 0)
+		Bprint(&bout, "mk: '%s' is up to date\n", node->name);
+}
+
+void
+clrmade(Node *n)
+{
+	Arc *a;
+
+	n->flags &= ~(CANPRETEND|PRETENDING);
+	if(strchr(n->name, '(') ==0 || n->time)
+		n->flags |= CANPRETEND;
+	MADESET(n, NOTMADE);
+	for(a = n->prereqs; a; a = a->next)
+		if(a->n)
+			clrmade(a->n);
+}
+
+static void
+unpretend(Node *n)
+{
+	MADESET(n, NOTMADE);
+	n->flags &= ~(CANPRETEND|PRETENDING);
+	n->time = 0;
+}
+
+int
+work(Node *node, Node *p, Arc *parc)
+{
+	Arc *a, *ra;
+	int weoutofdate;
+	int ready;
+	int did = 0;
+
+	/*print("work(%s) flags=0x%x time=%ld\n", node->name, node->flags, node->time);*//**/
+	if(node->flags&BEINGMADE)
+		return(did);
+	if((node->flags&MADE) && (node->flags&PRETENDING) && p && outofdate(p, parc, 0)){
+		if(explain)
+			fprint(1, "unpretending %s(%ld) because %s is out of date(%ld)\n",
+				node->name, node->time, p->name, p->time);
+		unpretend(node);
+	}
+	/*
+		have a look if we are pretending in case
+		someone has been unpretended out from underneath us
+	*/
+	if(node->flags&MADE){
+		if(node->flags&PRETENDING){
+			node->time = 0;
+		}else
+			return(did);
+	}
+	/* consider no prerequsite case */
+	if(node->prereqs == 0){
+		if(node->time == 0){
+			fprint(2, "mk: don't know how to make '%s'\n", node->name);
+			if(kflag){
+				node->flags |= BEINGMADE;
+				runerrs++;
+			} else
+				Exit();
+		} else
+			MADESET(node, MADE);
+		return(did);
+	}
+	/*
+		now see if we are out of date or what
+	*/
+	ready = 1;
+	weoutofdate = aflag;
+	ra = 0;
+	for(a = node->prereqs; a; a = a->next)
+		if(a->n){
+			did = work(a->n, node, a) || did;
+			if(a->n->flags&(NOTMADE|BEINGMADE))
+				ready = 0;
+			if(outofdate(node, a, 0)){
+				weoutofdate = 1;
+				if((ra == 0) || (ra->n == 0)
+						|| (ra->n->time < a->n->time))
+					ra = a;
+			}
+		} else {
+			if(node->time == 0){
+				if(ra == 0)
+					ra = a;
+				weoutofdate = 1;
+			}
+		}
+	if(ready == 0)	/* can't do anything now */
+		return(did);
+	if(weoutofdate == 0){
+		MADESET(node, MADE);
+		return(did);
+	}
+	/*
+		can we pretend to be made?
+	*/
+	if((iflag == 0) && (node->time == 0) && (node->flags&(PRETENDING|CANPRETEND))
+			&& p && ra->n && !outofdate(p, ra, 0)){
+		node->flags &= ~CANPRETEND;
+		MADESET(node, MADE);
+		if(explain && ((node->flags&PRETENDING) == 0))
+			fprint(1, "pretending %s has time %ld\n", node->name, node->time);
+		node->flags |= PRETENDING;
+		return(did);
+	}
+	/*
+		node is out of date and we REALLY do have to do something.
+		quickly rescan for pretenders
+	*/
+	for(a = node->prereqs; a; a = a->next)
+		if(a->n && (a->n->flags&PRETENDING)){
+			if(explain)
+				Bprint(&bout, "unpretending %s because of %s because of %s\n",
+				a->n->name, node->name, ra->n? ra->n->name : "rule with no prerequisites");
+
+			unpretend(a->n);
+			did = work(a->n, node, a) || did;
+			ready = 0;
+		}
+	if(ready == 0)	/* try later unless nothing has happened for -k's sake */
+		return(did || work(node, p, parc));
+	did = dorecipe(node) || did;
+	return(did);
+}
+
+void
+update(int fake, Node *node)
+{
+	Arc *a;
+
+	MADESET(node, fake? BEINGMADE : MADE);
+	if(((node->flags&VIRTUAL) == 0) && (access(node->name, 0) == 0)){
+		node->time = timeof(node->name, 1);
+		node->flags &= ~(CANPRETEND|PRETENDING);
+		for(a = node->prereqs; a; a = a->next)
+			if(a->prog)
+				outofdate(node, a, 1);
+	} else {
+		node->time = 1;
+		for(a = node->prereqs; a; a = a->next)
+			if(a->n && outofdate(node, a, 1))
+				node->time = a->n->time;
+	}
+/*	print("----node %s time=%ld flags=0x%x\n", node->name, node->time, node->flags);*//**/
+}
+
+static int
+pcmp(char *prog, char *p, char *q)
+{
+	char buf[3*NAMEBLOCK];
+	int pid;
+
+	Bflush(&bout);
+	sprint(buf, "%s '%s' '%s'\n", prog, p, q);
+	pid = pipecmd(buf, 0, 0);
+	while(waitup(-3, &pid) >= 0)
+		;
+	return(pid? 2:1);
+}
+
+int
+outofdate(Node *node, Arc *arc, int eval)
+{
+	char buf[3*NAMEBLOCK], *str;
+	Symtab *sym;
+	int ret;
+
+	str = 0;
+	if(arc->prog){
+		sprint(buf, "%s%c%s", node->name, 0377, arc->n->name);
+		sym = symlook(buf, S_OUTOFDATE, 0);
+		if(sym == 0 || eval){
+			if(sym == 0)
+				str = strdup(buf);
+			ret = pcmp(arc->prog, node->name, arc->n->name);
+			if(sym)
+				sym->value = (void *)ret;
+			else
+				symlook(str, S_OUTOFDATE, (void *)ret);
+		} else
+			ret = (int)sym->value;
+		return(ret-1);
+	} else if(strchr(arc->n->name, '(') && arc->n->time == 0)  /* missing archive member */
+		return 1;
+	else
+		return node->time < arc->n->time;
+}
diff --git a/src/cmd/mk/mk.h b/src/cmd/mk/mk.h
new file mode 100644
index 0000000..9b3f627
--- /dev/null
+++ b/src/cmd/mk/mk.h
@@ -0,0 +1,203 @@
+#include <utf.h>
+#include <fmt.h>
+#include <setjmp.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include	<bio.h>
+#include	<regexp9.h>
+#include <time.h>
+
+#define uchar _mkuchar
+#define ushort _mkushort
+#define uint _mkuint
+#define ulong _mkulong
+#define vlong _mkvlong
+#define uvlong _mkuvlong
+
+#define nil ((void*)0)
+
+typedef unsigned char		uchar;
+typedef unsigned short		ushort;
+typedef unsigned int		uint;
+typedef unsigned long		ulong;
+
+#define nelem(x) (sizeof(x)/sizeof((x)[0]))
+
+#define OREAD O_RDONLY
+#define OWRITE O_WRONLY
+#define ORDWR O_RDWR
+#define USED(x) if(x);else
+#define remove unlink
+#define seek lseek
+#define exits(s) exit((s) && ((char*)s)[0] ? 1 : 0)
+#define create(name, mode, perm) creat(name, perm)
+#define ERRMAX 256
+
+#undef assert
+#define	assert	mkassert
+extern Biobuf bout;
+
+typedef struct Bufblock
+{
+	struct Bufblock *next;
+	char 		*start;
+	char 		*end;
+	char 		*current;
+} Bufblock;
+
+typedef struct Word
+{
+	char 		*s;
+	struct Word 	*next;
+} Word;
+
+typedef struct Envy
+{
+	char 		*name;
+	Word 		*values;
+} Envy;
+
+extern Envy *envy;
+
+typedef struct Rule
+{
+	char 		*target;	/* one target */
+	Word 		*tail;		/* constituents of targets */
+	char 		*recipe;	/* do it ! */
+	short 		attr;		/* attributes */
+	short 		line;		/* source line */
+	char 		*file;		/* source file */
+	Word 		*alltargets;	/* all the targets */
+	int 		rule;		/* rule number */
+	Reprog		*pat;		/* reg exp goo */
+	char		*prog;		/* to use in out of date */
+	struct Rule	*chain;		/* hashed per target */
+	struct Rule	*next;
+} Rule;
+
+extern Rule *rules, *metarules, *patrule;
+
+/*	Rule.attr	*/
+#define		META		0x0001
+#define		UNUSED		0x0002
+#define		UPD		0x0004
+#define		QUIET		0x0008
+#define		VIR		0x0010
+#define		REGEXP		0x0020
+#define		NOREC		0x0040
+#define		DEL		0x0080
+#define		NOVIRT		0x0100
+
+#define		NREGEXP		10
+
+typedef struct Arc
+{
+	short		flag;
+	struct Node	*n;
+	Rule		*r;
+	char		*stem;
+	char		*prog;
+	char		*match[NREGEXP];
+	struct Arc	*next;
+} Arc;
+
+	/* Arc.flag */
+#define		TOGO		1
+
+typedef struct Node
+{
+	char		*name;
+	long		time;
+	unsigned short	flags;
+	Arc		*prereqs;
+	struct Node	*next;		/* list for a rule */
+} Node;
+
+	/* Node.flags */
+#define		VIRTUAL		0x0001
+#define		CYCLE		0x0002
+#define		READY		0x0004
+#define		CANPRETEND	0x0008
+#define		PRETENDING	0x0010
+#define		NOTMADE		0x0020
+#define		BEINGMADE	0x0040
+#define		MADE		0x0080
+#define		MADESET(n,m)	n->flags = (n->flags&~(NOTMADE|BEINGMADE|MADE))|(m)
+#define		PROBABLE	0x0100
+#define		VACUOUS		0x0200
+#define		NORECIPE	0x0400
+#define		DELETE		0x0800
+#define		NOMINUSE	0x1000
+
+typedef struct Job
+{
+	Rule		*r;	/* master rule for job */
+	Node		*n;	/* list of node targets */
+	char		*stem;
+	char		**match;
+	Word		*p;	/* prerequistes */
+	Word		*np;	/* new prerequistes */
+	Word		*t;	/* targets */
+	Word		*at;	/* all targets */
+	int		nproc;	/* slot number */
+	struct Job	*next;
+} Job;
+extern Job *jobs;
+
+typedef struct Symtab
+{
+	short		space;
+	char		*name;
+	void		*value;
+	struct Symtab	*next;
+} Symtab;
+
+enum {
+	S_VAR,		/* variable -> value */
+	S_TARGET,	/* target -> rule */
+	S_TIME,		/* file -> time */
+	S_PID,		/* pid -> products */
+	S_NODE,		/* target name -> node */
+	S_AGG,		/* aggregate -> time */
+	S_BITCH,	/* bitched about aggregate not there */
+	S_NOEXPORT,	/* var -> noexport */
+	S_OVERRIDE,	/* can't override */
+	S_OUTOFDATE,	/* n1\377n2 -> 2(outofdate) or 1(not outofdate) */
+	S_MAKEFILE,	/* target -> node */
+	S_MAKEVAR,	/* dumpable mk variable */
+	S_EXPORTED,	/* var -> current exported value */
+	S_WESET,	/* variable; we set in the mkfile */
+	S_INTERNAL	/* an internal mk variable (e.g., stem, target) */
+};
+
+extern	int	debug;
+extern	int	nflag, tflag, iflag, kflag, aflag, mflag;
+extern	int	mkinline;
+extern	char	*infile;
+extern	int	nreps;
+extern	char	*explain;
+extern	char	*termchars;
+extern	int	IWS;
+extern	char 	*shell;
+extern	char 	*shellname;
+extern	char 	*shflags;
+
+#define	SYNERR(l)	(fprint(2, "mk: %s:%d: syntax error; ", infile, ((l)>=0)?(l):mkinline))
+#define	RERR(r)		(fprint(2, "mk: %s:%d: rule error; ", (r)->file, (r)->line))
+#define	NAMEBLOCK	1000
+#define	BIGBLOCK	20000
+
+#define	SEP(c)		(((c)==' ')||((c)=='\t')||((c)=='\n'))
+#define WORDCHR(r)	((r) > ' ' && !utfrune("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", (r)))
+
+#define	DEBUG(x)	(debug&(x))
+#define		D_PARSE		0x01
+#define		D_GRAPH		0x02
+#define		D_EXEC		0x04
+
+#define	LSEEK(f,o,p)	seek(f,o,p)
+
+#define	PERCENT(ch)	(((ch) == '%') || ((ch) == '&'))
+
+#include	"fns.h"
diff --git a/src/cmd/mk/mkfile b/src/cmd/mk/mkfile
new file mode 100644
index 0000000..1015135
--- /dev/null
+++ b/src/cmd/mk/mkfile
@@ -0,0 +1,9 @@
+all:V: Makefile Make.FreeBSD-386 Make.Linux-386 Make.HP-UX-9000 Make.OSF1-alpha \
+	Make.SunOS-sun4u Make.SunOS-sun4u-cc Make.SunOS-sun4u-gcc \
+	Make.NetBSD-386 Make.Darwin-PowerMacintosh
+
+Makefile:D: ../libutf/Makefile.TOP Makefile.MID ../libutf/Makefile.CMD ../libutf/Makefile.BOT
+	cat $prereq >$target
+
+Make.%: ../libutf/Make.%
+	cp $prereq $target
diff --git a/src/cmd/mk/mkfile.test b/src/cmd/mk/mkfile.test
new file mode 100644
index 0000000..5c1c0ed
--- /dev/null
+++ b/src/cmd/mk/mkfile.test
@@ -0,0 +1,5 @@
+a: b
+	cp b a
+
+c:V:
+	echo hello world
diff --git a/src/cmd/mk/parse.c b/src/cmd/mk/parse.c
new file mode 100644
index 0000000..b7737ef
--- /dev/null
+++ b/src/cmd/mk/parse.c
@@ -0,0 +1,307 @@
+#include	"mk.h"
+
+char *infile;
+int mkinline;
+static int rhead(char *, Word **, Word **, int *, char **);
+static char *rbody(Biobuf*);
+extern Word *target1;
+
+void
+parse(char *f, int fd, int varoverride)
+{
+	int hline;
+	char *body;
+	Word *head, *tail;
+	int attr, set, pid;
+	char *prog, *p;
+	int newfd;
+	Biobuf in;
+	Bufblock *buf;
+
+	if(fd < 0){
+		fprint(2, "open %s: %r\n", f);
+		Exit();
+	}
+	ipush();
+	infile = strdup(f);
+	mkinline = 1;
+	Binit(&in, fd, OREAD);
+	buf = newbuf();
+	while(assline(&in, buf)){
+		hline = mkinline;
+		switch(rhead(buf->start, &head, &tail, &attr, &prog))
+		{
+		case '<':
+			p = wtos(tail, ' ');
+			if(*p == 0){
+				SYNERR(-1);
+				fprint(2, "missing include file name\n");
+				Exit();
+			}
+			newfd = open(p, OREAD);
+			if(newfd < 0){
+				fprint(2, "warning: skipping missing include file %s: %r\n", p);
+			} else
+				parse(p, newfd, 0);
+			break;
+		case '|':
+			p = wtos(tail, ' ');
+			if(*p == 0){
+				SYNERR(-1);
+				fprint(2, "missing include program name\n");
+				Exit();
+			}
+			execinit();
+			pid=pipecmd(p, envy, &newfd);
+			if(newfd < 0){
+				fprint(2, "warning: skipping missing program file %s: %r\n", p);
+			} else
+				parse(p, newfd, 0);
+			while(waitup(-3, &pid) >= 0)
+				;
+			if(pid != 0){
+				fprint(2, "bad include program status\n");
+				Exit();
+			}
+			break;
+		case ':':
+			body = rbody(&in);
+			addrules(head, tail, body, attr, hline, prog);
+			break;
+		case '=':
+			if(head->next){
+				SYNERR(-1);
+				fprint(2, "multiple vars on left side of assignment\n");
+				Exit();
+			}
+			if(symlook(head->s, S_OVERRIDE, 0)){
+				set = varoverride;
+			} else {
+				set = 1;
+				if(varoverride)
+					symlook(head->s, S_OVERRIDE, (void *)"");
+			}
+			if(set){
+/*
+char *cp;
+dumpw("tail", tail);
+cp = wtos(tail, ' '); print("assign %s to %s\n", head->s, cp); free(cp);
+*/
+				setvar(head->s, (void *) tail);
+				symlook(head->s, S_WESET, (void *)"");
+			}
+			if(attr)
+				symlook(head->s, S_NOEXPORT, (void *)"");
+			break;
+		default:
+			SYNERR(hline);
+			fprint(2, "expected one of :<=\n");
+			Exit();
+			break;
+		}
+	}
+	close(fd);
+	freebuf(buf);
+	ipop();
+}
+
+void
+addrules(Word *head, Word *tail, char *body, int attr, int hline, char *prog)
+{
+	Word *w;
+
+	assert("addrules args", head && body);
+		/* tuck away first non-meta rule as default target*/
+	if(target1 == 0 && !(attr&REGEXP)){
+		for(w = head; w; w = w->next)
+			if(charin(w->s, "%&"))
+				break;
+		if(w == 0)
+			target1 = wdup(head);
+	}
+	for(w = head; w; w = w->next)
+		addrule(w->s, tail, body, head, attr, hline, prog);
+}
+
+static int
+rhead(char *line, Word **h, Word **t, int *attr, char **prog)
+{
+	char *p;
+	char *pp;
+	int sep;
+	Rune r;
+	int n;
+	Word *w;
+
+	p = charin(line,":=<");
+	if(p == 0)
+		return('?');
+	sep = *p;
+	*p++ = 0;
+	if(sep == '<' && *p == '|'){
+		sep = '|';
+		p++;
+	}
+	*attr = 0;
+	*prog = 0;
+	if(sep == '='){
+		pp = charin(p, termchars);	/* termchars is shell-dependent */
+		if (pp && *pp == '=') {
+			while (p != pp) {
+				n = chartorune(&r, p);
+				switch(r)
+				{
+				default:
+					SYNERR(-1);
+					fprint(2, "unknown attribute '%c'\n",*p);
+					Exit();
+				case 'U':
+					*attr = 1;
+					break;
+				}
+				p += n;
+			}
+			p++;		/* skip trailing '=' */
+		}
+	}
+	if((sep == ':') && *p && (*p != ' ') && (*p != '\t')){
+		while (*p) {
+			n = chartorune(&r, p);
+			if (r == ':')
+				break;
+			p += n;
+			switch(r)
+			{
+			default:
+				SYNERR(-1);
+				fprint(2, "unknown attribute '%c'\n", p[-1]);
+				Exit();
+			case 'D':
+				*attr |= DEL;
+				break;
+			case 'E':
+				*attr |= NOMINUSE;
+				break;
+			case 'n':
+				*attr |= NOVIRT;
+				break;
+			case 'N':
+				*attr |= NOREC;
+				break;
+			case 'P':
+				pp = utfrune(p, ':');
+				if (pp == 0 || *pp == 0)
+					goto eos;
+				*pp = 0;
+				*prog = strdup(p);
+				*pp = ':';
+				p = pp;
+				break;
+			case 'Q':
+				*attr |= QUIET;
+				break;
+			case 'R':
+				*attr |= REGEXP;
+				break;
+			case 'U':
+				*attr |= UPD;
+				break;
+			case 'V':
+				*attr |= VIR;
+				break;
+			}
+		}
+		if (*p++ != ':') {
+	eos:
+			SYNERR(-1);
+			fprint(2, "missing trailing :\n");
+			Exit();
+		}
+	}
+	*h = w = stow(line);
+	if(*w->s == 0 && sep != '<' && sep != '|') {
+		SYNERR(mkinline-1);
+		fprint(2, "no var on left side of assignment/rule\n");
+		Exit();
+	}
+	*t = stow(p);
+	return(sep);
+}
+
+static char *
+rbody(Biobuf *in)
+{
+	Bufblock *buf;
+	int r, lastr;
+	char *p;
+
+	lastr = '\n';
+	buf = newbuf();
+	for(;;){
+		r = Bgetrune(in);
+		if (r < 0)
+			break;
+		if (lastr == '\n') {
+			if (r == '#')
+				rinsert(buf, r);
+			else if (r != ' ' && r != '\t') {
+				Bungetrune(in);
+				break;
+			}
+		} else
+			rinsert(buf, r);
+		lastr = r;
+		if (r == '\n')
+			mkinline++;
+	}
+	insert(buf, 0);
+	p = strdup(buf->start);
+	freebuf(buf);
+	return p;
+}
+
+struct input
+{
+	char *file;
+	int line;
+	struct input *next;
+};
+static struct input *inputs = 0;
+
+void
+ipush(void)
+{
+	struct input *in, *me;
+
+	me = (struct input *)Malloc(sizeof(*me));
+	me->file = infile;
+	me->line = mkinline;
+	me->next = 0;
+	if(inputs == 0)
+		inputs = me;
+	else {
+		for(in = inputs; in->next; )
+			in = in->next;
+		in->next = me;
+	}
+}
+
+void
+ipop(void)
+{
+	struct input *in, *me;
+
+	assert("pop input list", inputs != 0);
+	if(inputs->next == 0){
+		me = inputs;
+		inputs = 0;
+	} else {
+		for(in = inputs; in->next->next; )
+			in = in->next;
+		me = in->next;
+		in->next = 0;
+	}
+	infile = me->file;
+	mkinline = me->line;
+	free((char *)me);
+}
diff --git a/src/cmd/mk/rc.c b/src/cmd/mk/rc.c
new file mode 100644
index 0000000..657ddf2
--- /dev/null
+++ b/src/cmd/mk/rc.c
@@ -0,0 +1,175 @@
+#include	"mk.h"
+
+char	*termchars = "'= \t";	/*used in parse.c to isolate assignment attribute*/
+char	*shflags = "-I";	/* rc flag to force non-interactive mode */
+int	IWS = '\1';		/* inter-word separator in env - not used in plan 9 */
+
+/*
+ *	This file contains functions that depend on rc's syntax.  Most
+ *	of the routines extract strings observing rc's escape conventions
+ */
+
+
+/*
+ *	skip a token in single quotes.
+ */
+static char *
+squote(char *cp)
+{
+	Rune r;
+	int n;
+
+	while(*cp){
+		n = chartorune(&r, cp);
+		if(r == '\'') {
+			n += chartorune(&r, cp+n);
+			if(r != '\'')
+				return(cp);
+		}
+		cp += n;
+	}
+	SYNERR(-1);		/* should never occur */
+	fprint(2, "missing closing '\n");
+	return 0;
+}
+
+/*
+ *	search a string for characters in a pattern set
+ *	characters in quotes and variable generators are escaped
+ */
+char *
+charin(char *cp, char *pat)
+{
+	Rune r;
+	int n, vargen;
+
+	vargen = 0;
+	while(*cp){
+		n = chartorune(&r, cp);
+		switch(r){
+		case '\'':			/* skip quoted string */
+			cp = squote(cp+1);	/* n must = 1 */
+			if(!cp)
+				return 0;
+			break;
+		case '$':
+			if(*(cp+1) == '{')
+				vargen = 1;
+			break;
+		case '}':
+			if(vargen)
+				vargen = 0;
+			else if(utfrune(pat, r))
+				return cp;
+			break;
+		default:
+			if(vargen == 0 && utfrune(pat, r))
+				return cp;
+			break;
+		}
+		cp += n;
+	}
+	if(vargen){
+		SYNERR(-1);
+		fprint(2, "missing closing } in pattern generator\n");
+	}
+	return 0;
+}
+
+/*
+ *	extract an escaped token.  Possible escape chars are single-quote,
+ *	double-quote,and backslash.  Only the first is valid for rc. the
+ *	others are just inserted into the receiving buffer.
+ */
+char*
+expandquote(char *s, Rune r, Bufblock *b)
+{
+	if (r != '\'') {
+		rinsert(b, r);
+		return s;
+	}
+
+	while(*s){
+		s += chartorune(&r, s);
+		if(r == '\'') {
+			if(*s == '\'')
+				s++;
+			else
+				return s;
+		}
+		rinsert(b, r);
+	}
+	return 0;
+}
+
+/*
+ *	Input an escaped token.  Possible escape chars are single-quote,
+ *	double-quote and backslash.  Only the first is a valid escape for
+ *	rc; the others are just inserted into the receiving buffer.
+ */
+int
+escapetoken(Biobuf *bp, Bufblock *buf, int preserve, int esc)
+{
+	int c, line;
+
+	if(esc != '\'')
+		return 1;
+
+	line = mkinline;
+	while((c = nextrune(bp, 0)) > 0){
+		if(c == '\''){
+			if(preserve)
+				rinsert(buf, c);
+			c = Bgetrune(bp);
+			if (c < 0)
+				break;
+			if(c != '\''){
+				Bungetrune(bp);
+				return 1;
+			}
+		}
+		rinsert(buf, c);
+	}
+	SYNERR(line); fprint(2, "missing closing %c\n", esc);
+	return 0;
+}
+
+/*
+ *	copy a single-quoted string; s points to char after opening quote
+ */
+static char *
+copysingle(char *s, Bufblock *buf)
+{
+	Rune r;
+
+	while(*s){
+		s += chartorune(&r, s);
+		rinsert(buf, r);
+		if(r == '\'')
+			break;
+	}
+	return s;
+}
+/*
+ *	check for quoted strings.  backquotes are handled here; single quotes above.
+ *	s points to char after opening quote, q.
+ */
+char *
+copyq(char *s, Rune q, Bufblock *buf)
+{
+	if(q == '\'')				/* copy quoted string */
+		return copysingle(s, buf);
+
+	if(q != '`')				/* not quoted */
+		return s;
+
+	while(*s){				/* copy backquoted string */
+		s += chartorune(&q, s);
+		rinsert(buf, q);
+		if(q == '}')
+			break;
+		if(q == '\'')
+			s = copysingle(s, buf);	/* copy quoted string */
+	}
+	return s;
+}
diff --git a/src/cmd/mk/recipe.c b/src/cmd/mk/recipe.c
new file mode 100644
index 0000000..144a490
--- /dev/null
+++ b/src/cmd/mk/recipe.c
@@ -0,0 +1,117 @@
+#include	"mk.h"
+
+int
+dorecipe(Node *node)
+{
+	char buf[BIGBLOCK];
+	register Node *n;
+	Rule *r = 0;
+	Arc *a, *aa;
+	Word head, ahead, lp, ln, *w, *ww, *aw;
+	Symtab *s;
+	int did = 0;
+
+	aa = 0;
+	/*
+		pick up the rule
+	*/
+	for(a = node->prereqs; a; a = a->next)
+		if(*a->r->recipe)
+			r = (aa = a)->r;
+	/*
+		no recipe? go to buggery!
+	*/
+	if(r == 0){
+		if(!(node->flags&VIRTUAL) && !(node->flags&NORECIPE)){
+			fprint(2, "mk: no recipe to make '%s'\n", node->name);
+			Exit();
+		}
+		if(strchr(node->name, '(') && node->time == 0)
+			MADESET(node, MADE);
+		else
+			update(0, node);
+		if(tflag){
+			if(!(node->flags&VIRTUAL))
+				touch(node->name);
+			else if(explain)
+				Bprint(&bout, "no touch of virtual '%s'\n", node->name);
+		}
+		return(did);
+	}
+	/*
+		build the node list
+	*/
+	node->next = 0;
+	head.next = 0;
+	ww = &head;
+	ahead.next = 0;
+	aw = &ahead;
+	if(r->attr&REGEXP){
+		ww->next = newword(node->name);
+		aw->next = newword(node->name);
+	} else {
+		for(w = r->alltargets; w; w = w->next){
+			if(r->attr&META)
+				subst(aa->stem, w->s, buf);
+			else
+				strcpy(buf, w->s);
+			aw->next = newword(buf);
+			aw = aw->next;
+			if((s = symlook(buf, S_NODE, 0)) == 0)
+				continue;	/* not a node we are interested in */
+			n = (Node *)s->value;
+			if(aflag == 0 && n->time) {
+				for(a = n->prereqs; a; a = a->next)
+					if(a->n && outofdate(n, a, 0))
+						break;
+				if(a == 0)
+					continue;
+			}
+			ww->next = newword(buf);
+			ww = ww->next;
+			if(n == node) continue;
+			n->next = node->next;
+			node->next = n;
+		}
+	}
+	for(n = node; n; n = n->next)
+		if((n->flags&READY) == 0)
+			return(did);
+	/*
+		gather the params for the job
+	*/
+	lp.next = ln.next = 0;
+	for(n = node; n; n = n->next){
+		for(a = n->prereqs; a; a = a->next){
+			if(a->n){
+				addw(&lp, a->n->name);
+				if(outofdate(n, a, 0)){
+					addw(&ln, a->n->name);
+					if(explain)
+						fprint(1, "%s(%ld) < %s(%ld)\n",
+							n->name, n->time, a->n->name, a->n->time);
+				}
+			} else {
+				if(explain)
+					fprint(1, "%s has no prerequisites\n",
+							n->name);
+			}
+		}
+		MADESET(n, BEINGMADE);
+	}
+	/*print("lt=%s ln=%s lp=%s\n",wtos(head.next, ' '),wtos(ln.next, ' '),wtos(lp.next, ' '));*//**/
+	run(newjob(r, node, aa->stem, aa->match, lp.next, ln.next, head.next, ahead.next));
+	return(1);
+}
+
+void
+addw(Word *w, char *s)
+{
+	Word *lw;
+
+	for(lw = w; w = w->next; lw = w){
+		if(strcmp(s, w->s) == 0)
+			return;
+	}
+	lw->next = newword(s);
+}
diff --git a/src/cmd/mk/rpm.spec b/src/cmd/mk/rpm.spec
new file mode 100644
index 0000000..be75e6b
--- /dev/null
+++ b/src/cmd/mk/rpm.spec
@@ -0,0 +1,29 @@
+Summary: Streamlined replacement for make
+Name: mk
+Version: 2.0
+Release: 1
+Group: Development/Utils
+Copyright: Public Domain
+Packager: Russ Cox <rsc@post.harvard.edu>
+Source: http://pdos.lcs.mit.edu/~rsc/software/mk-2.0.tgz
+URL: http://pdos.lcs.mit.edu/~rsc/software/#mk
+Requires: libfmt libbio libregexp9 libutf
+
+%description
+Mk is a streamlined replacement for make, written for 
+Tenth Edition Research Unix by Andrew Hume.
+
+http://plan9.bell-labs.com/sys/doc/mk.pdf
+%prep
+%setup
+
+%build
+make
+
+%install
+make install
+
+%files
+/usr/local/doc/mk.pdf
+/usr/local/man/man1/mk.1
+/usr/local/bin/mk
diff --git a/src/cmd/mk/rule.c b/src/cmd/mk/rule.c
new file mode 100644
index 0000000..662f067
--- /dev/null
+++ b/src/cmd/mk/rule.c
@@ -0,0 +1,107 @@
+#include	"mk.h"
+
+static Rule *lr, *lmr;
+static int rcmp(Rule *r, char *target, Word *tail);
+static int nrules = 0;
+
+void
+addrule(char *head, Word *tail, char *body, Word *ahead, int attr, int hline, char *prog)
+{
+	Rule *r;
+	Rule *rr;
+	Symtab *sym;
+	int reuse;
+
+	r = 0;
+	reuse = 0;
+	if(sym = symlook(head, S_TARGET, 0)){
+		for(r = (Rule *)sym->value; r; r = r->chain)
+			if(rcmp(r, head, tail) == 0){
+				reuse = 1;
+				break;
+			}
+	}
+	if(r == 0)
+		r = (Rule *)Malloc(sizeof(Rule));
+	r->target = head;
+	r->tail = tail;
+	r->recipe = body;
+	r->line = hline;
+	r->file = infile;
+	r->attr = attr;
+	r->alltargets = ahead;
+	r->prog = prog;
+	r->rule = nrules++;
+	if(!reuse){
+		rr = (Rule *)symlook(head, S_TARGET, (void *)r)->value;
+		if(rr != r){
+			r->chain = rr->chain;
+			rr->chain = r;
+		} else
+			r->chain = 0;
+	}
+	if(!reuse)
+		r->next = 0;
+	if((attr&REGEXP) || charin(head, "%&")){
+		r->attr |= META;
+		if(reuse)
+			return;
+		if(attr&REGEXP){
+			patrule = r;
+			r->pat = regcomp(head);
+		}
+		if(metarules == 0)
+			metarules = lmr = r;
+		else {
+			lmr->next = r;
+			lmr = r;
+		}
+	} else {
+		if(reuse)
+			return;
+		r->pat = 0;
+		if(rules == 0)
+			rules = lr = r;
+		else {
+			lr->next = r;
+			lr = r;
+		}
+	}
+}
+
+void
+dumpr(char *s, Rule *r)
+{
+	Bprint(&bout, "%s: start=%ld\n", s, r);
+	for(; r; r = r->next){
+		Bprint(&bout, "\tRule %ld: %s[%d] attr=%x next=%ld chain=%ld alltarget='%s'",
+			r, r->file, r->line, r->attr, r->next, r->chain, wtos(r->alltargets, ' '));
+		if(r->prog)
+			Bprint(&bout, " prog='%s'", r->prog);
+		Bprint(&bout, "\n\ttarget=%s: %s\n", r->target, wtos(r->tail, ' '));
+		Bprint(&bout, "\trecipe@%ld='%s'\n", r->recipe, r->recipe);
+	}
+}
+
+static int
+rcmp(Rule *r, char *target, Word *tail)
+{
+	Word *w;
+
+	if(strcmp(r->target, target))
+		return 1;
+	for(w = r->tail; w && tail; w = w->next, tail = tail->next)
+		if(strcmp(w->s, tail->s))
+			return 1;
+	return(w || tail);
+}
+
+char *
+rulecnt(void)
+{
+	char *s;
+
+	s = Malloc(nrules);
+	memset(s, 0, nrules);
+	return(s);
+}
diff --git a/src/cmd/mk/run.c b/src/cmd/mk/run.c
new file mode 100644
index 0000000..3c0c7f3
--- /dev/null
+++ b/src/cmd/mk/run.c
@@ -0,0 +1,296 @@
+#include	"mk.h"
+
+typedef struct Event
+{
+	int pid;
+	Job *job;
+} Event;
+static Event *events;
+static int nevents, nrunning, nproclimit;
+
+typedef struct Process
+{
+	int pid;
+	int status;
+	struct Process *b, *f;
+} Process;
+static Process *phead, *pfree;
+static void sched(void);
+static void pnew(int, int), pdelete(Process *);
+
+int pidslot(int);
+
+void
+run(Job *j)
+{
+	Job *jj;
+
+	if(jobs){
+		for(jj = jobs; jj->next; jj = jj->next)
+			;
+		jj->next = j;
+	} else 
+		jobs = j;
+	j->next = 0;
+	/* this code also in waitup after parse redirect */
+	if(nrunning < nproclimit)
+		sched();
+}
+
+static void
+sched(void)
+{
+	char *flags;
+	Job *j;
+	Bufblock *buf;
+	int slot;
+	Node *n;
+	Envy *e;
+
+	if(jobs == 0){
+		usage();
+		return;
+	}
+	j = jobs;
+	jobs = j->next;
+	if(DEBUG(D_EXEC))
+		fprint(1, "firing up job for target %s\n", wtos(j->t, ' '));
+	slot = nextslot();
+	events[slot].job = j;
+	buf = newbuf();
+	e = buildenv(j, slot);
+	shprint(j->r->recipe, e, buf);
+	if(!tflag && (nflag || !(j->r->attr&QUIET)))
+		Bwrite(&bout, buf->start, (long)strlen(buf->start));
+	freebuf(buf);
+	if(nflag||tflag){
+		for(n = j->n; n; n = n->next){
+			if(tflag){
+				if(!(n->flags&VIRTUAL))
+					touch(n->name);
+				else if(explain)
+					Bprint(&bout, "no touch of virtual '%s'\n", n->name);
+			}
+			n->time = time((long *)0);
+			MADESET(n, MADE);
+		}
+	} else {
+		if(DEBUG(D_EXEC))
+			fprint(1, "recipe='%s'", j->r->recipe);/**/
+		Bflush(&bout);
+		if(j->r->attr&NOMINUSE)
+			flags = 0;
+		else
+			flags = "-e";
+		events[slot].pid = execsh(flags, j->r->recipe, 0, e);
+		usage();
+		nrunning++;
+		if(DEBUG(D_EXEC))
+			fprint(1, "pid for target %s = %d\n", wtos(j->t, ' '), events[slot].pid);
+	}
+}
+
+int
+waitup(int echildok, int *retstatus)
+{
+	Envy *e;
+	int pid;
+	int slot;
+	Symtab *s;
+	Word *w;
+	Job *j;
+	char buf[ERRMAX];
+	Bufblock *bp;
+	int uarg = 0;
+	int done;
+	Node *n;
+	Process *p;
+	extern int runerrs;
+
+	/* first check against the proces slist */
+	if(retstatus)
+		for(p = phead; p; p = p->f)
+			if(p->pid == *retstatus){
+				*retstatus = p->status;
+				pdelete(p);
+				return(-1);
+			}
+again:		/* rogue processes */
+	pid = waitfor(buf);
+	if(pid == -1){
+		if(echildok > 0)
+			return(1);
+		else {
+			fprint(2, "mk: (waitup %d): %r\n", echildok);
+			Exit();
+		}
+	}
+	if(DEBUG(D_EXEC))
+		fprint(1, "waitup got pid=%d, status='%s'\n", pid, buf);
+	if(retstatus && pid == *retstatus){
+		*retstatus = buf[0]? 1:0;
+		return(-1);
+	}
+	slot = pidslot(pid);
+	if(slot < 0){
+		if(DEBUG(D_EXEC))
+			fprint(2, "mk: wait returned unexpected process %d\n", pid);
+		pnew(pid, buf[0]? 1:0);
+		goto again;
+	}
+	j = events[slot].job;
+	usage();
+	nrunning--;
+	events[slot].pid = -1;
+	if(buf[0]){
+		e = buildenv(j, slot);
+		bp = newbuf();
+		shprint(j->r->recipe, e, bp);
+		front(bp->start);
+		fprint(2, "mk: %s: exit status=%s", bp->start, buf);
+		freebuf(bp);
+		for(n = j->n, done = 0; n; n = n->next)
+			if(n->flags&DELETE){
+				if(done++ == 0)
+					fprint(2, ", deleting");
+				fprint(2, " '%s'", n->name);
+				delete(n->name);
+			}
+		fprint(2, "\n");
+		if(kflag){
+			runerrs++;
+			uarg = 1;
+		} else {
+			jobs = 0;
+			Exit();
+		}
+	}
+	for(w = j->t; w; w = w->next){
+		if((s = symlook(w->s, S_NODE, 0)) == 0)
+			continue;	/* not interested in this node */
+		update(uarg, (Node *)s->value);
+	}
+	if(nrunning < nproclimit)
+		sched();
+	return(0);
+}
+
+void
+nproc(void)
+{
+	Symtab *sym;
+	Word *w;
+
+	if(sym = symlook("NPROC", S_VAR, 0)) {
+		w = (Word *) sym->value;
+		if (w && w->s && w->s[0])
+			nproclimit = atoi(w->s);
+	}
+	if(nproclimit < 1)
+		nproclimit = 1;
+	if(DEBUG(D_EXEC))
+		fprint(1, "nprocs = %d\n", nproclimit);
+	if(nproclimit > nevents){
+		if(nevents)
+			events = (Event *)Realloc((char *)events, nproclimit*sizeof(Event));
+		else
+			events = (Event *)Malloc(nproclimit*sizeof(Event));
+		while(nevents < nproclimit)
+			events[nevents++].pid = 0;
+	}
+}
+
+int
+nextslot(void)
+{
+	int i;
+
+	for(i = 0; i < nproclimit; i++)
+		if(events[i].pid <= 0) return i;
+	assert("out of slots!!", 0);
+	return 0;	/* cyntax */
+}
+
+int
+pidslot(int pid)
+{
+	int i;
+
+	for(i = 0; i < nevents; i++)
+		if(events[i].pid == pid) return(i);
+	if(DEBUG(D_EXEC))
+		fprint(2, "mk: wait returned unexpected process %d\n", pid);
+	return(-1);
+}
+
+
+static void
+pnew(int pid, int status)
+{
+	Process *p;
+
+	if(pfree){
+		p = pfree;
+		pfree = p->f;
+	} else
+		p = (Process *)Malloc(sizeof(Process));
+	p->pid = pid;
+	p->status = status;
+	p->f = phead;
+	phead = p;
+	if(p->f)
+		p->f->b = p;
+	p->b = 0;
+}
+
+static void
+pdelete(Process *p)
+{
+	if(p->f)
+		p->f->b = p->b;
+	if(p->b)
+		p->b->f = p->f;
+	else
+		phead = p->f;
+	p->f = pfree;
+	pfree = p;
+}
+
+void
+killchildren(char *msg)
+{
+	Process *p;
+
+	kflag = 1;	/* to make sure waitup doesn't exit */
+	jobs = 0;	/* make sure no more get scheduled */
+	for(p = phead; p; p = p->f)
+		expunge(p->pid, msg);
+	while(waitup(1, (int *)0) == 0)
+		;
+	Bprint(&bout, "mk: %s\n", msg);
+	Exit();
+}
+
+static long tslot[1000];
+static long tick;
+
+void
+usage(void)
+{
+	long t;
+
+	time(&t);
+	if(tick)
+		tslot[nrunning] += (t-tick);
+	tick = t;
+}
+
+void
+prusage(void)
+{
+	int i;
+
+	usage();
+	for(i = 0; i <= nevents; i++)
+		fprint(1, "%d: %ld\n", i, tslot[i]);
+}
diff --git a/src/cmd/mk/sh.c b/src/cmd/mk/sh.c
new file mode 100644
index 0000000..524167a
--- /dev/null
+++ b/src/cmd/mk/sh.c
@@ -0,0 +1,189 @@
+#include	"mk.h"
+
+char	*termchars = "\"'= \t";	/*used in parse.c to isolate assignment attribute*/
+char	*shflags = 0;
+int	IWS = ' ';		/* inter-word separator in env */
+
+/*
+ *	This file contains functions that depend on the shell's syntax.  Most
+ *	of the routines extract strings observing the shell's escape conventions.
+ */
+
+
+/*
+ *	skip a token in quotes.
+ */
+static char *
+squote(char *cp, int c)
+{
+	Rune r;
+	int n;
+
+	while(*cp){
+		n = chartorune(&r, cp);
+		if(r == c)
+			return cp;
+		if(r == '\\')
+			n += chartorune(&r, cp+n);
+		cp += n;
+	}
+	SYNERR(-1);		/* should never occur */
+	fprint(2, "missing closing '\n");
+	return 0;
+}
+/*
+ *	search a string for unescaped characters in a pattern set
+ */
+char *
+charin(char *cp, char *pat)
+{
+	Rune r;
+	int n, vargen;
+
+	vargen = 0;
+	while(*cp){
+		n = chartorune(&r, cp);
+		switch(r){
+		case '\\':			/* skip escaped char */
+			cp += n;
+			n = chartorune(&r, cp);
+			break;
+		case '\'':			/* skip quoted string */
+		case '"':
+			cp = squote(cp+1, r);	/* n must = 1 */
+			if(!cp)
+				return 0;
+			break;
+		case '$':
+			if(*(cp+1) == '{')
+				vargen = 1;
+			break;
+		case '}':
+			if(vargen)
+				vargen = 0;
+			else if(utfrune(pat, r))
+				return cp;
+			break;
+		default:
+			if(vargen == 0 && utfrune(pat, r))
+				return cp;
+			break;
+		}
+		cp += n;
+	}
+	if(vargen){
+		SYNERR(-1);
+		fprint(2, "missing closing } in pattern generator\n");
+	}
+	return 0;
+}
+
+/*
+ *	extract an escaped token.  Possible escape chars are single-quote,
+ *	double-quote,and backslash.
+ */
+char*
+expandquote(char *s, Rune esc, Bufblock *b)
+{
+	Rune r;
+
+	if (esc == '\\') {
+		s += chartorune(&r, s);
+		rinsert(b, r);
+		return s;
+	}
+
+	while(*s){
+		s += chartorune(&r, s);
+		if(r == esc)
+			return s;
+		if (r == '\\') {
+			rinsert(b, r);
+			s += chartorune(&r, s);
+		}
+		rinsert(b, r);
+	}
+	return 0;
+}
+
+/*
+ *	Input an escaped token.  Possible escape chars are single-quote,
+ *	double-quote and backslash.
+ */
+int
+escapetoken(Biobuf *bp, Bufblock *buf, int preserve, int esc)
+{
+	int c, line;
+
+	if(esc == '\\') {
+		c = Bgetrune(bp);
+		if(c == '\r')
+			c = Bgetrune(bp);
+		if (c == '\n')
+			mkinline++;
+		rinsert(buf, c);
+		return 1;
+	}
+
+	line = mkinline;
+	while((c = nextrune(bp, 0)) >= 0){
+		if(c == esc){
+			if(preserve)
+				rinsert(buf, c);
+			return 1;
+		}
+		if(c == '\\') {
+			rinsert(buf, c);
+			c = Bgetrune(bp);
+			if(c == '\r')
+				c = Bgetrune(bp);
+			if (c < 0)
+				break;
+			if (c == '\n')
+				mkinline++;
+		}
+		rinsert(buf, c);
+	}
+	SYNERR(line); fprint(2, "missing closing %c\n", esc);
+	return 0;
+}
+
+/*
+ *	copy a quoted string; s points to char after opening quote
+ */
+static char *
+copysingle(char *s, Rune q, Bufblock *buf)
+{
+	Rune r;
+
+	while(*s){
+		s += chartorune(&r, s);
+		rinsert(buf, r);
+		if(r == q)
+			break;
+	}
+	return s;
+}
+/*
+ *	check for quoted strings.  backquotes are handled here; single quotes above.
+ *	s points to char after opening quote, q.
+ */
+char *
+copyq(char *s, Rune q, Bufblock *buf)
+{
+	if(q == '\'' || q == '"')		/* copy quoted string */
+		return copysingle(s, q, buf);
+
+	if(q != '`')				/* not quoted */
+		return s;
+
+	while(*s){				/* copy backquoted string */
+		s += chartorune(&q, s);
+		rinsert(buf, q);
+		if(q == '`')
+			break;
+		if(q == '\'' || q == '"')
+			s = copysingle(s, q, buf);	/* copy quoted string */
+	}
+	return s;
+}
diff --git a/src/cmd/mk/shprint.c b/src/cmd/mk/shprint.c
new file mode 100644
index 0000000..9e15bce
--- /dev/null
+++ b/src/cmd/mk/shprint.c
@@ -0,0 +1,123 @@
+#include	"mk.h"
+
+static char *vexpand(char*, Envy*, Bufblock*);
+
+static int
+getfields(char *str, char **args, int max, int mflag, char *set)
+{
+	Rune r;
+	int nr, intok, narg;
+
+	if(max <= 0)
+		return 0;
+
+	narg = 0;
+	args[narg] = str;
+	if(!mflag)
+		narg++;
+	intok = 0;
+	for(;; str += nr) {
+		nr = chartorune(&r, str);
+		if(r == 0)
+			break;
+		if(utfrune(set, r)) {
+			if(narg >= max)
+				break;
+			*str = 0;
+			intok = 0;
+			args[narg] = str + nr;
+			if(!mflag)
+				narg++;
+		} else {
+			if(!intok && mflag)
+				narg++;
+			intok = 1;
+		}
+	}
+	return narg;
+}
+
+void
+shprint(char *s, Envy *env, Bufblock *buf)
+{
+	int n;
+	Rune r;
+
+	while(*s) {
+		n = chartorune(&r, s);
+		if (r == '$')
+			s = vexpand(s, env, buf);
+		else {
+			rinsert(buf, r);
+			s += n;
+			s = copyq(s, r, buf);	/*handle quoted strings*/
+		}
+	}
+	insert(buf, 0);
+}
+
+static char *
+mygetenv(char *name, Envy *env)
+{
+	if (!env)
+		return 0;
+	if (symlook(name, S_WESET, 0) == 0 && symlook(name, S_INTERNAL, 0) == 0)
+		return 0;
+		/* only resolve internal variables and variables we've set */
+	for(; env->name; env++){
+		if (strcmp(env->name, name) == 0)
+			return wtos(env->values, ' ');
+	}
+	return 0;
+}
+
+static char *
+vexpand(char *w, Envy *env, Bufblock *buf)
+{
+	char *s, carry, *p, *q;
+
+	assert("vexpand no $", *w == '$');
+	p = w+1;	/* skip dollar sign */
+	if(*p == '{') {
+		p++;
+		q = utfrune(p, '}');
+		if (!q)
+			q = strchr(p, 0);
+	} else
+		q = shname(p);
+	carry = *q;
+	*q = 0;
+	s = mygetenv(p, env);
+	*q = carry;
+	if (carry == '}')
+		q++;
+	if (s) {
+		bufcpy(buf, s, strlen(s));
+		free(s);
+	} else 		/* copy name intact*/
+		bufcpy(buf, w, q-w);
+	return(q);
+}
+
+void
+front(char *s)
+{
+	char *t, *q;
+	int i, j;
+	char *flds[512];
+
+	q = strdup(s);
+	i = getfields(q, flds, 512, 0, " \t\n");
+	if(i > 5){
+		flds[4] = flds[i-1];
+		flds[3] = "...";
+		i = 5;
+	}
+	t = s;
+	for(j = 0; j < i; j++){
+		for(s = flds[j]; *s; *t++ = *s++);
+		*t++ = ' ';
+	}
+	*t = 0;
+	free(q);
+}
diff --git a/src/cmd/mk/symtab.c b/src/cmd/mk/symtab.c
new file mode 100644
index 0000000..6f7b888
--- /dev/null
+++ b/src/cmd/mk/symtab.c
@@ -0,0 +1,95 @@
+#include	"mk.h"
+
+#define	NHASH	4099
+#define	HASHMUL	79L	/* this is a good value */
+static Symtab *hash[NHASH];
+
+void
+syminit(void)
+{
+	Symtab **s, *ss;
+
+	for(s = hash; s < &hash[NHASH]; s++){
+		for(ss = *s; ss; ss = ss->next)
+			free((char *)ss);
+		*s = 0;
+	}
+}
+
+Symtab *
+symlook(char *sym, int space, void *install)
+{
+	long h;
+	char *p;
+	Symtab *s;
+
+	for(p = sym, h = space; *p; h += *p++)
+		h *= HASHMUL;
+	if(h < 0)
+		h = ~h;
+	h %= NHASH;
+	for(s = hash[h]; s; s = s->next)
+		if((s->space == space) && (strcmp(s->name, sym) == 0))
+			return(s);
+	if(install == 0)
+		return(0);
+	s = (Symtab *)Malloc(sizeof(Symtab));
+	s->space = space;
+	s->name = sym;
+	s->value = install;
+	s->next = hash[h];
+	hash[h] = s;
+	return(s);
+}
+
+void
+symdel(char *sym, int space)
+{
+	long h;
+	char *p;
+	Symtab *s, *ls;
+
+	/* multiple memory leaks */
+
+	for(p = sym, h = space; *p; h += *p++)
+		h *= HASHMUL;
+	if(h < 0)
+		h = ~h;
+	h %= NHASH;
+	for(s = hash[h], ls = 0; s; ls = s, s = s->next)
+		if((s->space == space) && (strcmp(s->name, sym) == 0)){
+			if(ls)
+				ls->next = s->next;
+			else
+				hash[h] = s->next;
+			free((char *)s);
+		}
+}
+
+void
+symtraverse(int space, void (*fn)(Symtab*))
+{
+	Symtab **s, *ss;
+
+	for(s = hash; s < &hash[NHASH]; s++)
+		for(ss = *s; ss; ss = ss->next)
+			if(ss->space == space)
+				(*fn)(ss);
+}
+
+void
+symstat(void)
+{
+	Symtab **s, *ss;
+	int n;
+	int l[1000];
+
+	memset((char *)l, 0, sizeof(l));
+	for(s = hash; s < &hash[NHASH]; s++){
+		for(ss = *s, n = 0; ss; ss = ss->next)
+			n++;
+		l[n]++;
+	}
+	for(n = 0; n < 1000; n++)
+		if(l[n]) Bprint(&bout, "%ld of length %d\n", l[n], n);
+}
diff --git a/src/cmd/mk/unix.c b/src/cmd/mk/unix.c
new file mode 100644
index 0000000..6022278
--- /dev/null
+++ b/src/cmd/mk/unix.c
@@ -0,0 +1,306 @@
+#include	"mk.h"
+#include	<sys/wait.h>
+#include	<signal.h>
+#include	<sys/stat.h>
+#include	<sys/time.h>
+
+char	*shell = "/bin/sh";
+char	*shellname = "sh";
+
+extern char **environ;
+
+static void
+mkperror(char *s)
+{
+	fprint(2, "%s: %r\n", s);
+}
+
+void
+readenv(void)
+{
+	char **p, *s;
+	Word *w;
+
+	for(p = environ; *p; p++){
+		s = shname(*p);
+		if(*s == '=') {
+			*s = 0;
+			w = newword(s+1);
+		} else
+			w = newword("");
+		if (symlook(*p, S_INTERNAL, 0))
+			continue;
+		s = strdup(*p);
+		setvar(s, (void *)w);
+		symlook(s, S_EXPORTED, (void*)"")->value = (void*)"";
+	}
+}
+
+/*
+ *	done on child side of fork, so parent's env is not affected
+ *	and we don't care about freeing memory because we're going
+ *	to exec immediately after this.
+ */
+void
+exportenv(Envy *e)
+{
+	int i;
+	char **p;
+	char buf[4096];
+
+	p = 0;
+	for(i = 0; e->name; e++, i++) {
+		p = (char**) Realloc(p, (i+2)*sizeof(char*));
+		if(e->values)
+			sprint(buf, "%s=%s", e->name,  wtos(e->values, IWS));
+		else
+			sprint(buf, "%s=", e->name);
+		p[i] = strdup(buf);
+	}
+	p[i] = 0;
+	environ = p;
+}
+
+int
+waitfor(char *msg)
+{
+	int status;
+	int pid;
+
+	*msg = 0;
+	pid = wait(&status);
+	if(pid > 0) {
+		if(status&0x7f) {
+			if(status&0x80)
+				snprint(msg, ERRMAX, "signal %d, core dumped", status&0x7f);
+			else
+				snprint(msg, ERRMAX, "signal %d", status&0x7f);
+		} else if(status&0xff00)
+			snprint(msg, ERRMAX, "exit(%d)", (status>>8)&0xff);
+	}
+	return pid;
+}
+
+void
+expunge(int pid, char *msg)
+{
+	if(strcmp(msg, "interrupt"))
+		kill(pid, SIGINT);
+	else
+		kill(pid, SIGHUP);
+}
+
+int
+execsh(char *args, char *cmd, Bufblock *buf, Envy *e)
+{
+	char *p;
+	int tot, n, pid, in[2], out[2];
+
+	if(buf && pipe(out) < 0){
+		mkperror("pipe");
+		Exit();
+	}
+	pid = fork();
+	if(pid < 0){
+		mkperror("mk fork");
+		Exit();
+	}
+	if(pid == 0){
+		if(buf)
+			close(out[0]);
+		if(pipe(in) < 0){
+			mkperror("pipe");
+			Exit();
+		}
+		pid = fork();
+		if(pid < 0){
+			mkperror("mk fork");
+			Exit();
+		}
+		if(pid != 0){
+			dup2(in[0], 0);
+			if(buf){
+				dup2(out[1], 1);
+				close(out[1]);
+			}
+			close(in[0]);
+			close(in[1]);
+			if (e)
+				exportenv(e);
+			if(shflags)
+				execl(shell, shellname, shflags, args, 0);
+			else
+				execl(shell, shellname, args, 0);
+			mkperror(shell);
+			_exit(1);
+		}
+		close(out[1]);
+		close(in[0]);
+		if(DEBUG(D_EXEC))
+			fprint(1, "starting: %s\n", cmd);
+		p = cmd+strlen(cmd);
+		while(cmd < p){
+			n = write(in[1], cmd, p-cmd);
+			if(n < 0)
+				break;
+			cmd += n;
+		}
+		close(in[1]);
+		_exit(0);
+	}
+	if(buf){
+		close(out[1]);
+		tot = 0;
+		for(;;){
+			if (buf->current >= buf->end)
+				growbuf(buf);
+			n = read(out[0], buf->current, buf->end-buf->current);
+			if(n <= 0)
+				break;
+			buf->current += n;
+			tot += n;
+		}
+		if (tot && buf->current[-1] == '\n')
+			buf->current--;
+		close(out[0]);
+	}
+	return pid;
+}
+
+int
+pipecmd(char *cmd, Envy *e, int *fd)
+{
+	int pid, pfd[2];
+
+	if(DEBUG(D_EXEC))
+		fprint(1, "pipecmd='%s'\n", cmd);/**/
+
+	if(fd && pipe(pfd) < 0){
+		mkperror("pipe");
+		Exit();
+	}
+	pid = fork();
+	if(pid < 0){
+		mkperror("mk fork");
+		Exit();
+	}
+	if(pid == 0){
+		if(fd){
+			close(pfd[0]);
+			dup2(pfd[1], 1);
+			close(pfd[1]);
+		}
+		if(e)
+			exportenv(e);
+		if(shflags)
+			execl(shell, shellname, shflags, "-c", cmd, 0);
+		else
+			execl(shell, shellname, "-c", cmd, 0);
+		mkperror(shell);
+		_exit(1);
+	}
+	if(fd){
+		close(pfd[1]);
+		*fd = pfd[0];
+	}
+	return pid;
+}
+
+void
+Exit(void)
+{
+	while(wait(0) >= 0)
+		;
+	exits("error");
+}
+
+static	struct
+{
+	int	sig;
+	char	*msg;
+}	sigmsgs[] =
+{
+	SIGALRM,	"alarm",
+	SIGFPE,		"sys: fp: fptrap",
+	SIGPIPE,	"sys: write on closed pipe",
+	SIGILL,		"sys: trap: illegal instruction",
+	SIGSEGV,	"sys: segmentation violation",
+	0,		0
+};
+
+static void
+notifyf(int sig)
+{
+	int i;
+
+	for(i = 0; sigmsgs[i].msg; i++)
+		if(sigmsgs[i].sig == sig)
+			killchildren(sigmsgs[i].msg);
+
+	/* should never happen */
+	signal(sig, SIG_DFL);
+	kill(getpid(), sig);
+}
+
+void
+catchnotes()
+{
+	int i;
+
+	for(i = 0; sigmsgs[i].msg; i++)
+		signal(sigmsgs[i].sig, notifyf);
+}
+
+char*
+maketmp(int *pfd)
+{
+	static char temp[] = "/tmp/mkargXXXXXX";
+	static char buf[100];
+	int fd;
+
+	strcpy(buf, temp);
+	fd = mkstemp(buf);
+	if(fd < 0)
+		return 0;
+	*pfd = fd;
+	return buf;
+}
+
+int
+chgtime(char *name)
+{
+	if(access(name, 0) >= 0)
+		return utimes(name, 0);
+	return close(creat(name, 0666));
+}
+
+void
+rcopy(char **to, Resub *match, int n)
+{
+	int c;
+	char *p;
+
+	*to = match->s.sp;		/* stem0 matches complete target */
+	for(to++, match++; --n > 0; to++, match++){
+		if(match->s.sp && match->e.ep){
+			p = match->e.ep;
+			c = *p;
+			*p = 0;
+			*to = strdup(match->s.sp);
+			*p = c;
+		}
+		else
+			*to = 0;
+	}
+}
+
+ulong
+mkmtime(char *name)
+{
+	struct stat st;
+
+	if(stat(name, &st) < 0)
+		return 0;
+
+	return st.st_mtime;
+}
diff --git a/src/cmd/mk/var.c b/src/cmd/mk/var.c
new file mode 100644
index 0000000..8429918
--- /dev/null
+++ b/src/cmd/mk/var.c
@@ -0,0 +1,41 @@
+#include	"mk.h"
+
+void
+setvar(char *name, void *value)
+{
+	symlook(name, S_VAR, value)->value = value;
+	symlook(name, S_MAKEVAR, (void*)"");
+}
+
+static void
+print1(Symtab *s)
+{
+	Word *w;
+
+	Bprint(&bout, "\t%s=", s->name);
+	for (w = (Word *) s->value; w; w = w->next)
+		Bprint(&bout, "'%s'", w->s);
+	Bprint(&bout, "\n");
+}
+
+void
+dumpv(char *s)
+{
+	Bprint(&bout, "%s:\n", s);
+	symtraverse(S_VAR, print1);
+}
+
+char *
+shname(char *a)
+{
+	Rune r;
+	int n;
+
+	while (*a) {
+		n = chartorune(&r, a);
+		if (!WORDCHR(r))
+			break;
+		a += n;
+	}
+	return a;
+}
diff --git a/src/cmd/mk/varsub.c b/src/cmd/mk/varsub.c
new file mode 100644
index 0000000..2a9ad98
--- /dev/null
+++ b/src/cmd/mk/varsub.c
@@ -0,0 +1,256 @@
+#include	"mk.h"
+
+static	Word		*subsub(Word*, char*, char*);
+static	Word		*expandvar(char**);
+static	Bufblock	*varname(char**);
+static	Word		*extractpat(char*, char**, char*, char*);
+static	int		submatch(char*, Word*, Word*, int*, char**);
+static	Word		*varmatch(char *, char**);
+
+Word *
+varsub(char **s)
+{
+	Bufblock *b;
+	Word *w;
+
+	if(**s == '{')		/* either ${name} or ${name: A%B==C%D}*/
+		return expandvar(s);
+
+	b = varname(s);
+	if(b == 0)
+		return 0;
+
+	w = varmatch(b->start, s);
+	freebuf(b);
+	return w;
+}
+
+/*
+ *	extract a variable name
+ */
+static Bufblock*
+varname(char **s)
+{
+	Bufblock *b;
+	char *cp;
+	Rune r;
+	int n;
+
+	b = newbuf();
+	cp = *s;
+	for(;;){
+		n = chartorune(&r, cp);
+		if (!WORDCHR(r))
+			break;
+		rinsert(b, r);
+		cp += n;
+	}
+	if (b->current == b->start){
+		SYNERR(-1);
+		fprint(2, "missing variable name <%s>\n", *s);
+		freebuf(b);
+		return 0;
+	}
+	*s = cp;
+	insert(b, 0);
+	return b;
+}
+
+static Word*
+varmatch(char *name, char **s)
+{
+	Word *w;
+	Symtab *sym;
+	char *cp;
+	
+	sym = symlook(name, S_VAR, 0);
+	if(sym){
+			/* check for at least one non-NULL value */
+		for (w = (Word*)sym->value; w; w = w->next)
+			if(w->s && *w->s)
+				return wdup(w);
+	}
+	for(cp = *s; *cp == ' ' || *cp == '\t'; cp++)	/* skip trailing whitespace */
+			;
+	*s = cp;
+	return 0;
+}
+
+static Word*
+expandvar(char **s)
+{
+	Word *w;
+	Bufblock *buf;
+	Symtab *sym;
+	char *cp, *begin, *end;
+
+	begin = *s;
+	(*s)++;						/* skip the '{' */
+	buf = varname(s);
+	if (buf == 0)
+		return 0;
+	cp = *s;
+	if (*cp == '}') {				/* ${name} variant*/
+		(*s)++;					/* skip the '}' */
+		w = varmatch(buf->start, s);
+		freebuf(buf);
+		return w;
+	}
+	if (*cp != ':') {
+		SYNERR(-1);
+		fprint(2, "bad variable name <%s>\n", buf->start);
+		freebuf(buf);
+		return 0;
+	}
+	cp++;
+	end = charin(cp , "}");
+	if(end == 0){
+		SYNERR(-1);
+		fprint(2, "missing '}': %s\n", begin);
+		Exit();
+	}
+	*end = 0;
+	*s = end+1;
+	
+	sym = symlook(buf->start, S_VAR, 0);
+	if(sym == 0 || sym->value == 0)
+		w = newword(buf->start);
+	else
+		w = subsub((Word*) sym->value, cp, end);
+	freebuf(buf);
+	return w;
+}
+
+static Word*
+extractpat(char *s, char **r, char *term, char *end)
+{
+	int save;
+	char *cp;
+	Word *w;
+
+	cp = charin(s, term);
+	if(cp){
+		*r = cp;
+		if(cp == s)
+			return 0;
+		save = *cp;
+		*cp = 0;
+		w = stow(s);
+		*cp = save;
+	} else {
+		*r = end;
+		w = stow(s);
+	}
+	return w;
+}
+
+static Word*
+subsub(Word *v, char *s, char *end)
+{
+	int nmid;
+	Word *head, *tail, *w, *h;
+	Word *a, *b, *c, *d;
+	Bufblock *buf;
+	char *cp, *enda;
+
+	a = extractpat(s, &cp, "=%&", end);
+	b = c = d = 0;
+	if(PERCENT(*cp))
+		b = extractpat(cp+1, &cp, "=", end);
+	if(*cp == '=')
+		c = extractpat(cp+1, &cp, "&%", end);
+	if(PERCENT(*cp))
+		d = stow(cp+1);
+	else if(*cp)
+		d = stow(cp);
+
+	head = tail = 0;
+	buf = newbuf();
+	for(; v; v = v->next){
+		h = w = 0;
+		if(submatch(v->s, a, b, &nmid, &enda)){
+			/* enda points to end of A match in source;
+			 * nmid = number of chars between end of A and start of B
+			 */
+			if(c){
+				h = w = wdup(c);
+				while(w->next)
+					w = w->next;
+			}
+			if(PERCENT(*cp) && nmid > 0){	
+				if(w){
+					bufcpy(buf, w->s, strlen(w->s));
+					bufcpy(buf, enda, nmid);
+					insert(buf, 0);
+					free(w->s);
+					w->s = strdup(buf->start);
+				} else {
+					bufcpy(buf, enda, nmid);
+					insert(buf, 0);
+					h = w = newword(buf->start);
+				}
+				buf->current = buf->start;
+			}
+			if(d && *d->s){
+				if(w){
+
+					bufcpy(buf, w->s, strlen(w->s));
+					bufcpy(buf, d->s, strlen(d->s));
+					insert(buf, 0);
+					free(w->s);
+					w->s = strdup(buf->start);
+					w->next = wdup(d->next);
+					while(w->next)
+						w = w->next;
+					buf->current = buf->start;
+				} else
+					h = w = wdup(d);
+			}
+		}
+		if(w == 0)
+			h = w = newword(v->s);
+	
+		if(head == 0)
+			head = h;
+		else
+			tail->next = h;
+		tail = w;
+	}
+	freebuf(buf);
+	delword(a);
+	delword(b);
+	delword(c);
+	delword(d);
+	return head;
+}
+
+static int
+submatch(char *s, Word *a, Word *b, int *nmid, char **enda)
+{
+	Word *w;
+	int n;
+	char *end;
+
+	n = 0;
+	for(w = a; w; w = w->next){
+		n = strlen(w->s);
+		if(strncmp(s, w->s, n) == 0)
+			break;
+	}
+	if(a && w == 0)		/*  a == NULL matches everything*/
+		return 0;
+
+	*enda = s+n;		/* pointer to end a A part match */
+	*nmid = strlen(s)-n;	/* size of remainder of source */
+	end = *enda+*nmid;
+	for(w = b; w; w = w->next){
+		n = strlen(w->s);
+		if(strcmp(w->s, end-n) == 0){
+			*nmid -= n;
+			break;
+		}
+	}
+	if(b && w == 0)		/* b == NULL matches everything */
+		return 0;
+	return 1;
+}
diff --git a/src/cmd/mk/word.c b/src/cmd/mk/word.c
new file mode 100644
index 0000000..ac34c47
--- /dev/null
+++ b/src/cmd/mk/word.c
@@ -0,0 +1,180 @@
+#include	"mk.h"
+
+static	Word	*nextword(char**);
+
+Word*
+newword(char *s)
+{
+	Word *w;
+
+	w = (Word *)Malloc(sizeof(Word));
+	w->s = strdup(s);
+	w->next = 0;
+	return(w);
+}
+
+Word *
+stow(char *s)
+{
+	Word *head, *w, *new;
+
+	w = head = 0;
+	while(*s){
+		new = nextword(&s);
+		if(new == 0)
+			break;
+		if (w)
+			w->next = new;
+		else
+			head = w = new;
+		while(w->next)
+			w = w->next;
+		
+	}
+	if (!head)
+		head = newword("");
+	return(head);
+}
+
+char *
+wtos(Word *w, int sep)
+{
+	Bufblock *buf;
+	char *cp;
+
+	buf = newbuf();
+	for(; w; w = w->next){
+		for(cp = w->s; *cp; cp++)
+			insert(buf, *cp);
+		if(w->next)
+			insert(buf, sep);
+	}
+	insert(buf, 0);
+	cp = strdup(buf->start);
+	freebuf(buf);
+	return(cp);
+}
+
+Word*
+wdup(Word *w)
+{
+	Word *v, *new, *base;
+
+	v = base = 0;
+	while(w){
+		new = newword(w->s);
+		if(v)
+			v->next = new;
+		else
+			base = new;
+		v = new;
+		w = w->next;
+	}
+	return base;
+}
+
+void
+delword(Word *w)
+{
+	Word *v;
+
+	while(v = w){
+		w = w->next;
+		if(v->s)
+			free(v->s);
+		free(v);
+	}
+}
+
+/*
+ *	break out a word from a string handling quotes, executions,
+ *	and variable expansions.
+ */
+static Word*
+nextword(char **s)
+{
+	Bufblock *b;
+	Word *head, *tail, *w;
+	Rune r;
+	char *cp;
+
+	cp = *s;
+	b = newbuf();
+	head = tail = 0;
+	while(*cp == ' ' || *cp == '\t')		/* leading white space */
+		cp++;
+	while(*cp){
+		cp += chartorune(&r, cp);
+		switch(r)
+		{
+		case ' ':
+		case '\t':
+		case '\n':
+			goto out;
+		case '\\':
+		case '\'':
+		case '"':
+			cp = expandquote(cp, r, b);
+			if(cp == 0){
+				fprint(2, "missing closing quote: %s\n", *s);
+				Exit();
+			}
+			break;
+		case '$':
+			w = varsub(&cp);
+			if(w == 0)
+				break;
+			if(b->current != b->start){
+				bufcpy(b, w->s, strlen(w->s));
+				insert(b, 0);
+				free(w->s);
+				w->s = strdup(b->start);
+				b->current = b->start;
+			}
+			if(head){
+				bufcpy(b, tail->s, strlen(tail->s));
+				bufcpy(b, w->s, strlen(w->s));
+				insert(b, 0);
+				free(tail->s);
+				tail->s = strdup(b->start);
+				tail->next = w->next;
+				free(w->s);
+				free(w);
+				b->current = b->start;
+			} else
+				tail = head = w;
+			while(tail->next)
+				tail = tail->next;
+			break;
+		default:
+			rinsert(b, r);
+			break;
+		}
+	}
+out:
+	*s = cp;
+	if(b->current != b->start){
+		if(head){
+			cp = b->current;
+			bufcpy(b, tail->s, strlen(tail->s));
+			bufcpy(b, b->start, cp-b->start);
+			insert(b, 0);
+			free(tail->s);
+			tail->s = strdup(cp);
+		} else {
+			insert(b, 0);
+			head = newword(b->start);
+		}
+	}
+	freebuf(b);
+	return head;
+}
+
+void
+dumpw(char *s, Word *w)
+{
+	Bprint(&bout, "%s", s);
+	for(; w; w = w->next)
+		Bprint(&bout, " '%s'", w->s);
+	Bputc(&bout, '\n');
+}
diff --git a/src/cmd/sam/LICENSE b/src/cmd/sam/LICENSE
new file mode 100644
index 0000000..a5d7d87
--- /dev/null
+++ b/src/cmd/sam/LICENSE
@@ -0,0 +1,258 @@
+The Plan 9 software is provided under the terms of the
+Lucent Public License, Version 1.02, reproduced below,
+with the following exceptions:
+
+1. No right is granted to create derivative works of or
+   to redistribute (other than with the Plan 9 Operating System)
+   the screen imprinter fonts identified in subdirectory
+   /lib/font/bit/lucida and printer fonts (Lucida Sans Unicode, Lucida
+   Sans Italic, Lucida Sans Demibold, Lucida Typewriter, Lucida Sans
+   Typewriter83), identified in subdirectory /sys/lib/postscript/font.
+   These directories contain material copyrights by B&H Inc. and Y&Y Inc.
+
+2. The printer fonts identified in subdirectory /sys/lib/ghostscript/font
+   are subject to the GNU GPL, reproduced in the file /LICENSE.gpl.
+
+3. The ghostscript program in the subdirectory /sys/src/cmd/gs is
+   covered by the Aladdin Free Public License, reproduced in the file
+   /LICENSE.afpl.
+
+===================================================================
+
+Lucent Public License Version 1.02
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE
+PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+  a. in the case of Lucent Technologies Inc. ("LUCENT"), the Original
+     Program, and
+  b. in the case of each Contributor,
+
+     i. changes to the Program, and
+    ii. additions to the Program;
+
+    where such changes and/or additions to the Program were added to the
+    Program by such Contributor itself or anyone acting on such
+    Contributor's behalf, and the Contributor explicitly consents, in
+    accordance with Section 3C, to characterization of the changes and/or
+    additions as Contributions.
+
+"Contributor" means LUCENT and any other entity that has Contributed a
+Contribution to the Program.
+
+"Distributor" means a Recipient that distributes the Program,
+modifications to the Program, or any part thereof.
+
+"Licensed Patents" mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its Contribution
+alone or when combined with the Program.
+
+"Original Program" means the original version of the software
+accompanying this Agreement as released by LUCENT, including source
+code, object code and documentation, if any.
+
+"Program" means the Original Program and Contributions or any part
+thereof
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+ a. Subject to the terms of this Agreement, each Contributor hereby
+    grants Recipient a non-exclusive, worldwide, royalty-free copyright
+    license to reproduce, prepare derivative works of, publicly display,
+    publicly perform, distribute and sublicense the Contribution of such
+    Contributor, if any, and such derivative works, in source code and
+    object code form.
+    
+ b. Subject to the terms of this Agreement, each Contributor hereby
+    grants Recipient a non-exclusive, worldwide, royalty-free patent
+    license under Licensed Patents to make, use, sell, offer to sell,
+    import and otherwise transfer the Contribution of such Contributor, if
+    any, in source code and object code form. The patent license granted
+    by a Contributor shall also apply to the combination of the
+    Contribution of that Contributor and the Program if, at the time the
+    Contribution is added by the Contributor, such addition of the
+    Contribution causes such combination to be covered by the Licensed
+    Patents. The patent license granted by a Contributor shall not apply
+    to (i) any other combinations which include the Contribution, nor to
+    (ii) Contributions of other Contributors. No hardware per se is
+    licensed hereunder.
+    
+ c. Recipient understands that although each Contributor grants the
+    licenses to its Contributions set forth herein, no assurances are
+    provided by any Contributor that the Program does not infringe the
+    patent or other intellectual property rights of any other entity. Each
+    Contributor disclaims any liability to Recipient for claims brought by
+    any other entity based on infringement of intellectual property rights
+    or otherwise. As a condition to exercising the rights and licenses
+    granted hereunder, each Recipient hereby assumes sole responsibility
+    to secure any other intellectual property rights needed, if any. For
+    example, if a third party patent license is required to allow
+    Recipient to distribute the Program, it is Recipient's responsibility
+    to acquire that license before distributing the Program.
+
+ d. Each Contributor represents that to its knowledge it has sufficient
+    copyright rights in its Contribution, if any, to grant the copyright
+    license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A. Distributor may choose to distribute the Program in any form under
+this Agreement or under its own license agreement, provided that:
+
+ a. it complies with the terms and conditions of this Agreement;
+
+ b. if the Program is distributed in source code or other tangible
+    form, a copy of this Agreement or Distributor's own license agreement
+    is included with each copy of the Program; and
+
+ c. if distributed under Distributor's own license agreement, such
+    license agreement:
+
+      i. effectively disclaims on behalf of all Contributors all warranties
+         and conditions, express and implied, including warranties or
+         conditions of title and non-infringement, and implied warranties or
+         conditions of merchantability and fitness for a particular purpose;
+     ii. effectively excludes on behalf of all Contributors all liability
+         for damages, including direct, indirect, special, incidental and
+         consequential damages, such as lost profits; and
+    iii. states that any provisions which differ from this Agreement are
+         offered by that Contributor alone and not by any other party.
+
+B. Each Distributor must include the following in a conspicuous
+   location in the Program:
+
+   Copyright (C) 2003, Lucent Technologies Inc. and others. All Rights
+   Reserved.
+
+C. In addition, each Contributor must identify itself as the
+originator of its Contribution in a manner that reasonably allows
+subsequent Recipients to identify the originator of the Contribution.
+Also, each Contributor must agree that the additions and/or changes
+are intended to be a Contribution. Once a Contribution is contributed,
+it may not thereafter be revoked.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial use
+of the Program, the Distributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for Contributors. Therefore, if a
+Distributor includes the Program in a commercial product offering,
+such Distributor ("Commercial Distributor") hereby agrees to defend
+and indemnify every Contributor ("Indemnified Contributor") against
+any losses, damages and costs (collectively"Losses") arising from
+claims, lawsuits and other legal actions brought by a third party
+against the Indemnified Contributor to the extent caused by the acts
+or omissions of such Commercial Distributor in connection with its
+distribution of the Program in a commercial product offering. The
+obligations in this section do not apply to any claims or Losses
+relating to any actual or alleged intellectual property infringement.
+In order to qualify, an Indemnified Contributor must: a) promptly
+notify the Commercial Distributor in writing of such claim, and b)
+allow the Commercial Distributor to control, and cooperate with the
+Commercial Distributor in, the defense and any related settlement
+negotiations. The Indemnified Contributor may participate in any such
+claim at its own expense.
+
+For example, a Distributor might include the Program in a commercial
+product offering, Product X. That Distributor is then a Commercial
+Distributor. If that Commercial Distributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Distributor's responsibility
+alone. Under this section, the Commercial Distributor would have to
+defend claims against the Contributors related to those performance
+claims and warranties, and if a court requires any Contributor to pay
+any damages as a result, the Commercial Distributor must pay those
+damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with its
+exercise of rights under this Agreement, including but not limited to
+the risks and costs of program errors, compliance with applicable
+laws, damage to or loss of data, programs or equipment, and
+unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR
+ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
+WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
+DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
+HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. EXPORT CONTROL
+
+Recipient agrees that Recipient alone is responsible for compliance
+with the United States export administration regulations (and the
+export control laws and regulation of any other countries).
+
+8. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed to the
+minimum extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against a Contributor with
+respect to a patent applicable to software (including a cross-claim or
+counterclaim in a lawsuit), then any patent licenses granted by that
+Contributor to such Recipient under this Agreement shall terminate as
+of the date such litigation is filed. In addition, if Recipient
+institutes patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Program
+itself (excluding combinations of the Program with other software or
+hardware) infringes such Recipient's patent(s), then such Recipient's
+rights granted under Section 2(b) shall terminate as of the date such
+litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it
+fails to comply with any of the material terms or conditions of this
+Agreement and does not cure such failure in a reasonable period of
+time after becoming aware of such noncompliance. If all Recipient's
+rights under this Agreement terminate, Recipient agrees to cease use
+and distribution of the Program as soon as reasonably practicable.
+However, Recipient's obligations under this Agreement and any licenses
+granted by Recipient relating to the Program shall continue and
+survive.
+
+LUCENT may publish new versions (including revisions) of this
+Agreement from time to time. Each new version of the Agreement will be
+given a distinguishing version number. The Program (including
+Contributions) may always be distributed subject to the version of the
+Agreement under which it was received. In addition, after a new
+version of the Agreement is published, Contributor may elect to
+distribute the Program (including its Contributions) under the new
+version. No one other than LUCENT has the right to modify this
+Agreement. Except as expressly stated in Sections 2(a) and 2(b) above,
+Recipient receives no rights or licenses to the intellectual property
+of any Contributor under this Agreement, whether expressly, by
+implication, estoppel or otherwise. All rights in the Program not
+expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this Agreement
+more than one year after the cause of action arose. Each party waives
+its rights to a jury trial in any resulting litigation.
+
diff --git a/src/cmd/sam/Makefile b/src/cmd/sam/Makefile
new file mode 100644
index 0000000..f5e9146
--- /dev/null
+++ b/src/cmd/sam/Makefile
@@ -0,0 +1,18 @@
+H=errors.h mesg.h parse.h plumb.h sam.h
+SRC= address.c buff.c cmd.c disk.c error.c file.c io.c\
+     list.c mesg.c moveto.c multi.c unix.c rasp.c regexp.c\
+     sam.c shell.c string.c sys.c util.c xec.c plumb.c
+
+CC=gcc
+PREFIX=$(HOME)
+#PREFIX=/usr/local
+CFLAGS=-I. -I$(PREFIX)/include -O -g
+LDFLAGS=-L$(PREFIX)/lib
+LDLIBS=-l9 -lfmt -lutf
+
+all: sam
+sam: $(SRC) $(H)
+	$(CC) -o $@ $(CFLAGS) $(SRC) $(LDFLAGS) $(LDLIBS)
+clean:
+	rm -f *.o *~
+	rm -f sam
diff --git a/src/cmd/sam/address.c b/src/cmd/sam/address.c
new file mode 100644
index 0000000..85cca17
--- /dev/null
+++ b/src/cmd/sam/address.c
@@ -0,0 +1,240 @@
+#include "sam.h"
+#include "parse.h"
+
+Address	addr;
+String	lastpat;
+int	patset;
+File	*menu;
+
+File	*matchfile(String*);
+Address	charaddr(Posn, Address, int);
+
+Address
+address(Addr *ap, Address a, int sign)
+{
+	File *f = a.f;
+	Address a1, a2;
+
+	do{
+		switch(ap->type){
+		case 'l':
+		case '#':
+			a = (*(ap->type=='#'?charaddr:lineaddr))(ap->num, a, sign);
+			break;
+
+		case '.':
+			a = f->dot;
+			break;
+
+		case '$':
+			a.r.p1 = a.r.p2 = f->_.nc;
+			break;
+
+		case '\'':
+			a.r = f->mark;
+			break;
+
+		case '?':
+			sign = -sign;
+			if(sign == 0)
+				sign = -1;
+			/* fall through */
+		case '/':
+			nextmatch(f, ap->are, sign>=0? a.r.p2 : a.r.p1, sign);
+			a.r = sel.p[0];
+			break;
+
+		case '"':
+			a = matchfile(ap->are)->dot;
+			f = a.f;
+			if(f->unread)
+				load(f);
+			break;
+
+		case '*':
+			a.r.p1 = 0, a.r.p2 = f->_.nc;
+			return a;
+
+		case ',':
+		case ';':
+			if(ap->left)
+				a1 = address(ap->left, a, 0);
+			else
+				a1.f = a.f, a1.r.p1 = a1.r.p2 = 0;
+			if(ap->type == ';'){
+				f = a1.f;
+				a = a1;
+				f->dot = a1;
+			}
+			if(ap->next)
+				a2 = address(ap->next, a, 0);
+			else
+				a2.f = a.f, a2.r.p1 = a2.r.p2 = f->_.nc;
+			if(a1.f != a2.f)
+				error(Eorder);
+			a.f = a1.f, a.r.p1 = a1.r.p1, a.r.p2 = a2.r.p2;
+			if(a.r.p2 < a.r.p1)
+				error(Eorder);
+			return a;
+
+		case '+':
+		case '-':
+			sign = 1;
+			if(ap->type == '-')
+				sign = -1;
+			if(ap->next==0 || ap->next->type=='+' || ap->next->type=='-')
+				a = lineaddr(1L, a, sign);
+			break;
+		default:
+			panic("address");
+			return a;
+		}
+	}while(ap = ap->next);	/* assign = */
+	return a;
+}
+
+void
+nextmatch(File *f, String *r, Posn p, int sign)
+{
+	compile(r);
+	if(sign >= 0){
+		if(!execute(f, p, INFINITY))
+			error(Esearch);
+		if(sel.p[0].p1==sel.p[0].p2 && sel.p[0].p1==p){
+			if(++p>f->_.nc)
+				p = 0;
+			if(!execute(f, p, INFINITY))
+				panic("address");
+		}
+	}else{
+		if(!bexecute(f, p))
+			error(Esearch);
+		if(sel.p[0].p1==sel.p[0].p2 && sel.p[0].p2==p){
+			if(--p<0)
+				p = f->_.nc;
+			if(!bexecute(f, p))
+				panic("address");
+		}
+	}
+}
+
+File *
+matchfile(String *r)
+{
+	File *f;
+	File *match = 0;
+	int i;
+
+	for(i = 0; i<file.nused; i++){
+		f = file.filepptr[i];
+		if(f == cmd)
+			continue;
+		if(filematch(f, r)){
+			if(match)
+				error(Emanyfiles);
+			match = f;
+		}
+	}
+	if(!match)
+		error(Efsearch);
+	return match;
+}
+
+int
+filematch(File *f, String *r)
+{
+	char *c, buf[STRSIZE+100];
+	String *t;
+
+	c = Strtoc(&f->name);
+	sprint(buf, "%c%c%c %s\n", " '"[f->mod],
+		"-+"[f->rasp!=0], " ."[f==curfile], c);
+	free(c);
+	t = tmpcstr(buf);
+	Strduplstr(&genstr, t);
+	freetmpstr(t);
+	/* A little dirty... */
+	if(menu == 0)
+		menu = fileopen();
+	bufreset(menu);
+	bufinsert(menu, 0, genstr.s, genstr.n);
+	compile(r);
+	return execute(menu, 0, menu->_.nc);
+}
+
+Address
+charaddr(Posn l, Address addr, int sign)
+{
+	if(sign == 0)
+		addr.r.p1 = addr.r.p2 = l;
+	else if(sign < 0)
+		addr.r.p2 = addr.r.p1-=l;
+	else if(sign > 0)
+		addr.r.p1 = addr.r.p2+=l;
+	if(addr.r.p1<0 || addr.r.p2>addr.f->_.nc)
+		error(Erange);
+	return addr;
+}
+
+Address
+lineaddr(Posn l, Address addr, int sign)
+{
+	int n;
+	int c;
+	File *f = addr.f;
+	Address a;
+	Posn p;
+
+	a.f = f;
+	if(sign >= 0){
+		if(l == 0){
+			if(sign==0 || addr.r.p2==0){
+				a.r.p1 = a.r.p2 = 0;
+				return a;
+			}
+			a.r.p1 = addr.r.p2;
+			p = addr.r.p2-1;
+		}else{
+			if(sign==0 || addr.r.p2==0){
+				p = (Posn)0;
+				n = 1;
+			}else{
+				p = addr.r.p2-1;
+				n = filereadc(f, p++)=='\n';
+			}
+			while(n < l){
+				if(p >= f->_.nc)
+					error(Erange);
+				if(filereadc(f, p++) == '\n')
+					n++;
+			}
+			a.r.p1 = p;
+		}
+		while(p < f->_.nc && filereadc(f, p++)!='\n')
+			;
+		a.r.p2 = p;
+	}else{
+		p = addr.r.p1;
+		if(l == 0)
+			a.r.p2 = addr.r.p1;
+		else{
+			for(n = 0; n<l; ){	/* always runs once */
+				if(p == 0){
+					if(++n != l)
+						error(Erange);
+				}else{
+					c = filereadc(f, p-1);
+					if(c != '\n' || ++n != l)
+						p--;
+				}
+			}
+			a.r.p2 = p;
+			if(p > 0)
+				p--;
+		}
+		while(p > 0 && filereadc(f, p-1)!='\n')	/* lines start after a newline */
+			p--;
+		a.r.p1 = p;
+	}
+	return a;
+}
diff --git a/src/cmd/sam/buff.c b/src/cmd/sam/buff.c
new file mode 100644
index 0000000..30493c3
--- /dev/null
+++ b/src/cmd/sam/buff.c
@@ -0,0 +1,302 @@
+#include "sam.h"
+
+enum
+{
+	Slop = 100,	/* room to grow with reallocation */
+};
+
+static
+void
+sizecache(Buffer *b, uint n)
+{
+	if(n <= b->cmax)
+		return;
+	b->cmax = n+Slop;
+	b->c = runerealloc(b->c, b->cmax);
+}
+
+static
+void
+addblock(Buffer *b, uint i, uint n)
+{
+	if(i > b->nbl)
+		panic("internal error: addblock");
+
+	b->bl = realloc(b->bl, (b->nbl+1)*sizeof b->bl[0]);
+	if(i < b->nbl)
+		memmove(b->bl+i+1, b->bl+i, (b->nbl-i)*sizeof(Block*));
+	b->bl[i] = disknewblock(disk, n);
+	b->nbl++;
+}
+
+
+static
+void
+delblock(Buffer *b, uint i)
+{
+	if(i >= b->nbl)
+		panic("internal error: delblock");
+
+	diskrelease(disk, b->bl[i]);
+	b->nbl--;
+	if(i < b->nbl)
+		memmove(b->bl+i, b->bl+i+1, (b->nbl-i)*sizeof(Block*));
+	b->bl = realloc(b->bl, b->nbl*sizeof b->bl[0]);
+}
+
+/*
+ * Move cache so b->cq <= q0 < b->cq+b->cnc.
+ * If at very end, q0 will fall on end of cache block.
+ */
+
+static
+void
+flush(Buffer *b)
+{
+	if(b->cdirty || b->cnc==0){
+		if(b->cnc == 0)
+			delblock(b, b->cbi);
+		else
+			diskwrite(disk, &b->bl[b->cbi], b->c, b->cnc);
+		b->cdirty = FALSE;
+	}
+}
+
+static
+void
+setcache(Buffer *b, uint q0)
+{
+	Block **blp, *bl;
+	uint i, q;
+
+	if(q0 > b->nc)
+		panic("internal error: setcache");
+	/*
+	 * flush and reload if q0 is not in cache.
+	 */
+	if(b->nc == 0 || (b->cq<=q0 && q0<b->cq+b->cnc))
+		return;
+	/*
+	 * if q0 is at end of file and end of cache, continue to grow this block
+	 */
+	if(q0==b->nc && q0==b->cq+b->cnc && b->cnc<=Maxblock)
+		return;
+	flush(b);
+	/* find block */
+	if(q0 < b->cq){
+		q = 0;
+		i = 0;
+	}else{
+		q = b->cq;
+		i = b->cbi;
+	}
+	blp = &b->bl[i];
+	while(q+(*blp)->_.n <= q0 && q+(*blp)->_.n < b->nc){
+		q += (*blp)->_.n;
+		i++;
+		blp++;
+		if(i >= b->nbl)
+			panic("block not found");
+	}
+	bl = *blp;
+	/* remember position */
+	b->cbi = i;
+	b->cq = q;
+	sizecache(b, bl->_.n);
+	b->cnc = bl->_.n;
+	/*read block*/
+	diskread(disk, bl, b->c, b->cnc);
+}
+
+void
+bufinsert(Buffer *b, uint q0, Rune *s, uint n)
+{
+	uint i, m, t, off;
+
+	if(q0 > b->nc)
+		panic("internal error: bufinsert");
+
+	while(n > 0){
+		setcache(b, q0);
+		off = q0-b->cq;
+		if(b->cnc+n <= Maxblock){
+			/* Everything fits in one block. */
+			t = b->cnc+n;
+			m = n;
+			if(b->bl == nil){	/* allocate */
+				if(b->cnc != 0)
+					panic("internal error: bufinsert1 cnc!=0");
+				addblock(b, 0, t);
+				b->cbi = 0;
+			}
+			sizecache(b, t);
+			runemove(b->c+off+m, b->c+off, b->cnc-off);
+			runemove(b->c+off, s, m);
+			b->cnc = t;
+			goto Tail;
+		}
+		/*
+		 * We must make a new block.  If q0 is at
+		 * the very beginning or end of this block,
+		 * just make a new block and fill it.
+		 */
+		if(q0==b->cq || q0==b->cq+b->cnc){
+			if(b->cdirty)
+				flush(b);
+			m = min(n, Maxblock);
+			if(b->bl == nil){	/* allocate */
+				if(b->cnc != 0)
+					panic("internal error: bufinsert2 cnc!=0");
+				i = 0;
+			}else{
+				i = b->cbi;
+				if(q0 > b->cq)
+					i++;
+			}
+			addblock(b, i, m);
+			sizecache(b, m);
+			runemove(b->c, s, m);
+			b->cq = q0;
+			b->cbi = i;
+			b->cnc = m;
+			goto Tail;
+		}
+		/*
+		 * Split the block; cut off the right side and
+		 * let go of it.
+		 */
+		m = b->cnc-off;
+		if(m > 0){
+			i = b->cbi+1;
+			addblock(b, i, m);
+			diskwrite(disk, &b->bl[i], b->c+off, m);
+			b->cnc -= m;
+		}
+		/*
+		 * Now at end of block.  Take as much input
+		 * as possible and tack it on end of block.
+		 */
+		m = min(n, Maxblock-b->cnc);
+		sizecache(b, b->cnc+m);
+		runemove(b->c+b->cnc, s, m);
+		b->cnc += m;
+  Tail:
+		b->nc += m;
+		q0 += m;
+		s += m;
+		n -= m;
+		b->cdirty = TRUE;
+	}
+}
+
+void
+bufdelete(Buffer *b, uint q0, uint q1)
+{
+	uint m, n, off;
+
+	if(!(q0<=q1 && q0<=b->nc && q1<=b->nc))
+		panic("internal error: bufdelete");
+	while(q1 > q0){
+		setcache(b, q0);
+		off = q0-b->cq;
+		if(q1 > b->cq+b->cnc)
+			n = b->cnc - off;
+		else
+			n = q1-q0;
+		m = b->cnc - (off+n);
+		if(m > 0)
+			runemove(b->c+off, b->c+off+n, m);
+		b->cnc -= n;
+		b->cdirty = TRUE;
+		q1 -= n;
+		b->nc -= n;
+	}
+}
+
+uint
+bufload(Buffer *b, uint q0, int fd, int *nulls)
+{
+	char *p;
+	Rune *r;
+	int l, m, n, nb, nr;
+	uint q1;
+
+	if(q0 > b->nc)
+		panic("internal error: bufload");
+	p = malloc((Maxblock+UTFmax+1)*sizeof p[0]);
+	if(p == nil)
+		panic("bufload: malloc failed");
+	r = runemalloc(Maxblock);
+	m = 0;
+	n = 1;
+	q1 = q0;
+	/*
+	 * At top of loop, may have m bytes left over from
+	 * last pass, possibly representing a partial rune.
+	 */
+	while(n > 0){
+		n = read(fd, p+m, Maxblock);
+		if(n < 0){
+			error(Ebufload);
+			break;
+		}
+		m += n;
+		p[m] = 0;
+		l = m;
+		if(n > 0)
+			l -= UTFmax;
+		cvttorunes(p, l, r, &nb, &nr, nulls);
+		memmove(p, p+nb, m-nb);
+		m -= nb;
+		bufinsert(b, q1, r, nr);
+		q1 += nr;
+	}
+	free(p);
+	free(r);
+	return q1-q0;
+}
+
+void
+bufread(Buffer *b, uint q0, Rune *s, uint n)
+{
+	uint m;
+
+	if(!(q0<=b->nc && q0+n<=b->nc))
+		panic("bufread: internal error");
+
+	while(n > 0){
+		setcache(b, q0);
+		m = min(n, b->cnc-(q0-b->cq));
+		runemove(s, b->c+(q0-b->cq), m);
+		q0 += m;
+		s += m;
+		n -= m;
+	}
+}
+
+void
+bufreset(Buffer *b)
+{
+	int i;
+
+	b->nc = 0;
+	b->cnc = 0;
+	b->cq = 0;
+	b->cdirty = 0;
+	b->cbi = 0;
+	/* delete backwards to avoid n² behavior */
+	for(i=b->nbl-1; --i>=0; )
+		delblock(b, i);
+}
+
+void
+bufclose(Buffer *b)
+{
+	bufreset(b);
+	free(b->c);
+	b->c = nil;
+	b->cnc = 0;
+	free(b->bl);
+	b->bl = nil;
+	b->nbl = 0;
+}
diff --git a/src/cmd/sam/cmd.c b/src/cmd/sam/cmd.c
new file mode 100644
index 0000000..8c152f9
--- /dev/null
+++ b/src/cmd/sam/cmd.c
@@ -0,0 +1,594 @@
+#include "sam.h"
+#include "parse.h"
+
+static char	linex[]="\n";
+static char	wordx[]=" \t\n";
+struct cmdtab cmdtab[]={
+/*	cmdc	text	regexp	addr	defcmd	defaddr	count	token	 fn	*/
+	'\n',	0,	0,	0,	0,	aDot,	0,	0,	nl_cmd,
+	'a',	1,	0,	0,	0,	aDot,	0,	0,	a_cmd,
+	'b',	0,	0,	0,	0,	aNo,	0,	linex,	b_cmd,
+	'B',	0,	0,	0,	0,	aNo,	0,	linex,	b_cmd,
+	'c',	1,	0,	0,	0,	aDot,	0,	0,	c_cmd,
+	'd',	0,	0,	0,	0,	aDot,	0,	0,	d_cmd,
+	'D',	0,	0,	0,	0,	aNo,	0,	linex,	D_cmd,
+	'e',	0,	0,	0,	0,	aNo,	0,	wordx,	e_cmd,
+	'f',	0,	0,	0,	0,	aNo,	0,	wordx,	f_cmd,
+	'g',	0,	1,	0,	'p',	aDot,	0,	0,	g_cmd,
+	'i',	1,	0,	0,	0,	aDot,	0,	0,	i_cmd,
+	'k',	0,	0,	0,	0,	aDot,	0,	0,	k_cmd,
+	'm',	0,	0,	1,	0,	aDot,	0,	0,	m_cmd,
+	'n',	0,	0,	0,	0,	aNo,	0,	0,	n_cmd,
+	'p',	0,	0,	0,	0,	aDot,	0,	0,	p_cmd,
+	'q',	0,	0,	0,	0,	aNo,	0,	0,	q_cmd,
+	'r',	0,	0,	0,	0,	aDot,	0,	wordx,	e_cmd,
+	's',	0,	1,	0,	0,	aDot,	1,	0,	s_cmd,
+	't',	0,	0,	1,	0,	aDot,	0,	0,	m_cmd,
+	'u',	0,	0,	0,	0,	aNo,	2,	0,	u_cmd,
+	'v',	0,	1,	0,	'p',	aDot,	0,	0,	g_cmd,
+	'w',	0,	0,	0,	0,	aAll,	0,	wordx,	w_cmd,
+	'x',	0,	1,	0,	'p',	aDot,	0,	0,	x_cmd,
+	'y',	0,	1,	0,	'p',	aDot,	0,	0,	x_cmd,
+	'X',	0,	1,	0,	'f',	aNo,	0,	0,	X_cmd,
+	'Y',	0,	1,	0,	'f',	aNo,	0,	0,	X_cmd,
+	'!',	0,	0,	0,	0,	aNo,	0,	linex,	plan9_cmd,
+	'>',	0,	0,	0,	0,	aDot,	0,	linex,	plan9_cmd,
+	'<',	0,	0,	0,	0,	aDot,	0,	linex,	plan9_cmd,
+	'|',	0,	0,	0,	0,	aDot,	0,	linex,	plan9_cmd,
+	'=',	0,	0,	0,	0,	aDot,	0,	linex,	eq_cmd,
+	'c'|0x100,0,	0,	0,	0,	aNo,	0,	wordx,	cd_cmd,
+	0,	0,	0,	0,	0,	0,	0,	0,
+};
+Cmd	*parsecmd(int);
+Addr	*compoundaddr(void);
+Addr	*simpleaddr(void);
+void	freecmd(void);
+void	okdelim(int);
+
+Rune	line[BLOCKSIZE];
+Rune	termline[BLOCKSIZE];
+Rune	*linep = line;
+Rune	*terminp = termline;
+Rune	*termoutp = termline;
+List	cmdlist;
+List	addrlist;
+List	relist;
+List	stringlist;
+int	eof;
+
+void
+resetcmd(void)
+{
+	linep = line;
+	*linep = 0;
+	terminp = termoutp = termline;
+	freecmd();
+}
+
+int
+inputc(void)
+{
+	int n, nbuf;
+	char buf[3];
+	Rune r;
+
+    Again:
+	nbuf = 0;
+	if(downloaded){
+		while(termoutp == terminp){
+			cmdupdate();
+			if(patset)
+				tellpat();
+			while(termlocked > 0){
+				outT0(Hunlock);
+				termlocked--;
+			}
+			if(rcv() == 0)
+				return -1;
+		}
+		r = *termoutp++;
+		if(termoutp == terminp)
+			terminp = termoutp = termline;
+	}else{
+   		do{
+			n = read(0, buf+nbuf, 1);
+			if(n <= 0)
+				return -1;
+			nbuf += n;
+		}while(!fullrune(buf, nbuf));
+		chartorune(&r, buf);
+	}
+	if(r == 0){
+		warn(Wnulls);
+		goto Again;
+	}
+	return r;
+}
+
+int
+inputline(void)
+{
+	int i, c;
+
+	linep = line;
+	i = 0;
+	do{
+		if((c = inputc())<=0)
+			return -1;
+		if(i == (sizeof line)/RUNESIZE-1)
+			error(Etoolong);
+	}while((line[i++]=c) != '\n');
+	line[i] = 0;
+	return 1;
+}
+
+int
+getch(void)
+{
+	if(eof)
+		return -1;
+	if(*linep==0 && inputline()<0){
+		eof = TRUE;
+		return -1;
+	}
+	return *linep++;
+}
+
+int
+nextc(void)
+{
+	if(*linep == 0)
+		return -1;
+	return *linep;
+}
+
+void
+ungetch(void)
+{
+	if(--linep < line)
+		panic("ungetch");
+}
+
+Posn
+getnum(int signok)
+{
+	Posn n=0;
+	int c, sign;
+
+	sign = 1;
+	if(signok>1 && nextc()=='-'){
+		sign = -1;
+		getch();
+	}
+	if((c=nextc())<'0' || '9'<c)	/* no number defaults to 1 */
+		return sign;
+	while('0'<=(c=getch()) && c<='9')
+		n = n*10 + (c-'0');
+	ungetch();
+	return sign*n;
+}
+
+int
+skipbl(void)
+{
+	int c;
+	do
+		c = getch();
+	while(c==' ' || c=='\t');
+	if(c >= 0)
+		ungetch();
+	return c;
+}
+
+void
+termcommand(void)
+{
+	Posn p;
+
+	for(p=cmdpt; p<cmd->_.nc; p++){
+		if(terminp >= &termline[BLOCKSIZE]){
+			cmdpt = cmd->_.nc;
+			error(Etoolong);
+		}
+		*terminp++ = filereadc(cmd, p);
+	}
+	cmdpt = cmd->_.nc;
+}
+
+void
+cmdloop(void)
+{
+	Cmd *cmdp;
+	File *ocurfile;
+	int loaded;
+
+	for(;;){
+		if(!downloaded && curfile && curfile->unread)
+			load(curfile);
+		if((cmdp = parsecmd(0))==0){
+			if(downloaded){
+				rescue();
+				exits("eof");
+			}
+			break;
+		}
+		ocurfile = curfile;
+		loaded = curfile && !curfile->unread;
+		if(cmdexec(curfile, cmdp) == 0)
+			break;
+		freecmd();
+		cmdupdate();
+		update();
+		if(downloaded && curfile &&
+		    (ocurfile!=curfile || (!loaded && !curfile->unread)))
+			outTs(Hcurrent, curfile->tag);
+			/* don't allow type ahead on files that aren't bound */
+		if(downloaded && curfile && curfile->rasp == 0)
+			terminp = termoutp;
+	}
+}
+
+Cmd *
+newcmd(void){
+	Cmd *p;
+
+	p = emalloc(sizeof(Cmd));
+	inslist(&cmdlist, cmdlist.nused, (long)p);
+	return p;
+}
+
+Addr*
+newaddr(void)
+{
+	Addr *p;
+
+	p = emalloc(sizeof(Addr));
+	inslist(&addrlist, addrlist.nused, (long)p);
+	return p;
+}
+
+String*
+newre(void)
+{
+	String *p;
+
+	p = emalloc(sizeof(String));
+	inslist(&relist, relist.nused, (long)p);
+	Strinit(p);
+	return p;
+}
+
+String*
+newstring(void)
+{
+	String *p;
+
+	p = emalloc(sizeof(String));
+	inslist(&stringlist, stringlist.nused, (long)p);
+	Strinit(p);
+	return p;
+}
+
+void
+freecmd(void)
+{
+	int i;
+
+	while(cmdlist.nused > 0)
+		free(cmdlist.ucharpptr[--cmdlist.nused]);
+	while(addrlist.nused > 0)
+		free(addrlist.ucharpptr[--addrlist.nused]);
+	while(relist.nused > 0){
+		i = --relist.nused;
+		Strclose(relist.stringpptr[i]);
+		free(relist.stringpptr[i]);
+	}
+	while(stringlist.nused>0){
+		i = --stringlist.nused;
+		Strclose(stringlist.stringpptr[i]);
+		free(stringlist.stringpptr[i]);
+	}
+}
+
+int
+lookup(int c)
+{
+	int i;
+
+	for(i=0; cmdtab[i].cmdc; i++)
+		if(cmdtab[i].cmdc == c)
+			return i;
+	return -1;
+}
+
+void
+okdelim(int c)
+{
+	if(c=='\\' || ('a'<=c && c<='z')
+	|| ('A'<=c && c<='Z') || ('0'<=c && c<='9'))
+		error_c(Edelim, c);
+}
+
+void
+atnl(void)
+{
+	skipbl();
+	if(getch() != '\n')
+		error(Enewline);
+}
+
+void
+getrhs(String *s, int delim, int cmd)
+{
+	int c;
+
+	while((c = getch())>0 && c!=delim && c!='\n'){
+		if(c == '\\'){
+			if((c=getch()) <= 0)
+				error(Ebadrhs);
+			if(c == '\n'){
+				ungetch();
+				c='\\';
+			}else if(c == 'n')
+				c='\n';
+			else if(c!=delim && (cmd=='s' || c!='\\'))	/* s does its own */
+				Straddc(s, '\\');
+		}
+		Straddc(s, c);
+	}
+	ungetch();	/* let client read whether delimeter, '\n' or whatever */
+}
+
+String *
+collecttoken(char *end)
+{
+	String *s = newstring();
+	int c;
+
+	while((c=nextc())==' ' || c=='\t')
+		Straddc(s, getch()); /* blanks significant for getname() */
+	while((c=getch())>0 && utfrune(end, c)==0)
+		Straddc(s, c);
+	Straddc(s, 0);
+	if(c != '\n')
+		atnl();
+	return s;
+}
+
+String *
+collecttext(void)
+{
+	String *s = newstring();
+	int begline, i, c, delim;
+
+	if(skipbl()=='\n'){
+		getch();
+		i = 0;
+		do{
+			begline = i;
+			while((c = getch())>0 && c!='\n')
+				i++, Straddc(s, c);
+			i++, Straddc(s, '\n');
+			if(c < 0)
+				goto Return;
+		}while(s->s[begline]!='.' || s->s[begline+1]!='\n');
+		Strdelete(s, s->n-2, s->n);
+	}else{
+		okdelim(delim = getch());
+		getrhs(s, delim, 'a');
+		if(nextc()==delim)
+			getch();
+		atnl();
+	}
+    Return:
+	Straddc(s, 0);		/* JUST FOR CMDPRINT() */
+	return s;
+}
+
+Cmd *
+parsecmd(int nest)
+{
+	int i, c;
+	struct cmdtab *ct;
+	Cmd *cp, *ncp;
+	Cmd cmd;
+
+	cmd.next = cmd.ccmd = 0;
+	cmd.re = 0;
+	cmd.flag = cmd.num = 0;
+	cmd.addr = compoundaddr();
+	if(skipbl() == -1)
+		return 0;
+	if((c=getch())==-1)
+		return 0;
+	cmd.cmdc = c;
+	if(cmd.cmdc=='c' && nextc()=='d'){	/* sleazy two-character case */
+		getch();		/* the 'd' */
+		cmd.cmdc='c'|0x100;
+	}
+	i = lookup(cmd.cmdc);
+	if(i >= 0){
+		if(cmd.cmdc == '\n')
+			goto Return;	/* let nl_cmd work it all out */
+		ct = &cmdtab[i];
+		if(ct->defaddr==aNo && cmd.addr)
+			error(Enoaddr);
+		if(ct->count)
+			cmd.num = getnum(ct->count);
+		if(ct->regexp){
+			/* x without pattern -> .*\n, indicated by cmd.re==0 */
+			/* X without pattern is all files */
+			if((ct->cmdc!='x' && ct->cmdc!='X') ||
+			   ((c = nextc())!=' ' && c!='\t' && c!='\n')){
+				skipbl();
+				if((c = getch())=='\n' || c<0)
+					error(Enopattern);
+				okdelim(c);
+				cmd.re = getregexp(c);
+				if(ct->cmdc == 's'){
+					cmd.ctext = newstring();
+					getrhs(cmd.ctext, c, 's');
+					if(nextc() == c){
+						getch();
+						if(nextc() == 'g')
+							cmd.flag = getch();
+					}
+			
+				}
+			}
+		}
+		if(ct->addr && (cmd.caddr=simpleaddr())==0)
+			error(Eaddress);
+		if(ct->defcmd){
+			if(skipbl() == '\n'){
+				getch();
+				cmd.ccmd = newcmd();
+				cmd.ccmd->cmdc = ct->defcmd;
+			}else if((cmd.ccmd = parsecmd(nest))==0)
+				panic("defcmd");
+		}else if(ct->text)
+			cmd.ctext = collecttext();
+		else if(ct->token)
+			cmd.ctext = collecttoken(ct->token);
+		else
+			atnl();
+	}else
+		switch(cmd.cmdc){
+		case '{':
+			cp = 0;
+			do{
+				if(skipbl()=='\n')
+					getch();
+				ncp = parsecmd(nest+1);
+				if(cp)
+					cp->next = ncp;
+				else
+					cmd.ccmd = ncp;
+			}while(cp = ncp);
+			break;
+		case '}':
+			atnl();
+			if(nest==0)
+				error(Enolbrace);
+			return 0;
+		default:
+			error_c(Eunk, cmd.cmdc);
+		}
+    Return:
+	cp = newcmd();
+	*cp = cmd;
+	return cp;
+}
+
+String*				/* BUGGERED */
+getregexp(int delim)
+{
+	String *r = newre();
+	int c;
+
+	for(Strzero(&genstr); ; Straddc(&genstr, c))
+		if((c = getch())=='\\'){
+			if(nextc()==delim)
+				c = getch();
+			else if(nextc()=='\\'){
+				Straddc(&genstr, c);
+				c = getch();
+			}
+		}else if(c==delim || c=='\n')
+			break;
+	if(c!=delim && c)
+		ungetch();
+	if(genstr.n > 0){
+		patset = TRUE;
+		Strduplstr(&lastpat, &genstr);
+		Straddc(&lastpat, '\0');
+	}
+	if(lastpat.n <= 1)
+		error(Epattern);
+	Strduplstr(r, &lastpat);
+	return r;
+}
+
+Addr *
+simpleaddr(void)
+{
+	Addr addr;
+	Addr *ap, *nap;
+
+	addr.next = 0;
+	addr.left = 0;
+	switch(skipbl()){
+	case '#':
+		addr.type = getch();
+		addr.num = getnum(1);
+		break;
+	case '0': case '1': case '2': case '3': case '4':
+	case '5': case '6': case '7': case '8': case '9': 
+		addr.num = getnum(1);
+		addr.type='l';
+		break;
+	case '/': case '?': case '"':
+		addr.are = getregexp(addr.type = getch());
+		break;
+	case '.':
+	case '$':
+	case '+':
+	case '-':
+	case '\'':
+		addr.type = getch();
+		break;
+	default:
+		return 0;
+	}
+	if(addr.next = simpleaddr())
+		switch(addr.next->type){
+		case '.':
+		case '$':
+		case '\'':
+			if(addr.type!='"')
+		case '"':
+				error(Eaddress);
+			break;
+		case 'l':
+		case '#':
+			if(addr.type=='"')
+				break;
+			/* fall through */
+		case '/':
+		case '?':
+			if(addr.type!='+' && addr.type!='-'){
+				/* insert the missing '+' */
+				nap = newaddr();
+				nap->type='+';
+				nap->next = addr.next;
+				addr.next = nap;
+			}
+			break;
+		case '+':
+		case '-':
+			break;
+		default:
+			panic("simpleaddr");
+		}
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
+
+Addr *
+compoundaddr(void)
+{
+	Addr addr;
+	Addr *ap, *next;
+
+	addr.left = simpleaddr();
+	if((addr.type = skipbl())!=',' && addr.type!=';')
+		return addr.left;
+	getch();
+	next = addr.next = compoundaddr();
+	if(next && (next->type==',' || next->type==';') && next->left==0)
+		error(Eaddress);
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
diff --git a/src/cmd/sam/disk.c b/src/cmd/sam/disk.c
new file mode 100644
index 0000000..83b2553
--- /dev/null
+++ b/src/cmd/sam/disk.c
@@ -0,0 +1,122 @@
+#include "sam.h"
+
+static	Block	*blist;
+
+#if 0
+static int
+tempdisk(void)
+{
+	char buf[128];
+	int i, fd;
+
+	snprint(buf, sizeof buf, "/tmp/X%d.%.4ssam", getpid(), getuser());
+	for(i='A'; i<='Z'; i++){
+		buf[5] = i;
+		if(access(buf, AEXIST) == 0)
+			continue;
+		fd = create(buf, ORDWR|ORCLOSE|OCEXEC, 0600);
+		if(fd >= 0)
+			return fd;
+	}
+	return -1;
+}
+#else
+extern int tempdisk(void);
+#endif
+
+Disk*
+diskinit()
+{
+	Disk *d;
+
+	d = emalloc(sizeof(Disk));
+	d->fd = tempdisk();
+	if(d->fd < 0){
+		fprint(2, "sam: can't create temp file: %r\n");
+		exits("diskinit");
+	}
+	return d;
+}
+
+static
+uint
+ntosize(uint n, uint *ip)
+{
+	uint size;
+
+	if(n > Maxblock)
+		panic("internal error: ntosize");
+	size = n;
+	if(size & (Blockincr-1))
+		size += Blockincr - (size & (Blockincr-1));
+	/* last bucket holds blocks of exactly Maxblock */
+	if(ip)
+		*ip = size/Blockincr;
+	return size * sizeof(Rune);
+}
+
+Block*
+disknewblock(Disk *d, uint n)
+{
+	uint i, j, size;
+	Block *b;
+
+	size = ntosize(n, &i);
+	b = d->free[i];
+	if(b)
+		d->free[i] = b->_.next;
+	else{
+		/* allocate in chunks to reduce malloc overhead */
+		if(blist == nil){
+			blist = emalloc(100*sizeof(Block));
+			for(j=0; j<100-1; j++)
+				blist[j]._.next = &blist[j+1];
+		}
+		b = blist;
+		blist = b->_.next;
+		b->addr = d->addr;
+		d->addr += size;
+	}
+	b->_.n = n;
+	return b;
+}
+
+void
+diskrelease(Disk *d, Block *b)
+{
+	uint i;
+
+	ntosize(b->_.n, &i);
+	b->_.next = d->free[i];
+	d->free[i] = b;
+}
+
+void
+diskwrite(Disk *d, Block **bp, Rune *r, uint n)
+{
+	int size, nsize;
+	Block *b;
+
+	b = *bp;
+	size = ntosize(b->_.n, nil);
+	nsize = ntosize(n, nil);
+	if(size != nsize){
+		diskrelease(d, b);
+		b = disknewblock(d, n);
+		*bp = b;
+	}
+	if(pwrite(d->fd, r, n*sizeof(Rune), b->addr) != n*sizeof(Rune))
+		panic("write error to temp file");
+	b->_.n = n;
+}
+
+void
+diskread(Disk *d, Block *b, Rune *r, uint n)
+{
+	if(n > b->_.n)
+		panic("internal error: diskread");
+
+	ntosize(b->_.n, nil);	/* called only for sanity check on Maxblock */
+	if(pread(d->fd, r, n*sizeof(Rune), b->addr) != n*sizeof(Rune))
+		panic("read error from temp file");
+}
diff --git a/src/cmd/sam/error.c b/src/cmd/sam/error.c
new file mode 100644
index 0000000..d19b962
--- /dev/null
+++ b/src/cmd/sam/error.c
@@ -0,0 +1,144 @@
+#include "sam.h"
+
+static char *emsg[]={
+	/* error_s */
+	"can't open",
+	"can't create",
+	"not in menu:",
+	"changes to",
+	"I/O error:",
+	"can't write while changing:",
+	/* error_c */
+	"unknown command",
+	"no operand for",
+	"bad delimiter",
+	/* error */
+	"can't fork",
+	"interrupt",
+	"address",
+	"search",
+	"pattern",
+	"newline expected",
+	"blank expected",
+	"pattern expected",
+	"can't nest X or Y",
+	"unmatched `}'",
+	"command takes no address",
+	"addresses overlap",
+	"substitution",
+	"& match too long",
+	"bad \\ in rhs",
+	"address range",
+	"changes not in sequence",
+	"addresses out of order",
+	"no file name",
+	"unmatched `('",
+	"unmatched `)'",
+	"malformed `[]'",
+	"malformed regexp",
+	"reg. exp. list overflow",
+	"plan 9 command",
+	"can't pipe",
+	"no current file",
+	"string too long",
+	"changed files",
+	"empty string",
+	"file search",
+	"non-unique match for \"\"",
+	"tag match too long",
+	"too many subexpressions",
+	"temporary file too large",
+	"file is append-only",
+	"no destination for plumb message",
+	"internal read error in buffer load",
+};
+static char *wmsg[]={
+	/* warn_s */
+	"duplicate file name",
+	"no such file",
+	"write might change good version of",
+	/* warn_S */
+	"files might be aliased",
+	/* warn */
+	"null characters elided",
+	"can't run pwd",
+	"last char not newline",
+	"exit status not 0",
+};
+
+void
+error(Err s)
+{
+	char buf[512];
+
+	sprint(buf, "?%s", emsg[s]);
+	hiccough(buf);
+}
+
+void
+error_s(Err s, char *a)
+{
+	char buf[512];
+
+	sprint(buf, "?%s \"%s\"", emsg[s], a);
+	hiccough(buf);
+}
+
+void
+error_r(Err s, char *a)
+{
+	char buf[512];
+
+	sprint(buf, "?%s \"%s\": %r", emsg[s], a);
+	hiccough(buf);
+}
+
+void
+error_c(Err s, int c)
+{
+	char buf[512];
+
+	sprint(buf, "?%s `%C'", emsg[s], c);
+	hiccough(buf);
+}
+
+void
+warn(Warn s)
+{
+	dprint("?warning: %s\n", wmsg[s]);
+}
+
+void
+warn_S(Warn s, String *a)
+{
+	print_s(wmsg[s], a);
+}
+
+void
+warn_SS(Warn s, String *a, String *b)
+{
+	print_ss(wmsg[s], a, b);
+}
+
+void
+warn_s(Warn s, char *a)
+{
+	dprint("?warning: %s `%s'\n", wmsg[s], a);
+}
+
+void
+termwrite(char *s)
+{
+	String *p;
+
+	if(downloaded){
+		p = tmpcstr(s);
+		if(cmd)
+			loginsert(cmd, cmdpt, p->s, p->n);
+		else
+			Strinsert(&cmdstr, p, cmdstr.n);
+		cmdptadv += p->n;
+		free(p);
+	}else
+		Write(2, s, strlen(s));
+}
diff --git a/src/cmd/sam/errors.h b/src/cmd/sam/errors.h
new file mode 100644
index 0000000..7bf46ea
--- /dev/null
+++ b/src/cmd/sam/errors.h
@@ -0,0 +1,65 @@
+typedef enum Err{
+	/* error_s */
+	Eopen,
+	Ecreate,
+	Emenu,
+	Emodified,
+	Eio,
+	Ewseq,
+	/* error_c */
+	Eunk,
+	Emissop,
+	Edelim,
+	/* error */
+	Efork,
+	Eintr,
+	Eaddress,
+	Esearch,
+	Epattern,
+	Enewline,
+	Eblank,
+	Enopattern,
+	EnestXY,
+	Enolbrace,
+	Enoaddr,
+	Eoverlap,
+	Enosub,
+	Elongrhs,
+	Ebadrhs,
+	Erange,
+	Esequence,
+	Eorder,
+	Enoname,
+	Eleftpar,
+	Erightpar,
+	Ebadclass,
+	Ebadregexp,
+	Eoverflow,
+	Enocmd,
+	Epipe,
+	Enofile,
+	Etoolong,
+	Echanges,
+	Eempty,
+	Efsearch,
+	Emanyfiles,
+	Elongtag,
+	Esubexp,
+	Etmpovfl,
+	Eappend,
+	Ecantplumb,
+	Ebufload,
+}Err;
+typedef enum Warn{
+	/* warn_s */
+	Wdupname,
+	Wfile,
+	Wdate,
+	/* warn_ss */
+	Wdupfile,
+	/* warn */
+	Wnulls,
+	Wpwd,
+	Wnotnewline,
+	Wbadstatus,
+}Warn;
diff --git a/src/cmd/sam/file.c b/src/cmd/sam/file.c
new file mode 100644
index 0000000..0428379
--- /dev/null
+++ b/src/cmd/sam/file.c
@@ -0,0 +1,631 @@
+#include "sam.h"
+
+/*
+ * Structure of Undo list:
+ * 	The Undo structure follows any associated data, so the list
+ *	can be read backwards: read the structure, then read whatever
+ *	data is associated (insert string, file name) and precedes it.
+ *	The structure includes the previous value of the modify bit
+ *	and a sequence number; successive Undo structures with the
+ *	same sequence number represent simultaneous changes.
+ */
+
+typedef struct Undo Undo;
+typedef struct Merge Merge;
+
+struct Undo
+{
+	short	type;		/* Delete, Insert, Filename, Dot, Mark */
+	short	mod;		/* modify bit */
+	uint	seq;		/* sequence number */
+	uint	p0;		/* location of change (unused in f) */
+	uint	n;		/* # runes in string or file name */
+};
+
+struct Merge
+{
+	File	*f;
+	uint	seq;		/* of logged change */
+	uint	p0;		/* location of change (unused in f) */
+	uint	n;		/* # runes to delete */
+	uint	nbuf;		/* # runes to insert */
+	Rune	buf[RBUFSIZE];
+};
+
+enum
+{
+	Maxmerge = 50,
+	Undosize = sizeof(Undo)/sizeof(Rune),
+};
+
+static Merge	merge;
+
+File*
+fileopen(void)
+{
+	File *f;
+
+	f = emalloc(sizeof(File));
+	f->dot.f = f;
+	f->ndot.f = f;
+	f->seq = 0;
+	f->mod = FALSE;
+	f->unread = TRUE;
+	Strinit0(&f->name);
+	return f;
+}
+
+int
+fileisdirty(File *f)
+{
+	return f->seq != f->cleanseq;
+}
+
+static void
+wrinsert(Buffer *delta, int seq, int mod, uint p0, Rune *s, uint ns)
+{
+	Undo u;
+
+	u.type = Insert;
+	u.mod = mod;
+	u.seq = seq;
+	u.p0 = p0;
+	u.n = ns;
+	bufinsert(delta, delta->nc, s, ns);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+static void
+wrdelete(Buffer *delta, int seq, int mod, uint p0, uint p1)
+{
+	Undo u;
+
+	u.type = Delete;
+	u.mod = mod;
+	u.seq = seq;
+	u.p0 = p0;
+	u.n = p1 - p0;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+void
+flushmerge(void)
+{
+	File *f;
+
+	f = merge.f;
+	if(f == nil)
+		return;
+	if(merge.seq != f->seq)
+		panic("flushmerge seq mismatch");
+	if(merge.n != 0)
+		wrdelete(&f->epsilon, f->seq, TRUE, merge.p0, merge.p0+merge.n);
+	if(merge.nbuf != 0)
+		wrinsert(&f->epsilon, f->seq, TRUE, merge.p0+merge.n, merge.buf, merge.nbuf);
+	merge.f = nil;
+	merge.n = 0;
+	merge.nbuf = 0;
+}
+
+void
+mergeextend(File *f, uint p0)
+{
+	uint mp0n;
+
+	mp0n = merge.p0+merge.n;
+	if(mp0n != p0){
+		bufread(f, mp0n, merge.buf+merge.nbuf, p0-mp0n);
+		merge.nbuf += p0-mp0n;
+		merge.n = p0-merge.p0;
+	}
+}
+
+/*
+ * like fileundelete, but get the data from arguments
+ */
+void
+loginsert(File *f, uint p0, Rune *s, uint ns)
+{
+	if(f->rescuing)
+		return;
+	if(ns == 0)
+		return;
+	if(ns<0 || ns>STRSIZE)
+		panic("loginsert");
+	if(f->seq < seq)
+		filemark(f);
+	if(p0 < f->hiposn)
+		error(Esequence);
+
+	if(merge.f != f
+	|| p0-(merge.p0+merge.n)>Maxmerge			/* too far */
+	|| merge.nbuf+((p0+ns)-(merge.p0+merge.n))>RBUFSIZE)	/* too long */
+		flushmerge();
+
+	if(ns>=RBUFSIZE){
+		if(!(merge.n == 0 && merge.nbuf == 0 && merge.f == nil))
+			panic("loginsert bad merge state");
+		wrinsert(&f->epsilon, f->seq, TRUE, p0, s, ns);
+	}else{
+		if(merge.f != f){
+			merge.f = f;
+			merge.p0 = p0;
+			merge.seq = f->seq;
+		}
+		mergeextend(f, p0);
+
+		/* append string to merge */
+		runemove(merge.buf+merge.nbuf, s, ns);
+		merge.nbuf += ns;
+	}
+
+	f->hiposn = p0;
+	if(!f->unread && !f->mod)
+		state(f, Dirty);
+}
+
+void
+logdelete(File *f, uint p0, uint p1)
+{
+	if(f->rescuing)
+		return;
+	if(p0 == p1)
+		return;
+	if(f->seq < seq)
+		filemark(f);
+	if(p0 < f->hiposn)
+		error(Esequence);
+
+	if(merge.f != f
+	|| p0-(merge.p0+merge.n)>Maxmerge			/* too far */
+	|| merge.nbuf+(p0-(merge.p0+merge.n))>RBUFSIZE){	/* too long */
+		flushmerge();
+		merge.f = f;
+		merge.p0 = p0;
+		merge.seq = f->seq;
+	}
+
+	mergeextend(f, p0);
+
+	/* add to deletion */
+	merge.n = p1-merge.p0;
+
+	f->hiposn = p1;
+	if(!f->unread && !f->mod)
+		state(f, Dirty);
+}
+
+/*
+ * like fileunsetname, but get the data from arguments
+ */
+void
+logsetname(File *f, String *s)
+{
+	Undo u;
+	Buffer *delta;
+
+	if(f->rescuing)
+		return;
+
+	if(f->unread){	/* This is setting initial file name */
+		filesetname(f, s);
+		return;
+	}
+
+	if(f->seq < seq)
+		filemark(f);
+
+	/* undo a file name change by restoring old name */
+	delta = &f->epsilon;
+	u.type = Filename;
+	u.mod = TRUE;
+	u.seq = f->seq;
+	u.p0 = 0;	/* unused */
+	u.n = s->n;
+	if(s->n)
+		bufinsert(delta, delta->nc, s->s, s->n);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+	if(!f->unread && !f->mod)
+		state(f, Dirty);
+}
+
+#ifdef NOTEXT
+File*
+fileaddtext(File *f, Text *t)
+{
+	if(f == nil){
+		f = emalloc(sizeof(File));
+		f->unread = TRUE;
+	}
+	f->text = realloc(f->text, (f->ntext+1)*sizeof(Text*));
+	f->text[f->ntext++] = t;
+	f->curtext = t;
+	return f;
+}
+
+void
+filedeltext(File *f, Text *t)
+{
+	int i;
+
+	for(i=0; i<f->ntext; i++)
+		if(f->text[i] == t)
+			goto Found;
+	panic("can't find text in filedeltext");
+
+    Found:
+	f->ntext--;
+	if(f->ntext == 0){
+		fileclose(f);
+		return;
+	}
+	memmove(f->text+i, f->text+i+1, (f->ntext-i)*sizeof(Text*));
+	if(f->curtext == t)
+		f->curtext = f->text[0];
+}
+#endif
+
+void
+fileinsert(File *f, uint p0, Rune *s, uint ns)
+{
+	if(p0 > f->_.nc)
+		panic("internal error: fileinsert");
+	if(f->seq > 0)
+		fileuninsert(f, &f->delta, p0, ns);
+	bufinsert(f, p0, s, ns);
+	if(ns)
+		f->mod = TRUE;
+}
+
+void
+fileuninsert(File *f, Buffer *delta, uint p0, uint ns)
+{
+	Undo u;
+
+	/* undo an insertion by deleting */
+	u.type = Delete;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = p0;
+	u.n = ns;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+void
+filedelete(File *f, uint p0, uint p1)
+{
+	if(!(p0<=p1 && p0<=f->_.nc && p1<=f->_.nc))
+		panic("internal error: filedelete");
+	if(f->seq > 0)
+		fileundelete(f, &f->delta, p0, p1);
+	bufdelete(f, p0, p1);
+	if(p1 > p0)
+		f->mod = TRUE;
+}
+
+void
+fileundelete(File *f, Buffer *delta, uint p0, uint p1)
+{
+	Undo u;
+	Rune *buf;
+	uint i, n;
+
+	/* undo a deletion by inserting */
+	u.type = Insert;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = p0;
+	u.n = p1-p0;
+	buf = fbufalloc();
+	for(i=p0; i<p1; i+=n){
+		n = p1 - i;
+		if(n > RBUFSIZE)
+			n = RBUFSIZE;
+		bufread(f, i, buf, n);
+		bufinsert(delta, delta->nc, buf, n);
+	}
+	fbuffree(buf);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+
+}
+
+int
+filereadc(File *f, uint q)
+{
+	Rune r;
+
+	if(q >= f->_.nc)
+		return -1;
+	bufread(f, q, &r, 1);
+	return r;
+}
+
+void
+filesetname(File *f, String *s)
+{
+	if(!f->unread)	/* This is setting initial file name */
+		fileunsetname(f, &f->delta);
+	Strduplstr(&f->name, s);
+	sortname(f);
+	f->unread = TRUE;
+}
+
+void
+fileunsetname(File *f, Buffer *delta)
+{
+	String s;
+	Undo u;
+
+	/* undo a file name change by restoring old name */
+	u.type = Filename;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = 0;	/* unused */
+	Strinit(&s);
+	Strduplstr(&s, &f->name);
+	fullname(&s);
+	u.n = s.n;
+	if(s.n)
+		bufinsert(delta, delta->nc, s.s, s.n);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+	Strclose(&s);
+}
+
+void
+fileunsetdot(File *f, Buffer *delta, Range dot)
+{
+	Undo u;
+
+	u.type = Dot;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = dot.p1;
+	u.n = dot.p2 - dot.p1;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+void
+fileunsetmark(File *f, Buffer *delta, Range mark)
+{
+	Undo u;
+
+	u.type = Mark;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = mark.p1;
+	u.n = mark.p2 - mark.p1;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+uint
+fileload(File *f, uint p0, int fd, int *nulls)
+{
+	if(f->seq > 0)
+		panic("undo in file.load unimplemented");
+	return bufload(f, p0, fd, nulls);
+}
+
+int
+fileupdate(File *f, int notrans, int toterm)
+{
+	uint p1, p2;
+	int mod;
+
+	if(f->rescuing)
+		return FALSE;
+
+	flushmerge();
+
+	/*
+	 * fix the modification bit
+	 * subtle point: don't save it away in the log.
+	 *
+	 * if another change is made, the correct f->mod
+	 * state is saved  in the undo log by filemark
+	 * when setting the dot and mark.
+	 *
+	 * if the change is undone, the correct state is
+	 * saved from f in the fileun... routines.
+	 */
+	mod = f->mod;
+	f->mod = f->prevmod;
+	if(f == cmd)
+		notrans = TRUE;
+	else{
+		fileunsetdot(f, &f->delta, f->prevdot);
+		fileunsetmark(f, &f->delta, f->prevmark);
+	}
+	f->dot = f->ndot;
+	fileundo(f, FALSE, !notrans, &p1, &p2, toterm);
+	f->mod = mod;
+
+	if(f->delta.nc == 0)
+		f->seq = 0;
+
+	if(f == cmd)
+		return FALSE;
+
+	if(f->mod){
+		f->closeok = 0;
+		quitok = 0;
+	}else
+		f->closeok = 1;
+	return TRUE;
+}
+
+long
+prevseq(Buffer *b)
+{
+	Undo u;
+	uint up;
+
+	up = b->nc;
+	if(up == 0)
+		return 0;
+	up -= Undosize;
+	bufread(b, up, (Rune*)&u, Undosize);
+	return u.seq;
+}
+
+long
+undoseq(File *f, int isundo)
+{
+	if(isundo)
+		return f->seq;
+
+	return prevseq(&f->epsilon);
+}
+
+void
+fileundo(File *f, int isundo, int canredo, uint *q0p, uint *q1p, int flag)
+{
+	Undo u;
+	Rune *buf;
+	uint i, n, up;
+	uint stop;
+	Buffer *delta, *epsilon;
+
+	if(isundo){
+		/* undo; reverse delta onto epsilon, seq decreases */
+		delta = &f->delta;
+		epsilon = &f->epsilon;
+		stop = f->seq;
+	}else{
+		/* redo; reverse epsilon onto delta, seq increases */
+		delta = &f->epsilon;
+		epsilon = &f->delta;
+		stop = 0;	/* don't know yet */
+	}
+
+	raspstart(f);
+	while(delta->nc > 0){
+		up = delta->nc-Undosize;
+		bufread(delta, up, (Rune*)&u, Undosize);
+		if(isundo){
+			if(u.seq < stop){
+				f->seq = u.seq;
+				raspdone(f, flag);
+				return;
+			}
+		}else{
+			if(stop == 0)
+				stop = u.seq;
+			if(u.seq > stop){
+				raspdone(f, flag);
+				return;
+			}
+		}
+		switch(u.type){
+		default:
+			panic("undo unknown u.type");
+			break;
+
+		case Delete:
+			f->seq = u.seq;
+			if(canredo)
+				fileundelete(f, epsilon, u.p0, u.p0+u.n);
+			f->mod = u.mod;
+			bufdelete(f, u.p0, u.p0+u.n);
+			raspdelete(f, u.p0, u.p0+u.n, flag);
+			*q0p = u.p0;
+			*q1p = u.p0;
+			break;
+
+		case Insert:
+			f->seq = u.seq;
+			if(canredo)
+				fileuninsert(f, epsilon, u.p0, u.n);
+			f->mod = u.mod;
+			up -= u.n;
+			buf = fbufalloc();
+			for(i=0; i<u.n; i+=n){
+				n = u.n - i;
+				if(n > RBUFSIZE)
+					n = RBUFSIZE;
+				bufread(delta, up+i, buf, n);
+				bufinsert(f, u.p0+i, buf, n);
+				raspinsert(f, u.p0+i, buf, n, flag);
+			}
+			fbuffree(buf);
+			*q0p = u.p0;
+			*q1p = u.p0+u.n;
+			break;
+
+		case Filename:
+			f->seq = u.seq;
+			if(canredo)
+				fileunsetname(f, epsilon);
+			f->mod = u.mod;
+			up -= u.n;
+
+			Strinsure(&f->name, u.n+1);
+			bufread(delta, up, f->name.s, u.n);
+			f->name.s[u.n] = 0;
+			f->name.n = u.n;
+			fixname(&f->name);
+			sortname(f);
+			break;
+		case Dot:
+			f->seq = u.seq;
+			if(canredo)
+				fileunsetdot(f, epsilon, f->dot.r);
+			f->mod = u.mod;
+			f->dot.r.p1 = u.p0;
+			f->dot.r.p2 = u.p0 + u.n;
+			break;
+		case Mark:
+			f->seq = u.seq;
+			if(canredo)
+				fileunsetmark(f, epsilon, f->mark);
+			f->mod = u.mod;
+			f->mark.p1 = u.p0;
+			f->mark.p2 = u.p0 + u.n;
+			break;
+		}
+		bufdelete(delta, up, delta->nc);
+	}
+	if(isundo)
+		f->seq = 0;
+	raspdone(f, flag);
+}
+
+void
+filereset(File *f)
+{
+	bufreset(&f->delta);
+	bufreset(&f->epsilon);
+	f->seq = 0;
+}
+
+void
+fileclose(File *f)
+{
+	Strclose(&f->name);
+	bufclose(f);
+	bufclose(&f->delta);
+	bufclose(&f->epsilon);
+	if(f->rasp)
+		listfree(f->rasp);
+	free(f);
+}
+
+void
+filemark(File *f)
+{
+
+	if(f->unread)
+		return;
+	if(f->epsilon.nc)
+		bufdelete(&f->epsilon, 0, f->epsilon.nc);
+
+	if(f != cmd){
+		f->prevdot = f->dot.r;
+		f->prevmark = f->mark;
+		f->prevseq = f->seq;
+		f->prevmod = f->mod;
+	}
+
+	f->ndot = f->dot;
+	f->seq = seq;
+	f->hiposn = 0;
+}
diff --git a/src/cmd/sam/io.c b/src/cmd/sam/io.c
new file mode 100644
index 0000000..236090a
--- /dev/null
+++ b/src/cmd/sam/io.c
@@ -0,0 +1,262 @@
+#include "sam.h"
+
+#define	NSYSFILE	3
+#define	NOFILE		128
+
+void
+checkqid(File *f)
+{
+	int i, w;
+	File *g;
+
+	w = whichmenu(f);
+	for(i=1; i<file.nused; i++){
+		g = file.filepptr[i];
+		if(w == i)
+			continue;
+		if(f->dev==g->dev && f->qidpath==g->qidpath)
+			warn_SS(Wdupfile, &f->name, &g->name);
+	}
+}
+
+void
+writef(File *f)
+{
+	Posn n;
+	char *name;
+	int i, samename, newfile;
+	ulong dev;
+	uvlong qid;
+	long mtime, appendonly, length;
+
+	newfile = 0;
+	samename = Strcmp(&genstr, &f->name) == 0;
+	name = Strtoc(&f->name);
+	i = statfile(name, &dev, &qid, &mtime, 0, 0);
+	if(i == -1)
+		newfile++;
+	else if(samename &&
+	        (f->dev!=dev || f->qidpath!=qid || f->mtime<mtime)){
+		f->dev = dev;
+		f->qidpath = qid;
+		f->mtime = mtime;
+		warn_S(Wdate, &genstr);
+		return;
+	}
+	if(genc)
+		free(genc);
+	genc = Strtoc(&genstr);
+	if((io=create(genc, 1, 0666L)) < 0)
+		error_r(Ecreate, genc);
+	dprint("%s: ", genc);
+	if(statfd(io, 0, 0, 0, &length, &appendonly) > 0 && appendonly && length>0)
+		error(Eappend);
+	n = writeio(f);
+	if(f->name.s[0]==0 || samename){
+		if(addr.r.p1==0 && addr.r.p2==f->_.nc)
+			f->cleanseq = f->seq;
+		state(f, f->cleanseq==f->seq? Clean : Dirty);
+	}
+	if(newfile)
+		dprint("(new file) ");
+	if(addr.r.p2>0 && filereadc(f, addr.r.p2-1)!='\n')
+		warn(Wnotnewline);
+	closeio(n);
+	if(f->name.s[0]==0 || samename){
+		if(statfile(name, &dev, &qid, &mtime, 0, 0) > 0){
+			f->dev = dev;
+			f->qidpath = qid;
+			f->mtime = mtime;
+			checkqid(f);
+		}
+	}
+}
+
+Posn
+readio(File *f, int *nulls, int setdate, int toterm)
+{
+	int n, b, w;
+	Rune *r;
+	Posn nt;
+	Posn p = addr.r.p2;
+	ulong dev;
+	uvlong qid;
+	long mtime;
+	char buf[BLOCKSIZE+1], *s;
+
+	*nulls = FALSE;
+	b = 0;
+	if(f->unread){
+		nt = bufload(f, 0, io, nulls);
+		if(toterm)
+			raspload(f);
+	}else
+		for(nt = 0; (n = read(io, buf+b, BLOCKSIZE-b))>0; nt+=(r-genbuf)){
+			n += b;
+			b = 0;
+			r = genbuf;
+			s = buf;
+			while(n > 0){
+				if((*r = *(uchar*)s) < Runeself){
+					if(*r)
+						r++;
+					else
+						*nulls = TRUE;
+					--n;
+					s++;
+					continue;
+				}
+				if(fullrune(s, n)){
+					w = chartorune(r, s);
+					if(*r)
+						r++;
+					else
+						*nulls = TRUE;
+					n -= w;
+					s += w;
+					continue;
+				}
+				b = n;
+				memmove(buf, s, b);
+				break;
+			}
+			loginsert(f, p, genbuf, r-genbuf);
+		}
+	if(b)
+		*nulls = TRUE;
+	if(*nulls)
+		warn(Wnulls);
+	if(setdate){
+		if(statfd(io, &dev, &qid, &mtime, 0, 0) > 0){
+			f->dev = dev;
+			f->qidpath = qid;
+			f->mtime = mtime;
+			checkqid(f);
+		}
+	}
+	return nt;
+}
+
+Posn
+writeio(File *f)
+{
+	int m, n;
+	Posn p = addr.r.p1;
+	char *c;
+
+	while(p < addr.r.p2){
+		if(addr.r.p2-p>BLOCKSIZE)
+			n = BLOCKSIZE;
+		else
+			n = addr.r.p2-p;
+		bufread(f, p, genbuf, n);
+		c = Strtoc(tmprstr(genbuf, n));
+		m = strlen(c);
+		if(Write(io, c, m) != m){
+			free(c);
+			if(p > 0)
+				p += n;
+			break;
+		}
+		free(c);
+		p += n;
+	}
+	return p-addr.r.p1;
+}
+void
+closeio(Posn p)
+{
+	close(io);
+	io = 0;
+	if(p >= 0)
+		dprint("#%lud\n", p);
+}
+
+int	remotefd0 = 0;
+int	remotefd1 = 1;
+
+void
+bootterm(char *machine, char **argv, char **end)
+{
+	int ph2t[2], pt2h[2];
+
+	if(machine){
+		dup(remotefd0, 0);
+		dup(remotefd1, 1);
+		close(remotefd0);
+		close(remotefd1);
+		argv[0] = "samterm";
+		*end = 0;
+		exec(samterm, argv);
+		fprint(2, "can't exec: ");
+		perror(samterm);
+		_exits("damn");
+	}
+	if(pipe(ph2t)==-1 || pipe(pt2h)==-1)
+		panic("pipe");
+	switch(fork()){
+	case 0:
+		dup(ph2t[0], 0);
+		dup(pt2h[1], 1);
+		close(ph2t[0]);
+		close(ph2t[1]);
+		close(pt2h[0]);
+		close(pt2h[1]);
+		argv[0] = "samterm";
+		*end = 0;
+		exec(samterm, argv);
+		fprint(2, "can't exec: ");
+		perror(samterm);
+		_exits("damn");
+	case -1:
+		panic("can't fork samterm");
+	}
+	dup(pt2h[0], 0);
+	dup(ph2t[1], 1);
+	close(ph2t[0]);
+	close(ph2t[1]);
+	close(pt2h[0]);
+	close(pt2h[1]);
+}
+
+void
+connectto(char *machine)
+{
+	int p1[2], p2[2];
+
+	if(pipe(p1)<0 || pipe(p2)<0){
+		dprint("can't pipe\n");
+		exits("pipe");
+	}
+	remotefd0 = p1[0];
+	remotefd1 = p2[1];
+	switch(fork()){
+	case 0:
+		dup(p2[0], 0);
+		dup(p1[1], 1);
+		close(p1[0]);
+		close(p1[1]);
+		close(p2[0]);
+		close(p2[1]);
+		execl(RXPATH, RX, machine, rsamname, "-R", (char*)0);
+		dprint("can't exec %s\n", RXPATH);
+		exits("exec");
+
+	case -1:
+		dprint("can't fork\n");
+		exits("fork");
+	}
+	close(p1[1]);
+	close(p2[0]);
+}
+
+void
+startup(char *machine, int Rflag, char **argv, char **end)
+{
+	if(machine)
+		connectto(machine);
+	if(!Rflag)
+		bootterm(machine, argv, end);
+	downloaded = 1;
+	outTs(Hversion, VERSION);
+}
diff --git a/src/cmd/sam/list.c b/src/cmd/sam/list.c
new file mode 100644
index 0000000..a810542
--- /dev/null
+++ b/src/cmd/sam/list.c
@@ -0,0 +1,47 @@
+#include "sam.h"
+
+/*
+ * Check that list has room for one more element.
+ */
+void
+growlist(List *l)
+{
+	if(l->listptr==0 || l->nalloc==0){
+		l->nalloc = INCR;
+		l->listptr = emalloc(INCR*sizeof(long));
+		l->nused = 0;
+	}else if(l->nused == l->nalloc){
+		l->listptr = erealloc(l->listptr, (l->nalloc+INCR)*sizeof(long));
+		memset((void*)(l->longptr+l->nalloc), 0, INCR*sizeof(long));
+		l->nalloc += INCR;
+	}
+}
+
+/*
+ * Remove the ith element from the list
+ */
+void
+dellist(List *l, int i)
+{
+	memmove(&l->longptr[i], &l->longptr[i+1], (l->nused-(i+1))*sizeof(long));
+	l->nused--;
+}
+
+/*
+ * Add a new element, whose position is i, to the list
+ */
+void
+inslist(List *l, int i, long val)
+{
+	growlist(l);
+	memmove(&l->longptr[i+1], &l->longptr[i], (l->nused-i)*sizeof(long));
+	l->longptr[i] = val;
+	l->nused++;
+}
+
+void
+listfree(List *l)
+{
+	free(l->listptr);
+	free(l);
+}
diff --git a/src/cmd/sam/mesg.c b/src/cmd/sam/mesg.c
new file mode 100644
index 0000000..189c11a
--- /dev/null
+++ b/src/cmd/sam/mesg.c
@@ -0,0 +1,821 @@
+#include "sam.h"
+
+Header	h;
+uchar	indata[DATASIZE];
+uchar	outdata[2*DATASIZE+3];	/* room for overflow message */
+uchar	*inp;
+uchar	*outp;
+uchar	*outmsg = outdata;
+Posn	cmdpt;
+Posn	cmdptadv;
+Buffer	snarfbuf;
+int	waitack;
+int	noflush;
+int	tversion;
+
+long	inlong(void);
+long	invlong(void);
+int	inshort(void);
+int	inmesg(Tmesg);
+void	setgenstr(File*, Posn, Posn);
+
+#ifdef DEBUG
+char *hname[] = {
+	[Hversion]	"Hversion",
+	[Hbindname]	"Hbindname",
+	[Hcurrent]	"Hcurrent",
+	[Hnewname]	"Hnewname",
+	[Hmovname]	"Hmovname",
+	[Hgrow]		"Hgrow",
+	[Hcheck0]	"Hcheck0",
+	[Hcheck]	"Hcheck",
+	[Hunlock]	"Hunlock",
+	[Hdata]		"Hdata",
+	[Horigin]	"Horigin",
+	[Hunlockfile]	"Hunlockfile",
+	[Hsetdot]	"Hsetdot",
+	[Hgrowdata]	"Hgrowdata",
+	[Hmoveto]	"Hmoveto",
+	[Hclean]	"Hclean",
+	[Hdirty]	"Hdirty",
+	[Hcut]		"Hcut",
+	[Hsetpat]	"Hsetpat",
+	[Hdelname]	"Hdelname",
+	[Hclose]	"Hclose",
+	[Hsetsnarf]	"Hsetsnarf",
+	[Hsnarflen]	"Hsnarflen",
+	[Hack]		"Hack",
+	[Hexit]		"Hexit",
+	[Hplumb]		"Hplumb",
+};
+
+char *tname[] = {
+	[Tversion]	"Tversion",
+	[Tstartcmdfile]	"Tstartcmdfile",
+	[Tcheck]	"Tcheck",
+	[Trequest]	"Trequest",
+	[Torigin]	"Torigin",
+	[Tstartfile]	"Tstartfile",
+	[Tworkfile]	"Tworkfile",
+	[Ttype]		"Ttype",
+	[Tcut]		"Tcut",
+	[Tpaste]	"Tpaste",
+	[Tsnarf]	"Tsnarf",
+	[Tstartnewfile]	"Tstartnewfile",
+	[Twrite]	"Twrite",
+	[Tclose]	"Tclose",
+	[Tlook]		"Tlook",
+	[Tsearch]	"Tsearch",
+	[Tsend]		"Tsend",
+	[Tdclick]	"Tdclick",
+	[Tstartsnarf]	"Tstartsnarf",
+	[Tsetsnarf]	"Tsetsnarf",
+	[Tack]		"Tack",
+	[Texit]		"Texit",
+	[Tplumb]		"Tplumb",
+};
+
+void
+journal(int out, char *s)
+{
+	static int fd = 0;
+
+	if(fd <= 0)
+		fd = create("/tmp/sam.out", 1, 0666L);
+	fprint(fd, "%s%s\n", out? "out: " : "in:  ", s);
+}
+
+void
+journaln(int out, long n)
+{
+	char buf[32];
+
+	sprint(buf, "%ld", n);
+	journal(out, buf);
+}
+#else
+#define	journal(a, b)
+#define journaln(a, b)
+#endif
+
+int
+rcvchar(void){
+	static uchar buf[64];
+	static i, nleft = 0;
+
+	if(nleft <= 0){
+		nleft = read(0, (char *)buf, sizeof buf);
+		if(nleft <= 0)
+			return -1;
+		i = 0;
+	}
+	--nleft;
+	return buf[i++];
+}
+
+int
+rcv(void){
+	int c;
+	static state = 0;
+	static count = 0;
+	static i = 0;
+
+	while((c=rcvchar()) != -1)
+		switch(state){
+		case 0:
+			h.type = c;
+			state++;
+			break;
+
+		case 1:
+			h.count0 = c;
+			state++;
+			break;
+
+		case 2:
+			h.count1 = c;
+			count = h.count0|(h.count1<<8);
+			i = 0;
+			if(count > DATASIZE)
+				panic("count>DATASIZE");
+			if(count == 0)
+				goto zerocount;
+			state++;
+			break;
+
+		case 3:
+			indata[i++] = c;
+			if(i == count){
+		zerocount:
+				indata[i] = 0;
+				state = count = 0;
+				return inmesg(h.type);
+			}
+			break;
+		}
+	return 0;
+}
+
+File *
+whichfile(int tag)
+{
+	int i;
+
+	for(i = 0; i<file.nused; i++)
+		if(file.filepptr[i]->tag==tag)
+			return file.filepptr[i];
+	hiccough((char *)0);
+	return 0;
+}
+
+int
+inmesg(Tmesg type)
+{
+	Rune buf[1025];
+	char cbuf[64];
+	int i, m;
+	short s;
+	long l, l1;
+	File *f;
+	Posn p0, p1, p;
+	Range r;
+	String *str;
+	char *c, *wdir;
+	Rune *rp;
+	Plumbmsg *pm;
+
+	if(type > TMAX)
+		panic("inmesg");
+
+	journal(0, tname[type]);
+
+	inp = indata;
+	switch(type){
+	case -1:
+		panic("rcv error");
+
+	default:
+		fprint(2, "unknown type %d\n", type);
+		panic("rcv unknown");
+
+	case Tversion:
+		tversion = inshort();
+		journaln(0, tversion);
+		break;
+
+	case Tstartcmdfile:
+		l = invlong();		/* for 64-bit pointers */
+		journaln(0, l);
+		Strdupl(&genstr, samname);
+		cmd = newfile();
+		cmd->unread = 0;
+		outTsv(Hbindname, cmd->tag, l);
+		outTs(Hcurrent, cmd->tag);
+		logsetname(cmd, &genstr);
+		cmd->rasp = emalloc(sizeof(List));
+		cmd->mod = 0;
+		if(cmdstr.n){
+			loginsert(cmd, 0L, cmdstr.s, cmdstr.n);
+			Strdelete(&cmdstr, 0L, (Posn)cmdstr.n);
+		}
+		fileupdate(cmd, FALSE, TRUE);
+		outT0(Hunlock);
+		break;
+
+	case Tcheck:
+		/* go through whichfile to check the tag */
+		outTs(Hcheck, whichfile(inshort())->tag);
+		break;
+
+	case Trequest:
+		f = whichfile(inshort());
+		p0 = inlong();
+		p1 = p0+inshort();
+		journaln(0, p0);
+		journaln(0, p1-p0);
+		if(f->unread)
+			panic("Trequest: unread");
+		if(p1>f->_.nc)
+			p1 = f->_.nc;
+		if(p0>f->_.nc) /* can happen e.g. scrolling during command */
+			p0 = f->_.nc;
+		if(p0 == p1){
+			i = 0;
+			r.p1 = r.p2 = p0;
+		}else{
+			r = rdata(f->rasp, p0, p1-p0);
+			i = r.p2-r.p1;
+			bufread(f, r.p1, buf, i);
+		}
+		buf[i]=0;
+		outTslS(Hdata, f->tag, r.p1, tmprstr(buf, i+1));
+		break;
+
+	case Torigin:
+		s = inshort();
+		l = inlong();
+		l1 = inlong();
+		journaln(0, l1);
+		lookorigin(whichfile(s), l, l1);
+		break;
+
+	case Tstartfile:
+		termlocked++;
+		f = whichfile(inshort());
+		if(!f->rasp)	/* this might be a duplicate message */
+			f->rasp = emalloc(sizeof(List));
+		current(f);
+		outTsv(Hbindname, f->tag, invlong());	/* for 64-bit pointers */
+		outTs(Hcurrent, f->tag);
+		journaln(0, f->tag);
+		if(f->unread)
+			load(f);
+		else{
+			if(f->_.nc>0){
+				rgrow(f->rasp, 0L, f->_.nc);
+				outTsll(Hgrow, f->tag, 0L, f->_.nc);
+			}
+			outTs(Hcheck0, f->tag);
+			moveto(f, f->dot.r);
+		}
+		break;
+
+	case Tworkfile:
+		i = inshort();
+		f = whichfile(i);
+		current(f);
+		f->dot.r.p1 = inlong();
+		f->dot.r.p2 = inlong();
+		f->tdot = f->dot.r;
+		journaln(0, i);
+		journaln(0, f->dot.r.p1);
+		journaln(0, f->dot.r.p2);
+		break;
+
+	case Ttype:
+		f = whichfile(inshort());
+		p0 = inlong();
+		journaln(0, p0);
+		journal(0, (char*)inp);
+		str = tmpcstr((char*)inp);
+		i = str->n;
+		loginsert(f, p0, str->s, str->n);
+		if(fileupdate(f, FALSE, FALSE))
+			seq++;
+		if(f==cmd && p0==f->_.nc-i && i>0 && str->s[i-1]=='\n'){
+			freetmpstr(str);
+			termlocked++;
+			termcommand();
+		}else
+			freetmpstr(str);
+		f->dot.r.p1 = f->dot.r.p2 = p0+i; /* terminal knows this already */
+		f->tdot = f->dot.r;
+		break;
+
+	case Tcut:
+		f = whichfile(inshort());
+		p0 = inlong();
+		p1 = inlong();
+		journaln(0, p0);
+		journaln(0, p1);
+		logdelete(f, p0, p1);
+		if(fileupdate(f, FALSE, FALSE))
+			seq++;
+		f->dot.r.p1 = f->dot.r.p2 = p0;
+		f->tdot = f->dot.r;   /* terminal knows the value of dot already */
+		break;
+
+	case Tpaste:
+		f = whichfile(inshort());
+		p0 = inlong();
+		journaln(0, p0);
+		for(l=0; l<snarfbuf.nc; l+=m){
+			m = snarfbuf.nc-l;
+			if(m>BLOCKSIZE)
+				m = BLOCKSIZE;
+			bufread(&snarfbuf, l, genbuf, m);
+			loginsert(f, p0, tmprstr(genbuf, m)->s, m);
+		}
+		if(fileupdate(f, FALSE, TRUE))
+			seq++;
+		f->dot.r.p1 = p0;
+		f->dot.r.p2 = p0+snarfbuf.nc;
+		f->tdot.p1 = -1; /* force telldot to tell (arguably a BUG) */
+		telldot(f);
+		outTs(Hunlockfile, f->tag);
+		break;
+
+	case Tsnarf:
+		i = inshort();
+		p0 = inlong();
+		p1 = inlong();
+		snarf(whichfile(i), p0, p1, &snarfbuf, 0);
+		break;
+
+	case Tstartnewfile:
+		l = invlong();
+		Strdupl(&genstr, empty);
+		f = newfile();
+		f->rasp = emalloc(sizeof(List));
+		outTsv(Hbindname, f->tag, l);
+		logsetname(f, &genstr);
+		outTs(Hcurrent, f->tag);
+		current(f);
+		load(f);
+		break;
+
+	case Twrite:
+		termlocked++;
+		i = inshort();
+		journaln(0, i);
+		f = whichfile(i);
+		addr.r.p1 = 0;
+		addr.r.p2 = f->_.nc;
+		if(f->name.s[0] == 0)
+			error(Enoname);
+		Strduplstr(&genstr, &f->name);
+		writef(f);
+		break;
+
+	case Tclose:
+		termlocked++;
+		i = inshort();
+		journaln(0, i);
+		f = whichfile(i);
+		current(f);
+		trytoclose(f);
+		/* if trytoclose fails, will error out */
+		delete(f);
+		break;
+
+	case Tlook:
+		f = whichfile(inshort());
+		termlocked++;
+		p0 = inlong();
+		p1 = inlong();
+		journaln(0, p0);
+		journaln(0, p1);
+		setgenstr(f, p0, p1);
+		for(l = 0; l<genstr.n; l++){
+			i = genstr.s[l];
+			if(utfrune(".*+?(|)\\[]^$", i))
+				Strinsert(&genstr, tmpcstr("\\"), l++);
+		}
+		Straddc(&genstr, '\0');
+		nextmatch(f, &genstr, p1, 1);
+		moveto(f, sel.p[0]);
+		break;
+
+	case Tsearch:
+		termlocked++;
+		if(curfile == 0)
+			error(Enofile);
+		if(lastpat.s[0] == 0)
+			panic("Tsearch");
+		nextmatch(curfile, &lastpat, curfile->dot.r.p2, 1);
+		moveto(curfile, sel.p[0]);
+		break;
+
+	case Tsend:
+		termlocked++;
+		inshort();	/* ignored */
+		p0 = inlong();
+		p1 = inlong();
+		setgenstr(cmd, p0, p1);
+		bufreset(&snarfbuf);
+		bufinsert(&snarfbuf, (Posn)0, genstr.s, genstr.n);
+		outTl(Hsnarflen, genstr.n);
+		if(genstr.s[genstr.n-1] != '\n')
+			Straddc(&genstr, '\n');
+		loginsert(cmd, cmd->_.nc, genstr.s, genstr.n);
+		fileupdate(cmd, FALSE, TRUE);
+		cmd->dot.r.p1 = cmd->dot.r.p2 = cmd->_.nc;
+		telldot(cmd);
+		termcommand();
+		break;
+
+	case Tdclick:
+		f = whichfile(inshort());
+		p1 = inlong();
+		doubleclick(f, p1);
+		f->tdot.p1 = f->tdot.p2 = p1;
+		telldot(f);
+		outTs(Hunlockfile, f->tag);
+		break;
+
+	case Tstartsnarf:
+		if (snarfbuf.nc <= 0) {	/* nothing to export */
+			outTs(Hsetsnarf, 0);
+			break;
+		}
+		c = 0;
+		i = 0;
+		m = snarfbuf.nc;
+		if(m > SNARFSIZE) {
+			m = SNARFSIZE;
+			dprint("?warning: snarf buffer truncated\n");
+		}
+		rp = malloc(m*sizeof(Rune));
+		if(rp){
+			bufread(&snarfbuf, 0, rp, m);
+			c = Strtoc(tmprstr(rp, m));
+			free(rp);
+			i = strlen(c);
+		}
+		outTs(Hsetsnarf, i);
+		if(c){
+			Write(1, c, i);
+			free(c);
+		} else
+			dprint("snarf buffer too long\n");
+		break;
+
+	case Tsetsnarf:
+		m = inshort();
+		if(m > SNARFSIZE)
+			error(Etoolong);
+		c = malloc(m+1);
+		if(c){
+			for(i=0; i<m; i++)
+				c[i] = rcvchar();
+			c[m] = 0;
+			str = tmpcstr(c);
+			free(c);
+			bufreset(&snarfbuf);
+			bufinsert(&snarfbuf, (Posn)0, str->s, str->n);
+			freetmpstr(str);
+			outT0(Hunlock);
+		}
+		break;
+
+	case Tack:
+		waitack = 0;
+		break;
+
+	case Tplumb:
+		f = whichfile(inshort());
+		p0 = inlong();
+		p1 = inlong();
+		pm = emalloc(sizeof(Plumbmsg));
+		pm->src = strdup("sam");
+		pm->dst = 0;
+		/* construct current directory */
+		c = Strtoc(&f->name);
+		if(c[0] == '/')
+			pm->wdir = c;
+		else{
+			wdir = emalloc(1024);
+			getwd(wdir, 1024);
+			pm->wdir = emalloc(1024);
+			snprint(pm->wdir, 1024, "%s/%s", wdir, c);
+			cleanname(pm->wdir);
+			free(wdir);
+			free(c);
+		}
+		c = strrchr(pm->wdir, '/');
+		if(c)
+			*c = '\0';
+		pm->type = strdup("text");
+		if(p1 > p0)
+			pm->attr = nil;
+		else{
+			p = p0;
+			while(p0>0 && (i=filereadc(f, p0 - 1))!=' ' && i!='\t' && i!='\n')
+				p0--;
+			while(p1<f->_.nc && (i=filereadc(f, p1))!=' ' && i!='\t' && i!='\n')
+				p1++;
+			sprint(cbuf, "click=%ld", p-p0);
+			pm->attr = plumbunpackattr(cbuf);
+		}
+		if(p0==p1 || p1-p0>=BLOCKSIZE){
+			plumbfree(pm);
+			break;
+		}
+		setgenstr(f, p0, p1);
+		pm->data = Strtoc(&genstr);
+		pm->ndata = strlen(pm->data);
+		c = plumbpack(pm, &i);
+		if(c != 0){
+			outTs(Hplumb, i);
+			Write(1, c, i);
+			free(c);
+		}
+		plumbfree(pm);
+		break;
+
+	case Texit:
+		exits(0);
+	}
+	return TRUE;
+}
+
+void
+snarf(File *f, Posn p1, Posn p2, Buffer *buf, int emptyok)
+{
+	Posn l;
+	int i;
+
+	if(!emptyok && p1==p2)
+		return;
+	bufreset(buf);
+	/* Stage through genbuf to avoid compaction problems (vestigial) */
+	if(p2 > f->_.nc){
+		fprint(2, "bad snarf addr p1=%ld p2=%ld f->_.nc=%d\n", p1, p2, f->_.nc); /*ZZZ should never happen, can remove */
+		p2 = f->_.nc;
+	}
+	for(l=p1; l<p2; l+=i){
+		i = p2-l>BLOCKSIZE? BLOCKSIZE : p2-l;
+		bufread(f, l, genbuf, i);
+		bufinsert(buf, buf->nc, tmprstr(genbuf, i)->s, i);
+	}
+}
+
+int
+inshort(void)
+{
+	ushort n;
+
+	n = inp[0] | (inp[1]<<8);
+	inp += 2;
+	return n;
+}
+
+long
+inlong(void)
+{
+	ulong n;
+
+	n = inp[0] | (inp[1]<<8) | (inp[2]<<16) | (inp[3]<<24);
+	inp += 4;
+	return n;
+}
+
+long
+invlong(void)
+{
+	ulong n;
+	
+	n = (inp[7]<<24) | (inp[6]<<16) | (inp[5]<<8) | inp[4];
+	n = (n<<16) | (inp[3]<<8) | inp[2];
+	n = (n<<16) | (inp[1]<<8) | inp[0];
+	inp += 8;
+	return n;
+}
+
+void
+setgenstr(File *f, Posn p0, Posn p1)
+{
+	if(p0 != p1){
+		if(p1-p0 >= TBLOCKSIZE)
+			error(Etoolong);
+		Strinsure(&genstr, p1-p0);
+		bufread(f, p0, genbuf, p1-p0);
+		memmove(genstr.s, genbuf, RUNESIZE*(p1-p0));
+		genstr.n = p1-p0;
+	}else{
+		if(snarfbuf.nc == 0)
+			error(Eempty);
+		if(snarfbuf.nc > TBLOCKSIZE)
+			error(Etoolong);
+		bufread(&snarfbuf, (Posn)0, genbuf, snarfbuf.nc);
+		Strinsure(&genstr, snarfbuf.nc);
+		memmove(genstr.s, genbuf, RUNESIZE*snarfbuf.nc);
+		genstr.n = snarfbuf.nc;
+	}
+}
+
+void
+outT0(Hmesg type)
+{
+	outstart(type);
+	outsend();
+}
+
+void
+outTl(Hmesg type, long l)
+{
+	outstart(type);
+	outlong(l);
+	outsend();
+}
+
+void
+outTs(Hmesg type, int s)
+{
+	outstart(type);
+	journaln(1, s);
+	outshort(s);
+	outsend();
+}
+
+void
+outS(String *s)
+{
+	char *c;
+	int i;
+
+	c = Strtoc(s);
+	i = strlen(c);
+	outcopy(i, c);
+	if(i > 99)
+		c[99] = 0;
+	journaln(1, i);
+	journal(1, c);
+	free(c);
+}
+
+void
+outTsS(Hmesg type, int s1, String *s)
+{
+	outstart(type);
+	outshort(s1);
+	outS(s);
+	outsend();
+}
+
+void
+outTslS(Hmesg type, int s1, Posn l1, String *s)
+{
+	outstart(type);
+	outshort(s1);
+	journaln(1, s1);
+	outlong(l1);
+	journaln(1, l1);
+	outS(s);
+	outsend();
+}
+
+void
+outTS(Hmesg type, String *s)
+{
+	outstart(type);
+	outS(s);
+	outsend();
+}
+
+void
+outTsllS(Hmesg type, int s1, Posn l1, Posn l2, String *s)
+{
+	outstart(type);
+	outshort(s1);
+	outlong(l1);
+	outlong(l2);
+	journaln(1, l1);
+	journaln(1, l2);
+	outS(s);
+	outsend();
+}
+
+void
+outTsll(Hmesg type, int s, Posn l1, Posn l2)
+{
+	outstart(type);
+	outshort(s);
+	outlong(l1);
+	outlong(l2);
+	journaln(1, l1);
+	journaln(1, l2);
+	outsend();
+}
+
+void
+outTsl(Hmesg type, int s, Posn l)
+{
+	outstart(type);
+	outshort(s);
+	outlong(l);
+	journaln(1, l);
+	outsend();
+}
+
+void
+outTsv(Hmesg type, int s, Posn l)
+{
+	outstart(type);
+	outshort(s);
+	outvlong((void*)l);
+	journaln(1, l);
+	outsend();
+}
+
+void
+outstart(Hmesg type)
+{
+	journal(1, hname[type]);
+	outmsg[0] = type;
+	outp = outmsg+3;
+}
+
+void
+outcopy(int count, void *data)
+{
+	memmove(outp, data, count);
+	outp += count;
+}
+
+void
+outshort(int s)
+{
+	*outp++ = s;
+	*outp++ = s>>8; 
+}
+
+void
+outlong(long l)
+{
+	*outp++ = l;
+	*outp++ = l>>8;
+	*outp++ = l>>16;
+	*outp++ = l>>24;
+}
+
+void
+outvlong(void *v)
+{
+	int i;
+	ulong l;
+
+	l = (ulong) v;
+	for(i = 0; i < 8; i++, l >>= 8)
+		*outp++ = l;
+}
+
+void
+outsend(void)
+{
+	int outcount;
+
+	outcount = outp-outmsg;
+	outcount -= 3;
+	outmsg[1] = outcount;
+	outmsg[2] = outcount>>8;
+	outmsg = outp;
+	if(!noflush){
+		outcount = outmsg-outdata;
+		if (write(1, (char*) outdata, outcount) != outcount)
+			rescue();
+		outmsg = outdata;
+		return;
+	}
+	if(outmsg < outdata+DATASIZE)
+		return;
+	outflush();
+}
+
+void
+outflush(void)
+{
+	if(outmsg == outdata)
+		return;
+	noflush = 0;
+	outT0(Hack);
+	waitack = 1;
+	do
+		if(rcv() == 0){
+			rescue();
+			exits("eof");
+		}
+	while(waitack);
+	outmsg = outdata;
+	noflush = 1;
+}
diff --git a/src/cmd/sam/mesg.h b/src/cmd/sam/mesg.h
new file mode 100644
index 0000000..b88bf14
--- /dev/null
+++ b/src/cmd/sam/mesg.h
@@ -0,0 +1,131 @@
+/* VERSION 1 introduces plumbing
+	2 increases SNARFSIZE from 4096 to 32000
+ */
+#define	VERSION	2
+
+#define	TBLOCKSIZE 512		  /* largest piece of text sent to terminal */
+#define	DATASIZE  (UTFmax*TBLOCKSIZE+30) /* ... including protocol header stuff */
+#define	SNARFSIZE 32000		/* maximum length of exchanged snarf buffer, must fit in 15 bits */
+/*
+ * Messages originating at the terminal
+ */
+typedef enum Tmesg
+{
+	Tversion,	/* version */
+	Tstartcmdfile,	/* terminal just opened command frame */
+	Tcheck,		/* ask host to poke with Hcheck */
+	Trequest,	/* request data to fill a hole */
+	Torigin,	/* gimme an Horigin near here */
+	Tstartfile,	/* terminal just opened a file's frame */
+	Tworkfile,	/* set file to which commands apply */
+	Ttype,		/* add some characters, but terminal already knows */
+	Tcut,
+	Tpaste,
+	Tsnarf,
+	Tstartnewfile,	/* terminal just opened a new frame */
+	Twrite,		/* write file */
+	Tclose,		/* terminal requests file close; check mod. status */
+	Tlook,		/* search for literal current text */
+	Tsearch,	/* search for last regular expression */
+	Tsend,		/* pretend he typed stuff */
+	Tdclick,	/* double click */
+	Tstartsnarf,	/* initiate snarf buffer exchange */
+	Tsetsnarf,	/* remember string in snarf buffer */
+	Tack,		/* acknowledge Hack */
+	Texit,		/* exit */
+	Tplumb,		/* send plumb message */
+	TMAX,
+}Tmesg;
+/*
+ * Messages originating at the host
+ */
+typedef enum Hmesg
+{
+	Hversion,	/* version */
+	Hbindname,	/* attach name[0] to text in terminal */
+	Hcurrent,	/* make named file the typing file */
+	Hnewname,	/* create "" name in menu */
+	Hmovname,	/* move file name in menu */
+	Hgrow,		/* insert space in rasp */
+	Hcheck0,	/* see below */
+	Hcheck,		/* ask terminal to check whether it needs more data */
+	Hunlock,	/* command is finished; user can do things */
+	Hdata,		/* store this data in previously allocated space */
+	Horigin,	/* set origin of file/frame in terminal */
+	Hunlockfile,	/* unlock file in terminal */
+	Hsetdot,	/* set dot in terminal */
+	Hgrowdata,	/* Hgrow + Hdata folded together */
+	Hmoveto,	/* scrolling, context search, etc. */
+	Hclean,		/* named file is now 'clean' */
+	Hdirty,		/* named file is now 'dirty' */
+	Hcut,		/* remove space from rasp */
+	Hsetpat,	/* set remembered regular expression */
+	Hdelname,	/* delete file name from menu */
+	Hclose,		/* close file and remove from menu */
+	Hsetsnarf,	/* remember string in snarf buffer */
+	Hsnarflen,	/* report length of implicit snarf */
+	Hack,		/* request acknowledgement */
+	Hexit,
+	Hplumb,		/* return plumb message to terminal */
+	HMAX,
+}Hmesg;
+typedef struct Header{
+	uchar	type;		/* one of the above */
+	uchar	count0;		/* low bits of data size */
+	uchar	count1;		/* high bits of data size */
+	uchar	data[1];	/* variable size */
+}Header;
+
+/*
+ * File transfer protocol schematic, a la Holzmann
+ * #define N	6
+ * 
+ * chan h = [4] of { mtype };
+ * chan t = [4] of { mtype };
+ * 
+ * mtype = {	Hgrow, Hdata,
+ * 		Hcheck, Hcheck0,
+ * 		Trequest, Tcheck,
+ * 	};
+ * 
+ * active proctype host()
+ * {	byte n;
+ * 
+ * 	do
+ * 	:: n <  N -> n++; t!Hgrow
+ * 	:: n == N -> n++; t!Hcheck0
+ * 
+ * 	:: h?Trequest -> t!Hdata
+ * 	:: h?Tcheck   -> t!Hcheck
+ * 	od
+ * }
+ * 
+ * active proctype term()
+ * {
+ * 	do
+ * 	:: t?Hgrow   -> h!Trequest
+ * 	:: t?Hdata   -> skip
+ * 	:: t?Hcheck0 -> h!Tcheck
+ * 	:: t?Hcheck  ->
+ * 		if
+ * 		:: h!Trequest -> progress: h!Tcheck
+ * 		:: break
+ * 		fi
+ * 	od;
+ * 	printf("term exits\n")
+ * }
+ *
+ * From: gerard@research.bell-labs.com
+ * Date: Tue Jul 17 13:47:23 EDT 2001
+ * To: rob@research.bell-labs.com
+ * 
+ * spin -c 	(or -a) spec
+ * pcc -DNP -o pan pan.c
+ * pan -l
+ * 
+ * proves that there are no non-progress cycles
+ * (infinite executions *not* passing through
+ * the statement marked with a label starting
+ * with the prefix "progress")
+ * 
+ */
diff --git a/src/cmd/sam/mkfile b/src/cmd/sam/mkfile
new file mode 100644
index 0000000..cb60497
--- /dev/null
+++ b/src/cmd/sam/mkfile
@@ -0,0 +1,40 @@
+</$objtype/mkfile
+
+TARG=sam
+OFILES=sam.$O\
+	address.$O\
+	buff.$O\
+	cmd.$O\
+	disk.$O\
+	error.$O\
+	file.$O\
+	io.$O\
+	list.$O\
+	mesg.$O\
+	moveto.$O\
+	multi.$O\
+	plan9.$O\
+	rasp.$O\
+	regexp.$O\
+	shell.$O\
+	string.$O\
+	sys.$O\
+	util.$O\
+	xec.$O\
+
+HFILES=sam.h\
+	errors.h\
+	mesg.h\
+
+BIN=/$objtype/bin
+</sys/src/cmd/mkone
+
+address.$O cmd.$O parse.$O xec.$O unix.$O:	parse.h
+
+safeinstall: $O.out
+	mv $BIN/$TARG $BIN/o$TARG
+	cp $prereq $BIN/$TARG
+
+safeinstallall:V:
+	for (objtype in $CPUS)
+		mk safeinstall
diff --git a/src/cmd/sam/moveto.c b/src/cmd/sam/moveto.c
new file mode 100644
index 0000000..a84578c
--- /dev/null
+++ b/src/cmd/sam/moveto.c
@@ -0,0 +1,173 @@
+#include "sam.h"
+
+void
+moveto(File *f, Range r)
+{
+	Posn p1 = r.p1, p2 = r.p2;
+
+	f->dot.r.p1 = p1;
+	f->dot.r.p2 = p2;
+	if(f->rasp){
+		telldot(f);
+		outTsl(Hmoveto, f->tag, f->dot.r.p1);
+	}
+}
+
+void
+telldot(File *f)
+{
+	if(f->rasp == 0)
+		panic("telldot");
+	if(f->dot.r.p1==f->tdot.p1 && f->dot.r.p2==f->tdot.p2)
+		return;
+	outTsll(Hsetdot, f->tag, f->dot.r.p1, f->dot.r.p2);
+	f->tdot = f->dot.r;
+}
+
+void
+tellpat(void)
+{
+	outTS(Hsetpat, &lastpat);
+	patset = FALSE;
+}
+
+#define	CHARSHIFT	128
+
+void
+lookorigin(File *f, Posn p0, Posn ls)
+{
+	int nl, nc, c;
+	Posn p, oldp0;
+
+	if(p0 > f->_.nc)
+		p0 = f->_.nc;
+	oldp0 = p0;
+	p = p0;
+	for(nl=nc=c=0; c!=-1 && nl<ls && nc<ls*CHARSHIFT; nc++)
+		if((c=filereadc(f, --p)) == '\n'){
+			nl++;
+			oldp0 = p0-nc;
+		}
+	if(c == -1)
+		p0 = 0;
+	else if(nl==0){
+		if(p0>=CHARSHIFT/2)
+			p0-=CHARSHIFT/2;
+		else
+			p0 = 0;
+	}else
+		p0 = oldp0;
+	outTsl(Horigin, f->tag, p0);
+}
+
+int
+alnum(int c)
+{
+	/*
+	 * Hard to get absolutely right.  Use what we know about ASCII
+	 * and assume anything above the Latin control characters is
+	 * potentially an alphanumeric.
+	 */
+	if(c<=' ')
+		return 0;
+	if(0x7F<=c && c<=0xA0)
+		return 0;
+	if(utfrune("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c))
+		return 0;
+	return 1;
+}
+
+int
+clickmatch(File *f, int cl, int cr, int dir, Posn *p)
+{
+	int c;
+	int nest = 1;
+
+	for(;;){
+		if(dir > 0){
+			if(*p >= f->_.nc)
+				break;
+			c = filereadc(f, (*p)++);
+		}else{
+			if(*p == 0)
+				break;
+			c = filereadc(f, --(*p));
+		}
+		if(c == cr){
+			if(--nest==0)
+				return 1;
+		}else if(c == cl)
+			nest++;
+	}
+	return cl=='\n' && nest==1;
+}
+
+Rune*
+strrune(Rune *s, Rune c)
+{
+	Rune c1;
+
+	if(c == 0) {
+		while(*s++)
+			;
+		return s-1;
+	}
+
+	while(c1 = *s++)
+		if(c1 == c)
+			return s-1;
+	return 0;
+}
+
+void
+doubleclick(File *f, Posn p1)
+{
+	int c, i;
+	Rune *r, *l;
+	Posn p;
+
+	if(p1 > f->_.nc)
+		return;
+	f->dot.r.p1 = f->dot.r.p2 = p1;
+	for(i=0; left[i]; i++){
+		l = left[i];
+		r = right[i];
+		/* try left match */
+		p = p1;
+		if(p1 == 0)
+			c = '\n';
+		else
+			c = filereadc(f, p - 1);
+		if(strrune(l, c)){
+			if(clickmatch(f, c, r[strrune(l, c)-l], 1, &p)){
+				f->dot.r.p1 = p1;
+				f->dot.r.p2 = p-(c!='\n');
+			}
+			return;
+		}
+		/* try right match */
+		p = p1;
+		if(p1 == f->_.nc)
+			c = '\n';
+		else
+			c = filereadc(f, p);
+		if(strrune(r, c)){
+			if(clickmatch(f, c, l[strrune(r, c)-r], -1, &p)){
+				f->dot.r.p1 = p;
+				if(c!='\n' || p!=0 || filereadc(f, 0)=='\n')
+					f->dot.r.p1++;
+				f->dot.r.p2 = p1+(p1<f->_.nc && c=='\n');
+			}
+			return;
+		}
+	}
+	/* try filling out word to right */
+	p = p1;
+	while(p < f->_.nc && alnum(filereadc(f, p++)))
+		f->dot.r.p2++;
+	/* try filling out word to left */
+	p = p1;
+	while(--p >= 0 && alnum(filereadc(f, p)))
+		f->dot.r.p1--;
+}
+
diff --git a/src/cmd/sam/multi.c b/src/cmd/sam/multi.c
new file mode 100644
index 0000000..3df0dbe
--- /dev/null
+++ b/src/cmd/sam/multi.c
@@ -0,0 +1,123 @@
+#include "sam.h"
+
+List	file;
+ushort	tag;
+
+File *
+newfile(void)
+{
+	File *f;
+
+	f = fileopen();
+	inslist(&file, 0, (long)f);
+	f->tag = tag++;
+	if(downloaded)
+		outTs(Hnewname, f->tag);
+	/* already sorted; file name is "" */
+	return f;
+}
+
+int
+whichmenu(File *f)
+{
+	int i;
+
+	for(i=0; i<file.nused; i++)
+		if(file.filepptr[i]==f)
+			return i;
+	return -1;
+}
+
+void
+delfile(File *f)
+{
+	int w = whichmenu(f);
+
+	if(w < 0)	/* e.g. x/./D */
+		return;
+	if(downloaded)
+		outTs(Hdelname, f->tag);
+	dellist(&file, w);
+	fileclose(f);
+}
+
+void
+fullname(String *name)
+{
+	if(name->n > 0 && name->s[0]!='/' && name->s[0]!=0)
+		Strinsert(name, &curwd, (Posn)0);
+}
+
+void
+fixname(String *name)
+{
+	String *t;
+	char *s;
+
+	fullname(name);
+	s = Strtoc(name);
+	if(strlen(s) > 0)
+		s = cleanname(s);
+	t = tmpcstr(s);
+	Strduplstr(name, t);
+	free(s);
+	freetmpstr(t);
+
+	if(Strispre(&curwd, name))
+		Strdelete(name, 0, curwd.n);
+}
+
+void
+sortname(File *f)
+{
+	int i, cmp, w;
+	int dupwarned;
+
+	w = whichmenu(f);
+	dupwarned = FALSE;
+	dellist(&file, w);
+	if(f == cmd)
+		i = 0;
+	else{
+		for(i=0; i<file.nused; i++){
+			cmp = Strcmp(&f->name, &file.filepptr[i]->name);
+			if(cmp==0 && !dupwarned){
+				dupwarned = TRUE;
+				warn_S(Wdupname, &f->name);
+			}else if(cmp<0 && (i>0 || cmd==0))
+				break;
+		}
+	}
+	inslist(&file, i, (long)f);
+	if(downloaded)
+		outTsS(Hmovname, f->tag, &f->name);
+}
+
+void
+state(File *f, int cleandirty)
+{
+	if(f == cmd)
+		return;
+	f->unread = FALSE;
+	if(downloaded && whichmenu(f)>=0){	/* else flist or menu */
+		if(f->mod && cleandirty!=Dirty)
+			outTs(Hclean, f->tag);
+		else if(!f->mod && cleandirty==Dirty)
+			outTs(Hdirty, f->tag);
+	}
+	if(cleandirty == Clean)
+		f->mod = FALSE;
+	else
+		f->mod = TRUE;
+}
+
+File *
+lookfile(String *s)
+{
+	int i;
+
+	for(i=0; i<file.nused; i++)
+		if(Strcmp(&file.filepptr[i]->name, s) == 0)
+			return file.filepptr[i];
+	return 0;
+}
diff --git a/src/cmd/sam/parse.h b/src/cmd/sam/parse.h
new file mode 100644
index 0000000..12f293f
--- /dev/null
+++ b/src/cmd/sam/parse.h
@@ -0,0 +1,68 @@
+typedef struct Addr Addr;
+typedef struct Cmd Cmd;
+struct Addr
+{
+	char	type;	/* # (char addr), l (line addr), / ? . $ + - , ; */
+	union{
+		String	*re;
+		Addr	*aleft;		/* left side of , and ; */
+	} g;
+	Posn	num;
+	Addr	*next;			/* or right side of , and ; */
+};
+
+#define	are	g.re
+#define	left	g.aleft
+
+struct Cmd
+{
+	Addr	*addr;			/* address (range of text) */
+	String	*re;			/* regular expression for e.g. 'x' */
+	union{
+		Cmd	*cmd;		/* target of x, g, {, etc. */
+		String	*text;		/* text of a, c, i; rhs of s */
+		Addr	*addr;		/* address for m, t */
+	} g;
+	Cmd	*next;			/* pointer to next element in {} */
+	short	num;
+	ushort	flag;			/* whatever */
+	ushort	cmdc;			/* command character; 'x' etc. */
+};
+
+#define	ccmd	g.cmd
+#define	ctext	g.text
+#define	caddr	g.addr
+
+extern struct cmdtab{
+	ushort	cmdc;		/* command character */
+	uchar	text;		/* takes a textual argument? */
+	uchar	regexp;		/* takes a regular expression? */
+	uchar	addr;		/* takes an address (m or t)? */
+	uchar	defcmd;		/* default command; 0==>none */
+	uchar	defaddr;	/* default address */
+	uchar	count;		/* takes a count e.g. s2/// */
+	char	*token;		/* takes text terminated by one of these */
+	int	(*fn)(File*, Cmd*);	/* function to call with parse tree */
+}cmdtab[];
+
+enum Defaddr{	/* default addresses */
+	aNo,
+	aDot,
+	aAll,
+};
+
+int	nl_cmd(File*, Cmd*), a_cmd(File*, Cmd*), b_cmd(File*, Cmd*);
+int	c_cmd(File*, Cmd*), cd_cmd(File*, Cmd*), d_cmd(File*, Cmd*);
+int	D_cmd(File*, Cmd*), e_cmd(File*, Cmd*);
+int	f_cmd(File*, Cmd*), g_cmd(File*, Cmd*), i_cmd(File*, Cmd*);
+int	k_cmd(File*, Cmd*), m_cmd(File*, Cmd*), n_cmd(File*, Cmd*);
+int	p_cmd(File*, Cmd*), q_cmd(File*, Cmd*);
+int	s_cmd(File*, Cmd*), u_cmd(File*, Cmd*), w_cmd(File*, Cmd*);
+int	x_cmd(File*, Cmd*), X_cmd(File*, Cmd*), plan9_cmd(File*, Cmd*);
+int	eq_cmd(File*, Cmd*);
+
+
+String	*getregexp(int);
+Addr	*newaddr(void);
+Address	address(Addr*, Address, int);
+int	cmdexec(File*, Cmd*);
diff --git a/src/cmd/sam/plan9.c b/src/cmd/sam/plan9.c
new file mode 100644
index 0000000..6c3a60e
--- /dev/null
+++ b/src/cmd/sam/plan9.c
@@ -0,0 +1,185 @@
+#include "sam.h"
+
+Rune	samname[] = L"~~sam~~";
+
+Rune *left[]= {
+	L"{[(<«",
+	L"\n",
+	L"'\"`",
+	0
+};
+Rune *right[]= {
+	L"}])>»",
+	L"\n",
+	L"'\"`",
+	0
+};
+
+char	RSAM[] = "sam";
+char	SAMTERM[] = "/bin/aux/samterm";
+char	HOME[] = "home";
+char	TMPDIR[] = "/tmp";
+char	SH[] = "rc";
+char	SHPATH[] = "/bin/rc";
+char	RX[] = "rx";
+char	RXPATH[] = "/bin/rx";
+char	SAMSAVECMD[] = "/bin/rc\n/sys/lib/samsave";
+
+void
+dprint(char *z, ...)
+{
+	char buf[BLOCKSIZE];
+	va_list arg;
+
+	va_start(arg, z);
+	vseprint(buf, &buf[BLOCKSIZE], z, arg);
+	va_end(arg);
+	termwrite(buf);
+}
+
+void
+print_ss(char *s, String *a, String *b)
+{
+	dprint("?warning: %s: `%.*S' and `%.*S'\n", s, a->n, a->s, b->n, b->s);
+}
+
+void
+print_s(char *s, String *a)
+{
+	dprint("?warning: %s `%.*S'\n", s, a->n, a->s);
+}
+
+char*
+getuser(void)
+{
+	static char user[64];
+	int fd;
+
+	if(user[0] == 0){
+		fd = open("/dev/user", 0);
+		if(fd<0 || read(fd, user, sizeof user-1)<=0)
+			strcpy(user, "none");
+		close(fd);
+	}
+	return user;
+}
+
+int
+statfile(char *name, ulong *dev, uvlong *id, long *time, long *length, long *appendonly)
+{
+	Dir *dirb;
+
+	dirb = dirstat(name);
+	if(dirb == nil)
+		return -1;
+	if(dev)
+		*dev = dirb->type|(dirb->dev<<16);
+	if(id)
+		*id = dirb->qid.path;
+	if(time)
+		*time = dirb->mtime;
+	if(length)
+		*length = dirb->length;
+	if(appendonly)
+		*appendonly = dirb->mode & DMAPPEND;
+	free(dirb);
+	return 1;
+}
+
+int
+statfd(int fd, ulong *dev, uvlong *id, long *time, long *length, long *appendonly)
+{
+	Dir *dirb;
+
+	dirb = dirfstat(fd);
+	if(dirb == nil)
+		return -1;
+	if(dev)
+		*dev = dirb->type|(dirb->dev<<16);
+	if(id)
+		*id = dirb->qid.path;
+	if(time)
+		*time = dirb->mtime;
+	if(length)
+		*length = dirb->length;
+	if(appendonly)
+		*appendonly = dirb->mode & DMAPPEND;
+	free(dirb);
+	return 1;
+}
+
+void
+notifyf(void *a, char *s)
+{
+	USED(a);
+	if(bpipeok && strcmp(s, "sys: write on closed pipe") == 0)
+		noted(NCONT);
+	if(strcmp(s, "interrupt") == 0)
+		noted(NCONT);
+	panicking = 1;
+	rescue();
+	noted(NDFLT);
+}
+
+int
+newtmp(int num)
+{
+	int i, fd;
+	static char	tempnam[30];
+
+	i = getpid();
+	do
+		snprint(tempnam, sizeof tempnam, "%s/%d%.4s%dsam", TMPDIR, num, getuser(), i++);
+	while(access(tempnam, 0) == 0);
+	fd = create(tempnam, ORDWR|OCEXEC|ORCLOSE, 0000);
+	if(fd < 0){
+		remove(tempnam);
+		fd = create(tempnam, ORDWR|OCEXEC|ORCLOSE, 0000);
+	}
+	return fd;
+}
+
+int
+waitfor(int pid)
+{
+	int msg;
+	Waitmsg *w;
+
+	while((w = wait()) != nil){
+		if(w->pid != pid){
+			free(w);
+			continue;
+		}
+		msg = (w->msg[0] != '\0');
+		free(w);
+		return msg;
+	}
+	return -1;
+}
+
+void
+samerr(char *buf)
+{
+	sprint(buf, "%s/sam.err", TMPDIR);
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == 0)
+		panic("malloc fails");
+	memset(p, 0, n);
+	return p;
+}
+
+void*
+erealloc(void *p, ulong n)
+{
+	p = realloc(p, n);
+	if(p == 0)
+		panic("realloc fails");
+	return p;
+}
diff --git a/src/cmd/sam/plumb.c b/src/cmd/sam/plumb.c
new file mode 100644
index 0000000..d8db33d
--- /dev/null
+++ b/src/cmd/sam/plumb.c
@@ -0,0 +1,9 @@
+#include <u.h>
+#include "plumb.h"
+
+/* XXX - Can we do better than this? */
+char *cleanname(char *s) { return s; }
+char *plumbunpackattr(char *cbuf) {  return 0; }
+char *plumbpack(Plumbmsg *pm, int *i) { return 0; }
+int plumbfree(Plumbmsg *pm) { return 0; }
+
diff --git a/src/cmd/sam/rasp.c b/src/cmd/sam/rasp.c
new file mode 100644
index 0000000..45ac820
--- /dev/null
+++ b/src/cmd/sam/rasp.c
@@ -0,0 +1,325 @@
+#include "sam.h"
+/*
+ * GROWDATASIZE must be big enough that all errors go out as Hgrowdata's,
+ * so they will be scrolled into visibility in the ~~sam~~ window (yuck!).
+ */
+#define	GROWDATASIZE	50	/* if size is > this, send data with grow */
+
+void	rcut(List*, Posn, Posn);
+int	rterm(List*, Posn);
+void	rgrow(List*, Posn, Posn);
+
+static	Posn	growpos;
+static	Posn	grown;
+static	Posn	shrinkpos;
+static	Posn	shrunk;
+
+/*
+ * rasp routines inform the terminal of changes to the file.
+ *
+ * a rasp is a list of spans within the file, and an indication
+ * of whether the terminal knows about the span.
+ *
+ * optimize by coalescing multiple updates to the same span
+ * if it is not known by the terminal.
+ *
+ * other possible optimizations: flush terminal's rasp by cut everything,
+ * insert everything if rasp gets too large.
+ */
+
+/*
+ * only called for initial load of file
+ */
+void
+raspload(File *f)
+{
+	if(f->rasp == nil)
+		return;
+	grown = f->_.nc;
+	growpos = 0;
+	if(f->_.nc)
+		rgrow(f->rasp, 0, f->_.nc);
+	raspdone(f, 1);
+}
+
+void
+raspstart(File *f)
+{
+	if(f->rasp == nil)
+		return;
+	grown = 0;
+	shrunk = 0;
+	noflush = 1;
+}
+
+void
+raspdone(File *f, int toterm)
+{
+	if(f->dot.r.p1 > f->_.nc)
+		f->dot.r.p1 = f->_.nc;
+	if(f->dot.r.p2 > f->_.nc)
+		f->dot.r.p2 = f->_.nc;
+	if(f->mark.p1 > f->_.nc)
+		f->mark.p1 = f->_.nc;
+	if(f->mark.p2 > f->_.nc)
+		f->mark.p2 = f->_.nc;
+	if(f->rasp == nil)
+		return;
+	if(grown)
+		outTsll(Hgrow, f->tag, growpos, grown);
+	else if(shrunk)
+		outTsll(Hcut, f->tag, shrinkpos, shrunk);
+	if(toterm)
+		outTs(Hcheck0, f->tag);
+	outflush();
+	noflush = 0;
+	if(f == cmd){
+		cmdpt += cmdptadv;
+		cmdptadv = 0;
+	}
+}
+
+void
+raspdelete(File *f, uint p1, uint p2, int toterm)
+{
+	long n;
+
+	n = p2 - p1;
+	if(n == 0)
+		return;
+
+	if(p2 <= f->dot.r.p1){
+		f->dot.r.p1 -= n;
+		f->dot.r.p2 -= n;
+	}
+	if(p2 <= f->mark.p1){
+		f->mark.p1 -= n;
+		f->mark.p2 -= n;
+	}
+
+	if(f->rasp == nil)
+		return;
+
+	if(f==cmd && p1<cmdpt){
+		if(p2 <= cmdpt)
+			cmdpt -= n;
+		else
+			cmdpt = p1;
+	}
+	if(toterm){
+		if(grown){
+			outTsll(Hgrow, f->tag, growpos, grown);
+			grown = 0;
+		}else if(shrunk && shrinkpos!=p1 && shrinkpos!=p2){
+			outTsll(Hcut, f->tag, shrinkpos, shrunk);
+			shrunk = 0;
+		}
+		if(!shrunk || shrinkpos==p2)
+			shrinkpos = p1;
+		shrunk += n;
+	}
+	rcut(f->rasp, p1, p2);
+}
+
+void
+raspinsert(File *f, uint p1, Rune *buf, uint n, int toterm)
+{
+	Range r;
+
+	if(n == 0)
+		return;
+
+	if(p1 < f->dot.r.p1){
+		f->dot.r.p1 += n;
+		f->dot.r.p2 += n;
+	}
+	if(p1 < f->mark.p1){
+		f->mark.p1 += n;
+		f->mark.p2 += n;
+	}
+
+
+	if(f->rasp == nil)
+		return;
+	if(f==cmd && p1<cmdpt)
+		cmdpt += n;
+	if(toterm){
+		if(shrunk){
+			outTsll(Hcut, f->tag, shrinkpos, shrunk);
+			shrunk = 0;
+		}
+		if(n>GROWDATASIZE || !rterm(f->rasp, p1)){
+			rgrow(f->rasp, p1, n);
+			if(grown && growpos+grown!=p1 && growpos!=p1){
+				outTsll(Hgrow, f->tag, growpos, grown);
+				grown = 0;
+			}
+			if(!grown)
+				growpos = p1;
+			grown += n;
+		}else{
+			if(grown){
+				outTsll(Hgrow, f->tag, growpos, grown);
+				grown = 0;
+			}
+			rgrow(f->rasp, p1, n);
+			r = rdata(f->rasp, p1, n);
+			if(r.p1!=p1 || r.p2!=p1+n)
+				panic("rdata in toterminal");
+			outTsllS(Hgrowdata, f->tag, p1, n, tmprstr(buf, n));
+		}
+	}else{
+		rgrow(f->rasp, p1, n);
+		r = rdata(f->rasp, p1, n);
+		if(r.p1!=p1 || r.p2!=p1+n)
+			panic("rdata in toterminal");
+	}
+}
+
+#define	M	0x80000000L
+#define	P(i)	r->longptr[i]
+#define	T(i)	(P(i)&M)	/* in terminal */
+#define	L(i)	(P(i)&~M)	/* length of this piece */
+
+void
+rcut(List *r, Posn p1, Posn p2)
+{
+	Posn p, x;
+	int i;
+
+	if(p1 == p2)
+		panic("rcut 0");
+	for(p=0,i=0; i<r->nused && p+L(i)<=p1; p+=L(i++))
+		;
+	if(i == r->nused)
+		panic("rcut 1");
+	if(p < p1){	/* chop this piece */
+		if(p+L(i) < p2){
+			x = p1-p;
+			p += L(i);
+		}else{
+			x = L(i)-(p2-p1);
+			p = p2;
+		}
+		if(T(i))
+			P(i) = x|M;
+		else
+			P(i) = x;
+		i++;
+	}
+	while(i<r->nused && p+L(i)<=p2){
+		p += L(i);
+		dellist(r, i);
+	}
+	if(p < p2){
+		if(i == r->nused)
+			panic("rcut 2");
+		x = L(i)-(p2-p);
+		if(T(i))
+			P(i) = x|M;
+		else
+			P(i) = x;
+	}
+	/* can we merge i and i-1 ? */
+	if(i>0 && i<r->nused && T(i-1)==T(i)){
+		x = L(i-1)+L(i);
+		dellist(r, i--);
+		if(T(i))
+			P(i)=x|M;
+		else
+			P(i)=x;
+	}
+}
+
+void
+rgrow(List *r, Posn p1, Posn n)
+{
+	Posn p;
+	int i;
+
+	if(n == 0)
+		panic("rgrow 0");
+	for(p=0,i=0; i<r->nused && p+L(i)<=p1; p+=L(i++))
+		;
+	if(i == r->nused){	/* stick on end of file */
+		if(p!=p1)
+			panic("rgrow 1");
+		if(i>0 && !T(i-1))
+			P(i-1)+=n;
+		else
+			inslist(r, i, n);
+	}else if(!T(i))		/* goes in this empty piece */
+		P(i)+=n;
+	else if(p==p1 && i>0 && !T(i-1))	/* special case; simplifies life */
+		P(i-1)+=n;
+	else if(p==p1)
+		inslist(r, i, n);
+	else{			/* must break piece in terminal */
+		inslist(r, i+1, (L(i)-(p1-p))|M);
+		inslist(r, i+1, n);
+		P(i) = (p1-p)|M;
+	}
+}
+
+int
+rterm(List *r, Posn p1)
+{
+	Posn p;
+	int i;
+
+	for(p = 0,i = 0; i<r->nused && p+L(i)<=p1; p+=L(i++))
+		;
+	if(i==r->nused && (i==0 || !T(i-1)))
+		return 0;
+	return T(i);
+}
+
+Range
+rdata(List *r, Posn p1, Posn n)
+{
+	Posn p;
+	int i;
+	Range rg;
+
+	if(n==0)
+		panic("rdata 0");
+	for(p = 0,i = 0; i<r->nused && p+L(i)<=p1; p+=L(i++))
+		;
+	if(i==r->nused)
+		panic("rdata 1");
+	if(T(i)){
+		n-=L(i)-(p1-p);
+		if(n<=0){
+			rg.p1 = rg.p2 = p1;
+			return rg;
+		}
+		p+=L(i++);
+		p1 = p;
+	}
+	if(T(i) || i==r->nused)
+		panic("rdata 2");
+	if(p+L(i)<p1+n)
+		n = L(i)-(p1-p);
+	rg.p1 = p1;
+	rg.p2 = p1+n;
+	if(p!=p1){
+		inslist(r, i+1, L(i)-(p1-p));
+		P(i)=p1-p;
+		i++;
+	}
+	if(L(i)!=n){
+		inslist(r, i+1, L(i)-n);
+		P(i)=n;
+	}
+	P(i)|=M;
+	/* now i is set; can we merge? */
+	if(i<r->nused-1 && T(i+1)){
+		P(i)=(n+=L(i+1))|M;
+		dellist(r, i+1);
+	}
+	if(i>0 && T(i-1)){
+		P(i)=(n+L(i-1))|M;
+		dellist(r, i-1);
+	}
+	return rg;
+}
diff --git a/src/cmd/sam/regexp.c b/src/cmd/sam/regexp.c
new file mode 100644
index 0000000..dee4377
--- /dev/null
+++ b/src/cmd/sam/regexp.c
@@ -0,0 +1,801 @@
+#include "sam.h"
+
+Rangeset	sel;
+String		lastregexp;
+/*
+ * Machine Information
+ */
+typedef struct Inst Inst;
+
+struct Inst
+{
+	long	type;	/* < 0x10000 ==> literal, otherwise action */
+	union {
+		int rsid;
+		int rsubid;
+		int class;
+		struct Inst *rother;
+		struct Inst *rright;
+	} r;
+	union{
+		struct Inst *lleft;
+		struct Inst *lnext;
+	} l;
+};
+#define	sid	r.rsid
+#define	subid	r.rsubid
+#define	rclass	r.class
+#define	other	r.rother
+#define	right	r.rright
+#define	left	l.lleft
+#define	next	l.lnext
+
+#define	NPROG	1024
+Inst	program[NPROG];
+Inst	*progp;
+Inst	*startinst;	/* First inst. of program; might not be program[0] */
+Inst	*bstartinst;	/* same for backwards machine */
+
+typedef struct Ilist Ilist;
+struct Ilist
+{
+	Inst	*inst;		/* Instruction of the thread */
+	Rangeset se;
+	Posn	startp;		/* first char of match */
+};
+
+#define	NLIST	128
+
+Ilist	*tl, *nl;	/* This list, next list */
+Ilist	list[2][NLIST];
+static	Rangeset sempty;
+
+/*
+ * Actions and Tokens
+ *
+ *	0x100xx are operators, value == precedence
+ *	0x200xx are tokens, i.e. operands for operators
+ */
+#define	OPERATOR	0x10000	/* Bitmask of all operators */
+#define	START		0x10000	/* Start, used for marker on stack */
+#define	RBRA		0x10001	/* Right bracket, ) */
+#define	LBRA		0x10002	/* Left bracket, ( */
+#define	OR		0x10003	/* Alternation, | */
+#define	CAT		0x10004	/* Concatentation, implicit operator */
+#define	STAR		0x10005	/* Closure, * */
+#define	PLUS		0x10006	/* a+ == aa* */
+#define	QUEST		0x10007	/* a? == a|nothing, i.e. 0 or 1 a's */
+#define	ANY		0x20000	/* Any character but newline, . */
+#define	NOP		0x20001	/* No operation, internal use only */
+#define	BOL		0x20002	/* Beginning of line, ^ */
+#define	EOL		0x20003	/* End of line, $ */
+#define	CCLASS		0x20004	/* Character class, [] */
+#define	NCCLASS		0x20005	/* Negated character class, [^] */
+#define	END		0x20077	/* Terminate: match found */
+
+#define	ISATOR		0x10000
+#define	ISAND		0x20000
+
+/*
+ * Parser Information
+ */
+typedef struct Node Node;
+struct Node
+{
+	Inst	*first;
+	Inst	*last;
+};
+
+#define	NSTACK	20
+Node	andstack[NSTACK];
+Node	*andp;
+int	atorstack[NSTACK];
+int	*atorp;
+int	lastwasand;	/* Last token was operand */
+int	cursubid;
+int	subidstack[NSTACK];
+int	*subidp;
+int	backwards;
+int	nbra;
+Rune	*exprp;		/* pointer to next character in source expression */
+#define	DCLASS	10	/* allocation increment */
+int	nclass;		/* number active */
+int	Nclass;		/* high water mark */
+Rune	**class;
+int	negateclass;
+
+void	addinst(Ilist *l, Inst *inst, Rangeset *sep);
+void	newmatch(Rangeset*);
+void	bnewmatch(Rangeset*);
+void	pushand(Inst*, Inst*);
+void	pushator(int);
+Node	*popand(int);
+int	popator(void);
+void	startlex(Rune*);
+int	lex(void);
+void	operator(int);
+void	operand(int);
+void	evaluntil(int);
+void	optimize(Inst*);
+void	bldcclass(void);
+
+void
+regerror(Err e)
+{
+	Strzero(&lastregexp);
+	error(e);
+}
+
+void
+regerror_c(Err e, int c)
+{
+	Strzero(&lastregexp);
+	error_c(e, c);
+}
+
+Inst *
+newinst(int t)
+{
+	if(progp >= &program[NPROG])
+		regerror(Etoolong);
+	progp->type = t;
+	progp->left = 0;
+	progp->right = 0;
+	return progp++;
+}
+
+Inst *
+realcompile(Rune *s)
+{
+	int token;
+
+	startlex(s);
+	atorp = atorstack;
+	andp = andstack;
+	subidp = subidstack;
+	cursubid = 0;
+	lastwasand = FALSE;
+	/* Start with a low priority operator to prime parser */
+	pushator(START-1);
+	while((token=lex()) != END){
+		if((token&ISATOR) == OPERATOR)
+			operator(token);
+		else
+			operand(token);
+	}
+	/* Close with a low priority operator */
+	evaluntil(START);
+	/* Force END */
+	operand(END);
+	evaluntil(START);
+	if(nbra)
+		regerror(Eleftpar);
+	--andp;	/* points to first and only operand */
+	return andp->first;
+}
+
+void
+compile(String *s)
+{
+	int i;
+	Inst *oprogp;
+
+	if(Strcmp(s, &lastregexp)==0)
+		return;
+	for(i=0; i<nclass; i++)
+		free(class[i]);
+	nclass = 0;
+	progp = program;
+	backwards = FALSE;
+	startinst = realcompile(s->s);
+	optimize(program);
+	oprogp = progp;
+	backwards = TRUE;
+	bstartinst = realcompile(s->s);
+	optimize(oprogp);
+	Strduplstr(&lastregexp, s);
+}
+
+void
+operand(int t)
+{
+	Inst *i;
+	if(lastwasand)
+		operator(CAT);	/* catenate is implicit */
+	i = newinst(t);
+	if(t == CCLASS){
+		if(negateclass)
+			i->type = NCCLASS;	/* UGH */
+		i->rclass = nclass-1;		/* UGH */
+	}
+	pushand(i, i);
+	lastwasand = TRUE;
+}
+
+void
+operator(int t)
+{
+	if(t==RBRA && --nbra<0)
+		regerror(Erightpar);
+	if(t==LBRA){
+/*
+ *		if(++cursubid >= NSUBEXP)
+ *			regerror(Esubexp);
+ */
+		cursubid++;	/* silently ignored */
+		nbra++;
+		if(lastwasand)
+			operator(CAT);
+	}else
+		evaluntil(t);
+	if(t!=RBRA)
+		pushator(t);
+	lastwasand = FALSE;
+	if(t==STAR || t==QUEST || t==PLUS || t==RBRA)
+		lastwasand = TRUE;	/* these look like operands */
+}
+
+void
+cant(char *s)
+{
+	char buf[100];
+
+	sprint(buf, "regexp: can't happen: %s", s);
+	panic(buf);
+}
+
+void
+pushand(Inst *f, Inst *l)
+{
+	if(andp >= &andstack[NSTACK])
+		cant("operand stack overflow");
+	andp->first = f;
+	andp->last = l;
+	andp++;
+}
+
+void
+pushator(int t)
+{
+	if(atorp >= &atorstack[NSTACK])
+		cant("operator stack overflow");
+	*atorp++=t;
+	if(cursubid >= NSUBEXP)
+		*subidp++= -1;
+	else
+		*subidp++=cursubid;
+}
+
+Node *
+popand(int op)
+{
+	if(andp <= &andstack[0])
+		if(op)
+			regerror_c(Emissop, op);
+		else
+			regerror(Ebadregexp);
+	return --andp;
+}
+
+int
+popator(void)
+{
+	if(atorp <= &atorstack[0])
+		cant("operator stack underflow");
+	--subidp;
+	return *--atorp;
+}
+
+void
+evaluntil(int pri)
+{
+	Node *op1, *op2, *t;
+	Inst *inst1, *inst2;
+
+	while(pri==RBRA || atorp[-1]>=pri){
+		switch(popator()){
+		case LBRA:
+			op1 = popand('(');
+			inst2 = newinst(RBRA);
+			inst2->subid = *subidp;
+			op1->last->next = inst2;
+			inst1 = newinst(LBRA);
+			inst1->subid = *subidp;
+			inst1->next = op1->first;
+			pushand(inst1, inst2);
+			return;		/* must have been RBRA */
+		default:
+			panic("unknown regexp operator");
+			break;
+		case OR:
+			op2 = popand('|');
+			op1 = popand('|');
+			inst2 = newinst(NOP);
+			op2->last->next = inst2;
+			op1->last->next = inst2;
+			inst1 = newinst(OR);
+			inst1->right = op1->first;
+			inst1->left = op2->first;
+			pushand(inst1, inst2);
+			break;
+		case CAT:
+			op2 = popand(0);
+			op1 = popand(0);
+			if(backwards && op2->first->type!=END)
+				t = op1, op1 = op2, op2 = t;
+			op1->last->next = op2->first;
+			pushand(op1->first, op2->last);
+			break;
+		case STAR:
+			op2 = popand('*');
+			inst1 = newinst(OR);
+			op2->last->next = inst1;
+			inst1->right = op2->first;
+			pushand(inst1, inst1);
+			break;
+		case PLUS:
+			op2 = popand('+');
+			inst1 = newinst(OR);
+			op2->last->next = inst1;
+			inst1->right = op2->first;
+			pushand(op2->first, inst1);
+			break;
+		case QUEST:
+			op2 = popand('?');
+			inst1 = newinst(OR);
+			inst2 = newinst(NOP);
+			inst1->left = inst2;
+			inst1->right = op2->first;
+			op2->last->next = inst2;
+			pushand(inst1, inst2);
+			break;
+		}
+	}
+}
+
+
+void
+optimize(Inst *start)
+{
+	Inst *inst, *target;
+
+	for(inst=start; inst->type!=END; inst++){
+		target = inst->next;
+		while(target->type == NOP)
+			target = target->next;
+		inst->next = target;
+	}
+}
+
+#ifdef	DEBUG
+void
+dumpstack(void){
+	Node *stk;
+	int *ip;
+
+	dprint("operators\n");
+	for(ip = atorstack; ip<atorp; ip++)
+		dprint("0%o\n", *ip);
+	dprint("operands\n");
+	for(stk = andstack; stk<andp; stk++)
+		dprint("0%o\t0%o\n", stk->first->type, stk->last->type);
+}
+void
+dump(void){
+	Inst *l;
+
+	l = program;
+	do{
+		dprint("%d:\t0%o\t%d\t%d\n", l-program, l->type,
+			l->left-program, l->right-program);
+	}while(l++->type);
+}
+#endif
+
+void
+startlex(Rune *s)
+{
+	exprp = s;
+	nbra = 0;
+}
+
+
+int
+lex(void){
+	int c= *exprp++;
+
+	switch(c){
+	case '\\':
+		if(*exprp)
+			if((c= *exprp++)=='n')
+				c='\n';
+		break;
+	case 0:
+		c = END;
+		--exprp;	/* In case we come here again */
+		break;
+	case '*':
+		c = STAR;
+		break;
+	case '?':
+		c = QUEST;
+		break;
+	case '+':
+		c = PLUS;
+		break;
+	case '|':
+		c = OR;
+		break;
+	case '.':
+		c = ANY;
+		break;
+	case '(':
+		c = LBRA;
+		break;
+	case ')':
+		c = RBRA;
+		break;
+	case '^':
+		c = BOL;
+		break;
+	case '$':
+		c = EOL;
+		break;
+	case '[':
+		c = CCLASS;
+		bldcclass();
+		break;
+	}
+	return c;
+}
+
+long
+nextrec(void){
+	if(exprp[0]==0 || (exprp[0]=='\\' && exprp[1]==0))
+		regerror(Ebadclass);
+	if(exprp[0] == '\\'){
+		exprp++;
+		if(*exprp=='n'){
+			exprp++;
+			return '\n';
+		}
+		return *exprp++|0x10000;
+	}
+	return *exprp++;
+}
+
+void
+bldcclass(void)
+{
+	long c1, c2, n, na;
+	Rune *classp;
+
+	classp = emalloc(DCLASS*RUNESIZE);
+	n = 0;
+	na = DCLASS;
+	/* we have already seen the '[' */
+	if(*exprp == '^'){
+		classp[n++] = '\n';	/* don't match newline in negate case */
+		negateclass = TRUE;
+		exprp++;
+	}else
+		negateclass = FALSE;
+	while((c1 = nextrec()) != ']'){
+		if(c1 == '-'){
+    Error:
+			free(classp);
+			regerror(Ebadclass);
+		}
+		if(n+4 >= na){		/* 3 runes plus NUL */
+			na += DCLASS;
+			classp = erealloc(classp, na*RUNESIZE);
+		}
+		if(*exprp == '-'){
+			exprp++;	/* eat '-' */
+			if((c2 = nextrec()) == ']')
+				goto Error;
+			classp[n+0] = 0xFFFF;
+			classp[n+1] = c1;
+			classp[n+2] = c2;
+			n += 3;
+		}else
+			classp[n++] = c1;
+	}
+	classp[n] = 0;
+	if(nclass == Nclass){
+		Nclass += DCLASS;
+		class = erealloc(class, Nclass*sizeof(Rune*));
+	}
+	class[nclass++] = classp;
+}
+
+int
+classmatch(int classno, int c, int negate)
+{
+	Rune *p;
+
+	p = class[classno];
+	while(*p){
+		if(*p == 0xFFFF){
+			if(p[1]<=c && c<=p[2])
+				return !negate;
+			p += 3;
+		}else if(*p++ == c)
+			return !negate;
+	}
+	return negate;
+}
+
+/*
+ * Note optimization in addinst:
+ * 	*l must be pending when addinst called; if *l has been looked
+ *		at already, the optimization is a bug.
+ */
+void
+addinst(Ilist *l, Inst *inst, Rangeset *sep)
+{
+	Ilist *p;
+
+	for(p = l; p->inst; p++){
+		if(p->inst==inst){
+			if((sep)->p[0].p1 < p->se.p[0].p1)
+				p->se= *sep;	/* this would be bug */
+			return;	/* It's already there */
+		}
+	}
+	p->inst = inst;
+	p->se= *sep;
+	(p+1)->inst = 0;
+}
+
+int
+execute(File *f, Posn startp, Posn eof)
+{
+	int flag = 0;
+	Inst *inst;
+	Ilist *tlp;
+	Posn p = startp;
+	int nnl = 0, ntl;
+	int c;
+	int wrapped = 0;
+	int startchar = startinst->type<OPERATOR? startinst->type : 0;
+
+	list[0][0].inst = list[1][0].inst = 0;
+	sel.p[0].p1 = -1;
+	/* Execute machine once for each character */
+	for(;;p++){
+	doloop:
+		c = filereadc(f, p);
+		if(p>=eof || c<0){
+			switch(wrapped++){
+			case 0:		/* let loop run one more click */
+			case 2:
+				break;
+			case 1:		/* expired; wrap to beginning */
+				if(sel.p[0].p1>=0 || eof!=INFINITY)
+					goto Return;
+				list[0][0].inst = list[1][0].inst = 0;
+				p = 0;
+				goto doloop;
+			default:
+				goto Return;
+			}
+		}else if(((wrapped && p>=startp) || sel.p[0].p1>0) && nnl==0)
+			break;
+		/* fast check for first char */
+		if(startchar && nnl==0 && c!=startchar)
+			continue;
+		tl = list[flag];
+		nl = list[flag^=1];
+		nl->inst = 0;
+		ntl = nnl;
+		nnl = 0;
+		if(sel.p[0].p1<0 && (!wrapped || p<startp || startp==eof)){
+			/* Add first instruction to this list */
+			if(++ntl >= NLIST)
+	Overflow:
+				error(Eoverflow);
+			sempty.p[0].p1 = p;
+			addinst(tl, startinst, &sempty);
+		}
+		/* Execute machine until this list is empty */
+		for(tlp = tl; inst = tlp->inst; tlp++){	/* assignment = */
+	Switchstmt:
+			switch(inst->type){
+			default:	/* regular character */
+				if(inst->type==c){
+	Addinst:
+					if(++nnl >= NLIST)
+						goto Overflow;
+					addinst(nl, inst->next, &tlp->se);
+				}
+				break;
+			case LBRA:
+				if(inst->subid>=0)
+					tlp->se.p[inst->subid].p1 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case RBRA:
+				if(inst->subid>=0)
+					tlp->se.p[inst->subid].p2 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case ANY:
+				if(c!='\n')
+					goto Addinst;
+				break;
+			case BOL:
+				if(p==0 || filereadc(f, p - 1)=='\n'){
+	Step:
+					inst = inst->next;
+					goto Switchstmt;
+				}
+				break;
+			case EOL:
+				if(c == '\n')
+					goto Step;
+				break;
+			case CCLASS:
+				if(c>=0 && classmatch(inst->rclass, c, 0))
+					goto Addinst;
+				break;
+			case NCCLASS:
+				if(c>=0 && classmatch(inst->rclass, c, 1))
+					goto Addinst;
+				break;
+			case OR:
+				/* evaluate right choice later */
+				if(++ntl >= NLIST)
+					goto Overflow;
+				addinst(tlp, inst->right, &tlp->se);
+				/* efficiency: advance and re-evaluate */
+				inst = inst->left;
+				goto Switchstmt;
+			case END:	/* Match! */
+				tlp->se.p[0].p2 = p;
+				newmatch(&tlp->se);
+				break;
+			}
+		}
+	}
+    Return:
+	return sel.p[0].p1>=0;
+}
+
+void
+newmatch(Rangeset *sp)
+{
+	int i;
+
+	if(sel.p[0].p1<0 || sp->p[0].p1<sel.p[0].p1 ||
+	   (sp->p[0].p1==sel.p[0].p1 && sp->p[0].p2>sel.p[0].p2))
+		for(i = 0; i<NSUBEXP; i++)
+			sel.p[i] = sp->p[i];
+}
+
+int
+bexecute(File *f, Posn startp)
+{
+	int flag = 0;
+	Inst *inst;
+	Ilist *tlp;
+	Posn p = startp;
+	int nnl = 0, ntl;
+	int c;
+	int wrapped = 0;
+	int startchar = bstartinst->type<OPERATOR? bstartinst->type : 0;
+
+	list[0][0].inst = list[1][0].inst = 0;
+	sel.p[0].p1= -1;
+	/* Execute machine once for each character, including terminal NUL */
+	for(;;--p){
+	doloop:
+		if((c = filereadc(f, p - 1))==-1){
+			switch(wrapped++){
+			case 0:		/* let loop run one more click */
+			case 2:
+				break;
+			case 1:		/* expired; wrap to end */
+				if(sel.p[0].p1>=0)
+			case 3:
+					goto Return;
+				list[0][0].inst = list[1][0].inst = 0;
+				p = f->_.nc;
+				goto doloop;
+			default:
+				goto Return;
+			}
+		}else if(((wrapped && p<=startp) || sel.p[0].p1>0) && nnl==0)
+			break;
+		/* fast check for first char */
+		if(startchar && nnl==0 && c!=startchar)
+			continue;
+		tl = list[flag];
+		nl = list[flag^=1];
+		nl->inst = 0;
+		ntl = nnl;
+		nnl = 0;
+		if(sel.p[0].p1<0 && (!wrapped || p>startp)){
+			/* Add first instruction to this list */
+			if(++ntl >= NLIST)
+	Overflow:
+				error(Eoverflow);
+			/* the minus is so the optimizations in addinst work */
+			sempty.p[0].p1 = -p;
+			addinst(tl, bstartinst, &sempty);
+		}
+		/* Execute machine until this list is empty */
+		for(tlp = tl; inst = tlp->inst; tlp++){	/* assignment = */
+	Switchstmt:
+			switch(inst->type){
+			default:	/* regular character */
+				if(inst->type == c){
+	Addinst:
+					if(++nnl >= NLIST)
+						goto Overflow;
+					addinst(nl, inst->next, &tlp->se);
+				}
+				break;
+			case LBRA:
+				if(inst->subid>=0)
+					tlp->se.p[inst->subid].p1 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case RBRA:
+				if(inst->subid >= 0)
+					tlp->se.p[inst->subid].p2 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case ANY:
+				if(c != '\n')
+					goto Addinst;
+				break;
+			case BOL:
+				if(c=='\n' || p==0){
+	Step:
+					inst = inst->next;
+					goto Switchstmt;
+				}
+				break;
+			case EOL:
+				if(p==f->_.nc || filereadc(f, p)=='\n')
+					goto Step;
+				break;
+			case CCLASS:
+				if(c>=0 && classmatch(inst->rclass, c, 0))
+					goto Addinst;
+				break;
+			case NCCLASS:
+				if(c>=0 && classmatch(inst->rclass, c, 1))
+					goto Addinst;
+				break;
+			case OR:
+				/* evaluate right choice later */
+				if(++ntl >= NLIST)
+					goto Overflow;
+				addinst(tlp, inst->right, &tlp->se);
+				/* efficiency: advance and re-evaluate */
+				inst = inst->left;
+				goto Switchstmt;
+			case END:	/* Match! */
+				tlp->se.p[0].p1 = -tlp->se.p[0].p1; /* minus sign */
+				tlp->se.p[0].p2 = p;
+				bnewmatch(&tlp->se);
+				break;
+			}
+		}
+	}
+    Return:
+	return sel.p[0].p1>=0;
+}
+
+void
+bnewmatch(Rangeset *sp)
+{
+        int  i;
+        if(sel.p[0].p1<0 || sp->p[0].p1>sel.p[0].p2 || (sp->p[0].p1==sel.p[0].p2 && sp->p[0].p2<sel.p[0].p1))
+                for(i = 0; i<NSUBEXP; i++){       /* note the reversal; p1<=p2 */
+                        sel.p[i].p1 = sp->p[i].p2;
+                        sel.p[i].p2 = sp->p[i].p1;
+                }
+}
diff --git a/src/cmd/sam/sam b/src/cmd/sam/sam
new file mode 100755
index 0000000..733b555
--- /dev/null
+++ b/src/cmd/sam/sam
Binary files differ
diff --git a/src/cmd/sam/sam.c b/src/cmd/sam/sam.c
new file mode 100644
index 0000000..81ccaf7
--- /dev/null
+++ b/src/cmd/sam/sam.c
@@ -0,0 +1,739 @@
+#include "sam.h"
+
+Rune	genbuf[BLOCKSIZE];
+int	io;
+int	panicking;
+int	rescuing;
+String	genstr;
+String	rhs;
+String	curwd;
+String	cmdstr;
+Rune	empty[] = { 0 };
+char	*genc;
+File	*curfile;
+File	*flist;
+File	*cmd;
+jmp_buf	mainloop;
+List	tempfile;
+int	quitok = TRUE;
+int	downloaded;
+int	dflag;
+int	Rflag;
+char	*machine;
+char	*home;
+int	bpipeok;
+int	termlocked;
+char	*samterm = SAMTERM;
+char	*rsamname = RSAM;
+File	*lastfile;
+Disk	*disk;
+long	seq;
+
+Rune	baddir[] = { '<', 'b', 'a', 'd', 'd', 'i', 'r', '>', '\n'};
+
+void	usage(void);
+
+int main(int argc, char *argv[])
+{
+	int i;
+	String *t;
+	char **ap, **arg;
+
+	{ // libfmt-2.0 uses %lu where we need %lud
+	  extern int __flagfmt(Fmt*);
+	  fmtinstall('u', __flagfmt);
+	}
+
+	arg = argv++;
+	ap = argv;
+	while(argc>1 && argv[0] && argv[0][0]=='-'){
+		switch(argv[0][1]){
+		case 'd':
+			dflag++;
+			break;
+
+		case 'r':
+			--argc, argv++;
+			if(argc == 1)
+				usage();
+			machine = *argv;
+			break;
+
+		case 'R':
+			Rflag++;
+			break;
+
+		case 't':
+			--argc, argv++;
+			if(argc == 1)
+				usage();
+			samterm = *argv;
+			break;
+
+		case 's':
+			--argc, argv++;
+			if(argc == 1)
+				usage();
+			rsamname = *argv;
+			break;
+
+		case 'x':       /* x11 option - strip the x */
+                        strcpy(*argv+1, *argv+2);
+                        *ap++ = *argv++;
+                        *ap++ = *argv;
+                        argc--;
+                        break;
+
+		default:
+			dprint("sam: unknown flag %c\n", argv[0][1]);
+			exits("usage");
+		}
+		--argc, argv++;
+	}
+	Strinit(&cmdstr);
+	Strinit0(&lastpat);
+	Strinit0(&lastregexp);
+	Strinit0(&genstr);
+	Strinit0(&rhs);
+	Strinit0(&curwd);
+	tempfile.listptr = emalloc(1);	/* so it can be freed later */
+	Strinit0(&plan9cmd);
+	home = getenv(HOME);
+	disk = diskinit();
+	if(home == 0)
+		home = "/";
+	if(!dflag)
+		startup(machine, Rflag, arg, ap);
+	notify(notifyf);
+	getcurwd();
+	if(argc>1){
+		for(i=0; i<argc-1; i++){
+			if(!setjmp(mainloop)){
+				t = tmpcstr(argv[i]);
+				Straddc(t, '\0');
+				Strduplstr(&genstr, t);
+				freetmpstr(t);
+				fixname(&genstr);
+				logsetname(newfile(), &genstr);
+			}
+		}
+	}else if(!downloaded)
+		newfile();
+	seq++;
+	if(file.nused)
+		current(file.filepptr[0]);
+	setjmp(mainloop);
+	cmdloop();
+	trytoquit();	/* if we already q'ed, quitok will be TRUE */
+	exits(0);
+}
+
+void
+usage(void)
+{
+	dprint("usage: sam [-d] [-t samterm] [-s sam name] -r machine\n");
+	exits("usage");
+}
+
+void
+rescue(void)
+{
+	int i, nblank = 0;
+	File *f;
+	char *c;
+	char buf[256];
+
+	if(rescuing++)
+		return;
+	io = -1;
+	for(i=0; i<file.nused; i++){
+		f = file.filepptr[i];
+		if(f==cmd || f->_.nc==0 || !fileisdirty(f))
+			continue;
+		if(io == -1){
+			sprint(buf, "%s/sam.save", home);
+			io = create(buf, 1, 0777);
+			if(io<0)
+				return;
+		}
+		if(f->name.s[0]){
+			c = Strtoc(&f->name);
+			strncpy(buf, c, sizeof buf-1);
+			buf[sizeof buf-1] = 0;
+			free(c);
+		}else
+			sprint(buf, "nameless.%d", nblank++);
+		fprint(io, "#!%s '%s' $* <<'---%s'\n", SAMSAVECMD, buf, buf);
+		addr.r.p1 = 0, addr.r.p2 = f->_.nc;
+		writeio(f);
+		fprint(io, "\n---%s\n", (char *)buf);
+	}
+}
+
+void
+panic(char *s)
+{
+	int wasd;
+
+	if(!panicking++ && !setjmp(mainloop)){
+		wasd = downloaded;
+		downloaded = 0;
+		dprint("sam: panic: %s: %r\n", s);
+		if(wasd)
+			fprint(2, "sam: panic: %s: %r\n", s);
+		rescue();
+		abort();
+	}
+}
+
+void
+hiccough(char *s)
+{
+	File *f;
+	int i;
+
+	if(rescuing)
+		exits("rescue");
+	if(s)
+		dprint("%s\n", s);
+	resetcmd();
+	resetxec();
+	resetsys();
+	if(io > 0)
+		close(io);
+
+	/*
+	 * back out any logged changes & restore old sequences
+	 */
+	for(i=0; i<file.nused; i++){
+		f = file.filepptr[i];
+		if(f==cmd)
+			continue;
+		if(f->seq==seq){
+			bufdelete(&f->epsilon, 0, f->epsilon.nc);
+			f->seq = f->prevseq;
+			f->dot.r = f->prevdot;
+			f->mark = f->prevmark;
+			state(f, f->prevmod ? Dirty: Clean);
+		}
+	}
+
+	update();
+	if (curfile) {
+		if (curfile->unread)
+			curfile->unread = FALSE;
+		else if (downloaded)
+			outTs(Hcurrent, curfile->tag);
+	}
+	longjmp(mainloop, 1);
+}
+
+void
+intr(void)
+{
+	error(Eintr);
+}
+
+void
+trytoclose(File *f)
+{
+	char *t;
+	char buf[256];
+
+	if(f == cmd)	/* possible? */
+		return;
+	if(f->deleted)
+		return;
+	if(fileisdirty(f) && !f->closeok){
+		f->closeok = TRUE;
+		if(f->name.s[0]){
+			t = Strtoc(&f->name);
+			strncpy(buf, t, sizeof buf-1);
+			free(t);
+		}else
+			strcpy(buf, "nameless file");
+		error_s(Emodified, buf);
+	}
+	f->deleted = TRUE;
+}
+
+void
+trytoquit(void)
+{
+	int c;
+	File *f;
+
+	if(!quitok){
+		for(c = 0; c<file.nused; c++){
+			f = file.filepptr[c];
+			if(f!=cmd && fileisdirty(f)){
+				quitok = TRUE;
+				eof = FALSE;
+				error(Echanges);
+			}
+		}
+	}
+}
+
+void
+load(File *f)
+{
+	Address saveaddr;
+
+	Strduplstr(&genstr, &f->name);
+	filename(f);
+	if(f->name.s[0]){
+		saveaddr = addr;
+		edit(f, 'I');
+		addr = saveaddr;
+	}else{
+		f->unread = 0;
+		f->cleanseq = f->seq;
+	}
+
+	fileupdate(f, TRUE, TRUE);
+}
+
+void
+cmdupdate(void)
+{
+	if(cmd && cmd->seq!=0){
+		fileupdate(cmd, FALSE, downloaded);
+		cmd->dot.r.p1 = cmd->dot.r.p2 = cmd->_.nc;
+		telldot(cmd);
+	}
+}
+
+void
+delete(File *f)
+{
+	if(downloaded && f->rasp)
+		outTs(Hclose, f->tag);
+	delfile(f);
+	if(f == curfile)
+		current(0);
+}
+
+void
+update(void)
+{
+	int i, anymod;
+	File *f;
+
+	settempfile();
+	for(anymod = i=0; i<tempfile.nused; i++){
+		f = tempfile.filepptr[i];
+		if(f==cmd)	/* cmd gets done in main() */
+			continue;
+		if(f->deleted) {
+			delete(f);
+			continue;
+		}
+		if(f->seq==seq && fileupdate(f, FALSE, downloaded))
+			anymod++;
+		if(f->rasp)
+			telldot(f);
+	}
+	if(anymod)
+		seq++;
+}
+
+File *
+current(File *f)
+{
+	return curfile = f;
+}
+
+void
+edit(File *f, int cmd)
+{
+	int empty = TRUE;
+	Posn p;
+	int nulls;
+
+	if(cmd == 'r')
+		logdelete(f, addr.r.p1, addr.r.p2);
+	if(cmd=='e' || cmd=='I'){
+		logdelete(f, (Posn)0, f->_.nc);
+		addr.r.p2 = f->_.nc;
+	}else if(f->_.nc!=0 || (f->name.s[0] && Strcmp(&genstr, &f->name)!=0))
+		empty = FALSE;
+	if((io = open(genc, OREAD))<0) {
+		if (curfile && curfile->unread)
+			curfile->unread = FALSE;
+		error_r(Eopen, genc);
+	}
+	p = readio(f, &nulls, empty, TRUE);
+	closeio((cmd=='e' || cmd=='I')? -1 : p);
+	if(cmd == 'r')
+		f->ndot.r.p1 = addr.r.p2, f->ndot.r.p2 = addr.r.p2+p;
+	else
+		f->ndot.r.p1 = f->ndot.r.p2 = 0;
+	f->closeok = empty;
+	if (quitok)
+		quitok = empty;
+	else
+		quitok = FALSE;
+	state(f, empty && !nulls? Clean : Dirty);
+	if(empty && !nulls)
+		f->cleanseq = f->seq;
+	if(cmd == 'e')
+		filename(f);
+}
+
+int
+getname(File *f, String *s, int save)
+{
+	int c, i;
+
+	Strzero(&genstr);
+	if(genc){
+		free(genc);
+		genc = 0;
+	}
+	if(s==0 || (c = s->s[0])==0){		/* no name provided */
+		if(f)
+			Strduplstr(&genstr, &f->name);
+		goto Return;
+	}
+	if(c!=' ' && c!='\t')
+		error(Eblank);
+	for(i=0; (c=s->s[i])==' ' || c=='\t'; i++)
+		;
+	while(s->s[i] > ' ')
+		Straddc(&genstr, s->s[i++]);
+	if(s->s[i])
+		error(Enewline);
+	fixname(&genstr);
+	if(f && (save || f->name.s[0]==0)){
+		logsetname(f, &genstr);
+		if(Strcmp(&f->name, &genstr)){
+			quitok = f->closeok = FALSE;
+			f->qidpath = 0;
+			f->mtime = 0;
+			state(f, Dirty); /* if it's 'e', fix later */
+		}
+	}
+    Return:
+	genc = Strtoc(&genstr);
+	i = genstr.n;
+	if(i && genstr.s[i-1]==0)
+		i--;
+	return i;	/* strlen(name) */
+}
+
+void
+filename(File *f)
+{
+	if(genc)
+		free(genc);
+	genc = Strtoc(&genstr);
+	dprint("%c%c%c %s\n", " '"[f->mod],
+		"-+"[f->rasp!=0], " ."[f==curfile], genc);
+}
+
+void
+undostep(File *f, int isundo)
+{
+	uint p1, p2;
+	int mod;
+
+	mod = f->mod;
+	fileundo(f, isundo, 1, &p1, &p2, TRUE);
+	f->ndot = f->dot;
+	if(f->mod){
+		f->closeok = 0;
+		quitok = 0;
+	}else
+		f->closeok = 1;
+
+	if(f->mod != mod){
+		f->mod = mod;
+		if(mod)
+			mod = Clean;
+		else
+			mod = Dirty;
+		state(f, mod);
+	}
+}
+
+int
+undo(int isundo)
+{
+	File *f;
+	int i;
+	Mod max;
+
+	max = undoseq(curfile, isundo);
+	if(max == 0)
+		return 0;
+	settempfile();
+	for(i = 0; i<tempfile.nused; i++){
+		f = tempfile.filepptr[i];
+		if(f!=cmd && undoseq(f, isundo)==max)
+			undostep(f, isundo);
+	}
+	return 1;
+}
+
+int
+readcmd(String *s)
+{
+	int retcode;
+
+	if(flist != 0)
+		fileclose(flist);
+	flist = fileopen();
+
+	addr.r.p1 = 0, addr.r.p2 = flist->_.nc;
+	retcode = plan9(flist, '<', s, FALSE);
+	fileupdate(flist, FALSE, FALSE);
+	flist->seq = 0;
+	if (flist->_.nc > BLOCKSIZE)
+		error(Etoolong);
+	Strzero(&genstr);
+	Strinsure(&genstr, flist->_.nc);
+	bufread(flist, (Posn)0, genbuf, flist->_.nc);
+	memmove(genstr.s, genbuf, flist->_.nc*RUNESIZE);
+	genstr.n = flist->_.nc;
+	Straddc(&genstr, '\0');
+	return retcode;
+}
+
+void
+getcurwd(void)
+{
+	String *t;
+	char buf[256];
+
+	buf[0] = 0;
+	getwd(buf, sizeof(buf));
+	t = tmpcstr(buf);
+	Strduplstr(&curwd, t);
+	freetmpstr(t);
+	if(curwd.n == 0)
+		warn(Wpwd);
+	else if(curwd.s[curwd.n-1] != '/')
+		Straddc(&curwd, '/');
+}
+
+void
+cd(String *str)
+{
+	int i, fd;
+	char *s;
+	File *f;
+	String owd;
+
+	getcurwd();
+	if(getname((File *)0, str, FALSE))
+		s = genc;
+	else
+		s = home;
+	if(chdir(s))
+		syserror("chdir");
+	fd = open("/dev/wdir", OWRITE);
+	if(fd > 0)
+		write(fd, s, strlen(s));
+	dprint("!\n");
+	Strinit(&owd);
+	Strduplstr(&owd, &curwd);
+	getcurwd();
+	settempfile();
+	for(i=0; i<tempfile.nused; i++){
+		f = tempfile.filepptr[i];
+		if(f!=cmd && f->name.s[0]!='/' && f->name.s[0]!=0){
+			Strinsert(&f->name, &owd, (Posn)0);
+			fixname(&f->name);
+			sortname(f);
+		}else if(f != cmd && Strispre(&curwd, &f->name)){
+			fixname(&f->name);
+			sortname(f);
+		}
+	}
+	Strclose(&owd);
+}
+
+int
+loadflist(String *s)
+{
+	int c, i;
+
+	c = s->s[0];
+	for(i = 0; s->s[i]==' ' || s->s[i]=='\t'; i++)
+		;
+	if((c==' ' || c=='\t') && s->s[i]!='\n'){
+		if(s->s[i]=='<'){
+			Strdelete(s, 0L, (long)i+1);
+			readcmd(s);
+		}else{
+			Strzero(&genstr);
+			while((c = s->s[i++]) && c!='\n')
+				Straddc(&genstr, c);
+			Straddc(&genstr, '\0');
+		}
+	}else{
+		if(c != '\n')
+			error(Eblank);
+		Strdupl(&genstr, empty);
+	}
+	if(genc)
+		free(genc);
+	genc = Strtoc(&genstr);
+	return genstr.s[0];
+}
+
+File *
+readflist(int readall, int delete)
+{
+	Posn i;
+	int c;
+	File *f;
+	String t;
+
+	Strinit(&t);
+	for(i=0,f=0; f==0 || readall || delete; i++){	/* ++ skips blank */
+		Strdelete(&genstr, (Posn)0, i);
+		for(i=0; (c = genstr.s[i])==' ' || c=='\t' || c=='\n'; i++)
+			;
+		if(i >= genstr.n)
+			break;
+		Strdelete(&genstr, (Posn)0, i);
+		for(i=0; (c=genstr.s[i]) && c!=' ' && c!='\t' && c!='\n'; i++)
+			;
+
+		if(i == 0)
+			break;
+		genstr.s[i] = 0;
+		Strduplstr(&t, tmprstr(genstr.s, i+1));
+		fixname(&t);
+		f = lookfile(&t);
+		if(delete){
+			if(f == 0)
+				warn_S(Wfile, &t);
+			else
+				trytoclose(f);
+		}else if(f==0 && readall)
+			logsetname(f = newfile(), &t);
+	}
+	Strclose(&t);
+	return f;
+}
+
+File *
+tofile(String *s)
+{
+	File *f;
+
+	if(s->s[0] != ' ')
+		error(Eblank);
+	if(loadflist(s) == 0){
+		f = lookfile(&genstr);	/* empty string ==> nameless file */
+		if(f == 0)
+			error_s(Emenu, genc);
+	}else if((f=readflist(FALSE, FALSE)) == 0)
+		error_s(Emenu, genc);
+	return current(f);
+}
+
+File *
+getfile(String *s)
+{
+	File *f;
+
+	if(loadflist(s) == 0)
+		logsetname(f = newfile(), &genstr);
+	else if((f=readflist(TRUE, FALSE)) == 0)
+		error(Eblank);
+	return current(f);
+}
+
+void
+closefiles(File *f, String *s)
+{
+	if(s->s[0] == 0){
+		if(f == 0)
+			error(Enofile);
+		trytoclose(f);
+		return;
+	}
+	if(s->s[0] != ' ')
+		error(Eblank);
+	if(loadflist(s) == 0)
+		error(Enewline);
+	readflist(FALSE, TRUE);
+}
+
+void
+copy(File *f, Address addr2)
+{
+	Posn p;
+	int ni;
+	for(p=addr.r.p1; p<addr.r.p2; p+=ni){
+		ni = addr.r.p2-p;
+		if(ni > BLOCKSIZE)
+			ni = BLOCKSIZE;
+		bufread(f, p, genbuf, ni);
+		loginsert(addr2.f, addr2.r.p2, tmprstr(genbuf, ni)->s, ni);
+	}
+	addr2.f->ndot.r.p2 = addr2.r.p2+(f->dot.r.p2-f->dot.r.p1);
+	addr2.f->ndot.r.p1 = addr2.r.p2;
+}
+
+void
+move(File *f, Address addr2)
+{
+	if(addr.r.p2 <= addr2.r.p2){
+		logdelete(f, addr.r.p1, addr.r.p2);
+		copy(f, addr2);
+	}else if(addr.r.p1 >= addr2.r.p2){
+		copy(f, addr2);
+		logdelete(f, addr.r.p1, addr.r.p2);
+	}else
+		error(Eoverlap);
+}
+
+Posn
+nlcount(File *f, Posn p0, Posn p1)
+{
+	Posn nl = 0;
+
+	while(p0 < p1)
+		if(filereadc(f, p0++)=='\n')
+			nl++;
+	return nl;
+}
+
+void
+printposn(File *f, int charsonly)
+{
+	Posn l1, l2;
+
+	if(!charsonly){
+		l1 = 1+nlcount(f, (Posn)0, addr.r.p1);
+		l2 = l1+nlcount(f, addr.r.p1, addr.r.p2);
+		/* check if addr ends with '\n' */
+		if(addr.r.p2>0 && addr.r.p2>addr.r.p1 && filereadc(f, addr.r.p2-1)=='\n')
+			--l2;
+		dprint("%lud", l1);
+		if(l2 != l1)
+			dprint(",%lud", l2);
+		dprint("; ");
+	}
+	dprint("#%lud", addr.r.p1);
+	if(addr.r.p2 != addr.r.p1)
+		dprint(",#%lud", addr.r.p2);
+	dprint("\n");
+}
+
+void
+settempfile(void)
+{
+	if(tempfile.nalloc < file.nused){
+		free(tempfile.listptr);
+		tempfile.listptr = emalloc(sizeof(*tempfile.filepptr)*file.nused);
+		tempfile.nalloc = file.nused;
+	}
+	tempfile.nused = file.nused;
+	memmove(&tempfile.filepptr[0], &file.filepptr[0], file.nused*sizeof(File*));
+}
diff --git a/src/cmd/sam/sam.h b/src/cmd/sam/sam.h
new file mode 100644
index 0000000..6a2708c
--- /dev/null
+++ b/src/cmd/sam/sam.h
@@ -0,0 +1,407 @@
+#include <u.h>
+#include <libc.h>
+#include <plumb.h>
+#include "errors.h"
+
+/*
+ * BLOCKSIZE is relatively small to keep memory consumption down.
+ */
+
+#define	BLOCKSIZE	2048
+#define	RUNESIZE	sizeof(Rune)
+#define	NDISC		5
+#define	NBUFFILES	3+2*NDISC	/* plan 9+undo+snarf+NDISC*(transcript+buf) */
+#define NSUBEXP	10
+
+#define	TRUE		1
+#define	FALSE		0
+
+#define	INFINITY	0x7FFFFFFFL
+#define	INCR		25
+#define	STRSIZE		(2*BLOCKSIZE)
+
+typedef long		Posn;		/* file position or address */
+typedef	ushort		Mod;		/* modification number */
+
+typedef struct Address	Address;
+typedef struct Block	Block;
+typedef struct Buffer	Buffer;
+typedef struct Disk	Disk;
+typedef struct Discdesc	Discdesc;
+typedef struct File	File;
+typedef struct List	List;
+typedef struct Range	Range;
+typedef struct Rangeset	Rangeset;
+typedef struct String	String;
+
+enum State
+{
+	Clean =		' ',
+	Dirty =		'\'',
+	Unread =	'-',
+};
+
+struct Range
+{
+	Posn	p1, p2;
+};
+
+struct Rangeset
+{
+	Range	p[NSUBEXP];
+};
+
+struct Address
+{
+	Range	r;
+	File	*f;
+};
+
+struct String
+{
+	short	n;
+	short	size;
+	Rune	*s;
+};
+
+struct List	/* code depends on a long being able to hold a pointer */
+{
+	int	nalloc;
+	int	nused;
+	union{
+		void	*listp;
+		Block	*blkp;
+		long	*longp;
+		uchar*	*ucharp;
+		String*	*stringp;
+		File*	*filep;
+		long	listv;
+	}g;
+};
+
+#define	listptr		g.listp
+#define	blkptr		g.blkp
+#define	longptr		g.longp
+#define	ucharpptr	g.ucharp
+#define	stringpptr	g.stringp
+#define	filepptr	g.filep
+#define	listval		g.listv
+
+enum
+{
+	Blockincr =	256,
+	Maxblock = 	8*1024,
+
+	BUFSIZE = Maxblock,	/* size from fbufalloc() */
+	RBUFSIZE = BUFSIZE/sizeof(Rune),
+};
+
+
+enum
+{
+	Null		= '-',
+	Delete		= 'd',
+	Insert		= 'i',
+	Filename	= 'f',
+	Dot		= 'D',
+	Mark		= 'm',
+};
+
+struct Block
+{
+	uint		addr;	/* disk address in bytes */
+	union
+	{
+		uint	n;	/* number of used runes in block */
+		Block	*next;	/* pointer to next in free list */
+	} _;
+};
+
+struct Disk
+{
+	int		fd;
+	uint		addr;	/* length of temp file */
+	Block		*free[Maxblock/Blockincr+1];
+};
+
+Disk*		diskinit(void);
+Block*		disknewblock(Disk*, uint);
+void		diskrelease(Disk*, Block*);
+void		diskread(Disk*, Block*, Rune*, uint);
+void		diskwrite(Disk*, Block**, Rune*, uint);
+
+struct Buffer
+{
+	uint		nc;
+	Rune		*c;	/* cache */
+	uint		cnc;	/* bytes in cache */
+	uint		cmax;	/* size of allocated cache */
+	uint		cq;	/* position of cache */
+	int		cdirty;	/* cache needs to be written */
+	uint		cbi;	/* index of cache Block */
+	Block		**bl;	/* array of blocks */
+	uint		nbl;	/* number of blocks */
+};
+void		bufinsert(Buffer*, uint, Rune*, uint);
+void		bufdelete(Buffer*, uint, uint);
+uint		bufload(Buffer*, uint, int, int*);
+void		bufread(Buffer*, uint, Rune*, uint);
+void		bufclose(Buffer*);
+void		bufreset(Buffer*);
+
+struct File
+{
+	Buffer _;				/* the data */
+	Buffer		delta;		/* transcript of changes */
+	Buffer		epsilon;	/* inversion of delta for redo */
+	String		name;		/* name of associated file */
+	uvlong		qidpath;	/* of file when read */
+	uint		mtime;		/* of file when read */
+	int		dev;		/* of file when read */
+	int		unread;		/* file has not been read from disk */
+
+	long		seq;		/* if seq==0, File acts like Buffer */
+	long		cleanseq;	/* f->seq at last read/write of file */
+	int		mod;		/* file appears modified in menu */
+	char		rescuing;	/* sam exiting; this file unusable */
+
+//	Text		*curtext;	/* most recently used associated text */
+//	Text		**text;		/* list of associated texts */
+//	int		ntext;
+//	int		dumpid;		/* used in dumping zeroxed windows */
+
+	Posn		hiposn;		/* highest address touched this Mod */
+	Address		dot;		/* current position */
+	Address		ndot;		/* new current position after update */
+	Range		tdot;		/* what terminal thinks is current range */
+	Range		mark;		/* tagged spot in text (don't confuse with Mark) */
+	List		*rasp;		/* map of what terminal's got */
+	short		tag;		/* for communicating with terminal */
+	char		closeok;	/* ok to close file? */
+	char		deleted;	/* delete at completion of command */
+	Range		prevdot;	/* state before start of change */
+	Range		prevmark;
+	long		prevseq;
+	int		prevmod;
+};
+//File*		fileaddtext(File*, Text*);
+void		fileclose(File*);
+void		filedelete(File*, uint, uint);
+//void		filedeltext(File*, Text*);
+void		fileinsert(File*, uint, Rune*, uint);
+uint		fileload(File*, uint, int, int*);
+void		filemark(File*);
+void		filereset(File*);
+void		filesetname(File*, String*);
+void		fileundelete(File*, Buffer*, uint, uint);
+void		fileuninsert(File*, Buffer*, uint, uint);
+void		fileunsetname(File*, Buffer*);
+void		fileundo(File*, int, int, uint*, uint*, int);
+int		fileupdate(File*, int, int);
+
+int		filereadc(File*, uint);
+File		*fileopen(void);
+void		loginsert(File*, uint, Rune*, uint);
+void		logdelete(File*, uint, uint);
+void		logsetname(File*, String*);
+int		fileisdirty(File*);
+long		undoseq(File*, int);
+long		prevseq(Buffer*);
+
+void		raspload(File*);
+void		raspstart(File*);
+void		raspdelete(File*, uint, uint, int);
+void		raspinsert(File*, uint, Rune*, uint, int);
+void		raspdone(File*, int);
+
+/*
+ * acme fns
+ */
+void*	fbufalloc(void);
+void	fbuffree(void*);
+uint	min(uint, uint);
+void	cvttorunes(char*, int, Rune*, int*, int*, int*);
+
+#define	runemalloc(a)		(Rune*)emalloc((a)*sizeof(Rune))
+#define	runerealloc(a, b)	(Rune*)realloc((a), (b)*sizeof(Rune))
+#define	runemove(a, b, c)	memmove((a), (b), (c)*sizeof(Rune))
+
+int	alnum(int);
+int	Read(int, void*, int);
+void	Seek(int, long, int);
+int	plan9(File*, int, String*, int);
+int	Write(int, void*, int);
+int	bexecute(File*, Posn);
+void	cd(String*);
+void	closefiles(File*, String*);
+void	closeio(Posn);
+void	cmdloop(void);
+void	cmdupdate(void);
+void	compile(String*);
+void	copy(File*, Address);
+File	*current(File*);
+void	delete(File*);
+void	delfile(File*);
+void	dellist(List*, int);
+void	doubleclick(File*, Posn);
+void	dprint(char*, ...);
+void	edit(File*, int);
+void	*emalloc(ulong);
+void	*erealloc(void*, ulong);
+void	error(Err);
+void	error_c(Err, int);
+void	error_r(Err, char*);
+void	error_s(Err, char*);
+int	execute(File*, Posn, Posn);
+int	filematch(File*, String*);
+void	filename(File*);
+void	fixname(String*);
+void	fullname(String*);
+void	getcurwd(void);
+File	*getfile(String*);
+int	getname(File*, String*, int);
+long	getnum(int);
+void	hiccough(char*);
+void	inslist(List*, int, long);
+Address	lineaddr(Posn, Address, int);
+void	listfree(List*);
+void	load(File*);
+File	*lookfile(String*);
+void	lookorigin(File*, Posn, Posn);
+int	lookup(int);
+void	move(File*, Address);
+void	moveto(File*, Range);
+File	*newfile(void);
+void	nextmatch(File*, String*, Posn, int);
+int	newtmp(int);
+void	notifyf(void*, char*);
+void	panic(char*);
+void	printposn(File*, int);
+void	print_ss(char*, String*, String*);
+void	print_s(char*, String*);
+int	rcv(void);
+Range	rdata(List*, Posn, Posn);
+Posn	readio(File*, int*, int, int);
+void	rescue(void);
+void	resetcmd(void);
+void	resetsys(void);
+void	resetxec(void);
+void	rgrow(List*, Posn, Posn);
+void	samerr(char*);
+void	settempfile(void);
+int	skipbl(void);
+void	snarf(File*, Posn, Posn, Buffer*, int);
+void	sortname(File*);
+void	startup(char*, int, char**, char**);
+void	state(File*, int);
+int	statfd(int, ulong*, uvlong*, long*, long*, long*);
+int	statfile(char*, ulong*, uvlong*, long*, long*, long*);
+void	Straddc(String*, int);
+void	Strclose(String*);
+int	Strcmp(String*, String*);
+void	Strdelete(String*, Posn, Posn);
+void	Strdupl(String*, Rune*);
+void	Strduplstr(String*, String*);
+void	Strinit(String*);
+void	Strinit0(String*);
+void	Strinsert(String*, String*, Posn);
+void	Strinsure(String*, ulong);
+int	Strispre(String*, String*);
+void	Strzero(String*);
+int	Strlen(Rune*);
+char	*Strtoc(String*);
+void	syserror(char*);
+void	telldot(File*);
+void	tellpat(void);
+String	*tmpcstr(char*);
+String	*tmprstr(Rune*, int);
+void	freetmpstr(String*);
+void	termcommand(void);
+void	termwrite(char*);
+File	*tofile(String*);
+void	trytoclose(File*);
+void	trytoquit(void);
+int	undo(int);
+void	update(void);
+int	waitfor(int);
+void	warn(Warn);
+void	warn_s(Warn, char*);
+void	warn_SS(Warn, String*, String*);
+void	warn_S(Warn, String*);
+int	whichmenu(File*);
+void	writef(File*);
+Posn	writeio(File*);
+Discdesc *Dstart(void);
+
+extern Rune	samname[];	/* compiler dependent */
+extern Rune	*left[];
+extern Rune	*right[];
+
+extern char	RSAM[];		/* system dependent */
+extern char	SAMTERM[];
+extern char	HOME[];
+extern char	TMPDIR[];
+extern char	SH[];
+extern char	SHPATH[];
+extern char	RX[];
+extern char	RXPATH[];
+extern char	SAMSAVECMD[];
+
+/*
+ * acme globals
+ */
+extern long		seq;
+extern Disk		*disk;
+
+extern char	*rsamname;	/* globals */
+extern char	*samterm;
+extern Rune	genbuf[];
+extern char	*genc;
+extern int	io;
+extern int	patset;
+extern int	quitok;
+extern Address	addr;
+extern Buffer	snarfbuf;
+extern Buffer	plan9buf;
+extern List	file;
+extern List	tempfile;
+extern File	*cmd;
+extern File	*curfile;
+extern File	*lastfile;
+extern Mod	modnum;
+extern Posn	cmdpt;
+extern Posn	cmdptadv;
+extern Rangeset	sel;
+extern String	curwd;
+extern String	cmdstr;
+extern String	genstr;
+extern String	lastpat;
+extern String	lastregexp;
+extern String	plan9cmd;
+extern int	downloaded;
+extern int	eof;
+extern int	bpipeok;
+extern int	panicking;
+extern Rune	empty[];
+extern int	termlocked;
+extern int	noflush;
+
+#include "mesg.h"
+
+void	outTs(Hmesg, int);
+void	outT0(Hmesg);
+void	outTl(Hmesg, long);
+void	outTslS(Hmesg, int, long, String*);
+void	outTS(Hmesg, String*);
+void	outTsS(Hmesg, int, String*);
+void	outTsllS(Hmesg, int, long, long, String*);
+void	outTsll(Hmesg, int, long, long);
+void	outTsl(Hmesg, int, long);
+void	outTsv(Hmesg, int, long);
+void	outstart(Hmesg);
+void	outcopy(int, void*);
+void	outshort(int);
+void	outlong(long);
+void	outvlong(void*);
+void	outsend(void);
+void	outflush(void);
diff --git a/src/cmd/sam/shell.c b/src/cmd/sam/shell.c
new file mode 100644
index 0000000..2cac31b
--- /dev/null
+++ b/src/cmd/sam/shell.c
@@ -0,0 +1,152 @@
+#include "sam.h"
+#include "parse.h"
+
+extern	jmp_buf	mainloop;
+
+char	errfile[64];
+String	plan9cmd;	/* null terminated */
+Buffer	plan9buf;
+void	checkerrs(void);
+
+int
+plan9(File *f, int type, String *s, int nest)
+{
+	long l;
+	int m;
+	int pid, fd;
+	int retcode;
+	int pipe1[2], pipe2[2];
+
+	if(s->s[0]==0 && plan9cmd.s[0]==0)
+		error(Enocmd);
+	else if(s->s[0])
+		Strduplstr(&plan9cmd, s);
+	if(downloaded){
+		samerr(errfile);
+		remove(errfile);
+	}
+	if(type!='!' && pipe(pipe1)==-1)
+		error(Epipe);
+	if(type=='|')
+		snarf(f, addr.r.p1, addr.r.p2, &plan9buf, 1);
+	if((pid=fork()) == 0){
+		if(downloaded){	/* also put nasty fd's into errfile */
+			fd = create(errfile, 1, 0666L);
+			if(fd < 0)
+				fd = create("/dev/null", 1, 0666L);
+			dup(fd, 2);
+			close(fd);
+			/* 2 now points at err file */
+			if(type == '>')
+				dup(2, 1);
+			else if(type=='!'){
+				dup(2, 1);
+				fd = open("/dev/null", 0);
+				dup(fd, 0);
+				close(fd);
+			}
+		}
+		if(type != '!') {
+			if(type=='<' || type=='|')
+				dup(pipe1[1], 1);
+			else if(type == '>')
+				dup(pipe1[0], 0);
+			close(pipe1[0]);
+			close(pipe1[1]);
+		}
+		if(type == '|'){
+			if(pipe(pipe2) == -1)
+				exits("pipe");
+			if((pid = fork())==0){
+				/*
+				 * It's ok if we get SIGPIPE here
+				 */
+				close(pipe2[0]);
+				io = pipe2[1];
+				if(retcode=!setjmp(mainloop)){	/* assignment = */
+					char *c;
+					for(l = 0; l<plan9buf.nc; l+=m){
+						m = plan9buf.nc-l;
+						if(m>BLOCKSIZE-1)
+							m = BLOCKSIZE-1;
+						bufread(&plan9buf, l, genbuf, m);
+						genbuf[m] = 0;
+						c = Strtoc(tmprstr(genbuf, m+1));
+						Write(pipe2[1], c, strlen(c));
+						free(c);
+					}
+				}
+				exits(retcode? "error" : 0);
+			}
+			if(pid==-1){
+				fprint(2, "Can't fork?!\n");
+				exits("fork");
+			}
+			dup(pipe2[0], 0);
+			close(pipe2[0]);
+			close(pipe2[1]);
+		}
+		if(type=='<'){
+			close(0);	/* so it won't read from terminal */
+			open("/dev/null", 0);
+		}
+		execl(SHPATH, SH, "-c", Strtoc(&plan9cmd), (char *)0);
+		exits("exec");
+	}
+	if(pid == -1)
+		error(Efork);
+	if(type=='<' || type=='|'){
+		int nulls;
+		if(downloaded && addr.r.p1 != addr.r.p2)
+			outTl(Hsnarflen, addr.r.p2-addr.r.p1);
+		snarf(f, addr.r.p1, addr.r.p2, &snarfbuf, 0);
+		logdelete(f, addr.r.p1, addr.r.p2);
+		close(pipe1[1]);
+		io = pipe1[0];
+		f->tdot.p1 = -1;
+		f->ndot.r.p2 = addr.r.p2+readio(f, &nulls, 0, FALSE);
+		f->ndot.r.p1 = addr.r.p2;
+		closeio((Posn)-1);
+	}else if(type=='>'){
+		close(pipe1[0]);
+		io = pipe1[1];
+		bpipeok = 1;
+		writeio(f);
+		bpipeok = 0;
+		closeio((Posn)-1);
+	}
+	retcode = waitfor(pid);
+	if(type=='|' || type=='<')
+		if(retcode!=0)
+			warn(Wbadstatus);
+	if(downloaded)
+		checkerrs();
+	if(!nest)
+		dprint("!\n");
+	return retcode;
+}
+
+void
+checkerrs(void)
+{
+	char buf[256];
+	int f, n, nl;
+	char *p;
+	long l;
+
+	if(statfile(errfile, 0, 0, 0, &l, 0) > 0 && l != 0){
+		if((f=open((char *)errfile, 0)) != -1){
+			if((n=read(f, buf, sizeof buf-1)) > 0){
+				for(nl=0,p=buf; nl<3 && p<&buf[n]; p++)
+					if(*p=='\n')
+						nl++;
+				*p = 0;
+				dprint("%s", buf);
+				if(p-buf < l-1)
+					dprint("(sam: more in %s)\n", errfile);
+			}
+			close(f);
+		}
+	}else
+		remove((char *)errfile);
+}
diff --git a/src/cmd/sam/unix.c b/src/cmd/sam/unix.c
new file mode 100644
index 0000000..cc15db4
--- /dev/null
+++ b/src/cmd/sam/unix.c
@@ -0,0 +1,272 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <pwd.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include "sam.h"
+
+Rune    samname[] = { '~', '~', 's', 'a', 'm', '~', '~', 0 };
+ 
+static Rune l1[] = { '{', '[', '(', '<', 0253, 0};
+static Rune l2[] = { '\n', 0};
+static Rune l3[] = { '\'', '"', '`', 0};
+Rune *left[]= { l1, l2, l3, 0};
+
+static Rune r1[] = {'}', ']', ')', '>', 0273, 0};
+static Rune r2[] = {'\n', 0};
+static Rune r3[] = {'\'', '"', '`', 0};
+Rune *right[]= { r1, r2, r3, 0};
+
+#ifndef SAMTERMNAME
+#define SAMTERMNAME "/usr/local/bin/samterm"
+#endif
+#ifndef TMPDIRNAME
+#define TMPDIRNAME "/tmp"
+#endif
+#ifndef SHNAME
+#define SHNAME "rc"
+#endif
+#ifndef SHPATHNAME
+#define SHPATHNAME "/bin/rc"
+#endif
+#ifndef RXNAME
+#define RXNAME "ssh"
+#endif
+#ifndef RXPATHNAME
+#define RXPATHNAME "/usr/local/bin/ssh"
+#endif
+#ifndef SAMSAVECMDNAME
+#define SAMSAVECMDNAME "/bin/rc\n/usr/local/bin/samsave"
+#endif
+
+char	RSAM[] = "sam";
+char	SAMTERM[] = SAMTERMNAME;
+char	HOME[] = "HOME";
+char	TMPDIR[] = TMPDIRNAME;
+char	SH[] = SHNAME;
+char	SHPATH[] = SHPATHNAME;
+char	RX[] = RXNAME;
+char	RXPATH[] = RXPATHNAME;
+char	SAMSAVECMD[] = SAMSAVECMDNAME;
+
+
+void
+dprint(char *z, ...)
+{
+	char buf[BLOCKSIZE];
+	va_list arg;
+
+	va_start(arg, z);
+	vseprint(buf, &buf[BLOCKSIZE], z, arg);
+	va_end(arg);
+	termwrite(buf);
+}
+
+void
+print_ss(char *s, String *a, String *b)
+{
+	dprint("?warning: %s: `%.*S' and `%.*S'\n", s, a->n, a->s, b->n, b->s);
+}
+
+void
+print_s(char *s, String *a)
+{
+	dprint("?warning: %s `%.*S'\n", s, a->n, a->s);
+}
+
+char*
+getuser(void)
+{
+	static char user[64];
+	if(user[0] == 0){
+		struct passwd *pw = getpwuid(getuid());
+		strcpy(user, pw ? pw->pw_name : "nobody");
+	}
+	return user;
+}
+
+int     
+statfile(char *name, ulong *dev, uvlong *id, long *time, long *length, long *appendonly) 
+{
+        struct stat dirb;
+
+        if (stat(name, &dirb) == -1)
+                return -1;
+        if (dev)
+                *dev = dirb.st_dev;   
+        if (id)
+                *id = dirb.st_ino;
+        if (time)
+                *time = dirb.st_mtime;
+        if (length)
+                *length = dirb.st_size;
+        if(appendonly)
+                *appendonly = 0;
+        return 1;
+}
+
+int
+statfd(int fd, ulong *dev, uvlong *id, long *time, long *length, long *appendonly)
+{
+        struct stat dirb;
+
+        if (fstat(fd, &dirb) == -1)   
+                return -1;
+        if (dev)
+                *dev = dirb.st_dev;
+        if (id) 
+                *id = dirb.st_ino;
+        if (time)
+                *time = dirb.st_mtime;
+        if (length)
+                *length = dirb.st_size;
+        if(appendonly)
+                *appendonly = 0;
+        return 1;
+}
+
+void
+hup(int sig)
+{
+        panicking = 1; // ???
+        rescue();
+        exit(1);
+}
+
+int
+notify (void(*f)(void *, char *))
+{
+        signal(SIGINT, SIG_IGN);
+        signal(SIGPIPE, SIG_IGN);  // XXX - bpipeok?
+        signal(SIGHUP, hup);
+        return 1;
+}
+
+void
+notifyf(void *a, char *b)       /* never called; hup is instead */
+{
+}
+
+static int
+temp_file(char *buf, int bufsize)
+{
+        char *tmp;
+        int n, fd;
+
+        tmp = getenv("TMPDIR");
+        if (!tmp)
+                tmp = TMPDIR;
+
+        n = snprint(buf, bufsize, "%s/sam.%d.XXXXXXX", tmp, getuid());
+        if (bufsize <= n)
+                return -1;
+        if ((fd = mkstemp(buf)) < 0)  /* SES - linux sometimes uses mode 0666 */
+                return -1;
+        if (fcntl(fd, F_SETFD, fcntl(fd,F_GETFD,0) | FD_CLOEXEC) < 0)
+                return -1;
+        return fd;
+}
+
+int
+tempdisk(void)
+{
+        char buf[4096];
+        int fd = temp_file(buf, sizeof buf);
+        if (fd >= 0)
+                remove(buf);
+        return fd; 
+}
+
+#undef wait
+int     
+waitfor(int pid)
+{
+        int wm; 
+        int rpid;
+                
+        do; while((rpid = wait(&wm)) != pid && rpid != -1);
+        return (WEXITSTATUS(wm));
+}       
+
+void
+samerr(char *buf)
+{
+	sprint(buf, "%s/sam.%s.err", TMPDIR, getuser());
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == 0)
+		panic("malloc fails");
+	memset(p, 0, n);
+	return p;
+}
+
+void*
+erealloc(void *p, ulong n)
+{
+	p = realloc(p, n);
+	if(p == 0)
+		panic("realloc fails");
+	return p;
+}
+
+#if 0
+char *
+strdup(const char *s)
+{
+	return strcpy(emalloc(strlen(s)), s);
+}
+#endif
+
+/*
+void exits(const char *s)
+{
+    if (s) fprint(2, "exit: %s\n", s);
+    exit(s != 0);
+}
+
+void
+_exits(const char *s)
+{
+    if (s) fprint(2, "exit: %s\n", s);
+    _exit(s != 0);
+}
+
+int errstr(char *buf, int size)
+{
+    extern int errno;
+                
+    snprint(buf, size, "%s", strerror(errno));
+    return 1;       
+}                       
+*/
+                    
+int create(char *name, int omode, int perm)
+{
+    int mode;
+    int fd; 
+        
+    if (omode & OWRITE) mode = O_WRONLY;
+    else if (omode & OREAD) mode = O_RDONLY;
+    else mode = O_RDWR;
+
+    if ((fd = open(name, mode|O_CREAT|O_TRUNC, perm)) < 0)
+	return fd;
+
+    if (omode & OCEXEC)
+	fcntl(fd, F_SETFD, fcntl(fd,F_GETFD,0) | FD_CLOEXEC);
+
+    /* SES - not exactly right, but hopefully good enough. */
+    if (omode & ORCLOSE)
+	remove(name);
+
+    return fd;                          
+}
diff --git a/src/cmd/sam/xec.c b/src/cmd/sam/xec.c
new file mode 100644
index 0000000..42acab0
--- /dev/null
+++ b/src/cmd/sam/xec.c
@@ -0,0 +1,508 @@
+#include "sam.h"
+#include "parse.h"
+
+int	Glooping;
+int	nest;
+
+int	append(File*, Cmd*, Posn);
+int	display(File*);
+void	looper(File*, Cmd*, int);
+void	filelooper(Cmd*, int);
+void	linelooper(File*, Cmd*);
+
+void
+resetxec(void)
+{
+	Glooping = nest = 0;
+}
+
+int
+cmdexec(File *f, Cmd *cp)
+{
+	int i;
+	Addr *ap;
+	Address a;
+
+	if(f && f->unread)
+		load(f);
+	if(f==0 && (cp->addr==0 || cp->addr->type!='"') &&
+	    !utfrune("bBnqUXY!", cp->cmdc) &&
+	    cp->cmdc!=('c'|0x100) && !(cp->cmdc=='D' && cp->ctext))
+		error(Enofile);
+	i = lookup(cp->cmdc);
+	if(i >= 0 && cmdtab[i].defaddr != aNo){
+		if((ap=cp->addr)==0 && cp->cmdc!='\n'){
+			cp->addr = ap = newaddr();
+			ap->type = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap->type = '*';
+		}else if(ap && ap->type=='"' && ap->next==0 && cp->cmdc!='\n'){
+			ap->next = newaddr();
+			ap->next->type = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap->next->type = '*';
+		}
+		if(cp->addr){	/* may be false for '\n' (only) */
+			static Address none = {0,0,0};
+			if(f)
+				addr = address(ap, f->dot, 0);
+			else	/* a " */
+				addr = address(ap, none, 0);
+			f = addr.f;
+		}
+	}
+	current(f);
+	switch(cp->cmdc){
+	case '{':
+		a = cp->addr? address(cp->addr, f->dot, 0): f->dot;
+		for(cp = cp->ccmd; cp; cp = cp->next){
+			a.f->dot = a;
+			cmdexec(a.f, cp);
+		}
+		break;
+	default:
+		i=(*cmdtab[i].fn)(f, cp);
+		return i;
+	}
+	return 1;
+}
+
+
+int
+a_cmd(File *f, Cmd *cp)
+{
+	return append(f, cp, addr.r.p2);
+}
+
+int
+b_cmd(File *f, Cmd *cp)
+{
+	USED(f);
+	f = cp->cmdc=='b'? tofile(cp->ctext) : getfile(cp->ctext);
+	if(f->unread)
+		load(f);
+	else if(nest == 0)
+		filename(f);
+	return TRUE;
+}
+
+int
+c_cmd(File *f, Cmd *cp)
+{
+	logdelete(f, addr.r.p1, addr.r.p2);
+	f->ndot.r.p1 = f->ndot.r.p2 = addr.r.p2;
+	return append(f, cp, addr.r.p2);
+}
+
+int
+d_cmd(File *f, Cmd *cp)
+{
+	USED(cp);
+	logdelete(f, addr.r.p1, addr.r.p2);
+	f->ndot.r.p1 = f->ndot.r.p2 = addr.r.p1;
+	return TRUE;
+}
+
+int
+D_cmd(File *f, Cmd *cp)
+{
+	closefiles(f, cp->ctext);
+	return TRUE;
+}
+
+int
+e_cmd(File *f, Cmd *cp)
+{
+	if(getname(f, cp->ctext, cp->cmdc=='e')==0)
+		error(Enoname);
+	edit(f, cp->cmdc);
+	return TRUE;
+}
+
+int
+f_cmd(File *f, Cmd *cp)
+{
+	getname(f, cp->ctext, TRUE);
+	filename(f);
+	return TRUE;
+}
+
+int
+g_cmd(File *f, Cmd *cp)
+{
+	if(f!=addr.f)panic("g_cmd f!=addr.f");
+	compile(cp->re);
+	if(execute(f, addr.r.p1, addr.r.p2) ^ cp->cmdc=='v'){
+		f->dot = addr;
+		return cmdexec(f, cp->ccmd);
+	}
+	return TRUE;
+}
+
+int
+i_cmd(File *f, Cmd *cp)
+{
+	return append(f, cp, addr.r.p1);
+}
+
+int
+k_cmd(File *f, Cmd *cp)
+{
+	USED(cp);
+	f->mark = addr.r;
+	return TRUE;
+}
+
+int
+m_cmd(File *f, Cmd *cp)
+{
+	Address addr2;
+
+	addr2 = address(cp->caddr, f->dot, 0);
+	if(cp->cmdc=='m')
+		move(f, addr2);
+	else
+		copy(f, addr2);
+	return TRUE;
+}
+
+int
+n_cmd(File *f, Cmd *cp)
+{
+	int i;
+	USED(f);
+	USED(cp);
+	for(i = 0; i<file.nused; i++){
+		if(file.filepptr[i] == cmd)
+			continue;
+		f = file.filepptr[i];
+		Strduplstr(&genstr, &f->name);
+		filename(f);
+	}
+	return TRUE;
+}
+
+int
+p_cmd(File *f, Cmd *cp)
+{
+	USED(cp);
+	return display(f);
+}
+
+int
+q_cmd(File *f, Cmd *cp)
+{
+	USED(cp);
+	USED(f);
+	trytoquit();
+	if(downloaded){
+		outT0(Hexit);
+		return TRUE;
+	}
+	return FALSE;
+}
+
+int
+s_cmd(File *f, Cmd *cp)
+{
+	int i, j, c, n;
+	Posn p1, op, didsub = 0, delta = 0;
+
+	n = cp->num;
+	op= -1;
+	compile(cp->re);
+	for(p1 = addr.r.p1; p1<=addr.r.p2 && execute(f, p1, addr.r.p2); ){
+		if(sel.p[0].p1==sel.p[0].p2){	/* empty match? */
+			if(sel.p[0].p1==op){
+				p1++;
+				continue;
+			}
+			p1 = sel.p[0].p2+1;
+		}else
+			p1 = sel.p[0].p2;
+		op = sel.p[0].p2;
+		if(--n>0)
+			continue;
+		Strzero(&genstr);
+		for(i = 0; i<cp->ctext->n; i++)
+			if((c = cp->ctext->s[i])=='\\' && i<cp->ctext->n-1){
+				c = cp->ctext->s[++i];
+				if('1'<=c && c<='9') {
+					j = c-'0';
+					if(sel.p[j].p2-sel.p[j].p1>BLOCKSIZE)
+						error(Elongtag);
+					bufread(f, sel.p[j].p1, genbuf, sel.p[j].p2-sel.p[j].p1);
+					Strinsert(&genstr, tmprstr(genbuf, (sel.p[j].p2-sel.p[j].p1)), genstr.n);
+				}else
+				 	Straddc(&genstr, c);
+			}else if(c!='&')
+				Straddc(&genstr, c);
+			else{
+				if(sel.p[0].p2-sel.p[0].p1>BLOCKSIZE)
+					error(Elongrhs);
+				bufread(f, sel.p[0].p1, genbuf, sel.p[0].p2-sel.p[0].p1);
+				Strinsert(&genstr,
+					tmprstr(genbuf, (int)(sel.p[0].p2-sel.p[0].p1)),
+					genstr.n);
+			}
+		if(sel.p[0].p1!=sel.p[0].p2){
+			logdelete(f, sel.p[0].p1, sel.p[0].p2);
+			delta-=sel.p[0].p2-sel.p[0].p1;
+		}
+		if(genstr.n){
+			loginsert(f, sel.p[0].p2, genstr.s, genstr.n);
+			delta+=genstr.n;
+		}
+		didsub = 1;
+		if(!cp->flag)
+			break;
+	}
+	if(!didsub && nest==0)
+		error(Enosub);
+	f->ndot.r.p1 = addr.r.p1, f->ndot.r.p2 = addr.r.p2+delta;
+	return TRUE;
+}
+
+int
+u_cmd(File *f, Cmd *cp)
+{
+	int n;
+
+	USED(f);
+	USED(cp);
+	n = cp->num;
+	if(n >= 0)
+		while(n-- && undo(TRUE))
+			;
+	else
+		while(n++ && undo(FALSE))
+			;
+	return TRUE;
+}
+
+int
+w_cmd(File *f, Cmd *cp)
+{
+	int fseq;
+
+	fseq = f->seq;
+	if(getname(f, cp->ctext, FALSE)==0)
+		error(Enoname);
+	if(fseq == seq)
+		error_s(Ewseq, genc);
+	writef(f);
+	return TRUE;
+}
+
+int
+x_cmd(File *f, Cmd *cp)
+{
+	if(cp->re)
+		looper(f, cp, cp->cmdc=='x');
+	else
+		linelooper(f, cp);
+	return TRUE;
+}
+
+int
+X_cmd(File *f, Cmd *cp)
+{
+	USED(f);
+	filelooper(cp, cp->cmdc=='X');
+	return TRUE;
+}
+
+int
+plan9_cmd(File *f, Cmd *cp)
+{
+	plan9(f, cp->cmdc, cp->ctext, nest);
+	return TRUE;
+}
+
+int
+eq_cmd(File *f, Cmd *cp)
+{
+	int charsonly;
+
+	switch(cp->ctext->n){
+	case 1:
+		charsonly = FALSE;
+		break;
+	case 2:
+		if(cp->ctext->s[0]=='#'){
+			charsonly = TRUE;
+			break;
+		}
+	default:
+		SET(charsonly);
+		error(Enewline);
+	}
+	printposn(f, charsonly);
+	return TRUE;
+}
+
+int
+nl_cmd(File *f, Cmd *cp)
+{
+	Address a;
+
+	if(cp->addr == 0){
+		/* First put it on newline boundaries */
+		addr = lineaddr((Posn)0, f->dot, -1);
+		a = lineaddr((Posn)0, f->dot, 1);
+		addr.r.p2 = a.r.p2;
+		if(addr.r.p1==f->dot.r.p1 && addr.r.p2==f->dot.r.p2)
+			addr = lineaddr((Posn)1, f->dot, 1);
+		display(f);
+	}else if(downloaded)
+		moveto(f, addr.r);
+	else
+		display(f);
+	return TRUE;
+}
+
+int
+cd_cmd(File *f, Cmd *cp)
+{
+	USED(f);
+	cd(cp->ctext);
+	return TRUE;
+}
+
+int
+append(File *f, Cmd *cp, Posn p)
+{
+	if(cp->ctext->n>0 && cp->ctext->s[cp->ctext->n-1]==0)
+		--cp->ctext->n;
+	if(cp->ctext->n>0)
+		loginsert(f, p, cp->ctext->s, cp->ctext->n);
+	f->ndot.r.p1 = p;
+	f->ndot.r.p2 = p+cp->ctext->n;
+	return TRUE;
+}
+
+int
+display(File *f)
+{
+	Posn p1, p2;
+	int np;
+	char *c;
+
+	p1 = addr.r.p1;
+	p2 = addr.r.p2;
+	if(p2 > f->_.nc){
+		fprint(2, "bad display addr p1=%ld p2=%ld f->_.nc=%d\n", p1, p2, f->_.nc); /*ZZZ should never happen, can remove */
+		p2 = f->_.nc;
+	}
+	while(p1 < p2){
+		np = p2-p1;
+		if(np>BLOCKSIZE-1)
+			np = BLOCKSIZE-1;
+		bufread(f, p1, genbuf, np);
+		genbuf[np] = 0;
+		c = Strtoc(tmprstr(genbuf, np+1));
+		if(downloaded)
+			termwrite(c);
+		else
+			Write(1, c, strlen(c));
+		free(c);
+		p1 += np;
+	}
+	f->dot = addr;
+	return TRUE;
+}
+
+void
+looper(File *f, Cmd *cp, int xy)
+{
+	Posn p, op;
+	Range r;
+
+	r = addr.r;
+	op= xy? -1 : r.p1;
+	nest++;
+	compile(cp->re);
+	for(p = r.p1; p<=r.p2; ){
+		if(!execute(f, p, r.p2)){ /* no match, but y should still run */
+			if(xy || op>r.p2)
+				break;
+			f->dot.r.p1 = op, f->dot.r.p2 = r.p2;
+			p = r.p2+1;	/* exit next loop */
+		}else{
+			if(sel.p[0].p1==sel.p[0].p2){	/* empty match? */
+				if(sel.p[0].p1==op){
+					p++;
+					continue;
+				}
+				p = sel.p[0].p2+1;
+			}else
+				p = sel.p[0].p2;
+			if(xy)
+				f->dot.r = sel.p[0];
+			else
+				f->dot.r.p1 = op, f->dot.r.p2 = sel.p[0].p1;
+		}
+		op = sel.p[0].p2;
+		cmdexec(f, cp->ccmd);
+		compile(cp->re);
+	}
+	--nest;
+}
+
+void
+linelooper(File *f, Cmd *cp)
+{
+	Posn p;
+	Range r, linesel;
+	Address a, a3;
+
+	nest++;
+	r = addr.r;
+	a3.f = f;
+	a3.r.p1 = a3.r.p2 = r.p1;
+	for(p = r.p1; p<r.p2; p = a3.r.p2){
+		a3.r.p1 = a3.r.p2;
+/*pjw		if(p!=r.p1 || (linesel = lineaddr((Posn)0, a3, 1)).r.p2==p)*/
+		if(p!=r.p1 || (a = lineaddr((Posn)0, a3, 1), linesel = a.r, linesel.p2==p)){
+			a = lineaddr((Posn)1, a3, 1);
+			linesel = a.r;
+		}
+		if(linesel.p1 >= r.p2)
+			break;
+		if(linesel.p2 >= r.p2)
+			linesel.p2 = r.p2;
+		if(linesel.p2 > linesel.p1)
+			if(linesel.p1>=a3.r.p2 && linesel.p2>a3.r.p2){
+				f->dot.r = linesel;
+				cmdexec(f, cp->ccmd);
+				a3.r = linesel;
+				continue;
+			}
+		break;
+	}
+	--nest;
+}
+
+void
+filelooper(Cmd *cp, int XY)
+{
+	File *f, *cur;
+	int i;
+
+	if(Glooping++)
+		error(EnestXY);
+	nest++;
+	settempfile();
+	cur = curfile;
+	for(i = 0; i<tempfile.nused; i++){
+		f = tempfile.filepptr[i];
+		if(f==cmd)
+			continue;
+		if(cp->re==0 || filematch(f, cp->re)==XY)
+			cmdexec(f, cp->ccmd);
+	}
+	if(cur && whichmenu(cur)>=0)	/* check that cur is still a file */
+		current(cur);
+	--Glooping;
+	--nest;
+}