Initial revision
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;
+}