loksh-noxz

[fork] a Linux port of OpenBSD's ksh
git clone https://noxz.tech/git/loksh-noxz.git
Log | Files | README

commit: ff72942b698b7ada9aa35ec0c77f80d47d222b0d
parent: 
author: Chris Noxz <chris@noxz.tech>
date:   Tue, 8 Oct 2019 21:01:41 +0200
Initial commit of fork of loksh (20190502-774e6d2)
A.travis.yml7+
ACONTRIBUTORS129+
ADockerfile3+
ALEGAL12+
AMakefile41+
ANOTES287+
APROJECTS66+
AREADME22+
AREADME.md21+
Aalloc.c128+
Ac_ksh.c1428+++++
Ac_sh.c887++++
Ac_test.c555++
Ac_test.h53+
Ac_ulimit.c194+
Acharclass.h29+
Aci.sh16+
Aconfig.h28+
Aedit.c911++++
Aedit.h56+
Aemacs.c2160++++++++
Aeval.c1322+++++
Aexec.c1450+++++
Aexpand.h106+
Aexpr.c601+++
Ahistory.c869+++
Aio.c461++
Ajobs.c1575++++++
Aksh.15600++++++++++++++++++++
Alex.c1668++++++
Alex.h121+
Amail.c207+
Amain.c824+++
Amisc.c1147++++
Apath.c266+
Areallocarray.c38+
Ash.12241++++++++
Ash.h606+++
Ashf.c998++++
Ashf.h79+
Astdlib.h45+
Astring.h45+
Astrlcat.c55+
Astrlcpy.c50+
Astrtonum.c65+
Asyn.c901++++
Asys/queue.h534++
Asys/time.h79+
Atable.c249+
Atable.h195+
Atrap.c440++
Atree.c687+++
Atree.h145+
Atty.c59+
Atty.h21+
Aunvis.c284+
Avar.c1232+++++
Aversion.c10+
Avi.c2229++++++++
Avis.c241+
Avis.h88+
61 files changed, 34866 insertions(+)
diff --git a/.travis.yml b/.travis.yml
@@ -0,0 +1,7 @@
+language: minimal
+
+services:
+  - docker
+
+script:
+  - docker run -e CI -w /root/loksh -v `pwd`:/root/loksh dimkr/loksh:latest ./ci.sh
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
@@ -0,0 +1,129 @@
+$OpenBSD: CONTRIBUTORS,v 1.11 2019/01/25 00:19:25 millert Exp $
+
+This is a partial history of this shell gleened from old change logs and
+readmes (most of which are still in the misc directory) and the source
+code.  Hopefully it is correct and no contributors have been left out
+(file a bug report if you spot a problem :-)).
+
+Release history:
+    * Eric Gisin (egisin@math.uwaterloo.ca), created pdksh, using
+      Charles Forsyth's public domain V7 shell as a base; also used parts
+      of the BRL shell (written by Doug A Gwyn, Doug Kingston, Ron Natalie,
+      Arnold Robbins, Lou Salkind, and others?, circa '87; the parts used in
+      pdksh included getopts, test builtin, ulimit, tty setting/getting, emacs
+      editing, and job control; the test builtin was based on code by Erik
+      Baalbergen).
+      '87..'89 ?
+      Released versions: .. 3.2
+    * John R MacMillan (@yonge.csri.toronto.edu:chance!john@sq.sq.com)
+      takes over as maintainer
+      dates?
+      Released versions: 3.3 (?)
+    * Simon J. Gerraty (sjg@zen.void.oz.au) takes over as maintainer
+      Nov '91..July '94 ?
+      Released versions: 4.0 .. 4.9
+    * Michael Rendell (michael@cs.mun.ca) takes over as maintainer
+      July, 1994
+      Released versions: 5.0 .. 5.2
+
+Major contributions:
+    * John R MacMillan (@yonge.csri.toronto.edu:chance!john@sq.sq.com), ?:
+      cleaned up configuration, many bug fixes (see misc/Changes.jrm).
+    * Simon Gerraty, (sjg@zen.void.oz.au), Nov '91..?: much improved emacs mode
+      ala at&t ksh, 386bsd port, sigaction routines for non-POSIX systems
+      (see misc/ChangeLog.sjg and misc/ReadME.sjg).
+    * Peter Collinson (pc@hillside.co.uk), July '92: added select, at&t ksh
+      style history file, original csh-style {} globbing, BSD/386 port,
+      misc bug fixes.
+    * Larry Bouzane (larry@compusult.nf.ca), Mar '89..'93: re-wrote job control,
+      added async job notification, added CDPATH and other cd fixes, misc bug
+      fixes.
+    * John Rochester (jr@cs.mun.ca), '87: wrote vi command line editor; various
+      bug fixes/enhancements.
+    * Jeff Sparkes (jsparkes@bnr.ca), Mar '89..Mar '90: added arrays,
+      merged John Rochester's vi code into pdksh, misc bug fixes.
+    * Michael Haardt (u31b3hs@POOL.Informatik.RWTH-Aachen.DE), Sept '94:
+      organized man page, filled in many of its copious blank spots; added
+      KSH ifdefs.
+    * Dale DePriest (daled@cadence.com): ported to OS/2 (initially based on
+      port of pdksh4.9 to OS/2 by Kai Rommel (rommel@ars.muc.de)); maintains
+      OS/2 port; misc bug fixes.
+
+Other contributors:
+    * Piercarlo Grandi (pcg@aber.ac.uk), Dec '93: fixes for linux port
+    * Neil Smithline (Neil.Smithline@eng.sun.com), Aug '92: emacs-style
+      filename completion.
+    * Mike Jetzer [mlj] (jetzer@studsys.mscs.mu.edu), ?;Nov '94: fixes for vi
+      mode (see misc/Changes.mlj), added v to vi, fixes for history; fixed
+      command redoing in vi; fixes to vi globbing.
+    * Robert J Gibson: mailbox checking code that was adapted for pdksh by
+      John R. MacMillan.
+    * ? (guy@demon.co.uk), ?: promptlen() function.
+    * J.T. Conklin (jtc@cygnus.com): POSIXized test builtin; miscellaneous
+      fixes/enhancements.
+    * Sean Hogan (sean@neweast.ca): fixes for ICS 3.0 Unix, found and helped
+      fix numerous problems.
+    * Gordan Larson (hoh@approve.se): fix to compile sans VI, ksh.1 typo.
+    * Thomas Gellekum (thomas@ghpc8.ihf.rwth-aachen.de): fixes for Makefile
+      typos, fixed CLK_TCK for FreeBSD, man page fixes.
+    * Ed Ferguson (Ed.Ferguson@dseg.ti.com): fix to compile sans VI.
+    * Brian Campbell (brianc@qnx.com): fixes to compile under QNX and
+      to compile with dmake.
+    * (guy@netapp.com), Oct '94: patch to use gmacs flag.
+    * Andrew Moore (alm@netcom.com): reported many bugs, fixes.
+    * William Bader (wbader@CSEE.Lehigh.Edu): fix to compile on SCO Unix
+      (struct winsize).
+    * Mike Long (mike.long@analog.com): makefile fix - use $manext, not 1.
+    * Art Mills (aem@hpbs9162.bio.hp.com): bug fix for vi file completion in
+      command mode.
+    * Tory Bollinger (tboll@authstin.ibm.com): allow ~ in vi mode to take
+      a count.
+    * Frank Edwards (<crash@azhrei.EEC.COM>): added macros to vi (@char).
+    * Fritz Heinrichmeyer (<Fritz.Heinrichmeyer@FernUni-Hagen.de>): fixes
+      to allow compile under Linux 1.4.3.
+    * Gabor Zahemszky (<zgabor@CoDe.hu>): SVR3_PGRP vs SYSV_PGRP, many
+      bug reports and man page fixes.
+    * Dave Kinchlea (<kinch@julian.uwo.ca>): DEFAULT_ENV patches.
+    * Paul Borman (<prb@bsdi.com>): j_exit: send HUP, then CONT; HUP fg process.
+    * DaviD W. Sanderson (<dws@ssec.wisc.edu>): patches to allow { .. } instead
+      of in .. esac in case statements.
+    * ? (<ra@rhi.hi.is>): partial patches to handle SIGWINCH for command line
+      editing.
+    * Jason Tyler (<jason@nc.bhpese.oz.au>): fixes for bugs in fc.
+    * Stefan Dalibor (<Stefan.Dalibor@informatik.uni-erlangen.de>): fix for
+      COLUMNS never being set in x_init().
+    * Arnon Kanfi (<arnon@gilly.datatools.com>): fix for prompt.
+    * Marc Olzheim (<marcolz@stack.nl>): patches to ifdef KSH the mail check
+      code and aliases; enum patches for old K&R compilers; handle missing dup2.
+    * Lars Hecking (<lhecking@nmrc.ucc.ie>): fixes so shell compiles as sh
+      again.
+    * Bill Kish (<kish@browncow.com>): added prompt delimiter hack for
+      hidden characters (eg, escape codes).
+    * Andrew S. Townley (<atownley@informix.com>): fixes for NeXT machines:
+      get a controlling if one needed, use correct profile.
+    * Eric J. Chet (<ejc@bazzle.com>): fix for core dump in . (quitenv() called
+      too soon).
+    * Greg A. Woods <woods@most.weird.com>: fix to make ^[_ in emacs work
+      as in at&t ksh.
+    * George Robbins <grr@shandakor.tharsis.com>: fix for sh mode to
+      keep exec'd file descriptors open.
+    * George White <gwhite@bodnext.bio.dfo.ca>: fix here-doc problem under OS/2
+      (memory allocated incorrectly).
+    * David E. Wexelblat <dwex@DataFocus.com>: fix to avoid memory overrun
+      in aresize(); fix to not print un-named options.
+    * Clifford Wolf (<clifford@clifford.at>): fix memory overrun in aresize();
+      fixed sys_siglist[] problem.
+    * Theo de Raadt (<deraadt@cvs.openbsd.org>): allow ". /dev/null".
+    * Eric Youngdale (<ericy@datafocus.com>): flag field incorrectly changed
+      in exec.c(flushcom).
+    * Todd. C Miller (Todd C. Miller <millert@openbsd.org>): fix
+      for coredump in jobs.
+    * Kevin Schoedel <schoedel@kw.igs.net>: fix for word location in file
+      completion.
+    * Martin Lucina <mato@kotelna.sk>: fix for argument parsing in exit command,
+      fix for KSH_CHECK_H_TYPE.
+    * Mark Funkenhauser <mark@interix.com>: added $LINENO support.
+    * Corinna Vinschen <Corinna@Vinschen.de> and Steven Hein <ssh@sgi.com>:
+      port to cyngin environment on win95/winnt.
+    * Martin Dalecki <dalecki@cs.net.pl>: changes for 8 bit emacs mode.
+    * Dave Hillman <daveh@gte.net>: patch for bug in test -nt.
diff --git a/Dockerfile b/Dockerfile
@@ -0,0 +1,3 @@
+FROM dimkr/c-dev
+
+RUN apt-get -y --no-install-recommends install pkg-config libncurses5-dev
diff --git a/LEGAL b/LEGAL
@@ -0,0 +1,12 @@
+$OpenBSD: LEGAL,v 1.2 2003/07/17 20:59:43 deraadt Exp $
+
+pdksh is provided AS IS, with NO WARRANTY, either expressed or implied.
+
+The vast majority of the code that makes pdksh is in the public domain.
+The exceptions are:
+	sigact.c and sigact.h
+		[REMOVED]
+	aclocal.m4
+		[REMOVED]
+
+That's it.  Short and simple.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,41 @@
+CC ?= cc
+CFLAGS ?= -O2
+override CFLAGS += -isystem . -D_GNU_SOURCE -DEMACS -DVI -std=gnu99 -Wall -Wno-format-security -Wno-pointer-sign
+LDFLAGS ?=
+DESTDIR ?= /
+PREFIX ?= /usr
+BIN_DIR ?= $(PREFIX)/bin
+BIN_NAME ?= ksh
+MAN_DIR ?= $(PREFIX)/share/man
+DOC_DIR ?= $(PREFIX)/share/doc/loksh
+
+NCURSES_CFLAGS = $(shell pkg-config --cflags ncurses)
+NCURSES_LDFLAGS = $(shell pkg-config --libs ncurses)
+
+OBJECTS = alloc.o c_ksh.o c_sh.o c_test.o c_ulimit.o edit.o emacs.o eval.o \
+          exec.o expr.o history.o io.o jobs.o lex.o mail.o main.o misc.o \
+          path.o shf.o syn.o table.o trap.o tree.o tty.o var.o version.o vi.o \
+          vis.o unvis.o strlcpy.o strlcat.o reallocarray.o strtonum.o
+HEADERS = c_test.h charclass.h config.h edit.h expand.h ksh_limval.h lex.h \
+          proto.h sh.h shf.h table.h tree.h tty.h
+
+all: ksh
+
+%.o: %.c $(HEADERS)
+	$(CC) -c -o $@ $< $(CFLAGS) $(NCURSES_CFLAGS)
+
+ksh: $(OBJECTS)
+	$(CC) -o $@ $^ $(LDFLAGS) $(NCURSES_LDFLAGS)
+
+clean:
+	rm -f $(BIN_NAME) *.o
+
+install: all
+	install -v -D -m 755 ksh $(DESTDIR)/$(BIN_DIR)/$(BIN_NAME)
+	install -v -D -m 644 ksh.1 $(DESTDIR)/$(MAN_DIR)/man1/$(BIN_NAME).1
+	install -v -m 644 sh.1 $(DESTDIR)/$(MAN_DIR)/man1/sh.1
+	install -v -D -m 644 README.md $(DESTDIR)/$(DOC_DIR)/README.md
+	install -v -m 644 README $(DESTDIR)/$(DOC_DIR)/README
+	install -v -m 644 CONTRIBUTORS $(DESTDIR)/$(DOC_DIR)/CONTRIBUTORS
+	install -v -m 644 PROJECTS $(DESTDIR)/$(DOC_DIR)/PROJETCS
+	install -v -m 644 LEGAL $(DESTDIR)/$(DOC_DIR)/LEGAL
diff --git a/NOTES b/NOTES
@@ -0,0 +1,287 @@
+$OpenBSD: NOTES,v 1.16 2018/01/12 14:20:57 jca Exp $
+
+General features of at&t ksh88 that are not (yet) in pdksh:
+    - exported aliases and functions (not in ksh93).
+    - set -t.
+    - signals/traps not cleared during functions.
+    - trap DEBUG, local ERR and EXIT traps in functions.
+    - ERRNO parameter.
+    - read/select aren't hooked in to the command line editor
+    - the last command of a pipeline is not run in the parent shell
+
+Known bugs (see also PROJECTS files):
+    Variable parsing, Expansion:
+	- some specials behave differently when unset (eg, IFS behaves like
+	  " \t\n") others lose their special meaning.  IFS/PATH taken care of,
+	  still need to sort out some others (eg, TMOUT).
+    Parsing,Lexing:
+	- line numbers in errors are wrong for nested constructs.  Need to
+	  keep track of the line a command started on (can use for LINENO
+	  parameter as well).
+	- a $(..) expression nested inside double quotes inside another $(..)
+	  isn't parsed correctly (eg, $(echo "foo$(echo ")")") )
+    Commands,Execution:
+	- setting special parameters that have side effects when
+	  changed/restored (ie, HISTFILE, OPTIND, RANDOM) in front
+	  of a command (eg, HISTFILE=/foo/bar echo hi) affects the parent
+	  shell.  Note that setting other (not so special) parameters
+	  does not affect the parent shell.
+	- `echo hi | exec cat -n' causes at&t to exit, `exec echo hi | cat -n'
+	  does not.  pdksh exits for neither.  Don't think POSIX requires
+	  an exit, but not sure.
+	- `echo foo | read bar; echo $bar' prints foo in at&t ksh, nothing
+	  in pdksh (ie, the read is done in a separate process in pdksh).
+
+Known problems not caused by ksh:
+    - after stoping a job, emacs/vi is not re-entered.  Hitting return
+      prints the prompt and everything is fine again.  Problem (often
+      involving a pager like less) is related to order of process
+      scheduling (shell runs before `stop'ed (sub) processes have had a chance
+      to clean up the screen/terminal).
+
+Known differences between pdksh & at&t ksh (that may change)
+    - vi:
+	- `^U': at&t: kills only what has been inserted, pdksh: kills to
+	  start of line
+    - at&t ksh login shells say "Warning: you have running jobs" if you
+      try to exit when there are running jobs.  An immediate second attempt
+      to exit will kill the jobs and exit.  pdksh does not print a warning,
+      nor does it kill running jobs when it exits (it does warn/kill for
+      stopped jobs).
+    - TMOUT: at&t prints warning, then waits another 60 seconds.  If on screwed
+      up serial line, the output could cause more input, so pdksh just
+      prints a message and exits.  (Also, in at&t ksh, setting TMOUT has no
+      effect after the sequence "TMOUT=60; unset TMOUT", which could be
+      useful - pdksh may do this in the future).
+    - in pdksh, if the last command of a pipeline is a shell builtin, it is
+      not executed in the parent shell, so "echo a b | read foo bar" does not
+      set foo and bar in the parent shell (at&t ksh will).
+      This may get fixed in the future, but it may take a while.
+    - in pdksh, set +o lists the options that are currently set, in at&t ksh
+      it is the same as set -o.
+    - in pdksh emacs mode, ^T does what gnu emacs does, not what at&t ksh
+      does.
+    - in ksh93, `. name' calls a function (defined with function) with POSIX
+      semantics (instead of ksh semantics).  in pdksh, . does not call
+      functions.
+    - test: "test -f foo bar blah" is the same as "test -f foo" (the extra
+      arguments, of which there must be at least 2, are ignored) - pdksh
+      generates an error message (unexpected operator/operand "bar") as it
+      should.  Sometimes used to test file globs (e.g., if test -f *.o; ...).
+    - if the command 'sleep 5 && /bin/echo blah' is run interactively and
+      is the sleep is stopped (^Z), the echo is run immediately in pdksh.
+      In at&t ksh, the whole thing is stopped.
+    - LINENO:
+	- in ksh88 variable is always 1 (can't be changed) in interac mode;
+	  in pdksh it changes.
+	- Value of LINENO after it has been set by the script in one file
+	  is bizarre when used in another file.
+
+Known differences between pdksh & at&t ksh (that are not likely to change)
+    - at&t ksh seems to catch or ignore SIGALRM - pdksh dies upon receipt
+      (unless it's traped of course)
+    - typeset:
+	- at&t ksh overloads -u/-l options: for integers, means unsigned/long,
+	  for strings means uppercase/lowercase; pdksh just has the
+	  upper/lower case (which can be useful for integers when base > 10).
+	  unsigned/long really should have their own options.
+	- at&t ksh can't have justified integer variables
+	  (eg, typeset -iR5 j=10), pdksh can.
+	- in pdksh, number arguments for -L/-R/-Z/-i must follow the option
+	  character, at&t allows it at the end of the option group (eg,
+	  at&t ksh likes "typeset -iu5 j", pdksh wants "typeset -i5 -u j"
+	  or "typeset -ui5 j").  Also, pdksh allows "typeset -i 5 j" (same
+	  as "typeset -i5 j"), at&t ksh does not allow this.
+	- typeset -R: pdksh strips trailing space type characters (ie,
+	  uses isspace()), at&t ksh only skips blanks.
+	- at&t ksh allows attributes of read-only variables to be changed,
+	  pdksh allows only the export attribute to be set.
+    - (some) at&t ksh allows set -A of readonly variables, pdksh does not.
+    - at&t ksh allows command assignments of readonly variables (eg, YY=2 cat),
+      pdksh does not.
+    - at&t ksh does not exit scripts when an implicit assignment to an integer
+      variable fails due to an expression error: eg,
+		echo 2+ > /tmp/x
+		unset x; typeset -i x
+		read x < /tmp/x
+		echo still here
+      prints an error and then prints "still here", similarly for
+		unset x; typeset -i x
+		set +A x 1 2+ 3
+		echo still here
+      and
+		unset x y; typeset -i x y; set +A y 10 20 30
+		set +A x 1 1+y[2+] 3
+		echo still here
+      pdksh exits a script in all the above cases. (note that both shells
+      exit for:
+		unset x; typeset -i x
+		for x in 1 2+ 3; do echo x=$x; done
+		echo still here
+      ).
+    - at&t ksh seems to allow function calls inside expressions
+      (eg, typeset -i x='y(2)') but they do not seem to be regular functions
+      nor math functions (eg, pow, exp) - anyone known anything about this?
+    - `set -o nounset; unset foo; echo ${#foo}`: at&t ksh prints 0; pdksh
+      generates error.  Same for ${#foo[*]} and ${#foo[@]}.
+    - . file: at&t ksh parses the whole file before executing anything,
+      pdksh executes as it parses.  This means aliases defined in the file
+      will affect how pdksh parses the file, but won't affect how at&t ksh
+      parses the file.  Also means pdksh will not parse statements occurring
+      after a (executed) return statement.
+    - a return in $ENV in at&t ksh will cause the shell to exit, while in
+      pdksh it will stop executing the script (this is consistent with
+      what a return in .profile does in both shells).
+    - at&t ksh does file globbing for `echo "${foo:-"*"}"`, pdksh does not
+      (POSIX would seem to indicate pdksh is right).
+    - at&t ksh thinks ${a:##foo} is ok, pdksh doesn't.
+    - at&t does tilde expansion on here-document delimiters, pdksh does
+      not.  eg.
+	$ cat << ~michael
+	~michael
+	$
+      works for pdksh, not for at&t ksh (POSIX seems to agree with pdksh).
+    - in at&t ksh, tracked aliases have the export flag implicitly set
+      and tracked aliases and normal aliases live in the same name space
+      (eg, "alias" will list both tracked and normal aliases).
+      in pdksh, -t does not imply -x (since -x doesn't do anything yet), and
+      tracked/normal aliases live in separate name spaces.
+      in at&t ksh, alias accepts + options (eg, +x, +t) - pdksh does not.
+      in pdksh, alias has a -d option to allow examination/changing of
+      cached ~ entries, also unalias has -d and -t options (unalias -d
+      is useful if the ~ cache gets out of date - not sure how at&t deals
+      with this problem (it does cache ~ entries)).
+    - at&t ksh will stop a recursive function after about 60 calls; pdksh
+      will not since the limit is arbitrary and can't be controlled
+      by the user (hit ^C if you get in trouble).
+    - the wait command (with and without arguments) in at&t ksh will wait for
+      stopped jobs when job control is enabled.  pdksh doesn't.
+    - at&t ksh automatically sets the bgnice option for interactive shells;
+      pdksh does not.
+    - in at&t ksh, "eval `false`; echo $?" prints 1, pdksh prints 0 (which
+      is what POSIX says it should).  Same goes for "wait `false`; echo $?".
+      (same goes for "set `false`; echo $?" if posix option is set - some
+      scripts that use the old getopt depend on this, so be careful about
+      setting the posix option).
+    - in at&t ksh, print -uX and read -uX are interrperted as -u with no
+      argument (defaults to 1 and 0 respectively) and -X (which may or
+      may not be a valid flag).  In pdksh, -uX is interpreted as file
+      descriptor X.
+    - in at&t ksh, some signals (HUP, INT, QUIT) cause the read to exit, others
+      (ie, everything else) do not.  When it does cause exiting, anything read
+      to that point is used (usually an empty line) and read returns with 0
+      status.  pdksh currently does similar things, but for TERM as well and
+      the exit status is 128+<signal-number> - in future, pdksh's read will
+      do this for all signals that are normally fatal as required by POSIX.
+      (POSIX does not require the setting of variables to null so applications
+      shouldn't rely on this).
+    - in pdksh, ! substitution done before variable substitution; in at&t ksh
+      it is done after substitution (and therefore may do ! substitutions on
+      the result of variable substitutions).  POSIX doesn't say which is to be
+      done.
+    - pwd: in at&t ksh, it ignores arguments; in pdksh, it complains when given
+      arguments.
+    - the at&t ksh does not do command substition on PS1, pdksh does.
+    - ksh93 allows ". foo" to run the function foo if there is no file
+      called foo (go figure).
+    - field splitting (IFS): ksh88/ksh93 strip leading non-white space IFS
+      chars, pdksh (and POSIX, I think) leave them intact. e.g.
+	$ IFS="$IFS:"; read x; echo "<$x>"
+	::
+      prints "<>" in at&t ksh, "<::>" in pdksh.
+    - command completion: at&t ksh will do completion on a blank line (matching
+      all commands), pdksh does not (as this isn't very useful - use * if
+      you really want the list).
+    - co-processes: if ksh93, the write portion of the co-process output is
+      closed when the most recently started co-process exits. pdksh closes
+      it when all the co-processes using it have exited.
+    - pdksh accepts empty command lists for while and for statements, while
+      at&t ksh (and sh) don't.  Eg., pdksh likes
+	while false ; do done
+      but ksh88 doesn't like it.
+    - pdksh bumps RANDOM in parent after a fork, at&t ksh bumps it in both
+      parent and child:
+	RANDOM=1
+	echo child: `echo $RANDOM`
+	echo parent: $RANDOM
+      will produce "child: 16838 parent: 5758" in pdksh, while at&t ksh
+      will produce "child: 5758 parent: 5758".
+
+Oddities in ksh (pd & at&t):
+    - array references inside (())/$(()) are strange:
+	  $(( x[2] )) does the expected, $(( $x[2] )) doesn't.
+    - `typeset -R3 X='x '; echo "($X)"` produces (  x) - trailing
+      spaces are stripped.
+    - typeset -R turns off Z flag.
+    - both shells have the following mis-feature:
+	$ x='function xx {
+		cat -n <<- EOF
+		here we are in xx
+		EOF
+		}'
+	$ (eval "$x"; (sleep 2; xx) & echo bye)
+	[1] 1234
+	bye
+	$ xx: /tmp/sh1234.1: cannot open
+    - bizarre special handling of alias/export/readonly/typeset arguments
+	$ touch a=a; typeset a=[ab]; echo "$a"
+	a=[ab]
+	$ x=typeset; $x a=[ab]; echo "$a"
+	a=a
+	$
+    - both ignore SIGTSTP,SIGTTIN,SIGTTOU in exec'd processes when talking
+      and not monitoring (at&t ksh kind of does this).  Doesn't really make
+      sense.
+      (Note that ksh.att -ic 'set +m; check-sigs' shows TSTP et al aren't
+       ignored, while ksh.att -ic 'set +m^J check-sigs' does... very strange)
+    - when tracing (set -x), and a command's stderr is redirected, the trace
+      output is also redirected. so "set -x; echo foo 2> /tmp/O > /dev/null"
+      will create /tmp/foo with the lines "+ > /dev/null" and "+ echo foo".
+    - undocumented at&t ksh88, documented in ksh93: FPATH is searched
+      after PATH if no executable is found, even if typeset -uf wasn't used.
+
+POSIX sh questions (references are to POSIX 1003.2-1992)
+	- arithmetic expressions: how are empty expressions treated?
+	  (eg, echo $((  ))).  at&t ksh (and now pdksh) echo 0.
+	  Same question goes for `test "" -eq 0' - does this generate an error
+	  or, if not, what is the exit code?
+	- if a signal is received during the execution of a built-in,
+	  does the builtin command exit or the whole shell?
+	- is it legal to execute last command of pipeline in current
+	  execution environment (eg, can "echo foo | read bar" set
+	  bar?)
+	- what action should be taken if there is an error doing a dup due
+	  to system limits (eg, not enough file destriptors): is this
+	  a "redirection error" (in which case a script will exit iff the
+	  error occured while executing a special built-in)?
+	  IMHO, shell should exit script.  Couldn't find a blanket statement
+	  like "if shell encounters an unexpected system error, it shall
+	  exit non-interactive scripts"...
+
+POSIX sh bugs (references are to POSIX 1003.2-1992)
+	- in vi insert mode, ^W deletes to beginning of line or to the first
+	  blank/punct character (para at line 9124, section 3).  This means
+	  "foo     ^W" will do nothing.  This is inconsistent with the vi
+	  spec, which says delete preceding word including and interceding
+	  blanks (para at line 5189, section 5).
+	- parameter expansion, section 3.6.2, line 391: `in each case that a
+	  value of word is needed (..), word shall be subjected to tilde
+	  expansion, parameter expansion, ...'.  Various expansions should not
+	  be performed if parameter is in double quotes.
+	- the getopts description says assigning OPTIND a value other than 1
+	  produces undefined results, while the rationale for getopts suggests
+	  saving/restoring the OPTIND value inside functions (since POSIX
+	  functions don't do the save/restore automatically).  Restoring
+	  OPTIND is kind of dumb since getopts may have been in the middle
+	  of parsing a group of flags (eg, -abc).
+	- `...` definition (3.6.3) says nothing about backslash followed by
+	  a newline, which sh and at&t ksh strip out completely.  e.g.,
+		$ show-args `echo 'X
+		Y'`
+		Number of args: 1
+			1: <XY>
+		$
+	  POSIX would indicate the backslash-newline would be preserved.
+	- does not say how "cat << ''" is to be treated (illegal, read 'til
+	  blank line, or read 'til eof).  at&t ksh reads til eof, bourne shell
+	  reads 'til blank line.  pdksh reads 'til blank line.
diff --git a/PROJECTS b/PROJECTS
@@ -0,0 +1,66 @@
+$OpenBSD: PROJECTS,v 1.9 2018/01/08 12:08:17 jca Exp $
+
+Things to be done in pdksh (see also the NOTES file):
+
+    * builtin utilities:
+      pdksh has most if not all POSIX/at&t ksh builtins, but they need to
+      be checked that they conform to POSIX/at&t manual.  Part of the
+      process is changing the builtins to use the ksh_getopt() routine.
+
+      The following builtins, which are defined by POSIX, haven't been
+      examined:
+	eval
+
+      The first pass has been done on the following commands:
+	. : alias bg break cd continue echo exec exit export false fc fg
+	getopts jobs kill pwd read readonly return set shift time trap true
+	umask unalias unset wait
+
+      The second pass (ie, believed to be completely POSIX) has been done on
+      the following commands:
+	test
+
+      (ulimit also needs to be examined to check that it fits the posix style)
+
+    * trap code
+	* add the DEBUG trap.
+	* fix up signal handling code.  In particular, fatal vs tty signals,
+	  have signal routine to call to check for pending/fatal traps, etc.
+
+    * lexing
+      the lexing may need a re-write since it currently doesn't parse $( .. ),
+      $(( .. )), (( ... )) properly.
+	* need to ignore contents of quoted strings (and escaped chars?)
+	  inside $( .. ) and $(( .. )) when counting parentheses.
+	* need to put bounds check on states[] array (if it still exists after
+	  the re-write)
+
+    * variables
+	* The "struct tbl" that is currently used for variables needs work since
+	  more information (eg, array stuff, fields) are needed for variables
+	  but not for the other things that use "struct tbl".
+	* Arrays need to be implemented differently: currently does a linear
+	  search of a linked list to find element i; the linked list is not
+	  freed when a variable is unset.
+
+    * functions
+      finish the differences between function x and x(): trap EXIT, traps
+      in general, treatment of OPTIND/OPTARG,
+
+    * history
+	* Add multiline knowledge
+	* bring history code up to POSIX standards (see POSIX description
+	  of fc, etc.).
+
+    * miscellaneous
+	* POSIX specifies what happens when various kinds of errors occur
+	  in special built-ins commands vs regular commands (builtin or
+	  otherwise) (see POSIX.2:3.8.1).  Some of this has been taken
+	  care of, but more needs doing.
+
+	* remove static limits created by fixed sized arrays
+	  (eg, ident[], heres[], PATH, buffer size in emacs/vi code)
+
+	* merge the emacs and vi code (should reduce the size of the shell and
+	  make maintenance easier); handle SIGWINCH while editing a line.
+	  [John Rochester is working on the merge]
diff --git a/README b/README
@@ -0,0 +1,22 @@
+$OpenBSD: README,v 1.16 2017/05/11 20:17:17 jmc Exp $
+
+Last updated Jul '99 for pdksh-5.2.14.
+
+PDksh is a mostly complete AT&T ksh look-alike (see NOTES file for a list
+of things not supported).  Work is mostly finished to make it fully
+compatible with both POSIX and AT&T ksh (when the two don't conflict).
+
+PDksh was being maintained by Michael Rendell (michael@cs.mun.ca),
+who took over from Simon J. Gerraty (sjg@zen.void.oz.au) at the latter's
+suggestion.
+
+Files of interest:
+	CONTRIBUTORS	short history of pdksh, people who contributed, etc.
+	NOTES		lists of known bugs in pdksh, at&t ksh, and posix.
+	PROJECTS	list of things that need to be done in pdksh.
+	LEGAL		A file detailing legal issues concerning pdksh.
+
+
+BTW, THE MOST FREQUENTLY REPORTED BUG IS
+	echo hi | read a; echo $a	# Does not print hi
+I'm aware of this and there is no need to report it.
diff --git a/README.md b/README.md
@@ -0,0 +1,21 @@
+```
+ _       _        _
+| | ___ | | _____| |__
+| |/ _ \| |/ / __| '_ \
+| | (_) |   <\__ \ | | |
+|_|\___/|_|\_\___/_| |_|
+```
+
+[![Build Status](https://travis-ci.org/dimkr/loksh.svg?branch=master)](https://travis-ci.org/dimkr/loksh)
+
+## Overview
+
+loksh is a [Linux](https://www.kernel.org/) port of [OpenBSD](http://www.openbsd.org/)'s *ksh*.
+
+Unlike other ports of *ksh*, loksh targets only one platform, follows upstream closely and keeps changes to a minimum. loksh does not add any extra features; this reduces the risk of introducing security vulnerabilities and makes loksh a good fit for resource-constrained systems.
+
+## Legal Information
+
+loksh is licensed under the same license as the upstream code it is based on.
+
+The ASCII art logo at the top was made using [FIGlet](http://www.figlet.org/).
diff --git a/alloc.c b/alloc.c
@@ -0,0 +1,128 @@
+/*	$OpenBSD: alloc.c,v 1.19 2018/01/16 22:52:32 jca Exp $	*/
+
+/* Public domain, like most of the rest of ksh */
+
+/*
+ * area-based allocation built on malloc/free
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "sh.h"
+
+struct link {
+	struct link *prev;
+	struct link *next;
+};
+
+Area *
+ainit(Area *ap)
+{
+	ap->freelist = NULL;
+	return ap;
+}
+
+void
+afreeall(Area *ap)
+{
+	struct link *l, *l2;
+
+	for (l = ap->freelist; l != NULL; l = l2) {
+		l2 = l->next;
+		free(l);
+	}
+	ap->freelist = NULL;
+}
+
+#define L2P(l)	( (void *)(((char *)(l)) + sizeof(struct link)) )
+#define P2L(p)	( (struct link *)(((char *)(p)) - sizeof(struct link)) )
+
+void *
+alloc(size_t size, Area *ap)
+{
+	struct link *l;
+
+	/* ensure that we don't overflow by allocating space for link */
+	if (size > SIZE_MAX - sizeof(struct link))
+		internal_errorf("unable to allocate memory");
+
+	l = malloc(sizeof(struct link) + size);
+	if (l == NULL)
+		internal_errorf("unable to allocate memory");
+	l->next = ap->freelist;
+	l->prev = NULL;
+	if (ap->freelist)
+		ap->freelist->prev = l;
+	ap->freelist = l;
+
+	return L2P(l);
+}
+
+/*
+ * Copied from calloc().
+ *
+ * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX
+ * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW
+ */
+#define MUL_NO_OVERFLOW	(1UL << (sizeof(size_t) * 4))
+
+void *
+areallocarray(void *ptr, size_t nmemb, size_t size, Area *ap)
+{
+	/* condition logic cloned from calloc() */
+	if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
+	    nmemb > 0 && SIZE_MAX / nmemb < size) {
+		internal_errorf("unable to allocate memory");
+	}
+
+	return aresize(ptr, nmemb * size, ap);
+}
+
+void *
+aresize(void *ptr, size_t size, Area *ap)
+{
+	struct link *l, *l2, *lprev, *lnext;
+
+	if (ptr == NULL)
+		return alloc(size, ap);
+
+	/* ensure that we don't overflow by allocating space for link */
+	if (size > SIZE_MAX - sizeof(struct link))
+		internal_errorf("unable to allocate memory");
+
+	l = P2L(ptr);
+	lprev = l->prev;
+	lnext = l->next;
+
+	l2 = realloc(l, sizeof(struct link) + size);
+	if (l2 == NULL)
+		internal_errorf("unable to allocate memory");
+	if (lprev)
+		lprev->next = l2;
+	else
+		ap->freelist = l2;
+	if (lnext)
+		lnext->prev = l2;
+
+	return L2P(l2);
+}
+
+void
+afree(void *ptr, Area *ap)
+{
+	struct link *l;
+
+	if (!ptr)
+		return;
+
+	l = P2L(ptr);
+	if (l->prev)
+		l->prev->next = l->next;
+	else
+		ap->freelist = l->next;
+	if (l->next)
+		l->next->prev = l->prev;
+
+	free(l);
+}
diff --git a/c_ksh.c b/c_ksh.c
@@ -0,0 +1,1428 @@
+/*	$OpenBSD: c_ksh.c,v 1.61 2018/05/18 13:25:20 benno Exp $	*/
+
+/*
+ * built-in Korn commands: c_*
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+int
+c_cd(char **wp)
+{
+	int optc;
+	int physical = Flag(FPHYSICAL);
+	int cdnode;			/* was a node from cdpath added in? */
+	int printpath = 0;		/* print where we cd'd? */
+	int rval;
+	struct tbl *pwd_s, *oldpwd_s;
+	XString xs;
+	char *xp;
+	char *dir, *try, *pwd;
+	int phys_path;
+	char *cdpath;
+	char *fdir = NULL;
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, "LP")) != -1)
+		switch (optc) {
+		case 'L':
+			physical = 0;
+			break;
+		case 'P':
+			physical = 1;
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+
+	if (Flag(FRESTRICTED)) {
+		bi_errorf("restricted shell - can't cd");
+		return 1;
+	}
+
+	pwd_s = global("PWD");
+	oldpwd_s = global("OLDPWD");
+
+	if (!wp[0]) {
+		/* No arguments - go home */
+		if ((dir = str_val(global("HOME"))) == null) {
+			bi_errorf("no home directory (HOME not set)");
+			return 1;
+		}
+	} else if (!wp[1]) {
+		/* One argument: - or dir */
+		dir = wp[0];
+		if (strcmp(dir, "-") == 0) {
+			dir = str_val(oldpwd_s);
+			if (dir == null) {
+				bi_errorf("no OLDPWD");
+				return 1;
+			}
+			printpath++;
+		}
+	} else if (!wp[2]) {
+		/* Two arguments - substitute arg1 in PWD for arg2 */
+		int ilen, olen, nlen, elen;
+		char *cp;
+
+		if (!current_wd[0]) {
+			bi_errorf("don't know current directory");
+			return 1;
+		}
+		/* substitute arg1 for arg2 in current path.
+		 * if the first substitution fails because the cd fails
+		 * we could try to find another substitution. For now
+		 * we don't
+		 */
+		if ((cp = strstr(current_wd, wp[0])) == NULL) {
+			bi_errorf("bad substitution");
+			return 1;
+		}
+		ilen = cp - current_wd;
+		olen = strlen(wp[0]);
+		nlen = strlen(wp[1]);
+		elen = strlen(current_wd + ilen + olen) + 1;
+		fdir = dir = alloc(ilen + nlen + elen, ATEMP);
+		memcpy(dir, current_wd, ilen);
+		memcpy(dir + ilen, wp[1], nlen);
+		memcpy(dir + ilen + nlen, current_wd + ilen + olen, elen);
+		printpath++;
+	} else {
+		bi_errorf("too many arguments");
+		return 1;
+	}
+
+	Xinit(xs, xp, PATH_MAX, ATEMP);
+	/* xp will have a bogus value after make_path() - set it to 0
+	 * so that if it's used, it will cause a dump
+	 */
+	xp = NULL;
+
+	cdpath = str_val(global("CDPATH"));
+	do {
+		cdnode = make_path(current_wd, dir, &cdpath, &xs, &phys_path);
+		if (physical)
+			rval = chdir(try = Xstring(xs, xp) + phys_path);
+		else {
+			simplify_path(Xstring(xs, xp));
+			rval = chdir(try = Xstring(xs, xp));
+		}
+	} while (rval < 0 && cdpath != NULL);
+
+	if (rval < 0) {
+		if (cdnode)
+			bi_errorf("%s: bad directory", dir);
+		else
+			bi_errorf("%s - %s", try, strerror(errno));
+		afree(fdir, ATEMP);
+		return 1;
+	}
+
+	/* Clear out tracked aliases with relative paths */
+	flushcom(0);
+
+	/* Set OLDPWD (note: unsetting OLDPWD does not disable this
+	 * setting in at&t ksh)
+	 */
+	if (current_wd[0])
+		/* Ignore failure (happens if readonly or integer) */
+		setstr(oldpwd_s, current_wd, KSH_RETURN_ERROR);
+
+	if (Xstring(xs, xp)[0] != '/') {
+		pwd = NULL;
+	} else
+	if (!physical || !(pwd = get_phys_path(Xstring(xs, xp))))
+		pwd = Xstring(xs, xp);
+
+	/* Set PWD */
+	if (pwd) {
+		char *ptmp = pwd;
+		set_current_wd(ptmp);
+		/* Ignore failure (happens if readonly or integer) */
+		setstr(pwd_s, ptmp, KSH_RETURN_ERROR);
+	} else {
+		set_current_wd(null);
+		pwd = Xstring(xs, xp);
+		/* XXX unset $PWD? */
+	}
+	if (printpath || cdnode)
+		shprintf("%s\n", pwd);
+
+	afree(fdir, ATEMP);
+
+	return 0;
+}
+
+int
+c_pwd(char **wp)
+{
+	int optc;
+	int physical = Flag(FPHYSICAL);
+	char *p, *freep = NULL;
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, "LP")) != -1)
+		switch (optc) {
+		case 'L':
+			physical = 0;
+			break;
+		case 'P':
+			physical = 1;
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+
+	if (wp[0]) {
+		bi_errorf("too many arguments");
+		return 1;
+	}
+	p = current_wd[0] ? (physical ? get_phys_path(current_wd) : current_wd) :
+	    NULL;
+	if (p && access(p, R_OK) < 0)
+		p = NULL;
+	if (!p) {
+		freep = p = ksh_get_wd(NULL, 0);
+		if (!p) {
+			bi_errorf("can't get current directory - %s",
+			    strerror(errno));
+			return 1;
+		}
+	}
+	shprintf("%s\n", p);
+	afree(freep, ATEMP);
+	return 0;
+}
+
+int
+c_print(char **wp)
+{
+#define PO_NL		BIT(0)	/* print newline */
+#define PO_EXPAND	BIT(1)	/* expand backslash sequences */
+#define PO_PMINUSMINUS	BIT(2)	/* print a -- argument */
+#define PO_HIST		BIT(3)	/* print to history instead of stdout */
+#define PO_COPROC	BIT(4)	/* printing to coprocess: block SIGPIPE */
+	int fd = 1;
+	int flags = PO_EXPAND|PO_NL;
+	char *s;
+	const char *emsg;
+	XString xs;
+	char *xp;
+
+	if (wp[0][0] == 'e') {	/* echo command */
+		int nflags = flags;
+
+		/* A compromise between sysV and BSD echo commands:
+		 * escape sequences are enabled by default, and
+		 * -n, -e and -E are recognized if they appear
+		 * in arguments with no illegal options (ie, echo -nq
+		 * will print -nq).
+		 * Different from sysV echo since options are recognized,
+		 * different from BSD echo since escape sequences are enabled
+		 * by default.
+		 */
+		wp += 1;
+		if (Flag(FPOSIX)) {
+			if (*wp && strcmp(*wp, "-n") == 0) {
+				flags &= ~PO_NL;
+				wp++;
+			}
+		} else {
+			while ((s = *wp) && *s == '-' && s[1]) {
+				while (*++s)
+					if (*s == 'n')
+						nflags &= ~PO_NL;
+					else if (*s == 'e')
+						nflags |= PO_EXPAND;
+					else if (*s == 'E')
+						nflags &= ~PO_EXPAND;
+					else
+						/* bad option: don't use
+						 * nflags, print argument
+						 */
+						break;
+				if (*s)
+					break;
+				wp++;
+				flags = nflags;
+			}
+		}
+	} else {
+		int optc;
+		const char *options = "Rnprsu,";
+		while ((optc = ksh_getopt(wp, &builtin_opt, options)) != -1)
+			switch (optc) {
+			case 'R': /* fake BSD echo command */
+				flags |= PO_PMINUSMINUS;
+				flags &= ~PO_EXPAND;
+				options = "ne";
+				break;
+			case 'e':
+				flags |= PO_EXPAND;
+				break;
+			case 'n':
+				flags &= ~PO_NL;
+				break;
+			case 'p':
+				if ((fd = coproc_getfd(W_OK, &emsg)) < 0) {
+					bi_errorf("-p: %s", emsg);
+					return 1;
+				}
+				break;
+			case 'r':
+				flags &= ~PO_EXPAND;
+				break;
+			case 's':
+				flags |= PO_HIST;
+				break;
+			case 'u':
+				if (!*(s = builtin_opt.optarg))
+					fd = 0;
+				else if ((fd = check_fd(s, W_OK, &emsg)) < 0) {
+					bi_errorf("-u: %s: %s", s, emsg);
+					return 1;
+				}
+				break;
+			case '?':
+				return 1;
+			}
+		if (!(builtin_opt.info & GI_MINUSMINUS)) {
+			/* treat a lone - like -- */
+			if (wp[builtin_opt.optind] &&
+			    strcmp(wp[builtin_opt.optind], "-") == 0)
+				builtin_opt.optind++;
+		} else if (flags & PO_PMINUSMINUS)
+			builtin_opt.optind--;
+		wp += builtin_opt.optind;
+	}
+
+	Xinit(xs, xp, 128, ATEMP);
+
+	while (*wp != NULL) {
+		int c;
+		s = *wp;
+		while ((c = *s++) != '\0') {
+			Xcheck(xs, xp);
+			if ((flags & PO_EXPAND) && c == '\\') {
+				int i;
+
+				switch ((c = *s++)) {
+				/* Oddly enough, \007 seems more portable than
+				 * \a (due to HP-UX cc, Ultrix cc, old pcc's,
+				 * etc.).
+				 */
+				case 'a': c = '\007'; break;
+				case 'b': c = '\b'; break;
+				case 'c': flags &= ~PO_NL;
+					  continue; /* AT&T brain damage */
+				case 'f': c = '\f'; break;
+				case 'n': c = '\n'; break;
+				case 'r': c = '\r'; break;
+				case 't': c = '\t'; break;
+				case 'v': c = 0x0B; break;
+				case '0':
+					/* Look for an octal number: can have
+					 * three digits (not counting the
+					 * leading 0).  Truly burnt.
+					 */
+					c = 0;
+					for (i = 0; i < 3; i++) {
+						if (*s >= '0' && *s <= '7')
+							c = c*8 + *s++ - '0';
+						else
+							break;
+					}
+					break;
+				case '\0': s--; c = '\\'; break;
+				case '\\': break;
+				default:
+					Xput(xs, xp, '\\');
+				}
+			}
+			Xput(xs, xp, c);
+		}
+		if (*++wp != NULL)
+			Xput(xs, xp, ' ');
+	}
+	if (flags & PO_NL)
+		Xput(xs, xp, '\n');
+
+	if (flags & PO_HIST) {
+		Xput(xs, xp, '\0');
+		source->line++;
+		histsave(source->line, Xstring(xs, xp), 1);
+		Xfree(xs, xp);
+	} else {
+		int n, len = Xlength(xs, xp);
+		int opipe = 0;
+
+		/* Ensure we aren't killed by a SIGPIPE while writing to
+		 * a coprocess.  at&t ksh doesn't seem to do this (seems
+		 * to just check that the co-process is alive, which is
+		 * not enough).
+		 */
+		if (coproc.write >= 0 && coproc.write == fd) {
+			flags |= PO_COPROC;
+			opipe = block_pipe();
+		}
+		for (s = Xstring(xs, xp); len > 0; ) {
+			n = write(fd, s, len);
+			if (n < 0) {
+				if (flags & PO_COPROC)
+					restore_pipe(opipe);
+				if (errno == EINTR) {
+					/* allow user to ^C out */
+					intrcheck();
+					if (flags & PO_COPROC)
+						opipe = block_pipe();
+					continue;
+				}
+				/* This doesn't really make sense - could
+				 * break scripts (print -p generates
+				 * error message).
+				*if (errno == EPIPE)
+				*	coproc_write_close(fd);
+				 */
+				return 1;
+			}
+			s += n;
+			len -= n;
+		}
+		if (flags & PO_COPROC)
+			restore_pipe(opipe);
+	}
+
+	return 0;
+}
+
+int
+c_whence(char **wp)
+{
+	struct tbl *tp;
+	char *id;
+	int pflag = 0, vflag = 0, Vflag = 0;
+	int ret = 0;
+	int optc;
+	int iam_whence;
+	int fcflags;
+	const char *options;
+
+	switch (wp[0][0]) {
+	case 'c': /* command */
+		iam_whence = 0;
+		options = "pvV";
+		break;
+	case 't': /* type */
+		vflag = 1;
+		/* FALLTHROUGH */
+	case 'w': /* whence */
+		iam_whence = 1;
+		options = "pv";
+		break;
+	default:
+		bi_errorf("builtin not handled by %s", __func__);
+		return 1;
+	}
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, options)) != -1)
+		switch (optc) {
+		case 'p':
+			pflag = 1;
+			break;
+		case 'v':
+			vflag = 1;
+			break;
+		case 'V':
+			Vflag = 1;
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+
+	fcflags = FC_BI | FC_PATH | FC_FUNC;
+	if (!iam_whence) {
+		/* Note that -p on its own is dealt with in comexec() */
+		if (pflag)
+			fcflags |= FC_DEFPATH;
+		/* Convert command options to whence options.  Note that
+		 * command -pV and command -pv use a different path search
+		 * than whence -v or whence -pv.  This should be considered
+		 * a feature.
+		 */
+		vflag = Vflag;
+	} else if (pflag)
+		fcflags &= ~(FC_BI | FC_FUNC);
+
+	while ((vflag || ret == 0) && (id = *wp++) != NULL) {
+		tp = NULL;
+		if (!iam_whence || !pflag)
+			tp = ktsearch(&keywords, id, hash(id));
+		if (!tp && (!iam_whence || !pflag)) {
+			tp = ktsearch(&aliases, id, hash(id));
+			if (tp && !(tp->flag & ISSET))
+				tp = NULL;
+		}
+		if (!tp)
+			tp = findcom(id, fcflags);
+		if (vflag || (tp->type != CALIAS && tp->type != CEXEC &&
+		    tp->type != CTALIAS))
+			shprintf("%s", id);
+		switch (tp->type) {
+		case CKEYWD:
+			if (vflag)
+				shprintf(" is a reserved word");
+			break;
+		case CALIAS:
+			if (vflag)
+				shprintf(" is an %salias for ",
+				    (tp->flag & EXPORT) ? "exported " : "");
+			if (!iam_whence && !vflag)
+				shprintf("alias %s=", id);
+			print_value_quoted(tp->val.s);
+			break;
+		case CFUNC:
+			if (vflag) {
+				shprintf(" is a");
+				if (tp->flag & EXPORT)
+					shprintf("n exported");
+				if (tp->flag & TRACE)
+					shprintf(" traced");
+				if (!(tp->flag & ISSET)) {
+					shprintf(" undefined");
+					if (tp->u.fpath)
+						shprintf(" (autoload from %s)",
+						    tp->u.fpath);
+				}
+				shprintf(" function");
+			}
+			break;
+		case CSHELL:
+			if (vflag)
+				shprintf(" is a%s shell builtin",
+				    (tp->flag & SPEC_BI) ? " special" : "");
+			break;
+		case CTALIAS:
+		case CEXEC:
+			if (tp->flag & ISSET) {
+				if (vflag) {
+					shprintf(" is ");
+					if (tp->type == CTALIAS)
+						shprintf("a tracked %salias for ",
+						    (tp->flag & EXPORT) ?
+						    "exported " : "");
+				}
+				shprintf("%s", tp->val.s);
+			} else {
+				if (vflag)
+					shprintf(" not found");
+				ret = 1;
+			}
+			break;
+		default:
+			shprintf("%s is *GOK*", id);
+			break;
+		}
+		if (vflag || !ret)
+			shprintf("\n");
+	}
+	return ret;
+}
+
+/* Deal with command -vV - command -p dealt with in comexec() */
+int
+c_command(char **wp)
+{
+	/* Let c_whence do the work.  Note that c_command() must be
+	 * a distinct function from c_whence() (tested in comexec()).
+	 */
+	return c_whence(wp);
+}
+
+int
+c_type(char **wp)
+{
+	/* Let c_whence do the work. type = command -V = whence -v */
+	return c_whence(wp);
+}
+
+/* typeset, export, and readonly */
+int
+c_typeset(char **wp)
+{
+	struct block *l;
+	struct tbl *vp, **p;
+	int fset = 0, fclr = 0, thing = 0, func = 0, local = 0, pflag = 0;
+	const char *options = "L#R#UZ#fi#lprtux";	/* see comment below */
+	char *fieldstr, *basestr;
+	int field, base, optc, flag;
+
+	switch (**wp) {
+	case 'e':		/* export */
+		fset |= EXPORT;
+		options = "p";
+		break;
+	case 'r':		/* readonly */
+		fset |= RDONLY;
+		options = "p";
+		break;
+	case 's':		/* set */
+		/* called with 'typeset -' */
+		break;
+	case 't':		/* typeset */
+		local = 1;
+		break;
+	}
+
+	fieldstr = basestr = NULL;
+	builtin_opt.flags |= GF_PLUSOPT;
+	/* at&t ksh seems to have 0-9 as options, which are multiplied
+	 * to get a number that is used with -L, -R, -Z or -i (eg, -1R2
+	 * sets right justify in a field of 12).  This allows options
+	 * to be grouped in an order (eg, -Lu12), but disallows -i8 -L3 and
+	 * does not allow the number to be specified as a separate argument
+	 * Here, the number must follow the RLZi option, but is optional
+	 * (see the # kludge in ksh_getopt()).
+	 */
+	while ((optc = ksh_getopt(wp, &builtin_opt, options)) != -1) {
+		flag = 0;
+		switch (optc) {
+		case 'L':
+			flag = LJUST;
+			fieldstr = builtin_opt.optarg;
+			break;
+		case 'R':
+			flag = RJUST;
+			fieldstr = builtin_opt.optarg;
+			break;
+		case 'U':
+			/* at&t ksh uses u, but this conflicts with
+			 * upper/lower case.  If this option is changed,
+			 * need to change the -U below as well
+			 */
+			flag = INT_U;
+			break;
+		case 'Z':
+			flag = ZEROFIL;
+			fieldstr = builtin_opt.optarg;
+			break;
+		case 'f':
+			func = 1;
+			break;
+		case 'i':
+			flag = INTEGER;
+			basestr = builtin_opt.optarg;
+			break;
+		case 'l':
+			flag = LCASEV;
+			break;
+		case 'p':
+			/* posix export/readonly -p flag.
+			 * typeset -p is the same as typeset (in pdksh);
+			 * here for compatibility with ksh93.
+			 */
+			pflag = 1;
+			break;
+		case 'r':
+			flag = RDONLY;
+			break;
+		case 't':
+			flag = TRACE;
+			break;
+		case 'u':
+			flag = UCASEV_AL;	/* upper case / autoload */
+			break;
+		case 'x':
+			flag = EXPORT;
+			break;
+		case '?':
+			return 1;
+		}
+		if (builtin_opt.info & GI_PLUS) {
+			fclr |= flag;
+			fset &= ~flag;
+			thing = '+';
+		} else {
+			fset |= flag;
+			fclr &= ~flag;
+			thing = '-';
+		}
+	}
+
+	field = 0;
+	if (fieldstr && !bi_getn(fieldstr, &field))
+		return 1;
+	base = 0;
+	if (basestr && !bi_getn(basestr, &base))
+		return 1;
+
+	if (!(builtin_opt.info & GI_MINUSMINUS) && wp[builtin_opt.optind] &&
+	    (wp[builtin_opt.optind][0] == '-' ||
+	    wp[builtin_opt.optind][0] == '+') &&
+	    wp[builtin_opt.optind][1] == '\0') {
+		thing = wp[builtin_opt.optind][0];
+		builtin_opt.optind++;
+	}
+
+	if (func && ((fset|fclr) & ~(TRACE|UCASEV_AL|EXPORT))) {
+		bi_errorf("only -t, -u and -x options may be used with -f");
+		return 1;
+	}
+	if (wp[builtin_opt.optind]) {
+		/* Take care of exclusions.
+		 * At this point, flags in fset are cleared in fclr and vise
+		 * versa.  This property should be preserved.
+		 */
+		if (fset & LCASEV)	/* LCASEV has priority over UCASEV_AL */
+			fset &= ~UCASEV_AL;
+		if (fset & LJUST)	/* LJUST has priority over RJUST */
+			fset &= ~RJUST;
+		if ((fset & (ZEROFIL|LJUST)) == ZEROFIL) { /* -Z implies -ZR */
+			fset |= RJUST;
+			fclr &= ~RJUST;
+		}
+		/* Setting these attributes clears the others, unless they
+		 * are also set in this command
+		 */
+		if (fset & (LJUST | RJUST | ZEROFIL | UCASEV_AL | LCASEV |
+		    INTEGER | INT_U | INT_L))
+			fclr |= ~fset & (LJUST | RJUST | ZEROFIL | UCASEV_AL |
+			    LCASEV | INTEGER | INT_U | INT_L);
+	}
+
+	/* set variables and attributes */
+	if (wp[builtin_opt.optind]) {
+		int i;
+		int rval = 0;
+		struct tbl *f;
+
+		if (local && !func)
+			fset |= LOCAL;
+		for (i = builtin_opt.optind; wp[i]; i++) {
+			if (func) {
+				f = findfunc(wp[i], hash(wp[i]),
+				    (fset&UCASEV_AL) ? true : false);
+				if (!f) {
+					/* at&t ksh does ++rval: bogus */
+					rval = 1;
+					continue;
+				}
+				if (fset | fclr) {
+					f->flag |= fset;
+					f->flag &= ~fclr;
+				} else
+					fptreef(shl_stdout, 0,
+					    f->flag & FKSH ?
+					    "function %s %T\n" :
+					    "%s() %T\n", wp[i], f->val.t);
+			} else if (!typeset(wp[i], fset, fclr, field, base)) {
+				bi_errorf("%s: not identifier", wp[i]);
+				return 1;
+			}
+		}
+		return rval;
+	}
+
+	/* list variables and attributes */
+	flag = fset | fclr; /* no difference at this point.. */
+	if (func) {
+		for (l = genv->loc; l; l = l->next) {
+			for (p = ktsort(&l->funs); (vp = *p++); ) {
+				if (flag && (vp->flag & flag) == 0)
+					continue;
+				if (thing == '-')
+					fptreef(shl_stdout, 0, vp->flag & FKSH ?
+					    "function %s %T\n" : "%s() %T\n",
+					    vp->name, vp->val.t);
+				else
+					shprintf("%s\n", vp->name);
+			}
+		}
+	} else {
+		for (l = genv->loc; l; l = l->next) {
+			for (p = ktsort(&l->vars); (vp = *p++); ) {
+				struct tbl *tvp;
+				int any_set = 0;
+				/*
+				 * See if the parameter is set (for arrays, if any
+				 * element is set).
+				 */
+				for (tvp = vp; tvp; tvp = tvp->u.array)
+					if (tvp->flag & ISSET) {
+						any_set = 1;
+						break;
+					}
+
+				/*
+				 * Check attributes - note that all array elements
+				 * have (should have?) the same attributes, so checking
+				 * the first is sufficient.
+				 *
+				 * Report an unset param only if the user has
+				 * explicitly given it some attribute (like export);
+				 * otherwise, after "echo $FOO", we would report FOO...
+				 */
+				if (!any_set && !(vp->flag & USERATTRIB))
+					continue;
+				if (flag && (vp->flag & flag) == 0)
+					continue;
+				for (; vp; vp = vp->u.array) {
+					/* Ignore array elements that aren't
+					 * set unless there are no set elements,
+					 * in which case the first is reported on */
+					if ((vp->flag&ARRAY) && any_set &&
+					    !(vp->flag & ISSET))
+						continue;
+					/* no arguments */
+					if (thing == 0 && flag == 0) {
+						/* at&t ksh prints things
+						 * like export, integer,
+						 * leftadj, zerofill, etc.,
+						 * but POSIX says must
+						 * be suitable for re-entry...
+						 */
+						shprintf("typeset ");
+						if ((vp->flag&INTEGER))
+							shprintf("-i ");
+						if ((vp->flag&EXPORT))
+							shprintf("-x ");
+						if ((vp->flag&RDONLY))
+							shprintf("-r ");
+						if ((vp->flag&TRACE))
+							shprintf("-t ");
+						if ((vp->flag&LJUST))
+							shprintf("-L%d ", vp->u2.field);
+						if ((vp->flag&RJUST))
+							shprintf("-R%d ", vp->u2.field);
+						if ((vp->flag&ZEROFIL))
+							shprintf("-Z ");
+						if ((vp->flag&LCASEV))
+							shprintf("-l ");
+						if ((vp->flag&UCASEV_AL))
+							shprintf("-u ");
+						if ((vp->flag&INT_U))
+							shprintf("-U ");
+						shprintf("%s\n", vp->name);
+						    if (vp->flag&ARRAY)
+						break;
+					} else {
+						if (pflag)
+							shprintf("%s ",
+							    (flag & EXPORT) ?
+							    "export" : "readonly");
+						if ((vp->flag&ARRAY) && any_set)
+							shprintf("%s[%d]",
+							    vp->name, vp->index);
+						else
+							shprintf("%s", vp->name);
+						if (thing == '-' && (vp->flag&ISSET)) {
+							char *s = str_val(vp);
+
+							shprintf("=");
+							/* at&t ksh can't have
+							 * justified integers.. */
+							if ((vp->flag &
+							    (INTEGER|LJUST|RJUST)) ==
+							    INTEGER)
+								shprintf("%s", s);
+							else
+								print_value_quoted(s);
+						}
+						shprintf("\n");
+					}
+					/* Only report first `element' of an array with
+					* no set elements.
+					*/
+					if (!any_set)
+						break;
+				}
+			}
+		}
+	}
+	return 0;
+}
+
+int
+c_alias(char **wp)
+{
+	struct table *t = &aliases;
+	int rv = 0, rflag = 0, tflag, Uflag = 0, pflag = 0, prefix = 0;
+	int xflag = 0;
+	int optc;
+
+	builtin_opt.flags |= GF_PLUSOPT;
+	while ((optc = ksh_getopt(wp, &builtin_opt, "dprtUx")) != -1) {
+		prefix = builtin_opt.info & GI_PLUS ? '+' : '-';
+		switch (optc) {
+		case 'd':
+			t = &homedirs;
+			break;
+		case 'p':
+			pflag = 1;
+			break;
+		case 'r':
+			rflag = 1;
+			break;
+		case 't':
+			t = &taliases;
+			break;
+		case 'U':
+			/*
+			 * kludge for tracked alias initialization
+			 * (don't do a path search, just make an entry)
+			 */
+			Uflag = 1;
+			break;
+		case 'x':
+			xflag = EXPORT;
+			break;
+		case '?':
+			return 1;
+		}
+	}
+	wp += builtin_opt.optind;
+
+	if (!(builtin_opt.info & GI_MINUSMINUS) && *wp &&
+	    (wp[0][0] == '-' || wp[0][0] == '+') && wp[0][1] == '\0') {
+		prefix = wp[0][0];
+		wp++;
+	}
+
+	tflag = t == &taliases;
+
+	/* "hash -r" means reset all the tracked aliases.. */
+	if (rflag) {
+		static const char *const args[] = {
+			"unalias", "-ta", NULL
+		};
+
+		if (!tflag || *wp) {
+			shprintf("alias: -r flag can only be used with -t"
+			    " and without arguments\n");
+			return 1;
+		}
+		ksh_getopt_reset(&builtin_opt, GF_ERROR);
+		return c_unalias((char **) args);
+	}
+
+	if (*wp == NULL) {
+		struct tbl *ap, **p;
+
+		for (p = ktsort(t); (ap = *p++) != NULL; )
+			if ((ap->flag & (ISSET|xflag)) == (ISSET|xflag)) {
+				if (pflag)
+					shf_puts("alias ", shl_stdout);
+				shf_puts(ap->name, shl_stdout);
+				if (prefix != '+') {
+					shf_putc('=', shl_stdout);
+					print_value_quoted(ap->val.s);
+				}
+				shprintf("\n");
+			}
+	}
+
+	for (; *wp != NULL; wp++) {
+		char *alias = *wp;
+		char *val = strchr(alias, '=');
+		char *newval;
+		struct tbl *ap;
+		int h;
+
+		if (val)
+			alias = str_nsave(alias, val++ - alias, ATEMP);
+		h = hash(alias);
+		if (val == NULL && !tflag && !xflag) {
+			ap = ktsearch(t, alias, h);
+			if (ap != NULL && (ap->flag&ISSET)) {
+				if (pflag)
+					shf_puts("alias ", shl_stdout);
+				shf_puts(ap->name, shl_stdout);
+				if (prefix != '+') {
+					shf_putc('=', shl_stdout);
+					print_value_quoted(ap->val.s);
+				}
+				shprintf("\n");
+			} else {
+				shprintf("%s alias not found\n", alias);
+				rv = 1;
+			}
+			continue;
+		}
+		ap = ktenter(t, alias, h);
+		ap->type = tflag ? CTALIAS : CALIAS;
+		/* Are we setting the value or just some flags? */
+		if ((val && !tflag) || (!val && tflag && !Uflag)) {
+			if (ap->flag&ALLOC) {
+				ap->flag &= ~(ALLOC|ISSET);
+				afree(ap->val.s, APERM);
+			}
+			/* ignore values for -t (at&t ksh does this) */
+			newval = tflag ? search(alias, search_path, X_OK, NULL)
+			    : val;
+			if (newval) {
+				ap->val.s = str_save(newval, APERM);
+				ap->flag |= ALLOC|ISSET;
+			} else
+				ap->flag &= ~ISSET;
+		}
+		ap->flag |= DEFINED;
+		if (prefix == '+')
+			ap->flag &= ~xflag;
+		else
+			ap->flag |= xflag;
+		if (val)
+			afree(alias, ATEMP);
+	}
+
+	return rv;
+}
+
+int
+c_unalias(char **wp)
+{
+	struct table *t = &aliases;
+	struct tbl *ap;
+	int rv = 0, all = 0;
+	int optc;
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, "adt")) != -1)
+		switch (optc) {
+		case 'a':
+			all = 1;
+			break;
+		case 'd':
+			t = &homedirs;
+			break;
+		case 't':
+			t = &taliases;
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+
+	for (; *wp != NULL; wp++) {
+		ap = ktsearch(t, *wp, hash(*wp));
+		if (ap == NULL) {
+			rv = 1;	/* POSIX */
+			continue;
+		}
+		if (ap->flag&ALLOC) {
+			ap->flag &= ~(ALLOC|ISSET);
+			afree(ap->val.s, APERM);
+		}
+		ap->flag &= ~(DEFINED|ISSET|EXPORT);
+	}
+
+	if (all) {
+		struct tstate ts;
+
+		for (ktwalk(&ts, t); (ap = ktnext(&ts)); ) {
+			if (ap->flag&ALLOC) {
+				ap->flag &= ~(ALLOC|ISSET);
+				afree(ap->val.s, APERM);
+			}
+			ap->flag &= ~(DEFINED|ISSET|EXPORT);
+		}
+	}
+
+	return rv;
+}
+
+int
+c_let(char **wp)
+{
+	int rv = 1;
+	int64_t val;
+
+	if (wp[1] == NULL) /* at&t ksh does this */
+		bi_errorf("no arguments");
+	else
+		for (wp++; *wp; wp++)
+			if (!evaluate(*wp, &val, KSH_RETURN_ERROR, true)) {
+				rv = 2;	/* distinguish error from zero result */
+				break;
+			} else
+				rv = val == 0;
+	return rv;
+}
+
+int
+c_jobs(char **wp)
+{
+	int optc;
+	int flag = 0;
+	int nflag = 0;
+	int rv = 0;
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, "lpnz")) != -1)
+		switch (optc) {
+		case 'l':
+			flag = 1;
+			break;
+		case 'p':
+			flag = 2;
+			break;
+		case 'n':
+			nflag = 1;
+			break;
+		case 'z':	/* debugging: print zombies */
+			nflag = -1;
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+	if (!*wp) {
+		if (j_jobs(NULL, flag, nflag))
+			rv = 1;
+	} else {
+		for (; *wp; wp++)
+			if (j_jobs(*wp, flag, nflag))
+				rv = 1;
+	}
+	return rv;
+}
+
+int
+c_fgbg(char **wp)
+{
+	int bg = strcmp(*wp, "bg") == 0;
+	int rv = 0;
+
+	if (!Flag(FMONITOR)) {
+		bi_errorf("job control not enabled");
+		return 1;
+	}
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+	wp += builtin_opt.optind;
+	if (*wp)
+		for (; *wp; wp++)
+			rv = j_resume(*wp, bg);
+	else
+		rv = j_resume("%%", bg);
+	/* POSIX says fg shall return 0 (unless an error occurs).
+	 * at&t ksh returns the exit value of the job...
+	 */
+	return (bg || Flag(FPOSIX)) ? 0 : rv;
+}
+
+struct kill_info {
+	int num_width;
+	int name_width;
+};
+static char *kill_fmt_entry(void *arg, int i, char *buf, int buflen);
+
+/* format a single kill item */
+static char *
+kill_fmt_entry(void *arg, int i, char *buf, int buflen)
+{
+	struct kill_info *ki = (struct kill_info *) arg;
+
+	i++;
+	if (sigtraps[i].name)
+		shf_snprintf(buf, buflen, "%*d %*s %s",
+		    ki->num_width, i,
+		    ki->name_width, sigtraps[i].name,
+		    sigtraps[i].mess);
+	else
+		shf_snprintf(buf, buflen, "%*d %*d %s",
+		    ki->num_width, i,
+		    ki->name_width, sigtraps[i].signal,
+		    sigtraps[i].mess);
+	return buf;
+}
+
+
+int
+c_kill(char **wp)
+{
+	Trap *t = NULL;
+	char *p;
+	int lflag = 0;
+	int i, n, rv, sig;
+
+	/* assume old style options if -digits or -UPPERCASE */
+	if ((p = wp[1]) && *p == '-' &&
+	    (digit(p[1]) || isupper((unsigned char)p[1]))) {
+		if (!(t = gettrap(p + 1, true))) {
+			bi_errorf("bad signal `%s'", p + 1);
+			return 1;
+		}
+		i = (wp[2] && strcmp(wp[2], "--") == 0) ? 3 : 2;
+	} else {
+		int optc;
+
+		while ((optc = ksh_getopt(wp, &builtin_opt, "ls:")) != -1)
+			switch (optc) {
+			case 'l':
+				lflag = 1;
+				break;
+			case 's':
+				if (!(t = gettrap(builtin_opt.optarg, true))) {
+					bi_errorf("bad signal `%s'",
+					    builtin_opt.optarg);
+					return 1;
+				}
+				break;
+			case '?':
+				return 1;
+			}
+		i = builtin_opt.optind;
+	}
+	if ((lflag && t) || (!wp[i] && !lflag)) {
+		shf_fprintf(shl_out,
+		    "usage: kill [-s signame | -signum | -signame] { job | pid | pgrp } ...\n"
+		    "       kill -l [exit_status ...]\n");
+		bi_errorf(NULL);
+		return 1;
+	}
+
+	if (lflag) {
+		if (wp[i]) {
+			for (; wp[i]; i++) {
+				if (!bi_getn(wp[i], &n))
+					return 1;
+				if (n > 128 && n < 128 + NSIG)
+					n -= 128;
+				if (n > 0 && n < NSIG && sigtraps[n].name)
+					shprintf("%s\n", sigtraps[n].name);
+				else
+					shprintf("%d\n", n);
+			}
+		} else if (Flag(FPOSIX)) {
+			p = null;
+			for (i = 1; i < NSIG; i++, p = " ")
+				if (sigtraps[i].name)
+					shprintf("%s%s", p, sigtraps[i].name);
+			shprintf("\n");
+		} else {
+			int mess_width = 0, w;
+			struct kill_info ki = {
+				.num_width = 1,
+				.name_width = 0,
+			};
+
+			for (i = NSIG; i >= 10; i /= 10)
+				ki.num_width++;
+
+			for (i = 0; i < NSIG; i++) {
+				w = sigtraps[i].name ?
+				    (int)strlen(sigtraps[i].name) :
+				    ki.num_width;
+				if (w > ki.name_width)
+					ki.name_width = w;
+				w = strlen(sigtraps[i].mess);
+				if (w > mess_width)
+					mess_width = w;
+			}
+
+			print_columns(shl_stdout, NSIG - 1,
+			    kill_fmt_entry, (void *) &ki,
+			    ki.num_width + ki.name_width + mess_width + 3, 1);
+		}
+		return 0;
+	}
+	rv = 0;
+	sig = t ? t->signal : SIGTERM;
+	for (; (p = wp[i]); i++) {
+		if (*p == '%') {
+			if (j_kill(p, sig))
+				rv = 1;
+		} else if (!getn(p, &n)) {
+			bi_errorf("%s: arguments must be jobs or process IDs",
+			    p);
+			rv = 1;
+		} else {
+			/* use killpg if < -1 since -1 does special things for
+			 * some non-killpg-endowed kills
+			 */
+			if ((n < -1 ? killpg(-n, sig) : kill(n, sig)) < 0) {
+				bi_errorf("%s: %s", p, strerror(errno));
+				rv = 1;
+			}
+		}
+	}
+	return rv;
+}
+
+void
+getopts_reset(int val)
+{
+	if (val >= 1) {
+		ksh_getopt_reset(&user_opt,
+		    GF_NONAME | (Flag(FPOSIX) ? 0 : GF_PLUSOPT));
+		user_opt.optind = user_opt.uoptind = val;
+	}
+}
+
+int
+c_getopts(char **wp)
+{
+	int	argc;
+	const char *options;
+	const char *var;
+	int	optc;
+	int	ret;
+	char	buf[3];
+	struct tbl *vq, *voptarg;
+
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+	wp += builtin_opt.optind;
+
+	options = *wp++;
+	if (!options) {
+		bi_errorf("missing options argument");
+		return 1;
+	}
+
+	var = *wp++;
+	if (!var) {
+		bi_errorf("missing name argument");
+		return 1;
+	}
+	if (!*var || *skip_varname(var, true)) {
+		bi_errorf("%s: is not an identifier", var);
+		return 1;
+	}
+
+	if (genv->loc->next == NULL) {
+		internal_warningf("%s: no argv", __func__);
+		return 1;
+	}
+	/* Which arguments are we parsing... */
+	if (*wp == NULL)
+		wp = genv->loc->next->argv;
+	else
+		*--wp = genv->loc->next->argv[0];
+
+	/* Check that our saved state won't cause a core dump... */
+	for (argc = 0; wp[argc]; argc++)
+		;
+	if (user_opt.optind > argc ||
+	    (user_opt.p != 0 &&
+	    user_opt.p > strlen(wp[user_opt.optind - 1]))) {
+		bi_errorf("arguments changed since last call");
+		return 1;
+	}
+
+	user_opt.optarg = NULL;
+	optc = ksh_getopt(wp, &user_opt, options);
+
+	if (optc >= 0 && optc != '?' && (user_opt.info & GI_PLUS)) {
+		buf[0] = '+';
+		buf[1] = optc;
+		buf[2] = '\0';
+	} else {
+		/* POSIX says var is set to ? at end-of-options, at&t ksh
+		 * sets it to null - we go with POSIX...
+		 */
+		buf[0] = optc < 0 ? '?' : optc;
+		buf[1] = '\0';
+	}
+
+	/* at&t ksh does not change OPTIND if it was an unknown option.
+	 * Scripts counting on this are prone to break... (ie, don't count
+	 * on this staying).
+	 */
+	if (optc != '?') {
+		user_opt.uoptind = user_opt.optind;
+	}
+
+	voptarg = global("OPTARG");
+	voptarg->flag &= ~RDONLY;	/* at&t ksh clears ro and int */
+	/* Paranoia: ensure no bizarre results. */
+	if (voptarg->flag & INTEGER)
+	    typeset("OPTARG", 0, INTEGER, 0, 0);
+	if (user_opt.optarg == NULL)
+		unset(voptarg, 0);
+	else
+		/* This can't fail (have cleared readonly/integer) */
+		setstr(voptarg, user_opt.optarg, KSH_RETURN_ERROR);
+
+	ret = 0;
+
+	vq = global(var);
+	/* Error message already printed (integer, readonly) */
+	if (!setstr(vq, buf, KSH_RETURN_ERROR))
+	    ret = 1;
+	if (Flag(FEXPORT))
+		typeset(var, EXPORT, 0, 0, 0);
+
+	return optc < 0 ? 1 : ret;
+}
+
+#ifdef EMACS
+int
+c_bind(char **wp)
+{
+	int optc, rv = 0, macro = 0, list = 0;
+	char *cp;
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, "lm")) != -1)
+		switch (optc) {
+		case 'l':
+			list = 1;
+			break;
+		case 'm':
+			macro = 1;
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+
+	if (*wp == NULL)	/* list all */
+		rv = x_bind(NULL, NULL, 0, list);
+
+	for (; *wp != NULL; wp++) {
+		cp = strchr(*wp, '=');
+		if (cp != NULL)
+			*cp++ = '\0';
+		if (x_bind(*wp, cp, macro, 0))
+			rv = 1;
+	}
+
+	return rv;
+}
+#endif
+
+/* A leading = means assignments before command are kept;
+ * a leading * means a POSIX special builtin;
+ * a leading + means a POSIX regular builtin
+ * (* and + should not be combined).
+ */
+const struct builtin kshbuiltins [] = {
+	{"+alias", c_alias},	/* no =: at&t manual wrong */
+	{"+cd", c_cd},
+	{"+command", c_command},
+	{"echo", c_print},
+	{"*=export", c_typeset},
+	{"+fc", c_fc},
+	{"+getopts", c_getopts},
+	{"+jobs", c_jobs},
+	{"+kill", c_kill},
+	{"let", c_let},
+	{"print", c_print},
+	{"pwd", c_pwd},
+	{"*=readonly", c_typeset},
+	{"type", c_type},
+	{"=typeset", c_typeset},
+	{"+unalias", c_unalias},
+	{"whence", c_whence},
+	{"+bg", c_fgbg},
+	{"+fg", c_fgbg},
+#ifdef EMACS
+	{"bind", c_bind},
+#endif
+	{NULL, NULL}
+};
diff --git a/c_sh.c b/c_sh.c
@@ -0,0 +1,887 @@
+/*	$OpenBSD: c_sh.c,v 1.63 2018/04/09 17:53:36 tobias Exp $	*/
+
+/*
+ * built-in Bourne commands
+ */
+
+#include <sys/types.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+static void p_tv(struct shf *, int, struct timeval *, int, char *, char *);
+static void p_ts(struct shf *, int, struct timespec *, int, char *, char *);
+
+/* :, false and true */
+int
+c_label(char **wp)
+{
+	return wp[0][0] == 'f' ? 1 : 0;
+}
+
+int
+c_shift(char **wp)
+{
+	struct block *l = genv->loc;
+	int n;
+	int64_t val;
+	char *arg;
+
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+	arg = wp[builtin_opt.optind];
+
+	if (arg) {
+		evaluate(arg, &val, KSH_UNWIND_ERROR, false);
+		n = val;
+	} else
+		n = 1;
+	if (n < 0) {
+		bi_errorf("%s: bad number", arg);
+		return (1);
+	}
+	if (l->argc < n) {
+		bi_errorf("nothing to shift");
+		return (1);
+	}
+	l->argv[n] = l->argv[0];
+	l->argv += n;
+	l->argc -= n;
+	return 0;
+}
+
+int
+c_umask(char **wp)
+{
+	int i;
+	char *cp;
+	int symbolic = 0;
+	mode_t old_umask;
+	int optc;
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, "S")) != -1)
+		switch (optc) {
+		case 'S':
+			symbolic = 1;
+			break;
+		case '?':
+			return 1;
+		}
+	cp = wp[builtin_opt.optind];
+	if (cp == NULL) {
+		old_umask = umask(0);
+		umask(old_umask);
+		if (symbolic) {
+			char buf[18];
+			int j;
+
+			old_umask = ~old_umask;
+			cp = buf;
+			for (i = 0; i < 3; i++) {
+				*cp++ = "ugo"[i];
+				*cp++ = '=';
+				for (j = 0; j < 3; j++)
+					if (old_umask & (1 << (8 - (3*i + j))))
+						*cp++ = "rwx"[j];
+				*cp++ = ',';
+			}
+			cp[-1] = '\0';
+			shprintf("%s\n", buf);
+		} else
+			shprintf("%#3.3o\n", old_umask);
+	} else {
+		mode_t new_umask;
+
+		if (digit(*cp)) {
+			for (new_umask = 0; *cp >= '0' && *cp <= '7'; cp++)
+				new_umask = new_umask * 8 + (*cp - '0');
+			if (*cp) {
+				bi_errorf("bad number");
+				return 1;
+			}
+		} else {
+			/* symbolic format */
+			int positions, new_val;
+			char op;
+
+			old_umask = umask(0);
+			umask(old_umask); /* in case of error */
+			old_umask = ~old_umask;
+			new_umask = old_umask;
+			positions = 0;
+			while (*cp) {
+				while (*cp && strchr("augo", *cp))
+					switch (*cp++) {
+					case 'a':
+						positions |= 0111;
+						break;
+					case 'u':
+						positions |= 0100;
+						break;
+					case 'g':
+						positions |= 0010;
+						break;
+					case 'o':
+						positions |= 0001;
+						break;
+					}
+				if (!positions)
+					positions = 0111; /* default is a */
+				if (!strchr("=+-", op = *cp))
+					break;
+				cp++;
+				new_val = 0;
+				while (*cp && strchr("rwxugoXs", *cp))
+					switch (*cp++) {
+					case 'r': new_val |= 04; break;
+					case 'w': new_val |= 02; break;
+					case 'x': new_val |= 01; break;
+					case 'u': new_val |= old_umask >> 6;
+						  break;
+					case 'g': new_val |= old_umask >> 3;
+						  break;
+					case 'o': new_val |= old_umask >> 0;
+						  break;
+					case 'X': if (old_umask & 0111)
+							new_val |= 01;
+						  break;
+					case 's': /* ignored */
+						  break;
+					}
+				new_val = (new_val & 07) * positions;
+				switch (op) {
+				case '-':
+					new_umask &= ~new_val;
+					break;
+				case '=':
+					new_umask = new_val |
+					    (new_umask & ~(positions * 07));
+					break;
+				case '+':
+					new_umask |= new_val;
+				}
+				if (*cp == ',') {
+					positions = 0;
+					cp++;
+				} else if (!strchr("=+-", *cp))
+					break;
+			}
+			if (*cp) {
+				bi_errorf("bad mask");
+				return 1;
+			}
+			new_umask = ~new_umask;
+		}
+		umask(new_umask);
+	}
+	return 0;
+}
+
+int
+c_dot(char **wp)
+{
+	char *file, *cp;
+	char **argv;
+	int argc;
+	int i;
+	int err;
+
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+
+	if ((cp = wp[builtin_opt.optind]) == NULL)
+		return 0;
+	file = search(cp, search_path, R_OK, &err);
+	if (file == NULL) {
+		bi_errorf("%s: %s", cp, err ? strerror(err) : "not found");
+		return 1;
+	}
+
+	/* Set positional parameters? */
+	if (wp[builtin_opt.optind + 1]) {
+		argv = wp + builtin_opt.optind;
+		argv[0] = genv->loc->argv[0]; /* preserve $0 */
+		for (argc = 0; argv[argc + 1]; argc++)
+			;
+	} else {
+		argc = 0;
+		argv = NULL;
+	}
+	i = include(file, argc, argv, 0);
+	if (i < 0) { /* should not happen */
+		bi_errorf("%s: %s", cp, strerror(errno));
+		return 1;
+	}
+	return i;
+}
+
+int
+c_wait(char **wp)
+{
+	int rv = 0;
+	int sig;
+
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+	wp += builtin_opt.optind;
+	if (*wp == NULL) {
+		while (waitfor(NULL, &sig) >= 0)
+			;
+		rv = sig;
+	} else {
+		for (; *wp; wp++)
+			rv = waitfor(*wp, &sig);
+		if (rv < 0)
+			rv = sig ? sig : 127; /* magic exit code: bad job-id */
+	}
+	return rv;
+}
+
+int
+c_read(char **wp)
+{
+	int c = 0;
+	int expand = 1, savehist = 0;
+	int expanding;
+	int ecode = 0;
+	char *cp;
+	int fd = 0;
+	struct shf *shf;
+	int optc;
+	const char *emsg;
+	XString cs, xs;
+	struct tbl *vp;
+	char *xp = NULL;
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, "prsu,")) != -1)
+		switch (optc) {
+		case 'p':
+			if ((fd = coproc_getfd(R_OK, &emsg)) < 0) {
+				bi_errorf("-p: %s", emsg);
+				return 1;
+			}
+			break;
+		case 'r':
+			expand = 0;
+			break;
+		case 's':
+			savehist = 1;
+			break;
+		case 'u':
+			if (!*(cp = builtin_opt.optarg))
+				fd = 0;
+			else if ((fd = check_fd(cp, R_OK, &emsg)) < 0) {
+				bi_errorf("-u: %s: %s", cp, emsg);
+				return 1;
+			}
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+
+	if (*wp == NULL)
+		*--wp = "REPLY";
+
+	/* Since we can't necessarily seek backwards on non-regular files,
+	 * don't buffer them so we can't read too much.
+	 */
+	shf = shf_reopen(fd, SHF_RD | SHF_INTERRUPT | can_seek(fd), shl_spare);
+
+	if ((cp = strchr(*wp, '?')) != NULL) {
+		*cp = 0;
+		if (isatty(fd)) {
+			/* at&t ksh says it prints prompt on fd if it's open
+			 * for writing and is a tty, but it doesn't do it
+			 * (it also doesn't check the interactive flag,
+			 * as is indicated in the Kornshell book).
+			 */
+			shellf("%s", cp+1);
+		}
+	}
+
+	/* If we are reading from the co-process for the first time,
+	 * make sure the other side of the pipe is closed first.  This allows
+	 * the detection of eof.
+	 *
+	 * This is not compatible with at&t ksh... the fd is kept so another
+	 * coproc can be started with same output, however, this means eof
+	 * can't be detected...  This is why it is closed here.
+	 * If this call is removed, remove the eof check below, too.
+	 * coproc_readw_close(fd);
+	 */
+
+	if (savehist)
+		Xinit(xs, xp, 128, ATEMP);
+	expanding = 0;
+	Xinit(cs, cp, 128, ATEMP);
+	for (; *wp != NULL; wp++) {
+		for (cp = Xstring(cs, cp); ; ) {
+			if (c == '\n' || c == EOF)
+				break;
+			while (1) {
+				c = shf_getc(shf);
+				if (c == '\0')
+					continue;
+				if (c == EOF && shf_error(shf) &&
+				    shf->errno_ == EINTR) {
+					/* Was the offending signal one that
+					 * would normally kill a process?
+					 * If so, pretend the read was killed.
+					 */
+					ecode = fatal_trap_check();
+
+					/* non fatal (eg, CHLD), carry on */
+					if (!ecode) {
+						shf_clearerr(shf);
+						continue;
+					}
+				}
+				break;
+			}
+			if (savehist) {
+				Xcheck(xs, xp);
+				Xput(xs, xp, c);
+			}
+			Xcheck(cs, cp);
+			if (expanding) {
+				expanding = 0;
+				if (c == '\n') {
+					c = 0;
+					if (Flag(FTALKING_I) && isatty(fd)) {
+						/* set prompt in case this is
+						 * called from .profile or $ENV
+						 */
+						set_prompt(PS2);
+						pprompt(prompt, 0);
+					}
+				} else if (c != EOF)
+					Xput(cs, cp, c);
+				continue;
+			}
+			if (expand && c == '\\') {
+				expanding = 1;
+				continue;
+			}
+			if (c == '\n' || c == EOF)
+				break;
+			if (ctype(c, C_IFS)) {
+				if (Xlength(cs, cp) == 0 && ctype(c, C_IFSWS))
+					continue;
+				if (wp[1])
+					break;
+			}
+			Xput(cs, cp, c);
+		}
+		/* strip trailing IFS white space from last variable */
+		if (!wp[1])
+			while (Xlength(cs, cp) && ctype(cp[-1], C_IFS) &&
+			    ctype(cp[-1], C_IFSWS))
+				cp--;
+		Xput(cs, cp, '\0');
+		vp = global(*wp);
+		/* Must be done before setting export. */
+		if (vp->flag & RDONLY) {
+			shf_flush(shf);
+			bi_errorf("%s is read only", *wp);
+			return 1;
+		}
+		if (Flag(FEXPORT))
+			typeset(*wp, EXPORT, 0, 0, 0);
+		if (!setstr(vp, Xstring(cs, cp), KSH_RETURN_ERROR)) {
+		    shf_flush(shf);
+		    return 1;
+		}
+	}
+
+	shf_flush(shf);
+	if (savehist) {
+		Xput(xs, xp, '\0');
+		source->line++;
+		histsave(source->line, Xstring(xs, xp), 1);
+		Xfree(xs, xp);
+	}
+	/* if this is the co-process fd, close the file descriptor
+	 * (can get eof if and only if all processes are have died, ie,
+	 * coproc.njobs is 0 and the pipe is closed).
+	 */
+	if (c == EOF && !ecode)
+		coproc_read_close(fd);
+
+	return ecode ? ecode : c == EOF;
+}
+
+int
+c_eval(char **wp)
+{
+	struct source *s;
+	int rv;
+
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+	s = pushs(SWORDS, ATEMP);
+	s->u.strv = wp + builtin_opt.optind;
+	if (!Flag(FPOSIX)) {
+		/*
+		 * Handle case where the command is empty due to failed
+		 * command substitution, eg, eval "$(false)".
+		 * In this case, shell() will not set/change exstat (because
+		 * compiled tree is empty), so will use this value.
+		 * subst_exstat is cleared in execute(), so should be 0 if
+		 * there were no substitutions.
+		 *
+		 * A strict reading of POSIX says we don't do this (though
+		 * it is traditionally done). [from 1003.2-1992]
+		 *    3.9.1: Simple Commands
+		 *	... If there is a command name, execution shall
+		 *	continue as described in 3.9.1.1.  If there
+		 *	is no command name, but the command contained a command
+		 *	substitution, the command shall complete with the exit
+		 *	status of the last command substitution
+		 *    3.9.1.1: Command Search and Execution
+		 *	...(1)...(a) If the command name matches the name of
+		 *	a special built-in utility, that special built-in
+		 *	utility shall be invoked.
+		 * 3.14.5: Eval
+		 *	... If there are no arguments, or only null arguments,
+		 *	eval shall return an exit status of zero.
+		 */
+		exstat = subst_exstat;
+	}
+
+	rv = shell(s, false);
+	afree(s, ATEMP);
+	return (rv);
+}
+
+int
+c_trap(char **wp)
+{
+	int i;
+	char *s;
+	Trap *p;
+
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+	wp += builtin_opt.optind;
+
+	if (*wp == NULL) {
+		for (p = sigtraps, i = NSIG+1; --i >= 0; p++) {
+			if (p->trap != NULL) {
+				shprintf("trap -- ");
+				print_value_quoted(p->trap);
+				shprintf(" %s\n", p->name);
+			}
+		}
+		return 0;
+	}
+
+	/*
+	 * Use case sensitive lookup for first arg so the
+	 * command 'exit' isn't confused with the pseudo-signal
+	 * 'EXIT'.
+	 */
+	s = (gettrap(*wp, false) == NULL) ? *wp++ : NULL; /* get command */
+	if (s != NULL && s[0] == '-' && s[1] == '\0')
+		s = NULL;
+
+	/* set/clear traps */
+	while (*wp != NULL) {
+		p = gettrap(*wp++, true);
+		if (p == NULL) {
+			bi_errorf("bad signal %s", wp[-1]);
+			return 1;
+		}
+		settrap(p, s);
+	}
+	return 0;
+}
+
+int
+c_exitreturn(char **wp)
+{
+	int how = LEXIT;
+	int n;
+	char *arg;
+
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+	arg = wp[builtin_opt.optind];
+
+	if (arg) {
+		if (!getn(arg, &n)) {
+			exstat = 1;
+			warningf(true, "%s: bad number", arg);
+		} else
+			exstat = n;
+	}
+	if (wp[0][0] == 'r') { /* return */
+		struct env *ep;
+
+		/* need to tell if this is exit or return so trap exit will
+		 * work right (POSIX)
+		 */
+		for (ep = genv; ep; ep = ep->oenv)
+			if (STOP_RETURN(ep->type)) {
+				how = LRETURN;
+				break;
+			}
+	}
+
+	if (how == LEXIT && !really_exit && j_stopped_running()) {
+		really_exit = 1;
+		how = LSHELL;
+	}
+
+	quitenv(NULL);	/* get rid of any i/o redirections */
+	unwind(how);
+	/* NOTREACHED */
+	return 0;
+}
+
+int
+c_brkcont(char **wp)
+{
+	int n, quit;
+	struct env *ep, *last_ep = NULL;
+	char *arg;
+
+	if (ksh_getopt(wp, &builtin_opt, null) == '?')
+		return 1;
+	arg = wp[builtin_opt.optind];
+
+	if (!arg)
+		n = 1;
+	else if (!bi_getn(arg, &n))
+		return 1;
+	quit = n;
+	if (quit <= 0) {
+		/* at&t ksh does this for non-interactive shells only - weird */
+		bi_errorf("%s: bad value", arg);
+		return 1;
+	}
+
+	/* Stop at E_NONE, E_PARSE, E_FUNC, or E_INCL */
+	for (ep = genv; ep && !STOP_BRKCONT(ep->type); ep = ep->oenv)
+		if (ep->type == E_LOOP) {
+			if (--quit == 0)
+				break;
+			ep->flags |= EF_BRKCONT_PASS;
+			last_ep = ep;
+		}
+
+	if (quit) {
+		/* at&t ksh doesn't print a message - just does what it
+		 * can.  We print a message 'cause it helps in debugging
+		 * scripts, but don't generate an error (ie, keep going).
+		 */
+		if (n == quit) {
+			warningf(true, "%s: cannot %s", wp[0], wp[0]);
+			return 0;
+		}
+		/* POSIX says if n is too big, the last enclosing loop
+		 * shall be used.  Doesn't say to print an error but we
+		 * do anyway 'cause the user messed up.
+		 */
+		if (last_ep)
+			last_ep->flags &= ~EF_BRKCONT_PASS;
+		warningf(true, "%s: can only %s %d level(s)",
+		    wp[0], wp[0], n - quit);
+	}
+
+	unwind(*wp[0] == 'b' ? LBREAK : LCONTIN);
+	/* NOTREACHED */
+}
+
+int
+c_set(char **wp)
+{
+	int argi, setargs;
+	struct block *l = genv->loc;
+	char **owp = wp;
+
+	if (wp[1] == NULL) {
+		static const char *const args [] = { "set", "-", NULL };
+		return c_typeset((char **) args);
+	}
+
+	argi = parse_args(wp, OF_SET, &setargs);
+	if (argi < 0)
+		return 1;
+	/* set $# and $* */
+	if (setargs) {
+		owp = wp += argi - 1;
+		wp[0] = l->argv[0]; /* save $0 */
+		while (*++wp != NULL)
+			*wp = str_save(*wp, &l->area);
+		l->argc = wp - owp - 1;
+		l->argv = areallocarray(NULL, l->argc+2, sizeof(char *), &l->area);
+		for (wp = l->argv; (*wp++ = *owp++) != NULL; )
+			;
+	}
+	/* POSIX says set exit status is 0, but old scripts that use
+	 * getopt(1), use the construct: set -- `getopt ab:c "$@"`
+	 * which assumes the exit value set will be that of the ``
+	 * (subst_exstat is cleared in execute() so that it will be 0
+	 * if there are no command substitutions).
+	 */
+	return Flag(FPOSIX) ? 0 : subst_exstat;
+}
+
+int
+c_unset(char **wp)
+{
+	char *id;
+	int optc, unset_var = 1;
+
+	while ((optc = ksh_getopt(wp, &builtin_opt, "fv")) != -1)
+		switch (optc) {
+		case 'f':
+			unset_var = 0;
+			break;
+		case 'v':
+			unset_var = 1;
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+	for (; (id = *wp) != NULL; wp++)
+		if (unset_var) {	/* unset variable */
+			struct tbl *vp = global(id);
+
+			if ((vp->flag&RDONLY)) {
+				bi_errorf("%s is read only", vp->name);
+				return 1;
+			}
+			unset(vp, strchr(id, '[') ? 1 : 0);
+		} else {		/* unset function */
+			define(id, NULL);
+		}
+	return 0;
+}
+
+static void
+p_tv(struct shf *shf, int posix, struct timeval *tv, int width, char *prefix,
+    char *suffix)
+{
+	if (posix)
+		shf_fprintf(shf, "%s%*lld.%02ld%s", prefix ? prefix : "",
+		    width, (long long)tv->tv_sec, tv->tv_usec / 10000, suffix);
+	else
+		shf_fprintf(shf, "%s%*lldm%02lld.%02lds%s", prefix ? prefix : "",
+		    width, (long long)tv->tv_sec / 60,
+		    (long long)tv->tv_sec % 60,
+		    tv->tv_usec / 10000, suffix);
+}
+
+static void
+p_ts(struct shf *shf, int posix, struct timespec *ts, int width, char *prefix,
+    char *suffix)
+{
+	if (posix)
+		shf_fprintf(shf, "%s%*lld.%02ld%s", prefix ? prefix : "",
+		    width, (long long)ts->tv_sec, ts->tv_nsec / 10000000,
+		    suffix);
+	else
+		shf_fprintf(shf, "%s%*lldm%02lld.%02lds%s", prefix ? prefix : "",
+		    width, (long long)ts->tv_sec / 60,
+		    (long long)ts->tv_sec % 60,
+		    ts->tv_nsec / 10000000, suffix);
+}
+
+
+int
+c_times(char **wp)
+{
+	struct rusage usage;
+
+	(void) getrusage(RUSAGE_SELF, &usage);
+	p_tv(shl_stdout, 0, &usage.ru_utime, 0, NULL, " ");
+	p_tv(shl_stdout, 0, &usage.ru_stime, 0, NULL, "\n");
+
+	(void) getrusage(RUSAGE_CHILDREN, &usage);
+	p_tv(shl_stdout, 0, &usage.ru_utime, 0, NULL, " ");
+	p_tv(shl_stdout, 0, &usage.ru_stime, 0, NULL, "\n");
+
+	return 0;
+}
+
+/*
+ * time pipeline (really a statement, not a built-in command)
+ */
+int
+timex(struct op *t, int f, volatile int *xerrok)
+{
+#define TF_NOARGS	BIT(0)
+#define TF_NOREAL	BIT(1)		/* don't report real time */
+#define TF_POSIX	BIT(2)		/* report in posix format */
+	int rv = 0;
+	struct rusage ru0, ru1, cru0, cru1;
+	struct timeval usrtime, systime;
+	struct timespec ts0, ts1, ts2;
+	int tf = 0;
+	extern struct timeval j_usrtime, j_systime; /* computed by j_wait */
+
+	clock_gettime(CLOCK_MONOTONIC, &ts0);
+	getrusage(RUSAGE_SELF, &ru0);
+	getrusage(RUSAGE_CHILDREN, &cru0);
+	if (t->left) {
+		/*
+		 * Two ways of getting cpu usage of a command: just use t0
+		 * and t1 (which will get cpu usage from other jobs that
+		 * finish while we are executing t->left), or get the
+		 * cpu usage of t->left. at&t ksh does the former, while
+		 * pdksh tries to do the later (the j_usrtime hack doesn't
+		 * really work as it only counts the last job).
+		 */
+		timerclear(&j_usrtime);
+		timerclear(&j_systime);
+		rv = execute(t->left, f | XTIME, xerrok);
+		if (t->left->type == TCOM)
+			tf |= t->left->str[0];
+		clock_gettime(CLOCK_MONOTONIC, &ts1);
+		getrusage(RUSAGE_SELF, &ru1);
+		getrusage(RUSAGE_CHILDREN, &cru1);
+	} else
+		tf = TF_NOARGS;
+
+	if (tf & TF_NOARGS) { /* ksh93 - report shell times (shell+kids) */
+		tf |= TF_NOREAL;
+		timeradd(&ru0.ru_utime, &cru0.ru_utime, &usrtime);
+		timeradd(&ru0.ru_stime, &cru0.ru_stime, &systime);
+	} else {
+		timersub(&ru1.ru_utime, &ru0.ru_utime, &usrtime);
+		timeradd(&usrtime, &j_usrtime, &usrtime);
+		timersub(&ru1.ru_stime, &ru0.ru_stime, &systime);
+		timeradd(&systime, &j_systime, &systime);
+	}
+
+	if (!(tf & TF_NOREAL)) {
+		timespecsub(&ts1, &ts0, &ts2);
+		if (tf & TF_POSIX)
+			p_ts(shl_out, 1, &ts2, 5, "real ", "\n");
+		else
+			p_ts(shl_out, 0, &ts2, 5, NULL, " real ");
+	}
+	if (tf & TF_POSIX)
+		p_tv(shl_out, 1, &usrtime, 5, "user ", "\n");
+	else
+		p_tv(shl_out, 0, &usrtime, 5, NULL, " user ");
+	if (tf & TF_POSIX)
+		p_tv(shl_out, 1, &systime, 5, "sys  ", "\n");
+	else
+		p_tv(shl_out, 0, &systime, 5, NULL, " system\n");
+	shf_flush(shl_out);
+
+	return rv;
+}
+
+void
+timex_hook(struct op *t, char **volatile *app)
+{
+	char **wp = *app;
+	int optc;
+	int i, j;
+	Getopt opt;
+
+	ksh_getopt_reset(&opt, 0);
+	opt.optind = 0;	/* start at the start */
+	while ((optc = ksh_getopt(wp, &opt, ":p")) != -1)
+		switch (optc) {
+		case 'p':
+			t->str[0] |= TF_POSIX;
+			break;
+		case '?':
+			errorf("time: -%s unknown option", opt.optarg);
+		case ':':
+			errorf("time: -%s requires an argument",
+			    opt.optarg);
+		}
+	/* Copy command words down over options. */
+	if (opt.optind != 0) {
+		for (i = 0; i < opt.optind; i++)
+			afree(wp[i], ATEMP);
+		for (i = 0, j = opt.optind; (wp[i] = wp[j]); i++, j++)
+			;
+	}
+	if (!wp[0])
+		t->str[0] |= TF_NOARGS;
+	*app = wp;
+}
+
+/* exec with no args - args case is taken care of in comexec() */
+int
+c_exec(char **wp)
+{
+	int i;
+
+	/* make sure redirects stay in place */
+	if (genv->savefd != NULL) {
+		for (i = 0; i < NUFILE; i++) {
+			if (genv->savefd[i] > 0)
+				close(genv->savefd[i]);
+			/*
+			 * For ksh keep anything > 2 private,
+			 * for sh, let them be (POSIX says what
+			 * happens is unspecified and the bourne shell
+			 * keeps them open).
+			 */
+			if (!Flag(FSH) && i > 2 && genv->savefd[i])
+				fcntl(i, F_SETFD, FD_CLOEXEC);
+		}
+		genv->savefd = NULL;
+	}
+	return 0;
+}
+
+/* dummy function, special case in comexec() */
+int
+c_builtin(char **wp)
+{
+	return 0;
+}
+
+extern	int c_test(char **wp);			/* in c_test.c */
+extern	int c_ulimit(char **wp);		/* in c_ulimit.c */
+
+/* A leading = means assignments before command are kept;
+ * a leading * means a POSIX special builtin;
+ * a leading + means a POSIX regular builtin
+ * (* and + should not be combined).
+ */
+const struct builtin shbuiltins [] = {
+	{"*=.", c_dot},
+	{"*=:", c_label},
+	{"[", c_test},
+	{"*=break", c_brkcont},
+	{"=builtin", c_builtin},
+	{"*=continue", c_brkcont},
+	{"*=eval", c_eval},
+	{"*=exec", c_exec},
+	{"*=exit", c_exitreturn},
+	{"+false", c_label},
+	{"*=return", c_exitreturn},
+	{"*=set", c_set},
+	{"*=shift", c_shift},
+	{"*=times", c_times},
+	{"*=trap", c_trap},
+	{"+=wait", c_wait},
+	{"+read", c_read},
+	{"test", c_test},
+	{"+true", c_label},
+	{"ulimit", c_ulimit},
+	{"+umask", c_umask},
+	{"*=unset", c_unset},
+	{NULL, NULL}
+};
diff --git a/c_test.c b/c_test.c
@@ -0,0 +1,555 @@
+/*	$OpenBSD: c_test.c,v 1.25 2018/04/09 17:53:36 tobias Exp $	*/
+
+/*
+ * test(1); version 7-like  --  author Erik Baalbergen
+ * modified by Eric Gisin to be used as built-in.
+ * modified by Arnold Robbins to add SVR3 compatibility
+ * (-x -c -b -p -u -g -k) plus Korn's -L -nt -ot -ef and new -S (socket).
+ * modified by Michael Rendell to add Korn's [[ .. ]] expressions.
+ * modified by J.T. Conklin to add POSIX compatibility.
+ */
+
+#include <sys/stat.h>
+
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+#include "c_test.h"
+
+/* test(1) accepts the following grammar:
+	oexpr	::= aexpr | aexpr "-o" oexpr ;
+	aexpr	::= nexpr | nexpr "-a" aexpr ;
+	nexpr	::= primary | "!" nexpr ;
+	primary	::= unary-operator operand
+		| operand binary-operator operand
+		| operand
+		| "(" oexpr ")"
+		;
+
+	unary-operator ::= "-a"|"-r"|"-w"|"-x"|"-e"|"-f"|"-d"|"-c"|"-b"|"-p"|
+			   "-u"|"-g"|"-k"|"-s"|"-t"|"-z"|"-n"|"-o"|"-O"|"-G"|
+			   "-L"|"-h"|"-S"|"-H";
+
+	binary-operator ::= "="|"=="|"!="|"-eq"|"-ne"|"-ge"|"-gt"|"-le"|"-lt"|
+			    "-nt"|"-ot"|"-ef"|
+			    "<"|">"	# rules used for [[ .. ]] expressions
+			    ;
+	operand ::= <any thing>
+*/
+
+#define T_ERR_EXIT	2	/* POSIX says > 1 for errors */
+
+struct t_op {
+	char	op_text[4];
+	Test_op	op_num;
+};
+static const struct t_op u_ops [] = {
+	{"-a",	TO_FILAXST },
+	{"-b",	TO_FILBDEV },
+	{"-c",	TO_FILCDEV },
+	{"-d",	TO_FILID },
+	{"-e",	TO_FILEXST },
+	{"-f",	TO_FILREG },
+	{"-G",	TO_FILGID },
+	{"-g",	TO_FILSETG },
+	{"-h",	TO_FILSYM },
+	{"-H",	TO_FILCDF },
+	{"-k",	TO_FILSTCK },
+	{"-L",	TO_FILSYM },
+	{"-n",	TO_STNZE },
+	{"-O",	TO_FILUID },
+	{"-o",	TO_OPTION },
+	{"-p",	TO_FILFIFO },
+	{"-r",	TO_FILRD },
+	{"-s",	TO_FILGZ },
+	{"-S",	TO_FILSOCK },
+	{"-t",	TO_FILTT },
+	{"-u",	TO_FILSETU },
+	{"-w",	TO_FILWR },
+	{"-x",	TO_FILEX },
+	{"-z",	TO_STZER },
+	{"",	TO_NONOP }
+};
+static const struct t_op b_ops [] = {
+	{"=",	TO_STEQL },
+	{"==",	TO_STEQL },
+	{"!=",	TO_STNEQ },
+	{"<",	TO_STLT },
+	{">",	TO_STGT },
+	{"-eq",	TO_INTEQ },
+	{"-ne",	TO_INTNE },
+	{"-gt",	TO_INTGT },
+	{"-ge",	TO_INTGE },
+	{"-lt",	TO_INTLT },
+	{"-le",	TO_INTLE },
+	{"-ef",	TO_FILEQ },
+	{"-nt",	TO_FILNT },
+	{"-ot",	TO_FILOT },
+	{"",	TO_NONOP }
+};
+
+static int	test_eaccess(const char *, int);
+static int	test_oexpr(Test_env *, int);
+static int	test_aexpr(Test_env *, int);
+static int	test_nexpr(Test_env *, int);
+static int	test_primary(Test_env *, int);
+static int	ptest_isa(Test_env *, Test_meta);
+static const char *ptest_getopnd(Test_env *, Test_op, int);
+static int	ptest_eval(Test_env *, Test_op, const char *,
+		    const char *, int);
+static void	ptest_error(Test_env *, int, const char *);
+
+int
+c_test(char **wp)
+{
+	int argc;
+	int res;
+	Test_env te;
+
+	te.flags = 0;
+	te.isa = ptest_isa;
+	te.getopnd = ptest_getopnd;
+	te.eval = ptest_eval;
+	te.error = ptest_error;
+
+	for (argc = 0; wp[argc]; argc++)
+		;
+
+	if (strcmp(wp[0], "[") == 0) {
+		if (strcmp(wp[--argc], "]") != 0) {
+			bi_errorf("missing ]");
+			return T_ERR_EXIT;
+		}
+	}
+
+	te.pos.wp = wp + 1;
+	te.wp_end = wp + argc;
+
+	/*
+	 * Handle the special cases from POSIX.2, section 4.62.4.
+	 * Implementation of all the rules isn't necessary since
+	 * our parser does the right thing for the omitted steps.
+	 */
+	if (argc <= 5) {
+		char **owp = wp;
+		int invert = 0;
+		Test_op	op;
+		const char *opnd1, *opnd2;
+
+		while (--argc >= 0) {
+			if ((*te.isa)(&te, TM_END))
+				return !0;
+			if (argc == 3) {
+				opnd1 = (*te.getopnd)(&te, TO_NONOP, 1);
+				if ((op = (Test_op) (*te.isa)(&te, TM_BINOP))) {
+					opnd2 = (*te.getopnd)(&te, op, 1);
+					res = (*te.eval)(&te, op, opnd1,
+					    opnd2, 1);
+					if (te.flags & TEF_ERROR)
+						return T_ERR_EXIT;
+					if (invert & 1)
+						res = !res;
+					return !res;
+				}
+				/* back up to opnd1 */
+				te.pos.wp--;
+			}
+			if (argc == 1) {
+				opnd1 = (*te.getopnd)(&te, TO_NONOP, 1);
+				/* Historically, -t by itself test if fd 1
+				 * is a file descriptor, but POSIX says its
+				 * a string test...
+				 */
+				if (!Flag(FPOSIX) && strcmp(opnd1, "-t") == 0)
+				    break;
+				res = (*te.eval)(&te, TO_STNZE, opnd1,
+				    NULL, 1);
+				if (invert & 1)
+					res = !res;
+				return !res;
+			}
+			if ((*te.isa)(&te, TM_NOT)) {
+				invert++;
+			} else
+				break;
+		}
+		te.pos.wp = owp + 1;
+	}
+
+	return test_parse(&te);
+}
+
+/*
+ * Generic test routines.
+ */
+
+Test_op
+test_isop(Test_env *te, Test_meta meta, const char *s)
+{
+	char sc1;
+	const struct t_op *otab;
+
+	otab = meta == TM_UNOP ? u_ops : b_ops;
+	if (*s) {
+		sc1 = s[1];
+		for (; otab->op_text[0]; otab++)
+			if (sc1 == otab->op_text[1] &&
+			    strcmp(s, otab->op_text) == 0 &&
+			    ((te->flags & TEF_DBRACKET) ||
+			    (otab->op_num != TO_STLT && otab->op_num != TO_STGT)))
+				return otab->op_num;
+	}
+	return TO_NONOP;
+}
+
+int
+test_eval(Test_env *te, Test_op op, const char *opnd1, const char *opnd2,
+    int do_eval)
+{
+	int res;
+	int not;
+	struct stat b1, b2;
+
+	if (!do_eval)
+		return 0;
+
+	switch ((int) op) {
+	/*
+	 * Unary Operators
+	 */
+	case TO_STNZE: /* -n */
+		return *opnd1 != '\0';
+	case TO_STZER: /* -z */
+		return *opnd1 == '\0';
+	case TO_OPTION: /* -o */
+		if ((not = *opnd1 == '!'))
+			opnd1++;
+		if ((res = option(opnd1)) < 0)
+			res = 0;
+		else {
+			res = Flag(res);
+			if (not)
+				res = !res;
+		}
+		return res;
+	case TO_FILRD: /* -r */
+		return test_eaccess(opnd1, R_OK) == 0;
+	case TO_FILWR: /* -w */
+		return test_eaccess(opnd1, W_OK) == 0;
+	case TO_FILEX: /* -x */
+		return test_eaccess(opnd1, X_OK) == 0;
+	case TO_FILAXST: /* -a */
+		return stat(opnd1, &b1) == 0;
+	case TO_FILEXST: /* -e */
+		/* at&t ksh does not appear to do the /dev/fd/ thing for
+		 * this (unless the os itself handles it)
+		 */
+		return stat(opnd1, &b1) == 0;
+	case TO_FILREG: /* -r */
+		return stat(opnd1, &b1) == 0 && S_ISREG(b1.st_mode);
+	case TO_FILID: /* -d */
+		return stat(opnd1, &b1) == 0 && S_ISDIR(b1.st_mode);
+	case TO_FILCDEV: /* -c */
+		return stat(opnd1, &b1) == 0 && S_ISCHR(b1.st_mode);
+	case TO_FILBDEV: /* -b */
+		return stat(opnd1, &b1) == 0 && S_ISBLK(b1.st_mode);
+	case TO_FILFIFO: /* -p */
+		return stat(opnd1, &b1) == 0 && S_ISFIFO(b1.st_mode);
+	case TO_FILSYM: /* -h -L */
+		return lstat(opnd1, &b1) == 0 && S_ISLNK(b1.st_mode);
+	case TO_FILSOCK: /* -S */
+		return stat(opnd1, &b1) == 0 && S_ISSOCK(b1.st_mode);
+	case TO_FILCDF:/* -H HP context dependent files (directories) */
+		return 0;
+	case TO_FILSETU: /* -u */
+		return stat(opnd1, &b1) == 0 &&
+		    (b1.st_mode & S_ISUID) == S_ISUID;
+	case TO_FILSETG: /* -g */
+		return stat(opnd1, &b1) == 0 &&
+		    (b1.st_mode & S_ISGID) == S_ISGID;
+	case TO_FILSTCK: /* -k */
+		return stat(opnd1, &b1) == 0 &&
+		    (b1.st_mode & S_ISVTX) == S_ISVTX;
+	case TO_FILGZ: /* -s */
+		return stat(opnd1, &b1) == 0 && b1.st_size > 0L;
+	case TO_FILTT: /* -t */
+		if (opnd1 && !bi_getn(opnd1, &res)) {
+			te->flags |= TEF_ERROR;
+			res = 0;
+		} else {
+			/* generate error if in FPOSIX mode? */
+			res = isatty(opnd1 ? res : 0);
+		}
+		return res;
+	case TO_FILUID: /* -O */
+		return stat(opnd1, &b1) == 0 && b1.st_uid == ksheuid;
+	case TO_FILGID: /* -G */
+		return stat(opnd1, &b1) == 0 && b1.st_gid == getegid();
+	/*
+	 * Binary Operators
+	 */
+	case TO_STEQL: /* = */
+		if (te->flags & TEF_DBRACKET)
+			return gmatch(opnd1, opnd2, false);
+		return strcmp(opnd1, opnd2) == 0;
+	case TO_STNEQ: /* != */
+		if (te->flags & TEF_DBRACKET)
+			return !gmatch(opnd1, opnd2, false);
+		return strcmp(opnd1, opnd2) != 0;
+	case TO_STLT: /* < */
+		return strcmp(opnd1, opnd2) < 0;
+	case TO_STGT: /* > */
+		return strcmp(opnd1, opnd2) > 0;
+	case TO_INTEQ: /* -eq */
+	case TO_INTNE: /* -ne */
+	case TO_INTGE: /* -ge */
+	case TO_INTGT: /* -gt */
+	case TO_INTLE: /* -le */
+	case TO_INTLT: /* -lt */
+		{
+			int64_t v1, v2;
+
+			if (!evaluate(opnd1, &v1, KSH_RETURN_ERROR, false) ||
+			    !evaluate(opnd2, &v2, KSH_RETURN_ERROR, false)) {
+				/* error already printed.. */
+				te->flags |= TEF_ERROR;
+				return 1;
+			}
+			switch ((int) op) {
+			case TO_INTEQ:
+				return v1 == v2;
+			case TO_INTNE:
+				return v1 != v2;
+			case TO_INTGE:
+				return v1 >= v2;
+			case TO_INTGT:
+				return v1 > v2;
+			case TO_INTLE:
+				return v1 <= v2;
+			case TO_INTLT:
+				return v1 < v2;
+			}
+		}
+	case TO_FILNT: /* -nt */
+		{
+			int s2;
+			/* ksh88/ksh93 succeed if file2 can't be stated
+			 * (subtly different from `does not exist').
+			 */
+			return stat(opnd1, &b1) == 0 &&
+			    (((s2 = stat(opnd2, &b2)) == 0 &&
+			    b1.st_mtime > b2.st_mtime) || s2 < 0);
+		}
+	case TO_FILOT: /* -ot */
+		{
+			int s1;
+			/* ksh88/ksh93 succeed if file1 can't be stated
+			 * (subtly different from `does not exist').
+			 */
+			return stat(opnd2, &b2) == 0 &&
+			    (((s1 = stat(opnd1, &b1)) == 0 &&
+			    b1.st_mtime < b2.st_mtime) || s1 < 0);
+		}
+	case TO_FILEQ: /* -ef */
+		return stat (opnd1, &b1) == 0 && stat (opnd2, &b2) == 0 &&
+		    b1.st_dev == b2.st_dev && b1.st_ino == b2.st_ino;
+	}
+	(*te->error)(te, 0, "internal error: unknown op");
+	return 1;
+}
+
+/* Routine to deal with X_OK on non-directories when running as root.
+ */
+static int
+test_eaccess(const char *path, int amode)
+{
+	int res;
+
+	res = access(path, amode);
+	/*
+	 * On most (all?) unixes, access() says everything is executable for
+	 * root - avoid this on files by using stat().
+	 */
+	if (res == 0 && ksheuid == 0 && (amode & X_OK)) {
+		struct stat statb;
+
+		if (stat(path, &statb) < 0)
+			res = -1;
+		else if (S_ISDIR(statb.st_mode))
+			res = 0;
+		else
+			res = (statb.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)) ?
+			    0 : -1;
+	}
+
+	return res;
+}
+
+int
+test_parse(Test_env *te)
+{
+	int res;
+
+	res = test_oexpr(te, 1);
+
+	if (!(te->flags & TEF_ERROR) && !(*te->isa)(te, TM_END))
+		(*te->error)(te, 0, "unexpected operator/operand");
+
+	return (te->flags & TEF_ERROR) ? T_ERR_EXIT : !res;
+}
+
+static int
+test_oexpr(Test_env *te, int do_eval)
+{
+	int res;
+
+	res = test_aexpr(te, do_eval);
+	if (res)
+		do_eval = 0;
+	if (!(te->flags & TEF_ERROR) && (*te->isa)(te, TM_OR))
+		return test_oexpr(te, do_eval) || res;
+	return res;
+}
+
+static int
+test_aexpr(Test_env *te, int do_eval)
+{
+	int res;
+
+	res = test_nexpr(te, do_eval);
+	if (!res)
+		do_eval = 0;
+	if (!(te->flags & TEF_ERROR) && (*te->isa)(te, TM_AND))
+		return test_aexpr(te, do_eval) && res;
+	return res;
+}
+
+static int
+test_nexpr(Test_env *te, int do_eval)
+{
+	if (!(te->flags & TEF_ERROR) && (*te->isa)(te, TM_NOT))
+		return !test_nexpr(te, do_eval);
+	return test_primary(te, do_eval);
+}
+
+static int
+test_primary(Test_env *te, int do_eval)
+{
+	const char *opnd1, *opnd2;
+	int res;
+	Test_op op;
+
+	if (te->flags & TEF_ERROR)
+		return 0;
+	if ((*te->isa)(te, TM_OPAREN)) {
+		res = test_oexpr(te, do_eval);
+		if (te->flags & TEF_ERROR)
+			return 0;
+		if (!(*te->isa)(te, TM_CPAREN)) {
+			(*te->error)(te, 0, "missing closing paren");
+			return 0;
+		}
+		return res;
+	}
+	/*
+	 * Binary should have precedence over unary in this case
+	 * so that something like test \( -f = -f \) is accepted
+	 */
+	if ((te->flags & TEF_DBRACKET) || (&te->pos.wp[1] < te->wp_end &&
+	    !test_isop(te, TM_BINOP, te->pos.wp[1]))) {
+		if ((op = (Test_op) (*te->isa)(te, TM_UNOP))) {
+			/* unary expression */
+			opnd1 = (*te->getopnd)(te, op, do_eval);
+			if (!opnd1) {
+				(*te->error)(te, -1, "missing argument");
+				return 0;
+			}
+
+			return (*te->eval)(te, op, opnd1, NULL,
+			    do_eval);
+		}
+	}
+	opnd1 = (*te->getopnd)(te, TO_NONOP, do_eval);
+	if (!opnd1) {
+		(*te->error)(te, 0, "expression expected");
+		return 0;
+	}
+	if ((op = (Test_op) (*te->isa)(te, TM_BINOP))) {
+		/* binary expression */
+		opnd2 = (*te->getopnd)(te, op, do_eval);
+		if (!opnd2) {
+			(*te->error)(te, -1, "missing second argument");
+			return 0;
+		}
+
+		return (*te->eval)(te, op, opnd1, opnd2, do_eval);
+	}
+	if (te->flags & TEF_DBRACKET) {
+		(*te->error)(te, -1, "missing expression operator");
+		return 0;
+	}
+	return (*te->eval)(te, TO_STNZE, opnd1, NULL, do_eval);
+}
+
+/*
+ * Plain test (test and [ .. ]) specific routines.
+ */
+
+/* Test if the current token is a whatever.  Accepts the current token if
+ * it is.  Returns 0 if it is not, non-zero if it is (in the case of
+ * TM_UNOP and TM_BINOP, the returned value is a Test_op).
+ */
+static int
+ptest_isa(Test_env *te, Test_meta meta)
+{
+	/* Order important - indexed by Test_meta values */
+	static const char *const tokens[] = {
+		"-o", "-a", "!", "(", ")"
+	};
+	int ret;
+
+	if (te->pos.wp >= te->wp_end)
+		return meta == TM_END;
+
+	if (meta == TM_UNOP || meta == TM_BINOP)
+		ret = (int) test_isop(te, meta, *te->pos.wp);
+	else if (meta == TM_END)
+		ret = 0;
+	else
+		ret = strcmp(*te->pos.wp, tokens[(int) meta]) == 0;
+
+	/* Accept the token? */
+	if (ret)
+		te->pos.wp++;
+
+	return ret;
+}
+
+static const char *
+ptest_getopnd(Test_env *te, Test_op op, int do_eval)
+{
+	if (te->pos.wp >= te->wp_end)
+		return op == TO_FILTT ? "1" : NULL;
+	return *te->pos.wp++;
+}
+
+static int
+ptest_eval(Test_env *te, Test_op op, const char *opnd1, const char *opnd2,
+    int do_eval)
+{
+	return test_eval(te, op, opnd1, opnd2, do_eval);
+}
+
+static void
+ptest_error(Test_env *te, int offset, const char *msg)
+{
+	const char *op = te->pos.wp + offset >= te->wp_end ?
+	    NULL : te->pos.wp[offset];
+
+	te->flags |= TEF_ERROR;
+	if (op)
+		bi_errorf("%s: %s", op, msg);
+	else
+		bi_errorf("%s", msg);
+}
diff --git a/c_test.h b/c_test.h
@@ -0,0 +1,53 @@
+/*	$OpenBSD: c_test.h,v 1.4 2004/12/20 11:34:26 otto Exp $	*/
+
+/* Various types of operations.  Keeping things grouped nicely
+ * (unary,binary) makes switch() statements more efficient.
+ */
+enum Test_op {
+	TO_NONOP = 0,	/* non-operator */
+	/* unary operators */
+	TO_STNZE, TO_STZER, TO_OPTION,
+	TO_FILAXST,
+	TO_FILEXST,
+	TO_FILREG, TO_FILBDEV, TO_FILCDEV, TO_FILSYM, TO_FILFIFO, TO_FILSOCK,
+	TO_FILCDF, TO_FILID, TO_FILGID, TO_FILSETG, TO_FILSTCK, TO_FILUID,
+	TO_FILRD, TO_FILGZ, TO_FILTT, TO_FILSETU, TO_FILWR, TO_FILEX,
+	/* binary operators */
+	TO_STEQL, TO_STNEQ, TO_STLT, TO_STGT, TO_INTEQ, TO_INTNE, TO_INTGT,
+	TO_INTGE, TO_INTLT, TO_INTLE, TO_FILEQ, TO_FILNT, TO_FILOT
+};
+typedef enum Test_op Test_op;
+
+/* Used by Test_env.isa() (order important - used to index *_tokens[] arrays) */
+enum Test_meta {
+	TM_OR,		/* -o or || */
+	TM_AND,		/* -a or && */
+	TM_NOT,		/* ! */
+	TM_OPAREN,	/* ( */
+	TM_CPAREN,	/* ) */
+	TM_UNOP,	/* unary operator */
+	TM_BINOP,	/* binary operator */
+	TM_END		/* end of input */
+};
+typedef enum Test_meta Test_meta;
+
+#define TEF_ERROR	BIT(0)		/* set if we've hit an error */
+#define TEF_DBRACKET	BIT(1)		/* set if [[ .. ]] test */
+
+typedef struct test_env Test_env;
+struct test_env {
+	int	flags;		/* TEF_* */
+	union {
+		char	**wp;		/* used by ptest_* */
+		XPtrV	*av;		/* used by dbtestp_* */
+	} pos;
+	char **wp_end;			/* used by ptest_* */
+	int	(*isa)(Test_env *, Test_meta);
+	const char *(*getopnd) (Test_env *, Test_op, int);
+	int	(*eval)(Test_env *, Test_op, const char *, const char *, int);
+	void	(*error)(Test_env *, int, const char *);
+};
+
+Test_op	test_isop(Test_env *, Test_meta, const char *);
+int     test_eval(Test_env *, Test_op, const char *, const char *, int);
+int	test_parse(Test_env *);
diff --git a/c_ulimit.c b/c_ulimit.c
@@ -0,0 +1,194 @@
+/*	$OpenBSD: c_ulimit.c,v 1.28 2018/04/09 17:53:36 tobias Exp $	*/
+
+/*
+	ulimit -- handle "ulimit" builtin
+
+	Reworked to use getrusage() and ulimit() at once (as needed on
+	some schizophrenic systems, eg, HP-UX 9.01), made argument parsing
+	conform to at&t ksh, added autoconf support.  Michael Rendell, May, '94
+
+	Eric Gisin, September 1988
+	Adapted to PD KornShell. Removed AT&T code.
+
+	last edit:	06-Jun-1987	D A Gwyn
+
+	This started out as the BRL UNIX System V system call emulation
+	for 4.nBSD, and was later extended by Doug Kingston to handle
+	the extended 4.nBSD resource limits.  It now includes the code
+	that was originally under case SYSULIMIT in source file "xec.c".
+*/
+
+#include <sys/types.h>
+#include <sys/resource.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <string.h>
+
+#include "sh.h"
+
+#define SOFT	0x1
+#define HARD	0x2
+
+struct limits {
+	const char *name;
+	int	resource;	/* resource to get/set */
+	int	factor;		/* multiply by to get rlim_{cur,max} values */
+	char	option;		/* option character (-d, -f, ...) */
+};
+
+static void print_ulimit(const struct limits *, int);
+static int set_ulimit(const struct limits *, const char *, int);
+
+int
+c_ulimit(char **wp)
+{
+	static const struct limits limits[] = {
+		/* Do not use options -H, -S or -a or change the order. */
+		{ "time(cpu-seconds)", RLIMIT_CPU, 1, 't' },
+		{ "file(blocks)", RLIMIT_FSIZE, 512, 'f' },
+		{ "coredump(blocks)", RLIMIT_CORE, 512, 'c' },
+		{ "data(kbytes)", RLIMIT_DATA, 1024, 'd' },
+		{ "stack(kbytes)", RLIMIT_STACK, 1024, 's' },
+		{ "lockedmem(kbytes)", RLIMIT_MEMLOCK, 1024, 'l' },
+		{ "memory(kbytes)", RLIMIT_RSS, 1024, 'm' },
+		{ "nofiles(descriptors)", RLIMIT_NOFILE, 1, 'n' },
+		{ "processes", RLIMIT_NPROC, 1, 'p' },
+		{ NULL }
+	};
+	const char	*options = "HSat#f#c#d#s#l#m#n#p#";
+	int		how = SOFT | HARD;
+	const struct limits	*l;
+	int		optc, all = 0;
+
+	/* First check for -a, -H and -S. */
+	while ((optc = ksh_getopt(wp, &builtin_opt, options)) != -1)
+		switch (optc) {
+		case 'H':
+			how = HARD;
+			break;
+		case 'S':
+			how = SOFT;
+			break;
+		case 'a':
+			all = 1;
+			break;
+		case '?':
+			return 1;
+		default:
+			break;
+		}
+
+	if (wp[builtin_opt.optind] != NULL) {
+		bi_errorf("usage: ulimit [-acdfHlmnpSst] [value]");
+		return 1;
+	}
+
+	/* Then parse and act on the actual limits, one at a time */
+	ksh_getopt_reset(&builtin_opt, GF_ERROR);
+	while ((optc = ksh_getopt(wp, &builtin_opt, options)) != -1)
+		switch (optc) {
+		case 'a':
+		case 'H':
+		case 'S':
+			break;
+		case '?':
+			return 1;
+		default:
+			for (l = limits; l->name && l->option != optc; l++)
+				;
+			if (!l->name) {
+				internal_warningf("%s: %c", __func__, optc);
+				return 1;
+			}
+			if (builtin_opt.optarg) {
+				if (set_ulimit(l, builtin_opt.optarg, how))
+					return 1;
+			} else
+				print_ulimit(l, how);
+			break;
+		}
+
+	wp += builtin_opt.optind;
+
+	if (all) {
+		for (l = limits; l->name; l++) {
+			shprintf("%-20s ", l->name);
+			print_ulimit(l, how);
+		}
+	} else if (builtin_opt.optind == 1) {
+		/* No limit specified, use file size */
+		l = &limits[1];
+		if (wp[0] != NULL) {
+			if (set_ulimit(l, wp[0], how))
+				return 1;
+			wp++;
+		} else {
+			print_ulimit(l, how);
+		}
+	}
+
+	return 0;
+}
+
+static int
+set_ulimit(const struct limits *l, const char *v, int how)
+{
+	rlim_t		val = 0;
+	struct rlimit	limit;
+
+	if (strcmp(v, "unlimited") == 0)
+		val = RLIM_INFINITY;
+	else {
+		int64_t rval;
+
+		if (!evaluate(v, &rval, KSH_RETURN_ERROR, false))
+			return 1;
+		/*
+		 * Avoid problems caused by typos that evaluate misses due
+		 * to evaluating unset parameters to 0...
+		 * If this causes problems, will have to add parameter to
+		 * evaluate() to control if unset params are 0 or an error.
+		 */
+		if (!rval && !digit(v[0])) {
+			bi_errorf("invalid limit: %s", v);
+			return 1;
+		}
+		val = (rlim_t)rval * l->factor;
+	}
+
+	getrlimit(l->resource, &limit);
+	if (how & SOFT)
+		limit.rlim_cur = val;
+	if (how & HARD)
+		limit.rlim_max = val;
+	if (setrlimit(l->resource, &limit) < 0) {
+		if (errno == EPERM)
+			bi_errorf("-%c exceeds allowable limit", l->option);
+		else
+			bi_errorf("bad -%c limit: %s", l->option,
+			    strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static void
+print_ulimit(const struct limits *l, int how)
+{
+	rlim_t		val = 0;
+	struct rlimit	limit;
+
+	getrlimit(l->resource, &limit);
+	if (how & SOFT)
+		val = limit.rlim_cur;
+	else if (how & HARD)
+		val = limit.rlim_max;
+	if (val == RLIM_INFINITY)
+		shprintf("unlimited\n");
+	else {
+		val /= l->factor;
+		shprintf("%" PRIi64 "\n", (int64_t) val);
+	}
+}
diff --git a/charclass.h b/charclass.h
@@ -0,0 +1,29 @@
+/*
+ * Public domain, 2008, Todd C. Miller <millert@openbsd.org>
+ *
+ * $OpenBSD: charclass.h,v 1.2 2019/01/25 00:19:25 millert Exp $
+ */
+
+/*
+ * POSIX character class support for fnmatch() and glob().
+ */
+static struct cclass {
+	const char *name;
+	int (*isctype)(int);
+} cclasses[] = {
+	{ "alnum",	isalnum },
+	{ "alpha",	isalpha },
+	{ "blank",	isblank },
+	{ "cntrl",	iscntrl },
+	{ "digit",	isdigit },
+	{ "graph",	isgraph },
+	{ "lower",	islower },
+	{ "print",	isprint },
+	{ "punct",	ispunct },
+	{ "space",	isspace },
+	{ "upper",	isupper },
+	{ "xdigit",	isxdigit },
+	{ NULL,		NULL }
+};
+
+#define NCCLASSES	(sizeof(cclasses) / sizeof(cclasses[0]) - 1)
diff --git a/ci.sh b/ci.sh
@@ -0,0 +1,16 @@
+#!/bin/sh -xe
+
+jobs=`nproc`
+CC=gcc-8 make -j$jobs
+make DESTDIR=dst install
+[ "`./dst/usr/bin/ksh -c 'echo $((1337 * 2))'`" -ne 2674 ] && exit 1
+[ "`./dst/usr/bin/ksh -c 'seq 1337 | sort -rn | head -n 1'`" -ne 1337 ] && exit 1
+make clean
+CC=clang-8 make -j$jobs
+
+if [ "`readlink /proc/$$/exe`" != /tmp/ksh ]
+then
+	cp -f ksh /tmp/
+	make clean
+	exec /tmp/ksh -xe $0
+fi
diff --git a/config.h b/config.h
@@ -0,0 +1,28 @@
+/*	$OpenBSD: config.h,v 1.19 2018/01/15 14:58:05 jca Exp $	*/
+
+/* config.h.  NOT generated automatically. */
+
+/*
+ * This file, config.h, which is a part of pdksh (the public domain ksh),
+ * is placed in the public domain.  It comes with no licence, warranty
+ * or guarantee of any kind (i.e., at your own risk).
+ */
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+/* Strict POSIX behaviour? */
+/* #undef POSIXLY_CORRECT */
+
+/* Specify default $ENV? */
+/* #undef DEFAULT_ENV */
+
+/*
+ * End of configuration stuff for PD ksh.
+ */
+
+#if !defined(EMACS) && !defined(VI)
+# error "Define either EMACS or VI."
+#endif
+
+#endif /* CONFIG_H */
diff --git a/edit.c b/edit.c
@@ -0,0 +1,911 @@
+/*	$OpenBSD: edit.c,v 1.67 2018/12/30 23:09:58 guenther Exp $	*/
+
+/*
+ * Command line editing - common code
+ *
+ */
+
+#include "config.h"
+
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+#include "edit.h"
+#include "tty.h"
+
+X_chars edchars;
+
+static void x_sigwinch(int);
+volatile sig_atomic_t got_sigwinch;
+static void check_sigwinch(void);
+
+static int	x_file_glob(int, const char *, int, char ***);
+static int	x_command_glob(int, const char *, int, char ***);
+static int	x_locate_word(const char *, int, int, int *, int *);
+
+
+/* Called from main */
+void
+x_init(void)
+{
+	/* set to -2 to force initial binding */
+	edchars.erase = edchars.kill = edchars.intr = edchars.quit =
+	    edchars.eof = -2;
+	/* default value for deficient systems */
+	edchars.werase = 027;	/* ^W */
+
+	if (setsig(&sigtraps[SIGWINCH], x_sigwinch, SS_RESTORE_ORIG|SS_SHTRAP))
+		sigtraps[SIGWINCH].flags |= TF_SHELL_USES;
+	got_sigwinch = 1; /* force initial check */
+	check_sigwinch();
+
+#ifdef EMACS
+	x_init_emacs();
+#endif /* EMACS */
+}
+
+static void
+x_sigwinch(int sig)
+{
+	got_sigwinch = 1;
+}
+
+static void
+check_sigwinch(void)
+{
+	if (got_sigwinch) {
+		struct winsize ws;
+
+		got_sigwinch = 0;
+		if (procpid == kshpid && ioctl(tty_fd, TIOCGWINSZ, &ws) >= 0) {
+			struct tbl *vp;
+
+			/* Do NOT export COLUMNS/LINES.  Many applications
+			 * check COLUMNS/LINES before checking ws.ws_col/row,
+			 * so if the app is started with C/L in the environ
+			 * and the window is then resized, the app won't
+			 * see the change cause the environ doesn't change.
+			 */
+			if (ws.ws_col) {
+				x_cols = ws.ws_col < MIN_COLS ? MIN_COLS :
+				    ws.ws_col;
+
+				if ((vp = typeset("COLUMNS", 0, 0, 0, 0)))
+					setint(vp, (int64_t) ws.ws_col);
+			}
+			if (ws.ws_row && (vp = typeset("LINES", 0, 0, 0, 0)))
+				setint(vp, (int64_t) ws.ws_row);
+		}
+	}
+}
+
+/*
+ * read an edited command line
+ */
+int
+x_read(char *buf, size_t len)
+{
+	int	i;
+
+	x_mode(true);
+#ifdef EMACS
+	if (Flag(FEMACS) || Flag(FGMACS))
+		i = x_emacs(buf, len);
+	else
+#endif
+#ifdef VI
+	if (Flag(FVI))
+		i = x_vi(buf, len);
+	else
+#endif
+		i = -1;		/* internal error */
+	x_mode(false);
+	check_sigwinch();
+	return i;
+}
+
+/* tty I/O */
+
+int
+x_getc(void)
+{
+	char c;
+	int n;
+
+	while ((n = blocking_read(STDIN_FILENO, &c, 1)) < 0 && errno == EINTR)
+		if (trap) {
+			x_mode(false);
+			runtraps(0);
+			x_mode(true);
+		}
+	if (n != 1)
+		return -1;
+	return (int) (unsigned char) c;
+}
+
+void
+x_flush(void)
+{
+	shf_flush(shl_out);
+}
+
+int
+x_putc(int c)
+{
+	return shf_putc(c, shl_out);
+}
+
+void
+x_puts(const char *s)
+{
+	while (*s != 0)
+		shf_putc(*s++, shl_out);
+}
+
+bool
+x_mode(bool onoff)
+{
+	static bool	x_cur_mode;
+	bool		prev;
+
+	if (x_cur_mode == onoff)
+		return x_cur_mode;
+	prev = x_cur_mode;
+	x_cur_mode = onoff;
+
+	if (onoff) {
+		struct termios	cb;
+		X_chars		oldchars;
+
+		oldchars = edchars;
+		cb = tty_state;
+
+		edchars.erase = cb.c_cc[VERASE];
+		edchars.kill = cb.c_cc[VKILL];
+		edchars.intr = cb.c_cc[VINTR];
+		edchars.quit = cb.c_cc[VQUIT];
+		edchars.eof = cb.c_cc[VEOF];
+		edchars.werase = cb.c_cc[VWERASE];
+		cb.c_iflag &= ~(INLCR|ICRNL);
+		cb.c_lflag &= ~(ISIG|ICANON|ECHO);
+		/* osf/1 processes lnext when ~icanon */
+		cb.c_cc[VLNEXT] = _POSIX_VDISABLE;
+		/* sunos 4.1.x & osf/1 processes discard(flush) when ~icanon */
+		cb.c_cc[VDISCARD] = _POSIX_VDISABLE;
+		cb.c_cc[VTIME] = 0;
+		cb.c_cc[VMIN] = 1;
+
+		tcsetattr(tty_fd, TCSADRAIN, &cb);
+
+		/* Convert unset values to internal `unset' value */
+		if (edchars.erase == _POSIX_VDISABLE)
+			edchars.erase = -1;
+		if (edchars.kill == _POSIX_VDISABLE)
+			edchars.kill = -1;
+		if (edchars.intr == _POSIX_VDISABLE)
+			edchars.intr = -1;
+		if (edchars.quit == _POSIX_VDISABLE)
+			edchars.quit = -1;
+		if (edchars.eof == _POSIX_VDISABLE)
+			edchars.eof = -1;
+		if (edchars.werase == _POSIX_VDISABLE)
+			edchars.werase = -1;
+		if (memcmp(&edchars, &oldchars, sizeof(edchars)) != 0) {
+#ifdef EMACS
+			x_emacs_keys(&edchars);
+#endif
+		}
+	} else {
+		tcsetattr(tty_fd, TCSADRAIN, &tty_state);
+	}
+
+	return prev;
+}
+
+void
+set_editmode(const char *ed)
+{
+	static const enum sh_flag edit_flags[] = {
+#ifdef EMACS
+		FEMACS, FGMACS,
+#endif
+#ifdef VI
+		FVI,
+#endif
+	};
+	char *rcp;
+	unsigned int ele;
+
+	if ((rcp = strrchr(ed, '/')))
+		ed = ++rcp;
+	for (ele = 0; ele < NELEM(edit_flags); ele++)
+		if (strstr(ed, sh_options[(int) edit_flags[ele]].name)) {
+			change_flag(edit_flags[ele], OF_SPECIAL, 1);
+			return;
+		}
+}
+
+/* ------------------------------------------------------------------------- */
+/*           Misc common code for vi/emacs				     */
+
+/* Handle the commenting/uncommenting of a line.
+ * Returns:
+ *	1 if a carriage return is indicated (comment added)
+ *	0 if no return (comment removed)
+ *	-1 if there is an error (not enough room for comment chars)
+ * If successful, *lenp contains the new length.  Note: cursor should be
+ * moved to the start of the line after (un)commenting.
+ */
+int
+x_do_comment(char *buf, int bsize, int *lenp)
+{
+	int i, j;
+	int len = *lenp;
+
+	if (len == 0)
+		return 1; /* somewhat arbitrary - it's what at&t ksh does */
+
+	/* Already commented? */
+	if (buf[0] == '#') {
+		int saw_nl = 0;
+
+		for (j = 0, i = 1; i < len; i++) {
+			if (!saw_nl || buf[i] != '#')
+				buf[j++] = buf[i];
+			saw_nl = buf[i] == '\n';
+		}
+		*lenp = j;
+		return 0;
+	} else {
+		int n = 1;
+
+		/* See if there's room for the #'s - 1 per \n */
+		for (i = 0; i < len; i++)
+			if (buf[i] == '\n')
+				n++;
+		if (len + n >= bsize)
+			return -1;
+		/* Now add them... */
+		for (i = len, j = len + n; --i >= 0; ) {
+			if (buf[i] == '\n')
+				buf[--j] = '#';
+			buf[--j] = buf[i];
+		}
+		buf[0] = '#';
+		*lenp += n;
+		return 1;
+	}
+}
+
+/* ------------------------------------------------------------------------- */
+/*           Common file/command completion code for vi/emacs	             */
+
+
+static char	*add_glob(const char *str, int slen);
+static void	glob_table(const char *pat, XPtrV *wp, struct table *tp);
+static void	glob_path(int flags, const char *pat, XPtrV *wp,
+				const char *path);
+
+void
+x_print_expansions(int nwords, char *const *words, int is_command)
+{
+	int prefix_len;
+
+	/* Check if all matches are in the same directory (in this
+	 * case, we want to omit the directory name)
+	 */
+	if (!is_command &&
+	    (prefix_len = x_longest_prefix(nwords, words)) > 0) {
+		int i;
+
+		/* Special case for 1 match (prefix is whole word) */
+		if (nwords == 1)
+			prefix_len = x_basename(words[0], NULL);
+		/* Any (non-trailing) slashes in non-common word suffixes? */
+		for (i = 0; i < nwords; i++)
+			if (x_basename(words[i] + prefix_len, NULL) >
+			    prefix_len)
+				break;
+		/* All in same directory? */
+		if (i == nwords) {
+			XPtrV l;
+
+			while (prefix_len > 0 && words[0][prefix_len - 1] != '/')
+				prefix_len--;
+			XPinit(l, nwords + 1);
+			for (i = 0; i < nwords; i++)
+				XPput(l, words[i] + prefix_len);
+			XPput(l, NULL);
+
+			/* Enumerate expansions */
+			x_putc('\r');
+			x_putc('\n');
+			pr_list((char **) XPptrv(l));
+
+			XPfree(l); /* not x_free_words() */
+			return;
+		}
+	}
+
+	/* Enumerate expansions */
+	x_putc('\r');
+	x_putc('\n');
+	pr_list(words);
+}
+
+/*
+ *  Do file globbing:
+ *	- appends * to (copy of) str if no globbing chars found
+ *	- does expansion, checks for no match, etc.
+ *	- sets *wordsp to array of matching strings
+ *	- returns number of matching strings
+ */
+static int
+x_file_glob(int flags, const char *str, int slen, char ***wordsp)
+{
+	char *toglob;
+	char **words;
+	int nwords;
+	XPtrV w;
+	struct source *s, *sold;
+
+	if (slen < 0)
+		return 0;
+
+	toglob = add_glob(str, slen);
+
+	/*
+	 * Convert "foo*" (toglob) to an array of strings (words)
+	 */
+	sold = source;
+	s = pushs(SWSTR, ATEMP);
+	s->start = s->str = toglob;
+	source = s;
+	if (yylex(ONEWORD|UNESCAPE) != LWORD) {
+		source = sold;
+		internal_warningf("%s: substitute error", __func__);
+		return 0;
+	}
+	source = sold;
+	XPinit(w, 32);
+	expand(yylval.cp, &w, DOGLOB|DOTILDE|DOMARKDIRS);
+	XPput(w, NULL);
+	words = (char **) XPclose(w);
+
+	for (nwords = 0; words[nwords]; nwords++)
+		;
+	if (nwords == 1) {
+		struct stat statb;
+
+		/* Check if file exists, also, check for empty
+		 * result - happens if we tried to glob something
+		 * which evaluated to an empty string (e.g.,
+		 * "$FOO" when there is no FOO, etc).
+		 */
+		 if ((lstat(words[0], &statb) < 0) ||
+		    words[0][0] == '\0') {
+			x_free_words(nwords, words);
+			words = NULL;
+			nwords = 0;
+		}
+	}
+	afree(toglob, ATEMP);
+
+	if (nwords) {
+		*wordsp = words;
+	} else if (words) {
+		x_free_words(nwords, words);
+		*wordsp = NULL;
+	}
+
+	return nwords;
+}
+
+/* Data structure used in x_command_glob() */
+struct path_order_info {
+	char *word;
+	int base;
+	int path_order;
+};
+
+static int path_order_cmp(const void *aa, const void *bb);
+
+/* Compare routine used in x_command_glob() */
+static int
+path_order_cmp(const void *aa, const void *bb)
+{
+	const struct path_order_info *a = (const struct path_order_info *) aa;
+	const struct path_order_info *b = (const struct path_order_info *) bb;
+	int t;
+
+	t = strcmp(a->word + a->base, b->word + b->base);
+	return t ? t : a->path_order - b->path_order;
+}
+
+static int
+x_command_glob(int flags, const char *str, int slen, char ***wordsp)
+{
+	char *toglob;
+	char *pat;
+	char *fpath;
+	int nwords;
+	XPtrV w;
+	struct block *l;
+
+	if (slen < 0)
+		return 0;
+
+	toglob = add_glob(str, slen);
+
+	/* Convert "foo*" (toglob) to a pattern for future use */
+	pat = evalstr(toglob, DOPAT|DOTILDE);
+	afree(toglob, ATEMP);
+
+	XPinit(w, 32);
+
+	glob_table(pat, &w, &keywords);
+	glob_table(pat, &w, &aliases);
+	glob_table(pat, &w, &builtins);
+	for (l = genv->loc; l; l = l->next)
+		glob_table(pat, &w, &l->funs);
+
+	glob_path(flags, pat, &w, search_path);
+	if ((fpath = str_val(global("FPATH"))) != null)
+		glob_path(flags, pat, &w, fpath);
+
+	nwords = XPsize(w);
+
+	if (!nwords) {
+		*wordsp = NULL;
+		XPfree(w);
+		return 0;
+	}
+
+	/* Sort entries */
+	if (flags & XCF_FULLPATH) {
+		/* Sort by basename, then path order */
+		struct path_order_info *info;
+		struct path_order_info *last_info = NULL;
+		char **words = (char **) XPptrv(w);
+		int path_order = 0;
+		int i;
+
+		info = areallocarray(NULL, nwords,
+		    sizeof(struct path_order_info), ATEMP);
+
+		for (i = 0; i < nwords; i++) {
+			info[i].word = words[i];
+			info[i].base = x_basename(words[i], NULL);
+			if (!last_info || info[i].base != last_info->base ||
+			    strncmp(words[i], last_info->word, info[i].base) != 0) {
+				last_info = &info[i];
+				path_order++;
+			}
+			info[i].path_order = path_order;
+		}
+		qsort(info, nwords, sizeof(struct path_order_info),
+			path_order_cmp);
+		for (i = 0; i < nwords; i++)
+			words[i] = info[i].word;
+		afree(info, ATEMP);
+	} else {
+		/* Sort and remove duplicate entries */
+		char **words = (char **) XPptrv(w);
+		int i, j;
+
+		qsortp(XPptrv(w), (size_t) nwords, xstrcmp);
+
+		for (i = j = 0; i < nwords - 1; i++) {
+			if (strcmp(words[i], words[i + 1]))
+				words[j++] = words[i];
+			else
+				afree(words[i], ATEMP);
+		}
+		words[j++] = words[i];
+		nwords = j;
+		w.cur = (void **) &words[j];
+	}
+
+	XPput(w, NULL);
+	*wordsp = (char **) XPclose(w);
+
+	return nwords;
+}
+
+#define IS_WORDC(c)	!( ctype(c, C_LEX1) || (c) == '\'' || (c) == '"' || \
+			    (c) == '`' || (c) == '=' || (c) == ':' )
+
+static int
+x_locate_word(const char *buf, int buflen, int pos, int *startp,
+    int *is_commandp)
+{
+	int p;
+	int start, end;
+
+	/* Bad call?  Probably should report error */
+	if (pos < 0 || pos > buflen) {
+		*startp = pos;
+		*is_commandp = 0;
+		return 0;
+	}
+	/* The case where pos == buflen happens to take care of itself... */
+
+	start = pos;
+	/* Keep going backwards to start of word (has effect of allowing
+	 * one blank after the end of a word)
+	 */
+	for (; (start > 0 && IS_WORDC(buf[start - 1])) ||
+	    (start > 1 && buf[start-2] == '\\'); start--)
+		;
+	/* Go forwards to end of word */
+	for (end = start; end < buflen && IS_WORDC(buf[end]); end++) {
+		if (buf[end] == '\\' && (end+1) < buflen)
+			end++;
+	}
+
+	if (is_commandp) {
+		int iscmd;
+
+		/* Figure out if this is a command */
+		for (p = start - 1; p >= 0 && isspace((unsigned char)buf[p]);
+		    p--)
+			;
+		iscmd = p < 0 || strchr(";|&()`", buf[p]);
+		if (iscmd) {
+			/* If command has a /, path, etc. is not searched;
+			 * only current directory is searched, which is just
+			 * like file globbing.
+			 */
+			for (p = start; p < end; p++)
+				if (buf[p] == '/')
+					break;
+			iscmd = p == end;
+		}
+		*is_commandp = iscmd;
+	}
+
+	*startp = start;
+
+	return end - start;
+}
+
+static int
+x_try_array(const char *buf, int buflen, const char *want, int wantlen,
+    int *nwords, char ***words)
+{
+	const char *cmd, *cp;
+	int cmdlen, n, i, slen;
+	char *name, *s;
+	struct tbl *v, *vp;
+
+	*nwords = 0;
+	*words = NULL;
+
+	/* Walk back to find start of command. */
+	if (want == buf)
+		return 0;
+	for (cmd = want; cmd > buf; cmd--) {
+		if (strchr(";|&()`", cmd[-1]) != NULL)
+			break;
+	}
+	while (cmd < want && isspace((u_char)*cmd))
+		cmd++;
+	cmdlen = 0;
+	while (cmd + cmdlen < want && !isspace((u_char)cmd[cmdlen]))
+		cmdlen++;
+	for (i = 0; i < cmdlen; i++) {
+		if (!isalnum((u_char)cmd[i]) && cmd[i] != '_')
+			return 0;
+	}
+
+	/* Take a stab at argument count from here. */
+	n = 1;
+	for (cp = cmd + cmdlen + 1; cp < want; cp++) {
+		if (!isspace((u_char)cp[-1]) && isspace((u_char)*cp))
+			n++;
+	}
+
+	/* Try to find the array. */
+	if (asprintf(&name, "complete_%.*s_%d", cmdlen, cmd, n) < 0)
+		internal_errorf("unable to allocate memory");
+	v = global(name);
+	free(name);
+	if (~v->flag & (ISSET|ARRAY)) {
+		if (asprintf(&name, "complete_%.*s", cmdlen, cmd) < 0)
+			internal_errorf("unable to allocate memory");
+		v = global(name);
+		free(name);
+		if (~v->flag & (ISSET|ARRAY))
+			return 0;
+	}
+
+	/* Walk the array and build words list. */
+	for (vp = v; vp; vp = vp->u.array) {
+		if (~vp->flag & ISSET)
+			continue;
+
+		s = str_val(vp);
+		slen = strlen(s);
+
+		if (slen < wantlen)
+			continue;
+		if (slen > wantlen)
+			slen = wantlen;
+		if (slen != 0 && strncmp(s, want, slen) != 0)
+			continue;
+
+		*words = areallocarray(*words, (*nwords) + 2, sizeof **words,
+		    ATEMP);
+		(*words)[(*nwords)++] = str_save(s, ATEMP);
+	}
+	if (*nwords != 0)
+		(*words)[*nwords] = NULL;
+
+	return *nwords != 0;
+}
+
+int
+x_cf_glob(int flags, const char *buf, int buflen, int pos, int *startp,
+    int *endp, char ***wordsp, int *is_commandp)
+{
+	int len;
+	int nwords;
+	char **words = NULL;
+	int is_command;
+
+	len = x_locate_word(buf, buflen, pos, startp, &is_command);
+	if (!(flags & XCF_COMMAND))
+		is_command = 0;
+	/* Don't do command globing on zero length strings - it takes too
+	 * long and isn't very useful.  File globs are more likely to be
+	 * useful, so allow these.
+	 */
+	if (len == 0 && is_command)
+		return 0;
+
+	if (is_command)
+		nwords = x_command_glob(flags, buf + *startp, len, &words);
+	else if (!x_try_array(buf, buflen, buf + *startp, len, &nwords, &words))
+		nwords = x_file_glob(flags, buf + *startp, len, &words);
+	if (nwords == 0) {
+		*wordsp = NULL;
+		return 0;
+	}
+
+	if (is_commandp)
+		*is_commandp = is_command;
+	*wordsp = words;
+	*endp = *startp + len;
+
+	return nwords;
+}
+
+/* Given a string, copy it and possibly add a '*' to the end.  The
+ * new string is returned.
+ */
+static char *
+add_glob(const char *str, int slen)
+{
+	char *toglob;
+	char *s;
+	bool saw_slash = false;
+
+	if (slen < 0)
+		return NULL;
+
+	toglob = str_nsave(str, slen + 1, ATEMP); /* + 1 for "*" */
+	toglob[slen] = '\0';
+
+	/*
+	 * If the pathname contains a wildcard (an unquoted '*',
+	 * '?', or '[') or parameter expansion ('$'), or a ~username
+	 * with no trailing slash, then it is globbed based on that
+	 * value (i.e., without the appended '*').
+	 */
+	for (s = toglob; *s; s++) {
+		if (*s == '\\' && s[1])
+			s++;
+		else if (*s == '*' || *s == '[' || *s == '?' || *s == '$' ||
+		    (s[1] == '(' /*)*/ && strchr("+@!", *s)))
+			break;
+		else if (*s == '/')
+			saw_slash = true;
+	}
+	if (!*s && (*toglob != '~' || saw_slash)) {
+		toglob[slen] = '*';
+		toglob[slen + 1] = '\0';
+	}
+
+	return toglob;
+}
+
+/*
+ * Find longest common prefix
+ */
+int
+x_longest_prefix(int nwords, char *const *words)
+{
+	int i, j;
+	int prefix_len;
+	char *p;
+
+	if (nwords <= 0)
+		return 0;
+
+	prefix_len = strlen(words[0]);
+	for (i = 1; i < nwords; i++)
+		for (j = 0, p = words[i]; j < prefix_len; j++)
+			if (p[j] != words[0][j]) {
+				prefix_len = j;
+				break;
+			}
+	return prefix_len;
+}
+
+void
+x_free_words(int nwords, char **words)
+{
+	int i;
+
+	for (i = 0; i < nwords; i++)
+		afree(words[i], ATEMP);
+	afree(words, ATEMP);
+}
+
+/* Return the offset of the basename of string s (which ends at se - need not
+ * be null terminated).  Trailing slashes are ignored.  If s is just a slash,
+ * then the offset is 0 (actually, length - 1).
+ *	s		Return
+ *	/etc		1
+ *	/etc/		1
+ *	/etc//		1
+ *	/etc/fo		5
+ *	foo		0
+ *	///		2
+ *			0
+ */
+int
+x_basename(const char *s, const char *se)
+{
+	const char *p;
+
+	if (se == NULL)
+		se = s + strlen(s);
+	if (s == se)
+		return 0;
+
+	/* Skip trailing slashes */
+	for (p = se - 1; p > s && *p == '/'; p--)
+		;
+	for (; p > s && *p != '/'; p--)
+		;
+	if (*p == '/' && p + 1 < se)
+		p++;
+
+	return p - s;
+}
+
+/*
+ *  Apply pattern matching to a table: all table entries that match a pattern
+ * are added to wp.
+ */
+static void
+glob_table(const char *pat, XPtrV *wp, struct table *tp)
+{
+	struct tstate ts;
+	struct tbl *te;
+
+	for (ktwalk(&ts, tp); (te = ktnext(&ts)); ) {
+		if (gmatch(te->name, pat, false))
+			XPput(*wp, str_save(te->name, ATEMP));
+	}
+}
+
+static void
+glob_path(int flags, const char *pat, XPtrV *wp, const char *path)
+{
+	const char *sp, *p;
+	char *xp;
+	int staterr;
+	int pathlen;
+	int patlen;
+	int oldsize, newsize, i, j;
+	char **words;
+	XString xs;
+
+	patlen = strlen(pat) + 1;
+	sp = path;
+	Xinit(xs, xp, patlen + 128, ATEMP);
+	while (sp) {
+		xp = Xstring(xs, xp);
+		if (!(p = strchr(sp, ':')))
+			p = sp + strlen(sp);
+		pathlen = p - sp;
+		if (pathlen) {
+			/* Copy sp into xp, stuffing any MAGIC characters
+			 * on the way
+			 */
+			const char *s = sp;
+
+			XcheckN(xs, xp, pathlen * 2);
+			while (s < p) {
+				if (ISMAGIC(*s))
+					*xp++ = MAGIC;
+				*xp++ = *s++;
+			}
+			*xp++ = '/';
+			pathlen++;
+		}
+		sp = p;
+		XcheckN(xs, xp, patlen);
+		memcpy(xp, pat, patlen);
+
+		oldsize = XPsize(*wp);
+		glob_str(Xstring(xs, xp), wp, 1); /* mark dirs */
+		newsize = XPsize(*wp);
+
+		/* Check that each match is executable... */
+		words = (char **) XPptrv(*wp);
+		for (i = j = oldsize; i < newsize; i++) {
+			staterr = 0;
+			if ((search_access(words[i], X_OK, &staterr) >= 0) ||
+			    (staterr == EISDIR)) {
+				words[j] = words[i];
+				if (!(flags & XCF_FULLPATH))
+					memmove(words[j], words[j] + pathlen,
+					    strlen(words[j] + pathlen) + 1);
+				j++;
+			} else
+				afree(words[i], ATEMP);
+		}
+		wp->cur = (void **) &words[j];
+
+		if (!*sp++)
+			break;
+	}
+	Xfree(xs, xp);
+}
+
+/*
+ * if argument string contains any special characters, they will
+ * be escaped and the result will be put into edit buffer by
+ * keybinding-specific function
+ */
+int
+x_escape(const char *s, size_t len, int (*putbuf_func) (const char *, size_t))
+{
+	size_t add, wlen;
+	const char *ifs = str_val(local("IFS", 0));
+	int rval = 0;
+
+	for (add = 0, wlen = len; wlen - add > 0; add++) {
+		if (strchr("!\"#$&'()*:;<=>?[\\]`{|}", s[add]) ||
+		    strchr(ifs, s[add])) {
+			if (putbuf_func(s, add) != 0) {
+				rval = -1;
+				break;
+			}
+
+			putbuf_func("\\", 1);
+			putbuf_func(&s[add], 1);
+
+			add++;
+			wlen -= add;
+			s += add;
+			add = -1; /* after the increment it will go to 0 */
+		}
+	}
+	if (wlen > 0 && rval == 0)
+		rval = putbuf_func(s, wlen);
+
+	return (rval);
+}
diff --git a/edit.h b/edit.h
@@ -0,0 +1,56 @@
+/*	$OpenBSD: edit.h,v 1.12 2018/06/18 17:03:58 millert Exp $	*/
+
+/* NAME:
+ *      edit.h - globals for edit modes
+ *
+ * DESCRIPTION:
+ *      This header defines various global edit objects.
+ *
+ * SEE ALSO:
+ *
+ *
+ * RCSid:
+ *      $From: edit.h,v 1.2 1994/05/19 18:32:40 michael Exp michael $
+ *
+ */
+
+#define	BEL		0x07
+
+/* tty driver characters we are interested in */
+typedef struct {
+	int erase;
+	int kill;
+	int werase;
+	int intr;
+	int quit;
+	int eof;
+} X_chars;
+
+extern X_chars edchars;
+
+/* x_cf_glob() flags */
+#define XCF_COMMAND	BIT(0)	/* Do command completion */
+#define XCF_FILE	BIT(1)	/* Do file completion */
+#define XCF_FULLPATH	BIT(2)	/* command completion: store full path */
+#define XCF_COMMAND_FILE (XCF_COMMAND|XCF_FILE)
+
+/* edit.c */
+int	x_getc(void);
+void	x_flush(void);
+int	x_putc(int);
+void	x_puts(const char *);
+bool	x_mode(bool);
+int	promptlen(const char *, const char **);
+int	x_do_comment(char *, int, int *);
+void	x_print_expansions(int, char *const *, int);
+int	x_cf_glob(int, const char *, int, int, int *, int *, char ***, int *);
+int	x_longest_prefix(int , char *const *);
+int	x_basename(const char *, const char *);
+void	x_free_words(int, char **);
+int	x_escape(const char *, size_t, int (*)(const char *, size_t));
+/* emacs.c */
+int	x_emacs(char *, size_t);
+void	x_init_emacs(void);
+void	x_emacs_keys(X_chars *);
+/* vi.c */
+int	x_vi(char *, size_t);
diff --git a/emacs.c b/emacs.c
@@ -0,0 +1,2160 @@
+/*	$OpenBSD: emacs.c,v 1.86 2019/04/03 14:55:12 jca Exp $	*/
+
+/*
+ *  Emacs-like command line editing and history
+ *
+ *  created by Ron Natalie at BRL
+ *  modified by Doug Kingston, Doug Gwyn, and Lou Salkind
+ *  adapted to PD ksh by Eric Gisin
+ *
+ * partial rewrite by Marco Peereboom <marco@openbsd.org>
+ * under the same license
+ */
+
+#include "config.h"
+#ifdef EMACS
+
+#include <sys/queue.h>
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifndef SMALL
+# include <term.h>
+# include <curses.h>
+#endif
+
+#include "sh.h"
+#include "edit.h"
+
+static	Area	aedit;
+#define	AEDIT	&aedit		/* area for kill ring and macro defns */
+
+#undef CTRL
+#define	CTRL(x)		((x) == '?' ? 0x7F : (x) & 0x1F)	/* ASCII */
+#define	UNCTRL(x)	((x) == 0x7F ? '?' : (x) | 0x40)	/* ASCII */
+
+/* values returned by keyboard functions */
+#define	KSTD	0
+#define	KEOL	1		/* ^M, ^J */
+#define	KINTR	2		/* ^G, ^C */
+
+struct	x_ftab {
+	int		(*xf_func)(int c);
+	const char	*xf_name;
+	short		xf_flags;
+};
+
+#define XF_ARG		1	/* command takes number prefix */
+#define	XF_NOBIND	2	/* not allowed to bind to function */
+#define	XF_PREFIX	4	/* function sets prefix */
+
+/* Separator for completion */
+#define	is_cfs(c)	(c == ' ' || c == '\t' || c == '"' || c == '\'')
+
+/* Separator for motion */
+#define	is_mfs(c)	(!(isalnum((unsigned char)c) || \
+			c == '_' || c == '$' || c & 0x80))
+
+/* Arguments for do_complete()
+ * 0 = enumerate  M-= complete as much as possible and then list
+ * 1 = complete   M-Esc
+ * 2 = list       M-?
+ */
+typedef enum {
+	CT_LIST,	/* list the possible completions */
+	CT_COMPLETE,	/* complete to longest prefix */
+	CT_COMPLIST	/* complete and then list (if non-exact) */
+} Comp_type;
+
+/* keybindings */
+struct kb_entry {
+	TAILQ_ENTRY(kb_entry)	entry;
+	unsigned char		*seq;
+	int			len;
+	struct x_ftab		*ftab;
+	void			*args;
+};
+TAILQ_HEAD(kb_list, kb_entry);
+struct kb_list			kblist = TAILQ_HEAD_INITIALIZER(kblist);
+
+/* { from 4.9 edit.h */
+/*
+ * The following are used for my horizontal scrolling stuff
+ */
+static char    *xbuf;		/* beg input buffer */
+static char    *xend;		/* end input buffer */
+static char    *xcp;		/* current position */
+static char    *xep;		/* current end */
+static char    *xbp;		/* start of visible portion of input buffer */
+static char    *xlp;		/* last byte visible on screen */
+static int	x_adj_ok;
+/*
+ * we use x_adj_done so that functions can tell
+ * whether x_adjust() has been called while they are active.
+ */
+static int	x_adj_done;
+
+static int	xx_cols;
+static int	x_col;
+static int	x_displen;
+static int	x_arg;		/* general purpose arg */
+static int	x_arg_defaulted;/* x_arg not explicitly set; defaulted to 1 */
+
+static int	xlp_valid;
+/* end from 4.9 edit.h } */
+static	int	x_tty;		/* are we on a tty? */
+static	int	x_bind_quiet;	/* be quiet when binding keys */
+static int	(*x_last_command)(int);
+
+static	char   **x_histp;	/* history position */
+static	int	x_nextcmd;	/* for newline-and-next */
+static	char	*xmp;		/* mark pointer */
+#define	KILLSIZE	20
+static	char	*killstack[KILLSIZE];
+static	int	killsp, killtp;
+static	int	x_literal_set;
+static	int	x_arg_set;
+static	char	*macro_args;
+static	int	prompt_skip;
+static	int	prompt_redraw;
+
+static int	x_ins(char *);
+static void	x_delete(int, int);
+static int	x_bword(void);
+static int	x_fword(void);
+static void	x_goto(char *);
+static void	x_bs(int);
+static int	x_size_str(char *);
+static int	x_size(int);
+static void	x_zots(char *);
+static void	x_zotc(int);
+static void	x_load_hist(char **);
+static int	x_search(char *, int, int);
+static int	x_match(char *, char *);
+static void	x_redraw(int);
+static void	x_push(int);
+static void	x_adjust(void);
+static void	x_e_ungetc(int);
+static int	x_e_getc(void);
+static int	x_e_getu8(char *, int);
+static void	x_e_putc(int);
+static void	x_e_puts(const char *);
+static int	x_comment(int);
+static int	x_fold_case(int);
+static char	*x_lastcp(void);
+static void	do_complete(int, Comp_type);
+static int	isu8cont(unsigned char);
+
+/* proto's for keybindings */
+static int	x_abort(int);
+static int	x_beg_hist(int);
+static int	x_clear_screen(int);
+static int	x_comp_comm(int);
+static int	x_comp_file(int);
+static int	x_complete(int);
+static int	x_del_back(int);
+static int	x_del_bword(int);
+static int	x_del_char(int);
+static int	x_del_fword(int);
+static int	x_del_line(int);
+static int	x_draw_line(int);
+static int	x_end_hist(int);
+static int	x_end_of_text(int);
+static int	x_enumerate(int);
+static int	x_eot_del(int);
+static int	x_error(int);
+static int	x_goto_hist(int);
+static int	x_ins_string(int);
+static int	x_insert(int);
+static int	x_kill(int);
+static int	x_kill_region(int);
+static int	x_list_comm(int);
+static int	x_list_file(int);
+static int	x_literal(int);
+static int	x_meta_yank(int);
+static int	x_mv_back(int);
+static int	x_mv_begin(int);
+static int	x_mv_bword(int);
+static int	x_mv_end(int);
+static int	x_mv_forw(int);
+static int	x_mv_fword(int);
+static int	x_newline(int);
+static int	x_next_com(int);
+static int	x_nl_next_com(int);
+static int	x_noop(int);
+static int	x_prev_com(int);
+static int	x_prev_histword(int);
+static int	x_search_char_forw(int);
+static int	x_search_char_back(int);
+static int	x_search_hist(int);
+static int	x_set_mark(int);
+static int	x_transpose(int);
+static int	x_xchg_point_mark(int);
+static int	x_yank(int);
+static int	x_comp_list(int);
+static int	x_expand(int);
+static int	x_fold_capitalize(int);
+static int	x_fold_lower(int);
+static int	x_fold_upper(int);
+static int	x_set_arg(int);
+static int	x_comment(int);
+#ifdef DEBUG
+static int	x_debug_info(int);
+#endif
+
+static const struct x_ftab x_ftab[] = {
+	{ x_abort,		"abort",			0 },
+	{ x_beg_hist,		"beginning-of-history",		0 },
+	{ x_clear_screen,	"clear-screen",			0 },
+	{ x_comp_comm,		"complete-command",		0 },
+	{ x_comp_file,		"complete-file",		0 },
+	{ x_complete,		"complete",			0 },
+	{ x_del_back,		"delete-char-backward",		XF_ARG },
+	{ x_del_bword,		"delete-word-backward",		XF_ARG },
+	{ x_del_char,		"delete-char-forward",		XF_ARG },
+	{ x_del_fword,		"delete-word-forward",		XF_ARG },
+	{ x_del_line,		"kill-line",			0 },
+	{ x_draw_line,		"redraw",			0 },
+	{ x_end_hist,		"end-of-history",		0 },
+	{ x_end_of_text,	"eot",				0 },
+	{ x_enumerate,		"list",				0 },
+	{ x_eot_del,		"eot-or-delete",		XF_ARG },
+	{ x_error,		"error",			0 },
+	{ x_goto_hist,		"goto-history",			XF_ARG },
+	{ x_ins_string,		"macro-string",			XF_NOBIND },
+	{ x_insert,		"auto-insert",			XF_ARG },
+	{ x_kill,		"kill-to-eol",			XF_ARG },
+	{ x_kill_region,	"kill-region",			0 },
+	{ x_list_comm,		"list-command",			0 },
+	{ x_list_file,		"list-file",			0 },
+	{ x_literal,		"quote",			0 },
+	{ x_meta_yank,		"yank-pop",			0 },
+	{ x_mv_back,		"backward-char",		XF_ARG },
+	{ x_mv_begin,		"beginning-of-line",		0 },
+	{ x_mv_bword,		"backward-word",		XF_ARG },
+	{ x_mv_end,		"end-of-line",			0 },
+	{ x_mv_forw,		"forward-char",			XF_ARG },
+	{ x_mv_fword,		"forward-word",			XF_ARG },
+	{ x_newline,		"newline",			0 },
+	{ x_next_com,		"down-history",			XF_ARG },
+	{ x_nl_next_com,	"newline-and-next",		0 },
+	{ x_noop,		"no-op",			0 },
+	{ x_prev_com,		"up-history",			XF_ARG },
+	{ x_prev_histword,	"prev-hist-word",		XF_ARG },
+	{ x_search_char_forw,	"search-character-forward",	XF_ARG },
+	{ x_search_char_back,	"search-character-backward",	XF_ARG },
+	{ x_search_hist,	"search-history",		0 },
+	{ x_set_mark,		"set-mark-command",		0 },
+	{ x_transpose,		"transpose-chars",		0 },
+	{ x_xchg_point_mark,	"exchange-point-and-mark",	0 },
+	{ x_yank,		"yank",				0 },
+	{ x_comp_list,		"complete-list",		0 },
+	{ x_expand,		"expand-file",			0 },
+	{ x_fold_capitalize,	"capitalize-word",		XF_ARG },
+	{ x_fold_lower,		"downcase-word",		XF_ARG },
+	{ x_fold_upper,		"upcase-word",			XF_ARG },
+	{ x_set_arg,		"set-arg",			XF_NOBIND },
+	{ x_comment,		"comment",			0 },
+	{ 0, 0, 0 },
+#ifdef DEBUG
+	{ x_debug_info,		"debug-info",			0 },
+#else
+	{ 0, 0, 0 },
+#endif
+	{ 0, 0, 0 },
+};
+
+int
+isu8cont(unsigned char c)
+{
+	return (c & (0x80 | 0x40)) == 0x80;
+}
+
+int
+x_emacs(char *buf, size_t len)
+{
+	struct kb_entry		*k, *kmatch = NULL;
+	char			line[LINE + 1];
+	int			at = 0, ntries = 0, submatch, ret;
+	const char		*p;
+
+	xbp = xbuf = buf; xend = buf + len;
+	xlp = xcp = xep = buf;
+	*xcp = 0;
+	xlp_valid = true;
+	xmp = NULL;
+	x_histp = histptr + 1;
+
+	xx_cols = x_cols;
+	x_col = promptlen(prompt, &p);
+	prompt_skip = p - prompt;
+	x_adj_ok = 1;
+	prompt_redraw = 1;
+	if (x_col > xx_cols)
+		x_col = x_col - (x_col / xx_cols) * xx_cols;
+	x_displen = xx_cols - 2 - x_col;
+	x_adj_done = 0;
+
+	pprompt(prompt, 0);
+	if (x_displen < 1) {
+		x_col = 0;
+		x_displen = xx_cols - 2;
+		x_e_putc('\n');
+		prompt_redraw = 0;
+	}
+
+	if (x_nextcmd >= 0) {
+		int off = source->line - x_nextcmd;
+		if (histptr - history >= off)
+			x_load_hist(histptr - off);
+		x_nextcmd = -1;
+	}
+
+	x_literal_set = 0;
+	x_arg = -1;
+	x_last_command = NULL;
+	while (1) {
+		x_flush();
+		if ((at = x_e_getu8(line, at)) < 0)
+			return 0;
+		ntries++;
+
+		if (x_arg == -1) {
+			x_arg = 1;
+			x_arg_defaulted = 1;
+		}
+
+		if (x_literal_set) {
+			/* literal, so insert it */
+			x_literal_set = 0;
+			submatch = 0;
+		} else {
+			submatch = 0;
+			kmatch = NULL;
+			TAILQ_FOREACH(k, &kblist, entry) {
+				if (at > k->len)
+					continue;
+
+				if (memcmp(k->seq, line, at) == 0) {
+					/* sub match */
+					submatch++;
+					if (k->len == at)
+						kmatch = k;
+				}
+
+				/* see if we can abort search early */
+				if (submatch > 1)
+					break;
+			}
+		}
+
+		if (submatch == 1 && kmatch) {
+			if (kmatch->ftab->xf_func == x_ins_string &&
+			    kmatch->args && !macro_args) {
+				/* treat macro string as input */
+				macro_args = kmatch->args;
+				ret = KSTD;
+			} else
+				ret = kmatch->ftab->xf_func(line[at - 1]);
+		} else {
+			if (submatch)
+				continue;
+			if (ntries > 1) {
+				ret = x_error(0); /* unmatched meta sequence */
+			} else if (at > 1) {
+				x_ins(line);
+				ret = KSTD;
+			} else {
+				ret = x_insert(line[0]);
+			}
+		}
+
+		switch (ret) {
+		case KSTD:
+			if (kmatch)
+				x_last_command = kmatch->ftab->xf_func;
+			else
+				x_last_command = NULL;
+			break;
+		case KEOL:
+			ret = xep - xbuf;
+			return (ret);
+			break;
+		case KINTR:
+			trapsig(SIGINT);
+			x_mode(false);
+			unwind(LSHELL);
+			x_arg = -1;
+			break;
+		default:
+			bi_errorf("invalid return code"); /* can't happen */
+		}
+
+		/* reset meta sequence */
+		at = ntries = 0;
+		if (x_arg_set)
+			x_arg_set = 0; /* reset args next time around */
+		else
+			x_arg = -1;
+	}
+}
+
+static int
+x_insert(int c)
+{
+	char	str[2];
+
+	/*
+	 *  Should allow tab and control chars.
+	 */
+	if (c == 0) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+	str[0] = c;
+	str[1] = '\0';
+	while (x_arg--)
+		x_ins(str);
+	return KSTD;
+}
+
+static int
+x_ins_string(int c)
+{
+	return x_insert(c);
+}
+
+static int
+x_do_ins(const char *cp, size_t len)
+{
+	if (xep+len >= xend) {
+		x_e_putc(BEL);
+		return -1;
+	}
+
+	memmove(xcp+len, xcp, xep - xcp + 1);
+	memmove(xcp, cp, len);
+	xcp += len;
+	xep += len;
+	return 0;
+}
+
+static int
+x_ins(char *s)
+{
+	char	*cp = xcp;
+	int	adj = x_adj_done;
+
+	if (x_do_ins(s, strlen(s)) < 0)
+		return -1;
+	/*
+	 * x_zots() may result in a call to x_adjust()
+	 * we want xcp to reflect the new position.
+	 */
+	xlp_valid = false;
+	x_lastcp();
+	x_adj_ok = (xcp >= xlp);
+	x_zots(cp);
+	if (adj == x_adj_done) {	/* has x_adjust() been called? */
+		/* no */
+		for (cp = xlp; cp > xcp; )
+			x_bs(*--cp);
+	}
+
+	x_adj_ok = 1;
+	return 0;
+}
+
+static int
+x_del_back(int c)
+{
+	int col = xcp - xbuf;
+
+	if (col == 0) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+	if (x_arg > col)
+		x_arg = col;
+	while (x_arg < col && isu8cont(xcp[-x_arg]))
+		x_arg++;
+	x_goto(xcp - x_arg);
+	x_delete(x_arg, false);
+	return KSTD;
+}
+
+static int
+x_del_char(int c)
+{
+	int nleft = xep - xcp;
+
+	if (!nleft) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+	if (x_arg > nleft)
+		x_arg = nleft;
+	while (x_arg < nleft && isu8cont(xcp[x_arg]))
+		x_arg++;
+	x_delete(x_arg, false);
+	return KSTD;
+}
+
+/* Delete nc bytes to the right of the cursor (including cursor position) */
+static void
+x_delete(int nc, int push)
+{
+	int	i,j;
+	char	*cp;
+
+	if (nc == 0)
+		return;
+	if (xmp != NULL && xmp > xcp) {
+		if (xcp + nc > xmp)
+			xmp = xcp;
+		else
+			xmp -= nc;
+	}
+
+	/*
+	 * This lets us yank a word we have deleted.
+	 */
+	if (push)
+		x_push(nc);
+
+	xep -= nc;
+	cp = xcp;
+	j = 0;
+	i = nc;
+	while (i--) {
+		j += x_size((unsigned char)*cp++);
+	}
+	memmove(xcp, xcp+nc, xep - xcp + 1);	/* Copies the null */
+	x_adj_ok = 0;			/* don't redraw */
+	xlp_valid = false;
+	x_zots(xcp);
+	/*
+	 * if we are already filling the line,
+	 * there is no need to ' ','\b'.
+	 * But if we must, make sure we do the minimum.
+	 */
+	if ((i = xx_cols - 2 - x_col) > 0) {
+		j = (j < i) ? j : i;
+		i = j;
+		while (i--)
+			x_e_putc(' ');
+		i = j;
+		while (i--)
+			x_e_putc('\b');
+	}
+	/*x_goto(xcp);*/
+	x_adj_ok = 1;
+	xlp_valid = false;
+	for (cp = x_lastcp(); cp > xcp; )
+		x_bs(*--cp);
+
+	return;
+}
+
+static int
+x_del_bword(int c)
+{
+	x_delete(x_bword(), true);
+	return KSTD;
+}
+
+static int
+x_mv_bword(int c)
+{
+	(void)x_bword();
+	return KSTD;
+}
+
+static int
+x_mv_fword(int c)
+{
+	x_goto(xcp + x_fword());
+	return KSTD;
+}
+
+static int
+x_del_fword(int c)
+{
+	x_delete(x_fword(), true);
+	return KSTD;
+}
+
+static int
+x_bword(void)
+{
+	int	nc = 0;
+	char	*cp = xcp;
+
+	if (cp == xbuf) {
+		x_e_putc(BEL);
+		return 0;
+	}
+	while (x_arg--) {
+		while (cp != xbuf && is_mfs(cp[-1])) {
+			cp--;
+			nc++;
+		}
+		while (cp != xbuf && !is_mfs(cp[-1])) {
+			cp--;
+			nc++;
+		}
+	}
+	x_goto(cp);
+	return nc;
+}
+
+static int
+x_fword(void)
+{
+	int	nc = 0;
+	char	*cp = xcp;
+
+	if (cp == xep) {
+		x_e_putc(BEL);
+		return 0;
+	}
+	while (x_arg--) {
+		while (cp != xep && is_mfs(*cp)) {
+			cp++;
+			nc++;
+		}
+		while (cp != xep && !is_mfs(*cp)) {
+			cp++;
+			nc++;
+		}
+	}
+	return nc;
+}
+
+static void
+x_goto(char *cp)
+{
+	if (cp < xbp || cp >= (xbp + x_displen)) {
+		/* we are heading off screen */
+		xcp = cp;
+		x_adjust();
+	} else if (cp < xcp) {		/* move back */
+		while (cp < xcp)
+			x_bs((unsigned char)*--xcp);
+	} else if (cp > xcp) {		/* move forward */
+		while (cp > xcp)
+			x_zotc((unsigned char)*xcp++);
+	}
+}
+
+static void
+x_bs(int c)
+{
+	int i;
+
+	i = x_size(c);
+	while (i--)
+		x_e_putc('\b');
+}
+
+static int
+x_size_str(char *cp)
+{
+	int size = 0;
+	while (*cp)
+		size += x_size(*cp++);
+	return size;
+}
+
+static int
+x_size(int c)
+{
+	if (c=='\t')
+		return 4;	/* Kludge, tabs are always four spaces. */
+	if (iscntrl(c))		/* control char */
+		return 2;
+	if (isu8cont(c))
+		return 0;
+	return 1;
+}
+
+static void
+x_zots(char *str)
+{
+	int	adj = x_adj_done;
+
+	if (str > xbuf && isu8cont(*str)) {
+		while (str > xbuf && isu8cont(*str))
+			str--;
+		x_e_putc('\b');
+	}
+	x_lastcp();
+	while (*str && str < xlp && adj == x_adj_done)
+		x_zotc(*str++);
+}
+
+static void
+x_zotc(int c)
+{
+	if (c == '\t') {
+		/*  Kludge, tabs are always four spaces.  */
+		x_e_puts("    ");
+	} else if (iscntrl(c)) {
+		x_e_putc('^');
+		x_e_putc(UNCTRL(c));
+	} else
+		x_e_putc(c);
+}
+
+static int
+x_mv_back(int c)
+{
+	int col = xcp - xbuf;
+
+	if (col == 0) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+	if (x_arg > col)
+		x_arg = col;
+	while (x_arg < col && isu8cont(xcp[-x_arg]))
+		x_arg++;
+	x_goto(xcp - x_arg);
+	return KSTD;
+}
+
+static int
+x_mv_forw(int c)
+{
+	int nleft = xep - xcp;
+
+	if (!nleft) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+	if (x_arg > nleft)
+		x_arg = nleft;
+	while (x_arg < nleft && isu8cont(xcp[x_arg]))
+		x_arg++;
+	x_goto(xcp + x_arg);
+	return KSTD;
+}
+
+static int
+x_search_char_forw(int c)
+{
+	char *cp = xcp;
+
+	*xep = '\0';
+	c = x_e_getc();
+	while (x_arg--) {
+		if (c < 0 ||
+		    ((cp = (cp == xep) ? NULL : strchr(cp + 1, c)) == NULL &&
+		    (cp = strchr(xbuf, c)) == NULL)) {
+			x_e_putc(BEL);
+			return KSTD;
+		}
+	}
+	x_goto(cp);
+	return KSTD;
+}
+
+static int
+x_search_char_back(int c)
+{
+	char *cp = xcp, *p;
+
+	c = x_e_getc();
+	for (; x_arg--; cp = p)
+		for (p = cp; ; ) {
+			if (p-- == xbuf)
+				p = xep;
+			if (c < 0 || p == cp) {
+				x_e_putc(BEL);
+				return KSTD;
+			}
+			if (*p == c)
+				break;
+		}
+	x_goto(cp);
+	return KSTD;
+}
+
+static int
+x_newline(int c)
+{
+	x_e_putc('\r');
+	x_e_putc('\n');
+	x_flush();
+	*xep++ = '\n';
+	return KEOL;
+}
+
+static int
+x_end_of_text(int c)
+{
+	x_zotc(edchars.eof);
+	x_putc('\r');
+	x_putc('\n');
+	x_flush();
+	return KEOL;
+}
+
+static int x_beg_hist(int c) { x_load_hist(history); return KSTD;}
+
+static int x_end_hist(int c) { x_load_hist(histptr); return KSTD;}
+
+static int x_prev_com(int c) { x_load_hist(x_histp - x_arg); return KSTD;}
+
+static int x_next_com(int c) { x_load_hist(x_histp + x_arg); return KSTD;}
+
+/* Goto a particular history number obtained from argument.
+ * If no argument is given history 1 is probably not what you
+ * want so we'll simply go to the oldest one.
+ */
+static int
+x_goto_hist(int c)
+{
+	if (x_arg_defaulted)
+		x_load_hist(history);
+	else
+		x_load_hist(histptr + x_arg - source->line);
+	return KSTD;
+}
+
+static void
+x_load_hist(char **hp)
+{
+	int	oldsize;
+
+	if (hp < history || hp > histptr) {
+		x_e_putc(BEL);
+		return;
+	}
+	x_histp = hp;
+	oldsize = x_size_str(xbuf);
+	strlcpy(xbuf, *hp, xend - xbuf);
+	xbp = xbuf;
+	xep = xcp = xbuf + strlen(xbuf);
+	xlp_valid = false;
+	if (xep <= x_lastcp())
+		x_redraw(oldsize);
+	x_goto(xep);
+}
+
+static int
+x_nl_next_com(int c)
+{
+	x_nextcmd = source->line - (histptr - x_histp) + 1;
+	return (x_newline(c));
+}
+
+static int
+x_eot_del(int c)
+{
+	if (xep == xbuf && x_arg_defaulted)
+		return (x_end_of_text(c));
+	else
+		return (x_del_char(c));
+}
+
+static void *
+kb_find_hist_func(char c)
+{
+	struct kb_entry		*k;
+	char			line[LINE + 1];
+
+	line[0] = c;
+	line[1] = '\0';
+	TAILQ_FOREACH(k, &kblist, entry)
+		if (!strcmp(k->seq, line))
+			return (k->ftab->xf_func);
+
+	return (x_insert);
+}
+
+/* reverse incremental history search */
+static int
+x_search_hist(int c)
+{
+	int offset = -1;	/* offset of match in xbuf, else -1 */
+	char pat [256+1];	/* pattern buffer */
+	char *p = pat;
+	int (*f)(int);
+
+	*p = '\0';
+	while (1) {
+		if (offset < 0) {
+			x_e_puts("\nI-search: ");
+			x_e_puts(pat);
+		}
+		x_flush();
+		if ((c = x_e_getc()) < 0)
+			return KSTD;
+		f = kb_find_hist_func(c);
+		if (c == CTRL('[')) {
+			x_e_ungetc(c);
+			break;
+		} else if (f == x_search_hist)
+			offset = x_search(pat, 0, offset);
+		else if (f == x_del_back) {
+			if (p == pat) {
+				offset = -1;
+				break;
+			}
+			if (p > pat)
+				*--p = '\0';
+			if (p == pat)
+				offset = -1;
+			else
+				offset = x_search(pat, 1, offset);
+			continue;
+		} else if (f == x_insert) {
+			/* add char to pattern */
+			/* overflow check... */
+			if (p >= &pat[sizeof(pat) - 1]) {
+				x_e_putc(BEL);
+				continue;
+			}
+			*p++ = c, *p = '\0';
+			if (offset >= 0) {
+				/* already have partial match */
+				offset = x_match(xbuf, pat);
+				if (offset >= 0) {
+					x_goto(xbuf + offset + (p - pat) -
+					    (*pat == '^'));
+					continue;
+				}
+			}
+			offset = x_search(pat, 0, offset);
+		} else { /* other command */
+			x_e_ungetc(c);
+			break;
+		}
+	}
+	if (offset < 0)
+		x_redraw(-1);
+	return KSTD;
+}
+
+/* search backward from current line */
+static int
+x_search(char *pat, int sameline, int offset)
+{
+	char **hp;
+	int i;
+
+	for (hp = x_histp - (sameline ? 0 : 1) ; hp >= history; --hp) {
+		i = x_match(*hp, pat);
+		if (i >= 0) {
+			if (offset < 0)
+				x_e_putc('\n');
+			x_load_hist(hp);
+			x_goto(xbuf + i + strlen(pat) - (*pat == '^'));
+			return i;
+		}
+	}
+	x_e_putc(BEL);
+	x_histp = histptr;
+	return -1;
+}
+
+/* return position of first match of pattern in string, else -1 */
+static int
+x_match(char *str, char *pat)
+{
+	if (*pat == '^') {
+		return (strncmp(str, pat+1, strlen(pat+1)) == 0) ? 0 : -1;
+	} else {
+		char *q = strstr(str, pat);
+		return (q == NULL) ? -1 : q - str;
+	}
+}
+
+static int
+x_del_line(int c)
+{
+	int	i, j;
+
+	*xep = 0;
+	i = xep - xbuf;
+	j = x_size_str(xbuf);
+	xcp = xbuf;
+	x_push(i);
+	xlp = xbp = xep = xbuf;
+	xlp_valid = true;
+	*xcp = 0;
+	xmp = NULL;
+	x_redraw(j);
+	return KSTD;
+}
+
+static int
+x_mv_end(int c)
+{
+	x_goto(xep);
+	return KSTD;
+}
+
+static int
+x_mv_begin(int c)
+{
+	x_goto(xbuf);
+	return KSTD;
+}
+
+static int
+x_draw_line(int c)
+{
+	x_redraw(-1);
+	return KSTD;
+}
+
+static int
+x_clear_screen(int c)
+{
+	x_redraw(-2);
+	return KSTD;
+}
+
+/* Redraw (part of) the line.
+ * A non-negative limit is the screen column up to which needs
+ * redrawing. A limit of -1 redraws on a new line, while a limit
+ * of -2 (attempts to) clear the screen.
+ */
+static void
+x_redraw(int limit)
+{
+	int	i, j, truncate = 0;
+	char	*cp;
+
+	x_adj_ok = 0;
+	if (limit == -2) {
+		int cleared = 0;
+#ifndef SMALL
+		if (cur_term != NULL && clear_screen != NULL) {
+			if (tputs(clear_screen, 1, x_putc) != ERR)
+				cleared = 1;
+		}
+#endif
+		if (!cleared)
+			x_e_putc('\n');
+	}
+	else if (limit == -1)
+		x_e_putc('\n');
+	else if (limit >= 0)
+		x_e_putc('\r');
+	x_flush();
+	if (xbp == xbuf) {
+		x_col = promptlen(prompt, NULL);
+		if (x_col > xx_cols)
+			truncate = (x_col / xx_cols) * xx_cols;
+		if (prompt_redraw)
+			pprompt(prompt + prompt_skip, truncate);
+	}
+	if (x_col > xx_cols)
+		x_col = x_col - (x_col / xx_cols) * xx_cols;
+	x_displen = xx_cols - 2 - x_col;
+	if (x_displen < 1) {
+		x_col = 0;
+		x_displen = xx_cols - 2;
+	}
+	xlp_valid = false;
+	x_lastcp();
+	x_zots(xbp);
+	if (xbp != xbuf || xep > xlp)
+		limit = xx_cols;
+	if (limit >= 0) {
+		if (xep > xlp)
+			i = 0;			/* we fill the line */
+		else
+			i = limit - (xlp - xbp);
+
+		for (j = 0; j < i && x_col < (xx_cols - 2); j++)
+			x_e_putc(' ');
+		i = ' ';
+		if (xep > xlp) {		/* more off screen */
+			if (xbp > xbuf)
+				i = '*';
+			else
+				i = '>';
+		} else if (xbp > xbuf)
+			i = '<';
+		x_e_putc(i);
+		j++;
+		while (j--)
+			x_e_putc('\b');
+	}
+	for (cp = xlp; cp > xcp; )
+		x_bs(*--cp);
+	x_adj_ok = 1;
+#ifdef DEBUG
+	x_flush();
+#endif
+	return;
+}
+
+static int
+x_transpose(int c)
+{
+	char	tmp;
+
+	/* What transpose is meant to do seems to be up for debate. This
+	 * is a general summary of the options; the text is abcd with the
+	 * upper case character or underscore indicating the cursor position:
+	 *     Who			Before	After  Before	After
+	 *     at&t ksh in emacs mode:	abCd	abdC   abcd_	(bell)
+	 *     at&t ksh in gmacs mode:	abCd	baCd   abcd_	abdc_
+	 *     gnu emacs:		abCd	acbD   abcd_	abdc_
+	 * Pdksh currently goes with GNU behavior since I believe this is the
+	 * most common version of emacs, unless in gmacs mode, in which case
+	 * it does the at&t ksh gmacs mode.
+	 * This should really be broken up into 3 functions so users can bind
+	 * to the one they want.
+	 */
+	if (xcp == xbuf) {
+		x_e_putc(BEL);
+		return KSTD;
+	} else if (xcp == xep || Flag(FGMACS)) {
+		if (xcp - xbuf == 1) {
+			x_e_putc(BEL);
+			return KSTD;
+		}
+		/* Gosling/Unipress emacs style: Swap two characters before the
+		 * cursor, do not change cursor position
+		 */
+		x_bs(xcp[-1]);
+		x_bs(xcp[-2]);
+		x_zotc(xcp[-1]);
+		x_zotc(xcp[-2]);
+		tmp = xcp[-1];
+		xcp[-1] = xcp[-2];
+		xcp[-2] = tmp;
+	} else {
+		/* GNU emacs style: Swap the characters before and under the
+		 * cursor, move cursor position along one.
+		 */
+		x_bs(xcp[-1]);
+		x_zotc(xcp[0]);
+		x_zotc(xcp[-1]);
+		tmp = xcp[-1];
+		xcp[-1] = xcp[0];
+		xcp[0] = tmp;
+		x_bs(xcp[0]);
+		x_goto(xcp + 1);
+	}
+	return KSTD;
+}
+
+static int
+x_literal(int c)
+{
+	x_literal_set = 1;
+	return KSTD;
+}
+
+static int
+x_kill(int c)
+{
+	int col = xcp - xbuf;
+	int lastcol = xep - xbuf;
+	int ndel;
+
+	if (x_arg_defaulted)
+		x_arg = lastcol;
+	else if (x_arg > lastcol)
+		x_arg = lastcol;
+	while (x_arg < lastcol && isu8cont(xbuf[x_arg]))
+		x_arg++;
+	ndel = x_arg - col;
+	if (ndel < 0) {
+		x_goto(xbuf + x_arg);
+		ndel = -ndel;
+	}
+	x_delete(ndel, true);
+	return KSTD;
+}
+
+static void
+x_push(int nchars)
+{
+	char	*cp = str_nsave(xcp, nchars, AEDIT);
+	afree(killstack[killsp], AEDIT);
+	killstack[killsp] = cp;
+	killsp = (killsp + 1) % KILLSIZE;
+}
+
+static int
+x_yank(int c)
+{
+	if (killsp == 0)
+		killtp = KILLSIZE;
+	else
+		killtp = killsp;
+	killtp --;
+	if (killstack[killtp] == 0) {
+		x_e_puts("\nnothing to yank");
+		x_redraw(-1);
+		return KSTD;
+	}
+	xmp = xcp;
+	x_ins(killstack[killtp]);
+	return KSTD;
+}
+
+static int
+x_meta_yank(int c)
+{
+	int	len;
+	if ((x_last_command != x_yank && x_last_command != x_meta_yank) ||
+	    killstack[killtp] == 0) {
+		killtp = killsp;
+		x_e_puts("\nyank something first");
+		x_redraw(-1);
+		return KSTD;
+	}
+	len = strlen(killstack[killtp]);
+	x_goto(xcp - len);
+	x_delete(len, false);
+	do {
+		if (killtp == 0)
+			killtp = KILLSIZE - 1;
+		else
+			killtp--;
+	} while (killstack[killtp] == 0);
+	x_ins(killstack[killtp]);
+	return KSTD;
+}
+
+static int
+x_abort(int c)
+{
+	/* x_zotc(c); */
+	xlp = xep = xcp = xbp = xbuf;
+	xlp_valid = true;
+	*xcp = 0;
+	return KINTR;
+}
+
+static int
+x_error(int c)
+{
+	x_e_putc(BEL);
+	return KSTD;
+}
+
+static char *
+kb_encode(const char *s)
+{
+	static char		l[LINE + 1];
+	int			at = 0;
+
+	l[at] = '\0';
+	while (*s) {
+		if (*s == '^') {
+			s++;
+			if (*s >= '?')
+				l[at++] = CTRL(*s);
+			else {
+				l[at++] = '^';
+				s--;
+			}
+		} else
+			l[at++] = *s;
+		l[at] = '\0';
+		s++;
+	}
+	return (l);
+}
+
+static char *
+kb_decode(const char *s)
+{
+	static char		l[LINE + 1];
+	unsigned int		i, at = 0;
+
+	l[0] = '\0';
+	for (i = 0; i < strlen(s); i++) {
+		if (iscntrl((unsigned char)s[i])) {
+			l[at++] = '^';
+			l[at++] = UNCTRL(s[i]);
+		} else
+			l[at++] = s[i];
+		l[at] = '\0';
+	}
+
+	return (l);
+}
+
+static int
+kb_match(char *s)
+{
+	int			len = strlen(s);
+	struct kb_entry		*k;
+
+	TAILQ_FOREACH(k, &kblist, entry) {
+		if (len > k->len)
+			continue;
+
+		if (memcmp(k->seq, s, len) == 0)
+			return (1);
+	}
+
+	return (0);
+}
+
+static void
+kb_del(struct kb_entry *k)
+{
+	TAILQ_REMOVE(&kblist, k, entry);
+	free(k->args);
+	afree(k, AEDIT);
+}
+
+static struct kb_entry *
+kb_add_string(void *func, void *args, char *str)
+{
+	unsigned int		ele, count;
+	struct kb_entry		*k;
+	struct x_ftab		*xf = NULL;
+
+	for (ele = 0; ele < NELEM(x_ftab); ele++)
+		if (x_ftab[ele].xf_func == func) {
+			xf = (struct x_ftab *)&x_ftab[ele];
+			break;
+		}
+	if (xf == NULL)
+		return (NULL);
+
+	if (kb_match(str)) {
+		if (x_bind_quiet == 0)
+			bi_errorf("duplicate binding for %s", kb_decode(str));
+		return (NULL);
+	}
+	count = strlen(str);
+
+	k = alloc(sizeof *k + count + 1, AEDIT);
+	k->seq = (unsigned char *)(k + 1);
+	k->len = count;
+	k->ftab = xf;
+	k->args = args ? strdup(args) : NULL;
+
+	strlcpy(k->seq, str, count + 1);
+
+	TAILQ_INSERT_TAIL(&kblist, k, entry);
+
+	return (k);
+}
+
+static struct kb_entry *
+kb_add(void *func, ...)
+{
+	va_list			ap;
+	unsigned char		ch;
+	unsigned int		i;
+	char			line[LINE + 1];
+
+	va_start(ap, func);
+	for (i = 0; i < sizeof(line) - 1; i++) {
+		ch = va_arg(ap, unsigned int);
+		if (ch == 0)
+			break;
+		line[i] = ch;
+	}
+	va_end(ap);
+	line[i] = '\0';
+
+	return (kb_add_string(func, NULL, line));
+}
+
+static void
+kb_print(struct kb_entry *k)
+{
+	if (!(k->ftab->xf_flags & XF_NOBIND))
+		shprintf("%s = %s\n",
+		    kb_decode(k->seq), k->ftab->xf_name);
+	else if (k->args) {
+		shprintf("%s = ", kb_decode(k->seq));
+		shprintf("'%s'\n", kb_decode(k->args));
+	}
+}
+
+int
+x_bind(const char *a1, const char *a2,
+	int macro,		/* bind -m */
+	int list)		/* bind -l */
+{
+	unsigned int		i;
+	struct kb_entry		*k, *kb;
+	char			in[LINE + 1];
+
+	if (x_tty == 0) {
+		bi_errorf("cannot bind, not a tty");
+		return (1);
+	}
+
+	if (list) {
+		/* show all function names */
+		for (i = 0; i < NELEM(x_ftab); i++) {
+			if (x_ftab[i].xf_name == NULL)
+				continue;
+			if (x_ftab[i].xf_name &&
+			    !(x_ftab[i].xf_flags & XF_NOBIND))
+				shprintf("%s\n", x_ftab[i].xf_name);
+		}
+		return (0);
+	}
+
+	if (a1 == NULL) {
+		/* show all bindings */
+		TAILQ_FOREACH(k, &kblist, entry)
+			kb_print(k);
+		return (0);
+	}
+
+	snprintf(in, sizeof in, "%s", kb_encode(a1));
+	if (a2 == NULL) {
+		/* print binding */
+		TAILQ_FOREACH(k, &kblist, entry)
+			if (!strcmp(k->seq, in)) {
+				kb_print(k);
+				return (0);
+			}
+		shprintf("%s = %s\n", kb_decode(a1), "auto-insert");
+		return (0);
+	}
+
+	if (strlen(a2) == 0) {
+		/* clear binding */
+		TAILQ_FOREACH_SAFE(k, &kblist, entry, kb)
+			if (!strcmp(k->seq, in)) {
+				kb_del(k);
+				break;
+			}
+		return (0);
+	}
+
+	/* set binding */
+	if (macro) {
+		/* delete old mapping */
+		TAILQ_FOREACH_SAFE(k, &kblist, entry, kb)
+			if (!strcmp(k->seq, in)) {
+				kb_del(k);
+				break;
+			}
+		kb_add_string(x_ins_string, kb_encode(a2), in);
+		return (0);
+	}
+
+	/* set non macro binding */
+	for (i = 0; i < NELEM(x_ftab); i++) {
+		if (x_ftab[i].xf_name == NULL)
+			continue;
+		if (!strcmp(x_ftab[i].xf_name, a2)) {
+			/* delete old mapping */
+			TAILQ_FOREACH_SAFE(k, &kblist, entry, kb)
+				if (!strcmp(k->seq, in)) {
+					kb_del(k);
+					break;
+				}
+			kb_add_string(x_ftab[i].xf_func, NULL, in);
+			return (0);
+		}
+	}
+	bi_errorf("%s: no such function", a2);
+	return (1);
+}
+
+void
+x_init_emacs(void)
+{
+	x_tty = 1;
+	ainit(AEDIT);
+	x_nextcmd = -1;
+
+	TAILQ_INIT(&kblist);
+
+	/* man page order */
+	kb_add(x_abort,			CTRL('G'), 0);
+	kb_add(x_mv_back,		CTRL('B'), 0);
+	kb_add(x_mv_back,		CTRL('X'), CTRL('D'), 0);
+	kb_add(x_mv_bword,		CTRL('['), 'b', 0);
+	kb_add(x_beg_hist,		CTRL('['), '<', 0);
+	kb_add(x_mv_begin,		CTRL('A'), 0);
+	kb_add(x_fold_capitalize,	CTRL('['), 'C', 0);
+	kb_add(x_fold_capitalize,	CTRL('['), 'c', 0);
+	kb_add(x_comment,		CTRL('['), '#', 0);
+	kb_add(x_complete,		CTRL('['), CTRL('['), 0);
+	kb_add(x_comp_comm,		CTRL('X'), CTRL('['), 0);
+	kb_add(x_comp_file,		CTRL('['), CTRL('X'), 0);
+	kb_add(x_comp_list,		CTRL('I'), 0);
+	kb_add(x_comp_list,		CTRL('['), '=', 0);
+	kb_add(x_del_back,		CTRL('?'), 0);
+	kb_add(x_del_back,		CTRL('H'), 0);
+	kb_add(x_del_char,		CTRL('['), '[', '3', '~', 0); /* delete */
+	kb_add(x_del_bword,		CTRL('W'), 0);
+	kb_add(x_del_bword,		CTRL('['), CTRL('?'), 0);
+	kb_add(x_del_bword,		CTRL('['), CTRL('H'), 0);
+	kb_add(x_del_bword,		CTRL('['), 'h', 0);
+	kb_add(x_del_fword,		CTRL('['), 'd', 0);
+	kb_add(x_next_com,		CTRL('N'), 0);
+	kb_add(x_next_com,		CTRL('X'), 'B', 0);
+	kb_add(x_fold_lower,		CTRL('['), 'L', 0);
+	kb_add(x_fold_lower,		CTRL('['), 'l', 0);
+	kb_add(x_end_hist,		CTRL('['), '>', 0);
+	kb_add(x_mv_end,		CTRL('E'), 0);
+	/* how to handle: eot: ^_, underneath copied from original keybindings */
+	kb_add(x_end_of_text,		CTRL('_'), 0);
+	kb_add(x_eot_del,		CTRL('D'), 0);
+	/* error */
+	kb_add(x_xchg_point_mark,	CTRL('X'), CTRL('X'), 0);
+	kb_add(x_expand,		CTRL('['), '*', 0);
+	kb_add(x_mv_forw,		CTRL('F'), 0);
+	kb_add(x_mv_forw,		CTRL('X'), 'C', 0);
+	kb_add(x_mv_fword,		CTRL('['), 'f', 0);
+	kb_add(x_goto_hist,		CTRL('['), 'g', 0);
+	/* kill-line */
+	kb_add(x_kill,			CTRL('K'), 0);
+	kb_add(x_enumerate,		CTRL('['), '?', 0);
+	kb_add(x_list_comm,		CTRL('X'), '?', 0);
+	kb_add(x_list_file,		CTRL('X'), CTRL('Y'), 0);
+	kb_add(x_newline,		CTRL('J'), 0);
+	kb_add(x_newline,		CTRL('M'), 0);
+	kb_add(x_nl_next_com,		CTRL('O'), 0);
+	/* no-op */
+	kb_add(x_prev_histword,		CTRL('['), '.', 0);
+	kb_add(x_prev_histword,		CTRL('['), '_', 0);
+	/* how to handle: quote: ^^ */
+	kb_add(x_literal,		CTRL('^'), 0);
+	kb_add(x_clear_screen,		CTRL('L'), 0);
+	kb_add(x_search_char_back,	CTRL('['), CTRL(']'), 0);
+	kb_add(x_search_char_forw,	CTRL(']'), 0);
+	kb_add(x_search_hist,		CTRL('R'), 0);
+	kb_add(x_set_mark,		CTRL('['), ' ', 0);
+	kb_add(x_transpose,		CTRL('T'), 0);
+	kb_add(x_prev_com,		CTRL('P'), 0);
+	kb_add(x_prev_com,		CTRL('X'), 'A', 0);
+	kb_add(x_fold_upper,		CTRL('['), 'U', 0);
+	kb_add(x_fold_upper,		CTRL('['), 'u', 0);
+	kb_add(x_literal,		CTRL('V'), 0);
+	kb_add(x_yank,			CTRL('Y'), 0);
+	kb_add(x_meta_yank,		CTRL('['), 'y', 0);
+	/* man page ends here */
+
+	/* arrow keys */
+	kb_add(x_prev_com,		CTRL('['), '[', 'A', 0); /* up */
+	kb_add(x_next_com,		CTRL('['), '[', 'B', 0); /* down */
+	kb_add(x_mv_forw,		CTRL('['), '[', 'C', 0); /* right */
+	kb_add(x_mv_back,		CTRL('['), '[', 'D', 0); /* left */
+	kb_add(x_prev_com,		CTRL('['), 'O', 'A', 0); /* up */
+	kb_add(x_next_com,		CTRL('['), 'O', 'B', 0); /* down */
+	kb_add(x_mv_forw,		CTRL('['), 'O', 'C', 0); /* right */
+	kb_add(x_mv_back,		CTRL('['), 'O', 'D', 0); /* left */
+
+	/* more navigation keys */
+	kb_add(x_mv_begin,		CTRL('['), '[', 'H', 0); /* home */
+	kb_add(x_mv_end,		CTRL('['), '[', 'F', 0); /* end */
+	kb_add(x_mv_begin,		CTRL('['), 'O', 'H', 0); /* home */
+	kb_add(x_mv_end,		CTRL('['), 'O', 'F', 0); /* end */
+	kb_add(x_mv_begin,		CTRL('['), '[', '1', '~', 0); /* home */
+	kb_add(x_mv_end,		CTRL('['), '[', '4', '~', 0); /* end */
+	kb_add(x_mv_begin,		CTRL('['), '[', '7', '~', 0); /* home */
+	kb_add(x_mv_end,		CTRL('['), '[', '8', '~', 0); /* end */
+
+	/* can't be bound */
+	kb_add(x_set_arg,		CTRL('['), '0', 0);
+	kb_add(x_set_arg,		CTRL('['), '1', 0);
+	kb_add(x_set_arg,		CTRL('['), '2', 0);
+	kb_add(x_set_arg,		CTRL('['), '3', 0);
+	kb_add(x_set_arg,		CTRL('['), '4', 0);
+	kb_add(x_set_arg,		CTRL('['), '5', 0);
+	kb_add(x_set_arg,		CTRL('['), '6', 0);
+	kb_add(x_set_arg,		CTRL('['), '7', 0);
+	kb_add(x_set_arg,		CTRL('['), '8', 0);
+	kb_add(x_set_arg,		CTRL('['), '9', 0);
+
+	/* ctrl arrow keys */
+	kb_add(x_mv_end,		CTRL('['), '[', '1', ';', '5', 'A', 0); /* ctrl up */
+	kb_add(x_mv_begin,		CTRL('['), '[', '1', ';', '5', 'B', 0); /* ctrl down */
+	kb_add(x_mv_fword,		CTRL('['), '[', '1', ';', '5', 'C', 0); /* ctrl right */
+	kb_add(x_mv_bword,		CTRL('['), '[', '1', ';', '5', 'D', 0); /* ctrl left */
+}
+
+void
+x_emacs_keys(X_chars *ec)
+{
+	x_bind_quiet = 1;
+	if (ec->erase >= 0) {
+		kb_add(x_del_back, ec->erase, 0);
+		kb_add(x_del_bword, CTRL('['), ec->erase, 0);
+	}
+	if (ec->kill >= 0)
+		kb_add(x_del_line, ec->kill, 0);
+	if (ec->werase >= 0)
+		kb_add(x_del_bword, ec->werase, 0);
+	if (ec->intr >= 0)
+		kb_add(x_abort, ec->intr, 0);
+	if (ec->quit >= 0)
+		kb_add(x_noop, ec->quit, 0);
+	x_bind_quiet = 0;
+}
+
+static int
+x_set_mark(int c)
+{
+	xmp = xcp;
+	return KSTD;
+}
+
+static int
+x_kill_region(int c)
+{
+	int	rsize;
+	char	*xr;
+
+	if (xmp == NULL) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+	if (xmp > xcp) {
+		rsize = xmp - xcp;
+		xr = xcp;
+	} else {
+		rsize = xcp - xmp;
+		xr = xmp;
+	}
+	x_goto(xr);
+	x_delete(rsize, true);
+	xmp = xr;
+	return KSTD;
+}
+
+static int
+x_xchg_point_mark(int c)
+{
+	char	*tmp;
+
+	if (xmp == NULL) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+	tmp = xmp;
+	xmp = xcp;
+	x_goto( tmp );
+	return KSTD;
+}
+
+static int
+x_noop(int c)
+{
+	return KSTD;
+}
+
+/*
+ *	File/command name completion routines
+ */
+
+static int
+x_comp_comm(int c)
+{
+	do_complete(XCF_COMMAND, CT_COMPLETE);
+	return KSTD;
+}
+static int
+x_list_comm(int c)
+{
+	do_complete(XCF_COMMAND, CT_LIST);
+	return KSTD;
+}
+static int
+x_complete(int c)
+{
+	do_complete(XCF_COMMAND_FILE, CT_COMPLETE);
+	return KSTD;
+}
+static int
+x_enumerate(int c)
+{
+	do_complete(XCF_COMMAND_FILE, CT_LIST);
+	return KSTD;
+}
+static int
+x_comp_file(int c)
+{
+	do_complete(XCF_FILE, CT_COMPLETE);
+	return KSTD;
+}
+static int
+x_list_file(int c)
+{
+	do_complete(XCF_FILE, CT_LIST);
+	return KSTD;
+}
+static int
+x_comp_list(int c)
+{
+	do_complete(XCF_COMMAND_FILE, CT_COMPLIST);
+	return KSTD;
+}
+static int
+x_expand(int c)
+{
+	char **words;
+	int nwords = 0;
+	int start, end;
+	int is_command;
+	int i;
+
+	nwords = x_cf_glob(XCF_FILE, xbuf, xep - xbuf, xcp - xbuf,
+	    &start, &end, &words, &is_command);
+
+	if (nwords == 0) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+
+	x_goto(xbuf + start);
+	x_delete(end - start, false);
+	for (i = 0; i < nwords;) {
+		if (x_escape(words[i], strlen(words[i]), x_do_ins) < 0 ||
+		    (++i < nwords && x_ins(" ") < 0)) {
+			x_e_putc(BEL);
+			return KSTD;
+		}
+	}
+	x_adjust();
+
+	return KSTD;
+}
+
+/* type == 0 for list, 1 for complete and 2 for complete-list */
+static void
+do_complete(int flags,	/* XCF_{COMMAND,FILE,COMMAND_FILE} */
+    Comp_type type)
+{
+	char **words;
+	int nwords;
+	int start, end, nlen, olen;
+	int is_command;
+	int completed = 0;
+
+	nwords = x_cf_glob(flags, xbuf, xep - xbuf, xcp - xbuf,
+	    &start, &end, &words, &is_command);
+	/* no match */
+	if (nwords == 0) {
+		x_e_putc(BEL);
+		return;
+	}
+
+	if (type == CT_LIST) {
+		x_print_expansions(nwords, words, is_command);
+		x_redraw(0);
+		x_free_words(nwords, words);
+		return;
+	}
+
+	olen = end - start;
+	nlen = x_longest_prefix(nwords, words);
+	/* complete */
+	if (nwords == 1 || nlen > olen) {
+		x_goto(xbuf + start);
+		x_delete(olen, false);
+		x_escape(words[0], nlen, x_do_ins);
+		x_adjust();
+		completed = 1;
+	}
+	/* add space if single non-dir match */
+	if (nwords == 1 && words[0][nlen - 1] != '/') {
+		x_ins(" ");
+		completed = 1;
+	}
+
+	if (type == CT_COMPLIST && !completed) {
+		x_print_expansions(nwords, words, is_command);
+		completed = 1;
+	}
+
+	if (completed)
+		x_redraw(0);
+
+	x_free_words(nwords, words);
+}
+
+/* NAME:
+ *      x_adjust - redraw the line adjusting starting point etc.
+ *
+ * DESCRIPTION:
+ *      This function is called when we have exceeded the bounds
+ *      of the edit window.  It increments x_adj_done so that
+ *      functions like x_ins and x_delete know that we have been
+ *      called and can skip the x_bs() stuff which has already
+ *      been done by x_redraw.
+ *
+ * RETURN VALUE:
+ *      None
+ */
+
+static void
+x_adjust(void)
+{
+	x_adj_done++;			/* flag the fact that we were called. */
+	/*
+	 * we had a problem if the prompt length > xx_cols / 2
+	 */
+	if ((xbp = xcp - (x_displen / 2)) < xbuf)
+		xbp = xbuf;
+	xlp_valid = false;
+	x_redraw(xx_cols);
+	x_flush();
+}
+
+static int unget_char = -1;
+
+static void
+x_e_ungetc(int c)
+{
+	unget_char = c;
+}
+
+static int
+x_e_getc(void)
+{
+	int c;
+
+	if (unget_char >= 0) {
+		c = unget_char;
+		unget_char = -1;
+	} else if (macro_args) {
+		c = *macro_args++;
+		if (!c) {
+			macro_args = NULL;
+			c = x_getc();
+		}
+	} else
+		c = x_getc();
+
+	return c;
+}
+
+static int
+x_e_getu8(char *buf, int off)
+{
+	int	c, cc, len;
+
+	c = x_e_getc();
+	if (c == -1)
+		return -1;
+	buf[off++] = c;
+
+	if (c == 0xf4)
+		len = 4;
+	else if ((c & 0xf0) == 0xe0)
+		len = 3;
+	else if ((c & 0xe0) == 0xc0 && c > 0xc1)
+		len = 2;
+	else
+		len = 1;
+
+	for (; len > 1; len--) {
+		cc = x_e_getc();
+		if (cc == -1)
+			break;
+		if (isu8cont(cc) == 0 ||
+		    (c == 0xe0 && len == 3 && cc < 0xa0) ||
+		    (c == 0xed && len == 3 && cc & 0x20) ||
+		    (c == 0xf4 && len == 4 && cc & 0x30)) {
+			x_e_ungetc(cc);
+			break;
+		}
+		buf[off++] = cc;
+	}
+	buf[off] = '\0';
+
+	return off;
+}
+
+static void
+x_e_putc(int c)
+{
+	if (c == '\r' || c == '\n')
+		x_col = 0;
+	if (x_col < xx_cols) {
+		x_putc(c);
+		switch (c) {
+		case BEL:
+			break;
+		case '\r':
+		case '\n':
+			break;
+		case '\b':
+			x_col--;
+			break;
+		default:
+			if (!isu8cont(c))
+				x_col++;
+			break;
+		}
+	}
+	if (x_adj_ok && (x_col < 0 || x_col >= (xx_cols - 2)))
+		x_adjust();
+}
+
+#ifdef DEBUG
+static int
+x_debug_info(int c)
+{
+	x_flush();
+	shellf("\nksh debug:\n");
+	shellf("\tx_col == %d,\t\tx_cols == %d,\tx_displen == %d\n",
+	    x_col, xx_cols, x_displen);
+	shellf("\txcp == 0x%lx,\txep == 0x%lx\n", (long) xcp, (long) xep);
+	shellf("\txbp == 0x%lx,\txbuf == 0x%lx\n", (long) xbp, (long) xbuf);
+	shellf("\txlp == 0x%lx\n", (long) xlp);
+	shellf("\txlp == 0x%lx\n", (long) x_lastcp());
+	shellf("\n");
+	x_redraw(-1);
+	return 0;
+}
+#endif
+
+static void
+x_e_puts(const char *s)
+{
+	int	adj = x_adj_done;
+
+	while (*s && adj == x_adj_done)
+		x_e_putc(*s++);
+}
+
+/* NAME:
+ *      x_set_arg - set an arg value for next function
+ *
+ * DESCRIPTION:
+ *      This is a simple implementation of M-[0-9].
+ *
+ * RETURN VALUE:
+ *      KSTD
+ */
+
+static int
+x_set_arg(int c)
+{
+	int n = 0;
+	int first = 1;
+
+	for (; c >= 0 && isdigit(c); c = x_e_getc(), first = 0)
+		n = n * 10 + (c - '0');
+	if (c < 0 || first) {
+		x_e_putc(BEL);
+		x_arg = 1;
+		x_arg_defaulted = 1;
+	} else {
+		x_e_ungetc(c);
+		x_arg = n;
+		x_arg_defaulted = 0;
+		x_arg_set = 1;
+	}
+	return KSTD;
+}
+
+
+/* Comment or uncomment the current line. */
+static int
+x_comment(int c)
+{
+	int oldsize = x_size_str(xbuf);
+	int len = xep - xbuf;
+	int ret = x_do_comment(xbuf, xend - xbuf, &len);
+
+	if (ret < 0)
+		x_e_putc(BEL);
+	else {
+		xep = xbuf + len;
+		*xep = '\0';
+		xcp = xbp = xbuf;
+		x_redraw(oldsize);
+		if (ret > 0)
+			return x_newline('\n');
+	}
+	return KSTD;
+}
+
+
+/* NAME:
+ *      x_prev_histword - recover word from prev command
+ *
+ * DESCRIPTION:
+ *      This function recovers the last word from the previous
+ *      command and inserts it into the current edit line.  If a
+ *      numeric arg is supplied then the n'th word from the
+ *      start of the previous command is used.
+ *
+ *      Bound to M-.
+ *
+ * RETURN VALUE:
+ *      KSTD
+ */
+
+static int
+x_prev_histword(int c)
+{
+	char *rcp;
+	char *cp;
+
+	cp = *histptr;
+	if (!cp)
+		x_e_putc(BEL);
+	else if (x_arg_defaulted) {
+		rcp = &cp[strlen(cp) - 1];
+		/*
+		 * ignore white-space after the last word
+		 */
+		while (rcp > cp && is_cfs(*rcp))
+			rcp--;
+		while (rcp > cp && !is_cfs(*rcp))
+			rcp--;
+		if (is_cfs(*rcp))
+			rcp++;
+		x_ins(rcp);
+	} else {
+		rcp = cp;
+		/*
+		 * ignore white-space at start of line
+		 */
+		while (*rcp && is_cfs(*rcp))
+			rcp++;
+		while (x_arg-- > 1) {
+			while (*rcp && !is_cfs(*rcp))
+				rcp++;
+			while (*rcp && is_cfs(*rcp))
+				rcp++;
+		}
+		cp = rcp;
+		while (*rcp && !is_cfs(*rcp))
+			rcp++;
+		c = *rcp;
+		*rcp = '\0';
+		x_ins(cp);
+		*rcp = c;
+	}
+	return KSTD;
+}
+
+/* Uppercase N(1) words */
+static int
+x_fold_upper(int c)
+{
+	return x_fold_case('U');
+}
+
+/* Lowercase N(1) words */
+static int
+x_fold_lower(int c)
+{
+	return x_fold_case('L');
+}
+
+/* Lowercase N(1) words */
+static int
+x_fold_capitalize(int c)
+{
+	return x_fold_case('C');
+}
+
+/* NAME:
+ *      x_fold_case - convert word to UPPER/lower/Capital case
+ *
+ * DESCRIPTION:
+ *      This function is used to implement M-U,M-u,M-L,M-l,M-C and M-c
+ *      to UPPER case, lower case or Capitalize words.
+ *
+ * RETURN VALUE:
+ *      None
+ */
+
+static int
+x_fold_case(int c)
+{
+	char *cp = xcp;
+
+	if (cp == xep) {
+		x_e_putc(BEL);
+		return KSTD;
+	}
+	while (x_arg--) {
+		/*
+		 * first skip over any white-space
+		 */
+		while (cp != xep && is_mfs(*cp))
+			cp++;
+		/*
+		 * do the first char on its own since it may be
+		 * a different action than for the rest.
+		 */
+		if (cp != xep) {
+			if (c == 'L') {		/* lowercase */
+				if (isupper((unsigned char)*cp))
+					*cp = tolower((unsigned char)*cp);
+			} else {		/* uppercase, capitalize */
+				if (islower((unsigned char)*cp))
+					*cp = toupper((unsigned char)*cp);
+			}
+			cp++;
+		}
+		/*
+		 * now for the rest of the word
+		 */
+		while (cp != xep && !is_mfs(*cp)) {
+			if (c == 'U') {		/* uppercase */
+				if (islower((unsigned char)*cp))
+					*cp = toupper((unsigned char)*cp);
+			} else {		/* lowercase, capitalize */
+				if (isupper((unsigned char)*cp))
+					*cp = tolower((unsigned char)*cp);
+			}
+			cp++;
+		}
+	}
+	x_goto(cp);
+	return KSTD;
+}
+
+/* NAME:
+ *      x_lastcp - last visible byte
+ *
+ * SYNOPSIS:
+ *      x_lastcp()
+ *
+ * DESCRIPTION:
+ *      This function returns a pointer to that byte in the
+ *      edit buffer that will be the last displayed on the
+ *      screen.  The sequence:
+ *
+ *      for (cp = x_lastcp(); cp > xcp; cp)
+ *        x_bs(*--cp);
+ *
+ *      Will position the cursor correctly on the screen.
+ *
+ * RETURN VALUE:
+ *      cp or NULL
+ */
+
+static char *
+x_lastcp(void)
+{
+	char *rcp;
+	int i;
+
+	if (!xlp_valid) {
+		for (i = 0, rcp = xbp; rcp < xep && i < x_displen; rcp++)
+			i += x_size((unsigned char)*rcp);
+		xlp = rcp;
+	}
+	xlp_valid = true;
+	return (xlp);
+}
+
+#endif /* EMACS */
diff --git a/eval.c b/eval.c
@@ -0,0 +1,1322 @@
+/*	$OpenBSD: eval.c,v 1.64 2019/02/20 23:59:17 schwarze Exp $	*/
+
+/*
+ * Expansion - quoting, separation, substitution, globbing
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+/*
+ * string expansion
+ *
+ * first pass: quoting, IFS separation, ~, ${}, $() and $(()) substitution.
+ * second pass: alternation ({,}), filename expansion (*?[]).
+ */
+
+/* expansion generator state */
+typedef struct Expand {
+	/* int  type; */	/* see expand() */
+	const char *str;	/* string */
+	union {
+		const char **strv;/* string[] */
+		struct shf *shf;/* file */
+	} u;			/* source */
+	struct tbl *var;	/* variable in ${var..} */
+	short	split;		/* split "$@" / call waitlast $() */
+} Expand;
+
+#define	XBASE		0	/* scanning original */
+#define	XSUB		1	/* expanding ${} string */
+#define	XARGSEP		2	/* ifs0 between "$*" */
+#define	XARG		3	/* expanding $*, $@ */
+#define	XCOM		4	/* expanding $() */
+#define XNULLSUB	5	/* "$@" when $# is 0 (don't generate word) */
+#define XSUBMID		6	/* middle of expanding ${} */
+
+/* States used for field splitting */
+#define IFS_WORD	0	/* word has chars (or quotes) */
+#define IFS_WS		1	/* have seen IFS white-space */
+#define IFS_NWS		2	/* have seen IFS non-white-space */
+
+static	int	varsub(Expand *, char *, char *, int *, int *);
+static	int	comsub(Expand *, char *);
+static	char   *trimsub(char *, char *, int);
+static	void	glob(char *, XPtrV *, int);
+static	void	globit(XString *, char **, char *, XPtrV *, int);
+static char	*maybe_expand_tilde(char *, XString *, char **, int);
+static	char   *tilde(char *);
+static	char   *homedir(char *);
+static void	alt_expand(XPtrV *, char *, char *, char *, int);
+
+static struct tbl *varcpy(struct tbl *);
+
+/* compile and expand word */
+char *
+substitute(const char *cp, int f)
+{
+	struct source *s, *sold;
+
+	if (disable_subst)
+		return str_save(cp, ATEMP);
+
+	sold = source;
+	s = pushs(SWSTR, ATEMP);
+	s->start = s->str = cp;
+	source = s;
+	if (yylex(ONEWORD) != LWORD)
+		internal_errorf("substitute");
+	source = sold;
+	afree(s, ATEMP);
+	return evalstr(yylval.cp, f);
+}
+
+/*
+ * expand arg-list
+ */
+char **
+eval(char **ap, int f)
+{
+	XPtrV w;
+
+	if (*ap == NULL)
+		return ap;
+	XPinit(w, 32);
+	XPput(w, NULL);		/* space for shell name */
+	while (*ap != NULL)
+		expand(*ap++, &w, f);
+	XPput(w, NULL);
+	return (char **) XPclose(w) + 1;
+}
+
+/*
+ * expand string
+ */
+char *
+evalstr(char *cp, int f)
+{
+	XPtrV w;
+
+	XPinit(w, 1);
+	expand(cp, &w, f);
+	cp = (XPsize(w) == 0) ? null : (char*) *XPptrv(w);
+	XPfree(w);
+	return cp;
+}
+
+/*
+ * expand string - return only one component
+ * used from iosetup to expand redirection files
+ */
+char *
+evalonestr(char *cp, int f)
+{
+	XPtrV w;
+
+	XPinit(w, 1);
+	expand(cp, &w, f);
+	switch (XPsize(w)) {
+	case 0:
+		cp = null;
+		break;
+	case 1:
+		cp = (char*) *XPptrv(w);
+		break;
+	default:
+		cp = evalstr(cp, f&~DOGLOB);
+		break;
+	}
+	XPfree(w);
+	return cp;
+}
+
+/* for nested substitution: ${var:=$var2} */
+typedef struct SubType {
+	short	stype;		/* [=+-?%#] action after expanded word */
+	short	base;		/* begin position of expanded word */
+	short	f;		/* saved value of f (DOPAT, etc) */
+	struct tbl *var;	/* variable for ${var..} */
+	short	quote;		/* saved value of quote (for ${..[%#]..}) */
+	struct SubType *prev;	/* old type */
+	struct SubType *next;	/* poped type (to avoid re-allocating) */
+} SubType;
+
+void
+expand(char *cp,	/* input word */
+    XPtrV *wp,		/* output words */
+    int f)		/* DO* flags */
+{
+	int c = 0;
+	int type;		/* expansion type */
+	int quote = 0;		/* quoted */
+	XString ds;		/* destination string */
+	char *dp, *sp;		/* dest., source */
+	int fdo, word;		/* second pass flags; have word */
+	int doblank;		/* field splitting of parameter/command subst */
+	Expand x = {
+		/* expansion variables */
+		NULL, { NULL }, NULL, 0
+	};
+	SubType st_head, *st;
+	int newlines = 0; /* For trailing newlines in COMSUB */
+	int saw_eq, tilde_ok;
+	int make_magic;
+	size_t len;
+
+	if (cp == NULL)
+		internal_errorf("expand(NULL)");
+	/* for alias, readonly, set, typeset commands */
+	if ((f & DOVACHECK) && is_wdvarassign(cp)) {
+		f &= ~(DOVACHECK|DOBLANK|DOGLOB|DOTILDE);
+		f |= DOASNTILDE;
+	}
+	if (Flag(FNOGLOB))
+		f &= ~DOGLOB;
+	if (Flag(FMARKDIRS))
+		f |= DOMARKDIRS;
+	if (Flag(FBRACEEXPAND) && (f & DOGLOB))
+		f |= DOBRACE_;
+
+	Xinit(ds, dp, 128, ATEMP);	/* init dest. string */
+	type = XBASE;
+	sp = cp;
+	fdo = 0;
+	saw_eq = 0;
+	tilde_ok = (f & (DOTILDE|DOASNTILDE)) ? 1 : 0; /* must be 1/0 */
+	doblank = 0;
+	make_magic = 0;
+	word = (f&DOBLANK) ? IFS_WS : IFS_WORD;
+
+	memset(&st_head, 0, sizeof(st_head));
+	st = &st_head;
+
+	while (1) {
+		Xcheck(ds, dp);
+
+		switch (type) {
+		case XBASE:	/* original prefixed string */
+			c = *sp++;
+			switch (c) {
+			case EOS:
+				c = 0;
+				break;
+			case CHAR:
+				c = *sp++;
+				break;
+			case QCHAR:
+				quote |= 2; /* temporary quote */
+				c = *sp++;
+				break;
+			case OQUOTE:
+				word = IFS_WORD;
+				tilde_ok = 0;
+				quote = 1;
+				continue;
+			case CQUOTE:
+				quote = 0;
+				continue;
+			case COMSUB:
+				tilde_ok = 0;
+				if (f & DONTRUNCOMMAND) {
+					word = IFS_WORD;
+					*dp++ = '$'; *dp++ = '(';
+					while (*sp != '\0') {
+						Xcheck(ds, dp);
+						*dp++ = *sp++;
+					}
+					*dp++ = ')';
+				} else {
+					type = comsub(&x, sp);
+					if (type == XCOM && (f&DOBLANK))
+						doblank++;
+					sp = strchr(sp, 0) + 1;
+					newlines = 0;
+				}
+				continue;
+			case EXPRSUB:
+				word = IFS_WORD;
+				tilde_ok = 0;
+				if (f & DONTRUNCOMMAND) {
+					*dp++ = '$'; *dp++ = '('; *dp++ = '(';
+					while (*sp != '\0') {
+						Xcheck(ds, dp);
+						*dp++ = *sp++;
+					}
+					*dp++ = ')'; *dp++ = ')';
+				} else {
+					struct tbl v;
+					char *p;
+
+					v.flag = DEFINED|ISSET|INTEGER;
+					v.type = 10; /* not default */
+					v.name[0] = '\0';
+					v_evaluate(&v, substitute(sp, 0),
+					    KSH_UNWIND_ERROR, true);
+					sp = strchr(sp, 0) + 1;
+					for (p = str_val(&v); *p; ) {
+						Xcheck(ds, dp);
+						*dp++ = *p++;
+					}
+				}
+				continue;
+			case OSUBST: /* ${{#}var{:}[=+-?#%]word} */
+			  /* format is:
+			   *   OSUBST [{x] plain-variable-part \0
+			   *     compiled-word-part CSUBST [}x]
+			   * This is where all syntax checking gets done...
+			   */
+			    {
+				char *varname = ++sp; /* skip the { or x (}) */
+				int stype;
+				int slen = 0;
+
+				sp = strchr(sp, '\0') + 1; /* skip variable */
+				type = varsub(&x, varname, sp, &stype, &slen);
+				if (type < 0) {
+					char endc;
+					char *str, *end;
+
+					sp = varname - 2; /* restore sp */
+					end = (char *) wdscan(sp, CSUBST);
+					/* ({) the } or x is already skipped */
+					endc = *end;
+					*end = EOS;
+					str = snptreef(NULL, 64, "%S", sp);
+					*end = endc;
+					errorf("%s: bad substitution", str);
+				}
+				if (f&DOBLANK)
+					doblank++;
+				tilde_ok = 0;
+				if (type == XBASE) {	/* expand? */
+					if (!st->next) {
+						SubType *newst;
+
+						newst = alloc(
+						    sizeof(SubType), ATEMP);
+						newst->next = NULL;
+						newst->prev = st;
+						st->next = newst;
+					}
+					st = st->next;
+					st->stype = stype;
+					st->base = Xsavepos(ds, dp);
+					st->f = f;
+					st->var = varcpy(x.var);
+					st->quote = quote;
+					/* skip qualifier(s) */
+					if (stype)
+						sp += slen;
+					switch (stype & 0x7f) {
+					case '#':
+					case '%':
+						/* ! DOBLANK,DOBRACE_,DOTILDE */
+						f = DOPAT | (f&DONTRUNCOMMAND) |
+						    DOTEMP_;
+						quote = 0;
+						/* Prepend open pattern (so |
+						 * in a trim will work as
+						 * expected)
+						 */
+						*dp++ = MAGIC;
+						*dp++ = '@' + 0x80U;
+						break;
+					case '=':
+						/* Enabling tilde expansion
+						 * after :'s here is
+						 * non-standard ksh, but is
+						 * consistent with rules for
+						 * other assignments.  Not
+						 * sure what POSIX thinks of
+						 * this.
+						 * Not doing tilde expansion
+						 * for integer variables is a
+						 * non-POSIX thing - makes
+						 * sense though, since ~ is
+						 * a arithmetic operator.
+						 */
+						if (!(x.var->flag & INTEGER))
+							f |= DOASNTILDE|DOTILDE;
+						f |= DOTEMP_;
+						/* These will be done after the
+						 * value has been assigned.
+						 */
+						f &= ~(DOBLANK|DOGLOB|DOBRACE_);
+						tilde_ok = 1;
+						break;
+					case '?':
+						f &= ~DOBLANK;
+						f |= DOTEMP_;
+						/* FALLTHROUGH */
+					default:
+						/* Enable tilde expansion */
+						tilde_ok = 1;
+						f |= DOTILDE;
+					}
+				} else
+					/* skip word */
+					sp = (char *) wdscan(sp, CSUBST);
+				continue;
+			    }
+			case CSUBST: /* only get here if expanding word */
+				sp++; /* ({) skip the } or x */
+				tilde_ok = 0;	/* in case of ${unset:-} */
+				*dp = '\0';
+				quote = st->quote;
+				f = st->f;
+				if (f&DOBLANK)
+					doblank--;
+				switch (st->stype&0x7f) {
+				case '#':
+				case '%':
+					/* Append end-pattern */
+					*dp++ = MAGIC; *dp++ = ')'; *dp = '\0';
+					dp = Xrestpos(ds, dp, st->base);
+					/* Must use st->var since calling
+					 * global would break things
+					 * like x[i+=1].
+					 */
+					x.str = trimsub(str_val(st->var),
+						dp, st->stype);
+					if (x.str[0] != '\0' || st->quote)
+						type = XSUB;
+					else
+						type = XNULLSUB;
+					if (f&DOBLANK)
+						doblank++;
+					st = st->prev;
+					continue;
+				case '=':
+					/* Restore our position and substitute
+					 * the value of st->var (may not be
+					 * the assigned value in the presence
+					 * of integer/right-adj/etc attributes).
+					 */
+					dp = Xrestpos(ds, dp, st->base);
+					/* Must use st->var since calling
+					 * global would cause with things
+					 * like x[i+=1] to be evaluated twice.
+					 */
+					/* Note: not exported by FEXPORT
+					 * in at&t ksh.
+					 */
+					/* XXX POSIX says readonly is only
+					 * fatal for special builtins (setstr
+					 * does readonly check).
+					 */
+					len = strlen(dp) + 1;
+					setstr(st->var,
+					    debunk(alloc(len, ATEMP),
+					    dp, len), KSH_UNWIND_ERROR);
+					x.str = str_val(st->var);
+					type = XSUB;
+					if (f&DOBLANK)
+						doblank++;
+					st = st->prev;
+					continue;
+				case '?':
+				    {
+					char *s = Xrestpos(ds, dp, st->base);
+
+					errorf("%s: %s", st->var->name,
+					    dp == s ?
+					    "parameter null or not set" :
+					    (debunk(s, s, strlen(s) + 1), s));
+				    }
+				}
+				st = st->prev;
+				type = XBASE;
+				continue;
+
+			case OPAT: /* open pattern: *(foo|bar) */
+				/* Next char is the type of pattern */
+				make_magic = 1;
+				c = *sp++ + 0x80;
+				break;
+
+			case SPAT: /* pattern separator (|) */
+				make_magic = 1;
+				c = '|';
+				break;
+
+			case CPAT: /* close pattern */
+				make_magic = 1;
+				c = /*(*/ ')';
+				break;
+			}
+			break;
+
+		case XNULLSUB:
+			/* Special case for "$@" (and "${foo[@]}") - no
+			 * word is generated if $# is 0 (unless there is
+			 * other stuff inside the quotes).
+			 */
+			type = XBASE;
+			if (f&DOBLANK) {
+				doblank--;
+				/* not really correct: x=; "$x$@" should
+				 * generate a null argument and
+				 * set A; "${@:+}" shouldn't.
+				 */
+				if (dp == Xstring(ds, dp))
+					word = IFS_WS;
+			}
+			continue;
+
+		case XSUB:
+		case XSUBMID:
+			if ((c = *x.str++) == 0) {
+				type = XBASE;
+				if (f&DOBLANK)
+					doblank--;
+				continue;
+			}
+			break;
+
+		case XARGSEP:
+			type = XARG;
+			quote = 1;
+		case XARG:
+			if ((c = *x.str++) == '\0') {
+				/* force null words to be created so
+				 * set -- '' 2 ''; foo "$@" will do
+				 * the right thing
+				 */
+				if (quote && x.split)
+					word = IFS_WORD;
+				if ((x.str = *x.u.strv++) == NULL) {
+					type = XBASE;
+					if (f&DOBLANK)
+						doblank--;
+					continue;
+				}
+				c = ifs0;
+				if (c == 0) {
+					if (quote && !x.split)
+						continue;
+					c = ' ';
+				}
+				if (quote && x.split) {
+					/* terminate word for "$@" */
+					type = XARGSEP;
+					quote = 0;
+				}
+			}
+			break;
+
+		case XCOM:
+			if (x.u.shf == NULL)	/* $(< ...) failed, fake EOF */
+				c = EOF;
+			else if (newlines) {		/* Spit out saved nl's */
+				c = '\n';
+				--newlines;
+			} else {
+				while ((c = shf_getc(x.u.shf)) == 0 || c == '\n')
+				    if (c == '\n')
+					    newlines++;	/* Save newlines */
+				if (newlines && c != EOF) {
+					shf_ungetc(c, x.u.shf);
+					c = '\n';
+					--newlines;
+				}
+			}
+			if (c == EOF) {
+				newlines = 0;
+				if (x.u.shf != NULL)
+					shf_close(x.u.shf);
+				if (x.split)
+					subst_exstat = waitlast();
+				else
+					subst_exstat = (x.u.shf == NULL);
+				type = XBASE;
+				if (f&DOBLANK)
+					doblank--;
+				continue;
+			}
+			break;
+		}
+
+		/* check for end of word or IFS separation */
+		if (c == 0 || (!quote && (f & DOBLANK) && doblank &&
+		    !make_magic && ctype(c, C_IFS))) {
+			/* How words are broken up:
+			 *		   |       value of c
+			 *	  word	   |	ws	nws	0
+			 *	-----------------------------------
+			 *	IFS_WORD	w/WS	w/NWS	w
+			 *	IFS_WS		-/WS	w/NWS	-
+			 *	IFS_NWS		-/NWS	w/NWS	w
+			 *   (w means generate a word)
+			 * Note that IFS_NWS/0 generates a word (at&t ksh
+			 * doesn't do this, but POSIX does).
+			 */
+			if (word == IFS_WORD ||
+			    (!ctype(c, C_IFSWS) && c && word == IFS_NWS)) {
+				char *p;
+
+				*dp++ = '\0';
+				p = Xclose(ds, dp);
+				if (fdo & DOBRACE_)
+					/* also does globbing */
+					alt_expand(wp, p, p,
+					    p + Xlength(ds, (dp - 1)),
+					    fdo | (f & DOMARKDIRS));
+				else if (fdo & DOGLOB)
+					glob(p, wp, f & DOMARKDIRS);
+				else if ((f & DOPAT) || !(fdo & DOMAGIC_))
+					XPput(*wp, p);
+				else
+					XPput(*wp, debunk(p, p, strlen(p) + 1));
+				fdo = 0;
+				saw_eq = 0;
+				tilde_ok = (f & (DOTILDE|DOASNTILDE)) ? 1 : 0;
+				if (c != 0)
+					Xinit(ds, dp, 128, ATEMP);
+			}
+			if (c == 0)
+				goto done;
+			if (word != IFS_NWS)
+				word = ctype(c, C_IFSWS) ? IFS_WS : IFS_NWS;
+		} else {
+			if (type == XSUB) {
+				if (word == IFS_NWS &&
+				    Xlength(ds, dp) == 0) {
+					char *p;
+
+					if ((p = strdup("")) == NULL)
+						internal_errorf("unable "
+						    "to allocate memory");
+					XPput(*wp, p);
+				}
+				type = XSUBMID;
+			}
+
+			/* age tilde_ok info - ~ code tests second bit */
+			tilde_ok <<= 1;
+			/* mark any special second pass chars */
+			if (!quote)
+				switch (c) {
+				case '[':
+				case '!':
+				case '-':
+				case ']':
+					/* For character classes - doesn't hurt
+					 * to have magic !,-,]'s outside of
+					 * [...] expressions.
+					 */
+					if (f & (DOPAT | DOGLOB)) {
+						fdo |= DOMAGIC_;
+						if (c == '[')
+							fdo |= f & DOGLOB;
+						*dp++ = MAGIC;
+					}
+					break;
+				case '*':
+				case '?':
+					if (f & (DOPAT | DOGLOB)) {
+						fdo |= DOMAGIC_ | (f & DOGLOB);
+						*dp++ = MAGIC;
+					}
+					break;
+				case OBRACE:
+				case ',':
+				case CBRACE:
+					if ((f & DOBRACE_) && (c == OBRACE ||
+					    (fdo & DOBRACE_))) {
+						fdo |= DOBRACE_|DOMAGIC_;
+						*dp++ = MAGIC;
+					}
+					break;
+				case '=':
+					/* Note first unquoted = for ~ */
+					if (!(f & DOTEMP_) && !saw_eq) {
+						saw_eq = 1;
+						tilde_ok = 1;
+					}
+					break;
+				case ':': /* : */
+					/* Note unquoted : for ~ */
+					if (!(f & DOTEMP_) && (f & DOASNTILDE))
+						tilde_ok = 1;
+					break;
+				case '~':
+					/* tilde_ok is reset whenever
+					 * any of ' " $( $(( ${ } are seen.
+					 * Note that tilde_ok must be preserved
+					 * through the sequence ${A=a=}~
+					 */
+					if (type == XBASE &&
+					    (f & (DOTILDE|DOASNTILDE)) &&
+					    (tilde_ok & 2)) {
+						char *p, *dp_x;
+
+						dp_x = dp;
+						p = maybe_expand_tilde(sp,
+						    &ds, &dp_x,
+						    f & DOASNTILDE);
+						if (p) {
+							if (dp != dp_x)
+								word = IFS_WORD;
+							dp = dp_x;
+							sp = p;
+							continue;
+						}
+					}
+					break;
+				}
+			else
+				quote &= ~2; /* undo temporary */
+
+			if (make_magic) {
+				make_magic = 0;
+				fdo |= DOMAGIC_ | (f & DOGLOB);
+				*dp++ = MAGIC;
+			} else if (ISMAGIC(c)) {
+				fdo |= DOMAGIC_;
+				*dp++ = MAGIC;
+			}
+			*dp++ = c; /* save output char */
+			word = IFS_WORD;
+		}
+	}
+
+done:
+	for (st = &st_head; st != NULL; st = st->next) {
+		if (st->var == NULL || (st->var->flag & RDONLY) == 0)
+			continue;
+
+		afree(st->var, ATEMP);
+	}
+}
+
+/*
+ * Prepare to generate the string returned by ${} substitution.
+ */
+static int
+varsub(Expand *xp, char *sp, char *word,
+    int *stypep,	/* becomes qualifier type */
+    int *slenp)		/* " " len (=, :=, etc.) valid iff *stypep != 0 */
+{
+	int c;
+	int state;	/* next state: XBASE, XARG, XSUB, XNULLSUB */
+	int stype;	/* substitution type */
+	int slen;
+	char *p;
+	struct tbl *vp;
+	int zero_ok = 0;
+
+	if (sp[0] == '\0')	/* Bad variable name */
+		return -1;
+
+	xp->var = NULL;
+
+	/* ${#var}, string length or array size */
+	if (sp[0] == '#' && (c = sp[1]) != '\0') {
+		/* Can't have any modifiers for ${#...} */
+		if (*word != CSUBST)
+			return -1;
+		sp++;
+		/* Check for size of array */
+		if ((p=strchr(sp,'[')) && (p[1]=='*'||p[1]=='@') && p[2]==']') {
+			int n = 0;
+
+			vp = global(arrayname(sp));
+			if (vp->flag & (ISSET|ARRAY))
+				zero_ok = 1;
+			for (; vp; vp = vp->u.array)
+				if (vp->flag & ISSET)
+					n++;
+			c = n; /* ksh88/ksh93 go for number, not max index */
+		} else if (c == '*' || c == '@')
+			c = genv->loc->argc;
+		else {
+			p = str_val(global(sp));
+			zero_ok = p != null;
+			c = strlen(p);
+		}
+		if (Flag(FNOUNSET) && c == 0 && !zero_ok)
+			errorf("%s: parameter not set", sp);
+		*stypep = 0; /* unqualified variable/string substitution */
+		xp->str = str_save(u64ton((uint64_t)c, 10), ATEMP);
+		return XSUB;
+	}
+
+	/* Check for qualifiers in word part */
+	stype = 0;
+	c = word[slen = 0] == CHAR ? word[1] : 0;
+	if (c == ':') {
+		slen += 2;
+		stype = 0x80;
+		c = word[slen + 0] == CHAR ? word[slen + 1] : 0;
+	}
+	if (ctype(c, C_SUBOP1)) {
+		slen += 2;
+		stype |= c;
+	} else if (ctype(c, C_SUBOP2)) { /* Note: ksh88 allows :%, :%%, etc */
+		slen += 2;
+		stype = c;
+		if (word[slen + 0] == CHAR && c == word[slen + 1]) {
+			stype |= 0x80;
+			slen += 2;
+		}
+	} else if (stype)	/* : is not ok */
+		return -1;
+	if (!stype && *word != CSUBST)
+		return -1;
+	*stypep = stype;
+	*slenp = slen;
+
+	c = sp[0];
+	if (c == '*' || c == '@') {
+		switch (stype & 0x7f) {
+		case '=':	/* can't assign to a vector */
+		case '%':	/* can't trim a vector (yet) */
+		case '#':
+			return -1;
+		}
+		if (genv->loc->argc == 0) {
+			xp->str = null;
+			xp->var = global(sp);
+			state = c == '@' ? XNULLSUB : XSUB;
+		} else {
+			xp->u.strv = (const char **) genv->loc->argv + 1;
+			xp->str = *xp->u.strv++;
+			xp->split = c == '@'; /* $@ */
+			state = XARG;
+		}
+		zero_ok = 1;	/* exempt "$@" and "$*" from 'set -u' */
+	} else {
+		if ((p=strchr(sp,'[')) && (p[1]=='*'||p[1]=='@') && p[2]==']') {
+			XPtrV wv;
+
+			switch (stype & 0x7f) {
+			case '=':	/* can't assign to a vector */
+			case '%':	/* can't trim a vector (yet) */
+			case '#':
+			case '?':
+				return -1;
+			}
+			XPinit(wv, 32);
+			vp = global(arrayname(sp));
+			for (; vp; vp = vp->u.array) {
+				if (!(vp->flag&ISSET))
+					continue;
+				XPput(wv, str_val(vp));
+			}
+			if (XPsize(wv) == 0) {
+				xp->str = null;
+				state = p[1] == '@' ? XNULLSUB : XSUB;
+				XPfree(wv);
+			} else {
+				XPput(wv, 0);
+				xp->u.strv = (const char **) XPptrv(wv);
+				xp->str = *xp->u.strv++;
+				xp->split = p[1] == '@'; /* ${foo[@]} */
+				state = XARG;
+			}
+		} else {
+			/* Can't assign things like $! or $1 */
+			if ((stype & 0x7f) == '=' &&
+			    (ctype(*sp, C_VAR1) || digit(*sp)))
+				return -1;
+			xp->var = global(sp);
+			xp->str = str_val(xp->var);
+			state = XSUB;
+		}
+	}
+
+	c = stype&0x7f;
+	/* test the compiler's code generator */
+	if (ctype(c, C_SUBOP2) ||
+	    (((stype&0x80) ? *xp->str=='\0' : xp->str==null) ? /* undef? */
+	    c == '=' || c == '-' || c == '?' : c == '+'))
+		state = XBASE;	/* expand word instead of variable value */
+	if (Flag(FNOUNSET) && xp->str == null && !zero_ok &&
+	    (ctype(c, C_SUBOP2) || (state != XBASE && c != '+')))
+		errorf("%s: parameter not set", sp);
+	return state;
+}
+
+/*
+ * Run the command in $(...) and read its output.
+ */
+static int
+comsub(Expand *xp, char *cp)
+{
+	Source *s, *sold;
+	struct op *t;
+	struct shf *shf;
+
+	s = pushs(SSTRING, ATEMP);
+	s->start = s->str = cp;
+	sold = source;
+	t = compile(s);
+	afree(s, ATEMP);
+	source = sold;
+
+	if (t == NULL)
+		return XBASE;
+
+	if (t != NULL && t->type == TCOM && /* $(<file) */
+	    *t->args == NULL && *t->vars == NULL && t->ioact != NULL) {
+		struct ioword *io = *t->ioact;
+		char *name;
+
+		if ((io->flag&IOTYPE) != IOREAD)
+			errorf("funny $() command: %s",
+			    snptreef(NULL, 32, "%R", io));
+		shf = shf_open(name = evalstr(io->name, DOTILDE), O_RDONLY, 0,
+			SHF_MAPHI|SHF_CLEXEC);
+		if (shf == NULL)
+			warningf(!Flag(FTALKING),
+			    "%s: cannot open $(<) input", name);
+		xp->split = 0;	/* no waitlast() */
+	} else {
+		int ofd1, pv[2];
+		openpipe(pv);
+		shf = shf_fdopen(pv[0], SHF_RD, NULL);
+		ofd1 = savefd(1);
+		if (pv[1] != 1) {
+			ksh_dup2(pv[1], 1, false);
+			close(pv[1]);
+		}
+		execute(t, XFORK|XXCOM|XPIPEO, NULL);
+		restfd(1, ofd1);
+		startlast();
+		xp->split = 1;	/* waitlast() */
+	}
+
+	xp->u.shf = shf;
+	return XCOM;
+}
+
+/*
+ * perform #pattern and %pattern substitution in ${}
+ */
+
+static char *
+trimsub(char *str, char *pat, int how)
+{
+	char *end = strchr(str, 0);
+	char *p, c;
+
+	switch (how&0xff) {	/* UCHAR_MAX maybe? */
+	case '#':		/* shortest at beginning */
+		for (p = str; p <= end; p++) {
+			c = *p; *p = '\0';
+			if (gmatch(str, pat, false)) {
+				*p = c;
+				return p;
+			}
+			*p = c;
+		}
+		break;
+	case '#'|0x80:	/* longest match at beginning */
+		for (p = end; p >= str; p--) {
+			c = *p; *p = '\0';
+			if (gmatch(str, pat, false)) {
+				*p = c;
+				return p;
+			}
+			*p = c;
+		}
+		break;
+	case '%':		/* shortest match at end */
+		for (p = end; p >= str; p--) {
+			if (gmatch(p, pat, false))
+				return str_nsave(str, p - str, ATEMP);
+		}
+		break;
+	case '%'|0x80:	/* longest match at end */
+		for (p = str; p <= end; p++) {
+			if (gmatch(p, pat, false))
+				return str_nsave(str, p - str, ATEMP);
+		}
+		break;
+	}
+
+	return str;		/* no match, return string */
+}
+
+/*
+ * glob
+ * Name derived from V6's /etc/glob, the program that expanded filenames.
+ */
+
+/* XXX cp not const 'cause slashes are temporarily replaced with nulls... */
+static void
+glob(char *cp, XPtrV *wp, int markdirs)
+{
+	int oldsize = XPsize(*wp);
+
+	if (glob_str(cp, wp, markdirs) == 0)
+		XPput(*wp, debunk(cp, cp, strlen(cp) + 1));
+	else
+		qsortp(XPptrv(*wp) + oldsize, (size_t)(XPsize(*wp) - oldsize),
+			xstrcmp);
+}
+
+#define GF_NONE		0
+#define GF_EXCHECK	BIT(0)		/* do existence check on file */
+#define GF_GLOBBED	BIT(1)		/* some globbing has been done */
+#define GF_MARKDIR	BIT(2)		/* add trailing / to directories */
+
+/* Apply file globbing to cp and store the matching files in wp.  Returns
+ * the number of matches found.
+ */
+int
+glob_str(char *cp, XPtrV *wp, int markdirs)
+{
+	int oldsize = XPsize(*wp);
+	XString xs;
+	char *xp;
+
+	Xinit(xs, xp, 256, ATEMP);
+	globit(&xs, &xp, cp, wp, markdirs ? GF_MARKDIR : GF_NONE);
+	Xfree(xs, xp);
+
+	return XPsize(*wp) - oldsize;
+}
+
+static void
+globit(XString *xs,	/* dest string */
+    char **xpp,		/* ptr to dest end */
+    char *sp,		/* source path */
+    XPtrV *wp,		/* output list */
+    int check)		/* GF_* flags */
+{
+	char *np;		/* next source component */
+	char *xp = *xpp;
+	char *se;
+	char odirsep;
+
+	/* This to allow long expansions to be interrupted */
+	intrcheck();
+
+	if (sp == NULL) {	/* end of source path */
+		/* We only need to check if the file exists if a pattern
+		 * is followed by a non-pattern (eg, foo*x/bar; no check
+		 * is needed for foo* since the match must exist) or if
+		 * any patterns were expanded and the markdirs option is set.
+		 * Symlinks make things a bit tricky...
+		 */
+		if ((check & GF_EXCHECK) ||
+		    ((check & GF_MARKDIR) && (check & GF_GLOBBED))) {
+#define stat_check()	(stat_done ? stat_done : \
+			    (stat_done = stat(Xstring(*xs, xp), &statb) < 0 \
+				? -1 : 1))
+			struct stat lstatb, statb;
+			int stat_done = 0;	 /* -1: failed, 1 ok */
+
+			if (lstat(Xstring(*xs, xp), &lstatb) < 0)
+				return;
+			/* special case for systems which strip trailing
+			 * slashes from regular files (eg, /etc/passwd/).
+			 * SunOS 4.1.3 does this...
+			 */
+			if ((check & GF_EXCHECK) && xp > Xstring(*xs, xp) &&
+			    xp[-1] == '/' && !S_ISDIR(lstatb.st_mode) &&
+			    (!S_ISLNK(lstatb.st_mode) ||
+			    stat_check() < 0 || !S_ISDIR(statb.st_mode)))
+				return;
+			/* Possibly tack on a trailing / if there isn't already
+			 * one and if the file is a directory or a symlink to a
+			 * directory
+			 */
+			if (((check & GF_MARKDIR) && (check & GF_GLOBBED)) &&
+			    xp > Xstring(*xs, xp) && xp[-1] != '/' &&
+			    (S_ISDIR(lstatb.st_mode) ||
+			    (S_ISLNK(lstatb.st_mode) && stat_check() > 0 &&
+			    S_ISDIR(statb.st_mode)))) {
+				*xp++ = '/';
+				*xp = '\0';
+			}
+		}
+		XPput(*wp, str_nsave(Xstring(*xs, xp), Xlength(*xs, xp), ATEMP));
+		return;
+	}
+
+	if (xp > Xstring(*xs, xp))
+		*xp++ = '/';
+	while (*sp == '/') {
+		Xcheck(*xs, xp);
+		*xp++ = *sp++;
+	}
+	np = strchr(sp, '/');
+	if (np != NULL) {
+		se = np;
+		odirsep = *np;	/* don't assume '/', can be multiple kinds */
+		*np++ = '\0';
+	} else {
+		odirsep = '\0'; /* keep gcc quiet */
+		se = sp + strlen(sp);
+	}
+
+
+	/* Check if sp needs globbing - done to avoid pattern checks for strings
+	 * containing MAGIC characters, open ['s without the matching close ],
+	 * etc. (otherwise opendir() will be called which may fail because the
+	 * directory isn't readable - if no globbing is needed, only execute
+	 * permission should be required (as per POSIX)).
+	 */
+	if (!has_globbing(sp, se)) {
+		XcheckN(*xs, xp, se - sp + 1);
+		debunk(xp, sp, Xnleft(*xs, xp));
+		xp += strlen(xp);
+		*xpp = xp;
+		globit(xs, xpp, np, wp, check);
+	} else {
+		DIR *dirp;
+		struct dirent *d;
+		char *name;
+		int len;
+		int prefix_len;
+
+		*xp = '\0';
+		prefix_len = Xlength(*xs, xp);
+		dirp = opendir(prefix_len ? Xstring(*xs, xp) : ".");
+		if (dirp == NULL)
+			goto Nodir;
+		while ((d = readdir(dirp)) != NULL) {
+			name = d->d_name;
+			if (name[0] == '.' &&
+			    (name[1] == 0 || (name[1] == '.' && name[2] == 0)))
+				continue; /* always ignore . and .. */
+			if ((*name == '.' && *sp != '.') ||
+			    !gmatch(name, sp, true))
+				continue;
+
+			len = strlen(d->d_name) + 1;
+			XcheckN(*xs, xp, len);
+			memcpy(xp, name, len);
+			*xpp = xp + len - 1;
+			globit(xs, xpp, np, wp,
+				(check & GF_MARKDIR) | GF_GLOBBED
+				| (np ? GF_EXCHECK : GF_NONE));
+			xp = Xstring(*xs, xp) + prefix_len;
+		}
+		closedir(dirp);
+	  Nodir:;
+	}
+
+	if (np != NULL)
+		*--np = odirsep;
+}
+
+/* remove MAGIC from string */
+char *
+debunk(char *dp, const char *sp, size_t dlen)
+{
+	char *d, *s;
+
+	if ((s = strchr(sp, MAGIC))) {
+		size_t slen = s - sp;
+		if (slen >= dlen)
+			return dp;
+		memcpy(dp, sp, slen);
+		for (d = dp + slen; *s && (d < dp + dlen); s++)
+			if (!ISMAGIC(*s) || !(*++s & 0x80) ||
+			    !strchr("*+?@! ", *s & 0x7f))
+				*d++ = *s;
+			else {
+				/* extended pattern operators: *+?@! */
+				if ((*s & 0x7f) != ' ')
+					*d++ = *s & 0x7f;
+				if (d < dp + dlen)
+					*d++ = '(';
+			}
+		*d = '\0';
+	} else if (dp != sp)
+		strlcpy(dp, sp, dlen);
+	return dp;
+}
+
+/* Check if p is an unquoted name, possibly followed by a / or :.  If so
+ * puts the expanded version in *dcp,dp and returns a pointer in p just
+ * past the name, otherwise returns 0.
+ */
+static char *
+maybe_expand_tilde(char *p, XString *dsp, char **dpp, int isassign)
+{
+	XString ts;
+	char *dp = *dpp;
+	char *tp, *r;
+
+	Xinit(ts, tp, 16, ATEMP);
+	/* : only for DOASNTILDE form */
+	while (p[0] == CHAR && p[1] != '/' && (!isassign || p[1] != ':'))
+	{
+		Xcheck(ts, tp);
+		*tp++ = p[1];
+		p += 2;
+	}
+	*tp = '\0';
+	r = (p[0] == EOS || p[0] == CHAR || p[0] == CSUBST) ?
+	    tilde(Xstring(ts, tp)) : NULL;
+	Xfree(ts, tp);
+	if (r) {
+		while (*r) {
+			Xcheck(*dsp, dp);
+			if (ISMAGIC(*r))
+				*dp++ = MAGIC;
+			*dp++ = *r++;
+		}
+		*dpp = dp;
+		r = p;
+	}
+	return r;
+}
+
+/*
+ * tilde expansion
+ *
+ * based on a version by Arnold Robbins
+ */
+
+static char *
+tilde(char *cp)
+{
+	char *dp;
+
+	if (cp[0] == '\0')
+		dp = str_val(global("HOME"));
+	else if (cp[0] == '+' && cp[1] == '\0')
+		dp = str_val(global("PWD"));
+	else if (cp[0] == '-' && cp[1] == '\0')
+		dp = str_val(global("OLDPWD"));
+	else
+		dp = homedir(cp);
+	/* If HOME, PWD or OLDPWD are not set, don't expand ~ */
+	if (dp == null)
+		dp = NULL;
+	return dp;
+}
+
+/*
+ * map userid to user's home directory.
+ * note that 4.3's getpw adds more than 6K to the shell,
+ * and the YP version probably adds much more.
+ * we might consider our own version of getpwnam() to keep the size down.
+ */
+
+static char *
+homedir(char *name)
+{
+	struct tbl *ap;
+
+	ap = ktenter(&homedirs, name, hash(name));
+	if (!(ap->flag & ISSET)) {
+		struct passwd *pw;
+
+		pw = getpwnam(name);
+		if (pw == NULL)
+			return NULL;
+		ap->val.s = str_save(pw->pw_dir, APERM);
+		ap->flag |= DEFINED|ISSET|ALLOC;
+	}
+	return ap->val.s;
+}
+
+static void
+alt_expand(XPtrV *wp, char *start, char *exp_start, char *end, int fdo)
+{
+	int count = 0;
+	char *brace_start, *brace_end, *comma = NULL;
+	char *field_start;
+	char *p;
+
+	/* search for open brace */
+	for (p = exp_start; (p = strchr(p, MAGIC)) && p[1] != OBRACE; p += 2)
+		;
+	brace_start = p;
+
+	/* find matching close brace, if any */
+	if (p) {
+		comma = NULL;
+		count = 1;
+		for (p += 2; *p && count; p++) {
+			if (ISMAGIC(*p)) {
+				if (*++p == OBRACE)
+					count++;
+				else if (*p == CBRACE)
+					--count;
+				else if (*p == ',' && count == 1)
+					comma = p;
+			}
+		}
+	}
+	/* no valid expansions... */
+	if (!p || count != 0) {
+		/* Note that given a{{b,c} we do not expand anything (this is
+		 * what at&t ksh does.  This may be changed to do the {b,c}
+		 * expansion. }
+		 */
+		if (fdo & DOGLOB)
+			glob(start, wp, fdo & DOMARKDIRS);
+		else
+			XPput(*wp, debunk(start, start, end - start));
+		return;
+	}
+	brace_end = p;
+	if (!comma) {
+		alt_expand(wp, start, brace_end, end, fdo);
+		return;
+	}
+
+	/* expand expression */
+	field_start = brace_start + 2;
+	count = 1;
+	for (p = brace_start + 2; p != brace_end; p++) {
+		if (ISMAGIC(*p)) {
+			if (*++p == OBRACE)
+				count++;
+			else if ((*p == CBRACE && --count == 0) ||
+			    (*p == ',' && count == 1)) {
+				char *new;
+				int l1, l2, l3;
+
+				l1 = brace_start - start;
+				l2 = (p - 1) - field_start;
+				l3 = end - brace_end;
+				new = alloc(l1 + l2 + l3 + 1, ATEMP);
+				memcpy(new, start, l1);
+				memcpy(new + l1, field_start, l2);
+				memcpy(new + l1 + l2, brace_end, l3);
+				new[l1 + l2 + l3] = '\0';
+				alt_expand(wp, new, new + l1,
+				    new + l1 + l2 + l3, fdo);
+				field_start = p + 1;
+			}
+		}
+	}
+	return;
+}
+
+/*
+ * Copy the given variable if it's flagged as read-only.
+ * Such variables have static storage and only one can therefore be referenced
+ * at a time.
+ * This is necessary in order to allow variable expansion expressions to refer
+ * to multiple read-only variables.
+ */
+static struct tbl *
+varcpy(struct tbl *vp)
+{
+	struct tbl *cpy;
+
+	if (vp == NULL || (vp->flag & RDONLY) == 0)
+		return vp;
+
+	cpy = alloc(sizeof(struct tbl), ATEMP);
+	memcpy(cpy, vp, sizeof(struct tbl));
+	return cpy;
+}
diff --git a/exec.c b/exec.c
@@ -0,0 +1,1450 @@
+/*	$OpenBSD: exec.c,v 1.73 2018/03/15 16:51:29 anton Exp $	*/
+
+/*
+ * execute command tree
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <paths.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+#include "c_test.h"
+
+/* Does ps4 get parameter substitutions done? */
+# define PS4_SUBSTITUTE(s)	substitute((s), 0)
+
+static int	comexec(struct op *, struct tbl *volatile, char **,
+		    int volatile, volatile int *);
+static void	scriptexec(struct op *, char **);
+static int	call_builtin(struct tbl *, char **);
+static int	iosetup(struct ioword *, struct tbl *);
+static int	herein(const char *, int);
+static char	*do_selectargs(char **, bool);
+static int	dbteste_isa(Test_env *, Test_meta);
+static const char *dbteste_getopnd(Test_env *, Test_op, int);
+static int	dbteste_eval(Test_env *, Test_op, const char *, const char *,
+		    int);
+static void	dbteste_error(Test_env *, int, const char *);
+
+
+/*
+ * execute command tree
+ */
+int
+execute(struct op *volatile t,
+    volatile int flags,		/* if XEXEC don't fork */
+    volatile int *xerrok)	/* inform recursive callers in -e mode that
+				 * short-circuit && or || shouldn't be treated
+				 * as an error */
+{
+	int i, dummy = 0, save_xerrok = 0;
+	volatile int rv = 0;
+	int pv[2];
+	char ** volatile ap;
+	char *s, *cp;
+	struct ioword **iowp;
+	struct tbl *tp = NULL;
+
+	if (t == NULL)
+		return 0;
+
+	/* Caller doesn't care if XERROK should propagate. */
+	if (xerrok == NULL)
+		xerrok = &dummy;
+
+	/* Is this the end of a pipeline?  If so, we want to evaluate the
+	 * command arguments
+	bool eval_done = false;
+	if ((flags&XFORK) && !(flags&XEXEC) && (flags&XPCLOSE)) {
+		eval_done = true;
+		tp = eval_execute_args(t, &ap);
+	}
+	 */
+	if ((flags&XFORK) && !(flags&XEXEC) && t->type != TPIPE)
+		return exchild(t, flags & ~XTIME, xerrok, -1); /* run in sub-process */
+
+	newenv(E_EXEC);
+	if (trap)
+		runtraps(0);
+
+	if (t->type == TCOM) {
+		/* Clear subst_exstat before argument expansion.  Used by
+		 * null commands (see comexec() and c_eval()) and by c_set().
+		 */
+		subst_exstat = 0;
+
+		current_lineno = t->lineno;	/* for $LINENO */
+
+		/* POSIX says expand command words first, then redirections,
+		 * and assignments last..
+		 */
+		ap = eval(t->args, t->u.evalflags | DOBLANK | DOGLOB | DOTILDE);
+		if (flags & XTIME)
+			/* Allow option parsing (bizarre, but POSIX) */
+			timex_hook(t, &ap);
+		if (Flag(FXTRACE) && ap[0]) {
+			shf_fprintf(shl_out, "%s",
+				PS4_SUBSTITUTE(str_val(global("PS4"))));
+			for (i = 0; ap[i]; i++)
+				shf_fprintf(shl_out, "%s%s", ap[i],
+				    ap[i + 1] ? " " : "\n");
+			shf_flush(shl_out);
+		}
+		if (ap[0])
+			tp = findcom(ap[0], FC_BI|FC_FUNC);
+	}
+	flags &= ~XTIME;
+
+	if (t->ioact != NULL || t->type == TPIPE || t->type == TCOPROC) {
+		genv->savefd = areallocarray(NULL, NUFILE, sizeof(short), ATEMP);
+		/* initialize to not redirected */
+		memset(genv->savefd, 0, NUFILE * sizeof(short));
+	}
+
+	/* do redirection, to be restored in quitenv() */
+	if (t->ioact != NULL)
+		for (iowp = t->ioact; *iowp != NULL; iowp++) {
+			if (iosetup(*iowp, tp) < 0) {
+				exstat = rv = 1;
+				/* Redirection failures for special commands
+				 * cause (non-interactive) shell to exit.
+				 */
+				if (tp && tp->type == CSHELL &&
+				    (tp->flag & SPEC_BI))
+					errorf(NULL);
+				/* Deal with FERREXIT, quitenv(), etc. */
+				goto Break;
+			}
+		}
+
+	switch (t->type) {
+	case TCOM:
+		rv = comexec(t, tp, ap, flags, xerrok);
+		break;
+
+	case TPAREN:
+		rv = execute(t->left, flags|XFORK, xerrok);
+		break;
+
+	case TPIPE:
+		flags |= XFORK;
+		flags &= ~XEXEC;
+		genv->savefd[0] = savefd(0);
+		genv->savefd[1] = savefd(1);
+		while (t->type == TPIPE) {
+			openpipe(pv);
+			(void) ksh_dup2(pv[1], 1, false); /* stdout of curr */
+			/* Let exchild() close pv[0] in child
+			 * (if this isn't done, commands like
+			 *    (: ; cat /etc/termcap) | sleep 1
+			 *  will hang forever).
+			 */
+			exchild(t->left, flags|XPIPEO|XCCLOSE, NULL, pv[0]);
+			(void) ksh_dup2(pv[0], 0, false); /* stdin of next */
+			closepipe(pv);
+			flags |= XPIPEI;
+			t = t->right;
+		}
+		restfd(1, genv->savefd[1]); /* stdout of last */
+		genv->savefd[1] = 0; /* no need to re-restore this */
+		/* Let exchild() close 0 in parent, after fork, before wait */
+		i = exchild(t, flags|XPCLOSE, xerrok, 0);
+		if (!(flags&XBGND) && !(flags&XXCOM))
+			rv = i;
+		break;
+
+	case TLIST:
+		while (t->type == TLIST) {
+			execute(t->left, flags & XERROK, NULL);
+			t = t->right;
+		}
+		rv = execute(t, flags & XERROK, xerrok);
+		break;
+
+	case TCOPROC:
+	    {
+		sigset_t	omask;
+
+		/* Block sigchild as we are using things changed in the
+		 * signal handler
+		 */
+		sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+		genv->type = E_ERRH;
+		i = sigsetjmp(genv->jbuf, 0);
+		if (i) {
+			sigprocmask(SIG_SETMASK, &omask, NULL);
+			quitenv(NULL);
+			unwind(i);
+			/* NOTREACHED */
+		}
+		/* Already have a (live) co-process? */
+		if (coproc.job && coproc.write >= 0)
+			errorf("coprocess already exists");
+
+		/* Can we re-use the existing co-process pipe? */
+		coproc_cleanup(true);
+
+		/* do this before opening pipes, in case these fail */
+		genv->savefd[0] = savefd(0);
+		genv->savefd[1] = savefd(1);
+
+		openpipe(pv);
+		if (pv[0] != 0) {
+			ksh_dup2(pv[0], 0, false);
+			close(pv[0]);
+		}
+		coproc.write = pv[1];
+		coproc.job = NULL;
+
+		if (coproc.readw >= 0)
+			ksh_dup2(coproc.readw, 1, false);
+		else {
+			openpipe(pv);
+			coproc.read = pv[0];
+			ksh_dup2(pv[1], 1, false);
+			coproc.readw = pv[1];	 /* closed before first read */
+			coproc.njobs = 0;
+			/* create new coprocess id */
+			++coproc.id;
+		}
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		genv->type = E_EXEC; /* no more need for error handler */
+
+		/* exchild() closes coproc.* in child after fork,
+		 * will also increment coproc.njobs when the
+		 * job is actually created.
+		 */
+		flags &= ~XEXEC;
+		exchild(t->left, flags|XBGND|XFORK|XCOPROC|XCCLOSE,
+		    NULL, coproc.readw);
+		break;
+	    }
+
+	case TASYNC:
+		/* XXX non-optimal, I think - "(foo &)", forks for (),
+		 * forks again for async...  parent should optimize
+		 * this to "foo &"...
+		 */
+		rv = execute(t->left, (flags&~XEXEC)|XBGND|XFORK, xerrok);
+		break;
+
+	case TOR:
+	case TAND:
+		rv = execute(t->left, XERROK, xerrok);
+		if ((rv == 0) == (t->type == TAND))
+			rv = execute(t->right, flags & XERROK, xerrok);
+		else {
+			flags |= XERROK;
+			*xerrok = 1;
+		}
+		break;
+
+	case TBANG:
+		rv = !execute(t->right, XERROK, xerrok);
+		flags |= XERROK;
+		*xerrok = 1;
+		break;
+
+	case TDBRACKET:
+	    {
+		Test_env te;
+
+		te.flags = TEF_DBRACKET;
+		te.pos.wp = t->args;
+		te.isa = dbteste_isa;
+		te.getopnd = dbteste_getopnd;
+		te.eval = dbteste_eval;
+		te.error = dbteste_error;
+
+		rv = test_parse(&te);
+		break;
+	    }
+
+	case TFOR:
+	case TSELECT:
+	    {
+		volatile bool is_first = true;
+		ap = (t->vars != NULL) ? eval(t->vars, DOBLANK|DOGLOB|DOTILDE) :
+		    genv->loc->argv + 1;
+		genv->type = E_LOOP;
+		while (1) {
+			i = sigsetjmp(genv->jbuf, 0);
+			if (!i)
+				break;
+			if ((genv->flags&EF_BRKCONT_PASS) ||
+			    (i != LBREAK && i != LCONTIN)) {
+				quitenv(NULL);
+				unwind(i);
+			} else if (i == LBREAK) {
+				rv = 0;
+				goto Break;
+			}
+		}
+		rv = 0; /* in case of a continue */
+		if (t->type == TFOR) {
+			save_xerrok = *xerrok;
+			while (*ap != NULL) {
+				setstr(global(t->str), *ap++, KSH_UNWIND_ERROR);
+				/* undo xerrok in all iterations except the
+				 * last */
+				*xerrok = save_xerrok;
+				rv = execute(t->left, flags & XERROK, xerrok);
+			}
+			/* ripple xerrok set at final iteration */
+		} else { /* TSELECT */
+			for (;;) {
+				if (!(cp = do_selectargs(ap, is_first))) {
+					rv = 1;
+					break;
+				}
+				is_first = false;
+				setstr(global(t->str), cp, KSH_UNWIND_ERROR);
+				rv = execute(t->left, flags & XERROK, xerrok);
+			}
+		}
+	    }
+		break;
+
+	case TWHILE:
+	case TUNTIL:
+		genv->type = E_LOOP;
+		while (1) {
+			i = sigsetjmp(genv->jbuf, 0);
+			if (!i)
+				break;
+			if ((genv->flags&EF_BRKCONT_PASS) ||
+			    (i != LBREAK && i != LCONTIN)) {
+				quitenv(NULL);
+				unwind(i);
+			} else if (i == LBREAK) {
+				rv = 0;
+				goto Break;
+			}
+		}
+		rv = 0; /* in case of a continue */
+		while ((execute(t->left, XERROK, NULL) == 0) == (t->type == TWHILE))
+			rv = execute(t->right, flags & XERROK, xerrok);
+		break;
+
+	case TIF:
+	case TELIF:
+		if (t->right == NULL)
+			break;	/* should be error */
+		rv = execute(t->left, XERROK, NULL) == 0 ?
+		    execute(t->right->left, flags & XERROK, xerrok) :
+		    execute(t->right->right, flags & XERROK, xerrok);
+		break;
+
+	case TCASE:
+		cp = evalstr(t->str, DOTILDE);
+		for (t = t->left; t != NULL && t->type == TPAT; t = t->right) {
+			for (ap = t->vars; *ap; ap++) {
+				if ((s = evalstr(*ap, DOTILDE|DOPAT)) &&
+				    gmatch(cp, s, false))
+					goto Found;
+			}
+		}
+		break;
+	  Found:
+		rv = execute(t->left, flags & XERROK, xerrok);
+		break;
+
+	case TBRACE:
+		rv = execute(t->left, flags & XERROK, xerrok);
+		break;
+
+	case TFUNCT:
+		rv = define(t->str, t);
+		break;
+
+	case TTIME:
+		/* Clear XEXEC so nested execute() call doesn't exit
+		 * (allows "ls -l | time grep foo").
+		 */
+		rv = timex(t, flags & ~XEXEC, xerrok);
+		break;
+
+	case TEXEC:		/* an eval'd TCOM */
+		s = t->args[0];
+		ap = makenv();
+		restoresigs();
+		cleanup_proc_env();
+		execve(t->str, t->args, ap);
+		if (errno == ENOEXEC)
+			scriptexec(t, ap);
+		else
+			errorf("%s: %s", s, strerror(errno));
+	}
+    Break:
+	exstat = rv;
+
+	quitenv(NULL);		/* restores IO */
+	if ((flags&XEXEC))
+		unwind(LEXIT);	/* exit child */
+	if (rv != 0 && !(flags & XERROK) && !*xerrok) {
+		trapsig(SIGERR_);
+		if (Flag(FERREXIT))
+			unwind(LERROR);
+	}
+	return rv;
+}
+
+/*
+ * execute simple command
+ */
+
+static int
+comexec(struct op *t, struct tbl *volatile tp, char **ap, volatile int flags,
+    volatile int *xerrok)
+{
+	int i;
+	volatile int rv = 0;
+	char *cp;
+	char **lastp;
+	struct op texec;
+	int type_flags;
+	int keepasn_ok;
+	int fcflags = FC_BI|FC_FUNC|FC_PATH;
+	int bourne_function_call = 0;
+
+	/* snag the last argument for $_ XXX not the same as at&t ksh,
+	 * which only seems to set $_ after a newline (but not in
+	 * functions/dot scripts, but in interactive and script) -
+	 * perhaps save last arg here and set it in shell()?.
+	 */
+	if (!Flag(FSH) && Flag(FTALKING) && *(lastp = ap)) {
+		while (*++lastp)
+			;
+		/* setstr() can't fail here */
+		setstr(typeset("_", LOCAL, 0, INTEGER, 0), *--lastp,
+		    KSH_RETURN_ERROR);
+	}
+
+	/* Deal with the shell builtins builtin, exec and command since
+	 * they can be followed by other commands.  This must be done before
+	 * we know if we should create a local block, which must be done
+	 * before we can do a path search (in case the assignments change
+	 * PATH).
+	 * Odd cases:
+	 *   FOO=bar exec > /dev/null		FOO is kept but not exported
+	 *   FOO=bar exec foobar		FOO is exported
+	 *   FOO=bar command exec > /dev/null	FOO is neither kept nor exported
+	 *   FOO=bar command			FOO is neither kept nor exported
+	 *   PATH=... foobar			use new PATH in foobar search
+	 */
+	keepasn_ok = 1;
+	while (tp && tp->type == CSHELL) {
+		fcflags = FC_BI|FC_FUNC|FC_PATH;/* undo effects of command */
+		if (tp->val.f == c_builtin) {
+			if ((cp = *++ap) == NULL) {
+				tp = NULL;
+				break;
+			}
+			tp = findcom(cp, FC_BI);
+			if (tp == NULL)
+				errorf("builtin: %s: not a builtin", cp);
+			continue;
+		} else if (tp->val.f == c_exec) {
+			if (ap[1] == NULL)
+				break;
+			ap++;
+			flags |= XEXEC;
+		} else if (tp->val.f == c_command) {
+			int optc, saw_p = 0;
+
+			/* Ugly dealing with options in two places (here and
+			 * in c_command(), but such is life)
+			 */
+			ksh_getopt_reset(&builtin_opt, 0);
+			while ((optc = ksh_getopt(ap, &builtin_opt, ":p")) == 'p')
+				saw_p = 1;
+			if (optc != EOF)
+				break;	/* command -vV or something */
+			/* don't look for functions */
+			fcflags = FC_BI|FC_PATH;
+			if (saw_p) {
+				if (Flag(FRESTRICTED)) {
+					warningf(true,
+					    "command -p: restricted");
+					rv = 1;
+					goto Leave;
+				}
+				fcflags |= FC_DEFPATH;
+			}
+			ap += builtin_opt.optind;
+			/* POSIX says special builtins lose their status
+			 * if accessed using command.
+			 */
+			keepasn_ok = 0;
+			if (!ap[0]) {
+				/* ensure command with no args exits with 0 */
+				subst_exstat = 0;
+				break;
+			}
+		} else
+			break;
+		tp = findcom(ap[0], fcflags & (FC_BI|FC_FUNC));
+	}
+	if (keepasn_ok && (!ap[0] || (tp && (tp->flag & KEEPASN))))
+		type_flags = 0;
+	else {
+		/* create new variable/function block */
+		newblock();
+		/* ksh functions don't keep assignments, POSIX functions do. */
+		if (keepasn_ok && tp && tp->type == CFUNC &&
+		    !(tp->flag & FKSH)) {
+			bourne_function_call = 1;
+			type_flags = 0;
+		} else
+			type_flags = LOCAL|LOCAL_COPY|EXPORT;
+	}
+	if (Flag(FEXPORT))
+		type_flags |= EXPORT;
+	for (i = 0; t->vars[i]; i++) {
+		cp = evalstr(t->vars[i], DOASNTILDE);
+		if (Flag(FXTRACE)) {
+			if (i == 0)
+				shf_fprintf(shl_out, "%s",
+				    PS4_SUBSTITUTE(str_val(global("PS4"))));
+			shf_fprintf(shl_out, "%s%s", cp,
+			    t->vars[i + 1] ? " " : "\n");
+			if (!t->vars[i + 1])
+				shf_flush(shl_out);
+		}
+		typeset(cp, type_flags, 0, 0, 0);
+		if (bourne_function_call && !(type_flags & EXPORT))
+			typeset(cp, LOCAL|LOCAL_COPY|EXPORT, 0, 0, 0);
+	}
+
+	if ((cp = *ap) == NULL) {
+		rv = subst_exstat;
+		goto Leave;
+	} else if (!tp) {
+		if (Flag(FRESTRICTED) && strchr(cp, '/')) {
+			warningf(true, "%s: restricted", cp);
+			rv = 1;
+			goto Leave;
+		}
+		tp = findcom(cp, fcflags);
+	}
+
+	switch (tp->type) {
+	case CSHELL:			/* shell built-in */
+		rv = call_builtin(tp, ap);
+		break;
+
+	case CFUNC:			/* function call */
+	    {
+		volatile int old_xflag, old_inuse;
+		const char *volatile old_kshname;
+
+		if (!(tp->flag & ISSET)) {
+			struct tbl *ftp;
+
+			if (!tp->u.fpath) {
+				if (tp->u2.errno_) {
+					warningf(true,
+					    "%s: can't find function "
+					    "definition file - %s",
+					    cp, strerror(tp->u2.errno_));
+					rv = 126;
+				} else {
+					warningf(true,
+					    "%s: can't find function "
+					    "definition file", cp);
+					rv = 127;
+				}
+				break;
+			}
+			if (include(tp->u.fpath, 0, NULL, 0) < 0) {
+				warningf(true,
+				    "%s: can't open function definition file %s - %s",
+				    cp, tp->u.fpath, strerror(errno));
+				rv = 127;
+				break;
+			}
+			if (!(ftp = findfunc(cp, hash(cp), false)) ||
+			    !(ftp->flag & ISSET)) {
+				warningf(true,
+				    "%s: function not defined by %s",
+				    cp, tp->u.fpath);
+				rv = 127;
+				break;
+			}
+			tp = ftp;
+		}
+
+		/* ksh functions set $0 to function name, POSIX functions leave
+		 * $0 unchanged.
+		 */
+		old_kshname = kshname;
+		if (tp->flag & FKSH)
+			kshname = ap[0];
+		else
+			ap[0] = (char *) kshname;
+		genv->loc->argv = ap;
+		for (i = 0; *ap++ != NULL; i++)
+			;
+		genv->loc->argc = i - 1;
+		/* ksh-style functions handle getopts sanely,
+		 * bourne/posix functions are insane...
+		 */
+		if (tp->flag & FKSH) {
+			genv->loc->flags |= BF_DOGETOPTS;
+			genv->loc->getopts_state = user_opt;
+			getopts_reset(1);
+		}
+
+		old_xflag = Flag(FXTRACE);
+		Flag(FXTRACE) = tp->flag & TRACE ? true : false;
+
+		old_inuse = tp->flag & FINUSE;
+		tp->flag |= FINUSE;
+
+		genv->type = E_FUNC;
+		i = sigsetjmp(genv->jbuf, 0);
+		if (i == 0) {
+			/* seems odd to pass XERROK here, but at&t ksh does */
+			exstat = execute(tp->val.t, flags & XERROK, xerrok);
+			i = LRETURN;
+		}
+		kshname = old_kshname;
+		Flag(FXTRACE) = old_xflag;
+		tp->flag = (tp->flag & ~FINUSE) | old_inuse;
+		/* Were we deleted while executing?  If so, free the execution
+		 * tree.  todo: Unfortunately, the table entry is never re-used
+		 * until the lookup table is expanded.
+		 */
+		if ((tp->flag & (FDELETE|FINUSE)) == FDELETE) {
+			if (tp->flag & ALLOC) {
+				tp->flag &= ~ALLOC;
+				tfree(tp->val.t, tp->areap);
+			}
+			tp->flag = 0;
+		}
+		switch (i) {
+		case LRETURN:
+		case LERROR:
+			rv = exstat;
+			break;
+		case LINTR:
+		case LEXIT:
+		case LLEAVE:
+		case LSHELL:
+			quitenv(NULL);
+			unwind(i);
+			/* NOTREACHED */
+		default:
+			quitenv(NULL);
+			internal_errorf("CFUNC %d", i);
+		}
+		break;
+	    }
+
+	case CEXEC:		/* executable command */
+	case CTALIAS:		/* tracked alias */
+		if (!(tp->flag&ISSET)) {
+			/* errno_ will be set if the named command was found
+			 * but could not be executed (permissions, no execute
+			 * bit, directory, etc).  Print out a (hopefully)
+			 * useful error message and set the exit status to 126.
+			 */
+			if (tp->u2.errno_) {
+				warningf(true, "%s: cannot execute - %s", cp,
+				    strerror(tp->u2.errno_));
+				rv = 126;	/* POSIX */
+			} else {
+				warningf(true, "%s: not found", cp);
+				rv = 127;
+			}
+			break;
+		}
+
+		if (!Flag(FSH)) {
+			/* set $_ to program's full path */
+			/* setstr() can't fail here */
+			setstr(typeset("_", LOCAL|EXPORT, 0, INTEGER, 0),
+			    tp->val.s, KSH_RETURN_ERROR);
+		}
+
+		if (flags&XEXEC) {
+			j_exit();
+			if (!(flags&XBGND) || Flag(FMONITOR)) {
+				setexecsig(&sigtraps[SIGINT], SS_RESTORE_ORIG);
+				setexecsig(&sigtraps[SIGQUIT], SS_RESTORE_ORIG);
+			}
+		}
+
+		/* to fork we set up a TEXEC node and call execute */
+		memset(&texec, 0, sizeof(texec));
+		texec.type = TEXEC;
+		texec.left = t;	/* for tprint */
+		texec.str = tp->val.s;
+		texec.args = ap;
+		rv = exchild(&texec, flags, xerrok, -1);
+		break;
+	}
+  Leave:
+	if (flags & XEXEC) {
+		exstat = rv;
+		unwind(LLEAVE);
+	}
+	return rv;
+}
+
+static void
+scriptexec(struct op *tp, char **ap)
+{
+	char *shell;
+
+	shell = str_val(global("EXECSHELL"));
+	if (shell && *shell)
+		shell = search(shell, search_path, X_OK, NULL);
+	if (!shell || !*shell)
+		shell = _PATH_BSHELL;
+
+	*tp->args-- = tp->str;
+	*tp->args = shell;
+
+	execve(tp->args[0], tp->args, ap);
+
+	/* report both the program that was run and the bogus shell */
+	errorf("%s: %s: %s", tp->str, shell, strerror(errno));
+}
+
+int
+shcomexec(char **wp)
+{
+	struct tbl *tp;
+
+	tp = ktsearch(&builtins, *wp, hash(*wp));
+	if (tp == NULL)
+		internal_errorf("%s: %s", __func__, *wp);
+	return call_builtin(tp, wp);
+}
+
+/*
+ * Search function tables for a function.  If create set, a table entry
+ * is created if none is found.
+ */
+struct tbl *
+findfunc(const char *name, unsigned int h, int create)
+{
+	struct block *l;
+	struct tbl *tp = NULL;
+
+	for (l = genv->loc; l; l = l->next) {
+		tp = ktsearch(&l->funs, name, h);
+		if (tp)
+			break;
+		if (!l->next && create) {
+			tp = ktenter(&l->funs, name, h);
+			tp->flag = DEFINED;
+			tp->type = CFUNC;
+			tp->val.t = NULL;
+			break;
+		}
+	}
+	return tp;
+}
+
+/*
+ * define function.  Returns 1 if function is being undefined (t == 0) and
+ * function did not exist, returns 0 otherwise.
+ */
+int
+define(const char *name, struct op *t)
+{
+	struct tbl *tp;
+	int was_set = 0;
+
+	while (1) {
+		tp = findfunc(name, hash(name), true);
+
+		if (tp->flag & ISSET)
+			was_set = 1;
+		/* If this function is currently being executed, we zap this
+		 * table entry so findfunc() won't see it
+		 */
+		if (tp->flag & FINUSE) {
+			tp->name[0] = '\0';
+			tp->flag &= ~DEFINED; /* ensure it won't be found */
+			tp->flag |= FDELETE;
+		} else
+			break;
+	}
+
+	if (tp->flag & ALLOC) {
+		tp->flag &= ~(ISSET|ALLOC);
+		tfree(tp->val.t, tp->areap);
+	}
+
+	if (t == NULL) {		/* undefine */
+		ktdelete(tp);
+		return was_set ? 0 : 1;
+	}
+
+	tp->val.t = tcopy(t->left, tp->areap);
+	tp->flag |= (ISSET|ALLOC);
+	if (t->u.ksh_func)
+		tp->flag |= FKSH;
+
+	return 0;
+}
+
+/*
+ * add builtin
+ */
+void
+builtin(const char *name, int (*func) (char **))
+{
+	struct tbl *tp;
+	int flag;
+
+	/* see if any flags should be set for this builtin */
+	for (flag = 0; ; name++) {
+		if (*name == '=')	/* command does variable assignment */
+			flag |= KEEPASN;
+		else if (*name == '*')	/* POSIX special builtin */
+			flag |= SPEC_BI;
+		else if (*name == '+')	/* POSIX regular builtin */
+			flag |= REG_BI;
+		else
+			break;
+	}
+
+	tp = ktenter(&builtins, name, hash(name));
+	tp->flag = DEFINED | flag;
+	tp->type = CSHELL;
+	tp->val.f = func;
+}
+
+/*
+ * find command
+ * either function, hashed command, or built-in (in that order)
+ */
+struct tbl *
+findcom(const char *name, int flags)
+{
+	static struct tbl temp;
+	unsigned int h = hash(name);
+	struct tbl *tp = NULL, *tbi;
+	int insert = Flag(FTRACKALL);	/* insert if not found */
+	char *fpath;			/* for function autoloading */
+	char *npath;
+
+	if (strchr(name, '/') != NULL) {
+		insert = 0;
+		/* prevent FPATH search below */
+		flags &= ~FC_FUNC;
+		goto Search;
+	}
+	tbi = (flags & FC_BI) ? ktsearch(&builtins, name, h) : NULL;
+	/* POSIX says special builtins first, then functions, then
+	 * POSIX regular builtins, then search path...
+	 */
+	if ((flags & FC_SPECBI) && tbi && (tbi->flag & SPEC_BI))
+		tp = tbi;
+	if (!tp && (flags & FC_FUNC)) {
+		tp = findfunc(name, h, false);
+		if (tp && !(tp->flag & ISSET)) {
+			if ((fpath = str_val(global("FPATH"))) == null) {
+				tp->u.fpath = NULL;
+				tp->u2.errno_ = 0;
+			} else
+				tp->u.fpath = search(name, fpath, R_OK,
+				    &tp->u2.errno_);
+		}
+	}
+	if (!tp && (flags & FC_REGBI) && tbi && (tbi->flag & REG_BI))
+		tp = tbi;
+	/* todo: posix says non-special/non-regular builtins must
+	 * be triggered by some user-controllable means like a
+	 * special directory in PATH.  Requires modifications to
+	 * the search() function.  Tracked aliases should be
+	 * modified to allow tracking of builtin commands.
+	 * This should be under control of the FPOSIX flag.
+	 * If this is changed, also change c_whence...
+	 */
+	if (!tp && (flags & FC_UNREGBI) && tbi)
+		tp = tbi;
+	if (!tp && (flags & FC_PATH) && !(flags & FC_DEFPATH)) {
+		tp = ktsearch(&taliases, name, h);
+		if (tp && (tp->flag & ISSET) && access(tp->val.s, X_OK) != 0) {
+			if (tp->flag & ALLOC) {
+				tp->flag &= ~ALLOC;
+				afree(tp->val.s, APERM);
+			}
+			tp->flag &= ~ISSET;
+		}
+	}
+
+  Search:
+	if ((!tp || (tp->type == CTALIAS && !(tp->flag&ISSET))) &&
+	    (flags & FC_PATH)) {
+		if (!tp) {
+			if (insert && !(flags & FC_DEFPATH)) {
+				tp = ktenter(&taliases, name, h);
+				tp->type = CTALIAS;
+			} else {
+				tp = &temp;
+				tp->type = CEXEC;
+			}
+			tp->flag = DEFINED;	/* make ~ISSET */
+		}
+		npath = search(name, flags & FC_DEFPATH ? def_path :
+		    search_path, X_OK, &tp->u2.errno_);
+		if (npath) {
+			if (tp == &temp) {
+				tp->val.s = npath;
+			} else {
+				tp->val.s = str_save(npath, APERM);
+				if (npath != name)
+					afree(npath, ATEMP);
+			}
+			tp->flag |= ISSET|ALLOC;
+		} else if ((flags & FC_FUNC) &&
+		    (fpath = str_val(global("FPATH"))) != null &&
+		    (npath = search(name, fpath, R_OK,
+		    &tp->u2.errno_)) != NULL) {
+			/* An undocumented feature of at&t ksh is that it
+			 * searches FPATH if a command is not found, even
+			 * if the command hasn't been set up as an autoloaded
+			 * function (ie, no typeset -uf).
+			 */
+			tp = &temp;
+			tp->type = CFUNC;
+			tp->flag = DEFINED; /* make ~ISSET */
+			tp->u.fpath = npath;
+		}
+	}
+	return tp;
+}
+
+/*
+ * flush executable commands with relative paths
+ */
+void
+flushcom(int all)	/* just relative or all */
+{
+	struct tbl *tp;
+	struct tstate ts;
+
+	for (ktwalk(&ts, &taliases); (tp = ktnext(&ts)) != NULL; )
+		if ((tp->flag&ISSET) && (all || tp->val.s[0] != '/')) {
+			if (tp->flag&ALLOC) {
+				tp->flag &= ~(ALLOC|ISSET);
+				afree(tp->val.s, APERM);
+			}
+			tp->flag &= ~ISSET;
+		}
+}
+
+/* Check if path is something we want to find.  Returns -1 for failure. */
+int
+search_access(const char *path, int mode,
+    int *errnop)	/* set if candidate found, but not suitable */
+{
+	int ret, err = 0;
+	struct stat statb;
+
+	if (stat(path, &statb) < 0)
+		return -1;
+	ret = access(path, mode);
+	if (ret < 0)
+		err = errno; /* File exists, but we can't access it */
+	else if (mode == X_OK && (!S_ISREG(statb.st_mode) ||
+	    !(statb.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)))) {
+	    /* This 'cause access() says root can execute everything */
+		ret = -1;
+		err = S_ISDIR(statb.st_mode) ? EISDIR : EACCES;
+	}
+	if (err && errnop && !*errnop)
+		*errnop = err;
+	return ret;
+}
+
+/*
+ * search for command with PATH
+ */
+char *
+search(const char *name, const char *path,
+    int mode,		/* R_OK or X_OK */
+    int *errnop)	/* set if candidate found, but not suitable */
+{
+	const char *sp, *p;
+	char *xp;
+	XString xs;
+	int namelen;
+
+	if (errnop)
+		*errnop = 0;
+	if (strchr(name, '/')) {
+		if (search_access(name, mode, errnop) == 0)
+			return (char *) name;
+		return NULL;
+	}
+
+	namelen = strlen(name) + 1;
+	Xinit(xs, xp, 128, ATEMP);
+
+	sp = path;
+	while (sp != NULL) {
+		xp = Xstring(xs, xp);
+		if (!(p = strchr(sp, ':')))
+			p = sp + strlen(sp);
+		if (p != sp) {
+			XcheckN(xs, xp, p - sp);
+			memcpy(xp, sp, p - sp);
+			xp += p - sp;
+			*xp++ = '/';
+		}
+		sp = p;
+		XcheckN(xs, xp, namelen);
+		memcpy(xp, name, namelen);
+		if (search_access(Xstring(xs, xp), mode, errnop) == 0)
+			return Xclose(xs, xp + namelen);
+		if (*sp++ == '\0')
+			sp = NULL;
+	}
+	Xfree(xs, xp);
+	return NULL;
+}
+
+static int
+call_builtin(struct tbl *tp, char **wp)
+{
+	int rv;
+
+	builtin_argv0 = wp[0];
+	builtin_flag = tp->flag;
+	shf_reopen(1, SHF_WR, shl_stdout);
+	shl_stdout_ok = 1;
+	ksh_getopt_reset(&builtin_opt, GF_ERROR);
+	rv = (*tp->val.f)(wp);
+	shf_flush(shl_stdout);
+	shl_stdout_ok = 0;
+	builtin_flag = 0;
+	builtin_argv0 = NULL;
+	return rv;
+}
+
+/*
+ * set up redirection, saving old fd's in e->savefd
+ */
+static int
+iosetup(struct ioword *iop, struct tbl *tp)
+{
+	int u = -1;
+	char *cp = iop->name;
+	int iotype = iop->flag & IOTYPE;
+	int do_open = 1, do_close = 0, flags = 0;
+	struct ioword iotmp;
+	struct stat statb;
+
+	if (iotype != IOHERE)
+		cp = evalonestr(cp, DOTILDE|(Flag(FTALKING_I) ? DOGLOB : 0));
+
+	/* Used for tracing and error messages to print expanded cp */
+	iotmp = *iop;
+	iotmp.name = (iotype == IOHERE) ? NULL : cp;
+	iotmp.flag |= IONAMEXP;
+
+	if (Flag(FXTRACE))
+		shellf("%s%s\n",
+		    PS4_SUBSTITUTE(str_val(global("PS4"))),
+		    snptreef(NULL, 32, "%R", &iotmp));
+
+	switch (iotype) {
+	case IOREAD:
+		flags = O_RDONLY;
+		break;
+
+	case IOCAT:
+		flags = O_WRONLY | O_APPEND | O_CREAT;
+		break;
+
+	case IOWRITE:
+		flags = O_WRONLY | O_CREAT | O_TRUNC;
+		/* The stat() is here to allow redirections to
+		 * things like /dev/null without error.
+		 */
+		if (Flag(FNOCLOBBER) && !(iop->flag & IOCLOB) &&
+		    (stat(cp, &statb) < 0 || S_ISREG(statb.st_mode)))
+			flags |= O_EXCL;
+		break;
+
+	case IORDWR:
+		flags = O_RDWR | O_CREAT;
+		break;
+
+	case IOHERE:
+		do_open = 0;
+		/* herein() returns -2 if error has been printed */
+		u = herein(iop->heredoc, iop->flag & IOEVAL);
+		/* cp may have wrong name */
+		break;
+
+	case IODUP:
+	    {
+		const char *emsg;
+
+		do_open = 0;
+		if (*cp == '-' && !cp[1]) {
+			u = 1009;	 /* prevent error return below */
+			do_close = 1;
+		} else if ((u = check_fd(cp,
+		    X_OK | ((iop->flag & IORDUP) ? R_OK : W_OK),
+		    &emsg)) < 0) {
+			warningf(true, "%s: %s",
+			    snptreef(NULL, 32, "%R", &iotmp), emsg);
+			return -1;
+		}
+		if (u == iop->unit)
+			return 0;		/* "dup from" == "dup to" */
+		break;
+	    }
+	}
+
+	if (do_open) {
+		if (Flag(FRESTRICTED) && (flags & O_CREAT)) {
+			warningf(true, "%s: restricted", cp);
+			return -1;
+		}
+		u = open(cp, flags, 0666);
+	}
+	if (u < 0) {
+		/* herein() may already have printed message */
+		if (u == -1)
+			warningf(true, "cannot %s %s: %s",
+			    iotype == IODUP ? "dup" :
+			    (iotype == IOREAD || iotype == IOHERE) ?
+			    "open" : "create", cp, strerror(errno));
+		return -1;
+	}
+	/* Do not save if it has already been redirected (i.e. "cat >x >y"). */
+	if (genv->savefd[iop->unit] == 0) {
+		/* If these are the same, it means unit was previously closed */
+		if (u == iop->unit)
+			genv->savefd[iop->unit] = -1;
+		else
+			/* c_exec() assumes e->savefd[fd] set for any
+			 * redirections.  Ask savefd() not to close iop->unit;
+			 * this allows error messages to be seen if iop->unit
+			 * is 2; also means we can't lose the fd (eg, both
+			 * dup2 below and dup2 in restfd() failing).
+			 */
+			genv->savefd[iop->unit] = savefd(iop->unit);
+	}
+
+	if (do_close)
+		close(iop->unit);
+	else if (u != iop->unit) {
+		if (ksh_dup2(u, iop->unit, true) < 0) {
+			warningf(true,
+			    "could not finish (dup) redirection %s: %s",
+			    snptreef(NULL, 32, "%R", &iotmp),
+			    strerror(errno));
+			if (iotype != IODUP)
+				close(u);
+			return -1;
+		}
+		if (iotype != IODUP)
+			close(u);
+		/* Touching any co-process fd in an empty exec
+		 * causes the shell to close its copies
+		 */
+		else if (tp && tp->type == CSHELL && tp->val.f == c_exec) {
+			if (iop->flag & IORDUP)	/* possible exec <&p */
+				coproc_read_close(u);
+			else			/* possible exec >&p */
+				coproc_write_close(u);
+		}
+	}
+	if (u == 2) /* Clear any write errors */
+		shf_reopen(2, SHF_WR, shl_out);
+	return 0;
+}
+
+/*
+ * open here document temp file.
+ * if unquoted here, expand here temp file into second temp file.
+ */
+static int
+herein(const char *content, int sub)
+{
+	volatile int fd = -1;
+	struct source *s, *volatile osource;
+	struct shf *volatile shf;
+	struct temp *h;
+	int i;
+
+	/* ksh -c 'cat << EOF' can cause this... */
+	if (content == NULL) {
+		warningf(true, "here document missing");
+		return -2; /* special to iosetup(): don't print error */
+	}
+
+	/* Create temp file to hold content (done before newenv so temp
+	 * doesn't get removed too soon).
+	 */
+	h = maketemp(ATEMP, TT_HEREDOC_EXP, &genv->temps);
+	if (!(shf = h->shf) || (fd = open(h->name, O_RDONLY, 0)) < 0) {
+		warningf(true, "can't %s temporary file %s: %s",
+		    !shf ? "create" : "open",
+		    h->name, strerror(errno));
+		if (shf)
+			shf_close(shf);
+		return -2 /* special to iosetup(): don't print error */;
+	}
+
+	osource = source;
+	newenv(E_ERRH);
+	i = sigsetjmp(genv->jbuf, 0);
+	if (i) {
+		source = osource;
+		quitenv(shf);
+		close(fd);
+		return -2; /* special to iosetup(): don't print error */
+	}
+	if (sub) {
+		/* Do substitutions on the content of heredoc */
+		s = pushs(SSTRING, ATEMP);
+		s->start = s->str = content;
+		source = s;
+		if (yylex(ONEWORD|HEREDOC) != LWORD)
+			internal_errorf("%s: yylex", __func__);
+		source = osource;
+		shf_puts(evalstr(yylval.cp, 0), shf);
+	} else
+		shf_puts(content, shf);
+
+	quitenv(NULL);
+
+	if (shf_close(shf) == EOF) {
+		close(fd);
+		warningf(true, "error writing %s: %s", h->name,
+		    strerror(errno));
+		return -2; /* special to iosetup(): don't print error */
+	}
+
+	return fd;
+}
+
+/*
+ *	ksh special - the select command processing section
+ *	print the args in column form - assuming that we can
+ */
+static char *
+do_selectargs(char **ap, bool print_menu)
+{
+	static const char *const read_args[] = {
+		"read", "-r", "REPLY", NULL
+	};
+	const char *errstr;
+	char *s;
+	int i, argct;
+
+	for (argct = 0; ap[argct]; argct++)
+		;
+	while (1) {
+		/* Menu is printed if
+		 *	- this is the first time around the select loop
+		 *	- the user enters a blank line
+		 *	- the REPLY parameter is empty
+		 */
+		if (print_menu || !*str_val(global("REPLY")))
+			pr_menu(ap);
+		shellf("%s", str_val(global("PS3")));
+		if (call_builtin(findcom("read", FC_BI), (char **) read_args))
+			return NULL;
+		s = str_val(global("REPLY"));
+		if (*s) {
+			i = strtonum(s, 1, argct, &errstr);
+			if (errstr)
+				return null;
+			return ap[i - 1];
+		}
+		print_menu = 1;
+	}
+}
+
+struct select_menu_info {
+	char	*const *args;
+	int	arg_width;
+	int	num_width;
+};
+
+static char *select_fmt_entry(void *arg, int i, char *buf, int buflen);
+
+/* format a single select menu item */
+static char *
+select_fmt_entry(void *arg, int i, char *buf, int buflen)
+{
+	struct select_menu_info *smi = (struct select_menu_info *) arg;
+
+	shf_snprintf(buf, buflen, "%*d) %s",
+	    smi->num_width, i + 1, smi->args[i]);
+	return buf;
+}
+
+/*
+ *	print a select style menu
+ */
+int
+pr_menu(char *const *ap)
+{
+	struct select_menu_info smi;
+	char *const *pp;
+	int nwidth, dwidth;
+	int i, n;
+
+	/* Width/column calculations were done once and saved, but this
+	 * means select can't be used recursively so we re-calculate each
+	 * time (could save in a structure that is returned, but its probably
+	 * not worth the bother).
+	 */
+
+	/*
+	 * get dimensions of the list
+	 */
+	for (n = 0, nwidth = 0, pp = ap; *pp; n++, pp++) {
+		i = strlen(*pp);
+		nwidth = (i > nwidth) ? i : nwidth;
+	}
+	/*
+	 * we will print an index of the form
+	 *	%d)
+	 * in front of each entry
+	 * get the max width of this
+	 */
+	for (i = n, dwidth = 1; i >= 10; i /= 10)
+		dwidth++;
+
+	smi.args = ap;
+	smi.arg_width = nwidth;
+	smi.num_width = dwidth;
+	print_columns(shl_out, n, select_fmt_entry, (void *) &smi,
+	    dwidth + nwidth + 2, 1);
+
+	return n;
+}
+
+/* XXX: horrible kludge to fit within the framework */
+
+static char *plain_fmt_entry(void *arg, int i, char *buf, int buflen);
+
+static char *
+plain_fmt_entry(void *arg, int i, char *buf, int buflen)
+{
+	shf_snprintf(buf, buflen, "%s", ((char *const *)arg)[i]);
+	return buf;
+}
+
+int
+pr_list(char *const *ap)
+{
+	char *const *pp;
+	int nwidth;
+	int i, n;
+
+	for (n = 0, nwidth = 0, pp = ap; *pp; n++, pp++) {
+		i = strlen(*pp);
+		nwidth = (i > nwidth) ? i : nwidth;
+	}
+	print_columns(shl_out, n, plain_fmt_entry, (void *) ap, nwidth + 1, 0);
+
+	return n;
+}
+
+/*
+ *	[[ ... ]] evaluation routines
+ */
+
+extern const char *const dbtest_tokens[];
+extern const char db_close[];
+
+/* Test if the current token is a whatever.  Accepts the current token if
+ * it is.  Returns 0 if it is not, non-zero if it is (in the case of
+ * TM_UNOP and TM_BINOP, the returned value is a Test_op).
+ */
+static int
+dbteste_isa(Test_env *te, Test_meta meta)
+{
+	int ret = 0;
+	int uqword;
+	char *p;
+
+	if (!*te->pos.wp)
+		return meta == TM_END;
+
+	/* unquoted word? */
+	for (p = *te->pos.wp; *p == CHAR; p += 2)
+		;
+	uqword = *p == EOS;
+
+	if (meta == TM_UNOP || meta == TM_BINOP) {
+		if (uqword) {
+			char buf[8];	/* longer than the longest operator */
+			char *q = buf;
+			for (p = *te->pos.wp;
+			    *p == CHAR && q < &buf[sizeof(buf) - 1]; p += 2)
+				*q++ = p[1];
+			*q = '\0';
+			ret = (int) test_isop(te, meta, buf);
+		}
+	} else if (meta == TM_END)
+		ret = 0;
+	else
+		ret = uqword &&
+		    strcmp(*te->pos.wp, dbtest_tokens[(int) meta]) == 0;
+
+	/* Accept the token? */
+	if (ret)
+		te->pos.wp++;
+
+	return ret;
+}
+
+static const char *
+dbteste_getopnd(Test_env *te, Test_op op, int do_eval)
+{
+	char *s = *te->pos.wp;
+
+	if (!s)
+		return NULL;
+
+	te->pos.wp++;
+
+	if (!do_eval)
+		return null;
+
+	if (op == TO_STEQL || op == TO_STNEQ)
+		s = evalstr(s, DOTILDE | DOPAT);
+	else
+		s = evalstr(s, DOTILDE);
+
+	return s;
+}
+
+static int
+dbteste_eval(Test_env *te, Test_op op, const char *opnd1, const char *opnd2,
+    int do_eval)
+{
+	return test_eval(te, op, opnd1, opnd2, do_eval);
+}
+
+static void
+dbteste_error(Test_env *te, int offset, const char *msg)
+{
+	te->flags |= TEF_ERROR;
+	internal_warningf("%s: %s (offset %d)", __func__, msg, offset);
+}
diff --git a/expand.h b/expand.h
@@ -0,0 +1,106 @@
+/*	$OpenBSD: expand.h,v 1.15 2018/01/06 16:28:58 millert Exp $	*/
+
+/*
+ * Expanding strings
+ */
+
+#define X_EXTRA		8	/* this many extra bytes in X string */
+
+#if 0				/* Usage */
+	XString xs;
+	char *xp;
+
+	Xinit(xs, xp, 128, ATEMP); /* allocate initial string */
+	while ((c = generate()) {
+		Xcheck(xs, xp);	/* expand string if necessary */
+		Xput(xs, xp, c); /* add character */
+	}
+	return Xclose(xs, xp);	/* resize string */
+/*
+ * NOTE:
+ *     The Xcheck and Xinit macros have a magic + X_EXTRA in the lengths.
+ *     This is so that you can put up to X_EXTRA characters in a XString
+ *     before calling Xcheck. (See yylex in lex.c)
+ */
+#endif /* 0 */
+
+typedef struct XString {
+	char   *end, *beg;	/* end, begin of string */
+	size_t	len;		/* length */
+	Area	*areap;		/* area to allocate/free from */
+} XString;
+
+typedef char * XStringP;
+
+/* initialize expandable string */
+#define	Xinit(xs, xp, length, area) do { \
+			(xs).len = length; \
+			(xs).areap = (area); \
+			(xs).beg = alloc((xs).len + X_EXTRA, (xs).areap); \
+			(xs).end = (xs).beg + (xs).len; \
+			xp = (xs).beg; \
+		} while (0)
+
+/* stuff char into string */
+#define	Xput(xs, xp, c)	(*xp++ = (c))
+
+/* check if there are at least n bytes left */
+#define	XcheckN(xs, xp, n) do { \
+		    ptrdiff_t more = ((xp) + (n)) - (xs).end; \
+		    if (more > 0) \
+			xp = Xcheck_grow_(&xs, xp, more); \
+		} while (0)
+
+/* check for overflow, expand string */
+#define Xcheck(xs, xp)	XcheckN(xs, xp, 1)
+
+/* free string */
+#define	Xfree(xs, xp)	afree((xs).beg, (xs).areap)
+
+/* close, return string */
+#define	Xclose(xs, xp)	aresize((xs).beg, ((xp) - (xs).beg), (xs).areap)
+/* begin of string */
+#define	Xstring(xs, xp)	((xs).beg)
+
+#define Xnleft(xs, xp) ((xs).end - (xp))	/* may be less than 0 */
+#define	Xlength(xs, xp) ((xp) - (xs).beg)
+#define Xsize(xs, xp) ((xs).end - (xs).beg)
+#define	Xsavepos(xs, xp) ((xp) - (xs).beg)
+#define	Xrestpos(xs, xp, n) ((xs).beg + (n))
+
+char *	Xcheck_grow_(XString *xsp, char *xp, size_t more);
+
+/*
+ * expandable vector of generic pointers
+ */
+
+typedef struct XPtrV {
+	void  **cur;		/* next avail pointer */
+	void  **beg, **end;	/* begin, end of vector */
+} XPtrV;
+
+#define	XPinit(x, n) do { \
+			void **vp__; \
+			vp__ = areallocarray(NULL, n, sizeof(void *), ATEMP); \
+			(x).cur = (x).beg = vp__; \
+			(x).end = vp__ + n; \
+		} while (0)
+
+#define	XPput(x, p) do { \
+			if ((x).cur >= (x).end) { \
+				int n = XPsize(x); \
+				(x).beg = areallocarray((x).beg, n, \
+						   2 * sizeof(void *), ATEMP); \
+				(x).cur = (x).beg + n; \
+				(x).end = (x).cur + n; \
+			} \
+			*(x).cur++ = (p); \
+		} while (0)
+
+#define	XPptrv(x)	((x).beg)
+#define	XPsize(x)	((x).cur - (x).beg)
+
+#define	XPclose(x)	areallocarray((x).beg, XPsize(x), \
+					 sizeof(void *), ATEMP)
+
+#define	XPfree(x)	afree((x).beg, ATEMP)
diff --git a/expr.c b/expr.c
@@ -0,0 +1,601 @@
+/*	$OpenBSD: expr.c,v 1.34 2019/02/20 23:59:17 schwarze Exp $	*/
+
+/*
+ * Korn expression evaluation
+ */
+/*
+ * todo: better error handling: if in builtin, should be builtin error, etc.
+ */
+
+#include <ctype.h>
+#include <limits.h>
+#include <string.h>
+
+#include "sh.h"
+
+/* The order of these enums is constrained by the order of opinfo[] */
+enum token {
+	/* some (long) unary operators */
+	O_PLUSPLUS = 0, O_MINUSMINUS,
+	/* binary operators */
+	O_EQ, O_NE,
+	/* assignments are assumed to be in range O_ASN .. O_BORASN */
+	O_ASN, O_TIMESASN, O_DIVASN, O_MODASN, O_PLUSASN, O_MINUSASN,
+	O_LSHIFTASN, O_RSHIFTASN, O_BANDASN, O_BXORASN, O_BORASN,
+	O_LSHIFT, O_RSHIFT,
+	O_LE, O_GE, O_LT, O_GT,
+	O_LAND,
+	O_LOR,
+	O_TIMES, O_DIV, O_MOD,
+	O_PLUS, O_MINUS,
+	O_BAND,
+	O_BXOR,
+	O_BOR,
+	O_TERN,
+	O_COMMA,
+	/* things after this aren't used as binary operators */
+	/* unary that are not also binaries */
+	O_BNOT, O_LNOT,
+	/* misc */
+	OPEN_PAREN, CLOSE_PAREN, CTERN,
+	/* things that don't appear in the opinfo[] table */
+	VAR, LIT, END, BAD
+};
+#define IS_BINOP(op) (((int)op) >= (int)O_EQ && ((int)op) <= (int)O_COMMA)
+#define IS_ASSIGNOP(op)	((int)(op) >= (int)O_ASN && (int)(op) <= (int)O_BORASN)
+
+enum prec {
+	P_PRIMARY = 0,		/* VAR, LIT, (), ~ ! - + */
+	P_MULT,			/* * / % */
+	P_ADD,			/* + - */
+	P_SHIFT,		/* << >> */
+	P_RELATION,		/* < <= > >= */
+	P_EQUALITY,		/* == != */
+	P_BAND,			/* & */
+	P_BXOR,			/* ^ */
+	P_BOR,			/* | */
+	P_LAND,			/* && */
+	P_LOR,			/* || */
+	P_TERN,			/* ?: */
+	P_ASSIGN,		/* = *= /= %= += -= <<= >>= &= ^= |= */
+	P_COMMA			/* , */
+};
+#define MAX_PREC	P_COMMA
+
+struct opinfo {
+	char		name[4];
+	int		len;	/* name length */
+	enum prec	prec;	/* precedence: lower is higher */
+};
+
+/* Tokens in this table must be ordered so the longest are first
+ * (eg, += before +).  If you change something, change the order
+ * of enum token too.
+ */
+static const struct opinfo opinfo[] = {
+	{ "++",	 2, P_PRIMARY },	/* before + */
+	{ "--",	 2, P_PRIMARY },	/* before - */
+	{ "==",	 2, P_EQUALITY },	/* before = */
+	{ "!=",	 2, P_EQUALITY },	/* before ! */
+	{ "=",	 1, P_ASSIGN },		/* keep assigns in a block */
+	{ "*=",	 2, P_ASSIGN },
+	{ "/=",	 2, P_ASSIGN },
+	{ "%=",	 2, P_ASSIGN },
+	{ "+=",	 2, P_ASSIGN },
+	{ "-=",	 2, P_ASSIGN },
+	{ "<<=", 3, P_ASSIGN },
+	{ ">>=", 3, P_ASSIGN },
+	{ "&=",	 2, P_ASSIGN },
+	{ "^=",	 2, P_ASSIGN },
+	{ "|=",	 2, P_ASSIGN },
+	{ "<<",	 2, P_SHIFT },
+	{ ">>",	 2, P_SHIFT },
+	{ "<=",	 2, P_RELATION },
+	{ ">=",	 2, P_RELATION },
+	{ "<",	 1, P_RELATION },
+	{ ">",	 1, P_RELATION },
+	{ "&&",	 2, P_LAND },
+	{ "||",	 2, P_LOR },
+	{ "*",	 1, P_MULT },
+	{ "/",	 1, P_MULT },
+	{ "%",	 1, P_MULT },
+	{ "+",	 1, P_ADD },
+	{ "-",	 1, P_ADD },
+	{ "&",	 1, P_BAND },
+	{ "^",	 1, P_BXOR },
+	{ "|",	 1, P_BOR },
+	{ "?",	 1, P_TERN },
+	{ ",",	 1, P_COMMA },
+	{ "~",	 1, P_PRIMARY },
+	{ "!",	 1, P_PRIMARY },
+	{ "(",	 1, P_PRIMARY },
+	{ ")",	 1, P_PRIMARY },
+	{ ":",	 1, P_PRIMARY },
+	{ "",	 0, P_PRIMARY } /* end of table */
+};
+
+
+typedef struct expr_state Expr_state;
+struct expr_state {
+	const char *expression;		/* expression being evaluated */
+	const char *tokp;		/* lexical position */
+	enum token  tok;		/* token from token() */
+	int	    noassign;		/* don't do assigns (for ?:,&&,||) */
+	bool	    arith;		/* true if evaluating an $(())
+					 * expression
+					 */
+	struct tbl *val;		/* value from token() */
+	struct tbl *evaling;		/* variable that is being recursively
+					 * expanded (EXPRINEVAL flag set)
+					 */
+};
+
+enum error_type {
+	ET_UNEXPECTED, ET_BADLIT, ET_RECURSIVE,
+	ET_LVALUE, ET_RDONLY, ET_STR
+};
+
+static void	   evalerr(Expr_state *, enum error_type, const char *)
+		    __attribute__((__noreturn__));
+static struct tbl *evalexpr(Expr_state *, enum prec);
+static void	   token(Expr_state *);
+static struct tbl *do_ppmm(Expr_state *, enum token, struct tbl *, bool);
+static void	   assign_check(Expr_state *, enum token, struct tbl *);
+static struct tbl *tempvar(void);
+static struct tbl *intvar(Expr_state *, struct tbl *);
+
+/*
+ * parse and evaluate expression
+ */
+int
+evaluate(const char *expr, int64_t *rval, int error_ok, bool arith)
+{
+	struct tbl v;
+	int ret;
+
+	v.flag = DEFINED|INTEGER;
+	v.type = 0;
+	ret = v_evaluate(&v, expr, error_ok, arith);
+	*rval = v.val.i;
+	return ret;
+}
+
+/*
+ * parse and evaluate expression, storing result in vp.
+ */
+int
+v_evaluate(struct tbl *vp, const char *expr, volatile int error_ok,
+    bool arith)
+{
+	struct tbl *v;
+	Expr_state curstate;
+	Expr_state * const es = &curstate;
+	int save_disable_subst;
+	int i;
+
+	/* save state to allow recursive calls */
+	curstate.expression = curstate.tokp = expr;
+	curstate.noassign = 0;
+	curstate.arith = arith;
+	curstate.evaling = NULL;
+	curstate.val = NULL;
+
+	newenv(E_ERRH);
+	save_disable_subst = disable_subst;
+	i = sigsetjmp(genv->jbuf, 0);
+	if (i) {
+		disable_subst = save_disable_subst;
+		/* Clear EXPRINEVAL in of any variables we were playing with */
+		if (curstate.evaling)
+			curstate.evaling->flag &= ~EXPRINEVAL;
+		quitenv(NULL);
+		if (i == LAEXPR) {
+			if (error_ok == KSH_RETURN_ERROR)
+				return 0;
+			errorf(NULL);
+		}
+		unwind(i);
+		/* NOTREACHED */
+	}
+
+	token(es);
+#if 1 /* ifdef-out to disallow empty expressions to be treated as 0 */
+	if (es->tok == END) {
+		es->tok = LIT;
+		es->val = tempvar();
+	}
+#endif /* 0 */
+	v = intvar(es, evalexpr(es, MAX_PREC));
+
+	if (es->tok != END)
+		evalerr(es, ET_UNEXPECTED, NULL);
+
+	if (vp->flag & INTEGER)
+		setint_v(vp, v, es->arith);
+	else
+		/* can fail if readonly */
+		setstr(vp, str_val(v), error_ok);
+
+	quitenv(NULL);
+
+	return 1;
+}
+
+static void
+evalerr(Expr_state *es, enum error_type type, const char *str)
+{
+	char tbuf[2];
+	const char *s;
+
+	es->arith = false;
+	switch (type) {
+	case ET_UNEXPECTED:
+		switch (es->tok) {
+		case VAR:
+			s = es->val->name;
+			break;
+		case LIT:
+			s = str_val(es->val);
+			break;
+		case END:
+			s = "end of expression";
+			break;
+		case BAD:
+			tbuf[0] = *es->tokp;
+			tbuf[1] = '\0';
+			s = tbuf;
+			break;
+		default:
+			s = opinfo[(int)es->tok].name;
+		}
+		warningf(true, "%s: unexpected `%s'", es->expression, s);
+		break;
+
+	case ET_BADLIT:
+		warningf(true, "%s: bad number `%s'", es->expression, str);
+		break;
+
+	case ET_RECURSIVE:
+		warningf(true, "%s: expression recurses on parameter `%s'",
+		    es->expression, str);
+		break;
+
+	case ET_LVALUE:
+		warningf(true, "%s: %s requires lvalue",
+		    es->expression, str);
+		break;
+
+	case ET_RDONLY:
+		warningf(true, "%s: %s applied to read only variable",
+		    es->expression, str);
+		break;
+
+	default: /* keep gcc happy */
+	case ET_STR:
+		warningf(true, "%s: %s", es->expression, str);
+		break;
+	}
+	unwind(LAEXPR);
+}
+
+static struct tbl *
+evalexpr(Expr_state *es, enum prec prec)
+{
+	struct tbl *vl, *vr = NULL, *vasn;
+	enum token op;
+	int64_t res = 0;
+
+	if (prec == P_PRIMARY) {
+		op = es->tok;
+		if (op == O_BNOT || op == O_LNOT || op == O_MINUS ||
+		    op == O_PLUS) {
+			token(es);
+			vl = intvar(es, evalexpr(es, P_PRIMARY));
+			if (op == O_BNOT)
+				vl->val.i = ~vl->val.i;
+			else if (op == O_LNOT)
+				vl->val.i = !vl->val.i;
+			else if (op == O_MINUS)
+				vl->val.i = -vl->val.i;
+			/* op == O_PLUS is a no-op */
+		} else if (op == OPEN_PAREN) {
+			token(es);
+			vl = evalexpr(es, MAX_PREC);
+			if (es->tok != CLOSE_PAREN)
+				evalerr(es, ET_STR, "missing )");
+			token(es);
+		} else if (op == O_PLUSPLUS || op == O_MINUSMINUS) {
+			token(es);
+			vl = do_ppmm(es, op, es->val, true);
+			token(es);
+		} else if (op == VAR || op == LIT) {
+			vl = es->val;
+			token(es);
+		} else {
+			evalerr(es, ET_UNEXPECTED, NULL);
+			/* NOTREACHED */
+		}
+		if (es->tok == O_PLUSPLUS || es->tok == O_MINUSMINUS) {
+			vl = do_ppmm(es, es->tok, vl, false);
+			token(es);
+		}
+		return vl;
+	}
+	vl = evalexpr(es, ((int) prec) - 1);
+	for (op = es->tok; IS_BINOP(op) && opinfo[(int) op].prec == prec;
+	    op = es->tok) {
+		token(es);
+		vasn = vl;
+		if (op != O_ASN) /* vl may not have a value yet */
+			vl = intvar(es, vl);
+		if (IS_ASSIGNOP(op)) {
+			assign_check(es, op, vasn);
+			vr = intvar(es, evalexpr(es, P_ASSIGN));
+		} else if (op != O_TERN && op != O_LAND && op != O_LOR)
+			vr = intvar(es, evalexpr(es, ((int) prec) - 1));
+		if ((op == O_DIV || op == O_MOD || op == O_DIVASN ||
+		    op == O_MODASN) && vr->val.i == 0) {
+			if (es->noassign)
+				vr->val.i = 1;
+			else
+				evalerr(es, ET_STR, "zero divisor");
+		}
+		switch ((int) op) {
+		case O_TIMES:
+		case O_TIMESASN:
+			res = vl->val.i * vr->val.i;
+			break;
+		case O_DIV:
+		case O_DIVASN:
+			if (vl->val.i == LONG_MIN && vr->val.i == -1)
+				res = LONG_MIN;
+			else
+				res = vl->val.i / vr->val.i;
+			break;
+		case O_MOD:
+		case O_MODASN:
+			if (vl->val.i == LONG_MIN && vr->val.i == -1)
+				res = 0;
+			else
+				res = vl->val.i % vr->val.i;
+			break;
+		case O_PLUS:
+		case O_PLUSASN:
+			res = vl->val.i + vr->val.i;
+			break;
+		case O_MINUS:
+		case O_MINUSASN:
+			res = vl->val.i - vr->val.i;
+			break;
+		case O_LSHIFT:
+		case O_LSHIFTASN:
+			res = vl->val.i << vr->val.i;
+			break;
+		case O_RSHIFT:
+		case O_RSHIFTASN:
+			res = vl->val.i >> vr->val.i;
+			break;
+		case O_LT:
+			res = vl->val.i < vr->val.i;
+			break;
+		case O_LE:
+			res = vl->val.i <= vr->val.i;
+			break;
+		case O_GT:
+			res = vl->val.i > vr->val.i;
+			break;
+		case O_GE:
+			res = vl->val.i >= vr->val.i;
+			break;
+		case O_EQ:
+			res = vl->val.i == vr->val.i;
+			break;
+		case O_NE:
+			res = vl->val.i != vr->val.i;
+			break;
+		case O_BAND:
+		case O_BANDASN:
+			res = vl->val.i & vr->val.i;
+			break;
+		case O_BXOR:
+		case O_BXORASN:
+			res = vl->val.i ^ vr->val.i;
+			break;
+		case O_BOR:
+		case O_BORASN:
+			res = vl->val.i | vr->val.i;
+			break;
+		case O_LAND:
+			if (!vl->val.i)
+				es->noassign++;
+			vr = intvar(es, evalexpr(es, ((int) prec) - 1));
+			res = vl->val.i && vr->val.i;
+			if (!vl->val.i)
+				es->noassign--;
+			break;
+		case O_LOR:
+			if (vl->val.i)
+				es->noassign++;
+			vr = intvar(es, evalexpr(es, ((int) prec) - 1));
+			res = vl->val.i || vr->val.i;
+			if (vl->val.i)
+				es->noassign--;
+			break;
+		case O_TERN:
+			{
+				int e = vl->val.i != 0;
+
+				if (!e)
+					es->noassign++;
+				vl = evalexpr(es, MAX_PREC);
+				if (!e)
+					es->noassign--;
+				if (es->tok != CTERN)
+					evalerr(es, ET_STR, "missing :");
+				token(es);
+				if (e)
+					es->noassign++;
+				vr = evalexpr(es, P_TERN);
+				if (e)
+					es->noassign--;
+				vl = e ? vl : vr;
+			}
+			break;
+		case O_ASN:
+			res = vr->val.i;
+			break;
+		case O_COMMA:
+			res = vr->val.i;
+			break;
+		}
+		if (IS_ASSIGNOP(op)) {
+			vr->val.i = res;
+			if (vasn->flag & INTEGER)
+				setint_v(vasn, vr, es->arith);
+			else
+				setint(vasn, res);
+			vl = vr;
+		} else if (op != O_TERN)
+			vl->val.i = res;
+	}
+	return vl;
+}
+
+static void
+token(Expr_state *es)
+{
+	const char *cp;
+	int c;
+	char *tvar;
+
+	/* skip white space */
+	for (cp = es->tokp; (c = *cp), isspace((unsigned char)c); cp++)
+		;
+	es->tokp = cp;
+
+	if (c == '\0')
+		es->tok = END;
+	else if (letter(c)) {
+		for (; letnum(c); c = *cp)
+			cp++;
+		if (c == '[') {
+			int len;
+
+			len = array_ref_len(cp);
+			if (len == 0)
+				evalerr(es, ET_STR, "missing ]");
+			cp += len;
+		} else if (c == '(' /*)*/ ) {
+			/* todo: add math functions (all take single argument):
+			 * abs acos asin atan cos cosh exp int log sin sinh sqrt
+			 * tan tanh
+			 */
+			;
+		}
+		if (es->noassign) {
+			es->val = tempvar();
+			es->val->flag |= EXPRLVALUE;
+		} else {
+			tvar = str_nsave(es->tokp, cp - es->tokp, ATEMP);
+			es->val = global(tvar);
+			afree(tvar, ATEMP);
+		}
+		es->tok = VAR;
+	} else if (digit(c)) {
+		for (; c != '_' && (letnum(c) || c == '#'); c = *cp++)
+			;
+		tvar = str_nsave(es->tokp, --cp - es->tokp, ATEMP);
+		es->val = tempvar();
+		es->val->flag &= ~INTEGER;
+		es->val->type = 0;
+		es->val->val.s = tvar;
+		if (setint_v(es->val, es->val, es->arith) == NULL)
+			evalerr(es, ET_BADLIT, tvar);
+		afree(tvar, ATEMP);
+		es->tok = LIT;
+	} else {
+		int i, n0;
+
+		for (i = 0; (n0 = opinfo[i].name[0]); i++)
+			if (c == n0 &&
+			    strncmp(cp, opinfo[i].name, opinfo[i].len) == 0) {
+				es->tok = (enum token) i;
+				cp += opinfo[i].len;
+				break;
+			}
+		if (!n0)
+			es->tok = BAD;
+	}
+	es->tokp = cp;
+}
+
+/* Do a ++ or -- operation */
+static struct tbl *
+do_ppmm(Expr_state *es, enum token op, struct tbl *vasn, bool is_prefix)
+{
+	struct tbl *vl;
+	int oval;
+
+	assign_check(es, op, vasn);
+
+	vl = intvar(es, vasn);
+	oval = op == O_PLUSPLUS ? vl->val.i++ : vl->val.i--;
+	if (vasn->flag & INTEGER)
+		setint_v(vasn, vl, es->arith);
+	else
+		setint(vasn, vl->val.i);
+	if (!is_prefix)		/* undo the inc/dec */
+		vl->val.i = oval;
+
+	return vl;
+}
+
+static void
+assign_check(Expr_state *es, enum token op, struct tbl *vasn)
+{
+	if (es->tok == END || vasn == NULL ||
+	    (vasn->name[0] == '\0' && !(vasn->flag & EXPRLVALUE)))
+		evalerr(es, ET_LVALUE, opinfo[(int) op].name);
+	else if (vasn->flag & RDONLY)
+		evalerr(es, ET_RDONLY, opinfo[(int) op].name);
+}
+
+static struct tbl *
+tempvar(void)
+{
+	struct tbl *vp;
+
+	vp = alloc(sizeof(struct tbl), ATEMP);
+	vp->flag = ISSET|INTEGER;
+	vp->type = 0;
+	vp->areap = ATEMP;
+	vp->val.i = 0;
+	vp->name[0] = '\0';
+	return vp;
+}
+
+/* cast (string) variable to temporary integer variable */
+static struct tbl *
+intvar(Expr_state *es, struct tbl *vp)
+{
+	struct tbl *vq;
+
+	/* try to avoid replacing a temp var with another temp var */
+	if (vp->name[0] == '\0' &&
+	    (vp->flag & (ISSET|INTEGER|EXPRLVALUE)) == (ISSET|INTEGER))
+		return vp;
+
+	vq = tempvar();
+	if (setint_v(vq, vp, es->arith) == NULL) {
+		if (vp->flag & EXPRINEVAL)
+			evalerr(es, ET_RECURSIVE, vp->name);
+		es->evaling = vp;
+		vp->flag |= EXPRINEVAL;
+		disable_subst++;
+		v_evaluate(vq, str_val(vp), KSH_UNWIND_ERROR, es->arith);
+		disable_subst--;
+		vp->flag &= ~EXPRINEVAL;
+		es->evaling = NULL;
+	}
+	return vq;
+}
diff --git a/history.c b/history.c
@@ -0,0 +1,869 @@
+/*	$OpenBSD: history.c,v 1.81 2018/11/20 07:02:23 martijn Exp $	*/
+
+/*
+ * command history
+ */
+
+/*
+ *	This file contains
+ *	a)	the original in-memory history  mechanism
+ *	b)	a more complicated mechanism done by  pc@hillside.co.uk
+ *		that more closely follows the real ksh way of doing
+ *		things.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/uio.h>
+#include <sys/file.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <vis.h>
+#include <inttypes.h>
+
+#include "sh.h"
+
+static void	history_write(void);
+static FILE	*history_open(void);
+static void	history_load(Source *);
+static void	history_close(void);
+
+static int	hist_execute(char *);
+static int	hist_replace(char **, const char *, const char *, int);
+static char   **hist_get(const char *, int, int);
+static char   **hist_get_oldest(void);
+static void	histbackup(void);
+
+static FILE	*histfh;
+static char   **histbase;	/* actual start of the history[] allocation */
+static char   **current;	/* current position in history[] */
+static char    *hname;		/* current name of history file */
+static int	hstarted;	/* set after hist_init() called */
+static int	ignoredups;	/* ditch duplicated history lines? */
+static int	ignorespace;	/* ditch lines starting with a space? */
+static Source	*hist_source;
+static uint32_t	line_co;
+
+static struct stat last_sb;
+
+static volatile sig_atomic_t	c_fc_depth;
+
+int
+c_fc(char **wp)
+{
+	struct shf *shf;
+	struct temp *tf = NULL;
+	char *p, *editor = NULL;
+	int gflag = 0, lflag = 0, nflag = 0, sflag = 0, rflag = 0;
+	int optc, ret;
+	char *first = NULL, *last = NULL;
+	char **hfirst, **hlast, **hp;
+
+	if (c_fc_depth != 0) {
+		bi_errorf("history function called recursively");
+		return 1;
+	}
+
+	if (!Flag(FTALKING_I)) {
+		bi_errorf("history functions not available");
+		return 1;
+	}
+
+	while ((optc = ksh_getopt(wp, &builtin_opt,
+	    "e:glnrs0,1,2,3,4,5,6,7,8,9,")) != -1)
+		switch (optc) {
+		case 'e':
+			p = builtin_opt.optarg;
+			if (strcmp(p, "-") == 0)
+				sflag++;
+			else {
+				size_t len = strlen(p) + 4;
+				editor = str_nsave(p, len, ATEMP);
+				strlcat(editor, " $_", len);
+			}
+			break;
+		case 'g': /* non-at&t ksh */
+			gflag++;
+			break;
+		case 'l':
+			lflag++;
+			break;
+		case 'n':
+			nflag++;
+			break;
+		case 'r':
+			rflag++;
+			break;
+		case 's':	/* posix version of -e - */
+			sflag++;
+			break;
+		  /* kludge city - accept -num as -- -num (kind of) */
+		case '0': case '1': case '2': case '3': case '4':
+		case '5': case '6': case '7': case '8': case '9':
+			p = shf_smprintf("-%c%s",
+					optc, builtin_opt.optarg);
+			if (!first)
+				first = p;
+			else if (!last)
+				last = p;
+			else {
+				bi_errorf("too many arguments");
+				return 1;
+			}
+			break;
+		case '?':
+			return 1;
+		}
+	wp += builtin_opt.optind;
+
+	/* Substitute and execute command */
+	if (sflag) {
+		char *pat = NULL, *rep = NULL;
+
+		if (editor || lflag || nflag || rflag) {
+			bi_errorf("can't use -e, -l, -n, -r with -s (-e -)");
+			return 1;
+		}
+
+		/* Check for pattern replacement argument */
+		if (*wp && **wp && (p = strchr(*wp + 1, '='))) {
+			pat = str_save(*wp, ATEMP);
+			p = pat + (p - *wp);
+			*p++ = '\0';
+			rep = p;
+			wp++;
+		}
+		/* Check for search prefix */
+		if (!first && (first = *wp))
+			wp++;
+		if (last || *wp) {
+			bi_errorf("too many arguments");
+			return 1;
+		}
+
+		hp = first ? hist_get(first, false, false) :
+		    hist_get_newest(false);
+		if (!hp)
+			return 1;
+		c_fc_depth++;
+		ret = hist_replace(hp, pat, rep, gflag);
+		c_fc_reset();
+		return ret;
+	}
+
+	if (editor && (lflag || nflag)) {
+		bi_errorf("can't use -l, -n with -e");
+		return 1;
+	}
+
+	if (!first && (first = *wp))
+		wp++;
+	if (!last && (last = *wp))
+		wp++;
+	if (*wp) {
+		bi_errorf("too many arguments");
+		return 1;
+	}
+	if (!first) {
+		hfirst = lflag ? hist_get("-16", true, true) :
+		    hist_get_newest(false);
+		if (!hfirst)
+			return 1;
+		/* can't fail if hfirst didn't fail */
+		hlast = hist_get_newest(false);
+	} else {
+		/* POSIX says not an error if first/last out of bounds
+		 * when range is specified; at&t ksh and pdksh allow out of
+		 * bounds for -l as well.
+		 */
+		hfirst = hist_get(first, (lflag || last) ? true : false,
+		    lflag ? true : false);
+		if (!hfirst)
+			return 1;
+		hlast = last ? hist_get(last, true, lflag ? true : false) :
+		    (lflag ? hist_get_newest(false) : hfirst);
+		if (!hlast)
+			return 1;
+	}
+	if (hfirst > hlast) {
+		char **temp;
+
+		temp = hfirst; hfirst = hlast; hlast = temp;
+		rflag = !rflag; /* POSIX */
+	}
+
+	/* List history */
+	if (lflag) {
+		char *s, *t;
+		const char *nfmt = nflag ? "\t" : "%d\t";
+
+		for (hp = rflag ? hlast : hfirst;
+		    hp >= hfirst && hp <= hlast; hp += rflag ? -1 : 1) {
+			shf_fprintf(shl_stdout, nfmt,
+			    hist_source->line - (int) (histptr - hp));
+			/* print multi-line commands correctly */
+			for (s = *hp; (t = strchr(s, '\n')); s = t)
+				shf_fprintf(shl_stdout, "%.*s\t", ++t - s, s);
+			shf_fprintf(shl_stdout, "%s\n", s);
+		}
+		shf_flush(shl_stdout);
+		return 0;
+	}
+
+	/* Run editor on selected lines, then run resulting commands */
+
+	tf = maketemp(ATEMP, TT_HIST_EDIT, &genv->temps);
+	if (!(shf = tf->shf)) {
+		bi_errorf("cannot create temp file %s - %s",
+		    tf->name, strerror(errno));
+		return 1;
+	}
+	for (hp = rflag ? hlast : hfirst;
+	    hp >= hfirst && hp <= hlast; hp += rflag ? -1 : 1)
+		shf_fprintf(shf, "%s\n", *hp);
+	if (shf_close(shf) == EOF) {
+		bi_errorf("error writing temporary file - %s", strerror(errno));
+		return 1;
+	}
+
+	/* Ignore setstr errors here (arbitrary) */
+	setstr(local("_", false), tf->name, KSH_RETURN_ERROR);
+
+	/* XXX: source should not get trashed by this.. */
+	{
+		Source *sold = source;
+
+		ret = command(editor ? editor : "${FCEDIT:-/bin/ed} $_", 0);
+		source = sold;
+		if (ret)
+			return ret;
+	}
+
+	{
+		struct stat statb;
+		XString xs;
+		char *xp;
+		int n;
+
+		if (!(shf = shf_open(tf->name, O_RDONLY, 0, 0))) {
+			bi_errorf("cannot open temp file %s", tf->name);
+			return 1;
+		}
+
+		n = fstat(shf->fd, &statb) < 0 ? 128 :
+		    statb.st_size + 1;
+		Xinit(xs, xp, n, hist_source->areap);
+		while ((n = shf_read(xp, Xnleft(xs, xp), shf)) > 0) {
+			xp += n;
+			if (Xnleft(xs, xp) <= 0)
+				XcheckN(xs, xp, Xlength(xs, xp));
+		}
+		if (n < 0) {
+			bi_errorf("error reading temp file %s - %s",
+			    tf->name, strerror(shf->errno_));
+			shf_close(shf);
+			return 1;
+		}
+		shf_close(shf);
+		*xp = '\0';
+		strip_nuls(Xstring(xs, xp), Xlength(xs, xp));
+		c_fc_depth++;
+		ret = hist_execute(Xstring(xs, xp));
+		c_fc_reset();
+		return ret;
+	}
+}
+
+/* Reset the c_fc depth counter.
+ * Made available for when an fc call is interrupted.
+ */
+void
+c_fc_reset(void)
+{
+	c_fc_depth = 0;
+}
+
+/* Save cmd in history, execute cmd (cmd gets trashed) */
+static int
+hist_execute(char *cmd)
+{
+	Source *sold;
+	int ret;
+	char *p, *q;
+
+	histbackup();
+
+	for (p = cmd; p; p = q) {
+		if ((q = strchr(p, '\n'))) {
+			*q++ = '\0'; /* kill the newline */
+			if (!*q) /* ignore trailing newline */
+				q = NULL;
+		}
+		histsave(++(hist_source->line), p, 1);
+
+		shellf("%s\n", p); /* POSIX doesn't say this is done... */
+		if ((p = q)) /* restore \n (trailing \n not restored) */
+			q[-1] = '\n';
+	}
+
+	/* Commands are executed here instead of pushing them onto the
+	 * input 'cause posix says the redirection and variable assignments
+	 * in
+	 *	X=y fc -e - 42 2> /dev/null
+	 * are to effect the repeated commands environment.
+	 */
+	/* XXX: source should not get trashed by this.. */
+	sold = source;
+	ret = command(cmd, 0);
+	source = sold;
+	return ret;
+}
+
+static int
+hist_replace(char **hp, const char *pat, const char *rep, int global)
+{
+	char *line;
+
+	if (!pat)
+		line = str_save(*hp, ATEMP);
+	else {
+		char *s, *s1;
+		int pat_len = strlen(pat);
+		int rep_len = strlen(rep);
+		int len;
+		XString xs;
+		char *xp;
+		int any_subst = 0;
+
+		Xinit(xs, xp, 128, ATEMP);
+		for (s = *hp; (s1 = strstr(s, pat)) && (!any_subst || global);
+		    s = s1 + pat_len) {
+			any_subst = 1;
+			len = s1 - s;
+			XcheckN(xs, xp, len + rep_len);
+			memcpy(xp, s, len);		/* first part */
+			xp += len;
+			memcpy(xp, rep, rep_len);	/* replacement */
+			xp += rep_len;
+		}
+		if (!any_subst) {
+			bi_errorf("substitution failed");
+			return 1;
+		}
+		len = strlen(s) + 1;
+		XcheckN(xs, xp, len);
+		memcpy(xp, s, len);
+		xp += len;
+		line = Xclose(xs, xp);
+	}
+	return hist_execute(line);
+}
+
+/*
+ * get pointer to history given pattern
+ * pattern is a number or string
+ */
+static char **
+hist_get(const char *str, int approx, int allow_cur)
+{
+	char **hp = NULL;
+	int n;
+
+	if (getn(str, &n)) {
+		hp = histptr + (n < 0 ? n : (n - hist_source->line));
+		if ((long)hp < (long)history) {
+			if (approx)
+				hp = hist_get_oldest();
+			else {
+				bi_errorf("%s: not in history", str);
+				hp = NULL;
+			}
+		} else if (hp > histptr) {
+			if (approx)
+				hp = hist_get_newest(allow_cur);
+			else {
+				bi_errorf("%s: not in history", str);
+				hp = NULL;
+			}
+		} else if (!allow_cur && hp == histptr) {
+			bi_errorf("%s: invalid range", str);
+			hp = NULL;
+		}
+	} else {
+		int anchored = *str == '?' ? (++str, 0) : 1;
+
+		/* the -1 is to avoid the current fc command */
+		n = findhist(histptr - history - 1, 0, str, anchored);
+		if (n < 0) {
+			bi_errorf("%s: not in history", str);
+			hp = NULL;
+		} else
+			hp = &history[n];
+	}
+	return hp;
+}
+
+/* Return a pointer to the newest command in the history */
+char **
+hist_get_newest(int allow_cur)
+{
+	if (histptr < history || (!allow_cur && histptr == history)) {
+		bi_errorf("no history (yet)");
+		return NULL;
+	}
+	if (allow_cur)
+		return histptr;
+	return histptr - 1;
+}
+
+/* Return a pointer to the oldest command in the history */
+static char **
+hist_get_oldest(void)
+{
+	if (histptr <= history) {
+		bi_errorf("no history (yet)");
+		return NULL;
+	}
+	return history;
+}
+
+/******************************/
+/* Back up over last histsave */
+/******************************/
+static void
+histbackup(void)
+{
+	static int last_line = -1;
+
+	if (histptr >= history && last_line != hist_source->line) {
+		hist_source->line--;
+		afree(*histptr, APERM);
+		histptr--;
+		last_line = hist_source->line;
+	}
+}
+
+static void
+histreset(void)
+{
+	char **hp;
+
+	for (hp = history; hp <= histptr; hp++)
+		afree(*hp, APERM);
+
+	histptr = history - 1;
+	hist_source->line = 0;
+}
+
+/*
+ * Return the current position.
+ */
+char **
+histpos(void)
+{
+	return current;
+}
+
+int
+histnum(int n)
+{
+	int	last = histptr - history;
+
+	if (n < 0 || n >= last) {
+		current = histptr;
+		return last;
+	} else {
+		current = &history[n];
+		return n;
+	}
+}
+
+/*
+ * This will become unnecessary if hist_get is modified to allow
+ * searching from positions other than the end, and in either
+ * direction.
+ */
+int
+findhist(int start, int fwd, const char *str, int anchored)
+{
+	char	**hp;
+	int	maxhist = histptr - history;
+	int	incr = fwd ? 1 : -1;
+	int	len = strlen(str);
+
+	if (start < 0 || start >= maxhist)
+		start = maxhist;
+
+	hp = &history[start];
+	for (; hp >= history && hp <= histptr; hp += incr)
+		if ((anchored && strncmp(*hp, str, len) == 0) ||
+		    (!anchored && strstr(*hp, str)))
+			return hp - history;
+
+	return -1;
+}
+
+int
+findhistrel(const char *str)
+{
+	int	maxhist = histptr - history;
+	int	start = maxhist - 1;
+	int	rec = atoi(str);
+
+	if (rec == 0)
+		return -1;
+	if (rec > 0) {
+		if (rec > maxhist)
+			return -1;
+		return rec - 1;
+	}
+	if (rec > maxhist)
+		return -1;
+	return start + rec + 1;
+}
+
+void
+sethistcontrol(const char *str)
+{
+	char *spec, *tok, *state;
+
+	ignorespace = 0;
+	ignoredups = 0;
+
+	if (str == NULL)
+		return;
+
+	spec = str_save(str, ATEMP);
+	for (tok = strtok_r(spec, ":", &state); tok != NULL;
+	     tok = strtok_r(NULL, ":", &state)) {
+		if (strcmp(tok, "ignoredups") == 0)
+			ignoredups = 1;
+		else if (strcmp(tok, "ignorespace") == 0)
+			ignorespace = 1;
+	}
+	afree(spec, ATEMP);
+}
+
+/*
+ *	set history
+ *	this means reallocating the dataspace
+ */
+void
+sethistsize(int n)
+{
+	if (n > 0 && (uint32_t)n != histsize) {
+		int offset = histptr - history;
+
+		/* save most recent history */
+		if (offset > n - 1) {
+			char **hp;
+
+			offset = n - 1;
+			for (hp = history; hp < histptr - offset; hp++)
+				afree(*hp, APERM);
+			memmove(history, histptr - offset, n * sizeof(char *));
+		}
+
+		histsize = n;
+		histbase = areallocarray(histbase, n + 1, sizeof(char *), APERM);
+		history = histbase + 1;
+		histptr = history + offset;
+	}
+}
+
+/*
+ *	set history file
+ *	This can mean reloading/resetting/starting history file
+ *	maintenance
+ */
+void
+sethistfile(const char *name)
+{
+	/* if not started then nothing to do */
+	if (hstarted == 0)
+		return;
+
+	/* if the name is the same as the name we have */
+	if (hname && strcmp(hname, name) == 0)
+		return;
+	/*
+	 * its a new name - possibly
+	 */
+	if (hname) {
+		afree(hname, APERM);
+		hname = NULL;
+		histreset();
+	}
+
+	history_close();
+	hist_init(hist_source);
+}
+
+/*
+ *	initialise the history vector
+ */
+void
+init_histvec(void)
+{
+	if (histbase == NULL) {
+		histsize = HISTORYSIZE;
+		/*
+		 * allocate one extra element so that histptr always
+		 * lies within array bounds
+		 */
+		histbase = areallocarray(NULL, histsize + 1, sizeof(char *),
+		    APERM);
+		*histbase = NULL;
+		history = histbase + 1;
+		histptr = history - 1;
+	}
+}
+
+static void
+history_lock(int operation)
+{
+	while (flock(fileno(histfh), operation) != 0) {
+		if (errno == EINTR || errno == EAGAIN)
+			continue;
+		else
+			break;
+	}
+}
+
+/*
+ *	Routines added by Peter Collinson BSDI(Europe)/Hillside Systems to
+ *	a) permit HISTSIZE to control number of lines of history stored
+ *	b) maintain a physical history file
+ *
+ *	It turns out that there is a lot of ghastly hackery here
+ */
+
+
+/*
+ * save command in history
+ */
+void
+histsave(int lno, const char *cmd, int dowrite)
+{
+	char		*c, *cp;
+
+	if (ignorespace && cmd[0] == ' ')
+		return;
+
+	c = str_save(cmd, APERM);
+	if ((cp = strrchr(c, '\n')) != NULL)
+		*cp = '\0';
+
+	/*
+	 * XXX to properly check for duplicated lines we should first reload
+	 * the histfile if needed
+	 */
+	if (ignoredups && histptr >= history && strcmp(*histptr, c) == 0) {
+		afree(c, APERM);
+		return;
+	}
+
+	if (dowrite && histfh) {
+#ifndef SMALL
+		struct stat	sb;
+
+		history_lock(LOCK_EX);
+		if (fstat(fileno(histfh), &sb) != -1) {
+			if (timespeccmp(&sb.st_mtim, &last_sb.st_mtim, ==))
+				; /* file is unchanged */
+			else {
+				histreset();
+				history_load(hist_source);
+			}
+		}
+#endif
+	}
+
+	if (histptr < history + histsize - 1)
+		histptr++;
+	else { /* remove oldest command */
+		afree(*history, APERM);
+		memmove(history, history + 1,
+		    (histsize - 1) * sizeof(*history));
+	}
+	*histptr = c;
+
+	if (dowrite && histfh) {
+#ifndef SMALL
+		char *encoded;
+
+		/* append to file */
+		if (fseeko(histfh, 0, SEEK_END) == 0 &&
+		    stravis(&encoded, c, VIS_SAFE | VIS_NL) != -1) {
+			fprintf(histfh, "%s\n", encoded);
+			fflush(histfh);
+			fstat(fileno(histfh), &last_sb);
+			line_co++;
+			history_write();
+			free(encoded);
+		}
+		history_lock(LOCK_UN);
+#endif
+	}
+}
+
+static FILE *
+history_open(void)
+{
+	FILE		*f = NULL;
+#ifndef SMALL
+	struct stat	sb;
+	int		fd, fddup;
+
+	if ((fd = open(hname, O_RDWR | O_CREAT, 0600)) == -1)
+		return NULL;
+	if (fstat(fd, &sb) == -1 || sb.st_uid != getuid()) {
+		close(fd);
+		return NULL;
+	}
+	fddup = savefd(fd);
+	if (fddup != fd)
+		close(fd);
+
+	if ((f = fdopen(fddup, "r+")) == NULL)
+		close(fddup);
+	else
+		last_sb = sb;
+#endif
+	return f;
+}
+
+static void
+history_close(void)
+{
+	if (histfh) {
+		fflush(histfh);
+		fclose(histfh);
+		histfh = NULL;
+	}
+}
+
+static void
+history_load(Source *s)
+{
+	char		*p, encoded[LINE + 1], line[LINE + 1];
+	int		 toolongseen = 0;
+
+	rewind(histfh);
+	line_co = 1;
+
+	/* just read it all; will auto resize history upon next command */
+	while (fgets(encoded, sizeof(encoded), histfh)) {
+		if ((p = strchr(encoded, '\n')) == NULL) {
+			/* discard overlong line */
+			do {
+				/* maybe a missing trailing newline? */
+				if (strlen(encoded) != sizeof(encoded) - 1) {
+					bi_errorf("history file is corrupt");
+					return;
+				}
+			} while (fgets(encoded, sizeof(encoded), histfh)
+			    && strchr(encoded, '\n') == NULL);
+
+			if (!toolongseen) {
+				toolongseen = 1;
+				bi_errorf("ignored history line(s) longer than"
+				    " %d bytes", LINE);
+			}
+
+			continue;
+		}
+		*p = '\0';
+		s->line = line_co;
+		s->cmd_offset = line_co;
+		strunvis(line, encoded);
+		histsave(line_co, line, 0);
+		line_co++;
+	}
+
+	history_write();
+}
+
+#define HMAGIC1 0xab
+#define HMAGIC2 0xcd
+
+void
+hist_init(Source *s)
+{
+	int oldmagic1, oldmagic2;
+
+	if (Flag(FTALKING) == 0)
+		return;
+
+	hstarted = 1;
+
+	hist_source = s;
+
+	if (str_val(global("HISTFILE")) == null)
+		return;
+	hname = str_save(str_val(global("HISTFILE")), APERM);
+	histfh = history_open();
+	if (histfh == NULL)
+		return;
+
+	oldmagic1 = fgetc(histfh);
+	oldmagic2 = fgetc(histfh);
+
+	if (oldmagic1 == EOF || oldmagic2 == EOF) {
+		if (!feof(histfh) && ferror(histfh)) {
+			history_close();
+			return;
+		}
+	} else if (oldmagic1 == HMAGIC1 && oldmagic2 == HMAGIC2) {
+		bi_errorf("ignoring old style history file");
+		history_close();
+		return;
+	}
+
+	history_load(s);
+
+	history_lock(LOCK_UN);
+}
+
+static void
+history_write(void)
+{
+	char		**hp, *encoded;
+
+	/* see if file has grown over 25% */
+	if (line_co < histsize + (histsize / 4))
+		return;
+
+	/* rewrite the whole caboodle */
+	rewind(histfh);
+	if (ftruncate(fileno(histfh), 0) == -1) {
+		bi_errorf("failed to rewrite history file - %s",
+		    strerror(errno));
+	}
+	for (hp = history; hp <= histptr; hp++) {
+		if (stravis(&encoded, *hp, VIS_SAFE | VIS_NL) != -1) {
+			if (fprintf(histfh, "%s\n", encoded) == -1) {
+				free(encoded);
+				return;
+			}
+			free(encoded);
+		}
+	}
+
+	line_co = histsize;
+
+	fflush(histfh);
+	fstat(fileno(histfh), &last_sb);
+}
+
+void
+hist_finish(void)
+{
+	history_close();
+}
diff --git a/io.c b/io.c
@@ -0,0 +1,461 @@
+/*	$OpenBSD: io.c,v 1.36 2018/01/16 22:52:32 jca Exp $	*/
+
+/*
+ * shell buffered IO and formatted output
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+static int initio_done;
+
+/*
+ * formatted output functions
+ */
+
+
+/* A shell error occurred (eg, syntax error, etc.) */
+void
+errorf(const char *fmt, ...)
+{
+	va_list va;
+
+	shl_stdout_ok = 0;	/* debugging: note that stdout not valid */
+	exstat = 1;
+	if (fmt != NULL && *fmt != '\0') {
+		error_prefix(true);
+		va_start(va, fmt);
+		shf_vfprintf(shl_out, fmt, va);
+		va_end(va);
+		shf_putchar('\n', shl_out);
+	}
+	shf_flush(shl_out);
+	unwind(LERROR);
+}
+
+/* like errorf(), but no unwind is done */
+void
+warningf(bool show_lineno, const char *fmt, ...)
+{
+	va_list va;
+
+	error_prefix(show_lineno);
+	va_start(va, fmt);
+	shf_vfprintf(shl_out, fmt, va);
+	va_end(va);
+	shf_putchar('\n', shl_out);
+	shf_flush(shl_out);
+}
+
+/* Used by built-in utilities to prefix shell and utility name to message
+ * (also unwinds environments for special builtins).
+ */
+void
+bi_errorf(const char *fmt, ...)
+{
+	va_list va;
+
+	shl_stdout_ok = 0;	/* debugging: note that stdout not valid */
+	exstat = 1;
+	if (fmt != NULL && *fmt != '\0') {
+		error_prefix(true);
+		/* not set when main() calls parse_args() */
+		if (builtin_argv0)
+			shf_fprintf(shl_out, "%s: ", builtin_argv0);
+		va_start(va, fmt);
+		shf_vfprintf(shl_out, fmt, va);
+		va_end(va);
+		shf_putchar('\n', shl_out);
+	}
+	shf_flush(shl_out);
+	/* POSIX special builtins and ksh special builtins cause
+	 * non-interactive shells to exit.
+	 * XXX odd use of KEEPASN; also may not want LERROR here
+	 */
+	if ((builtin_flag & SPEC_BI) ||
+	    (Flag(FPOSIX) && (builtin_flag & KEEPASN))) {
+		builtin_argv0 = NULL;
+		unwind(LERROR);
+	}
+}
+
+static void
+internal_error_vwarn(const char *fmt, va_list va)
+{
+	error_prefix(true);
+	shf_fprintf(shl_out, "internal error: ");
+	shf_vfprintf(shl_out, fmt, va);
+	shf_putchar('\n', shl_out);
+	shf_flush(shl_out);
+}
+
+/* Warn when something that shouldn't happen does */
+void
+internal_warningf(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	internal_error_vwarn(fmt, va);
+	va_end(va);
+}
+
+/* Warn and unwind when something that shouldn't happen does */
+__attribute__((__noreturn__)) void
+internal_errorf(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	internal_error_vwarn(fmt, va);
+	va_end(va);
+	unwind(LERROR);
+}
+
+/* used by error reporting functions to print "ksh: .kshrc[25]: " */
+void
+error_prefix(int fileline)
+{
+	/* Avoid foo: foo[2]: ... */
+	if (!fileline || !source || !source->file ||
+	    strcmp(source->file, kshname) != 0)
+		shf_fprintf(shl_out, "%s: ", kshname + (*kshname == '-'));
+	if (fileline && source && source->file != NULL) {
+		shf_fprintf(shl_out, "%s[%d]: ", source->file,
+		    source->errline > 0 ? source->errline : source->line);
+		source->errline = 0;
+	}
+}
+
+/* printf to shl_out (stderr) with flush */
+void
+shellf(const char *fmt, ...)
+{
+	va_list va;
+
+	if (!initio_done) /* shl_out may not be set up yet... */
+		return;
+	va_start(va, fmt);
+	shf_vfprintf(shl_out, fmt, va);
+	va_end(va);
+	shf_flush(shl_out);
+}
+
+/* printf to shl_stdout (stdout) */
+void
+shprintf(const char *fmt, ...)
+{
+	va_list va;
+
+	if (!shl_stdout_ok)
+		internal_errorf("shl_stdout not valid");
+	va_start(va, fmt);
+	shf_vfprintf(shl_stdout, fmt, va);
+	va_end(va);
+}
+
+#ifdef KSH_DEBUG
+static struct shf *kshdebug_shf;
+
+void
+kshdebug_init_(void)
+{
+	if (kshdebug_shf)
+		shf_close(kshdebug_shf);
+	kshdebug_shf = shf_open("/tmp/ksh-debug.log",
+	    O_WRONLY|O_APPEND|O_CREAT, 0600, SHF_WR|SHF_MAPHI);
+	if (kshdebug_shf) {
+		shf_fprintf(kshdebug_shf, "\nNew shell[pid %d]\n", getpid());
+		shf_flush(kshdebug_shf);
+	}
+}
+
+/* print to debugging log */
+void
+kshdebug_printf_(const char *fmt, ...)
+{
+	va_list va;
+
+	if (!kshdebug_shf)
+		return;
+	va_start(va, fmt);
+	shf_fprintf(kshdebug_shf, "[%d] ", getpid());
+	shf_vfprintf(kshdebug_shf, fmt, va);
+	va_end(va);
+	shf_flush(kshdebug_shf);
+}
+
+void
+kshdebug_dump_(const char *str, const void *mem, int nbytes)
+{
+	int i, j;
+	int nprow = 16;
+
+	if (!kshdebug_shf)
+		return;
+	shf_fprintf(kshdebug_shf, "[%d] %s:\n", getpid(), str);
+	for (i = 0; i < nbytes; i += nprow) {
+		char c = '\t';
+
+		for (j = 0; j < nprow && i + j < nbytes; j++) {
+			shf_fprintf(kshdebug_shf, "%c%02x", c,
+			    ((const unsigned char *) mem)[i + j]);
+			c = ' ';
+		}
+		shf_fprintf(kshdebug_shf, "\n");
+	}
+	shf_flush(kshdebug_shf);
+}
+#endif /* KSH_DEBUG */
+
+/* test if we can seek backwards fd (returns 0 or SHF_UNBUF) */
+int
+can_seek(int fd)
+{
+	struct stat statb;
+
+	return fstat(fd, &statb) == 0 && !S_ISREG(statb.st_mode) ?
+	    SHF_UNBUF : 0;
+}
+
+struct shf	shf_iob[3];
+
+void
+initio(void)
+{
+	shf_fdopen(1, SHF_WR, shl_stdout);	/* force buffer allocation */
+	shf_fdopen(2, SHF_WR, shl_out);
+	shf_fdopen(2, SHF_WR, shl_spare);	/* force buffer allocation */
+	initio_done = 1;
+	kshdebug_init();
+}
+
+/* A dup2() with error checking */
+int
+ksh_dup2(int ofd, int nfd, int errok)
+{
+	int ret = dup2(ofd, nfd);
+
+	if (ret < 0 && errno != EBADF && !errok)
+		errorf("too many files open in shell");
+
+	return ret;
+}
+
+/*
+ * move fd from user space (0<=fd<10) to shell space (fd>=10),
+ * set close-on-exec flag.
+ */
+int
+savefd(int fd)
+{
+	int nfd;
+
+	if (fd < FDBASE) {
+		nfd = fcntl(fd, F_DUPFD_CLOEXEC, FDBASE);
+		if (nfd < 0) {
+			if (errno == EBADF)
+				return -1;
+			else
+				errorf("too many files open in shell");
+		}
+	} else {
+		nfd = fd;
+		fcntl(nfd, F_SETFD, FD_CLOEXEC);
+	}
+	return nfd;
+}
+
+void
+restfd(int fd, int ofd)
+{
+	if (fd == 2)
+		shf_flush(&shf_iob[fd]);
+	if (ofd < 0)		/* original fd closed */
+		close(fd);
+	else if (fd != ofd) {
+		ksh_dup2(ofd, fd, true); /* XXX: what to do if this fails? */
+		close(ofd);
+	}
+}
+
+void
+openpipe(int *pv)
+{
+	int lpv[2];
+
+	if (pipe(lpv) < 0)
+		errorf("can't create pipe - try again");
+	pv[0] = savefd(lpv[0]);
+	if (pv[0] != lpv[0])
+		close(lpv[0]);
+	pv[1] = savefd(lpv[1]);
+	if (pv[1] != lpv[1])
+		close(lpv[1]);
+}
+
+void
+closepipe(int *pv)
+{
+	close(pv[0]);
+	close(pv[1]);
+}
+
+/* Called by iosetup() (deals with 2>&4, etc.), c_read, c_print to turn
+ * a string (the X in 2>&X, read -uX, print -uX) into a file descriptor.
+ */
+int
+check_fd(char *name, int mode, const char **emsgp)
+{
+	int fd, fl;
+
+	if (isdigit((unsigned char)name[0]) && !name[1]) {
+		fd = name[0] - '0';
+		if ((fl = fcntl(fd, F_GETFL)) < 0) {
+			if (emsgp)
+				*emsgp = "bad file descriptor";
+			return -1;
+		}
+		fl &= O_ACCMODE;
+		/* X_OK is a kludge to disable this check for dups (x<&1):
+		 * historical shells never did this check (XXX don't know what
+		 * posix has to say).
+		 */
+		if (!(mode & X_OK) && fl != O_RDWR &&
+		    (((mode & R_OK) && fl != O_RDONLY) ||
+		    ((mode & W_OK) && fl != O_WRONLY))) {
+			if (emsgp)
+				*emsgp = (fl == O_WRONLY) ?
+				    "fd not open for reading" :
+				    "fd not open for writing";
+			return -1;
+		}
+		return fd;
+	} else if (name[0] == 'p' && !name[1])
+		return coproc_getfd(mode, emsgp);
+	if (emsgp)
+		*emsgp = "illegal file descriptor name";
+	return -1;
+}
+
+/* Called once from main */
+void
+coproc_init(void)
+{
+	coproc.read = coproc.readw = coproc.write = -1;
+	coproc.njobs = 0;
+	coproc.id = 0;
+}
+
+/* Called by c_read() when eof is read - close fd if it is the co-process fd */
+void
+coproc_read_close(int fd)
+{
+	if (coproc.read >= 0 && fd == coproc.read) {
+		coproc_readw_close(fd);
+		close(coproc.read);
+		coproc.read = -1;
+	}
+}
+
+/* Called by c_read() and by iosetup() to close the other side of the
+ * read pipe, so reads will actually terminate.
+ */
+void
+coproc_readw_close(int fd)
+{
+	if (coproc.readw >= 0 && coproc.read >= 0 && fd == coproc.read) {
+		close(coproc.readw);
+		coproc.readw = -1;
+	}
+}
+
+/* Called by c_print when a write to a fd fails with EPIPE and by iosetup
+ * when co-process input is dup'd
+ */
+void
+coproc_write_close(int fd)
+{
+	if (coproc.write >= 0 && fd == coproc.write) {
+		close(coproc.write);
+		coproc.write = -1;
+	}
+}
+
+/* Called to check for existence of/value of the co-process file descriptor.
+ * (Used by check_fd() and by c_read/c_print to deal with -p option).
+ */
+int
+coproc_getfd(int mode, const char **emsgp)
+{
+	int fd = (mode & R_OK) ? coproc.read : coproc.write;
+
+	if (fd >= 0)
+		return fd;
+	if (emsgp)
+		*emsgp = "no coprocess";
+	return -1;
+}
+
+/* called to close file descriptors related to the coprocess (if any)
+ * Should be called with SIGCHLD blocked.
+ */
+void
+coproc_cleanup(int reuse)
+{
+	/* This to allow co-processes to share output pipe */
+	if (!reuse || coproc.readw < 0 || coproc.read < 0) {
+		if (coproc.read >= 0) {
+			close(coproc.read);
+			coproc.read = -1;
+		}
+		if (coproc.readw >= 0) {
+			close(coproc.readw);
+			coproc.readw = -1;
+		}
+	}
+	if (coproc.write >= 0) {
+		close(coproc.write);
+		coproc.write = -1;
+	}
+}
+
+
+/*
+ * temporary files
+ */
+
+struct temp *
+maketemp(Area *ap, Temp_type type, struct temp **tlist)
+{
+	struct temp *tp;
+	int len;
+	int fd;
+	char *path;
+	const char *dir;
+
+	dir = tmpdir ? tmpdir : "/tmp";
+	/* The 20 + 20 is a paranoid worst case for pid/inc */
+	len = strlen(dir) + 3 + 20 + 20 + 1;
+	tp = alloc(sizeof(struct temp) + len, ap);
+	tp->name = path = (char *) &tp[1];
+	tp->shf = NULL;
+	tp->type = type;
+	shf_snprintf(path, len, "%s/shXXXXXXXX", dir);
+	fd = mkstemp(path);
+	if (fd >= 0)
+		tp->shf = shf_fdopen(fd, SHF_WR, NULL);
+	tp->pid = procpid;
+
+	tp->next = *tlist;
+	*tlist = tp;
+	return tp;
+}
diff --git a/jobs.c b/jobs.c
@@ -0,0 +1,1575 @@
+/*	$OpenBSD: jobs.c,v 1.60 2018/03/15 16:51:29 anton Exp $	*/
+
+/*
+ * Process and job control
+ */
+
+/*
+ * Reworked/Rewritten version of Eric Gisin's/Ron Natalie's code by
+ * Larry Bouzane (larry@cs.mun.ca) and hacked again by
+ * Michael Rendell (michael@cs.mun.ca)
+ *
+ * The interface to the rest of the shell should probably be changed
+ * to allow use of vfork() when available but that would be way too much
+ * work :)
+ *
+ */
+
+#include <sys/types.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+#include "tty.h"
+
+/* Order important! */
+#define PRUNNING	0
+#define PEXITED		1
+#define PSIGNALLED	2
+#define PSTOPPED	3
+
+typedef struct proc	Proc;
+struct proc {
+	Proc	*next;		/* next process in pipeline (if any) */
+	int	state;
+	int	status;		/* wait status */
+	pid_t	pid;		/* process id */
+	char	command[48];	/* process command string */
+};
+
+/* Notify/print flag - j_print() argument */
+#define JP_NONE		0	/* don't print anything */
+#define JP_SHORT	1	/* print signals processes were killed by */
+#define JP_MEDIUM	2	/* print [job-num] -/+ command */
+#define JP_LONG		3	/* print [job-num] -/+ pid command */
+#define JP_PGRP		4	/* print pgrp */
+
+/* put_job() flags */
+#define PJ_ON_FRONT	0	/* at very front */
+#define PJ_PAST_STOPPED	1	/* just past any stopped jobs */
+
+/* Job.flags values */
+#define JF_STARTED	0x001	/* set when all processes in job are started */
+#define JF_WAITING	0x002	/* set if j_waitj() is waiting on job */
+#define JF_W_ASYNCNOTIFY 0x004	/* set if waiting and async notification ok */
+#define JF_XXCOM	0x008	/* set for `command` jobs */
+#define JF_FG		0x010	/* running in foreground (also has tty pgrp) */
+#define JF_SAVEDTTY	0x020	/* j->ttystate is valid */
+#define JF_CHANGED	0x040	/* process has changed state */
+#define JF_KNOWN	0x080	/* $! referenced */
+#define JF_ZOMBIE	0x100	/* known, unwaited process */
+#define JF_REMOVE	0x200	/* flagged for removal (j_jobs()/j_notify()) */
+#define JF_USETTYMODE	0x400	/* tty mode saved if process exits normally */
+#define JF_SAVEDTTYPGRP	0x800	/* j->saved_ttypgrp is valid */
+
+typedef struct job Job;
+struct job {
+	Job	*next;		/* next job in list */
+	int	job;		/* job number: %n */
+	int	flags;		/* see JF_* */
+	int	state;		/* job state */
+	int	status;		/* exit status of last process */
+	pid_t	pgrp;		/* process group of job */
+	pid_t	ppid;		/* pid of process that forked job */
+	int	age;		/* number of jobs started */
+	struct timeval systime;	/* system time used by job */
+	struct timeval usrtime;	/* user time used by job */
+	Proc	*proc_list;	/* process list */
+	Proc	*last_proc;	/* last process in list */
+	Coproc_id coproc_id;	/* 0 or id of coprocess output pipe */
+	struct termios ttystate;/* saved tty state for stopped jobs */
+	pid_t	saved_ttypgrp;	/* saved tty process group for stopped jobs */
+};
+
+/* Flags for j_waitj() */
+#define JW_NONE		0x00
+#define JW_INTERRUPT	0x01	/* ^C will stop the wait */
+#define JW_ASYNCNOTIFY	0x02	/* asynchronous notification during wait ok */
+#define JW_STOPPEDWAIT	0x04	/* wait even if job stopped */
+
+/* Error codes for j_lookup() */
+#define JL_OK		0
+#define JL_NOSUCH	1	/* no such job */
+#define JL_AMBIG	2	/* %foo or %?foo is ambiguous */
+#define JL_INVALID	3	/* non-pid, non-% job id */
+
+static const char	*const lookup_msgs[] = {
+	null,
+	"no such job",
+	"ambiguous",
+	"argument must be %job or process id",
+	NULL
+};
+
+struct timeval	j_systime, j_usrtime;	/* user and system time of last j_waitjed job */
+
+static Job		*job_list;	/* job list */
+static Job		*last_job;
+static Job		*async_job;
+static pid_t		async_pid;
+
+static int		nzombie;	/* # of zombies owned by this process */
+int			njobs;		/* # of jobs started */
+static int		child_max;	/* CHILD_MAX */
+
+
+/* held_sigchld is set if sigchld occurs before a job is completely started */
+static volatile sig_atomic_t held_sigchld;
+
+static struct shf	*shl_j;
+static int		ttypgrp_ok;	/* set if can use tty pgrps */
+static pid_t		restore_ttypgrp = -1;
+static pid_t		our_pgrp;
+static int const	tt_sigs[] = { SIGTSTP, SIGTTIN, SIGTTOU };
+
+static void		j_set_async(Job *);
+static void		j_startjob(Job *);
+static int		j_waitj(Job *, int, const char *);
+static void		j_sigchld(int);
+static void		j_print(Job *, int, struct shf *);
+static Job		*j_lookup(const char *, int *);
+static Job		*new_job(void);
+static Proc		*new_proc(void);
+static void		check_job(Job *);
+static void		put_job(Job *, int);
+static void		remove_job(Job *, const char *);
+static int		kill_job(Job *, int);
+
+/* initialize job control */
+void
+j_init(int mflagset)
+{
+#ifdef CHILD_MAX
+	child_max = CHILD_MAX; /* so syscon() isn't always being called */
+#else
+	child_max = sysconf(_SC_CHILD_MAX);
+#endif
+
+	sigemptyset(&sm_default);
+	sigprocmask(SIG_SETMASK, &sm_default, NULL);
+
+	sigemptyset(&sm_sigchld);
+	sigaddset(&sm_sigchld, SIGCHLD);
+
+	setsig(&sigtraps[SIGCHLD], j_sigchld,
+	    SS_RESTORE_ORIG|SS_FORCE|SS_SHTRAP);
+
+	if (!mflagset && Flag(FTALKING))
+		Flag(FMONITOR) = 1;
+
+	/* shl_j is used to do asynchronous notification (used in
+	 * an interrupt handler, so need a distinct shf)
+	 */
+	shl_j = shf_fdopen(2, SHF_WR, NULL);
+
+	if (Flag(FMONITOR) || Flag(FTALKING)) {
+		int i;
+
+		/* the TF_SHELL_USES test is a kludge that lets us know if
+		 * if the signals have been changed by the shell.
+		 */
+		for (i = NELEM(tt_sigs); --i >= 0; ) {
+			sigtraps[tt_sigs[i]].flags |= TF_SHELL_USES;
+			/* j_change() sets this to SS_RESTORE_DFL if FMONITOR */
+			setsig(&sigtraps[tt_sigs[i]], SIG_IGN,
+			    SS_RESTORE_IGN|SS_FORCE);
+		}
+	}
+
+	/* j_change() calls tty_init() */
+	if (Flag(FMONITOR))
+		j_change();
+	else if (Flag(FTALKING))
+		tty_init(true);
+}
+
+/* job cleanup before shell exit */
+void
+j_exit(void)
+{
+	/* kill stopped, and possibly running, jobs */
+	Job	*j;
+	int	killed = 0;
+
+	for (j = job_list; j != NULL; j = j->next) {
+		if (j->ppid == procpid &&
+		    (j->state == PSTOPPED ||
+		    (j->state == PRUNNING &&
+		    ((j->flags & JF_FG) ||
+		    (Flag(FLOGIN) && !Flag(FNOHUP) && procpid == kshpid))))) {
+			killed = 1;
+			if (j->pgrp == 0)
+				kill_job(j, SIGHUP);
+			else
+				killpg(j->pgrp, SIGHUP);
+			if (j->state == PSTOPPED) {
+				if (j->pgrp == 0)
+					kill_job(j, SIGCONT);
+				else
+					killpg(j->pgrp, SIGCONT);
+			}
+		}
+	}
+	if (killed)
+		sleep(1);
+	j_notify();
+
+	if (kshpid == procpid && restore_ttypgrp >= 0) {
+		/* Need to restore the tty pgrp to what it was when the
+		 * shell started up, so that the process that started us
+		 * will be able to access the tty when we are done.
+		 * Also need to restore our process group in case we are
+		 * about to do an exec so that both our parent and the
+		 * process we are to become will be able to access the tty.
+		 */
+		tcsetpgrp(tty_fd, restore_ttypgrp);
+		setpgid(0, restore_ttypgrp);
+	}
+	if (Flag(FMONITOR)) {
+		Flag(FMONITOR) = 0;
+		j_change();
+	}
+}
+
+/* turn job control on or off according to Flag(FMONITOR) */
+void
+j_change(void)
+{
+	int i;
+
+	if (Flag(FMONITOR)) {
+		int use_tty;
+
+		if (Flag(FTALKING)) {
+			/* Don't call tcgetattr() 'til we own the tty process group */
+			use_tty = 1;
+			tty_init(false);
+		} else
+			use_tty = 0;
+
+		/* no controlling tty, no SIGT* */
+		ttypgrp_ok = use_tty && tty_fd >= 0 && tty_devtty;
+
+		if (ttypgrp_ok && (our_pgrp = getpgrp()) < 0) {
+			warningf(false, "%s: getpgrp() failed: %s",
+			    __func__, strerror(errno));
+			ttypgrp_ok = 0;
+		}
+		if (ttypgrp_ok) {
+			setsig(&sigtraps[SIGTTIN], SIG_DFL,
+			    SS_RESTORE_ORIG|SS_FORCE);
+			/* wait to be given tty (POSIX.1, B.2, job control) */
+			while (1) {
+				pid_t ttypgrp;
+
+				if ((ttypgrp = tcgetpgrp(tty_fd)) < 0) {
+					warningf(false,
+					    "%s: tcgetpgrp() failed: %s",
+					    __func__, strerror(errno));
+					ttypgrp_ok = 0;
+					break;
+				}
+				if (ttypgrp == our_pgrp)
+					break;
+				kill(0, SIGTTIN);
+			}
+		}
+		for (i = NELEM(tt_sigs); --i >= 0; )
+			setsig(&sigtraps[tt_sigs[i]], SIG_IGN,
+			    SS_RESTORE_DFL|SS_FORCE);
+		if (ttypgrp_ok && our_pgrp != kshpid) {
+			if (setpgid(0, kshpid) < 0) {
+				warningf(false, "%s: setpgid() failed: %s",
+				    __func__, strerror(errno));
+				ttypgrp_ok = 0;
+			} else {
+				if (tcsetpgrp(tty_fd, kshpid) < 0) {
+					warningf(false,
+					    "%s: tcsetpgrp() failed: %s",
+					    __func__, strerror(errno));
+					ttypgrp_ok = 0;
+				} else
+					restore_ttypgrp = our_pgrp;
+				our_pgrp = kshpid;
+			}
+		}
+		if (use_tty) {
+			if (!ttypgrp_ok)
+				warningf(false,
+				    "warning: won't have full job control");
+		}
+		if (tty_fd >= 0)
+			tcgetattr(tty_fd, &tty_state);
+	} else {
+		ttypgrp_ok = 0;
+		if (Flag(FTALKING))
+			for (i = NELEM(tt_sigs); --i >= 0; )
+				setsig(&sigtraps[tt_sigs[i]], SIG_IGN,
+				    SS_RESTORE_IGN|SS_FORCE);
+		else
+			for (i = NELEM(tt_sigs); --i >= 0; ) {
+				if (sigtraps[tt_sigs[i]].flags &
+				    (TF_ORIG_IGN | TF_ORIG_DFL))
+					setsig(&sigtraps[tt_sigs[i]],
+					    (sigtraps[tt_sigs[i]].flags & TF_ORIG_IGN) ?
+					    SIG_IGN : SIG_DFL,
+					    SS_RESTORE_ORIG|SS_FORCE);
+			}
+		if (!Flag(FTALKING))
+			tty_close();
+	}
+}
+
+/* execute tree in child subprocess */
+int
+exchild(struct op *t, int flags, volatile int *xerrok,
+    int close_fd)	/* used if XPCLOSE or XCCLOSE */
+{
+	static Proc	*last_proc;	/* for pipelines */
+
+	int		i;
+	sigset_t	omask;
+	Proc		*p;
+	Job		*j;
+	int		rv = 0;
+	int		forksleep;
+	int		ischild;
+
+	if (flags & XEXEC)
+		/* Clear XFORK|XPCLOSE|XCCLOSE|XCOPROC|XPIPEO|XPIPEI|XXCOM|XBGND
+		 * (also done in another execute() below)
+		 */
+		return execute(t, flags & (XEXEC | XERROK), xerrok);
+
+	/* no SIGCHLD's while messing with job and process lists */
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+
+	p = new_proc();
+	p->next = NULL;
+	p->state = PRUNNING;
+	p->status = 0;
+	p->pid = 0;
+
+	/* link process into jobs list */
+	if (flags&XPIPEI) {	/* continuing with a pipe */
+		if (!last_job)
+			internal_errorf("%s: XPIPEI and no last_job - pid %d",
+			    __func__, (int) procpid);
+		j = last_job;
+		last_proc->next = p;
+		last_proc = p;
+	} else {
+		j = new_job(); /* fills in j->job */
+		/* we don't consider XXCOM's foreground since they don't get
+		 * tty process group and we don't save or restore tty modes.
+		 */
+		j->flags = (flags & XXCOM) ? JF_XXCOM :
+		    ((flags & XBGND) ? 0 : (JF_FG|JF_USETTYMODE));
+		timerclear(&j->usrtime);
+		timerclear(&j->systime);
+		j->state = PRUNNING;
+		j->pgrp = 0;
+		j->ppid = procpid;
+		j->age = ++njobs;
+		j->proc_list = p;
+		j->coproc_id = 0;
+		last_job = j;
+		last_proc = p;
+		put_job(j, PJ_PAST_STOPPED);
+	}
+
+	snptreef(p->command, sizeof(p->command), "%T", t);
+
+	/* create child process */
+	forksleep = 1;
+	while ((i = fork()) < 0 && errno == EAGAIN && forksleep < 32) {
+		if (intrsig)	 /* allow user to ^C out... */
+			break;
+		sleep(forksleep);
+		forksleep <<= 1;
+	}
+	if (i < 0) {
+		kill_job(j, SIGKILL);
+		remove_job(j, "fork failed");
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		errorf("cannot fork - try again");
+	}
+	ischild = i == 0;
+	if (ischild)
+		p->pid = procpid = getpid();
+	else
+		p->pid = i;
+
+	/* job control set up */
+	if (Flag(FMONITOR) && !(flags&XXCOM)) {
+		int	dotty = 0;
+		if (j->pgrp == 0) {	/* First process */
+			j->pgrp = p->pid;
+			dotty = 1;
+		}
+
+		/* set pgrp in both parent and child to deal with race
+		 * condition
+		 */
+		setpgid(p->pid, j->pgrp);
+		/* YYY: should this be
+		   if (ttypgrp_ok && ischild && !(flags&XBGND))
+			tcsetpgrp(tty_fd, j->pgrp);
+		   instead? (see also YYY below)
+		 */
+		if (ttypgrp_ok && dotty && !(flags & XBGND))
+			tcsetpgrp(tty_fd, j->pgrp);
+	}
+
+	/* used to close pipe input fd */
+	if (close_fd >= 0 && (((flags & XPCLOSE) && !ischild) ||
+	    ((flags & XCCLOSE) && ischild)))
+		close(close_fd);
+	if (ischild) {		/* child */
+		/* Do this before restoring signal */
+		if (flags & XCOPROC)
+			coproc_cleanup(false);
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		cleanup_parents_env();
+		/* If FMONITOR or FTALKING is set, these signals are ignored,
+		 * if neither FMONITOR nor FTALKING are set, the signals have
+		 * their inherited values.
+		 */
+		if (Flag(FMONITOR) && !(flags & XXCOM)) {
+			for (i = NELEM(tt_sigs); --i >= 0; )
+				setsig(&sigtraps[tt_sigs[i]], SIG_DFL,
+				    SS_RESTORE_DFL|SS_FORCE);
+		}
+		if (Flag(FBGNICE) && (flags & XBGND))
+			nice(4);
+		if ((flags & XBGND) && !Flag(FMONITOR)) {
+			setsig(&sigtraps[SIGINT], SIG_IGN,
+			    SS_RESTORE_IGN|SS_FORCE);
+			setsig(&sigtraps[SIGQUIT], SIG_IGN,
+			    SS_RESTORE_IGN|SS_FORCE);
+			if (!(flags & (XPIPEI | XCOPROC))) {
+				int fd = open("/dev/null", O_RDONLY);
+				if (fd != 0) {
+					(void) ksh_dup2(fd, 0, true);
+					close(fd);
+				}
+			}
+		}
+		remove_job(j, "child");	/* in case of `jobs` command */
+		nzombie = 0;
+		ttypgrp_ok = 0;
+		Flag(FMONITOR) = 0;
+		Flag(FTALKING) = 0;
+		tty_close();
+		cleartraps();
+		execute(t, (flags & XERROK) | XEXEC, NULL); /* no return */
+		internal_warningf("%s: execute() returned", __func__);
+		unwind(LLEAVE);
+		/* NOTREACHED */
+	}
+
+	/* shell (parent) stuff */
+	/* Ensure next child gets a (slightly) different $RANDOM sequence */
+	change_random();
+	if (!(flags & XPIPEO)) {	/* last process in a job */
+		/* YYY: Is this needed? (see also YYY above)
+		   if (Flag(FMONITOR) && !(flags&(XXCOM|XBGND)))
+			tcsetpgrp(tty_fd, j->pgrp);
+		*/
+		j_startjob(j);
+		if (flags & XCOPROC) {
+			j->coproc_id = coproc.id;
+			coproc.njobs++; /* n jobs using co-process output */
+			coproc.job = (void *) j; /* j using co-process input */
+		}
+		if (flags & XBGND) {
+			j_set_async(j);
+			if (Flag(FTALKING)) {
+				shf_fprintf(shl_out, "[%d]", j->job);
+				for (p = j->proc_list; p; p = p->next)
+					shf_fprintf(shl_out, " %d", p->pid);
+				shf_putchar('\n', shl_out);
+				shf_flush(shl_out);
+			}
+		} else
+			rv = j_waitj(j, JW_NONE, "jw:last proc");
+	}
+
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+
+	return rv;
+}
+
+/* start the last job: only used for `command` jobs */
+void
+startlast(void)
+{
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+
+	if (last_job) { /* no need to report error - waitlast() will do it */
+		/* ensure it isn't removed by check_job() */
+		last_job->flags |= JF_WAITING;
+		j_startjob(last_job);
+	}
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+}
+
+/* wait for last job: only used for `command` jobs */
+int
+waitlast(void)
+{
+	int	rv;
+	Job	*j;
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+
+	j = last_job;
+	if (!j || !(j->flags & JF_STARTED)) {
+		if (!j)
+			warningf(true, "%s: no last job", __func__);
+		else
+			internal_warningf("%s: not started", __func__);
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		return 125; /* not so arbitrary, non-zero value */
+	}
+
+	rv = j_waitj(j, JW_NONE, "jw:waitlast");
+
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+
+	return rv;
+}
+
+/* wait for child, interruptable. */
+int
+waitfor(const char *cp, int *sigp)
+{
+	int	rv;
+	Job	*j;
+	int	ecode;
+	int	flags = JW_INTERRUPT|JW_ASYNCNOTIFY;
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+
+	*sigp = 0;
+
+	if (cp == NULL) {
+		/* wait for an unspecified job - always returns 0, so
+		 * don't have to worry about exited/signaled jobs
+		 */
+		for (j = job_list; j; j = j->next)
+			/* at&t ksh will wait for stopped jobs - we don't */
+			if (j->ppid == procpid && j->state == PRUNNING)
+				break;
+		if (!j) {
+			sigprocmask(SIG_SETMASK, &omask, NULL);
+			return -1;
+		}
+	} else if ((j = j_lookup(cp, &ecode))) {
+		/* don't report normal job completion */
+		flags &= ~JW_ASYNCNOTIFY;
+		if (j->ppid != procpid) {
+			sigprocmask(SIG_SETMASK, &omask, NULL);
+			return -1;
+		}
+	} else {
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		if (ecode != JL_NOSUCH)
+			bi_errorf("%s: %s", cp, lookup_msgs[ecode]);
+		return -1;
+	}
+
+	/* at&t ksh will wait for stopped jobs - we don't */
+	rv = j_waitj(j, flags, "jw:waitfor");
+
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+
+	if (rv < 0) /* we were interrupted */
+		*sigp = 128 + -rv;
+
+	return rv;
+}
+
+/* kill (built-in) a job */
+int
+j_kill(const char *cp, int sig)
+{
+	Job	*j;
+	int	rv = 0;
+	int	ecode;
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+
+	if ((j = j_lookup(cp, &ecode)) == NULL) {
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		bi_errorf("%s: %s", cp, lookup_msgs[ecode]);
+		return 1;
+	}
+
+	if (j->pgrp == 0) {	/* started when !Flag(FMONITOR) */
+		if (kill_job(j, sig) < 0) {
+			bi_errorf("%s: %s", cp, strerror(errno));
+			rv = 1;
+		}
+	} else {
+		if (j->state == PSTOPPED && (sig == SIGTERM || sig == SIGHUP))
+			(void) killpg(j->pgrp, SIGCONT);
+		if (killpg(j->pgrp, sig) < 0) {
+			bi_errorf("%s: %s", cp, strerror(errno));
+			rv = 1;
+		}
+	}
+
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+
+	return rv;
+}
+
+/* fg and bg built-ins: called only if Flag(FMONITOR) set */
+int
+j_resume(const char *cp, int bg)
+{
+	Job	*j;
+	Proc	*p;
+	int	ecode;
+	int	running;
+	int	rv = 0;
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+
+	if ((j = j_lookup(cp, &ecode)) == NULL) {
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		bi_errorf("%s: %s", cp, lookup_msgs[ecode]);
+		return 1;
+	}
+
+	if (j->pgrp == 0) {
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		bi_errorf("job not job-controlled");
+		return 1;
+	}
+
+	if (bg)
+		shprintf("[%d] ", j->job);
+
+	running = 0;
+	for (p = j->proc_list; p != NULL; p = p->next) {
+		if (p->state == PSTOPPED) {
+			p->state = PRUNNING;
+			p->status = 0;
+			running = 1;
+		}
+		shprintf("%s%s", p->command, p->next ? "| " : "");
+	}
+	shprintf("\n");
+	shf_flush(shl_stdout);
+	if (running)
+		j->state = PRUNNING;
+
+	put_job(j, PJ_PAST_STOPPED);
+	if (bg)
+		j_set_async(j);
+	else {
+		/* attach tty to job */
+		if (j->state == PRUNNING) {
+			if (ttypgrp_ok && (j->flags & JF_SAVEDTTY))
+				tcsetattr(tty_fd, TCSADRAIN, &j->ttystate);
+			/* See comment in j_waitj regarding saved_ttypgrp. */
+			if (ttypgrp_ok &&
+			    tcsetpgrp(tty_fd, (j->flags & JF_SAVEDTTYPGRP) ?
+			    j->saved_ttypgrp : j->pgrp) < 0) {
+				if (j->flags & JF_SAVEDTTY)
+					tcsetattr(tty_fd, TCSADRAIN, &tty_state);
+				sigprocmask(SIG_SETMASK, &omask, NULL);
+				bi_errorf("1st tcsetpgrp(%d, %d) failed: %s",
+				    tty_fd,
+				    (int) ((j->flags & JF_SAVEDTTYPGRP) ?
+				    j->saved_ttypgrp : j->pgrp),
+				    strerror(errno));
+				return 1;
+			}
+		}
+		j->flags |= JF_FG;
+		j->flags &= ~JF_KNOWN;
+		if (j == async_job)
+			async_job = NULL;
+	}
+
+	if (j->state == PRUNNING && killpg(j->pgrp, SIGCONT) < 0) {
+		int	err = errno;
+
+		if (!bg) {
+			j->flags &= ~JF_FG;
+			if (ttypgrp_ok && (j->flags & JF_SAVEDTTY))
+				tcsetattr(tty_fd, TCSADRAIN, &tty_state);
+			if (ttypgrp_ok && tcsetpgrp(tty_fd, our_pgrp) < 0) {
+				warningf(true,
+				    "fg: 2nd tcsetpgrp(%d, %d) failed: %s",
+				    tty_fd, (int) our_pgrp,
+				    strerror(errno));
+			}
+		}
+		sigprocmask(SIG_SETMASK, &omask, NULL);
+		bi_errorf("cannot continue job %s: %s",
+		    cp, strerror(err));
+		return 1;
+	}
+	if (!bg) {
+		if (ttypgrp_ok) {
+			j->flags &= ~(JF_SAVEDTTY | JF_SAVEDTTYPGRP);
+		}
+		rv = j_waitj(j, JW_NONE, "jw:resume");
+	}
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+	return rv;
+}
+
+/* are there any running or stopped jobs ? */
+int
+j_stopped_running(void)
+{
+	Job	*j;
+	int	which = 0;
+
+	for (j = job_list; j != NULL; j = j->next) {
+		if (j->ppid == procpid && j->state == PSTOPPED)
+			which |= 1;
+		if (Flag(FLOGIN) && !Flag(FNOHUP) && procpid == kshpid &&
+		    j->ppid == procpid && j->state == PRUNNING)
+			which |= 2;
+	}
+	if (which) {
+		shellf("You have %s%s%s jobs\n",
+		    which & 1 ? "stopped" : "",
+		    which == 3 ? " and " : "",
+		    which & 2 ? "running" : "");
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+j_njobs(void)
+{
+	Job *j;
+	int nj = 0;
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+	for (j = job_list; j; j = j->next)
+		nj++;
+
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+	return nj;
+}
+
+
+/* list jobs for jobs built-in */
+int
+j_jobs(const char *cp, int slp,
+    int nflag)		/* 0: short, 1: long, 2: pgrp */
+{
+	Job	*j, *tmp;
+	int	how;
+	int	zflag = 0;
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+
+	if (nflag < 0) { /* kludge: print zombies */
+		nflag = 0;
+		zflag = 1;
+	}
+	if (cp) {
+		int	ecode;
+
+		if ((j = j_lookup(cp, &ecode)) == NULL) {
+			sigprocmask(SIG_SETMASK, &omask, NULL);
+			bi_errorf("%s: %s", cp, lookup_msgs[ecode]);
+			return 1;
+		}
+	} else
+		j = job_list;
+	how = slp == 0 ? JP_MEDIUM : (slp == 1 ? JP_LONG : JP_PGRP);
+	for (; j; j = j->next) {
+		if ((!(j->flags & JF_ZOMBIE) || zflag) &&
+		    (!nflag || (j->flags & JF_CHANGED))) {
+			j_print(j, how, shl_stdout);
+			if (j->state == PEXITED || j->state == PSIGNALLED)
+				j->flags |= JF_REMOVE;
+		}
+		if (cp)
+			break;
+	}
+	/* Remove jobs after printing so there won't be multiple + or - jobs */
+	for (j = job_list; j; j = tmp) {
+		tmp = j->next;
+		if (j->flags & JF_REMOVE)
+			remove_job(j, "jobs");
+	}
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+	return 0;
+}
+
+/* list jobs for top-level notification */
+void
+j_notify(void)
+{
+	Job	*j, *tmp;
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+	for (j = job_list; j; j = j->next) {
+		if (Flag(FMONITOR) && (j->flags & JF_CHANGED))
+			j_print(j, JP_MEDIUM, shl_out);
+		/* Remove job after doing reports so there aren't
+		 * multiple +/- jobs.
+		 */
+		if (j->state == PEXITED || j->state == PSIGNALLED)
+			j->flags |= JF_REMOVE;
+	}
+	for (j = job_list; j; j = tmp) {
+		tmp = j->next;
+		if (j->flags & JF_REMOVE)
+			remove_job(j, "notify");
+	}
+	shf_flush(shl_out);
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+}
+
+/* Return pid of last process in last asynchronous job */
+pid_t
+j_async(void)
+{
+	sigset_t omask;
+
+	sigprocmask(SIG_BLOCK, &sm_sigchld, &omask);
+
+	if (async_job)
+		async_job->flags |= JF_KNOWN;
+
+	sigprocmask(SIG_SETMASK, &omask, NULL);
+
+	return async_pid;
+}
+
+/* Make j the last async process
+ *
+ * Expects sigchld to be blocked.
+ */
+static void
+j_set_async(Job *j)
+{
+	Job	*jl, *oldest;
+
+	if (async_job && (async_job->flags & (JF_KNOWN|JF_ZOMBIE)) == JF_ZOMBIE)
+		remove_job(async_job, "async");
+	if (!(j->flags & JF_STARTED)) {
+		internal_warningf("%s: job not started", __func__);
+		return;
+	}
+	async_job = j;
+	async_pid = j->last_proc->pid;
+	while (nzombie > child_max) {
+		oldest = NULL;
+		for (jl = job_list; jl; jl = jl->next)
+			if (jl != async_job && (jl->flags & JF_ZOMBIE) &&
+			    (!oldest || jl->age < oldest->age))
+				oldest = jl;
+		if (!oldest) {
+			/* XXX debugging */
+			if (!(async_job->flags & JF_ZOMBIE) || nzombie != 1) {
+				internal_warningf("%s: bad nzombie (%d)",
+				    __func__, nzombie);
+				nzombie = 0;
+			}
+			break;
+		}
+		remove_job(oldest, "zombie");
+	}
+}
+
+/* Start a job: set STARTED, check for held signals and set j->last_proc
+ *
+ * Expects sigchld to be blocked.
+ */
+static void
+j_startjob(Job *j)
+{
+	Proc	*p;
+
+	j->flags |= JF_STARTED;
+	for (p = j->proc_list; p->next; p = p->next)
+		;
+	j->last_proc = p;
+
+	if (held_sigchld) {
+		held_sigchld = 0;
+		/* Don't call j_sigchld() as it may remove job... */
+		kill(procpid, SIGCHLD);
+	}
+}
+
+/*
+ * wait for job to complete or change state
+ *
+ * Expects sigchld to be blocked.
+ */
+static int
+j_waitj(Job *j,
+    int flags,			/* see JW_* */
+    const char *where)
+{
+	int	rv;
+
+	/*
+	 * No auto-notify on the job we are waiting on.
+	 */
+	j->flags |= JF_WAITING;
+	if (flags & JW_ASYNCNOTIFY)
+		j->flags |= JF_W_ASYNCNOTIFY;
+
+	if (!Flag(FMONITOR))
+		flags |= JW_STOPPEDWAIT;
+
+	while ((volatile int) j->state == PRUNNING ||
+	    ((flags & JW_STOPPEDWAIT) && (volatile int) j->state == PSTOPPED)) {
+		sigsuspend(&sm_default);
+		if (fatal_trap) {
+			int oldf = j->flags & (JF_WAITING|JF_W_ASYNCNOTIFY);
+			j->flags &= ~(JF_WAITING|JF_W_ASYNCNOTIFY);
+			runtraps(TF_FATAL);
+			j->flags |= oldf; /* not reached... */
+		}
+		if ((flags & JW_INTERRUPT) && (rv = trap_pending())) {
+			j->flags &= ~(JF_WAITING|JF_W_ASYNCNOTIFY);
+			return -rv;
+		}
+	}
+	j->flags &= ~(JF_WAITING|JF_W_ASYNCNOTIFY);
+
+	if (j->flags & JF_FG) {
+		int	status;
+
+		j->flags &= ~JF_FG;
+		if (Flag(FMONITOR) && ttypgrp_ok && j->pgrp) {
+			/*
+			 * Save the tty's current pgrp so it can be restored
+			 * when the job is foregrounded.  This is to
+			 * deal with things like the GNU su which does
+			 * a fork/exec instead of an exec (the fork means
+			 * the execed shell gets a different pid from its
+			 * pgrp, so naturally it sets its pgrp and gets hosed
+			 * when it gets foregrounded by the parent shell, which
+			 * has restored the tty's pgrp to that of the su
+			 * process).
+			 */
+			if (j->state == PSTOPPED &&
+			    (j->saved_ttypgrp = tcgetpgrp(tty_fd)) >= 0)
+				j->flags |= JF_SAVEDTTYPGRP;
+			if (tcsetpgrp(tty_fd, our_pgrp) < 0) {
+				warningf(true,
+				    "%s: tcsetpgrp(%d, %d) failed: %s",
+				    __func__, tty_fd, (int)our_pgrp,
+					strerror(errno));
+			}
+			if (j->state == PSTOPPED) {
+				j->flags |= JF_SAVEDTTY;
+				tcgetattr(tty_fd, &j->ttystate);
+			}
+		}
+		if (tty_fd >= 0) {
+			/* Only restore tty settings if job was originally
+			 * started in the foreground.  Problems can be
+			 * caused by things like `more foobar &' which will
+			 * typically get and save the shell's vi/emacs tty
+			 * settings before setting up the tty for itself;
+			 * when more exits, it restores the `original'
+			 * settings, and things go down hill from there...
+			 */
+			if (j->state == PEXITED && j->status == 0 &&
+			    (j->flags & JF_USETTYMODE)) {
+				tcgetattr(tty_fd, &tty_state);
+			} else {
+				tcsetattr(tty_fd, TCSADRAIN, &tty_state);
+				/* Don't use tty mode if job is stopped and
+				 * later restarted and exits.  Consider
+				 * the sequence:
+				 *	vi foo (stopped)
+				 *	...
+				 *	stty something
+				 *	...
+				 *	fg (vi; ZZ)
+				 * mode should be that of the stty, not what
+				 * was before the vi started.
+				 */
+				if (j->state == PSTOPPED)
+					j->flags &= ~JF_USETTYMODE;
+			}
+		}
+		/* If it looks like user hit ^C to kill a job, pretend we got
+		 * one too to break out of for loops, etc.  (at&t ksh does this
+		 * even when not monitoring, but this doesn't make sense since
+		 * a tty generated ^C goes to the whole process group)
+		 */
+		status = j->last_proc->status;
+		if (Flag(FMONITOR) && j->state == PSIGNALLED &&
+		    WIFSIGNALED(status) &&
+		    (sigtraps[WTERMSIG(status)].flags & TF_TTY_INTR))
+			trapsig(WTERMSIG(status));
+	}
+
+	j_usrtime = j->usrtime;
+	j_systime = j->systime;
+	rv = j->status;
+
+	if (!(flags & JW_ASYNCNOTIFY) &&
+	    (!Flag(FMONITOR) || j->state != PSTOPPED)) {
+		j_print(j, JP_SHORT, shl_out);
+		shf_flush(shl_out);
+	}
+	if (j->state != PSTOPPED &&
+	    (!Flag(FMONITOR) || !(flags & JW_ASYNCNOTIFY)))
+		remove_job(j, where);
+
+	return rv;
+}
+
+/* SIGCHLD handler to reap children and update job states
+ *
+ * Expects sigchld to be blocked.
+ */
+static void
+j_sigchld(int sig)
+{
+	int		errno_ = errno;
+	Job		*j;
+	Proc		*p = NULL;
+	int		pid;
+	int		status;
+	struct rusage	ru0, ru1;
+
+	/* Don't wait for any processes if a job is partially started.
+	 * This is so we don't do away with the process group leader
+	 * before all the processes in a pipe line are started (so the
+	 * setpgid() won't fail)
+	 */
+	for (j = job_list; j; j = j->next)
+		if (j->ppid == procpid && !(j->flags & JF_STARTED)) {
+			held_sigchld = 1;
+			goto finished;
+		}
+
+	getrusage(RUSAGE_CHILDREN, &ru0);
+	do {
+		pid = waitpid(-1, &status, (WNOHANG|WUNTRACED));
+
+		if (pid <= 0)	/* return if would block (0) ... */
+			break;	/* ... or no children or interrupted (-1) */
+
+		getrusage(RUSAGE_CHILDREN, &ru1);
+
+		/* find job and process structures for this pid */
+		for (j = job_list; j != NULL; j = j->next)
+			for (p = j->proc_list; p != NULL; p = p->next)
+				if (p->pid == pid)
+					goto found;
+found:
+		if (j == NULL) {
+			/* Can occur if process has kids, then execs shell
+			warningf(true, "bad process waited for (pid = %d)",
+				pid);
+			 */
+			ru0 = ru1;
+			continue;
+		}
+
+		timeradd(&j->usrtime, &ru1.ru_utime, &j->usrtime);
+		timersub(&j->usrtime, &ru0.ru_utime, &j->usrtime);
+		timeradd(&j->systime, &ru1.ru_stime, &j->systime);
+		timersub(&j->systime, &ru0.ru_stime, &j->systime);
+		ru0 = ru1;
+		p->status = status;
+		if (WIFSTOPPED(status))
+			p->state = PSTOPPED;
+		else if (WIFSIGNALED(status))
+			p->state = PSIGNALLED;
+		else
+			p->state = PEXITED;
+
+		check_job(j);	/* check to see if entire job is done */
+	} while (1);
+
+finished:
+	errno = errno_;
+}
+
+/*
+ * Called only when a process in j has exited/stopped (ie, called only
+ * from j_sigchld()).  If no processes are running, the job status
+ * and state are updated, asynchronous job notification is done and,
+ * if unneeded, the job is removed.
+ *
+ * Expects sigchld to be blocked.
+ */
+static void
+check_job(Job *j)
+{
+	int	jstate;
+	Proc	*p;
+
+	/* XXX debugging (nasty - interrupt routine using shl_out) */
+	if (!(j->flags & JF_STARTED)) {
+		internal_warningf("%s: job started (flags 0x%x)",
+		    __func__, j->flags);
+		return;
+	}
+
+	jstate = PRUNNING;
+	for (p=j->proc_list; p != NULL; p = p->next) {
+		if (p->state == PRUNNING)
+			return;	/* some processes still running */
+		if (p->state > jstate)
+			jstate = p->state;
+	}
+	j->state = jstate;
+
+	switch (j->last_proc->state) {
+	case PEXITED:
+		j->status = WEXITSTATUS(j->last_proc->status);
+		break;
+	case PSIGNALLED:
+		j->status = 128 + WTERMSIG(j->last_proc->status);
+		break;
+	default:
+		j->status = 0;
+		break;
+	}
+
+	/* Note when co-process dies: can't be done in j_wait() nor
+	 * remove_job() since neither may be called for non-interactive
+	 * shells.
+	 */
+	if (j->state == PEXITED || j->state == PSIGNALLED) {
+		/* No need to keep co-process input any more
+		 * (at least, this is what ksh93d thinks)
+		 */
+		if (coproc.job == j) {
+			coproc.job = NULL;
+			/* XXX would be nice to get the closes out of here
+			 * so they aren't done in the signal handler.
+			 * Would mean a check in coproc_getfd() to
+			 * do "if job == 0 && write >= 0, close write".
+			 */
+			coproc_write_close(coproc.write);
+		}
+		/* Do we need to keep the output? */
+		if (j->coproc_id && j->coproc_id == coproc.id &&
+		    --coproc.njobs == 0)
+			coproc_readw_close(coproc.read);
+	}
+
+	j->flags |= JF_CHANGED;
+	if (Flag(FMONITOR) && !(j->flags & JF_XXCOM)) {
+		/* Only put stopped jobs at the front to avoid confusing
+		 * the user (don't want finished jobs effecting %+ or %-)
+		 */
+		if (j->state == PSTOPPED)
+			put_job(j, PJ_ON_FRONT);
+		if (Flag(FNOTIFY) &&
+		    (j->flags & (JF_WAITING|JF_W_ASYNCNOTIFY)) != JF_WAITING) {
+			/* Look for the real file descriptor 2 */
+			{
+				struct env *ep;
+				int fd = 2;
+
+				for (ep = genv; ep; ep = ep->oenv)
+					if (ep->savefd && ep->savefd[2])
+						fd = ep->savefd[2];
+				shf_reopen(fd, SHF_WR, shl_j);
+			}
+			/* Can't call j_notify() as it removes jobs.  The job
+			 * must stay in the job list as j_waitj() may be
+			 * running with this job.
+			 */
+			j_print(j, JP_MEDIUM, shl_j);
+			shf_flush(shl_j);
+			if (!(j->flags & JF_WAITING) && j->state != PSTOPPED)
+				remove_job(j, "notify");
+		}
+	}
+	if (!Flag(FMONITOR) && !(j->flags & (JF_WAITING|JF_FG)) &&
+	    j->state != PSTOPPED) {
+		if (j == async_job || (j->flags & JF_KNOWN)) {
+			j->flags |= JF_ZOMBIE;
+			j->job = -1;
+			nzombie++;
+		} else
+			remove_job(j, "checkjob");
+	}
+}
+
+/*
+ * Print job status in either short, medium or long format.
+ *
+ * Expects sigchld to be blocked.
+ */
+static void
+j_print(Job *j, int how, struct shf *shf)
+{
+	Proc	*p;
+	int	state;
+	int	status;
+	int	coredumped;
+	char	jobchar = ' ';
+	char	buf[64];
+	const char *filler;
+	int	output = 0;
+
+	if (how == JP_PGRP) {
+		/* POSIX doesn't say what to do if there is no process
+		 * group leader (ie, !FMONITOR).  We arbitrarily return
+		 * last pid (which is what $! returns).
+		 */
+		shf_fprintf(shf, "%d\n", j->pgrp ? j->pgrp :
+		    (j->last_proc ? j->last_proc->pid : 0));
+		return;
+	}
+	j->flags &= ~JF_CHANGED;
+	filler = j->job > 10 ?  "\n       " : "\n      ";
+	if (j == job_list)
+		jobchar = '+';
+	else if (j == job_list->next)
+		jobchar = '-';
+
+	for (p = j->proc_list; p != NULL;) {
+		coredumped = 0;
+		switch (p->state) {
+		case PRUNNING:
+			strlcpy(buf, "Running", sizeof buf);
+			break;
+		case PSTOPPED:
+			strlcpy(buf, sigtraps[WSTOPSIG(p->status)].mess,
+			    sizeof buf);
+			break;
+		case PEXITED:
+			if (how == JP_SHORT)
+				buf[0] = '\0';
+			else if (WEXITSTATUS(p->status) == 0)
+				strlcpy(buf, "Done", sizeof buf);
+			else
+				shf_snprintf(buf, sizeof(buf), "Done (%d)",
+				    WEXITSTATUS(p->status));
+			break;
+		case PSIGNALLED:
+			if (WCOREDUMP(p->status))
+				coredumped = 1;
+			/* kludge for not reporting `normal termination signals'
+			 * (ie, SIGINT, SIGPIPE)
+			 */
+			if (how == JP_SHORT && !coredumped &&
+			    (WTERMSIG(p->status) == SIGINT ||
+			    WTERMSIG(p->status) == SIGPIPE)) {
+				buf[0] = '\0';
+			} else
+				strlcpy(buf, sigtraps[WTERMSIG(p->status)].mess,
+				    sizeof buf);
+			break;
+		}
+
+		if (how != JP_SHORT) {
+			if (p == j->proc_list)
+				shf_fprintf(shf, "[%d] %c ", j->job, jobchar);
+			else
+				shf_fprintf(shf, "%s", filler);
+		}
+
+		if (how == JP_LONG)
+			shf_fprintf(shf, "%5d ", p->pid);
+
+		if (how == JP_SHORT) {
+			if (buf[0]) {
+				output = 1;
+				shf_fprintf(shf, "%s%s ",
+				    buf, coredumped ? " (core dumped)" : "");
+			}
+		} else {
+			output = 1;
+			shf_fprintf(shf, "%-20s %s%s%s", buf, p->command,
+			    p->next ? "|" : "",
+			    coredumped ? " (core dumped)" : "");
+		}
+
+		state = p->state;
+		status = p->status;
+		p = p->next;
+		while (p && p->state == state && p->status == status) {
+			if (how == JP_LONG)
+				shf_fprintf(shf, "%s%5d %-20s %s%s", filler, p->pid,
+				    " ", p->command, p->next ? "|" : "");
+			else if (how == JP_MEDIUM)
+				shf_fprintf(shf, " %s%s", p->command,
+				    p->next ? "|" : "");
+			p = p->next;
+		}
+	}
+	if (output)
+		shf_fprintf(shf, "\n");
+}
+
+/* Convert % sequence to job
+ *
+ * Expects sigchld to be blocked.
+ */
+static Job *
+j_lookup(const char *cp, int *ecodep)
+{
+	Job		*j, *last_match;
+	const char	*errstr;
+	Proc		*p;
+	int		len, job = 0;
+
+	if (digit(*cp)) {
+		job = strtonum(cp, 1, INT_MAX, &errstr);
+		if (errstr) {
+			if (ecodep)
+				*ecodep = JL_NOSUCH;
+			return NULL;
+		}
+		/* Look for last_proc->pid (what $! returns) first... */
+		for (j = job_list; j != NULL; j = j->next)
+			if (j->last_proc && j->last_proc->pid == job)
+				return j;
+		/* ...then look for process group (this is non-POSIX),
+		 * but should not break anything (so FPOSIX isn't used).
+		 */
+		for (j = job_list; j != NULL; j = j->next)
+			if (j->pgrp && j->pgrp == job)
+				return j;
+		if (ecodep)
+			*ecodep = JL_NOSUCH;
+		return NULL;
+	}
+	if (*cp != '%') {
+		if (ecodep)
+			*ecodep = JL_INVALID;
+		return NULL;
+	}
+	switch (*++cp) {
+	case '\0': /* non-standard */
+	case '+':
+	case '%':
+		if (job_list != NULL)
+			return job_list;
+		break;
+
+	case '-':
+		if (job_list != NULL && job_list->next)
+			return job_list->next;
+		break;
+
+	case '0': case '1': case '2': case '3': case '4':
+	case '5': case '6': case '7': case '8': case '9':
+		job = strtonum(cp, 1, INT_MAX, &errstr);
+		if (errstr)
+			break;
+		for (j = job_list; j != NULL; j = j->next)
+			if (j->job == job)
+				return j;
+		break;
+
+	case '?':		/* %?string */
+		last_match = NULL;
+		for (j = job_list; j != NULL; j = j->next)
+			for (p = j->proc_list; p != NULL; p = p->next)
+				if (strstr(p->command, cp+1) != NULL) {
+					if (last_match) {
+						if (ecodep)
+							*ecodep = JL_AMBIG;
+						return NULL;
+					}
+					last_match = j;
+				}
+		if (last_match)
+			return last_match;
+		break;
+
+	default:		/* %string */
+		len = strlen(cp);
+		last_match = NULL;
+		for (j = job_list; j != NULL; j = j->next)
+			if (strncmp(cp, j->proc_list->command, len) == 0) {
+				if (last_match) {
+					if (ecodep)
+						*ecodep = JL_AMBIG;
+					return NULL;
+				}
+				last_match = j;
+			}
+		if (last_match)
+			return last_match;
+		break;
+	}
+	if (ecodep)
+		*ecodep = JL_NOSUCH;
+	return NULL;
+}
+
+static Job	*free_jobs;
+static Proc	*free_procs;
+
+/* allocate a new job and fill in the job number.
+ *
+ * Expects sigchld to be blocked.
+ */
+static Job *
+new_job(void)
+{
+	int	i;
+	Job	*newj, *j;
+
+	if (free_jobs != NULL) {
+		newj = free_jobs;
+		free_jobs = free_jobs->next;
+	} else
+		newj = alloc(sizeof(Job), APERM);
+
+	/* brute force method */
+	for (i = 1; ; i++) {
+		for (j = job_list; j && j->job != i; j = j->next)
+			;
+		if (j == NULL)
+			break;
+	}
+	newj->job = i;
+
+	return newj;
+}
+
+/* Allocate new process struct
+ *
+ * Expects sigchld to be blocked.
+ */
+static Proc *
+new_proc(void)
+{
+	Proc	*p;
+
+	if (free_procs != NULL) {
+		p = free_procs;
+		free_procs = free_procs->next;
+	} else
+		p = alloc(sizeof(Proc), APERM);
+
+	return p;
+}
+
+/* Take job out of job_list and put old structures into free list.
+ * Keeps nzombies, last_job and async_job up to date.
+ *
+ * Expects sigchld to be blocked.
+ */
+static void
+remove_job(Job *j, const char *where)
+{
+	Proc	*p, *tmp;
+	Job	**prev, *curr;
+
+	prev = &job_list;
+	curr = *prev;
+	for (; curr != NULL && curr != j; prev = &curr->next, curr = *prev)
+		;
+	if (curr != j) {
+		internal_warningf("%s: job not found (%s)", __func__, where);
+		return;
+	}
+	*prev = curr->next;
+
+	/* free up proc structures */
+	for (p = j->proc_list; p != NULL; ) {
+		tmp = p;
+		p = p->next;
+		tmp->next = free_procs;
+		free_procs = tmp;
+	}
+
+	if ((j->flags & JF_ZOMBIE) && j->ppid == procpid)
+		--nzombie;
+	j->next = free_jobs;
+	free_jobs = j;
+
+	if (j == last_job)
+		last_job = NULL;
+	if (j == async_job)
+		async_job = NULL;
+}
+
+/* Put j in a particular location (taking it out of job_list if it is
+ * there already)
+ *
+ * Expects sigchld to be blocked.
+ */
+static void
+put_job(Job *j, int where)
+{
+	Job	**prev, *curr;
+
+	/* Remove job from list (if there) */
+	prev = &job_list;
+	curr = job_list;
+	for (; curr && curr != j; prev = &curr->next, curr = *prev)
+		;
+	if (curr == j)
+		*prev = curr->next;
+
+	switch (where) {
+	case PJ_ON_FRONT:
+		j->next = job_list;
+		job_list = j;
+		break;
+
+	case PJ_PAST_STOPPED:
+		prev = &job_list;
+		curr = job_list;
+		for (; curr && curr->state == PSTOPPED; prev = &curr->next,
+		    curr = *prev)
+			;
+		j->next = curr;
+		*prev = j;
+		break;
+	}
+}
+
+/* nuke a job (called when unable to start full job).
+ *
+ * Expects sigchld to be blocked.
+ */
+static int
+kill_job(Job *j, int sig)
+{
+	Proc	*p;
+	int	rval = 0;
+
+	for (p = j->proc_list; p != NULL; p = p->next)
+		if (p->pid != 0)
+			if (kill(p->pid, sig) < 0)
+				rval = -1;
+	return rval;
+}
diff --git a/ksh.1 b/ksh.1
@@ -0,0 +1,5600 @@
+.\"	$OpenBSD: ksh.1,v 1.203 2019/04/03 14:55:12 jca Exp $
+.\"
+.\"	Public Domain
+.\"
+.Dd $Mdocdate: April 3 2019 $
+.Dt KSH 1
+.Os
+.Sh NAME
+.Nm ksh ,
+.Nm rksh
+.Nd public domain Korn shell
+.Sh SYNOPSIS
+.Nm ksh
+.Bk -words
+.Op Fl +abCefhiklmnpruvXx
+.Op Fl +o Ar option
+.Op Fl c Ar string | Fl s | Ar file Op Ar argument ...
+.Ek
+.Sh DESCRIPTION
+.Nm
+is a command interpreter intended for both interactive and shell
+script use.
+Its command language is a superset of the
+.Xr sh 1
+shell language.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl c Ar string
+.Nm
+will execute the command(s) contained in
+.Ar string .
+.It Fl i
+Interactive shell.
+A shell is
+.Dq interactive
+if this
+option is used or if both standard input and standard error are attached
+to a
+.Xr tty 4 .
+An interactive shell has job control enabled, ignores the
+.Dv SIGINT ,
+.Dv SIGQUIT ,
+and
+.Dv SIGTERM
+signals, and prints prompts before reading input (see the
+.Ev PS1
+and
+.Ev PS2
+parameters).
+For non-interactive shells, the
+.Ic trackall
+option is on by default (see the
+.Ic set
+command below).
+.It Fl l
+Login shell.
+If the basename the shell is called with (i.e. argv[0])
+starts with
+.Ql -
+or if this option is used,
+the shell is assumed to be a login shell and the shell reads and executes
+the contents of
+.Pa /etc/profile
+and
+.Pa $HOME/.profile
+if they exist and are readable.
+.It Fl p
+Privileged shell.
+A shell is
+.Dq privileged
+if this option is used
+or if the real user ID or group ID does not match the
+effective user ID or group ID (see
+.Xr getuid 2
+and
+.Xr getgid 2 ) .
+A privileged shell does not process
+.Pa $HOME/.profile
+nor the
+.Ev ENV
+parameter (see below).
+Instead, the file
+.Pa /etc/suid_profile
+is processed.
+Clearing the privileged option causes the shell to set
+its effective user ID (group ID) to its real user ID (group ID).
+.It Fl r
+Restricted shell.
+A shell is
+.Dq restricted
+if this
+option is used;
+if the basename the shell was invoked with was
+.Dq rksh ;
+or if the
+.Ev SHELL
+parameter is set to
+.Dq rksh .
+The following restrictions come into effect after the shell processes any
+profile and
+.Ev ENV
+files:
+.Pp
+.Bl -bullet -compact
+.It
+The
+.Ic cd
+command is disabled.
+.It
+The
+.Ev SHELL ,
+.Ev ENV ,
+and
+.Ev PATH
+parameters cannot be changed.
+.It
+Command names can't be specified with absolute or relative paths.
+.It
+The
+.Fl p
+option of the built-in command
+.Ic command
+can't be used.
+.It
+Redirections that create files can't be used (i.e.\&
+.Sq Cm > ,
+.Sq Cm >| ,
+.Sq Cm >> ,
+.Sq Cm <> ) .
+.El
+.It Fl s
+The shell reads commands from standard input; all non-option arguments
+are positional parameters.
+.El
+.Pp
+In addition to the above, the options described in the
+.Ic set
+built-in command can also be used on the command line:
+both
+.Op Fl +abCefhkmnuvXx
+and
+.Op Fl +o Ar option
+can be used for single letter or long options, respectively.
+.Pp
+If neither the
+.Fl c
+nor the
+.Fl s
+option is specified, the first non-option argument specifies the name
+of a file the shell reads commands from.
+If there are no non-option
+arguments, the shell reads commands from the standard input.
+The name of the shell (i.e. the contents of $0)
+is determined as follows: if the
+.Fl c
+option is used and there is a non-option argument, it is used as the name;
+if commands are being read from a file, the file is used as the name;
+otherwise, the basename the shell was called with (i.e. argv[0]) is used.
+.Pp
+If the
+.Ev ENV
+parameter is set when an interactive shell starts (or,
+in the case of login shells,
+after any profiles are processed), its value is subjected to parameter,
+command, arithmetic, and tilde
+.Pq Sq ~
+substitution and the resulting file
+(if any) is read and executed.
+In order to have an interactive (as opposed to login) shell
+process a startup file,
+.Ev ENV
+may be set and exported (see below) in
+.Pa $HOME/.profile
+\- future interactive shell invocations will process any file pointed to by
+.Ev $ENV :
+.Pp
+.Dl export ENV=$HOME/.kshrc
+.Pp
+.Pa $HOME/.kshrc
+is then free to specify instructions for interactive shells.
+For example, the global configuration file may be sourced:
+.Bd -literal -offset indent
+\&. /etc/ksh.kshrc
+.Ed
+.Pp
+The above strategy may be employed to keep
+setup procedures for login shells in
+.Pa $HOME/.profile
+and setup procedures for interactive shells in
+.Pa $HOME/.kshrc .
+Of course, since login shells are also interactive,
+any commands placed in
+.Pa $HOME/.kshrc
+will be executed by login shells too.
+.Pp
+The exit status of the shell is 127 if the command file specified on the
+command line could not be opened, or non-zero if a fatal syntax error
+occurred during the execution of a script.
+In the absence of fatal errors,
+the exit status is that of the last command executed, or zero, if no
+command is executed.
+.Ss Command syntax
+The shell begins parsing its input by breaking it into
+.Em words .
+Words, which are sequences of characters, are delimited by unquoted whitespace
+characters (space, tab, and newline) or meta-characters
+.Po
+.Ql < ,
+.Ql > ,
+.Ql | ,
+.Ql \&; ,
+.Ql \&( ,
+.Ql \&) ,
+and
+.Ql &
+.Pc .
+Aside from delimiting words, spaces and tabs are ignored, while newlines
+usually delimit commands.
+The meta-characters are used in building the following
+.Em tokens :
+.Sq Cm < ,
+.Sq Cm <& ,
+.Sq Cm << ,
+.Sq Cm > ,
+.Sq Cm >& ,
+.Sq Cm >> ,
+etc. are used to specify redirections (see
+.Sx Input/output redirection
+below);
+.Ql |
+is used to create pipelines;
+.Ql |&
+is used to create co-processes (see
+.Sx Co-processes
+below);
+.Ql \&;
+is used to separate commands;
+.Ql &
+is used to create asynchronous pipelines;
+.Ql &&
+and
+.Ql ||
+are used to specify conditional execution;
+.Ql ;;
+is used in
+.Ic case
+statements;
+.Ql (( .. ))
+is used in arithmetic expressions;
+and lastly,
+.Ql \&( .. )\&
+is used to create subshells.
+.Pp
+Whitespace and meta-characters can be quoted individually using a backslash
+.Pq Sq \e ,
+or in groups using double
+.Pq Sq \&"
+or single
+.Pq Sq '
+quotes.
+The following characters are also treated specially by the
+shell and must be quoted if they are to represent themselves:
+.Ql \e ,
+.Ql \&" ,
+.Ql ' ,
+.Ql # ,
+.Ql $ ,
+.Ql ` ,
+.Ql ~ ,
+.Ql { ,
+.Ql } ,
+.Ql * ,
+.Ql \&? ,
+and
+.Ql \&[ .
+The first three of these are the above mentioned quoting characters (see
+.Sx Quoting
+below);
+.Ql # ,
+if used at the beginning of a word, introduces a comment \(em everything after
+the
+.Ql #
+up to the nearest newline is ignored;
+.Ql $
+is used to introduce parameter, command, and arithmetic substitutions (see
+.Sx Substitution
+below);
+.Ql `
+introduces an old-style command substitution (see
+.Sx Substitution
+below);
+.Ql ~
+begins a directory expansion (see
+.Sx Tilde expansion
+below);
+.Ql {
+and
+.Ql }
+delimit
+.Xr csh 1 Ns -style
+alternations (see
+.Sx Brace expansion
+below);
+and finally,
+.Ql * ,
+.Ql \&? ,
+and
+.Ql \&[
+are used in file name generation (see
+.Sx File name patterns
+below).
+.Pp
+As words and tokens are parsed, the shell builds commands, of which there
+are two basic types:
+.Em simple-commands ,
+typically programs that are executed, and
+.Em compound-commands ,
+such as
+.Ic for
+and
+.Ic if
+statements, grouping constructs, and function definitions.
+.Pp
+A simple-command consists of some combination of parameter assignments
+(see
+.Sx Parameters
+below),
+input/output redirections (see
+.Sx Input/output redirections
+below),
+and command words; the only restriction is that parameter assignments come
+before any command words.
+The command words, if any, define the command
+that is to be executed and its arguments.
+The command may be a shell built-in command, a function,
+or an external command
+(i.e. a separate executable file that is located using the
+.Ev PATH
+parameter; see
+.Sx Command execution
+below).
+.Pp
+All command constructs have an exit status.
+For external commands,
+this is related to the status returned by
+.Xr wait 2
+(if the command could not be found, the exit status is 127; if it could not
+be executed, the exit status is 126).
+The exit status of other command
+constructs (built-in commands, functions, compound-commands, pipelines, lists,
+etc.) are all well-defined and are described where the construct is
+described.
+The exit status of a command consisting only of parameter
+assignments is that of the last command substitution performed during the
+parameter assignment or 0 if there were no command substitutions.
+.Pp
+Commands can be chained together using the
+.Ql |
+token to form pipelines, in which the standard output of each command but the
+last is piped (see
+.Xr pipe 2 )
+to the standard input of the following command.
+The exit status of a pipeline is that of its last command.
+A pipeline may be prefixed by the
+.Ql \&!
+reserved word, which causes the exit status of the pipeline to be logically
+complemented: if the original status was 0, the complemented status will be 1;
+if the original status was not 0, the complemented status will be 0.
+.Pp
+.Em Lists
+of commands can be created by separating pipelines by any of the following
+tokens:
+.Ql && ,
+.Ql || ,
+.Ql & ,
+.Ql |& ,
+and
+.Ql \&; .
+The first two are for conditional execution:
+.Dq Ar cmd1 No && Ar cmd2
+executes
+.Ar cmd2
+only if the exit status of
+.Ar cmd1
+is zero;
+.Ql ||
+is the opposite \(em
+.Ar cmd2
+is executed only if the exit status of
+.Ar cmd1
+is non-zero.
+.Ql &&
+and
+.Ql ||
+have equal precedence which is higher than that of
+.Ql & ,
+.Ql |& ,
+and
+.Ql \&; ,
+which also have equal precedence.
+The
+.Ql &&
+and
+.Ql ||
+operators are
+.Qq left-associative .
+For example, both of these commands will print only
+.Qq bar :
+.Bd -literal -offset indent
+$ false && echo foo || echo bar
+$ true || echo foo && echo bar
+.Ed
+.Pp
+The
+.Ql &
+token causes the preceding command to be executed asynchronously; that is,
+the shell starts the command but does not wait for it to complete (the shell
+does keep track of the status of asynchronous commands; see
+.Sx Job control
+below).
+When an asynchronous command is started when job control is disabled
+(i.e. in most scripts), the command is started with signals
+.Dv SIGINT
+and
+.Dv SIGQUIT
+ignored and with input redirected from
+.Pa /dev/null
+(however, redirections specified in the asynchronous command have precedence).
+The
+.Ql |&
+operator starts a co-process which is a special kind of asynchronous process
+(see
+.Sx Co-processes
+below).
+A command must follow the
+.Ql &&
+and
+.Ql ||
+operators, while it need not follow
+.Ql & ,
+.Ql |& ,
+or
+.Ql \&; .
+The exit status of a list is that of the last command executed, with the
+exception of asynchronous lists, for which the exit status is 0.
+.Pp
+Compound commands are created using the following reserved words.
+These words
+are only recognized if they are unquoted and if they are used as the first
+word of a command (i.e. they can't be preceded by parameter assignments or
+redirections):
+.Bd -literal -offset indent
+case   esac       in       until   ((   }
+do     fi         name     while   ))
+done   for        select   !       [[
+elif   function   then     (       ]]
+else   if         time     )       {
+.Ed
+.Pp
+.Sy Note :
+Some shells (but not this one) execute control structure commands in a
+subshell when one or more of their file descriptors are redirected, so any
+environment changes inside them may fail.
+To be portable, the
+.Ic exec
+statement should be used instead to redirect file descriptors before the
+control structure.
+.Pp
+In the following compound command descriptions, command lists (denoted as
+.Em list )
+that are followed by reserved words must end with a semicolon, a newline, or
+a (syntactically correct) reserved word.
+For example, the following are all valid:
+.Bd -literal -offset indent
+$ { echo foo; echo bar; }
+$ { echo foo; echo bar<newline> }
+$ { { echo foo; echo bar; } }
+.Ed
+.Pp
+This is not valid:
+.Pp
+.Dl $ { echo foo; echo bar }
+.Bl -tag -width Ds
+.It Pq Ar list
+Execute
+.Ar list
+in a subshell.
+There is no implicit way to pass environment changes from a
+subshell back to its parent.
+.It { Ar list ; No }
+Compound construct;
+.Ar list
+is executed, but not in a subshell.
+Note that
+.Ql {
+and
+.Ql }
+are reserved words, not meta-characters.
+.It Xo Ic case Ar word Cm in
+.Oo Op \&(
+.Ar pattern
+.Op | Ar pattern
+.No ... )
+.Ar list No ;;\ \& Oc ... Cm esac
+.Xc
+The
+.Ic case
+statement attempts to match
+.Ar word
+against a specified
+.Ar pattern ;
+the
+.Ar list
+associated with the first successfully matched pattern is executed.
+Patterns used in
+.Ic case
+statements are the same as those used for file name patterns except that the
+restrictions regarding
+.Ql \&.
+and
+.Ql /
+are dropped.
+Note that any unquoted space before and after a pattern is
+stripped; any space within a pattern must be quoted.
+Both the word and the
+patterns are subject to parameter, command, and arithmetic substitution, as
+well as tilde substitution.
+For historical reasons, open and close braces may be used instead of
+.Cm in
+and
+.Cm esac
+e.g.\&
+.Ic case $foo { *) echo bar; } .
+The exit status of a
+.Ic case
+statement is that of the executed
+.Ar list ;
+if no
+.Ar list
+is executed, the exit status is zero.
+.It Xo Ic for Ar name
+.Op Cm in Op Ar word ... ;
+.Cm do Ar list ; Cm done
+.Xc
+For each
+.Ar word
+in the specified word list, the parameter
+.Ar name
+is set to the word and
+.Ar list
+is executed.
+If
+.Cm in
+is not used to specify a word list, the positional parameters
+($1, $2, etc.)\&
+are used instead.
+For historical reasons, open and close braces may be used instead of
+.Cm do
+and
+.Cm done
+e.g.\&
+.Ic for i; { echo $i; } .
+The exit status of a
+.Ic for
+statement is the last exit status of
+.Ar list .
+If there are no items,
+.Ar list
+is not executed and the exit status is zero.
+.It Xo Ic if Ar list ;
+.Cm then Ar list ;
+.Oo Cm elif Ar list ;
+.Cm then Ar list ; Oc ...
+.Oo Cm else Ar list ; Oc
+.Cm fi
+.Xc
+If the exit status of the first
+.Ar list
+is zero, the second
+.Ar list
+is executed; otherwise, the
+.Ar list
+following the
+.Cm elif ,
+if any, is executed with similar consequences.
+If all the lists following the
+.Ic if
+and
+.Cm elif Ns s
+fail (i.e. exit with non-zero status), the
+.Ar list
+following the
+.Cm else
+is executed.
+The exit status of an
+.Ic if
+statement is that of non-conditional
+.Ar list
+that is executed; if no non-conditional
+.Ar list
+is executed, the exit status is zero.
+.It Xo Ic select Ar name
+.Oo Cm in Ar word No ... Oc ;
+.Cm do Ar list ; Cm done
+.Xc
+The
+.Ic select
+statement provides an automatic method of presenting the user with a menu and
+selecting from it.
+An enumerated list of the specified
+.Ar word Ns (s)
+is printed on standard error, followed by a prompt
+.Po
+.Ev PS3 :
+normally
+.Sq #?\ \&
+.Pc .
+A number corresponding to one of the enumerated words is then read from
+standard input,
+.Ar name
+is set to the selected word (or unset if the selection is not valid),
+.Ev REPLY
+is set to what was read (leading/trailing space is stripped), and
+.Ar list
+is executed.
+If a blank line (i.e. zero or more
+.Ev IFS
+characters) is entered, the menu is reprinted without executing
+.Ar list .
+.Pp
+When
+.Ar list
+completes, the enumerated list is printed if
+.Ev REPLY
+is
+.Dv NULL ,
+the prompt is printed, and so on.
+This process continues until an end-of-file
+is read, an interrupt is received, or a
+.Ic break
+statement is executed inside the loop.
+If
+.Dq in word ...
+is omitted, the positional parameters are used
+(i.e. $1, $2, etc.).
+For historical reasons, open and close braces may be used instead of
+.Cm do
+and
+.Cm done
+e.g.\&
+.Ic select i; { echo $i; } .
+The exit status of a
+.Ic select
+statement is zero if a
+.Ic break
+statement is used to exit the loop, non-zero otherwise.
+.It Xo Ic until Ar list ;
+.Cm do Ar list ;
+.Cm done
+.Xc
+This works like
+.Ic while ,
+except that the body is executed only while the exit status of the first
+.Ar list
+is non-zero.
+.It Xo Ic while Ar list ;
+.Cm do Ar list ;
+.Cm done
+.Xc
+A
+.Ic while
+is a pre-checked loop.
+Its body is executed as often as the exit status of the first
+.Ar list
+is zero.
+The exit status of a
+.Ic while
+statement is the last exit status of the
+.Ar list
+in the body of the loop; if the body is not executed, the exit status is zero.
+.It Xo Ic function Ar name
+.No { Ar list ; No }
+.Xc
+Defines the function
+.Ar name
+(see
+.Sx Functions
+below).
+Note that redirections specified after a function definition are
+performed whenever the function is executed, not when the function definition
+is executed.
+.It Ar name Ns () Ar command
+Mostly the same as
+.Ic function
+(see
+.Sx Functions
+below).
+.It Xo Ic time Op Fl p
+.Op Ar pipeline
+.Xc
+The
+.Ic time
+reserved word is described in the
+.Sx Command execution
+section.
+.It Ic (( Ar expression Cm ))
+The arithmetic expression
+.Ar expression
+is evaluated; equivalent to
+.Ic let Ar expression
+(see
+.Sx Arithmetic expressions
+and the
+.Ic let
+command, below).
+.It Ic [[ Ar expression Cm ]]
+Similar to the
+.Ic test
+and
+.Ic \&[ No ... Cm \&]
+commands (described later), with the following exceptions:
+.Bl -bullet -offset indent
+.It
+Field splitting and file name generation are not performed on arguments.
+.It
+The
+.Fl a
+.Pq AND
+and
+.Fl o
+.Pq OR
+operators are replaced with
+.Ql &&
+and
+.Ql || ,
+respectively.
+.It
+Operators (e.g.\&
+.Sq Fl f ,
+.Sq = ,
+.Sq \&! )
+must be unquoted.
+.It
+The second operand of the
+.Sq !=
+and
+.Sq =
+expressions are patterns (e.g. the comparison
+.Ic [[ foobar = f*r ]]
+succeeds).
+.It
+There are two additional binary operators,
+.Ql <
+and
+.Ql > ,
+which return true if their first string operand is less than, or greater than,
+their second string operand, respectively.
+.It
+The single argument form of
+.Ic test ,
+which tests if the argument has a non-zero length, is not valid; explicit
+operators must always be used e.g. instead of
+.No \&[ Ar str No \&]
+use
+.No \&[[ -n Ar str No \&]] .
+.It
+Parameter, command, and arithmetic substitutions are performed as expressions
+are evaluated and lazy expression evaluation is used for the
+.Ql &&
+and
+.Ql ||
+operators.
+This means that in the following statement,
+.Ic $(< foo)
+is evaluated if and only if the file
+.Pa foo
+exists and is readable:
+.Bd -literal -offset indent
+$ [[ -r foo && $(< foo) = b*r ]]
+.Ed
+.El
+.El
+.Ss Quoting
+Quoting is used to prevent the shell from treating characters or words
+specially.
+There are three methods of quoting.
+First,
+.Ql \e
+quotes the following character, unless it is at the end of a line, in which
+case both the
+.Ql \e
+and the newline are stripped.
+Second, a single quote
+.Pq Sq '
+quotes everything up to the next single quote (this may span lines).
+Third, a double quote
+.Pq Sq \&"
+quotes all characters, except
+.Ql $ ,
+.Ql `
+and
+.Ql \e ,
+up to the next unquoted double quote.
+.Ql $
+and
+.Ql `
+inside double quotes have their usual meaning (i.e. parameter, command, or
+arithmetic substitution) except no field splitting is carried out on the
+results of double-quoted substitutions.
+If a
+.Ql \e
+inside a double-quoted string is followed by
+.Ql \e ,
+.Ql $ ,
+.Ql ` ,
+or
+.Ql \&" ,
+it is replaced by the second character; if it is followed by a newline, both
+the
+.Ql \e
+and the newline are stripped; otherwise, both the
+.Ql \e
+and the character following are unchanged.
+.Ss Aliases
+There are two types of aliases: normal command aliases and tracked aliases.
+Command aliases are normally used as a short hand for a long or often used
+command.
+The shell expands command aliases (i.e. substitutes the alias name
+for its value) when it reads the first word of a command.
+An expanded alias is re-processed to check for more aliases.
+If a command alias ends in a
+space or tab, the following word is also checked for alias expansion.
+The alias expansion process stops when a word that is not an alias is found,
+when a quoted word is found, or when an alias word that is currently being
+expanded is found.
+.Pp
+The following command aliases are defined automatically by the shell:
+.Pp
+.Bl -item -compact -offset indent
+.It
+.Ic autoload Ns ='typeset -fu'
+.It
+.Ic functions Ns ='typeset -f'
+.It
+.Ic hash Ns ='alias -t'
+.It
+.Ic history Ns ='fc -l'
+.It
+.Ic integer Ns ='typeset -i'
+.It
+.Ic local Ns ='typeset'
+.It
+.Ic login Ns ='exec login'
+.It
+.Ic nohup Ns ='nohup '
+.It
+.Ic r Ns ='fc -s'
+.It
+.Ic stop Ns ='kill -STOP'
+.El
+.Pp
+Tracked aliases allow the shell to remember where it found a particular
+command.
+The first time the shell does a path search for a command that is
+marked as a tracked alias, it saves the full path of the command.
+The next
+time the command is executed, the shell checks the saved path to see that it
+is still valid, and if so, avoids repeating the path search.
+Tracked aliases can be listed and created using
+.Ic alias -t .
+Note that changing the
+.Ev PATH
+parameter clears the saved paths for all tracked aliases.
+If the
+.Ic trackall
+option is set (i.e.\&
+.Ic set -o Ic trackall
+or
+.Ic set -h ) ,
+the shell tracks all commands.
+This option is set automatically for non-interactive shells.
+For interactive shells, only the following commands are
+automatically tracked:
+.Xr cat 1 ,
+.Xr cc 1 ,
+.Xr chmod 1 ,
+.Xr cp 1 ,
+.Xr date 1 ,
+.Xr ed 1 ,
+.Sy emacs ,
+.Xr grep 1 ,
+.Xr ls 1 ,
+.Xr mail 1 ,
+.Xr make 1 ,
+.Xr mv 1 ,
+.Xr pr 1 ,
+.Xr rm 1 ,
+.Xr sed 1 ,
+.Xr sh 1 ,
+.Xr vi 1 ,
+and
+.Xr who 1 .
+.Ss Substitution
+The first step the shell takes in executing a simple-command is to perform
+substitutions on the words of the command.
+There are three kinds of
+substitution: parameter, command, and arithmetic.
+Parameter substitutions,
+which are described in detail in the next section, take the form
+.Pf $ Ar name
+or
+.Pf ${ Ar ... Ns } ;
+command substitutions take the form
+.Pf $( Ar command )
+or
+.Pf ` Ar command Ns ` ;
+and arithmetic substitutions take the form
+.Pf $(( Ar expression ) ) .
+.Pp
+If a substitution appears outside of double quotes, the results of the
+substitution are generally subject to word or field splitting according to
+the current value of the
+.Ev IFS
+parameter.
+The
+.Ev IFS
+parameter specifies a list of characters which are used to break a string up
+into several words; any characters from the set space, tab, and newline that
+appear in the
+.Ev IFS
+characters are called
+.Dq IFS whitespace .
+Sequences of one or more
+.Ev IFS
+whitespace characters, in combination with zero or one
+.Pf non- Ev IFS
+whitespace
+characters, delimit a field.
+As a special case, leading and trailing
+.Ev IFS
+whitespace is stripped (i.e. no leading or trailing empty field is created by
+it); leading
+.Pf non- Ev IFS
+whitespace does create an empty field.
+.Pp
+Example: If
+.Ev IFS
+is set to
+.Dq <space>: ,
+and VAR is set to
+.Dq <space>A<space>:<space><space>B::D ,
+the substitution for $VAR results in four fields:
+.Sq A ,
+.Sq B ,
+.Sq
+(an empty field),
+and
+.Sq D .
+Note that if the
+.Ev IFS
+parameter is set to the
+.Dv NULL
+string, no field splitting is done; if the parameter is unset, the default
+value of space, tab, and newline is used.
+.Pp
+Also, note that the field splitting applies only to the immediate result of
+the substitution.
+Using the previous example, the substitution for $VAR:E
+results in the fields:
+.Sq A ,
+.Sq B ,
+.Sq ,
+and
+.Sq D:E ,
+not
+.Sq A ,
+.Sq B ,
+.Sq ,
+.Sq D ,
+and
+.Sq E .
+This behavior is POSIX compliant, but incompatible with some other shell
+implementations which do field splitting on the word which contained the
+substitution or use
+.Dv IFS
+as a general whitespace delimiter.
+.Pp
+The results of substitution are, unless otherwise specified, also subject to
+brace expansion and file name expansion (see the relevant sections below).
+.Pp
+A command substitution is replaced by the output generated by the specified
+command, which is run in a subshell.
+For
+.Pf $( Ar command )
+substitutions, normal quoting rules are used when
+.Ar command
+is parsed; however, for the
+.Pf ` Ar command Ns `
+form, a
+.Ql \e
+followed by any of
+.Ql $ ,
+.Ql ` ,
+or
+.Ql \e
+is stripped (a
+.Ql \e
+followed by any other character is unchanged).
+As a special case in command substitutions, a command of the form
+.Pf < Ar file
+is interpreted to mean substitute the contents of
+.Ar file .
+Note that
+.Ic $(< foo)
+has the same effect as
+.Ic $(cat foo) ,
+but it is carried out more efficiently because no process is started.
+.Pp
+Arithmetic substitutions are replaced by the value of the specified expression.
+For example, the command
+.Ic echo $((2+3*4))
+prints 14.
+See
+.Sx Arithmetic expressions
+for a description of an expression.
+.Ss Parameters
+Parameters are shell variables; they can be assigned values and their values
+can be accessed using a parameter substitution.
+A parameter name is either one
+of the special single punctuation or digit character parameters described
+below, or a letter followed by zero or more letters or digits
+.Po
+.Ql _
+counts as a letter
+.Pc .
+The latter form can be treated as arrays by appending an array index of the
+form
+.Op Ar expr
+where
+.Ar expr
+is an arithmetic expression.
+Parameter substitutions take the form
+.Pf $ Ar name ,
+.Pf ${ Ar name Ns } ,
+or
+.Sm off
+.Pf ${ Ar name Bo Ar expr Bc }
+.Sm on
+where
+.Ar name
+is a parameter name.
+If
+.Ar expr
+is a literal
+.Ql @
+then the named array is expanded using the same quoting rules as
+.Ql $@ ,
+while if
+.Ar expr
+is a literal
+.Ql *
+then the named array is expanded using the same quoting rules as
+.Ql $* .
+If substitution is performed on a parameter
+(or an array parameter element)
+that is not set, a null string is substituted unless the
+.Ic nounset
+option
+.Po
+.Ic set Fl o Ic nounset
+or
+.Ic set Fl u
+.Pc
+is set, in which case an error occurs.
+.Pp
+Parameters can be assigned values in a number of ways.
+First, the shell implicitly sets some parameters like
+.Ql # ,
+.Ql PWD ,
+and
+.Ql $ ;
+this is the only way the special single character parameters are set.
+Second, parameters are imported from the shell's environment at startup.
+Third, parameters can be assigned values on the command line: for example,
+.Ic FOO=bar
+sets the parameter
+.Dq FOO
+to
+.Dq bar ;
+multiple parameter assignments can be given on a single command line and they
+can be followed by a simple-command, in which case the assignments are in
+effect only for the duration of the command (such assignments are also
+exported; see below for the implications of this).
+Note that both the parameter name and the
+.Ql =
+must be unquoted for the shell to recognize a parameter assignment.
+The fourth way of setting a parameter is with the
+.Ic export ,
+.Ic readonly ,
+and
+.Ic typeset
+commands; see their descriptions in the
+.Sx Command execution
+section.
+Fifth,
+.Ic for
+and
+.Ic select
+loops set parameters as well as the
+.Ic getopts ,
+.Ic read ,
+and
+.Ic set -A
+commands.
+Lastly, parameters can be assigned values using assignment operators
+inside arithmetic expressions (see
+.Sx Arithmetic expressions
+below) or using the
+.Pf ${ Ar name Ns = Ns Ar value Ns }
+form of the parameter substitution (see below).
+.Pp
+Parameters with the export attribute (set using the
+.Ic export
+or
+.Ic typeset Fl x
+commands, or by parameter assignments followed by simple commands) are put in
+the environment (see
+.Xr environ 7 )
+of commands run by the shell as
+.Ar name Ns = Ns Ar value
+pairs.
+The order in which parameters appear in the environment of a command is
+unspecified.
+When the shell starts up, it extracts parameters and their values
+from its environment and automatically sets the export attribute for those
+parameters.
+.Pp
+Modifiers can be applied to the
+.Pf ${ Ar name Ns }
+form of parameter substitution:
+.Bl -tag -width Ds
+.Sm off
+.It ${ Ar name No :- Ar word No }
+.Sm on
+If
+.Ar name
+is set and not
+.Dv NULL ,
+it is substituted; otherwise,
+.Ar word
+is substituted.
+.Sm off
+.It ${ Ar name No :+ Ar word No }
+.Sm on
+If
+.Ar name
+is set and not
+.Dv NULL ,
+.Ar word
+is substituted; otherwise, nothing is substituted.
+.Sm off
+.It ${ Ar name No := Ar word No }
+.Sm on
+If
+.Ar name
+is set and not
+.Dv NULL ,
+it is substituted; otherwise, it is assigned
+.Ar word
+and the resulting value of
+.Ar name
+is substituted.
+.Sm off
+.It ${ Ar name No :? Ar word No }
+.Sm on
+If
+.Ar name
+is set and not
+.Dv NULL ,
+it is substituted; otherwise,
+.Ar word
+is printed on standard error (preceded by
+.Ar name : )
+and an error occurs (normally causing termination of a shell script, function,
+or script sourced using the
+.Sq \&.
+built-in).
+If
+.Ar word
+is omitted, the string
+.Dq parameter null or not set
+is used instead.
+.El
+.Pp
+In the above modifiers, the
+.Ql \&:
+can be omitted, in which case the conditions only depend on
+.Ar name
+being set (as opposed to set and not
+.Dv NULL ) .
+If
+.Ar word
+is needed, parameter, command, arithmetic, and tilde substitution are performed
+on it; if
+.Ar word
+is not needed, it is not evaluated.
+.Pp
+The following forms of parameter substitution can also be used:
+.Pp
+.Bl -tag -width Ds -compact
+.It Pf ${# Ar name Ns }
+The number of positional parameters if
+.Ar name
+is
+.Ql * ,
+.Ql @ ,
+or not specified; otherwise the length of the string value of parameter
+.Ar name .
+.Pp
+.It Pf ${# Ar name Ns [*]}
+.It Pf ${# Ar name Ns [@]}
+The number of elements in the array
+.Ar name .
+.Pp
+.It Pf ${ Ar name Ns # Ns Ar pattern Ns }
+.It Pf ${ Ar name Ns ## Ns Ar pattern Ns }
+If
+.Ar pattern
+matches the beginning of the value of parameter
+.Ar name ,
+the matched text is deleted from the result of substitution.
+A single
+.Ql #
+results in the shortest match, and two
+of them result in the longest match.
+.Pp
+.It Pf ${ Ar name Ns % Ns Ar pattern Ns }
+.It Pf ${ Ar name Ns %% Ns Ar pattern Ns }
+Like ${..#..} substitution, but it deletes from the end of the value.
+.El
+.Pp
+The following special parameters are implicitly set by the shell and cannot be
+set directly using assignments:
+.Bl -tag -width "1 ... 9"
+.It Ev \&!
+Process ID of the last background process started.
+If no background processes have been started, the parameter is not set.
+.It Ev \&#
+The number of positional parameters ($1, $2, etc.).
+.It Ev \&$
+The PID of the shell, or the PID of the original shell if it is a subshell.
+Do
+.Em NOT
+use this mechanism for generating temporary file names; see
+.Xr mktemp 1
+instead.
+.It Ev -
+The concatenation of the current single letter options (see the
+.Ic set
+command below for a list of options).
+.It Ev \&?
+The exit status of the last non-asynchronous command executed.
+If the last command was killed by a signal,
+.Ic $?\&
+is set to 128 plus the signal number.
+.It Ev 0
+The name of the shell, determined as follows:
+the first argument to
+.Nm
+if it was invoked with the
+.Fl c
+option and arguments were given; otherwise the
+.Ar file
+argument, if it was supplied;
+or else the basename the shell was invoked with (i.e.\&
+.Li argv[0] ) .
+.Ev $0
+is also set to the name of the current script or
+the name of the current function, if it was defined with the
+.Ic function
+keyword (i.e. a Korn shell style function).
+.It Ev 1 No ... Ev 9
+The first nine positional parameters that were supplied to the shell, function,
+or script sourced using the
+.Sq \&.
+built-in.
+Further positional parameters may be accessed using
+.Pf ${ Ar number Ns } .
+.It Ev *
+All positional parameters (except parameter 0) i.e. $1, $2, $3, ...
+If used
+outside of double quotes, parameters are separate words (which are subjected
+to word splitting); if used within double quotes, parameters are separated
+by the first character of the
+.Ev IFS
+parameter (or the empty string if
+.Ev IFS
+is
+.Dv NULL ) .
+.It Ev @
+Same as
+.Ic $* ,
+unless it is used inside double quotes, in which case a separate word is
+generated for each positional parameter.
+If there are no positional parameters, no word is generated.
+.Ic $@
+can be used to access arguments, verbatim, without losing
+.Dv NULL
+arguments or splitting arguments with spaces.
+.El
+.Pp
+The following parameters are set and/or used by the shell:
+.Bl -tag -width "EXECSHELL"
+.It Ev _ No (underscore)
+When an external command is executed by the shell, this parameter is set in the
+environment of the new process to the path of the executed command.
+In interactive use, this parameter is also set in the parent shell to the last
+word of the previous command.
+When
+.Ev MAILPATH
+messages are evaluated, this parameter contains the name of the file that
+changed (see the
+.Ev MAILPATH
+parameter, below).
+.It Ev CDPATH
+Search path for the
+.Ic cd
+built-in command.
+It works the same way as
+.Ev PATH
+for those directories not beginning with
+.Ql /
+or
+.Ql .\&
+in
+.Ic cd
+commands.
+Note that if
+.Ev CDPATH
+is set and does not contain
+.Sq \&.
+or contains an empty path, the current directory is not searched.
+Also, the
+.Ic cd
+built-in command will display the resulting directory when a match is found
+in any search path other than the empty path.
+.It Ev COLUMNS
+Set to the number of columns on the terminal or window.
+Currently set to the
+.Dq cols
+value as reported by
+.Xr stty 1
+if that value is non-zero.
+This parameter is used by the interactive line editing modes, and by the
+.Ic select ,
+.Ic set -o ,
+and
+.Ic kill -l
+commands to format information columns.
+.It Ev EDITOR
+If the
+.Ev VISUAL
+parameter is not set, this parameter controls the command-line editing mode for
+interactive shells.
+See the
+.Ev VISUAL
+parameter below for how this works.
+.Pp
+Note:
+traditionally,
+.Ev EDITOR
+was used to specify the name of an (old-style) line editor, such as
+.Xr ed 1 ,
+and
+.Ev VISUAL
+was used to specify a (new-style) screen editor, such as
+.Xr vi 1 .
+Hence if
+.Ev VISUAL
+is set, it overrides
+.Ev EDITOR .
+.It Ev ENV
+If this parameter is found to be set after any profile files are executed, the
+expanded value is used as a shell startup file.
+It typically contains function and alias definitions.
+.It Ev EXECSHELL
+If set, this parameter is assumed to contain the shell that is to be used to
+execute commands that
+.Xr execve 2
+fails to execute and which do not start with a
+.Dq #! Ns Ar shell
+sequence.
+.It Ev FCEDIT
+The editor used by the
+.Ic fc
+command (see below).
+.It Ev FPATH
+Like
+.Ev PATH ,
+but used when an undefined function is executed to locate the file defining the
+function.
+It is also searched when a command can't be found using
+.Ev PATH .
+See
+.Sx Functions
+below for more information.
+.It Ev HISTCONTROL
+A colon separated list of history settings.
+If
+.Li ignoredups
+is present, lines identical to the previous history line will not be saved.
+If
+.Li ignorespace
+is present, lines starting with a space will not be saved.
+Unknown settings are ignored.
+.It Ev HISTFILE
+The name of the file used to store command history.
+When assigned to, history is loaded from the specified file.
+Also, several invocations of the shell
+running on the same machine will share history if their
+.Ev HISTFILE
+parameters all point to the same file.
+.Pp
+.Sy Note :
+If
+.Ev HISTFILE
+isn't set, no history file is used.
+This is different from the original Korn shell, which uses
+.Pa $HOME/.sh_history .
+.It Ev HISTSIZE
+The number of commands normally stored for history.
+The default is 500.
+.It Ev HOME
+The default directory for the
+.Ic cd
+command and the value substituted for an unqualified
+.Ic ~
+(see
+.Sx Tilde expansion
+below).
+.It Ev IFS
+Internal field separator, used during substitution and by the
+.Ic read
+command, to split values into distinct arguments; normally set to space, tab,
+and newline.
+See
+.Sx Substitution
+above for details.
+.Pp
+.Sy Note :
+This parameter is not imported from the environment when the shell is
+started.
+.It Ev KSH_VERSION
+The version of the shell and the date the version was created (read-only).
+.It Ev LINENO
+The line number of the function or shell script that is currently being
+executed.
+.It Ev LINES
+Set to the number of lines on the terminal or window.
+.It Ev MAIL
+If set, the user will be informed of the arrival of mail in the named file.
+This parameter is ignored if the
+.Ev MAILPATH
+parameter is set.
+.It Ev MAILCHECK
+How often, in seconds, the shell will check for mail in the file(s) specified
+by
+.Ev MAIL
+or
+.Ev MAILPATH .
+If set to 0, the shell checks before each prompt.
+The default is 600 (10 minutes).
+.It Ev MAILPATH
+A list of files to be checked for mail.
+The list is colon separated, and each file may be followed by a
+.Ql \&?
+and a message to be printed if new mail has arrived.
+Command, parameter, and
+arithmetic substitution is performed on the message and, during substitution,
+the parameter
+.Ic $_
+contains the name of the file.
+The default message is
+.Dq you have mail in $_ .
+.It Ev OLDPWD
+The previous working directory.
+Unset if
+.Ic cd
+has not successfully changed directories since the shell started, or if the
+shell doesn't know where it is.
+.It Ev OPTARG
+When using
+.Ic getopts ,
+it contains the argument for a parsed option, if it requires one.
+.It Ev OPTIND
+The index of the next argument to be processed when using
+.Ic getopts .
+Assigning 1 to this parameter causes
+.Ic getopts
+to process arguments from the beginning the next time it is invoked.
+.It Ev PATH
+A colon separated list of directories that are searched when looking for
+commands and files sourced using the
+.Sq \&.
+command (see below).
+An empty string resulting from a leading or trailing
+colon, or two adjacent colons, is treated as a
+.Sq \&.
+(the current directory).
+.It Ev POSIXLY_CORRECT
+If set, this parameter causes the
+.Ic posix
+option to be enabled.
+See
+.Sx POSIX mode
+below.
+.It Ev PPID
+The process ID of the shell's parent (read-only).
+.It Ev PS1
+The primary prompt for interactive shells.
+Parameter, command, and arithmetic
+substitutions are performed,
+and the prompt string can be customised using
+backslash-escaped special characters.
+.Pp
+Note that since the command-line editors try to figure out how long the prompt
+is (so they know how far it is to the edge of the screen), escape codes in
+the prompt tend to mess things up.
+You can tell the shell not to count certain
+sequences (such as escape codes) by using the
+.Li \e[ Ns Ar ... Ns Li \e]
+substitution (see below) or by prefixing your prompt with a non-printing
+character (such as control-A) followed by a carriage return and then delimiting
+the escape codes with this non-printing character.
+By the way, don't blame me for
+this hack; it's in the original
+.Nm .
+.Pp
+The default prompt is the first part of the hostname, followed by
+.Sq $\ \&
+for non-root users,
+.Sq #\ \&
+for root.
+.Pp
+The following backslash-escaped special characters can be used
+to customise the prompt:
+.Pp
+.Bl -tag -width "\eD{format}XX" -compact
+.It Li \ea
+Insert an ASCII bell character.
+.It Li \ed
+The current date, in the format
+.Dq Day Month Date
+for example
+.Dq Wed Nov 03 .
+.It Li \eD Ns Brq Ar format
+The current date, with
+.Ar format
+converted by
+.Xr strftime 3 .
+The braces must be specified.
+.It Li \ee
+Insert an ASCII escape character.
+.It Li \eh
+The hostname, minus domain name.
+.It Li \eH
+The full hostname, including domain name.
+.It Li \ej
+Current number of jobs running
+(see
+.Sx Job control
+below).
+.It Li \el
+The controlling terminal.
+.It Li \en
+Insert a newline character.
+.It Li \er
+Insert a carriage return character.
+.It Li \es
+The name of the shell.
+.It Li \et
+The current time, in 24-hour HH:MM:SS format.
+.It Li \eT
+The current time, in 12-hour HH:MM:SS format.
+.It Li \e@
+The current time, in 12-hour HH:MM:SS AM/PM format.
+.It Li \eA
+The current time, in 24-hour HH:MM format.
+.It Li \eu
+The current user's username.
+.It Li \ev
+The current version of
+.Nm .
+.It Li \eV
+Like
+.Sq \ev ,
+but more verbose.
+.It Li \ew
+The current working directory.
+.Dv $HOME
+is abbreviated as
+.Sq ~ .
+.It Li \eW
+The basename of
+the current working directory.
+.Dv $HOME
+is abbreviated as
+.Sq ~ .
+.It Li \e!
+The current history number.
+An unescaped
+.Ql !\&
+will produce the current history number too,
+as per the POSIX specification.
+A literal
+.Ql \&!
+can be put in the prompt by placing
+.Ql !!
+in
+.Ev PS1 .
+.It Li \e#
+The current command number.
+This could be different to the current history number,
+if
+.Ev HISTFILE
+contains a history list from a previous session.
+.It Li \e$
+The default prompt character i.e.\&
+.Sq #
+if the effective UID is 0,
+otherwise
+.Sq $ .
+Since the shell interprets
+.Sq $
+as a special character within double quotes,
+it is safer in this case to escape the backslash
+than to try quoting it.
+.It Li \e Ns Ar nnn
+The octal character
+.Ar nnn .
+.It Li \e\e
+Insert a single backslash character.
+.It Li \e[
+Normally the shell keeps track of the number of characters in the prompt.
+Use of this sequence turns off that count.
+.It Li \e]
+Use of this sequence turns the count back on.
+.El
+.Pp
+Note that the backslash itself may be interpreted by the shell.
+Hence, to set
+.Ev PS1
+either escape the backslash itself,
+or use double quotes.
+The latter is more practical:
+.Bd -literal -offset indent
+PS1="\eu "
+.Ed
+.Pp
+This is a more complex example,
+which does not rely on the above backslash-escaped sequences.
+It embeds the current working directory,
+in reverse video,
+in the prompt string:
+.Bd -literal -offset indent
+x=$(print \e\e001)
+PS1="$x$(print \e\er)$x$(tput so)$x\e$PWD$x$(tput se)$x> "
+.Ed
+.It Ev PS2
+Secondary prompt string, by default
+.Sq >\ \& ,
+used when more input is needed to complete a command.
+.It Ev PS3
+Prompt used by the
+.Ic select
+statement when reading a menu selection.
+The default is
+.Sq #?\ \& .
+.It Ev PS4
+Used to prefix commands that are printed during execution tracing (see the
+.Ic set Fl x
+command below).
+Parameter, command, and arithmetic substitutions are performed
+before it is printed.
+The default is
+.Sq +\ \& .
+.It Ev PWD
+The current working directory.
+May be unset or
+.Dv NULL
+if the shell doesn't know where it is.
+.It Ev RANDOM
+A random number generator.
+Every time
+.Ev RANDOM
+is referenced, it is assigned the next random number in the range
+0\-32767.
+By default,
+.Xr arc4random 3
+is used to produce values.
+If the variable
+.Ev RANDOM
+is assigned a value, the value is used as the seed to
+.Xr srand_deterministic 3
+and subsequent references of
+.Ev RANDOM
+produce a predictable sequence.
+.It Ev REPLY
+Default parameter for the
+.Ic read
+command if no names are given.
+Also used in
+.Ic select
+loops to store the value that is read from standard input.
+.It Ev SECONDS
+The number of seconds since the shell started or, if the parameter has been
+assigned an integer value, the number of seconds since the assignment plus the
+value that was assigned.
+.It Ev TERM
+The user's terminal type.
+If set, it will be used to determine the escape sequence used to
+clear the screen.
+.It Ev TMOUT
+If set to a positive integer in an interactive shell, it specifies the maximum
+number of seconds the shell will wait for input after printing the primary
+prompt
+.Pq Ev PS1 .
+If the time is exceeded, the shell exits.
+.It Ev TMPDIR
+The directory temporary shell files are created in.
+If this parameter is not
+set, or does not contain the absolute path of a writable directory, temporary
+files are created in
+.Pa /tmp .
+.It Ev VISUAL
+If set, this parameter controls the command-line editing mode for interactive
+shells.
+If the last component of the path specified in this parameter contains
+the string
+.Dq vi ,
+.Dq emacs ,
+or
+.Dq gmacs ,
+the
+.Xr vi 1 ,
+emacs, or gmacs (Gosling emacs) editing mode is enabled, respectively.
+See also the
+.Ev EDITOR
+parameter, above.
+.El
+.Ss Tilde expansion
+Tilde expansion, which is done in parallel with parameter substitution, is done
+on words starting with an unquoted
+.Ql ~ .
+The characters following the tilde, up to the first
+.Ql / ,
+if any, are assumed to be a login name.
+If the login name is empty,
+.Ql + ,
+or
+.Ql - ,
+the value of the
+.Ev HOME ,
+.Ev PWD ,
+or
+.Ev OLDPWD
+parameter is substituted, respectively.
+Otherwise, the password file is
+searched for the login name, and the tilde expression is substituted with the
+user's home directory.
+If the login name is not found in the password file or
+if any quoting or parameter substitution occurs in the login name, no
+substitution is performed.
+.Pp
+In parameter assignments
+(such as those preceding a simple-command or those occurring
+in the arguments of
+.Ic alias ,
+.Ic export ,
+.Ic readonly ,
+and
+.Ic typeset ) ,
+tilde expansion is done after any assignment
+(i.e. after the equals sign)
+or after an unquoted colon
+.Pq Sq \&: ;
+login names are also delimited by colons.
+.Pp
+The home directory of previously expanded login names are cached and re-used.
+The
+.Ic alias -d
+command may be used to list, change, and add to this cache (e.g.\&
+.Ic alias -d fac=/usr/local/facilities; cd ~fac/bin ) .
+.Ss Brace expansion (alternation)
+Brace expressions take the following form:
+.Bd -unfilled -offset indent
+.Sm off
+.Xo
+.Ar prefix No { Ar str1 No ,...,
+.Ar strN No } Ar suffix
+.Xc
+.Sm on
+.Ed
+.Pp
+The expressions are expanded to
+.Ar N
+words, each of which is the concatenation of
+.Ar prefix ,
+.Ar str Ns i ,
+and
+.Ar suffix
+(e.g.\&
+.Dq a{c,b{X,Y},d}e
+expands to four words:
+.Dq ace ,
+.Dq abXe ,
+.Dq abYe ,
+and
+.Dq ade ) .
+As noted in the example, brace expressions can be nested and the resulting
+words are not sorted.
+Brace expressions must contain an unquoted comma
+.Pq Sq \&,
+for expansion to occur (e.g.\&
+.Ic {}
+and
+.Ic {foo}
+are not expanded).
+Brace expansion is carried out after parameter substitution
+and before file name generation.
+.Ss File name patterns
+A file name pattern is a word containing one or more unquoted
+.Ql \&? ,
+.Ql * ,
+.Ql + ,
+.Ql @ ,
+or
+.Ql \&!
+characters or
+.Dq [..]
+sequences.
+Once brace expansion has been performed, the shell replaces file
+name patterns with the sorted names of all the files that match the pattern
+(if no files match, the word is left unchanged).
+The pattern elements have the following meaning:
+.Bl -tag -width Ds
+.It \&?
+Matches any single character.
+.It \&*
+Matches any sequence of characters.
+.It [..]
+Matches any of the characters inside the brackets.
+Ranges of characters can be
+specified by separating two characters by a
+.Ql -
+(e.g.\&
+.Dq [a0-9]
+matches the letter
+.Sq a
+or any digit).
+In order to represent itself, a
+.Ql -
+must either be quoted or the first or last character in the character list.
+Similarly, a
+.Ql \&]
+must be quoted or the first character in the list if it is to represent itself
+instead of the end of the list.
+Also, a
+.Ql \&!
+appearing at the start of the list has special meaning (see below), so to
+represent itself it must be quoted or appear later in the list.
+.Pp
+Within a bracket expression, the name of a
+.Em character class
+enclosed in
+.Sq [:
+and
+.Sq :]
+stands for the list of all characters belonging to that class.
+Supported character classes:
+.Bd -literal -offset indent
+alnum	cntrl	lower	space
+alpha	digit	print	upper
+blank	graph	punct	xdigit
+.Ed
+.Pp
+These match characters using the macros specified in
+.Xr isalnum 3 ,
+.Xr isalpha 3 ,
+and so on.
+A character class may not be used as an endpoint of a range.
+.It [!..]
+Like [..],
+except it matches any character not inside the brackets.
+.Sm off
+.It *( Ar pattern Ns | No ...| Ar pattern )
+.Sm on
+Matches any string of characters that matches zero or more occurrences of the
+specified patterns.
+Example: The pattern
+.Ic *(foo|bar)
+matches the strings
+.Dq ,
+.Dq foo ,
+.Dq bar ,
+.Dq foobarfoo ,
+etc.
+.Sm off
+.It +( Ar pattern Ns | No ...| Ar pattern )
+.Sm on
+Matches any string of characters that matches one or more occurrences of the
+specified patterns.
+Example: The pattern
+.Ic +(foo|bar)
+matches the strings
+.Dq foo ,
+.Dq bar ,
+.Dq foobar ,
+etc.
+.Sm off
+.It ?( Ar pattern Ns | No ...| Ar pattern )
+.Sm on
+Matches the empty string or a string that matches one of the specified
+patterns.
+Example: The pattern
+.Ic ?(foo|bar)
+only matches the strings
+.Dq ,
+.Dq foo ,
+and
+.Dq bar .
+.Sm off
+.It @( Ar pattern Ns | No ...| Ar pattern )
+.Sm on
+Matches a string that matches one of the specified patterns.
+Example: The pattern
+.Ic @(foo|bar)
+only matches the strings
+.Dq foo
+and
+.Dq bar .
+.Sm off
+.It !( Ar pattern Ns | No ...| Ar pattern )
+.Sm on
+Matches any string that does not match one of the specified patterns.
+Examples: The pattern
+.Ic !(foo|bar)
+matches all strings except
+.Dq foo
+and
+.Dq bar ;
+the pattern
+.Ic !(*)
+matches no strings; the pattern
+.Ic !(?)*\&
+matches all strings (think about it).
+.El
+.Pp
+Unlike most shells,
+.Nm ksh
+never matches
+.Sq \&.
+and
+.Sq .. .
+.Pp
+Note that none of the above pattern elements match either a period
+.Pq Sq \&.
+at the start of a file name or a slash
+.Pq Sq / ,
+even if they are explicitly used in a [..] sequence; also, the names
+.Sq \&.
+and
+.Sq ..
+are never matched, even by the pattern
+.Sq .* .
+.Pp
+If the
+.Ic markdirs
+option is set, any directories that result from file name generation are marked
+with a trailing
+.Ql / .
+.Ss Input/output redirection
+When a command is executed, its standard input, standard output, and standard
+error (file descriptors 0, 1, and 2, respectively) are normally inherited from
+the shell.
+Three exceptions to this are commands in pipelines, for which
+standard input and/or standard output are those set up by the pipeline,
+asynchronous commands created when job control is disabled, for which standard
+input is initially set to be from
+.Pa /dev/null ,
+and commands for which any of the following redirections have been specified:
+.Bl -tag -width Ds
+.It Cm > Ar file
+Standard output is redirected to
+.Ar file .
+If
+.Ar file
+does not exist, it is created; if it does exist, is a regular file, and the
+.Ic noclobber
+option is set, an error occurs; otherwise, the file is truncated.
+Note that this means the command
+.Ic cmd < foo > foo
+will open
+.Ar foo
+for reading and then truncate it when it opens it for writing, before
+.Ar cmd
+gets a chance to actually read
+.Ar foo .
+.It Cm >| Ar file
+Same as
+.Cm > ,
+except the file is truncated, even if the
+.Ic noclobber
+option is set.
+.It Cm >> Ar file
+Same as
+.Cm > ,
+except if
+.Ar file
+exists it is appended to instead of being truncated.
+Also, the file is opened
+in append mode, so writes always go to the end of the file (see
+.Xr open 2 ) .
+.It Cm < Ar file
+Standard input is redirected from
+.Ar file ,
+which is opened for reading.
+.It Cm <> Ar file
+Same as
+.Cm < ,
+except the file is opened for reading and writing.
+.It Cm << Ar marker
+After reading the command line containing this kind of redirection (called a
+.Dq here document ) ,
+the shell copies lines from the command source into a temporary file until a
+line matching
+.Ar marker
+is read.
+When the command is executed, standard input is redirected from the
+temporary file.
+If
+.Ar marker
+contains no quoted characters, the contents of the temporary file are processed
+as if enclosed in double quotes each time the command is executed, so
+parameter, command, and arithmetic substitutions are performed, along with
+backslash
+.Pq Sq \e
+escapes for
+.Ql $ ,
+.Ql ` ,
+.Ql \e ,
+and
+.Ql \enewline .
+If multiple here documents are used on the same command line, they are saved in
+order.
+.It Cm <<- Ar marker
+Same as
+.Cm << ,
+except leading tabs are stripped from lines in the here document.
+.It Cm <& Ar fd
+Standard input is duplicated from file descriptor
+.Ar fd .
+.Ar fd
+can be a single digit, indicating the number of an existing file descriptor;
+the letter
+.Ql p ,
+indicating the file descriptor associated with the output of the current
+co-process; or the character
+.Ql - ,
+indicating standard input is to be closed.
+.It Cm >& Ar fd
+Same as
+.Cm <& ,
+except the operation is done on standard output.
+.El
+.Pp
+In any of the above redirections, the file descriptor that is redirected
+(i.e. standard input or standard output)
+can be explicitly given by preceding the
+redirection with a single digit.
+Parameter, command, and arithmetic
+substitutions, tilde substitutions, and (if the shell is interactive)
+file name generation are all performed on the
+.Ar file ,
+.Ar marker ,
+and
+.Ar fd
+arguments of redirections.
+Note, however, that the results of any file name
+generation are only used if a single file is matched; if multiple files match,
+the word with the expanded file name generation characters is used.
+Note
+that in restricted shells, redirections which can create files cannot be used.
+.Pp
+For simple-commands, redirections may appear anywhere in the command; for
+compound-commands
+.Po
+.Ic if
+statements, etc.
+.Pc ,
+any redirections must appear at the end.
+Redirections are processed after
+pipelines are created and in the order they are given, so the following
+will print an error with a line number prepended to it:
+.Pp
+.D1 $ cat /foo/bar 2>&1 > /dev/null | cat -n
+.Ss Arithmetic expressions
+Integer arithmetic expressions can be used with the
+.Ic let
+command, inside $((..)) expressions, inside array references (e.g.\&
+.Ar name Ns Bq Ar expr ) ,
+as numeric arguments to the
+.Ic test
+command, and as the value of an assignment to an integer parameter.
+.Pp
+Expressions may contain alpha-numeric parameter identifiers, array references,
+and integer constants and may be combined with the following C operators
+(listed and grouped in increasing order of precedence):
+.Pp
+Unary operators:
+.Bd -literal -offset indent
++ - ! ~ ++ --
+.Ed
+.Pp
+Binary operators:
+.Bd -literal -offset indent
+,
+= *= /= %= += -= <<= >>= &= ^= |=
+||
+&&
+|
+^
+&
+== !=
+< <= >= >
+<< >>
++ -
+* / %
+.Ed
+.Pp
+Ternary operators:
+.Bd -literal -offset indent
+?: (precedence is immediately higher than assignment)
+.Ed
+.Pp
+Grouping operators:
+.Bd -literal -offset indent
+( )
+.Ed
+.Pp
+A parameter that is NULL or unset evaluates to 0.
+Integer constants may be specified with arbitrary bases using the notation
+.Ar base Ns # Ns Ar number ,
+where
+.Ar base
+is a decimal integer specifying the base, and
+.Ar number
+is a number in the specified base.
+Additionally,
+integers may be prefixed with
+.Sq 0X
+or
+.Sq 0x
+(specifying base 16)
+or
+.Sq 0
+(base 8)
+in all forms of arithmetic expressions,
+except as numeric arguments to the
+.Ic test
+command.
+.Pp
+The operators are evaluated as follows:
+.Bl -tag -width Ds -offset indent
+.It unary +
+Result is the argument (included for completeness).
+.It unary -
+Negation.
+.It \&!
+Logical NOT;
+the result is 1 if argument is zero, 0 if not.
+.It ~
+Arithmetic (bit-wise) NOT.
+.It ++
+Increment; must be applied to a parameter (not a literal or other expression).
+The parameter is incremented by 1.
+When used as a prefix operator, the result
+is the incremented value of the parameter; when used as a postfix operator, the
+result is the original value of the parameter.
+.It --
+Similar to
+.Ic ++ ,
+except the parameter is decremented by 1.
+.It \&,
+Separates two arithmetic expressions; the left-hand side is evaluated first,
+then the right.
+The result is the value of the expression on the right-hand side.
+.It =
+Assignment; the variable on the left is set to the value on the right.
+.It Xo
+.No *= /= += -= <<=
+.No >>= &= ^= |=
+.Xc
+Assignment operators.
+.Sm off
+.Ao Ar var Ac Xo
+.Aq Ar op
+.No = Aq Ar expr
+.Xc
+.Sm on
+is the same as
+.Sm off
+.Ao Ar var Ac Xo
+.No = Aq Ar var
+.Aq Ar op
+.Aq Ar expr ,
+.Xc
+.Sm on
+with any operator precedence in
+.Aq Ar expr
+preserved.
+For example,
+.Dq var1 *= 5 + 3
+is the same as specifying
+.Dq var1 = var1 * (5 + 3) .
+.It ||
+Logical OR;
+the result is 1 if either argument is non-zero, 0 if not.
+The right argument is evaluated only if the left argument is zero.
+.It &&
+Logical AND;
+the result is 1 if both arguments are non-zero, 0 if not.
+The right argument is evaluated only if the left argument is non-zero.
+.It |
+Arithmetic (bit-wise) OR.
+.It ^
+Arithmetic (bit-wise) XOR
+(exclusive-OR).
+.It &
+Arithmetic (bit-wise) AND.
+.It ==
+Equal; the result is 1 if both arguments are equal, 0 if not.
+.It !=
+Not equal; the result is 0 if both arguments are equal, 1 if not.
+.It <
+Less than; the result is 1 if the left argument is less than the right, 0 if
+not.
+.It <= >= >
+Less than or equal, greater than or equal, greater than.
+See
+.Ic < .
+.It << >>
+Shift left (right); the result is the left argument with its bits shifted left
+(right) by the amount given in the right argument.
+.It + - * /
+Addition, subtraction, multiplication, and division.
+.It %
+Remainder; the result is the remainder of the division of the left argument by
+the right.
+The sign of the result is unspecified if either argument is negative.
+.It Xo
+.Sm off
+.Aq Ar arg1 ?
+.Aq Ar arg2 :
+.Aq Ar arg3
+.Sm on
+.Xc
+If
+.Aq Ar arg1
+is non-zero, the result is
+.Aq Ar arg2 ;
+otherwise the result is
+.Aq Ar arg3 .
+.El
+.Ss Co-processes
+A co-process, which is a pipeline created with the
+.Sq |&
+operator, is an asynchronous process that the shell can both write to (using
+.Ic print -p )
+and read from (using
+.Ic read -p ) .
+The input and output of the co-process can also be manipulated using
+.Cm >&p
+and
+.Cm <&p
+redirections, respectively.
+Once a co-process has been started, another can't
+be started until the co-process exits, or until the co-process's input has been
+redirected using an
+.Ic exec Ar n Ns Cm >&p
+redirection.
+If a co-process's input is redirected in this way, the next
+co-process to be started will share the output with the first co-process,
+unless the output of the initial co-process has been redirected using an
+.Ic exec Ar n Ns Cm <&p
+redirection.
+.Pp
+Some notes concerning co-processes:
+.Bl -bullet
+.It
+The only way to close the co-process's input (so the co-process reads an
+end-of-file) is to redirect the input to a numbered file descriptor and then
+close that file descriptor e.g.\&
+.Ic exec 3>&p; exec 3>&- .
+.It
+In order for co-processes to share a common output, the shell must keep the
+write portion of the output pipe open.
+This means that end-of-file will not be
+detected until all co-processes sharing the co-process's output have exited
+(when they all exit, the shell closes its copy of the pipe).
+This can be
+avoided by redirecting the output to a numbered file descriptor (as this also
+causes the shell to close its copy).
+Note that this behaviour is slightly
+different from the original Korn shell which closes its copy of the write
+portion of the co-process output when the most recently started co-process
+(instead of when all sharing co-processes) exits.
+.It
+.Ic print -p
+will ignore
+.Dv SIGPIPE
+signals during writes if the signal is not being trapped or ignored; the same
+is true if the co-process input has been duplicated to another file descriptor
+and
+.Ic print -u Ns Ar n
+is used.
+.El
+.Ss Functions
+Functions are defined using either Korn shell
+.Ic function Ar function-name
+syntax or the Bourne/POSIX shell
+.Ar function-name Ns ()
+syntax (see below for the difference between the two forms).
+Functions are like
+.Li .-scripts
+(i.e. scripts sourced using the
+.Sq \&.
+built-in)
+in that they are executed in the current environment.
+However, unlike
+.Li .-scripts ,
+shell arguments (i.e. positional parameters $1, $2, etc.)\&
+are never visible inside them.
+When the shell is determining the location of a command, functions
+are searched after special built-in commands, before regular and
+non-regular built-ins, and before the
+.Ev PATH
+is searched.
+.Pp
+An existing function may be deleted using
+.Ic unset Fl f Ar function-name .
+A list of functions can be obtained using
+.Ic typeset +f
+and the function definitions can be listed using
+.Ic typeset -f .
+The
+.Ic autoload
+command (which is an alias for
+.Ic typeset -fu )
+may be used to create undefined functions: when an undefined function is
+executed, the shell searches the path specified in the
+.Ev FPATH
+parameter for a file with the same name as the function, which, if found, is
+read and executed.
+If after executing the file the named function is found to
+be defined, the function is executed; otherwise, the normal command search is
+continued (i.e. the shell searches the regular built-in command table and
+.Ev PATH ) .
+Note that if a command is not found using
+.Ev PATH ,
+an attempt is made to autoload a function using
+.Ev FPATH
+(this is an undocumented feature of the original Korn shell).
+.Pp
+Functions can have two attributes,
+.Dq trace
+and
+.Dq export ,
+which can be set with
+.Ic typeset -ft
+and
+.Ic typeset -fx ,
+respectively.
+When a traced function is executed, the shell's
+.Ic xtrace
+option is turned on for the function's duration; otherwise, the
+.Ic xtrace
+option is turned off.
+The
+.Dq export
+attribute of functions is currently not used.
+In the original Korn shell,
+exported functions are visible to shell scripts that are executed.
+.Pp
+Since functions are executed in the current shell environment, parameter
+assignments made inside functions are visible after the function completes.
+If this is not the desired effect, the
+.Ic typeset
+command can be used inside a function to create a local parameter.
+Note that special parameters (e.g.\&
+.Ic \&$$ , $! )
+can't be scoped in this way.
+.Pp
+The exit status of a function is that of the last command executed in the
+function.
+A function can be made to finish immediately using the
+.Ic return
+command; this may also be used to explicitly specify the exit status.
+.Pp
+Functions defined with the
+.Ic function
+reserved word are treated differently in the following ways from functions
+defined with the
+.Ic ()
+notation:
+.Bl -bullet
+.It
+The $0 parameter is set to the name of the function
+(Bourne-style functions leave $0 untouched).
+.It
+Parameter assignments preceding function calls are not kept in the shell
+environment (executing Bourne-style functions will keep assignments).
+.It
+.Ev OPTIND
+is saved/reset and restored on entry and exit from the function so
+.Ic getopts
+can be used properly both inside and outside the function (Bourne-style
+functions leave
+.Ev OPTIND
+untouched, so using
+.Ic getopts
+inside a function interferes with using
+.Ic getopts
+outside the function).
+.El
+.Ss POSIX mode
+The shell is intended to be POSIX compliant;
+however, in some cases, POSIX behaviour is contrary either to
+the original Korn shell behaviour or to user convenience.
+How the shell behaves in these cases is determined by the state of the
+.Ic posix
+option
+.Pq Ic set -o posix .
+If it is on, the POSIX behaviour is followed; otherwise, it is not.
+The
+.Ic posix
+option is set automatically when the shell starts up if the environment
+contains the
+.Ev POSIXLY_CORRECT
+parameter.
+The shell can also be compiled so that it is in POSIX mode by default;
+however, this is usually not desirable.
+.Pp
+The following is a list of things that are affected by the state of the
+.Ic posix
+option:
+.Bl -bullet
+.It
+.Ic kill -l
+output.
+In POSIX mode, only signal names are listed (in a single line);
+in non-POSIX mode,
+signal numbers, names, and descriptions are printed (in columns).
+.It
+.Ic echo
+options.
+In POSIX mode,
+.Fl e
+and
+.Fl E
+are not treated as options, but printed like other arguments;
+in non-POSIX mode, these options control the interpretation
+of backslash sequences.
+.It
+.Ic fg
+exit status.
+In POSIX mode, the exit status is 0 if no errors occur;
+in non-POSIX mode, the exit status is that of the last foregrounded job.
+.It
+.Ic eval
+exit status.
+If
+.Ic eval
+gets to see an empty command (i.e.\&
+.Ic eval `false` ) ,
+its exit status in POSIX mode will be 0.
+In non-POSIX mode,
+it will be the exit status of the last command substitution that was
+done in the processing of the arguments to
+.Ic eval
+(or 0 if there were no command substitutions).
+.It
+.Ic getopts .
+In POSIX mode, options must start with a
+.Ql - ;
+in non-POSIX mode, options can start with either
+.Ql -
+or
+.Ql + .
+.It
+Brace expansion (also known as alternation).
+In POSIX mode, brace expansion is disabled;
+in non-POSIX mode, brace expansion is enabled.
+Note that
+.Ic set -o posix
+(or setting the
+.Ev POSIXLY_CORRECT
+parameter) automatically turns the
+.Ic braceexpand
+option off; however, it can be explicitly turned on later.
+.It
+.Ic set - .
+In POSIX mode, this does not clear the
+.Ic verbose
+or
+.Ic xtrace
+options; in non-POSIX mode, it does.
+.It
+.Ic set
+exit status.
+In POSIX mode, the exit status of
+.Ic set
+is 0 if there are no errors;
+in non-POSIX mode, the exit status is that of any
+command substitutions performed in generating the
+.Ic set
+command.
+For example,
+.Ic set -- `false`; echo $?\&
+prints 0 in POSIX mode, 1 in non-POSIX mode.
+This construct is used in most shell scripts that use the old
+.Xr getopt 1
+command.
+.It
+Argument expansion of the
+.Ic alias ,
+.Ic export ,
+.Ic readonly ,
+and
+.Ic typeset
+commands.
+In POSIX mode, normal argument expansion is done; in non-POSIX mode,
+field splitting, file globbing, brace expansion, and (normal) tilde expansion
+are turned off, while assignment tilde expansion is turned on.
+.It
+Signal specification.
+In POSIX mode, signals can be specified as digits, only
+if signal numbers match POSIX values
+(i.e. HUP=1, INT=2, QUIT=3, ABRT=6, KILL=9, ALRM=14, and TERM=15);
+in non-POSIX mode, signals can always be digits.
+.It
+Alias expansion.
+In POSIX mode, alias expansion is only carried out when reading command words;
+in non-POSIX mode, alias expansion is carried out on any
+word following an alias that ended in a space.
+For example, the following
+.Ic for
+loop uses parameter
+.Sq i
+in POSIX mode and
+.Sq j
+in non-POSIX mode:
+.Bd -literal -offset indent
+alias a='for ' i='j'
+a i in 1 2; do echo i=$i j=$j; done
+.Ed
+.It
+.Ic test .
+In POSIX mode, the expression
+.Sq Fl t
+(preceded by some number of
+.Sq \&!
+arguments) is always true as it is a non-zero length string;
+in non-POSIX mode, it tests if file descriptor 1 is a
+.Xr tty 4
+(i.e. the
+.Ar fd
+argument to the
+.Fl t
+test may be left out and defaults to 1).
+.El
+.Ss Strict Bourne shell mode
+When the
+.Ic sh
+option is enabled (see the
+.Ic set
+command),
+.Nm
+will behave like
+.Xr sh 1
+in the following ways:
+.Bl -bullet
+.It
+The parameter
+.Ic $_
+is not set to:
+.Pp
+.Bl -dash -compact
+.It
+the expanded alias' full program path after entering commands
+that are tracked aliases
+.It
+the last argument on the command line after entering external
+commands
+.It
+the file that changed when
+.Ev MAILPATH
+is set to monitor a mailbox
+.El
+.It
+File descriptors are left untouched when executing
+.Ic exec
+with no arguments.
+.It
+Backslash-escaped special characters are not substituted in
+.Ev PS1 .
+.It
+Sequences of
+.Sq ((...))
+are not interpreted as arithmetic expressions.
+.El
+.Ss Command execution
+After evaluation of command-line arguments, redirections, and parameter
+assignments, the type of command is determined: a special built-in, a
+function, a regular built-in, or the name of a file to execute found using the
+.Ev PATH
+parameter.
+The checks are made in the above order.
+Special built-in commands differ from other commands in that the
+.Ev PATH
+parameter is not used to find them, an error during their execution can
+cause a non-interactive shell to exit, and parameter assignments that are
+specified before the command are kept after the command completes.
+Just to confuse things, if the
+.Ic posix
+option is turned off (see the
+.Ic set
+command below), some special commands are very special in that no field
+splitting, file globbing, brace expansion, nor tilde expansion is performed
+on arguments that look like assignments.
+Regular built-in commands are different only in that the
+.Ev PATH
+parameter is not used to find them.
+.Pp
+The original
+.Nm ksh
+and POSIX differ somewhat in which commands are considered
+special or regular:
+.Pp
+POSIX special commands
+.Pp
+.Ic \&. , \&: , break , continue ,
+.Ic eval , exec , exit , export ,
+.Ic readonly , return , set , shift ,
+.Ic times , trap , unset
+.Pp
+Additional
+.Nm
+special commands
+.Pp
+.Ic builtin , typeset
+.Pp
+Very special commands
+.Pq when POSIX mode is off
+.Pp
+.Ic alias , readonly , set , typeset
+.Pp
+POSIX regular commands
+.Pp
+.Ic alias , bg , cd , command ,
+.Ic false , fc , fg , getopts ,
+.Ic jobs , kill , pwd , read ,
+.Ic true , umask , unalias , wait
+.Pp
+Additional
+.Nm
+regular commands
+.Pp
+.Ic \&[ , echo , let ,
+.Ic print , suspend , test ,
+.Ic ulimit , whence
+.Pp
+Once the type of command has been determined, any command-line parameter
+assignments are performed and exported for the duration of the command.
+.Pp
+The following describes the special and regular built-in commands:
+.Pp
+.Bl -tag -width Ds -compact
+.It Ic \&. Ar file Op Ar arg ...
+Execute the commands in
+.Ar file
+in the current environment.
+The file is searched for in the directories of
+.Ev PATH .
+If arguments are given, the positional parameters may be used to access them
+while
+.Ar file
+is being executed.
+If no arguments are given, the positional parameters are
+those of the environment the command is used in.
+.Pp
+.It Ic \&: Op Ar ...
+The null command.
+Exit status is set to zero.
+.Pp
+.It Xo Ic alias
+.Oo Fl d | t Oo Fl r Oc |
+.Cm +-x Oc
+.Op Fl p
+.Op Cm +
+.Oo Ar name
+.Op Ns = Ns Ar value
+.Ar ... Oc
+.Xc
+Without arguments,
+.Ic alias
+lists all aliases.
+For any name without a value, the existing alias is listed.
+Any name with a value defines an alias (see
+.Sx Aliases
+above).
+.Pp
+When listing aliases, one of two formats is used.
+Normally, aliases are listed as
+.Ar name Ns = Ns Ar value ,
+where
+.Ar value
+is quoted.
+If options were preceded with
+.Ql + ,
+or a lone
+.Ql +
+is given on the command line, only
+.Ar name
+is printed.
+.Pp
+The
+.Fl d
+option causes directory aliases, which are used in tilde expansion, to be
+listed or set (see
+.Sx Tilde expansion
+above).
+.Pp
+If the
+.Fl p
+option is used, each alias is prefixed with the string
+.Dq alias\ \& .
+.Pp
+The
+.Fl t
+option indicates that tracked aliases are to be listed/set (values specified on
+the command line are ignored for tracked aliases).
+The
+.Fl r
+option indicates that all tracked aliases are to be reset.
+.Pp
+The
+.Fl x
+option sets
+.Pq Ic +x No clears
+the export attribute of an alias or, if no names are given, lists the aliases
+with the export attribute (exporting an alias has no effect).
+.Pp
+.It Ic bg Op Ar job ...
+Resume the specified stopped job(s) in the background.
+If no jobs are specified,
+.Ic %+
+is assumed.
+See
+.Sx Job control
+below for more information.
+.Pp
+.It Ic bind Op Fl l
+The current bindings are listed.
+If the
+.Fl l
+flag is given,
+.Ic bind
+instead lists the names of the functions to which keys may be bound.
+See
+.Sx Emacs editing mode
+for more information.
+.Pp
+.It Xo Ic bind Op Fl m
+.Ar string Ns = Ns Op Ar substitute
+.Ar ...
+.Xc
+.It Xo Ic bind
+.Ar string Ns = Ns Op Ar editing-command
+.Ar ...
+.Xc
+In
+.Sx Emacs editing mode ,
+the specified editing command is bound to the given
+.Ar string .
+Future input of the
+.Ar string
+will cause the editing command to be immediately invoked.
+Bindings have no effect in
+.Sx Vi editing mode .
+.Pp
+If the
+.Fl m
+flag is given, the specified input
+.Ar string
+will afterwards be immediately replaced by the given
+.Ar substitute
+string, which may contain editing commands.
+Control characters may be written using caret notation.
+For example, ^X represents Control-X.
+.Pp
+If a certain character occurs as the first character of any bound
+multi-character
+.Ar string
+sequence, that character becomes a command prefix character.
+Any character sequence that starts with a command prefix character
+but that is not bound to a command or substitute
+is implicitly considered as bound to the
+.Sq error
+command.
+By default, two command prefix characters exist:
+Escape
+.Pq ^[
+and Control-X
+.Pq ^X .
+.Pp
+The following default bindings show how the arrow keys
+on an ANSI terminal or xterm are bound
+(of course some escape sequences won't work out quite this nicely):
+.Bd -literal -offset indent
+bind '^[[A'=up-history
+bind '^[[B'=down-history
+bind '^[[C'=forward-char
+bind '^[[D'=backward-char
+.Ed
+.Pp
+.It Ic break Op Ar level
+Exit the
+.Ar level Ns th
+inner-most
+.Ic for ,
+.Ic select ,
+.Ic until ,
+or
+.Ic while
+loop.
+.Ar level
+defaults to 1.
+.Pp
+.It Ic builtin Ar command Op Ar arg ...
+Execute the built-in command
+.Ar command .
+.Pp
+.It Xo
+.Ic cd
+.Op Fl LP
+.Op Ar dir
+.Xc
+Set the working directory to
+.Ar dir .
+If the parameter
+.Ev CDPATH
+is set, it lists the search path for the directory containing
+.Ar dir .
+A
+.Dv NULL
+path means the current directory.
+If
+.Ar dir
+is found in any component of the
+.Ev CDPATH
+search path other than the
+.Dv NULL
+path, the name of the new working directory will be written to standard output.
+If
+.Ar dir
+is missing, the home directory
+.Ev HOME
+is used.
+If
+.Ar dir
+is
+.Ql - ,
+the previous working directory is used (see the
+.Ev OLDPWD
+parameter).
+.Pp
+If the
+.Fl L
+option (logical path) is used or if the
+.Ic physical
+option isn't set (see the
+.Ic set
+command below), references to
+.Sq ..
+in
+.Ar dir
+are relative to the path used to get to the directory.
+If the
+.Fl P
+option (physical path) is used or if the
+.Ic physical
+option is set,
+.Sq ..
+is relative to the filesystem directory tree.
+The
+.Ev PWD
+and
+.Ev OLDPWD
+parameters are updated to reflect the current and old working directory,
+respectively.
+.Pp
+.It Xo
+.Ic cd
+.Op Fl LP
+.Ar old new
+.Xc
+The string
+.Ar new
+is substituted for
+.Ar old
+in the current directory, and the shell attempts to change to the new
+directory.
+.Pp
+.It Xo
+.Ic command
+.Op Fl pVv
+.Ar cmd
+.Op Ar arg ...
+.Xc
+If neither the
+.Fl v
+nor
+.Fl V
+option is given,
+.Ar cmd
+is executed exactly as if
+.Ic command
+had not been specified, with two exceptions:
+firstly,
+.Ar cmd
+cannot be an alias or a shell function;
+and secondly, special built-in commands lose their specialness
+(i.e. redirection and utility errors do not cause the shell to
+exit, and command assignments are not permanent).
+.Pp
+If the
+.Fl p
+option is given, a default search path is used instead of the current value of
+.Ev PATH
+(the actual value of the default path is system dependent: on
+POSIX-ish systems, it is the value returned by
+.Ic getconf PATH ) .
+Nevertheless, reserved words, aliases, shell functions, and
+builtin commands are still found before external commands.
+.Pp
+If the
+.Fl v
+option is given, instead of executing
+.Ar cmd ,
+information about what would be executed is given (and the same is done for
+.Ar arg ... ) .
+For special and regular built-in commands and functions, their names are simply
+printed; for aliases, a command that defines them is printed; and for commands
+found by searching the
+.Ev PATH
+parameter, the full path of the command is printed.
+If no command is found
+(i.e. the path search fails), nothing is printed and
+.Ic command
+exits with a non-zero status.
+The
+.Fl V
+option is like the
+.Fl v
+option, except it is more verbose.
+.Pp
+.It Ic continue Op Ar level
+Jumps to the beginning of the
+.Ar level Ns th
+inner-most
+.Ic for ,
+.Ic select ,
+.Ic until ,
+or
+.Ic while
+loop.
+.Ar level
+defaults to 1.
+.Pp
+.It Xo
+.Ic echo
+.Op Fl Een
+.Op Ar arg ...
+.Xc
+Prints its arguments (separated by spaces) followed by a newline, to the
+standard output.
+The newline is suppressed if any of the arguments contain the
+backslash sequence
+.Ql \ec .
+See the
+.Ic print
+command below for a list of other backslash sequences that are recognized.
+.Pp
+The options are provided for compatibility with
+.Bx
+shell scripts.
+The
+.Fl n
+option suppresses the trailing newline,
+.Fl e
+enables backslash interpretation (a no-op, since this is normally done), and
+.Fl E
+suppresses backslash interpretation.
+If the
+.Ic posix
+option is set, only the first argument is treated as an option, and only
+if it is exactly
+.Dq -n .
+.Pp
+.It Ic eval Ar command ...
+The arguments are concatenated (with spaces between them) to form a single
+string which the shell then parses and executes in the current environment.
+.Pp
+.It Xo
+.Ic exec
+.Op Ar command Op Ar arg ...
+.Xc
+The command is executed without forking, replacing the shell process.
+.Pp
+If no command is given except for I/O redirection, the I/O redirection is
+permanent and the shell is
+not replaced.
+Any file descriptors greater than 2 which are opened or
+.Xr dup 2 Ns 'd
+in this way are not made available to other executed commands (i.e. commands
+that are not built-in to the shell).
+Note that the Bourne shell differs here;
+it does pass these file descriptors on.
+.Pp
+.It Ic exit Op Ar status
+The shell exits with the specified exit status.
+If
+.Ar status
+is not specified, the exit status is the current value of the
+.Ic $?\&
+parameter.
+.Pp
+.It Xo
+.Ic export
+.Op Fl p
+.Op Ar parameter Ns Op = Ns Ar value
+.Xc
+Sets the export attribute of the named parameters.
+Exported parameters are passed in the environment to executed commands.
+If values are specified, the named parameters are also assigned.
+.Pp
+If no parameters are specified, the names of all parameters with the export
+attribute are printed one per line, unless the
+.Fl p
+option is used, in which case
+.Ic export
+commands defining all exported parameters, including their values, are printed.
+.Pp
+.It Ic false
+A command that exits with a non-zero status.
+.Pp
+.It Xo
+.Ic fc
+.Oo
+.Fl e Ar editor |
+.Fl l Op Fl n
+.Oc
+.Op Fl r
+.Op Ar first Op Ar last
+.Xc
+Fix command.
+.Ar first
+and
+.Ar last
+select commands from the history.
+Commands can be selected by history number
+or a string specifying the most recent command starting with that string.
+The
+.Fl l
+option lists the command on standard output, and
+.Fl n
+inhibits the default command numbers.
+The
+.Fl r
+option reverses the order of the list.
+Without
+.Fl l ,
+the selected commands are edited by the editor specified with the
+.Fl e
+option, or if no
+.Fl e
+is specified, the editor specified by the
+.Ev FCEDIT
+parameter (if this parameter is not set,
+.Pa /bin/ed
+is used), and then executed by the shell.
+.Pp
+.It Xo
+.Ic fc Fl s
+.Op Fl g
+.Op Ar old Ns = Ns Ar new
+.Op Ar prefix
+.Xc
+Re-execute the most recent command beginning with
+.Ar prefix ,
+or the previous command if no
+.Ar prefix
+is specified,
+performing the optional substitution of
+.Ar old
+with
+.Ar new .
+If
+.Fl g
+is specified, all occurrences of
+.Ar old
+are replaced with
+.Ar new .
+The editor is not invoked when the
+.Fl s
+flag is used.
+The obsolescent equivalent
+.Dq Fl e No -
+is also accepted.
+This command is usually accessed with the predefined
+.Ic alias r='fc -s' .
+.Pp
+.It Ic fg Op Ar job ...
+Resume the specified job(s) in the foreground.
+If no jobs are specified,
+.Ic %+
+is assumed.
+See
+.Sx Job control
+below for more information.
+.Pp
+.It Xo
+.Ic getopts
+.Ar optstring name
+.Op Ar arg ...
+.Xc
+Used by shell procedures to parse the specified arguments (or positional
+parameters, if no arguments are given) and to check for legal options.
+.Ar optstring
+contains the option letters that
+.Ic getopts
+is to recognize.
+If a letter is followed by a colon, the option is expected to
+have an argument.
+Options that do not take arguments may be grouped in a single argument.
+If an option takes an argument and the option character is not the
+last character of the argument it is found in, the remainder of the argument is
+taken to be the option's argument; otherwise, the next argument is the option's
+argument.
+.Pp
+Each time
+.Ic getopts
+is invoked, it places the next option in the shell parameter
+.Ar name
+and the index of the argument to be processed by the next call to
+.Ic getopts
+in the shell parameter
+.Ev OPTIND .
+If the option was introduced with a
+.Ql + ,
+the option placed in
+.Ar name
+is prefixed with a
+.Ql + .
+When an option requires an argument,
+.Ic getopts
+places it in the shell parameter
+.Ev OPTARG .
+.Pp
+When an illegal option or a missing option argument is encountered, a question
+mark or a colon is placed in
+.Ar name
+(indicating an illegal option or missing argument, respectively) and
+.Ev OPTARG
+is set to the option character that caused the problem.
+Furthermore, if
+.Ar optstring
+does not begin with a colon, a question mark is placed in
+.Ar name ,
+.Ev OPTARG
+is unset, and an error message is printed to standard error.
+.Pp
+When the end of the options is encountered,
+.Ic getopts
+exits with a non-zero exit status.
+Options end at the first (non-option
+argument) argument that does not start with a
+.Ql - ,
+or when a
+.Ql --
+argument is encountered.
+.Pp
+Option parsing can be reset by setting
+.Ev OPTIND
+to 1 (this is done automatically whenever the shell or a shell procedure is
+invoked).
+.Pp
+Warning: Changing the value of the shell parameter
+.Ev OPTIND
+to a value other than 1, or parsing different sets of arguments without
+resetting
+.Ev OPTIND ,
+may lead to unexpected results.
+.Pp
+.It Xo
+.Ic hash
+.Op Fl r
+.Op Ar name ...
+.Xc
+Without arguments, any hashed executable command pathnames are listed.
+The
+.Fl r
+option causes all hashed commands to be removed from the hash table.
+Each
+.Ar name
+is searched as if it were a command name and added to the hash table if it is
+an executable command.
+.Pp
+.It Xo
+.Ic jobs
+.Op Fl lnp
+.Op Ar job ...
+.Xc
+Display information about the specified job(s); if no jobs are specified, all
+jobs are displayed.
+The
+.Fl n
+option causes information to be displayed only for jobs that have changed
+state since the last notification.
+If the
+.Fl l
+option is used, the process ID of each process in a job is also listed.
+The
+.Fl p
+option causes only the process group of each job to be printed.
+See
+.Sx Job control
+below for the format of
+.Ar job
+and the displayed job.
+.Pp
+.It Xo
+.Ic kill
+.Oo Fl s Ar signame |
+.No - Ns Ar signum |
+.No - Ns Ar signame Oc
+.No { Ar job | pid | pgrp No }
+.Ar ...
+.Xc
+Send the specified signal to the specified jobs, process IDs, or process
+groups.
+If no signal is specified, the
+.Dv TERM
+signal is sent.
+If a job is specified, the signal is sent to the job's process group.
+See
+.Sx Job control
+below for the format of
+.Ar job .
+.Pp
+.It Xo
+.Ic kill
+.Fl l
+.Op Ar exit-status ...
+.Xc
+Print the signal name corresponding to
+.Ar exit-status .
+If no arguments are specified, a list of all the signals, their numbers, and
+a short description of them are printed.
+.Pp
+.It Ic let Op Ar expression ...
+Each expression is evaluated (see
+.Sx Arithmetic expressions
+above).
+If all expressions are successfully evaluated, the exit status is 0 (1)
+if the last expression evaluated to non-zero (zero).
+If an error occurs during
+the parsing or evaluation of an expression, the exit status is greater than 1.
+Since expressions may need to be quoted,
+.No (( Ar expr No ))
+is syntactic sugar for
+.No let \&" Ns Ar expr Ns \&" .
+.Pp
+.It Xo
+.Ic print
+.Oo
+.Fl nprsu Ns Oo Ar n Oc |
+.Fl R Op Fl en
+.Oc
+.Op Ar argument ...
+.Xc
+.Ic print
+prints its arguments on the standard output, separated by spaces and
+terminated with a newline.
+The
+.Fl n
+option suppresses the newline.
+By default, certain C escapes are translated.
+These include
+.Ql \eb ,
+.Ql \ef ,
+.Ql \en ,
+.Ql \er ,
+.Ql \et ,
+.Ql \ev ,
+and
+.Ql \e0###
+.Po
+.Ql #
+is an octal digit, of which there may be 0 to 3
+.Pc .
+.Ql \ec
+is equivalent to using the
+.Fl n
+option.
+.Ql \e
+expansion may be inhibited with the
+.Fl r
+option.
+The
+.Fl s
+option prints to the history file instead of standard output; the
+.Fl u
+option prints to file descriptor
+.Ar n
+.Po
+.Ar n
+defaults to 1 if omitted
+.Pc ;
+and the
+.Fl p
+option prints to the co-process (see
+.Sx Co-processes
+above).
+.Pp
+The
+.Fl R
+option is used to emulate, to some degree, the
+.Bx
+.Xr echo 1
+command, which does not process
+.Ql \e
+sequences unless the
+.Fl e
+option is given.
+As above, the
+.Fl n
+option suppresses the trailing newline.
+.Pp
+.It Ic pwd Op Fl LP
+Print the present working directory.
+If the
+.Fl L
+option is used or if the
+.Ic physical
+option isn't set (see the
+.Ic set
+command below), the logical path is printed (i.e. the path used to
+.Ic cd
+to the current directory).
+If the
+.Fl P
+option (physical path) is used or if the
+.Ic physical
+option is set, the path determined from the filesystem (by following
+.Sq ..
+directories to the root directory) is printed.
+.Pp
+.It Xo
+.Ic read
+.Op Fl prsu Ns Op Ar n
+.Op Ar parameter ...
+.Xc
+Reads a line of input from the standard input, separates the line into fields
+using the
+.Ev IFS
+parameter (see
+.Sx Substitution
+above), and assigns each field to the specified parameters.
+If there are more parameters than fields, the extra parameters are set to
+.Dv NULL ,
+or alternatively, if there are more fields than parameters, the last parameter
+is assigned the remaining fields (inclusive of any separating spaces).
+If no parameters are specified, the
+.Ev REPLY
+parameter is used.
+If the input line ends in a backslash and the
+.Fl r
+option was not used, the backslash and the newline are stripped and more input
+is read.
+If no input is read,
+.Ic read
+exits with a non-zero status.
+.Pp
+The first parameter may have a question mark and a string appended to it, in
+which case the string is used as a prompt (printed to standard error before
+any input is read) if the input is a
+.Xr tty 4
+(e.g.\&
+.Ic read nfoo?'number of foos: ' ) .
+.Pp
+The
+.Fl u Ns Ar n
+and
+.Fl p
+options cause input to be read from file descriptor
+.Ar n
+.Pf ( Ar n
+defaults to 0 if omitted)
+or the current co-process (see
+.Sx Co-processes
+above for comments on this), respectively.
+If the
+.Fl s
+option is used, input is saved to the history file.
+.Pp
+.It Xo
+.Ic readonly
+.Op Fl p
+.Oo Ar parameter
+.Op Ns = Ns Ar value
+.Ar ... Oc
+.Xc
+Sets the read-only attribute of the named parameters.
+If values are given,
+parameters are set to them before setting the attribute.
+Once a parameter is
+made read-only, it cannot be unset and its value cannot be changed.
+.Pp
+If no parameters are specified, the names of all parameters with the read-only
+attribute are printed one per line, unless the
+.Fl p
+option is used, in which case
+.Ic readonly
+commands defining all read-only parameters, including their values, are
+printed.
+.Pp
+.It Ic return Op Ar status
+Returns from a function or
+.Ic .\&
+script, with exit status
+.Ar status .
+If no
+.Ar status
+is given, the exit status of the last executed command is used.
+If used outside of a function or
+.Ic .\&
+script, it has the same effect as
+.Ic exit .
+Note that
+.Nm ksh
+treats both profile and
+.Ev ENV
+files as
+.Ic .\&
+scripts, while the original Korn shell only treats profiles as
+.Ic .\&
+scripts.
+.Pp
+.It Xo
+.Ic set Op Ic +-abCefhkmnpsuvXx
+.Op Ic +-o Ar option
+.Op Ic +-A Ar name
+.Op Fl -
+.Op Ar arg ...
+.Xc
+The
+.Ic set
+command can be used to set
+.Pq Ic -
+or clear
+.Pq Ic +
+shell options, set the positional parameters, or set an array parameter.
+Options can be changed using the
+.Cm +-o Ar option
+syntax, where
+.Ar option
+is the long name of an option, or using the
+.Cm +- Ns Ar letter
+syntax, where
+.Ar letter
+is the option's single letter name (not all options have a single letter name).
+The following table lists both option letters (if they exist) and long names
+along with a description of what the option does:
+.Bl -tag -width 15n
+.It Fl A Ar name
+Sets the elements of the array parameter
+.Ar name
+to
+.Ar arg ...
+If
+.Fl A
+is used, the array is reset (i.e. emptied) first; if
+.Ic +A
+is used, the first N elements are set (where N is the number of arguments);
+the rest are left untouched.
+.It Fl a | Ic allexport
+All new parameters are created with the export attribute.
+.It Fl b | Ic notify
+Print job notification messages asynchronously, instead of just before the
+prompt.
+Only used if job control is enabled
+.Pq Fl m .
+.It Fl C | Ic noclobber
+Prevent
+.Cm >
+redirection from overwriting existing files.
+Instead,
+.Cm >|
+must be used to force an overwrite.
+.It Fl e | Ic errexit
+Exit (after executing the
+.Dv ERR
+trap) as soon as an error occurs or a command fails (i.e. exits with a
+non-zero status).
+This does not apply to commands whose exit status is
+explicitly tested by a shell construct such as
+.Ic if ,
+.Ic until ,
+.Ic while ,
+or
+.Ic !\&
+statements.
+For
+.Ic &&
+or
+.Ic || ,
+only the status of the last command is tested.
+.It Fl f | Ic noglob
+Do not expand file name patterns.
+.It Fl h | Ic trackall
+Create tracked aliases for all executed commands (see
+.Sx Aliases
+above).
+Enabled by default for non-interactive shells.
+.It Fl k | Ic keyword
+Parameter assignments are recognized anywhere in a command.
+.It Fl m | Ic monitor
+Enable job control (default for interactive shells).
+.It Fl n | Ic noexec
+Do not execute any commands.
+Useful for checking the syntax of scripts
+(ignored if interactive).
+.It Fl p | Ic privileged
+The shell is a privileged shell.
+It is set automatically if, when the shell starts,
+the real UID or GID does not match
+the effective UID (EUID) or GID (EGID), respectively.
+See above for a description of what this means.
+.It Fl s | Ic stdin
+If used when the shell is invoked, commands are read from standard input.
+Set automatically if the shell is invoked with no arguments.
+.Pp
+When
+.Fl s
+is used with the
+.Ic set
+command it causes the specified arguments to be sorted before assigning them to
+the positional parameters (or to array
+.Ar name ,
+if
+.Fl A
+is used).
+.It Fl u | Ic nounset
+Referencing of an unset parameter is treated as an error, unless one of the
+.Ql - ,
+.Ql + ,
+or
+.Ql =
+modifiers is used.
+.It Fl v | Ic verbose
+Write shell input to standard error as it is read.
+.It Fl X | Ic markdirs
+Mark directories with a trailing
+.Ql /
+during file name generation.
+.It Fl x | Ic xtrace
+Print commands and parameter assignments when they are executed, preceded by
+the value of
+.Ev PS4 .
+.It Ic bgnice
+Background jobs are run with lower priority.
+.It Ic braceexpand
+Enable brace expansion (a.k.a. alternation).
+.It Ic csh-history
+Enables a subset of
+.Xr csh 1 Ns -style
+history editing using the
+.Ql !\&
+character.
+.It Ic emacs
+Enable BRL emacs-like command-line editing (interactive shells only); see
+.Sx Emacs editing mode .
+.It Ic gmacs
+Enable gmacs-like command-line editing (interactive shells only).
+Currently identical to emacs editing except that transpose (^T) acts slightly
+differently.
+.It Ic ignoreeof
+The shell will not (easily) exit when end-of-file is read;
+.Ic exit
+must be used.
+To avoid infinite loops, the shell will exit if
+.Dv EOF
+is read 13 times in a row.
+.It Ic interactive
+The shell is an interactive shell.
+This option can only be used when the shell is invoked.
+See above for a description of what this means.
+.It Ic login
+The shell is a login shell.
+This option can only be used when the shell is invoked.
+See above for a description of what this means.
+.It Ic nohup
+Do not kill running jobs with a
+.Dv SIGHUP
+signal when a login shell exits.
+Currently set by default;
+this is different from the original Korn shell (which
+doesn't have this option, but does send the
+.Dv SIGHUP
+signal).
+.It Ic nolog
+No effect.
+In the original Korn shell, this prevents function definitions from
+being stored in the history file.
+.It Ic physical
+Causes the
+.Ic cd
+and
+.Ic pwd
+commands to use
+.Dq physical
+(i.e. the filesystem's)
+.Sq ..
+directories instead of
+.Dq logical
+directories (i.e. the shell handles
+.Sq .. ,
+which allows the user to be oblivious of symbolic links to directories).
+Clear by default.
+Note that setting this option does not affect the current value of the
+.Ev PWD
+parameter; only the
+.Ic cd
+command changes
+.Ev PWD .
+See the
+.Ic cd
+and
+.Ic pwd
+commands above for more details.
+.It Ic posix
+Enable POSIX mode.
+See
+.Sx POSIX mode
+above.
+.It Ic restricted
+The shell is a restricted shell.
+This option can only be used when the shell is invoked.
+See above for a description of what this means.
+.It Ic sh
+Enable strict Bourne shell mode (see
+.Sx Strict Bourne shell mode
+above).
+.It Ic vi
+Enable
+.Xr vi 1 Ns -like
+command-line editing (interactive shells only).
+.It Ic vi-esccomplete
+In vi command-line editing, do command and file name completion when escape
+(^[) is entered in command mode.
+.It Ic vi-show8
+Prefix characters with the eighth bit set with
+.Sq M- .
+If this option is not set, characters in the range 128\-160 are printed as is,
+which may cause problems.
+.It Ic vi-tabcomplete
+In vi command-line editing, do command and file name completion when tab (^I)
+is entered in insert mode.
+This is the default.
+.It Ic viraw
+No effect.
+In the original Korn shell, unless
+.Ic viraw
+was set, the vi command-line mode would let the
+.Xr tty 4
+driver do the work until ESC (^[) was entered.
+.Nm ksh
+is always in viraw mode.
+.El
+.Pp
+These options can also be used upon invocation of the shell.
+The current set of
+options (with single letter names) can be found in the parameter
+.Sq $- .
+.Ic set Fl o
+with no option name will list all the options and whether each is on or off;
+.Ic set +o
+will print the current shell options in a form that
+can be reinput to the shell to achieve the same option settings.
+.Pp
+Remaining arguments, if any, are positional parameters and are assigned, in
+order, to the positional parameters (i.e. $1, $2, etc.).
+If options end with
+.Ql --
+and there are no remaining arguments, all positional parameters are cleared.
+If no options or arguments are given, the values of all names are printed.
+For unknown historical reasons, a lone
+.Ql -
+option is treated specially \- it clears both the
+.Fl x
+and
+.Fl v
+options.
+.Pp
+.It Ic shift Op Ar number
+The positional parameters
+.Ar number Ns +1 ,
+.Ar number Ns +2 ,
+etc. are renamed to
+.Sq 1 ,
+.Sq 2 ,
+etc.
+.Ar number
+defaults to 1.
+.Pp
+.It Ic suspend
+Stops the shell as if it had received the suspend character from
+the terminal.
+It is not possible to suspend a login shell unless the parent process
+is a member of the same terminal session but is a member of a different
+process group.
+As a general rule, if the shell was started by another shell or via
+.Xr su 1 ,
+it can be suspended.
+.Pp
+.It Ic test Ar expression
+.It Ic \&[ Ar expression Ic \&]
+.Ic test
+evaluates the
+.Ar expression
+and returns zero status if true, 1 if false, or greater than 1 if there
+was an error.
+It is normally used as the condition command of
+.Ic if
+and
+.Ic while
+statements.
+Symbolic links are followed for all
+.Ar file
+expressions except
+.Fl h
+and
+.Fl L .
+.Pp
+The following basic expressions are available:
+.Bl -tag -width 17n
+.It Fl a Ar file
+.Ar file
+exists.
+.It Fl b Ar file
+.Ar file
+is a block special device.
+.It Fl c Ar file
+.Ar file
+is a character special device.
+.It Fl d Ar file
+.Ar file
+is a directory.
+.It Fl e Ar file
+.Ar file
+exists.
+.It Fl f Ar file
+.Ar file
+is a regular file.
+.It Fl G Ar file
+.Ar file Ns 's
+group is the shell's effective group ID.
+.It Fl g Ar file
+.Ar file Ns 's
+mode has the setgid bit set.
+.It Fl h Ar file
+.Ar file
+is a symbolic link.
+.It Fl k Ar file
+.Ar file Ns 's
+mode has the
+.Xr sticky 8
+bit set.
+.It Fl L Ar file
+.Ar file
+is a symbolic link.
+.It Fl O Ar file
+.Ar file Ns 's
+owner is the shell's effective user ID.
+.It Fl o Ar option
+Shell
+.Ar option
+is set (see the
+.Ic set
+command above for a list of options).
+As a non-standard extension, if the option starts with a
+.Ql \&! ,
+the test is negated; the test always fails if
+.Ar option
+doesn't exist (so [ -o foo -o -o !foo ] returns true if and only if option
+.Ar foo
+exists).
+.It Fl p Ar file
+.Ar file
+is a named pipe.
+.It Fl r Ar file
+.Ar file
+exists and is readable.
+.It Fl S Ar file
+.Ar file
+is a
+.Xr unix 4 Ns -domain
+socket.
+.It Fl s Ar file
+.Ar file
+is not empty.
+.It Fl t Op Ar fd
+File descriptor
+.Ar fd
+is a
+.Xr tty 4
+device.
+If the
+.Ic posix
+option is not set,
+.Ar fd
+may be left out, in which case it is taken to be 1 (the behaviour differs due
+to the special POSIX rules described above).
+.It Fl u Ar file
+.Ar file Ns 's
+mode has the setuid bit set.
+.It Fl w Ar file
+.Ar file
+exists and is writable.
+.It Fl x Ar file
+.Ar file
+exists and is executable.
+.It Ar file1 Fl nt Ar file2
+.Ar file1
+is newer than
+.Ar file2
+or
+.Ar file1
+exists and
+.Ar file2
+does not.
+.It Ar file1 Fl ot Ar file2
+.Ar file1
+is older than
+.Ar file2
+or
+.Ar file2
+exists and
+.Ar file1
+does not.
+.It Ar file1 Fl ef Ar file2
+.Ar file1
+is the same file as
+.Ar file2 .
+.It Ar string
+.Ar string
+has non-zero length.
+.It Fl n Ar string
+.Ar string
+is not empty.
+.It Fl z Ar string
+.Ar string
+is empty.
+.It Ar string No = Ar string
+Strings are equal.
+.It Ar string No == Ar string
+Strings are equal.
+.It Ar string No != Ar string
+Strings are not equal.
+.It Ar number Fl eq Ar number
+Numbers compare equal.
+.It Ar number Fl ne Ar number
+Numbers compare not equal.
+.It Ar number Fl ge Ar number
+Numbers compare greater than or equal.
+.It Ar number Fl gt Ar number
+Numbers compare greater than.
+.It Ar number Fl le Ar number
+Numbers compare less than or equal.
+.It Ar number Fl \&lt Ar number
+Numbers compare less than.
+.El
+.Pp
+The above basic expressions, in which unary operators have precedence over
+binary operators, may be combined with the following operators (listed in
+increasing order of precedence):
+.Bd -literal -offset indent
+expr -o expr		Logical OR.
+expr -a expr		Logical AND.
+! expr			Logical NOT.
+( expr )		Grouping.
+.Ed
+.Pp
+On operating systems not supporting
+.Pa /dev/fd/ Ns Ar n
+devices (where
+.Ar n
+is a file descriptor number), the
+.Ic test
+command will attempt to fake it for all tests that operate on files (except the
+.Fl e
+test).
+For example,
+[ -w /dev/fd/2 ] tests if file descriptor 2 is writable.
+.Pp
+Note that some special rules are applied (courtesy of POSIX)
+if the number of
+arguments to
+.Ic test
+or
+.Ic \&[ ... \&]
+is less than five: if leading
+.Ql \&!
+arguments can be stripped such that only one argument remains then a string
+length test is performed (again, even if the argument is a unary operator); if
+leading
+.Ql \&!
+arguments can be stripped such that three arguments remain and the second
+argument is a binary operator, then the binary operation is performed (even
+if the first argument is a unary operator, including an unstripped
+.Ql \&! ) .
+.Pp
+.Sy Note :
+A common mistake is to use
+.Dq if \&[ $foo = bar \&]
+which fails if parameter
+.Dq foo
+is
+.Dv NULL
+or unset, if it has embedded spaces (i.e.\&
+.Ev IFS
+characters), or if it is a unary operator like
+.Sq \&!
+or
+.Sq Fl n .
+Use tests like
+.Dq if \&[ \&"X$foo\&" = Xbar \&]
+instead.
+.Pp
+.It Xo
+.Ic time
+.Op Fl p
+.Op Ar pipeline
+.Xc
+If a
+.Ar pipeline
+is given, the times used to execute the pipeline are reported.
+If no pipeline
+is given, then the user and system time used by the shell itself, and all the
+commands it has run since it was started, are reported.
+The times reported are the real time (elapsed time from start to finish),
+the user CPU time (time spent running in user mode), and the system CPU time
+(time spent running in kernel mode).
+Times are reported to standard error; the format of the output is:
+.Pp
+.Dl "0m0.00s real     0m0.00s user     0m0.00s system"
+.Pp
+If the
+.Fl p
+option is given the output is slightly longer:
+.Bd -literal -offset indent
+real     0.00
+user     0.00
+sys      0.00
+.Ed
+.Pp
+It is an error to specify the
+.Fl p
+option unless
+.Ar pipeline
+is a simple command.
+.Pp
+Simple redirections of standard error do not affect the output of the
+.Ic time
+command:
+.Pp
+.Dl $ time sleep 1 2> afile
+.Dl $ { time sleep 1; } 2> afile
+.Pp
+Times for the first command do not go to
+.Dq afile ,
+but those of the second command do.
+.Pp
+.It Ic times
+Print the accumulated user and system times used both by the shell
+and by processes that the shell started which have exited.
+The format of the output is:
+.Bd -literal -offset indent
+0m0.00s 0m0.00s
+0m0.00s 0m0.00s
+.Ed
+.Pp
+.It Ic trap Op Ar handler signal ...
+Sets a trap handler that is to be executed when any of the specified signals are
+received.
+.Ar handler
+is either a
+.Dv NULL
+string, indicating the signals are to be ignored, a minus sign
+.Pq Sq - ,
+indicating that the default action is to be taken for the signals (see
+.Xr signal 3 ) ,
+or a string containing shell commands to be evaluated and executed at the first
+opportunity (i.e. when the current command completes, or before printing the
+next
+.Ev PS1
+prompt) after receipt of one of the signals.
+.Ar signal
+is the name of a signal (e.g.\&
+.Dv PIPE
+or
+.Dv ALRM )
+or the number of the signal (see the
+.Ic kill -l
+command above).
+.Pp
+There are two special signals:
+.Dv EXIT
+(also known as 0), which is executed when the shell is about to exit, and
+.Dv ERR ,
+which is executed after an error occurs (an error is something that would cause
+the shell to exit if the
+.Fl e
+or
+.Ic errexit
+option were set \- see the
+.Ic set
+command above).
+.Dv EXIT
+handlers are executed in the environment of the last executed command.
+Note
+that for non-interactive shells, the trap handler cannot be changed for signals
+that were ignored when the shell started.
+.Pp
+With no arguments,
+.Ic trap
+lists, as a series of
+.Ic trap
+commands, the current state of the traps that have been set since the shell
+started.
+Note that the output of
+.Ic trap
+cannot be usefully piped to another process (an artifact of the fact that
+traps are cleared when subprocesses are created).
+.Pp
+The original Korn shell's
+.Dv DEBUG
+trap and the handling of
+.Dv ERR
+and
+.Dv EXIT
+traps in functions are not yet implemented.
+.Pp
+.It Ic true
+A command that exits with a zero value.
+.Pp
+.It Ic type
+Short form of
+.Ic command Fl V
+(see above).
+.Pp
+.It Xo
+.Ic typeset
+.Oo
+.Op Ic +-lprtUux
+.Op Fl L Ns Op Ar n
+.Op Fl R Ns Op Ar n
+.Op Fl Z Ns Op Ar n
+.Op Fl i Ns Op Ar n
+.No \&| Fl f Op Fl tux
+.Oc
+.Oo
+.Ar name
+.Op Ns = Ns Ar value
+.Ar ...
+.Oc
+.Xc
+Display or set parameter attributes.
+With no
+.Ar name
+arguments, parameter attributes are displayed; if no options are used, the
+current attributes of all parameters are printed as
+.Ic typeset
+commands; if an option is given (or
+.Ql -
+with no option letter), all parameters and their values with the specified
+attributes are printed; if options are introduced with
+.Ql + ,
+parameter values are not printed.
+.Pp
+If
+.Ar name
+arguments are given, the attributes of the named parameters are set
+.Pq Ic -
+or cleared
+.Pq Ic + .
+Values for parameters may optionally be specified.
+If
+.Ic typeset
+is used inside a function, any newly created parameters are local to the
+function.
+.Pp
+When
+.Fl f
+is used,
+.Ic typeset
+operates on the attributes of functions.
+As with parameters, if no
+.Ar name
+arguments are given,
+functions are listed with their values (i.e. definitions) unless
+options are introduced with
+.Ql + ,
+in which case only the function names are reported.
+.Bl -tag -width Ds
+.It Fl f
+Function mode.
+Display or set functions and their attributes, instead of parameters.
+.It Fl i Ns Op Ar n
+Integer attribute.
+.Ar n
+specifies the base to use when displaying the integer (if not specified, the
+base given in the first assignment is used).
+Parameters with this attribute may
+be assigned values containing arithmetic expressions.
+.It Fl L Ns Op Ar n
+Left justify attribute.
+.Ar n
+specifies the field width.
+If
+.Ar n
+is not specified, the current width of a parameter (or the width of its first
+assigned value) is used.
+Leading whitespace (and zeros, if used with the
+.Fl Z
+option) is stripped.
+If necessary, values are either truncated or space padded
+to fit the field width.
+.It Fl l
+Lower case attribute.
+All upper case characters in values are converted to lower case.
+(In the original Korn shell, this parameter meant
+.Dq long integer
+when used with the
+.Fl i
+option.)
+.It Fl p
+Print complete
+.Ic typeset
+commands that can be used to re-create the attributes (but not the values) of
+parameters.
+This is the default action (option exists for ksh93 compatibility).
+.It Fl R Ns Op Ar n
+Right justify attribute.
+.Ar n
+specifies the field width.
+If
+.Ar n
+is not specified, the current width of a parameter (or the width of its first
+assigned value) is used.
+Trailing whitespace is stripped.
+If necessary, values are either stripped of leading characters or space
+padded to make them fit the field width.
+.It Fl r
+Read-only attribute.
+Parameters with this attribute may not be assigned to or unset.
+Once this attribute is set, it cannot be turned off.
+.It Fl t
+Tag attribute.
+Has no meaning to the shell; provided for application use.
+.Pp
+For functions,
+.Fl t
+is the trace attribute.
+When functions with the trace attribute are executed, the
+.Ic xtrace
+.Pq Fl x
+shell option is temporarily turned on.
+.It Fl U
+Unsigned integer attribute.
+Integers are printed as unsigned values (only
+useful when combined with the
+.Fl i
+option).
+This option is not in the original Korn shell.
+.It Fl u
+Upper case attribute.
+All lower case characters in values are converted to upper case.
+(In the original Korn shell, this parameter meant
+.Dq unsigned integer
+when used with the
+.Fl i
+option, which meant upper case letters would never be used for bases greater
+than 10.
+See the
+.Fl U
+option.)
+.Pp
+For functions,
+.Fl u
+is the undefined attribute.
+See
+.Sx Functions
+above for the implications of this.
+.It Fl x
+Export attribute.
+Parameters (or functions) are placed in the environment of
+any executed commands.
+Exported functions are not yet implemented.
+.It Fl Z Ns Op Ar n
+Zero fill attribute.
+If not combined with
+.Fl L ,
+this is the same as
+.Fl R ,
+except zero padding is used instead of space padding.
+.El
+.Pp
+.It Xo
+.Ic ulimit
+.Op Fl acdfHlmnpSst Op Ar value
+.Ar ...
+.Xc
+Display or set process limits.
+If no options are used, the file size limit
+.Pq Fl f
+is assumed.
+.Ar value ,
+if specified, may be either an arithmetic expression starting with a
+number or the word
+.Dq unlimited .
+The limits affect the shell and any processes created by the shell after a
+limit is imposed; limits may not be increased once they are set.
+.Bl -tag -width 5n
+.It Fl a
+Display all limits; unless
+.Fl H
+is used, soft limits are displayed.
+.It Fl c Ar n
+Impose a size limit of
+.Ar n
+blocks on the size of core dumps.
+.It Fl d Ar n
+Impose a size limit of
+.Ar n
+kilobytes on the size of the data area.
+.It Fl f Ar n
+Impose a size limit of
+.Ar n
+blocks on files written by the shell and its child processes (files of any
+size may be read).
+.It Fl H
+Set the hard limit only (the default is to set both hard and soft limits).
+.It Fl l Ar n
+Impose a limit of
+.Ar n
+kilobytes on the amount of locked (wired) physical memory.
+.It Fl m Ar n
+Impose a limit of
+.Ar n
+kilobytes on the amount of physical memory used.
+This limit is not enforced.
+.It Fl n Ar n
+Impose a limit of
+.Ar n
+file descriptors that can be open at once.
+.It Fl p Ar n
+Impose a limit of
+.Ar n
+processes that can be run by the user at any one time.
+.It Fl S
+Set the soft limit only (the default is to set both hard and soft limits).
+.It Fl s Ar n
+Impose a size limit of
+.Ar n
+kilobytes on the size of the stack area.
+.It Fl t Ar n
+Impose a time limit of
+.Ar n
+CPU seconds spent in user mode to be used by each process.
+.\".It Fl v Ar n
+.\"Impose a limit of
+.\"Ar n
+.\"kilobytes on the amount of virtual memory used.
+.El
+.Pp
+As far as
+.Ic ulimit
+is concerned, a block is 512 bytes.
+.Pp
+.It Xo
+.Ic umask
+.Op Fl S
+.Op Ar mask
+.Xc
+Display or set the file permission creation mask, or umask (see
+.Xr umask 2 ) .
+If the
+.Fl S
+option is used, the mask displayed or set is symbolic; otherwise, it is an
+octal number.
+.Pp
+Symbolic masks are like those used by
+.Xr chmod 1 .
+When used, they describe what permissions may be made available (as opposed to
+octal masks in which a set bit means the corresponding bit is to be cleared).
+For example,
+.Dq ug=rwx,o=
+sets the mask so files will not be readable, writable, or executable by
+.Dq others ,
+and is equivalent (on most systems) to the octal mask
+.Dq 007 .
+.Pp
+.It Xo
+.Ic unalias
+.Op Fl adt
+.Op Ar name ...
+.Xc
+The aliases for the given names are removed.
+If the
+.Fl a
+option is used, all aliases are removed.
+If the
+.Fl t
+or
+.Fl d
+options are used, the indicated operations are carried out on tracked or
+directory aliases, respectively.
+.Pp
+.It Xo
+.Ic unset
+.Op Fl fv
+.Ar parameter ...
+.Xc
+Unset the named parameters
+.Po
+.Fl v ,
+the default
+.Pc
+or functions
+.Pq Fl f .
+The exit status is non-zero if any of the parameters have the read-only
+attribute set, zero otherwise.
+.Pp
+.It Ic wait Op Ar job ...
+Wait for the specified job(s) to finish.
+The exit status of
+.Ic wait
+is that of the last specified job; if the last job is killed by a signal, the
+exit status is 128 + the number of the signal (see
+.Ic kill -l Ar exit-status
+above); if the last specified job can't be found (because it never existed, or
+had already finished), the exit status of
+.Ic wait
+is 127.
+See
+.Sx Job control
+below for the format of
+.Ar job .
+.Ic wait
+will return if a signal for which a trap has been set is received, or if a
+.Dv SIGHUP ,
+.Dv SIGINT ,
+or
+.Dv SIGQUIT
+signal is received.
+.Pp
+If no jobs are specified,
+.Ic wait
+waits for all currently running jobs (if any) to finish and exits with a zero
+status.
+If job monitoring is enabled, the completion status of jobs is printed
+(this is not the case when jobs are explicitly specified).
+.Pp
+.It Xo
+.Ic whence
+.Op Fl pv
+.Op Ar name ...
+.Xc
+For each
+.Ar name ,
+the type of command is listed (reserved word, built-in, alias,
+function, tracked alias, or executable).
+If the
+.Fl p
+option is used, a path search is performed even if
+.Ar name
+is a reserved word, alias, etc.
+Without the
+.Fl v
+option,
+.Ic whence
+is similar to
+.Ic command Fl v
+except that
+.Ic whence
+won't print aliases as alias commands.
+With the
+.Fl v
+option,
+.Ic whence
+is the same as
+.Ic command Fl V .
+Note that for
+.Ic whence ,
+the
+.Fl p
+option does not affect the search path used, as it does for
+.Ic command .
+If the type of one or more of the names could not be determined, the exit
+status is non-zero.
+.El
+.Ss Job control
+Job control refers to the shell's ability to monitor and control jobs, which
+are processes or groups of processes created for commands or pipelines.
+At a minimum, the shell keeps track of the status of the background (i.e.\&
+asynchronous) jobs that currently exist; this information can be displayed
+using the
+.Ic jobs
+commands.
+If job control is fully enabled (using
+.Ic set -m
+or
+.Ic set -o monitor ) ,
+as it is for interactive shells, the processes of a job are placed in their
+own process group.
+Foreground jobs can be stopped by typing the suspend
+character from the terminal (normally ^Z), jobs can be restarted in either the
+foreground or background using the
+.Ic fg
+and
+.Ic bg
+commands, and the state of the terminal is saved or restored when a foreground
+job is stopped or restarted, respectively.
+.Pp
+Note that only commands that create processes (e.g. asynchronous commands,
+subshell commands, and non-built-in, non-function commands) can be stopped;
+commands like
+.Ic read
+cannot be.
+.Pp
+When a job is created, it is assigned a job number.
+For interactive shells, this number is printed inside
+.Dq [..] ,
+followed by the process IDs of the processes in the job when an asynchronous
+command is run.
+A job may be referred to in the
+.Ic bg ,
+.Ic fg ,
+.Ic jobs ,
+.Ic kill ,
+and
+.Ic wait
+commands either by the process ID of the last process in the command pipeline
+(as stored in the
+.Ic $!\&
+parameter) or by prefixing the job number with a percent
+sign
+.Pq Sq % .
+Other percent sequences can also be used to refer to jobs:
+.Bl -tag -width "%+ | %% | %XX"
+.It %+ | %% | %
+The most recently stopped job or, if there are no stopped jobs, the oldest
+running job.
+.It %-
+The job that would be the
+.Ic %+
+job if the latter did not exist.
+.It % Ns Ar n
+The job with job number
+.Ar n .
+.It %? Ns Ar string
+The job with its command containing the string
+.Ar string
+(an error occurs if multiple jobs are matched).
+.It % Ns Ar string
+The job with its command starting with the string
+.Ar string
+(an error occurs if multiple jobs are matched).
+.El
+.Pp
+When a job changes state (e.g. a background job finishes or foreground job is
+stopped), the shell prints the following status information:
+.Pp
+.D1 [ Ns Ar number ] Ar flag status command
+.Pp
+where...
+.Bl -tag -width "command"
+.It Ar number
+is the job number of the job;
+.It Ar flag
+is the
+.Ql +
+or
+.Ql -
+character if the job is the
+.Ic %+
+or
+.Ic %-
+job, respectively, or space if it is neither;
+.It Ar status
+indicates the current state of the job and can be:
+.Bl -tag -width "RunningXX"
+.It Done Op Ar number
+The job exited.
+.Ar number
+is the exit status of the job, which is omitted if the status is zero.
+.It Running
+The job has neither stopped nor exited (note that running does not necessarily
+mean consuming CPU time \-
+the process could be blocked waiting for some event).
+.It Stopped Op Ar signal
+The job was stopped by the indicated
+.Ar signal
+(if no signal is given, the job was stopped by
+.Dv SIGTSTP ) .
+.It Ar signal-description Op Dq core dumped
+The job was killed by a signal (e.g. memory fault, hangup); use
+.Ic kill -l
+for a list of signal descriptions.
+The
+.Dq core dumped
+message indicates the process created a core file.
+.El
+.It Ar command
+is the command that created the process.
+If there are multiple processes in
+the job, each process will have a line showing its
+.Ar command
+and possibly its
+.Ar status ,
+if it is different from the status of the previous process.
+.El
+.Pp
+When an attempt is made to exit the shell while there are jobs in the stopped
+state, the shell warns the user that there are stopped jobs and does not exit.
+If another attempt is immediately made to exit the shell, the stopped jobs are
+sent a
+.Dv SIGHUP
+signal and the shell exits.
+Similarly, if the
+.Ic nohup
+option is not set and there are running jobs when an attempt is made to exit
+a login shell, the shell warns the user and does not exit.
+If another attempt
+is immediately made to exit the shell, the running jobs are sent a
+.Dv SIGHUP
+signal and the shell exits.
+.Ss Interactive input line editing
+The shell supports three modes of reading command lines from a
+.Xr tty 4
+in an interactive session, controlled by the
+.Ic emacs ,
+.Ic gmacs ,
+and
+.Ic vi
+options (at most one of these can be set at once).
+The default is
+.Ic emacs .
+Editing modes can be set explicitly using the
+.Ic set
+built-in, or implicitly via the
+.Ev EDITOR
+and
+.Ev VISUAL
+environment variables.
+If none of these options are enabled,
+the shell simply reads lines using the normal
+.Xr tty 4
+driver.
+If the
+.Ic emacs
+or
+.Ic gmacs
+option is set, the shell allows emacs-like editing of the command; similarly,
+if the
+.Ic vi
+option is set, the shell allows vi-like editing of the command.
+These modes are described in detail in the following sections.
+.Pp
+In these editing modes, if a line is longer than the screen width (see the
+.Ev COLUMNS
+parameter),
+a
+.Ql > ,
+.Ql + ,
+or
+.Ql <
+character is displayed in the last column indicating that there are more
+characters after, before and after, or before the current position,
+respectively.
+The line is scrolled horizontally as necessary.
+.Ss Emacs editing mode
+When the
+.Ic emacs
+option is set, interactive input line editing is enabled.
+Warning: This mode is
+slightly different from the emacs mode in the original Korn shell.
+In this mode, various editing commands
+(typically bound to one or more control characters) cause immediate actions
+without waiting for a newline.
+Several editing commands are bound to particular
+control characters when the shell is invoked; these bindings can be changed
+using the
+.Ic bind
+command.
+.Pp
+The following is a list of available editing commands.
+Each description starts with the name of the command,
+suffixed with a colon;
+an
+.Op Ar n
+(if the command can be prefixed with a count); and any keys the command is
+bound to by default, written using caret notation
+e.g. the ASCII ESC character is written as ^[.
+^[A-Z] sequences are not case sensitive.
+A count prefix for a command is entered using the sequence
+.Pf ^[ Ar n ,
+where
+.Ar n
+is a sequence of 1 or more digits.
+Unless otherwise specified, if a count is
+omitted, it defaults to 1.
+.Pp
+Note that editing command names are used only with the
+.Ic bind
+command.
+Furthermore, many editing commands are useful only on terminals with
+a visible cursor.
+The default bindings were chosen to resemble corresponding
+Emacs key bindings.
+The user's
+.Xr tty 4
+characters (e.g.\&
+.Dv ERASE )
+are bound to
+reasonable substitutes and override the default bindings.
+.Bl -tag -width Ds
+.It abort: ^C, ^G
+Useful as a response to a request for a
+.Ic search-history
+pattern in order to abort the search.
+.It auto-insert: Op Ar n
+Simply causes the character to appear as literal input.
+Most ordinary characters are bound to this.
+.It Xo backward-char:
+.Op Ar n
+.No ^B , ^X^D
+.Xc
+Moves the cursor backward
+.Ar n
+characters.
+.It Xo backward-word:
+.Op Ar n
+.No ^[b
+.Xc
+Moves the cursor backward to the beginning of the word; words consist of
+alphanumerics, underscore
+.Pq Sq _ ,
+and dollar sign
+.Pq Sq $
+characters.
+.It beginning-of-history: ^[<
+Moves to the beginning of the history.
+.It beginning-of-line: ^A
+Moves the cursor to the beginning of the edited input line.
+.It Xo capitalize-word:
+.Op Ar n
+.No ^[C , ^[c
+.Xc
+Uppercase the first character in the next
+.Ar n
+words, leaving the cursor past the end of the last word.
+.It clear-screen: ^L
+Clears the screen if the
+.Ev TERM
+parameter is set and the terminal supports clearing the screen, then
+reprints the prompt string and the current input line.
+.It comment: ^[#
+If the current line does not begin with a comment character, one is added at
+the beginning of the line and the line is entered (as if return had been
+pressed); otherwise, the existing comment characters are removed and the cursor
+is placed at the beginning of the line.
+.It complete: ^[^[
+Automatically completes as much as is unique of the command name or the file
+name containing the cursor.
+If the entire remaining command or file name is
+unique, a space is printed after its completion, unless it is a directory name
+in which case
+.Ql /
+is appended.
+If there is no command or file name with the current partial word
+as its prefix, a bell character is output (usually causing a beep to be
+sounded).
+.Pp
+Custom completions may be configured by creating an array named
+.Ql complete_command ,
+optionally suffixed with an argument number to complete only for a single
+argument.
+So defining an array named
+.Ql complete_kill
+provides possible completions for any argument to the
+.Xr kill 1
+command, but
+.Ql complete_kill_1
+only completes the first argument.
+For example, the following command makes
+.Nm
+offer a selection of signal names for the first argument to
+.Xr kill 1 :
+.Pp
+.Dl set -A complete_kill_1 -- -9 -HUP -INFO -KILL -TERM
+.It complete-command: ^X^[
+Automatically completes as much as is unique of the command name having the
+partial word up to the cursor as its prefix, as in the
+.Ic complete
+command above.
+.It complete-file: ^[^X
+Automatically completes as much as is unique of the file name having the
+partial word up to the cursor as its prefix, as in the
+.Ic complete
+command described above.
+.It complete-list: ^I, ^[=
+Complete as much as is possible of the current word,
+and list the possible completions for it.
+If only one completion is possible,
+match as in the
+.Ic complete
+command above.
+.It Xo delete-char-backward:
+.Op Ar n
+.No ERASE , ^? , ^H
+.Xc
+Deletes
+.Ar n
+characters before the cursor.
+.It Xo delete-char-forward:
+.Op Ar n
+.No Delete
+.Xc
+Deletes
+.Ar n
+characters after the cursor.
+.It Xo delete-word-backward:
+.Op Ar n
+.No WERASE , ^[ERASE , ^W, ^[^? , ^[^H , ^[h
+.Xc
+Deletes
+.Ar n
+words before the cursor.
+.It Xo delete-word-forward:
+.Op Ar n
+.No ^[d
+.Xc
+Deletes
+.Ar n
+words after the cursor.
+.It Xo down-history:
+.Op Ar n
+.No ^N , ^XB
+.Xc
+Scrolls the history buffer forward
+.Ar n
+lines (later).
+Each input line originally starts just after the last entry
+in the history buffer, so
+.Ic down-history
+is not useful until either
+.Ic search-history
+or
+.Ic up-history
+has been performed.
+.It Xo downcase-word:
+.Op Ar n
+.No ^[L , ^[l
+.Xc
+Lowercases the next
+.Ar n
+words.
+.It end-of-history: ^[>
+Moves to the end of the history.
+.It end-of-line: ^E
+Moves the cursor to the end of the input line.
+.It eot: ^_
+Acts as an end-of-file; this is useful because edit-mode input disables
+normal terminal input canonicalization.
+.It Xo eot-or-delete:
+.Op Ar n
+.No ^D
+.Xc
+Acts as
+.Ic eot
+if alone on a line; otherwise acts as
+.Ic delete-char-forward .
+.It error:
+Error (ring the bell).
+.It exchange-point-and-mark: ^X^X
+Places the cursor where the mark is and sets the mark to where the cursor was.
+.It expand-file: ^[*
+Appends a
+.Ql *
+to the current word and replaces the word with the result of performing file
+globbing on the word.
+If no files match the pattern, the bell is rung.
+.It Xo forward-char:
+.Op Ar n
+.No ^F , ^XC
+.Xc
+Moves the cursor forward
+.Ar n
+characters.
+.It Xo forward-word:
+.Op Ar n
+.No ^[f
+.Xc
+Moves the cursor forward to the end of the
+.Ar n Ns th
+word.
+.It Xo goto-history:
+.Op Ar n
+.No ^[g
+.Xc
+Goes to history number
+.Ar n .
+.It kill-line: KILL
+Deletes the entire input line.
+.It Xo kill-to-eol:
+.Op Ar n
+.No ^K
+.Xc
+Deletes the input from the cursor to the end of the line if
+.Ar n
+is not specified; otherwise deletes characters between the cursor and column
+.Ar n .
+.It list: ^[?
+Prints a sorted, columnated list of command names or file names (if any) that
+can complete the partial word containing the cursor.
+Directory names have
+.Ql /
+appended to them.
+.It list-command: ^X?
+Prints a sorted, columnated list of command names (if any) that can complete
+the partial word containing the cursor.
+.It list-file: ^X^Y
+Prints a sorted, columnated list of file names (if any) that can complete the
+partial word containing the cursor.
+File type indicators are appended as described under
+.Ic list
+above.
+.It newline: ^J , ^M
+Causes the current input line to be processed by the shell.
+The current cursor position may be anywhere on the line.
+.It newline-and-next: ^O
+Causes the current input line to be processed by the shell, and the next line
+from history becomes the current line.
+This is only useful after an
+.Ic up-history
+or
+.Ic search-history .
+.It no-op: QUIT
+This does nothing.
+.It Xo prev-hist-word:
+.Op Ar n
+.No ^[. , ^[_
+.Xc
+The last
+.Pq Ar n Ns th
+word of the previous command is inserted at the cursor.
+.It quote: ^^
+The following character is taken literally rather than as an editing command.
+.It redraw:
+Reprints the prompt string and the current input line.
+.It Xo search-character-backward:
+.Op Ar n
+.No ^[^]
+.Xc
+Search backward in the current line for the
+.Ar n Ns th
+occurrence of the next character typed.
+.It Xo search-character-forward:
+.Op Ar n
+.No ^]
+.Xc
+Search forward in the current line for the
+.Ar n Ns th
+occurrence of the next character typed.
+.It search-history: ^R
+Enter incremental search mode.
+The internal history list is searched
+backwards for commands matching the input.
+An initial
+.Ql ^
+in the search string anchors the search.
+The abort key will leave search mode.
+Other commands will be executed after leaving search mode.
+Successive
+.Ic search-history
+commands continue searching backward to the next previous occurrence of the
+pattern.
+The history buffer retains only a finite number of lines; the oldest
+are discarded as necessary.
+.It set-mark-command: ^[ Ns Aq space
+Set the mark at the cursor position.
+.It transpose-chars: ^T
+If at the end of line, or if the
+.Ic gmacs
+option is set, this exchanges the two previous characters; otherwise, it
+exchanges the previous and current characters and moves the cursor one
+character to the right.
+.It Xo up-history:
+.Op Ar n
+.No ^P , ^XA
+.Xc
+Scrolls the history buffer backward
+.Ar n
+lines (earlier).
+.It Xo upcase-word:
+.Op Ar n
+.No ^[U , ^[u
+.Xc
+Uppercase the next
+.Ar n
+words.
+.It quote: ^V
+Synonym for ^^.
+.It yank: ^Y
+Inserts the most recently killed text string at the current cursor position.
+.It yank-pop: ^[y
+Immediately after a
+.Ic yank ,
+replaces the inserted text string with the next previously killed text string.
+.El
+.Pp
+The following editing commands lack default bindings but can be used with the
+.Ic bind
+command:
+.Bl -tag -width Ds
+.It kill-region
+Deletes the input between the cursor and the mark.
+.El
+.Ss Vi editing mode
+The vi command-line editor in
+.Nm
+has basically the same commands as the
+.Xr vi 1
+editor with the following exceptions:
+.Bl -bullet
+.It
+You start out in insert mode.
+.It
+There are file name and command completion commands:
+=, \e, *, ^X, ^E, ^F, and, optionally,
+.Aq tab
+and
+.Aq esc .
+.It
+The
+.Ic _
+command is different (in
+.Nm
+it is the last argument command; in
+.Xr vi 1
+it goes to the start of the current line).
+.It
+The
+.Ic /
+and
+.Ic G
+commands move in the opposite direction to the
+.Ic j
+command.
+.It
+Commands which don't make sense in a single line editor are not available
+(e.g. screen movement commands and
+.Xr ex 1 Ns -style
+colon
+.Pq Ic \&:
+commands).
+.El
+.Pp
+Note that the ^X stands for control-X; also
+.Aq esc ,
+.Aq space ,
+and
+.Aq tab
+are used for escape, space, and tab, respectively (no kidding).
+.Pp
+Like
+.Xr vi 1 ,
+there are two modes:
+.Dq insert
+mode and
+.Dq command
+mode.
+In insert mode, most characters are simply put in the buffer at the
+current cursor position as they are typed; however, some characters are
+treated specially.
+In particular, the following characters are taken from current
+.Xr tty 4
+settings
+(see
+.Xr stty 1 )
+and have their usual meaning (normal values are in parentheses): kill (^U),
+erase (^?), werase (^W), eof (^D), intr (^C), and quit (^\e).
+In addition to
+the above, the following characters are also treated specially in insert mode:
+.Bl -tag -width 10n
+.It ^E
+Command and file name enumeration (see below).
+.It ^F
+Command and file name completion (see below).
+If used twice in a row, the
+list of possible completions is displayed; if used a third time, the completion
+is undone.
+.It ^H
+Erases previous character.
+.It ^J | ^M
+End of line.
+The current line is read, parsed, and executed by the shell.
+.It ^V
+Literal next.
+The next character typed is not treated specially (can be used
+to insert the characters being described here).
+.It ^X
+Command and file name expansion (see below).
+.It Aq esc
+Puts the editor in command mode (see below).
+.It Aq tab
+Optional file name and command completion (see
+.Ic ^F
+above), enabled with
+.Ic set -o vi-tabcomplete .
+.El
+.Pp
+In command mode, each character is interpreted as a command.
+Characters that
+don't correspond to commands, are illegal combinations of commands, or are
+commands that can't be carried out, all cause beeps.
+In the following command descriptions, an
+.Op Ar n
+indicates the command may be prefixed by a number (e.g.\&
+.Ic 10l
+moves right 10 characters); if no number prefix is used,
+.Ar n
+is assumed to be 1 unless otherwise specified.
+The term
+.Dq current position
+refers to the position between the cursor and the character preceding the
+cursor.
+A
+.Dq word
+is a sequence of letters, digits, and underscore characters or a sequence of
+non-letter, non-digit, non-underscore, and non-whitespace characters (e.g.\&
+.Dq ab2*&^
+contains two words) and a
+.Dq big-word
+is a sequence of non-whitespace characters.
+.Pp
+Special
+.Nm
+vi commands:
+.Pp
+The following commands are not in, or are different from, the normal vi file
+editor:
+.Bl -tag -width 10n
+.It Xo
+.Oo Ar n Oc Ns _
+.Xc
+Insert a space followed by the
+.Ar n Ns th
+big-word from the last command in the history at the current position and enter
+insert mode; if
+.Ar n
+is not specified, the last word is inserted.
+.It #
+Insert the comment character
+.Pq Sq #
+at the start of the current line and return the line to the shell (equivalent
+to
+.Ic I#^J ) .
+.It Xo
+.Oo Ar n Oc Ns g
+.Xc
+Like
+.Ic G ,
+except if
+.Ar n
+is not specified, it goes to the most recent remembered line.
+.It Xo
+.Oo Ar n Oc Ns v
+.Xc
+Edit line
+.Ar n
+using the
+.Xr vi 1
+editor; if
+.Ar n
+is not specified, the current line is edited.
+The actual command executed is
+.Ic fc -e ${VISUAL:-${EDITOR:-vi}} Ar n .
+.It * and ^X
+Command or file name expansion is applied to the current big-word (with an
+appended
+.Ql *
+if the word contains no file globbing characters) \- the big-word is replaced
+with the resulting words.
+If the current big-word is the first on the line
+or follows one of the characters
+.Ql \&; ,
+.Ql | ,
+.Ql & ,
+.Ql \&( ,
+or
+.Ql \&) ,
+and does not contain a slash
+.Pq Sq / ,
+then command expansion is done; otherwise file name expansion is done.
+Command expansion will match the big-word against all aliases, functions, and
+built-in commands as well as any executable files found by searching the
+directories in the
+.Ev PATH
+parameter.
+File name expansion matches the big-word against the files in the
+current directory.
+After expansion, the cursor is placed just past the last
+word and the editor is in insert mode.
+.It Xo
+.Oo Ar n Oc Ns \e ,
+.Oo Ar n Oc Ns ^F ,
+.Oo Ar n Oc Ns Aq tab ,
+.No and
+.Oo Ar n Oc Ns Aq esc
+.Xc
+Command/file name completion.
+Replace the current big-word with the
+longest unique match obtained after performing command and file name expansion.
+.Aq tab
+is only recognized if the
+.Ic vi-tabcomplete
+option is set, while
+.Aq esc
+is only recognized if the
+.Ic vi-esccomplete
+option is set (see
+.Ic set -o ) .
+If
+.Ar n
+is specified, the
+.Ar n Ns th
+possible completion is selected (as reported by the command/file name
+enumeration command).
+.It = and ^E
+Command/file name enumeration.
+List all the commands or files that match the current big-word.
+.It @ Ns Ar c
+Macro expansion.
+Execute the commands found in the alias
+.No _ Ns Ar c .
+.El
+.Pp
+Intra-line movement commands:
+.Bl -tag -width Ds
+.It Xo
+.Oo Ar n Oc Ns h and
+.Oo Ar n Oc Ns ^H
+.Xc
+Move left
+.Ar n
+characters.
+.It Xo
+.Oo Ar n Oc Ns l and
+.Oo Ar n Oc Ns Aq space
+.Xc
+Move right
+.Ar n
+characters.
+.It 0
+Move to column 0.
+.It ^
+Move to the first non-whitespace character.
+.It Xo
+.Oo Ar n Oc Ns |
+.Xc
+Move to column
+.Ar n .
+.It $
+Move to the last character.
+.It Xo
+.Oo Ar n Oc Ns b
+.Xc
+Move back
+.Ar n
+words.
+.It Xo
+.Oo Ar n Oc Ns B
+.Xc
+Move back
+.Ar n
+big-words.
+.It Xo
+.Oo Ar n Oc Ns e
+.Xc
+Move forward to the end of the word,
+.Ar n
+times.
+.It Xo
+.Oo Ar n Oc Ns E
+.Xc
+Move forward to the end of the big-word,
+.Ar n
+times.
+.It Xo
+.Oo Ar n Oc Ns w
+.Xc
+Move forward
+.Ar n
+words.
+.It Xo
+.Oo Ar n Oc Ns W
+.Xc
+Move forward
+.Ar n
+big-words.
+.It %
+Find match.
+The editor looks forward for the nearest parenthesis, bracket, or
+brace and then moves the cursor to the matching parenthesis, bracket, or brace.
+.It Xo
+.Oo Ar n Oc Ns f Ns Ar c
+.Xc
+Move forward to the
+.Ar n Ns th
+occurrence of the character
+.Ar c .
+.It Xo
+.Oo Ar n Oc Ns F Ns Ar c
+.Xc
+Move backward to the
+.Ar n Ns th
+occurrence of the character
+.Ar c .
+.It Xo
+.Oo Ar n Oc Ns t Ns Ar c
+.Xc
+Move forward to just before the
+.Ar n Ns th
+occurrence of the character
+.Ar c .
+.It Xo
+.Oo Ar n Oc Ns T Ns Ar c
+.Xc
+Move backward to just before the
+.Ar n Ns th
+occurrence of the character
+.Ar c .
+.It Xo
+.Oo Ar n Oc Ns \&;
+.Xc
+Repeats the last
+.Ic f , F , t ,
+or
+.Ic T
+command.
+.It Xo
+.Oo Ar n Oc Ns \&,
+.Xc
+Repeats the last
+.Ic f , F , t ,
+or
+.Ic T
+command, but moves in the opposite direction.
+.El
+.Pp
+Inter-line movement commands:
+.Bl -tag -width Ds
+.It Xo
+.Oo Ar n Oc Ns j ,
+.Oo Ar n Oc Ns + ,
+.No and
+.Oo Ar n Oc Ns ^N
+.Xc
+Move to the
+.Ar n Ns th
+next line in the history.
+.It Xo
+.Oo Ar n Oc Ns k ,
+.Oo Ar n Oc Ns - ,
+.No and
+.Oo Ar n Oc Ns ^P
+.Xc
+Move to the
+.Ar n Ns th
+previous line in the history.
+.It Xo
+.Oo Ar n Oc Ns G
+.Xc
+Move to line
+.Ar n
+in the history; if
+.Ar n
+is not specified, the number of the first remembered line is used.
+.It Xo
+.Oo Ar n Oc Ns g
+.Xc
+Like
+.Ic G ,
+except if
+.Ar n
+is not specified, it goes to the most recent remembered line.
+.It Xo
+.Oo Ar n Oc Ns / Ns Ar string
+.Xc
+Search backward through the history for the
+.Ar n Ns th
+line containing
+.Ar string ;
+if
+.Ar string
+starts with
+.Ql ^ ,
+the remainder of the string must appear at the start of the history line for
+it to match.
+.It Xo
+.Oo Ar n Oc Ns \&? Ns Ar string
+.Xc
+Same as
+.Ic / ,
+except it searches forward through the history.
+.It Xo
+.Oo Ar n Oc Ns n
+.Xc
+Search for the
+.Ar n Ns th
+occurrence of the last search string;
+the direction of the search is the same as the last search.
+.It Xo
+.Oo Ar n Oc Ns N
+.Xc
+Search for the
+.Ar n Ns th
+occurrence of the last search string;
+the direction of the search is the opposite of the last search.
+.El
+.Pp
+Edit commands
+.Bl -tag -width Ds
+.It Xo
+.Oo Ar n Oc Ns a
+.Xc
+Append text
+.Ar n
+times; goes into insert mode just after the current position.
+The append is
+only replicated if command mode is re-entered i.e.\&
+.Aq esc
+is used.
+.It Xo
+.Oo Ar n Oc Ns A
+.Xc
+Same as
+.Ic a ,
+except it appends at the end of the line.
+.It Xo
+.Oo Ar n Oc Ns i
+.Xc
+Insert text
+.Ar n
+times; goes into insert mode at the current position.
+The insertion is only
+replicated if command mode is re-entered i.e.\&
+.Aq esc
+is used.
+.It Xo
+.Oo Ar n Oc Ns I
+.Xc
+Same as
+.Ic i ,
+except the insertion is done just before the first non-blank character.
+.It Xo
+.Oo Ar n Oc Ns s
+.Xc
+Substitute the next
+.Ar n
+characters (i.e. delete the characters and go into insert mode).
+.It S
+Substitute whole line.
+All characters from the first non-blank character to the
+end of the line are deleted and insert mode is entered.
+.It Xo
+.Oo Ar n Oc Ns c Ns Ar move-cmd
+.Xc
+Change from the current position to the position resulting from
+.Ar n move-cmd Ns s
+(i.e. delete the indicated region and go into insert mode); if
+.Ar move-cmd
+is
+.Ic c ,
+the line starting from the first non-blank character is changed.
+.It C
+Change from the current position to the end of the line (i.e. delete to the
+end of the line and go into insert mode).
+.It Xo
+.Oo Ar n Oc Ns x
+.Xc
+Delete the next
+.Ar n
+characters.
+.It Xo
+.Oo Ar n Oc Ns X
+.Xc
+Delete the previous
+.Ar n
+characters.
+.It D
+Delete to the end of the line.
+.It Xo
+.Oo Ar n Oc Ns d Ns Ar move-cmd
+.Xc
+Delete from the current position to the position resulting from
+.Ar n move-cmd Ns s ;
+.Ar move-cmd
+is a movement command (see above) or
+.Ic d ,
+in which case the current line is deleted.
+.It Xo
+.Oo Ar n Oc Ns r Ns Ar c
+.Xc
+Replace the next
+.Ar n
+characters with the character
+.Ar c .
+.It Xo
+.Oo Ar n Oc Ns R
+.Xc
+Replace.
+Enter insert mode but overwrite existing characters instead of
+inserting before existing characters.
+The replacement is repeated
+.Ar n
+times.
+.It Xo
+.Oo Ar n Oc Ns ~
+.Xc
+Change the case of the next
+.Ar n
+characters.
+.It Xo
+.Oo Ar n Oc Ns y Ns Ar move-cmd
+.Xc
+Yank from the current position to the position resulting from
+.Ar n move-cmd Ns s
+into the yank buffer; if
+.Ar move-cmd
+is
+.Ic y ,
+the whole line is yanked.
+.It Y
+Yank from the current position to the end of the line.
+.It Xo
+.Oo Ar n Oc Ns p
+.Xc
+Paste the contents of the yank buffer just after the current position,
+.Ar n
+times.
+.It Xo
+.Oo Ar n Oc Ns P
+.Xc
+Same as
+.Ic p ,
+except the buffer is pasted at the current position.
+.El
+.Pp
+Miscellaneous vi commands
+.Bl -tag -width Ds
+.It ^J and ^M
+The current line is read, parsed, and executed by the shell.
+.It ^L and ^R
+Redraw the current line.
+.It Xo
+.Oo Ar n Oc Ns \&.
+.Xc
+Redo the last edit command
+.Ar n
+times.
+.It u
+Undo the last edit command.
+.It U
+Undo all changes that have been made to the current line.
+.It Ar intr No and Ar quit
+The interrupt and quit terminal characters cause the current line to be
+deleted and a new prompt to be printed.
+.El
+.Sh FILES
+.Bl -tag -width "/etc/suid_profileXX" -compact
+.It Pa ~/.profile
+User's login profile.
+.It Pa /etc/ksh.kshrc
+Global configuration file.
+Not sourced by default.
+.It Pa /etc/profile
+System login profile.
+.It Pa /etc/shells
+Shell database.
+.It Pa /etc/suid_profile
+Privileged shell profile.
+.El
+.Sh SEE ALSO
+.Xr csh 1 ,
+.Xr ed 1 ,
+.Xr mg 1 ,
+.Xr sh 1 ,
+.Xr stty 1 ,
+.Xr vi 1 ,
+.Xr shells 5 ,
+.Xr environ 7 ,
+.Xr script 7
+.Rs
+.%A Morris Bolsky
+.%A David Korn
+.%B The KornShell Command and Programming Language, 2nd Edition
+.%D 1995
+.%I Prentice Hall
+.%O ISBN 0131827006
+.Re
+.Rs
+.%A Stephen G. Kochan
+.%A Patrick H. Wood
+.%B UNIX Shell Programming, 3rd Edition
+.%D 2003
+.%I Sams
+.%O ISBN 0672324903
+.Re
+.Rs
+.%A IEEE Inc.
+.%D 1993
+.%O ISBN 1-55937-266-9
+.%T IEEE Standard for Information Technology \- Portable Operating \
+    System Interface (POSIX) \- Part 2: Shell and Utilities
+.Re
+.Sh VERSION
+This page documents version @(#)PD KSH v5.2.14 99/07/13.2 of the public
+domain Korn shell.
+.Sh AUTHORS
+.An -nosplit
+This shell is based on the public domain 7th edition Bourne shell clone by
+.An Charles Forsyth
+and parts of the BRL shell by
+.An Doug A. Gwyn ,
+.An Doug Kingston ,
+.An Ron Natalie ,
+.An Arnold Robbins ,
+.An Lou Salkind ,
+and others.
+The first release of
+.Nm pdksh
+was created by
+.An Eric Gisin ,
+and it was subsequently maintained by
+.An John R. MacMillan Aq Mt change!john@sq.sq.com ,
+.An Simon J. Gerraty Aq Mt sjg@zen.void.oz.au ,
+and
+.An Michael Rendell Aq Mt michael@cs.mun.ca .
+The
+.Pa CONTRIBUTORS
+file in the source distribution contains a more complete list of people and
+their part in the shell's development.
+.Sh BUGS
+.Pf $( Ar command )
+expressions are currently parsed by finding the closest matching (unquoted)
+parenthesis.
+Thus constructs inside
+.Pf $( Ar command )
+may produce an error.
+For example, the parenthesis in
+.Ql x);;
+is interpreted as the closing parenthesis in
+.Ql $(case x in x);; *);; esac) .
diff --git a/lex.c b/lex.c
@@ -0,0 +1,1668 @@
+/*	$OpenBSD: lex.c,v 1.78 2018/01/15 14:58:05 jca Exp $	*/
+
+/*
+ * lexical analysis and source input
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <libgen.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+/*
+ * states while lexing word
+ */
+#define	SINVALID	-1	/* invalid state */
+#define	SBASE	0		/* outside any lexical constructs */
+#define	SWORD	1		/* implicit quoting for substitute() */
+#define	SLETPAREN 2		/* inside (( )), implicit quoting */
+#define	SSQUOTE	3		/* inside '' */
+#define	SDQUOTE	4		/* inside "" */
+#define	SBRACE	5		/* inside ${} */
+#define	SCSPAREN 6		/* inside $() */
+#define	SBQUOTE	7		/* inside `` */
+#define	SASPAREN 8		/* inside $(( )) */
+#define SHEREDELIM 9		/* parsing <<,<<- delimiter */
+#define SHEREDQUOTE 10		/* parsing " in <<,<<- delimiter */
+#define SPATTERN 11		/* parsing *(...|...) pattern (*+?@!) */
+#define STBRACE 12		/* parsing ${..[#%]..} */
+#define	SBRACEQ	13		/* inside "${}" */
+
+/* Structure to keep track of the lexing state and the various pieces of info
+ * needed for each particular state.
+ */
+typedef struct lex_state Lex_state;
+struct lex_state {
+	int ls_state;
+	union {
+		/* $(...) */
+		struct scsparen_info {
+			int nparen;	/* count open parenthesis */
+			int csstate;	/* XXX remove */
+#define ls_scsparen ls_info.u_scsparen
+		} u_scsparen;
+
+		/* $((...)) */
+		struct sasparen_info {
+			int nparen;	/* count open parenthesis */
+			int start;	/* marks start of $(( in output str */
+#define ls_sasparen ls_info.u_sasparen
+		} u_sasparen;
+
+		/* ((...)) */
+		struct sletparen_info {
+			int nparen;	/* count open parenthesis */
+#define ls_sletparen ls_info.u_sletparen
+		} u_sletparen;
+
+		/* `...` */
+		struct sbquote_info {
+			int indquotes;	/* true if in double quotes: "`...`" */
+#define ls_sbquote ls_info.u_sbquote
+		} u_sbquote;
+
+		Lex_state *base;	/* used to point to next state block */
+	} ls_info;
+};
+
+typedef struct State_info State_info;
+struct State_info {
+	Lex_state	*base;
+	Lex_state	*end;
+};
+
+
+static void	readhere(struct ioword *);
+static int	getsc__(void);
+static void	getsc_line(Source *);
+static int	getsc_bn(void);
+static char	*get_brace_var(XString *, char *);
+static int	arraysub(char **);
+static const char *ungetsc(int);
+static void	gethere(void);
+static Lex_state *push_state_(State_info *, Lex_state *);
+static Lex_state *pop_state_(State_info *, Lex_state *);
+static char	*special_prompt_expand(char *);
+static int	dopprompt(const char *, int, const char **, int);
+int		promptlen(const char *cp, const char **spp);
+
+static int backslash_skip;
+static int ignore_backslash_newline;
+
+Source *source;		/* yyparse/yylex source */
+YYSTYPE	yylval;		/* result from yylex */
+struct ioword *heres[HERES], **herep;
+char	ident[IDENT+1];
+
+char   **history;	/* saved commands */
+char   **histptr;	/* last history item */
+uint32_t histsize;	/* history size */
+
+/* optimized getsc_bn() */
+#define getsc()		(*source->str != '\0' && *source->str != '\\' \
+			 && !backslash_skip ? *source->str++ : getsc_bn())
+/* optimized getsc__() */
+#define	getsc_()	((*source->str != '\0') ? *source->str++ : getsc__())
+
+#define STATE_BSIZE	32
+
+#define PUSH_STATE(s)	do { \
+			    if (++statep == state_info.end) \
+				statep = push_state_(&state_info, statep); \
+			    state = statep->ls_state = (s); \
+			} while (0)
+
+#define POP_STATE()	do { \
+			    if (--statep == state_info.base) \
+				statep = pop_state_(&state_info, statep); \
+			    state = statep->ls_state; \
+			} while (0)
+
+
+
+/*
+ * Lexical analyzer
+ *
+ * tokens are not regular expressions, they are LL(1).
+ * for example, "${var:-${PWD}}", and "$(size $(whence ksh))".
+ * hence the state stack.
+ */
+
+int
+yylex(int cf)
+{
+	Lex_state states[STATE_BSIZE], *statep;
+	State_info state_info;
+	int c, state;
+	XString ws;		/* expandable output word */
+	char *wp;		/* output word pointer */
+	char *sp, *dp;
+	int c2;
+
+
+  Again:
+	states[0].ls_state = SINVALID;
+	states[0].ls_info.base = NULL;
+	statep = &states[1];
+	state_info.base = states;
+	state_info.end = &states[STATE_BSIZE];
+
+	Xinit(ws, wp, 64, ATEMP);
+
+	backslash_skip = 0;
+	ignore_backslash_newline = 0;
+
+	if (cf&ONEWORD)
+		state = SWORD;
+	else if (cf&LETEXPR) {
+		*wp++ = OQUOTE;	 /* enclose arguments in (double) quotes */
+		state = SLETPAREN;
+		statep->ls_sletparen.nparen = 0;
+	} else {		/* normal lexing */
+		state = (cf & HEREDELIM) ? SHEREDELIM : SBASE;
+		while ((c = getsc()) == ' ' || c == '\t')
+			;
+		if (c == '#') {
+			ignore_backslash_newline++;
+			while ((c = getsc()) != '\0' && c != '\n')
+				;
+			ignore_backslash_newline--;
+		}
+		ungetsc(c);
+	}
+	if (source->flags & SF_ALIAS) {	/* trailing ' ' in alias definition */
+		source->flags &= ~SF_ALIAS;
+		/* In POSIX mode, a trailing space only counts if we are
+		 * parsing a simple command
+		 */
+		if (!Flag(FPOSIX) || (cf & CMDWORD))
+			cf |= ALIAS;
+	}
+
+	/* Initial state: one of SBASE SHEREDELIM SWORD SASPAREN */
+	statep->ls_state = state;
+
+	/* collect non-special or quoted characters to form word */
+	while (!((c = getsc()) == 0 ||
+	    ((state == SBASE || state == SHEREDELIM) && ctype(c, C_LEX1)))) {
+		Xcheck(ws, wp);
+		switch (state) {
+		case SBASE:
+			if (Flag(FCSHHISTORY) && (source->flags & SF_TTY) &&
+			    c == '!') {
+				char **replace = NULL;
+				int get, i;
+				char match[200] = { 0 }, *str = match;
+				size_t mlen;
+
+				c2 = getsc();
+				if (c2 == '\0' || c2 == ' ' || c2 == '\t')
+					;
+				else if (c2 == '!')
+					replace = hist_get_newest(0);
+				else if (isdigit(c2) || c2 == '-' ||
+				    isalpha(c2)) {
+					get = !isalpha(c2);
+
+					*str++ = c2;
+					do {
+						if ((c2 = getsc()) == '\0')
+							break;
+						if (c2 == '\t' || c2 == ' ' ||
+						    c2 == '\n') {
+							ungetsc(c2);
+							break;
+						}
+						*str++ = c2;
+					} while (str < &match[sizeof(match)-1]);
+					*str = '\0';
+
+					if (get) {
+						int h = findhistrel(match);
+						if (h >= 0)
+							replace = &history[h];
+					} else {
+						int h = findhist(-1, 0, match, true);
+						if (h >= 0)
+							replace = &history[h];
+					}
+				}
+
+				/*
+				 * XXX ksh history buffer saves un-expanded
+				 * commands. Until the history buffer code is
+				 * changed to contain expanded commands, we
+				 * ignore the bad commands (spinning sucks)
+				 */
+				if (replace && **replace == '!')
+					ungetsc(c2);
+				else if (replace) {
+					Source *s;
+
+					/* do not strdup replacement via alloc */
+					s = pushs(SREREAD, source->areap);
+					s->start = s->str = *replace;
+					s->next = source;
+					s->u.freeme = NULL;
+					source = s;
+					continue;
+				} else if (*match != '\0') {
+					/* restore what followed the '!' */
+					mlen = strlen(match);
+					for (i = mlen-1; i >= 0; i--)
+						ungetsc(match[i]);
+				} else
+					ungetsc(c2);
+			}
+			if (c == '[' && (cf & (VARASN|ARRAYVAR))) {
+				*wp = EOS; /* temporary */
+				if (is_wdvarname(Xstring(ws, wp), false)) {
+					char *p, *tmp;
+
+					if (arraysub(&tmp)) {
+						*wp++ = CHAR;
+						*wp++ = c;
+						for (p = tmp; *p; ) {
+							Xcheck(ws, wp);
+							*wp++ = CHAR;
+							*wp++ = *p++;
+						}
+						afree(tmp, ATEMP);
+						break;
+					} else {
+						Source *s;
+
+						s = pushs(SREREAD,
+							  source->areap);
+						s->start = s->str
+							= s->u.freeme = tmp;
+						s->next = source;
+						source = s;
+					}
+				}
+				*wp++ = CHAR;
+				*wp++ = c;
+				break;
+			}
+			/* FALLTHROUGH */
+		  Sbase1:	/* includes *(...|...) pattern (*+?@!) */
+			if (c == '*' || c == '@' || c == '+' || c == '?' ||
+			    c == '!') {
+				c2 = getsc();
+				if (c2 == '(' /*)*/ ) {
+					*wp++ = OPAT;
+					*wp++ = c;
+					PUSH_STATE(SPATTERN);
+					break;
+				}
+				ungetsc(c2);
+			}
+			/* FALLTHROUGH */
+		  Sbase2:	/* doesn't include *(...|...) pattern (*+?@!) */
+			switch (c) {
+			case '\\':
+				c = getsc();
+				if (c) /* trailing \ is lost */
+					*wp++ = QCHAR, *wp++ = c;
+				break;
+			case '\'':
+				if ((cf & HEREDOC) || state == SBRACEQ) {
+					*wp++ = CHAR, *wp++ = c;
+					break;
+				}
+				*wp++ = OQUOTE;
+				ignore_backslash_newline++;
+				PUSH_STATE(SSQUOTE);
+				break;
+			case '"':
+				*wp++ = OQUOTE;
+				PUSH_STATE(SDQUOTE);
+				break;
+			default:
+				goto Subst;
+			}
+			break;
+
+		  Subst:
+			switch (c) {
+			case '\\':
+				c = getsc();
+				switch (c) {
+				case '\\':
+				case '$': case '`':
+					*wp++ = QCHAR, *wp++ = c;
+					break;
+				case '"':
+					if ((cf & HEREDOC) == 0) {
+						*wp++ = QCHAR, *wp++ = c;
+						break;
+					}
+					/* FALLTHROUGH */
+				default:
+					if (cf & UNESCAPE) {
+						*wp++ = QCHAR, *wp++ = c;
+						break;
+					}
+					Xcheck(ws, wp);
+					if (c) { /* trailing \ is lost */
+						*wp++ = CHAR, *wp++ = '\\';
+						*wp++ = CHAR, *wp++ = c;
+					}
+					break;
+				}
+				break;
+			case '$':
+				c = getsc();
+				if (c == '(') /*)*/ {
+					c = getsc();
+					if (c == '(') /*)*/ {
+						PUSH_STATE(SASPAREN);
+						statep->ls_sasparen.nparen = 2;
+						statep->ls_sasparen.start =
+						    Xsavepos(ws, wp);
+						*wp++ = EXPRSUB;
+					} else {
+						ungetsc(c);
+						PUSH_STATE(SCSPAREN);
+						statep->ls_scsparen.nparen = 1;
+						statep->ls_scsparen.csstate = 0;
+						*wp++ = COMSUB;
+					}
+				} else if (c == '{') /*}*/ {
+					*wp++ = OSUBST;
+					*wp++ = '{'; /*}*/
+					wp = get_brace_var(&ws, wp);
+					c = getsc();
+					/* allow :# and :% (ksh88 compat) */
+					if (c == ':') {
+						*wp++ = CHAR, *wp++ = c;
+						c = getsc();
+					}
+					/* If this is a trim operation,
+					 * treat (,|,) specially in STBRACE.
+					 */
+					if (c == '#' || c == '%') {
+						ungetsc(c);
+						PUSH_STATE(STBRACE);
+					} else {
+						ungetsc(c);
+						if (state == SDQUOTE ||
+						    state == SBRACEQ)
+							PUSH_STATE(SBRACEQ);
+						else
+							PUSH_STATE(SBRACE);
+					}
+				} else if (ctype(c, C_ALPHA)) {
+					*wp++ = OSUBST;
+					*wp++ = 'X';
+					do {
+						Xcheck(ws, wp);
+						*wp++ = c;
+						c = getsc();
+					} while (ctype(c, C_ALPHA) || digit(c));
+					*wp++ = '\0';
+					*wp++ = CSUBST;
+					*wp++ = 'X';
+					ungetsc(c);
+				} else if (ctype(c, C_VAR1) || digit(c)) {
+					Xcheck(ws, wp);
+					*wp++ = OSUBST;
+					*wp++ = 'X';
+					*wp++ = c;
+					*wp++ = '\0';
+					*wp++ = CSUBST;
+					*wp++ = 'X';
+				} else {
+					*wp++ = CHAR, *wp++ = '$';
+					ungetsc(c);
+				}
+				break;
+			case '`':
+				PUSH_STATE(SBQUOTE);
+				*wp++ = COMSUB;
+				/* Need to know if we are inside double quotes
+				 * since sh/at&t-ksh translate the \" to " in
+				 * "`..\"..`".
+				 */
+				statep->ls_sbquote.indquotes = 0;
+				Lex_state *s = statep;
+				Lex_state *base = state_info.base;
+				while (1) {
+					for (; s != base; s--) {
+						if (s->ls_state == SDQUOTE) {
+							statep->ls_sbquote.indquotes = 1;
+							break;
+						}
+					}
+					if (s != base)
+						break;
+					if (!(s = s->ls_info.base))
+						break;
+					base = s-- - STATE_BSIZE;
+				}
+				break;
+			default:
+				*wp++ = CHAR, *wp++ = c;
+			}
+			break;
+
+		case SSQUOTE:
+			if (c == '\'') {
+				POP_STATE();
+				if (state == SBRACEQ) {
+					*wp++ = CHAR, *wp++ = c;
+					break;
+				}
+				*wp++ = CQUOTE;
+				ignore_backslash_newline--;
+			} else
+				*wp++ = QCHAR, *wp++ = c;
+			break;
+
+		case SDQUOTE:
+			if (c == '"') {
+				POP_STATE();
+				*wp++ = CQUOTE;
+			} else
+				goto Subst;
+			break;
+
+		case SCSPAREN: /* $( .. ) */
+			/* todo: deal with $(...) quoting properly
+			 * kludge to partly fake quoting inside $(..): doesn't
+			 * really work because nested $(..) or ${..} inside
+			 * double quotes aren't dealt with.
+			 */
+			switch (statep->ls_scsparen.csstate) {
+			case 0: /* normal */
+				switch (c) {
+				case '(':
+					statep->ls_scsparen.nparen++;
+					break;
+				case ')':
+					statep->ls_scsparen.nparen--;
+					break;
+				case '\\':
+					statep->ls_scsparen.csstate = 1;
+					break;
+				case '"':
+					statep->ls_scsparen.csstate = 2;
+					break;
+				case '\'':
+					statep->ls_scsparen.csstate = 4;
+					ignore_backslash_newline++;
+					break;
+				}
+				break;
+
+			case 1: /* backslash in normal mode */
+			case 3: /* backslash in double quotes */
+				--statep->ls_scsparen.csstate;
+				break;
+
+			case 2: /* double quotes */
+				if (c == '"')
+					statep->ls_scsparen.csstate = 0;
+				else if (c == '\\')
+					statep->ls_scsparen.csstate = 3;
+				break;
+
+			case 4: /* single quotes */
+				if (c == '\'') {
+					statep->ls_scsparen.csstate = 0;
+					ignore_backslash_newline--;
+				}
+				break;
+			}
+			if (statep->ls_scsparen.nparen == 0) {
+				POP_STATE();
+				*wp++ = 0; /* end of COMSUB */
+			} else
+				*wp++ = c;
+			break;
+
+		case SASPAREN: /* $(( .. )) */
+			/* todo: deal with $((...); (...)) properly */
+			/* XXX should nest using existing state machine
+			 * (embed "..", $(...), etc.) */
+			if (c == '(')
+				statep->ls_sasparen.nparen++;
+			else if (c == ')') {
+				statep->ls_sasparen.nparen--;
+				if (statep->ls_sasparen.nparen == 1) {
+					/*(*/
+					if ((c2 = getsc()) == ')') {
+						POP_STATE();
+						*wp++ = 0; /* end of EXPRSUB */
+						break;
+					} else {
+						char *s;
+
+						ungetsc(c2);
+						/* mismatched parenthesis -
+						 * assume we were really
+						 * parsing a $(..) expression
+						 */
+						s = Xrestpos(ws, wp,
+						    statep->ls_sasparen.start);
+						memmove(s + 1, s, wp - s);
+						*s++ = COMSUB;
+						*s = '('; /*)*/
+						wp++;
+						statep->ls_scsparen.nparen = 1;
+						statep->ls_scsparen.csstate = 0;
+						state = statep->ls_state =
+						    SCSPAREN;
+					}
+				}
+			}
+			*wp++ = c;
+			break;
+
+		case SBRACEQ:
+			/*{*/
+			if (c == '}') {
+				POP_STATE();
+				*wp++ = CSUBST;
+				*wp++ = /*{*/ '}';
+			} else
+				goto Sbase2;
+			break;
+
+		case SBRACE:
+			/*{*/
+			if (c == '}') {
+				POP_STATE();
+				*wp++ = CSUBST;
+				*wp++ = /*{*/ '}';
+			} else
+				goto Sbase1;
+			break;
+
+		case STBRACE:
+			/* Same as SBRACE, except (,|,) treated specially */
+			/*{*/
+			if (c == '}') {
+				POP_STATE();
+				*wp++ = CSUBST;
+				*wp++ = /*{*/ '}';
+			} else if (c == '|') {
+				*wp++ = SPAT;
+			} else if (c == '(') {
+				*wp++ = OPAT;
+				*wp++ = ' ';	/* simile for @ */
+				PUSH_STATE(SPATTERN);
+			} else
+				goto Sbase1;
+			break;
+
+		case SBQUOTE:
+			if (c == '`') {
+				*wp++ = 0;
+				POP_STATE();
+			} else if (c == '\\') {
+				switch (c = getsc()) {
+				case '\\':
+				case '$': case '`':
+					*wp++ = c;
+					break;
+				case '"':
+					if (statep->ls_sbquote.indquotes) {
+						*wp++ = c;
+						break;
+					}
+					/* FALLTHROUGH */
+				default:
+					if (c) { /* trailing \ is lost */
+						*wp++ = '\\';
+						*wp++ = c;
+					}
+					break;
+				}
+			} else
+				*wp++ = c;
+			break;
+
+		case SWORD:	/* ONEWORD */
+			goto Subst;
+
+		case SLETPAREN:	/* LETEXPR: (( ... )) */
+			/*(*/
+			if (c == ')') {
+				if (statep->ls_sletparen.nparen > 0)
+				    --statep->ls_sletparen.nparen;
+				/*(*/
+				else if ((c2 = getsc()) == ')') {
+					c = 0;
+					*wp++ = CQUOTE;
+					goto Done;
+				} else
+					ungetsc(c2);
+			} else if (c == '(')
+				/* parenthesis inside quotes and backslashes
+				 * are lost, but at&t ksh doesn't count them
+				 * either
+				 */
+				++statep->ls_sletparen.nparen;
+			goto Sbase2;
+
+		case SHEREDELIM:	/* <<,<<- delimiter */
+			/* XXX chuck this state (and the next) - use
+			 * the existing states ($ and \`..` should be
+			 * stripped of their specialness after the
+			 * fact).
+			 */
+			/* here delimiters need a special case since
+			 * $ and `..` are not to be treated specially
+			 */
+			if (c == '\\') {
+				c = getsc();
+				if (c) { /* trailing \ is lost */
+					*wp++ = QCHAR;
+					*wp++ = c;
+				}
+			} else if (c == '\'') {
+				PUSH_STATE(SSQUOTE);
+				*wp++ = OQUOTE;
+				ignore_backslash_newline++;
+			} else if (c == '"') {
+				state = statep->ls_state = SHEREDQUOTE;
+				*wp++ = OQUOTE;
+			} else {
+				*wp++ = CHAR;
+				*wp++ = c;
+			}
+			break;
+
+		case SHEREDQUOTE:	/* " in <<,<<- delimiter */
+			if (c == '"') {
+				*wp++ = CQUOTE;
+				state = statep->ls_state = SHEREDELIM;
+			} else {
+				if (c == '\\') {
+					switch (c = getsc()) {
+					case '\\': case '"':
+					case '$': case '`':
+						break;
+					default:
+						if (c) { /* trailing \ lost */
+							*wp++ = CHAR;
+							*wp++ = '\\';
+						}
+						break;
+					}
+				}
+				*wp++ = CHAR;
+				*wp++ = c;
+			}
+			break;
+
+		case SPATTERN:	/* in *(...|...) pattern (*+?@!) */
+			if ( /*(*/ c == ')') {
+				*wp++ = CPAT;
+				POP_STATE();
+			} else if (c == '|') {
+				*wp++ = SPAT;
+			} else if (c == '(') {
+				*wp++ = OPAT;
+				*wp++ = ' ';	/* simile for @ */
+				PUSH_STATE(SPATTERN);
+			} else
+				goto Sbase1;
+			break;
+		}
+	}
+Done:
+	Xcheck(ws, wp);
+	if (statep != &states[1])
+		/* XXX figure out what is missing */
+		yyerror("no closing quote\n");
+
+	/* This done to avoid tests for SHEREDELIM wherever SBASE tested */
+	if (state == SHEREDELIM)
+		state = SBASE;
+
+	dp = Xstring(ws, wp);
+	if ((c == '<' || c == '>') && state == SBASE &&
+	    ((c2 = Xlength(ws, wp)) == 0 ||
+	    (c2 == 2 && dp[0] == CHAR && digit(dp[1])))) {
+		struct ioword *iop = alloc(sizeof(*iop), ATEMP);
+
+		if (c2 == 2)
+			iop->unit = dp[1] - '0';
+		else
+			iop->unit = c == '>'; /* 0 for <, 1 for > */
+
+		c2 = getsc();
+		/* <<, >>, <> are ok, >< is not */
+		if (c == c2 || (c == '<' && c2 == '>')) {
+			iop->flag = c == c2 ?
+			    (c == '>' ? IOCAT : IOHERE) : IORDWR;
+			if (iop->flag == IOHERE) {
+				if ((c2 = getsc()) == '-')
+					iop->flag |= IOSKIP;
+				else
+					ungetsc(c2);
+			}
+		} else if (c2 == '&')
+			iop->flag = IODUP | (c == '<' ? IORDUP : 0);
+		else {
+			iop->flag = c == '>' ? IOWRITE : IOREAD;
+			if (c == '>' && c2 == '|')
+				iop->flag |= IOCLOB;
+			else
+				ungetsc(c2);
+		}
+
+		iop->name = NULL;
+		iop->delim = NULL;
+		iop->heredoc = NULL;
+		Xfree(ws, wp);	/* free word */
+		yylval.iop = iop;
+		return REDIR;
+	}
+
+	if (wp == dp && state == SBASE) {
+		Xfree(ws, wp);	/* free word */
+		/* no word, process LEX1 character */
+		switch (c) {
+		default:
+			return c;
+
+		case '|':
+		case '&':
+		case ';':
+			if ((c2 = getsc()) == c)
+				c = (c == ';') ? BREAK :
+				    (c == '|') ? LOGOR :
+				    (c == '&') ? LOGAND :
+				    YYERRCODE;
+			else if (c == '|' && c2 == '&')
+				c = COPROC;
+			else
+				ungetsc(c2);
+			return c;
+
+		case '\n':
+			gethere();
+			if (cf & CONTIN)
+				goto Again;
+			return c;
+
+		case '(':  /*)*/
+			if (!Flag(FSH)) {
+				if ((c2 = getsc()) == '(') /*)*/
+					/* XXX need to handle ((...); (...)) */
+					c = MDPAREN;
+				else
+					ungetsc(c2);
+			}
+			return c;
+		  /*(*/
+		case ')':
+			return c;
+		}
+	}
+
+	*wp++ = EOS;		/* terminate word */
+	yylval.cp = Xclose(ws, wp);
+	if (state == SWORD || state == SLETPAREN)	/* ONEWORD? */
+		return LWORD;
+	ungetsc(c);		/* unget terminator */
+
+	/* copy word to unprefixed string ident */
+	for (sp = yylval.cp, dp = ident; dp < ident+IDENT && (c = *sp++) == CHAR; )
+		*dp++ = *sp++;
+	/* Make sure the ident array stays '\0' padded */
+	memset(dp, 0, (ident+IDENT) - dp + 1);
+	if (c != EOS)
+		*ident = '\0';	/* word is not unquoted */
+
+	if (*ident != '\0' && (cf&(KEYWORD|ALIAS))) {
+		struct tbl *p;
+		int h = hash(ident);
+
+		/* { */
+		if ((cf & KEYWORD) && (p = ktsearch(&keywords, ident, h)) &&
+		    (!(cf & ESACONLY) || p->val.i == ESAC || p->val.i == '}')) {
+			afree(yylval.cp, ATEMP);
+			return p->val.i;
+		}
+		if ((cf & ALIAS) && (p = ktsearch(&aliases, ident, h)) &&
+		    (p->flag & ISSET)) {
+			Source *s;
+
+			for (s = source; s->type == SALIAS; s = s->next)
+				if (s->u.tblp == p)
+					return LWORD;
+			/* push alias expansion */
+			s = pushs(SALIAS, source->areap);
+			s->start = s->str = p->val.s;
+			s->u.tblp = p;
+			s->next = source;
+			source = s;
+			afree(yylval.cp, ATEMP);
+			goto Again;
+		}
+	}
+
+	return LWORD;
+}
+
+static void
+gethere(void)
+{
+	struct ioword **p;
+
+	for (p = heres; p < herep; p++)
+		readhere(*p);
+	herep = heres;
+}
+
+/*
+ * read "<<word" text into temp file
+ */
+
+static void
+readhere(struct ioword *iop)
+{
+	int c;
+	char *volatile eof;
+	char *eofp;
+	int skiptabs;
+	XString xs;
+	char *xp;
+	int xpos;
+
+	eof = evalstr(iop->delim, 0);
+
+	if (!(iop->flag & IOEVAL))
+		ignore_backslash_newline++;
+
+	Xinit(xs, xp, 256, ATEMP);
+
+	for (;;) {
+		eofp = eof;
+		skiptabs = iop->flag & IOSKIP;
+		xpos = Xsavepos(xs, xp);
+		while ((c = getsc()) != 0) {
+			if (skiptabs) {
+				if (c == '\t')
+					continue;
+				skiptabs = 0;
+			}
+			if (c != *eofp)
+				break;
+			Xcheck(xs, xp);
+			Xput(xs, xp, c);
+			eofp++;
+		}
+		/* Allow EOF here so commands with out trailing newlines
+		 * will work (eg, ksh -c '...', $(...), etc).
+		 */
+		if (*eofp == '\0' && (c == 0 || c == '\n')) {
+			xp = Xrestpos(xs, xp, xpos);
+			break;
+		}
+		ungetsc(c);
+		while ((c = getsc()) != '\n') {
+			if (c == 0)
+				yyerror("here document `%s' unclosed\n", eof);
+			Xcheck(xs, xp);
+			Xput(xs, xp, c);
+		}
+		Xcheck(xs, xp);
+		Xput(xs, xp, c);
+	}
+	Xput(xs, xp, '\0');
+	iop->heredoc = Xclose(xs, xp);
+
+	if (!(iop->flag & IOEVAL))
+		ignore_backslash_newline--;
+}
+
+void
+yyerror(const char *fmt, ...)
+{
+	va_list va;
+
+	/* pop aliases and re-reads */
+	while (source->type == SALIAS || source->type == SREREAD)
+		source = source->next;
+	source->str = null;	/* zap pending input */
+
+	error_prefix(true);
+	va_start(va, fmt);
+	shf_vfprintf(shl_out, fmt, va);
+	va_end(va);
+	errorf(NULL);
+}
+
+/*
+ * input for yylex with alias expansion
+ */
+
+Source *
+pushs(int type, Area *areap)
+{
+	Source *s;
+
+	s = alloc(sizeof(Source), areap);
+	s->type = type;
+	s->str = null;
+	s->start = NULL;
+	s->line = 0;
+	s->cmd_offset = 0;
+	s->errline = 0;
+	s->file = NULL;
+	s->flags = 0;
+	s->next = NULL;
+	s->areap = areap;
+	if (type == SFILE || type == SSTDIN) {
+		char *dummy;
+		Xinit(s->xs, dummy, 256, s->areap);
+	} else
+		memset(&s->xs, 0, sizeof(s->xs));
+	return s;
+}
+
+static int
+getsc__(void)
+{
+	Source *s = source;
+	int c;
+
+	while ((c = *s->str++) == 0) {
+		s->str = NULL;		/* return 0 for EOF by default */
+		switch (s->type) {
+		case SEOF:
+			s->str = null;
+			return 0;
+
+		case SSTDIN:
+		case SFILE:
+			getsc_line(s);
+			break;
+
+		case SWSTR:
+			break;
+
+		case SSTRING:
+			break;
+
+		case SWORDS:
+			s->start = s->str = *s->u.strv++;
+			s->type = SWORDSEP;
+			break;
+
+		case SWORDSEP:
+			if (*s->u.strv == NULL) {
+				s->start = s->str = "\n";
+				s->type = SEOF;
+			} else {
+				s->start = s->str = " ";
+				s->type = SWORDS;
+			}
+			break;
+
+		case SALIAS:
+			if (s->flags & SF_ALIASEND) {
+				/* pass on an unused SF_ALIAS flag */
+				source = s->next;
+				source->flags |= s->flags & SF_ALIAS;
+				s = source;
+			} else if (*s->u.tblp->val.s &&
+			    isspace((unsigned char)strchr(s->u.tblp->val.s, 0)[-1])) {
+				source = s = s->next;	/* pop source stack */
+				/* Note that this alias ended with a space,
+				 * enabling alias expansion on the following
+				 * word.
+				 */
+				s->flags |= SF_ALIAS;
+			} else {
+				/* At this point, we need to keep the current
+				 * alias in the source list so recursive
+				 * aliases can be detected and we also need
+				 * to return the next character.  Do this
+				 * by temporarily popping the alias to get
+				 * the next character and then put it back
+				 * in the source list with the SF_ALIASEND
+				 * flag set.
+				 */
+				source = s->next;	/* pop source stack */
+				source->flags |= s->flags & SF_ALIAS;
+				c = getsc__();
+				if (c) {
+					s->flags |= SF_ALIASEND;
+					s->ugbuf[0] = c; s->ugbuf[1] = '\0';
+					s->start = s->str = s->ugbuf;
+					s->next = source;
+					source = s;
+				} else {
+					s = source;
+					/* avoid reading eof twice */
+					s->str = NULL;
+					break;
+				}
+			}
+			continue;
+
+		case SREREAD:
+			if (s->start != s->ugbuf) /* yuck */
+				afree(s->u.freeme, ATEMP);
+			source = s = s->next;
+			continue;
+		}
+		if (s->str == NULL) {
+			s->type = SEOF;
+			s->start = s->str = null;
+			return '\0';
+		}
+		if (s->flags & SF_ECHO) {
+			shf_puts(s->str, shl_out);
+			shf_flush(shl_out);
+		}
+	}
+	return c;
+}
+
+static void
+getsc_line(Source *s)
+{
+	char *xp = Xstring(s->xs, xp);
+	int interactive = Flag(FTALKING) && s->type == SSTDIN;
+	int have_tty = interactive && (s->flags & SF_TTY);
+
+	/* Done here to ensure nothing odd happens when a timeout occurs */
+	XcheckN(s->xs, xp, LINE);
+	*xp = '\0';
+	s->start = s->str = xp;
+
+	if (have_tty && ksh_tmout) {
+		ksh_tmout_state = TMOUT_READING;
+		alarm(ksh_tmout);
+	}
+	if (have_tty && (0
+#ifdef VI
+	    || Flag(FVI)
+#endif /* VI */
+#ifdef EMACS
+	    || Flag(FEMACS) || Flag(FGMACS)
+#endif /* EMACS */
+	    )) {
+		int nread;
+
+		nread = x_read(xp, LINE);
+		if (nread < 0)	/* read error */
+			nread = 0;
+		xp[nread] = '\0';
+		xp += nread;
+	} else {
+		if (interactive) {
+			pprompt(prompt, 0);
+		} else
+			s->line++;
+
+		while (1) {
+			char *p = shf_getse(xp, Xnleft(s->xs, xp), s->u.shf);
+
+			if (!p && shf_error(s->u.shf) &&
+			    s->u.shf->errno_ == EINTR) {
+				shf_clearerr(s->u.shf);
+				if (trap)
+					runtraps(0);
+				continue;
+			}
+			if (!p || (xp = p, xp[-1] == '\n'))
+				break;
+			/* double buffer size */
+			xp++; /* move past null so doubling works... */
+			XcheckN(s->xs, xp, Xlength(s->xs, xp));
+			xp--; /* ...and move back again */
+		}
+		/* flush any unwanted input so other programs/builtins
+		 * can read it.  Not very optimal, but less error prone
+		 * than flushing else where, dealing with redirections,
+		 * etc..
+		 * todo: reduce size of shf buffer (~128?) if SSTDIN
+		 */
+		if (s->type == SSTDIN)
+			shf_flush(s->u.shf);
+	}
+	/* XXX: temporary kludge to restore source after a
+	 * trap may have been executed.
+	 */
+	source = s;
+	if (have_tty && ksh_tmout) {
+		ksh_tmout_state = TMOUT_EXECUTING;
+		alarm(0);
+	}
+	s->start = s->str = Xstring(s->xs, xp);
+	strip_nuls(Xstring(s->xs, xp), Xlength(s->xs, xp));
+	/* Note: if input is all nulls, this is not eof */
+	if (Xlength(s->xs, xp) == 0) { /* EOF */
+		if (s->type == SFILE)
+			shf_fdclose(s->u.shf);
+		s->str = NULL;
+	} else if (interactive) {
+		char *p = Xstring(s->xs, xp);
+		if (cur_prompt == PS1)
+			while (*p && ctype(*p, C_IFS) && ctype(*p, C_IFSWS))
+				p++;
+		if (*p) {
+			s->line++;
+			histsave(s->line, s->str, 1);
+		}
+	}
+	if (interactive)
+		set_prompt(PS2);
+}
+
+static char *
+special_prompt_expand(char *str)
+{
+	char *p = str;
+
+	while ((p = strstr(p, "\\$")) != NULL) {
+		*(p+1) = 'p';
+	}
+	return str;
+}
+
+void
+set_prompt(int to)
+{
+	char *ps1;
+	Area *saved_atemp;
+
+	cur_prompt = to;
+
+	switch (to) {
+	case PS1: /* command */
+		ps1 = str_save(str_val(global("PS1")), ATEMP);
+		saved_atemp = ATEMP;	/* ps1 is freed by substitute() */
+		newenv(E_ERRH);
+		if (sigsetjmp(genv->jbuf, 0)) {
+			prompt = safe_prompt;
+			/* Don't print an error - assume it has already
+			 * been printed.  Reason is we may have forked
+			 * to run a command and the child may be
+			 * unwinding its stack through this code as it
+			 * exits.
+			 */
+		} else {
+			/* expand \$ before other substitutions are done */
+			char *tmp = special_prompt_expand(ps1);
+			prompt = str_save(substitute(tmp, 0), saved_atemp);
+		}
+		quitenv(NULL);
+		break;
+	case PS2: /* command continuation */
+		prompt = str_val(global("PS2"));
+		break;
+	}
+}
+
+static int
+dopprompt(const char *sp, int ntruncate, const char **spp, int doprint)
+{
+	char strbuf[1024], tmpbuf[1024], *p, *str, nbuf[32], delimiter = '\0';
+	int len, c, n, totlen = 0, indelimit = 0, counting = 1, delimitthis;
+	const char *cp = sp;
+	struct tm *tm;
+	time_t t;
+
+	if (*cp && cp[1] == '\r') {
+		delimiter = *cp;
+		cp += 2;
+	}
+
+	while (*cp != 0) {
+		delimitthis = 0;
+		if (indelimit && *cp != delimiter)
+			;
+		else if (*cp == '\n' || *cp == '\r') {
+			totlen = 0;
+			sp = cp + 1;
+		} else if (*cp == '\t') {
+			if (counting)
+				totlen = (totlen | 7) + 1;
+		} else if (*cp == delimiter) {
+			indelimit = !indelimit;
+			delimitthis = 1;
+		}
+
+		if (*cp == '\\') {
+			cp++;
+			if (!*cp)
+				break;
+			/* Expand \h and \$ for both, sh(1) and ksh(1) */
+			if (Flag(FSH) && !(*cp == 'h' || *cp == 'p'))
+				snprintf(strbuf, sizeof strbuf, "\\%c", *cp);
+			else switch (*cp) {
+			case 'a':	/* '\' 'a' bell */
+				strbuf[0] = '\007';
+				strbuf[1] = '\0';
+				break;
+			case 'd':	/* '\' 'd' Dow Mon DD */
+				time(&t);
+				tm = localtime(&t);
+				strftime(strbuf, sizeof strbuf, "%a %b %d", tm);
+				break;
+			case 'D': /* '\' 'D' '{' strftime format '}' */
+				p = strchr(cp + 2, '}');
+				if (cp[1] != '{' || p == NULL) {
+					snprintf(strbuf, sizeof strbuf,
+					    "\\%c", *cp);
+					break;
+				}
+				strlcpy(tmpbuf, cp + 2, sizeof tmpbuf);
+				p = strchr(tmpbuf, '}');
+				if (p)
+					*p = '\0';
+				time(&t);
+				tm = localtime(&t);
+				strftime(strbuf, sizeof strbuf, tmpbuf, tm);
+				cp = strchr(cp + 2, '}');
+				break;
+			case 'e':	/* '\' 'e' escape */
+				strbuf[0] = '\033';
+				strbuf[1] = '\0';
+				break;
+			case 'h':	/* '\' 'h' shortened hostname */
+				gethostname(strbuf, sizeof strbuf);
+				p = strchr(strbuf, '.');
+				if (p)
+					*p = '\0';
+				break;
+			case 'H':	/* '\' 'H' full hostname */
+				gethostname(strbuf, sizeof strbuf);
+				break;
+			case 'j':	/* '\' 'j' number of jobs */
+				snprintf(strbuf, sizeof strbuf, "%d",
+				    j_njobs());
+				break;
+			case 'l':	/* '\' 'l' basename of tty */
+				p = ttyname(0);
+				if (p)
+					p = basename(p);
+				if (p)
+					strlcpy(strbuf, p, sizeof strbuf);
+				break;
+			case 'n':	/* '\' 'n' newline */
+				strbuf[0] = '\n';
+				strbuf[1] = '\0';
+				totlen = 0;	/* reset for prompt re-print */
+				sp = cp + 1;
+				break;
+			case 'p':	/* '\' '$' $ or # */
+				strbuf[0] = ksheuid ? '$' : '#';
+				strbuf[1] = '\0';
+				break;
+			case 'r':	/* '\' 'r' return */
+				strbuf[0] = '\r';
+				strbuf[1] = '\0';
+				totlen = 0;	/* reset for prompt re-print */
+				sp = cp + 1;
+				break;
+			case 's':	/* '\' 's' basename $0 */
+				strlcpy(strbuf, kshname, sizeof strbuf);
+				break;
+			case 't':	/* '\' 't' 24 hour HH:MM:SS */
+				time(&t);
+				tm = localtime(&t);
+				strftime(strbuf, sizeof strbuf, "%T", tm);
+				break;
+			case 'T':	/* '\' 'T' 12 hour HH:MM:SS */
+				time(&t);
+				tm = localtime(&t);
+				strftime(strbuf, sizeof strbuf, "%l:%M:%S", tm);
+				break;
+			case '@':	/* '\' '@' 12 hour am/pm format */
+				time(&t);
+				tm = localtime(&t);
+				strftime(strbuf, sizeof strbuf, "%r", tm);
+				break;
+			case 'A':	/* '\' 'A' 24 hour HH:MM */
+				time(&t);
+				tm = localtime(&t);
+				strftime(strbuf, sizeof strbuf, "%R", tm);
+				break;
+			case 'u':	/* '\' 'u' username */
+				strlcpy(strbuf, username, sizeof strbuf);
+				break;
+			case 'v':	/* '\' 'v' version (short) */
+				p = strchr(ksh_version, ' ');
+				if (p)
+					p = strchr(p + 1, ' ');
+				if (p) {
+					p++;
+					strlcpy(strbuf, p, sizeof strbuf);
+					p = strchr(strbuf, ' ');
+					if (p)
+						*p = '\0';
+				}
+				break;
+			case 'V':	/* '\' 'V' version (long) */
+				strlcpy(strbuf, ksh_version, sizeof strbuf);
+				break;
+			case 'w':	/* '\' 'w' cwd */
+				p = str_val(global("PWD"));
+				n = strlen(str_val(global("HOME")));
+				if (strcmp(p, "/") == 0) {
+					strlcpy(strbuf, p, sizeof strbuf);
+				} else if (strcmp(p, str_val(global("HOME"))) == 0) {
+					strbuf[0] = '~';
+					strbuf[1] = '\0';
+				} else if (strncmp(p, str_val(global("HOME")), n)
+				    == 0 && p[n] == '/') {
+					snprintf(strbuf, sizeof strbuf, "~/%s",
+					    str_val(global("PWD")) + n + 1);
+				} else
+					strlcpy(strbuf, p, sizeof strbuf);
+				break;
+			case 'W':	/* '\' 'W' basename(cwd) */
+				p = str_val(global("PWD"));
+				if (strcmp(p, str_val(global("HOME"))) == 0) {
+					strbuf[0] = '~';
+					strbuf[1] = '\0';
+				} else
+					strlcpy(strbuf, basename(p), sizeof strbuf);
+				break;
+			case '!':	/* '\' '!' history line number */
+				snprintf(strbuf, sizeof strbuf, "%d",
+				    source->line + 1);
+				break;
+			case '#':	/* '\' '#' command line number */
+				snprintf(strbuf, sizeof strbuf, "%d",
+				    source->line - source->cmd_offset + 1);
+				break;
+			case '0':	/* '\' '#' '#' ' #' octal numeric handling */
+			case '1':
+			case '2':
+			case '3':
+			case '4':
+			case '5':
+			case '6':
+			case '7':
+				if ((cp[1] > '7' || cp[1] < '0') ||
+				    (cp[2] > '7' || cp[2] < '0')) {
+					snprintf(strbuf, sizeof strbuf,
+					    "\\%c", *cp);
+					break;
+				}
+				n = (cp[0] - '0') * 8 * 8 + (cp[1] - '0') * 8 +
+				    (cp[2] - '0');
+				snprintf(strbuf, sizeof strbuf, "%c", n);
+				cp += 2;
+				break;
+			case '\\':	/* '\' '\' */
+				strbuf[0] = '\\';
+				strbuf[1] = '\0';
+				break;
+			case '[': /* '\' '[' .... stop counting */
+				strbuf[0] = '\0';
+				counting = 0;
+				break;
+			case ']': /* '\' ']' restart counting */
+				strbuf[0] = '\0';
+				counting = 1;
+				break;
+
+			default:
+				snprintf(strbuf, sizeof strbuf, "\\%c", *cp);
+				break;
+			}
+			cp++;
+
+			str = strbuf;
+			len = strlen(str);
+			if (ntruncate) {
+				if (ntruncate >= len) {
+					ntruncate -= len;
+					continue;
+				}
+				str += ntruncate;
+				len -= ntruncate;
+				ntruncate = 0;
+			}
+			if (doprint)
+				shf_write(str, len, shl_out);
+			if (counting && !indelimit && !delimitthis)
+				totlen += len;
+			continue;
+		} else if (*cp != '!')
+			c = *cp++;
+		else if (*++cp == '!')
+			c = *cp++;
+		else {
+			shf_snprintf(p = nbuf, sizeof(nbuf), "%d",
+			    source->line + 1);
+			len = strlen(nbuf);
+			if (ntruncate) {
+				if (ntruncate >= len) {
+					ntruncate -= len;
+					continue;
+				}
+				p += ntruncate;
+				len -= ntruncate;
+				ntruncate = 0;
+			}
+			if (doprint)
+				shf_write(p, len, shl_out);
+			if (counting && !indelimit && !delimitthis)
+				totlen += len;
+			continue;
+		}
+		if (counting && ntruncate)
+			--ntruncate;
+		else if (doprint) {
+			shf_putc(c, shl_out);
+		}
+		if (counting && !indelimit && !delimitthis)
+			totlen++;
+	}
+	if (doprint)
+		shf_flush(shl_out);
+	if (spp)
+		*spp = sp;
+	return (totlen);
+}
+
+void
+pprompt(const char *cp, int ntruncate)
+{
+	dopprompt(cp, ntruncate, NULL, 1);
+}
+
+int
+promptlen(const char *cp, const char **spp)
+{
+	return dopprompt(cp, 0, spp, 0);
+}
+
+/* Read the variable part of a ${...} expression (ie, up to but not including
+ * the :[-+?=#%] or close-brace.
+ */
+static char *
+get_brace_var(XString *wsp, char *wp)
+{
+	enum parse_state {
+			   PS_INITIAL, PS_SAW_HASH, PS_IDENT,
+			   PS_NUMBER, PS_VAR1, PS_END
+			 }
+		state;
+	char c;
+
+	state = PS_INITIAL;
+	while (1) {
+		c = getsc();
+		/* State machine to figure out where the variable part ends. */
+		switch (state) {
+		case PS_INITIAL:
+			if (c == '#') {
+				state = PS_SAW_HASH;
+				break;
+			}
+			/* FALLTHROUGH */
+		case PS_SAW_HASH:
+			if (letter(c))
+				state = PS_IDENT;
+			else if (digit(c))
+				state = PS_NUMBER;
+			else if (ctype(c, C_VAR1))
+				state = PS_VAR1;
+			else
+				state = PS_END;
+			break;
+		case PS_IDENT:
+			if (!letnum(c)) {
+				state = PS_END;
+				if (c == '[') {
+					char *tmp, *p;
+
+					if (!arraysub(&tmp))
+						yyerror("missing ]\n");
+					*wp++ = c;
+					for (p = tmp; *p; ) {
+						Xcheck(*wsp, wp);
+						*wp++ = *p++;
+					}
+					afree(tmp, ATEMP);
+					c = getsc(); /* the ] */
+				}
+			}
+			break;
+		case PS_NUMBER:
+			if (!digit(c))
+				state = PS_END;
+			break;
+		case PS_VAR1:
+			state = PS_END;
+			break;
+		case PS_END: /* keep gcc happy */
+			break;
+		}
+		if (state == PS_END) {
+			*wp++ = '\0';	/* end of variable part */
+			ungetsc(c);
+			break;
+		}
+		Xcheck(*wsp, wp);
+		*wp++ = c;
+	}
+	return wp;
+}
+
+/*
+ * Save an array subscript - returns true if matching bracket found, false
+ * if eof or newline was found.
+ * (Returned string double null terminated)
+ */
+static int
+arraysub(char **strp)
+{
+	XString ws;
+	char	*wp;
+	char	c;
+	int	depth = 1;	/* we are just past the initial [ */
+
+	Xinit(ws, wp, 32, ATEMP);
+
+	do {
+		c = getsc();
+		Xcheck(ws, wp);
+		*wp++ = c;
+		if (c == '[')
+			depth++;
+		else if (c == ']')
+			depth--;
+	} while (depth > 0 && c && c != '\n');
+
+	*wp++ = '\0';
+	*strp = Xclose(ws, wp);
+
+	return depth == 0 ? 1 : 0;
+}
+
+/* Unget a char: handles case when we are already at the start of the buffer */
+static const char *
+ungetsc(int c)
+{
+	if (backslash_skip)
+		backslash_skip--;
+	/* Don't unget eof... */
+	if (source->str == null && c == '\0')
+		return source->str;
+	if (source->str > source->start)
+		source->str--;
+	else {
+		Source *s;
+
+		s = pushs(SREREAD, source->areap);
+		s->ugbuf[0] = c; s->ugbuf[1] = '\0';
+		s->start = s->str = s->ugbuf;
+		s->next = source;
+		source = s;
+	}
+	return source->str;
+}
+
+
+/* Called to get a char that isn't a \newline sequence. */
+static int
+getsc_bn(void)
+{
+	int c, c2;
+
+	if (ignore_backslash_newline)
+		return getsc_();
+
+	if (backslash_skip == 1) {
+		backslash_skip = 2;
+		return getsc_();
+	}
+
+	backslash_skip = 0;
+
+	while (1) {
+		c = getsc_();
+		if (c == '\\') {
+			if ((c2 = getsc_()) == '\n')
+				/* ignore the \newline; get the next char... */
+				continue;
+			ungetsc(c2);
+			backslash_skip = 1;
+		}
+		return c;
+	}
+}
+
+static Lex_state *
+push_state_(State_info *si, Lex_state *old_end)
+{
+	Lex_state *new = areallocarray(NULL, STATE_BSIZE,
+	    sizeof(Lex_state), ATEMP);
+
+	new[0].ls_info.base = old_end;
+	si->base = &new[0];
+	si->end = &new[STATE_BSIZE];
+	return &new[1];
+}
+
+static Lex_state *
+pop_state_(State_info *si, Lex_state *old_end)
+{
+	Lex_state *old_base = si->base;
+
+	si->base = old_end->ls_info.base - STATE_BSIZE;
+	si->end = old_end->ls_info.base;
+
+	afree(old_base, ATEMP);
+
+	return si->base + STATE_BSIZE - 1;
+}
diff --git a/lex.h b/lex.h
@@ -0,0 +1,121 @@
+/*	$OpenBSD: lex.h,v 1.21 2018/01/15 14:58:05 jca Exp $	*/
+
+/*
+ * Source input, lexer and parser
+ */
+
+/* $From: lex.h,v 1.4 1994/05/31 13:34:34 michael Exp $ */
+
+#include <inttypes.h>
+
+#define	IDENT	64
+
+typedef struct source Source;
+struct source {
+	const char *str;	/* input pointer */
+	int	type;		/* input type */
+	const char *start;	/* start of current buffer */
+	union {
+		char **strv;	/* string [] */
+		struct shf *shf; /* shell file */
+		struct tbl *tblp; /* alias (SALIAS) */
+		char *freeme;	/* also for SREREAD */
+	} u;
+	char	ugbuf[2];	/* buffer for ungetsc() (SREREAD) and
+				 * alias (SALIAS) */
+	int	line;		/* line number */
+	int	cmd_offset;	/* line number - command number */
+	int	errline;	/* line the error occurred on (0 if not set) */
+	const char *file;	/* input file name */
+	int	flags;		/* SF_* */
+	Area	*areap;
+	XString	xs;		/* input buffer */
+	Source *next;		/* stacked source */
+};
+
+/* Source.type values */
+#define	SEOF		0	/* input EOF */
+#define	SFILE		1	/* file input */
+#define SSTDIN		2	/* read stdin */
+#define	SSTRING		3	/* string */
+#define	SWSTR		4	/* string without \n */
+#define	SWORDS		5	/* string[] */
+#define	SWORDSEP	6	/* string[] separator */
+#define	SALIAS		7	/* alias expansion */
+#define SREREAD		8	/* read ahead to be re-scanned */
+
+/* Source.flags values */
+#define SF_ECHO		BIT(0)	/* echo input to shlout */
+#define SF_ALIAS	BIT(1)	/* faking space at end of alias */
+#define SF_ALIASEND	BIT(2)	/* faking space at end of alias */
+#define SF_TTY		BIT(3)	/* type == SSTDIN & it is a tty */
+
+typedef union {
+	int	i;
+	char   *cp;
+	char  **wp;
+	struct op *o;
+	struct ioword *iop;
+} YYSTYPE;
+
+/* If something is added here, add it to tokentab[] in syn.c as well */
+#define	LWORD	256
+#define	LOGAND	257		/* && */
+#define	LOGOR	258		/* || */
+#define	BREAK	259		/* ;; */
+#define	IF	260
+#define	THEN	261
+#define	ELSE	262
+#define	ELIF	263
+#define	FI	264
+#define	CASE	265
+#define	ESAC	266
+#define	FOR	267
+#define SELECT	268
+#define	WHILE	269
+#define	UNTIL	270
+#define	DO	271
+#define	DONE	272
+#define	IN	273
+#define	FUNCTION 274
+#define	TIME	275
+#define	REDIR	276
+#define MDPAREN	277		/* (( )) */
+#define BANG	278		/* ! */
+#define DBRACKET 279		/* [[ .. ]] */
+#define COPROC	280		/* |& */
+#define	YYERRCODE 300
+
+/* flags to yylex */
+#define	CONTIN	BIT(0)		/* skip new lines to complete command */
+#define	ONEWORD	BIT(1)		/* single word for substitute() */
+#define	ALIAS	BIT(2)		/* recognize alias */
+#define	KEYWORD	BIT(3)		/* recognize keywords */
+#define LETEXPR	BIT(4)		/* get expression inside (( )) */
+#define VARASN	BIT(5)		/* check for var=word */
+#define ARRAYVAR BIT(6)		/* parse x[1 & 2] as one word */
+#define ESACONLY BIT(7)		/* only accept esac keyword */
+#define CMDWORD BIT(8)		/* parsing simple command (alias related) */
+#define HEREDELIM BIT(9)	/* parsing <<,<<- delimiter */
+#define HEREDOC BIT(10)		/* parsing heredoc */
+#define UNESCAPE BIT(11)	/* remove backslashes */
+
+#define	HERES	10		/* max << in line */
+
+extern Source  *source;		/* yyparse/yylex source */
+extern YYSTYPE	yylval;		/* result from yylex */
+extern struct ioword *heres[HERES], **herep;
+extern char	ident[IDENT+1];
+
+#define HISTORYSIZE	500	/* size of saved history */
+
+extern char   **history;	/* saved commands */
+extern char   **histptr;	/* last history item */
+extern uint32_t	histsize;	/* history size */
+
+int	yylex(int);
+void	yyerror(const char *, ...)
+	    __attribute__((__noreturn__, __format__ (printf, 1, 2)));
+Source * pushs(int, Area *);
+void	set_prompt(int);
+void	pprompt(const char *, int);
diff --git a/mail.c b/mail.c
@@ -0,0 +1,207 @@
+/*	$OpenBSD: mail.c,v 1.27 2019/01/14 08:48:16 schwarze Exp $	*/
+
+/*
+ * Mailbox checking code by Robert J. Gibson, adapted for PD ksh by
+ * John R. MacMillan
+ */
+
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <string.h>
+#include <time.h>
+
+#include "config.h"
+#include "sh.h"
+
+#define MBMESSAGE	"you have mail in $_"
+
+typedef struct mbox {
+	struct mbox    *mb_next;	/* next mbox in list */
+	char	       *mb_path;	/* path to mail file */
+	char	       *mb_msg;		/* to announce arrival of new mail */
+	time_t		mb_mtime;	/* mtime of mail file */
+} mbox_t;
+
+/*
+ * $MAILPATH is a linked list of mboxes.  $MAIL is a treated as a
+ * special case of $MAILPATH, where the list has only one node.  The
+ * same list is used for both since they are exclusive.
+ */
+
+static mbox_t	*mplist;
+static mbox_t	mbox;
+static struct	timespec mlastchkd;	/* when mail was last checked */
+static time_t	mailcheck_interval;
+
+static void	munset(mbox_t *); /* free mlist and mval */
+static mbox_t * mballoc(char *, char *); /* allocate a new mbox */
+static void	mprintit(mbox_t *);
+
+void
+mcheck(void)
+{
+	mbox_t		*mbp;
+	struct timespec	 elapsed, now;
+	struct tbl	*vp;
+	struct stat	 stbuf;
+	static int	 first = 1;
+
+	if (mplist)
+		mbp = mplist;
+	else if ((vp = global("MAIL")) && (vp->flag & ISSET))
+		mbp = &mbox;
+	else
+		mbp = NULL;
+	if (mbp == NULL)
+		return;
+
+	clock_gettime(CLOCK_MONOTONIC, &now);
+	if (first) {
+		mlastchkd = now;
+		first = 0;
+	}
+	timespecsub(&now, &mlastchkd, &elapsed);
+	if (elapsed.tv_sec >= mailcheck_interval) {
+		mlastchkd = now;
+
+		while (mbp) {
+			if (mbp->mb_path && stat(mbp->mb_path, &stbuf) == 0 &&
+			    S_ISREG(stbuf.st_mode)) {
+				if (stbuf.st_size &&
+				    mbp->mb_mtime != stbuf.st_mtime &&
+				    stbuf.st_atime <= stbuf.st_mtime)
+					mprintit(mbp);
+				mbp->mb_mtime = stbuf.st_mtime;
+			} else {
+				/*
+				 * Some mail readers remove the mail
+				 * file if all mail is read.  If file
+				 * does not exist, assume this is the
+				 * case and set mtime to zero.
+				 */
+				mbp->mb_mtime = 0;
+			}
+			mbp = mbp->mb_next;
+		}
+	}
+}
+
+void
+mcset(int64_t interval)
+{
+	mailcheck_interval = interval;
+}
+
+void
+mbset(char *p)
+{
+	struct stat	stbuf;
+
+	afree(mbox.mb_msg, APERM);
+	afree(mbox.mb_path, APERM);
+	/* Save a copy to protect from export (which munges the string) */
+	mbox.mb_path = str_save(p, APERM);
+	mbox.mb_msg = NULL;
+	if (p && stat(p, &stbuf) == 0 && S_ISREG(stbuf.st_mode))
+		mbox.mb_mtime = stbuf.st_mtime;
+	else
+		mbox.mb_mtime = 0;
+}
+
+void
+mpset(char *mptoparse)
+{
+	mbox_t	*mbp;
+	char	*mpath, *mmsg, *mval;
+	char *p;
+
+	munset( mplist );
+	mplist = NULL;
+	mval = str_save(mptoparse, APERM);
+	while (mval) {
+		mpath = mval;
+		if ((mval = strchr(mval, ':')) != NULL) {
+			*mval = '\0';
+			mval++;
+		}
+		/* POSIX/bourne-shell say file%message */
+		for (p = mpath; (mmsg = strchr(p, '%')); ) {
+			/* a literal percent? (POSIXism) */
+			if (mmsg > mpath && mmsg[-1] == '\\') {
+				/* use memmove() to avoid overlap problems */
+				memmove(mmsg - 1, mmsg, strlen(mmsg) + 1);
+				p = mmsg;
+				continue;
+			}
+			break;
+		}
+		/* at&t ksh says file?message */
+		if (!mmsg && !Flag(FPOSIX))
+			mmsg = strchr(mpath, '?');
+		if (mmsg) {
+			*mmsg = '\0';
+			mmsg++;
+			if (*mmsg == '\0')
+				mmsg = NULL;
+		}
+		if (*mpath == '\0')
+			continue;
+		mbp = mballoc(mpath, mmsg);
+		mbp->mb_next = mplist;
+		mplist = mbp;
+	}
+}
+
+static void
+munset(mbox_t *mlist)
+{
+	mbox_t	*mbp;
+
+	while (mlist != NULL) {
+		mbp = mlist;
+		mlist = mbp->mb_next;
+		if (!mlist)
+			afree(mbp->mb_path, APERM);
+		afree(mbp, APERM);
+	}
+}
+
+static mbox_t *
+mballoc(char *p, char *m)
+{
+	struct stat	stbuf;
+	mbox_t	*mbp;
+
+	mbp = alloc(sizeof(mbox_t), APERM);
+	mbp->mb_next = NULL;
+	mbp->mb_path = p;
+	mbp->mb_msg = m;
+	if (stat(mbp->mb_path, &stbuf) == 0 && S_ISREG(stbuf.st_mode))
+		mbp->mb_mtime = stbuf.st_mtime;
+	else
+		mbp->mb_mtime = 0;
+	return(mbp);
+}
+
+static void
+mprintit(mbox_t *mbp)
+{
+	struct tbl	*vp;
+
+#if 0
+	/*
+	 * I doubt this $_ overloading is bad in /bin/sh mode.  Anyhow, we
+	 * crash as the code looks now if we do not set vp.  Now, this is
+	 * easy to fix too, but I'd like to see what POSIX says before doing
+	 * a change like that.
+	 */
+	if (!Flag(FSH))
+#endif
+		/* Ignore setstr errors here (arbitrary) */
+		setstr((vp = local("_", false)), mbp->mb_path, KSH_RETURN_ERROR);
+
+	shellf("%s\n", substitute(mbp->mb_msg ? mbp->mb_msg : MBMESSAGE, 0));
+
+	unset(vp, 0);
+}
diff --git a/main.c b/main.c
@@ -0,0 +1,824 @@
+/*	$OpenBSD: main.c,v 1.97 2019/02/20 23:59:17 schwarze Exp $	*/
+
+/*
+ * startup, main loop, environments and error handling
+ */
+
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <paths.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+#ifndef _PW_NAME_LEN
+#	define _PW_NAME_LEN 32 /* see the useradd(8) man page */
+#endif
+
+extern char **environ;
+
+/*
+ * global data
+ */
+
+static void	reclaim(void);
+static void	remove_temps(struct temp *tp);
+static int	is_restricted(char *name);
+static void	init_username(void);
+
+const char *kshname;
+pid_t	kshpid;
+pid_t	procpid;
+uid_t	ksheuid;
+int	exstat;
+int	subst_exstat;
+const char *safe_prompt;
+int	disable_subst;
+
+Area	aperm;
+
+struct env	*genv;
+
+char	shell_flags[FNFLAGS];
+
+char	null[] = "";
+
+int shl_stdout_ok;
+
+unsigned int	ksh_tmout;
+enum tmout_enum	ksh_tmout_state = TMOUT_EXECUTING;
+
+int	really_exit;
+
+int ifs0 = ' ';
+
+volatile sig_atomic_t	trap;
+volatile sig_atomic_t	intrsig;
+volatile sig_atomic_t	fatal_trap;
+
+Getopt	builtin_opt;
+Getopt	user_opt;
+
+struct coproc	coproc;
+sigset_t	sm_default, sm_sigchld;
+
+char	*builtin_argv0;
+int	 builtin_flag;
+
+char	*current_wd;
+int	 current_wd_size;
+
+int	x_cols = 80;
+
+/*
+ * shell initialization
+ */
+
+static const char initifs[] = "IFS= \t\n";
+
+static const char initsubs[] = "${PS2=> } ${PS3=#? } ${PS4=+ }";
+
+static const char *initcoms [] = {
+	"typeset", "-r", "KSH_VERSION", NULL,
+	"typeset", "-x", "SHELL", "PATH", "HOME", "PWD", "OLDPWD", NULL,
+	"typeset", "-ir", "PPID", NULL,
+	"typeset", "-i", "OPTIND=1", NULL,
+	"eval", "typeset -i RANDOM MAILCHECK=\"${MAILCHECK-600}\" SECONDS=\"${SECONDS-0}\" TMOUT=\"${TMOUT-0}\"", NULL,
+	"alias",
+	 /* Standard ksh aliases */
+	  "hash=alias -t",	/* not "alias -t --": hash -r needs to work */
+	  "stop=kill -STOP",
+	  "autoload=typeset -fu",
+	  "functions=typeset -f",
+	  "history=fc -l",
+	  "integer=typeset -i",
+	  "nohup=nohup ",
+	  "local=typeset",
+	  "r=fc -s",
+	 /* Aliases that are builtin commands in at&t */
+	  "login=exec login",
+	  NULL,
+	/* this is what at&t ksh seems to track, with the addition of emacs */
+	"alias", "-tU",
+	  "cat", "cc", "chmod", "cp", "date", "ed", "emacs", "grep", "ls",
+	  "mail", "make", "mv", "pr", "rm", "sed", "sh", "vi", "who",
+	  NULL,
+	NULL
+};
+
+char username[_PW_NAME_LEN + 1];
+
+#define version_param  (initcoms[2])
+
+/* The shell uses its own variation on argv, to build variables like
+ * $0 and $@.
+ * Allocate a new array since modifying the original argv will modify
+ * ps output.
+ */
+static char **
+make_argv(int argc, char *argv[])
+{
+	int i;
+	char **nargv;
+
+	nargv = areallocarray(NULL, argc + 1, sizeof(char *), &aperm);
+	nargv[0] = (char *) kshname;
+	for (i = 1; i < argc; i++)
+		nargv[i] = argv[i];
+	nargv[i] = NULL;
+
+	return nargv;
+}
+
+int
+main(int argc, char *argv[])
+{
+	int i;
+	int argi;
+	Source *s;
+	struct block *l;
+	int restricted, errexit;
+	char **wp;
+	struct env env;
+	pid_t ppid;
+
+	kshname = argv[0];
+
+	ainit(&aperm);		/* initialize permanent Area */
+
+	/* set up base environment */
+	memset(&env, 0, sizeof(env));
+	env.type = E_NONE;
+	ainit(&env.area);
+	genv = &env;
+	newblock();		/* set up global l->vars and l->funs */
+
+	/* Do this first so output routines (eg, errorf, shellf) can work */
+	initio();
+
+	initvar();
+
+	initctypes();
+
+	inittraps();
+
+	coproc_init();
+
+	/* set up variable and command dictionaries */
+	ktinit(&taliases, APERM, 0);
+	ktinit(&aliases, APERM, 0);
+	ktinit(&homedirs, APERM, 0);
+
+	/* define shell keywords */
+	initkeywords();
+
+	/* define built-in commands */
+	ktinit(&builtins, APERM, 64); /* must be 2^n (currently 40 builtins) */
+	for (i = 0; shbuiltins[i].name != NULL; i++)
+		builtin(shbuiltins[i].name, shbuiltins[i].func);
+	for (i = 0; kshbuiltins[i].name != NULL; i++)
+		builtin(kshbuiltins[i].name, kshbuiltins[i].func);
+
+	init_histvec();
+
+	def_path = _PATH_DEFPATH;
+	{
+		size_t len = confstr(_CS_PATH, NULL, 0);
+		char *new;
+
+		if (len > 0) {
+			confstr(_CS_PATH, new = alloc(len + 1, APERM), len + 1);
+			def_path = new;
+		}
+	}
+
+	/* Set PATH to def_path (will set the path global variable).
+	 * (import of environment below will probably change this setting).
+	 */
+	{
+		struct tbl *vp = global("PATH");
+		/* setstr can't fail here */
+		setstr(vp, def_path, KSH_RETURN_ERROR);
+	}
+
+
+	/* Turn on nohup by default for now - will change to off
+	 * by default once people are aware of its existence
+	 * (at&t ksh does not have a nohup option - it always sends
+	 * the hup).
+	 */
+	Flag(FNOHUP) = 1;
+
+	/* Turn on brace expansion by default.  At&t ksh's that have
+	 * alternation always have it on.  BUT, posix doesn't have
+	 * brace expansion, so set this before setting up FPOSIX
+	 * (change_flag() clears FBRACEEXPAND when FPOSIX is set).
+	 */
+	Flag(FBRACEEXPAND) = 1;
+
+	/* set posix flag just before environment so that it will have
+	 * exactly the same effect as the POSIXLY_CORRECT environment
+	 * variable.  If this needs to be done sooner to ensure correct posix
+	 * operation, an initial scan of the environment will also have
+	 * done sooner.
+	 */
+#ifdef POSIXLY_CORRECT
+	change_flag(FPOSIX, OF_SPECIAL, 1);
+#endif /* POSIXLY_CORRECT */
+
+	/* Check to see if we're /bin/sh. */
+	if (!strcmp(kshname, "sh") || !strcmp(kshname, "-sh") ||
+	    (strlen(kshname) >= 3 &&
+	    !strcmp(&kshname[strlen(kshname) - 3], "/sh"))) {
+		Flag(FSH) = 1;
+		version_param = "SH_VERSION";
+	}
+
+	/* Set edit mode to emacs by default, may be overridden
+	 * by the environment or the user.  Also, we want tab completion
+	 * on in vi by default. */
+#if defined(EMACS)
+	change_flag(FEMACS, OF_SPECIAL, 1);
+#endif /* EMACS */
+#if defined(VI)
+	Flag(FVITABCOMPLETE) = 1;
+#endif /* VI */
+
+	/* import environment */
+	if (environ != NULL)
+		for (wp = environ; *wp != NULL; wp++)
+			typeset(*wp, IMPORT|EXPORT, 0, 0, 0);
+
+	kshpid = procpid = getpid();
+	typeset(initifs, 0, 0, 0, 0);	/* for security */
+
+	/* assign default shell variable values */
+	substitute(initsubs, 0);
+
+	/* Figure out the current working directory and set $PWD */
+	{
+		struct stat s_pwd, s_dot;
+		struct tbl *pwd_v = global("PWD");
+		char *pwd = str_val(pwd_v);
+		char *pwdx = pwd;
+
+		/* Try to use existing $PWD if it is valid */
+		if (pwd[0] != '/' ||
+		    stat(pwd, &s_pwd) < 0 || stat(".", &s_dot) < 0 ||
+		    s_pwd.st_dev != s_dot.st_dev ||
+		    s_pwd.st_ino != s_dot.st_ino)
+			pwdx = NULL;
+		set_current_wd(pwdx);
+		if (current_wd[0])
+			simplify_path(current_wd);
+		/* Only set pwd if we know where we are or if it had a
+		 * bogus value
+		 */
+		if (current_wd[0] || pwd != null)
+			/* setstr can't fail here */
+			setstr(pwd_v, current_wd, KSH_RETURN_ERROR);
+	}
+	ppid = getppid();
+	setint(global("PPID"), (int64_t) ppid);
+	/* setstr can't fail here */
+	setstr(global(version_param), ksh_version, KSH_RETURN_ERROR);
+
+	/* execute initialization statements */
+	for (wp = (char**) initcoms; *wp != NULL; wp++) {
+		shcomexec(wp);
+		for (; *wp != NULL; wp++)
+			;
+	}
+
+
+	ksheuid = geteuid();
+	init_username();
+	safe_prompt = ksheuid ? "$ " : "# ";
+	{
+		struct tbl *vp = global("PS1");
+
+		/* Set PS1 if it isn't set */
+		if (!(vp->flag & ISSET)) {
+			/* setstr can't fail here */
+			setstr(vp, "\\h\\$ ", KSH_RETURN_ERROR);
+		}
+	}
+
+	/* Set this before parsing arguments */
+	Flag(FPRIVILEGED) = getuid() != ksheuid || getgid() != getegid();
+
+	/* this to note if monitor is set on command line (see below) */
+	Flag(FMONITOR) = 127;
+	argi = parse_args(argv, OF_CMDLINE, NULL);
+	if (argi < 0)
+		exit(1);
+
+	if (Flag(FCOMMAND)) {
+		s = pushs(SSTRING, ATEMP);
+		if (!(s->start = s->str = argv[argi++]))
+			errorf("-c requires an argument");
+		if (argv[argi])
+			kshname = argv[argi++];
+	} else if (argi < argc && !Flag(FSTDIN)) {
+		s = pushs(SFILE, ATEMP);
+		s->file = argv[argi++];
+		s->u.shf = shf_open(s->file, O_RDONLY, 0, SHF_MAPHI|SHF_CLEXEC);
+		if (s->u.shf == NULL) {
+			exstat = 127; /* POSIX */
+			errorf("%s: %s", s->file, strerror(errno));
+		}
+		kshname = s->file;
+	} else {
+		Flag(FSTDIN) = 1;
+		s = pushs(SSTDIN, ATEMP);
+		s->file = "<stdin>";
+		s->u.shf = shf_fdopen(0, SHF_RD | can_seek(0), NULL);
+		if (isatty(0) && isatty(2)) {
+			Flag(FTALKING) = Flag(FTALKING_I) = 1;
+			/* The following only if isatty(0) */
+			s->flags |= SF_TTY;
+			s->u.shf->flags |= SHF_INTERRUPT;
+			s->file = NULL;
+		}
+	}
+
+	/* This bizarreness is mandated by POSIX */
+	{
+		struct stat s_stdin;
+
+		if (fstat(0, &s_stdin) >= 0 && S_ISCHR(s_stdin.st_mode) &&
+		    Flag(FTALKING))
+			reset_nonblock(0);
+	}
+
+	/* initialize job control */
+	i = Flag(FMONITOR) != 127;
+	Flag(FMONITOR) = 0;
+	j_init(i);
+	/* Do this after j_init(), as tty_fd is not initialized 'til then */
+	if (Flag(FTALKING))
+		x_init();
+
+	l = genv->loc;
+	l->argv = make_argv(argc - (argi - 1), &argv[argi - 1]);
+	l->argc = argc - argi;
+	getopts_reset(1);
+
+	/* Disable during .profile/ENV reading */
+	restricted = Flag(FRESTRICTED);
+	Flag(FRESTRICTED) = 0;
+	errexit = Flag(FERREXIT);
+	Flag(FERREXIT) = 0;
+
+	/* Do this before profile/$ENV so that if it causes problems in them,
+	 * user will know why things broke.
+	 */
+	if (!current_wd[0] && Flag(FTALKING))
+		warningf(false, "Cannot determine current working directory");
+
+	if (Flag(FLOGIN)) {
+		include(KSH_SYSTEM_PROFILE, 0, NULL, 1);
+		if (!Flag(FPRIVILEGED))
+			include(substitute("$HOME/.profile", 0), 0, NULL, 1);
+	}
+
+	if (Flag(FPRIVILEGED))
+		include("/etc/suid_profile", 0, NULL, 1);
+	else if (Flag(FTALKING)) {
+		char *env_file;
+
+		/* include $ENV */
+		env_file = str_val(global("ENV"));
+
+#ifdef DEFAULT_ENV
+		/* If env isn't set, include default environment */
+		if (env_file == null)
+			env_file = DEFAULT_ENV;
+#endif /* DEFAULT_ENV */
+		env_file = substitute(env_file, DOTILDE);
+		if (*env_file != '\0')
+			include(env_file, 0, NULL, 1);
+	}
+
+	if (is_restricted(argv[0]) || is_restricted(str_val(global("SHELL"))))
+		restricted = 1;
+	if (restricted) {
+		static const char *const restr_com[] = {
+			"typeset", "-r", "PATH",
+			"ENV", "SHELL",
+			NULL
+		};
+		shcomexec((char **) restr_com);
+		/* After typeset command... */
+		Flag(FRESTRICTED) = 1;
+	}
+	if (errexit)
+		Flag(FERREXIT) = 1;
+
+	if (Flag(FTALKING)) {
+		hist_init(s);
+		alarm_init();
+	} else
+		Flag(FTRACKALL) = 1;	/* set after ENV */
+
+	shell(s, true);	/* doesn't return */
+	return 0;
+}
+
+static void
+init_username(void)
+{
+	char *p;
+	struct tbl *vp = global("USER");
+
+	if (vp->flag & ISSET)
+		p = ksheuid == 0 ? "root" : str_val(vp);
+	else
+		p = getlogin();
+
+	strlcpy(username, p != NULL ? p : "?", sizeof username);
+}
+
+int
+include(const char *name, int argc, char **argv, int intr_ok)
+{
+	Source *volatile s = NULL;
+	struct shf *shf;
+	char **volatile old_argv;
+	volatile int old_argc;
+	int i;
+
+	shf = shf_open(name, O_RDONLY, 0, SHF_MAPHI|SHF_CLEXEC);
+	if (shf == NULL)
+		return -1;
+
+	if (argv) {
+		old_argv = genv->loc->argv;
+		old_argc = genv->loc->argc;
+	} else {
+		old_argv = NULL;
+		old_argc = 0;
+	}
+	newenv(E_INCL);
+	i = sigsetjmp(genv->jbuf, 0);
+	if (i) {
+		quitenv(s ? s->u.shf : NULL);
+		if (old_argv) {
+			genv->loc->argv = old_argv;
+			genv->loc->argc = old_argc;
+		}
+		switch (i) {
+		case LRETURN:
+		case LERROR:
+			return exstat & 0xff; /* see below */
+		case LINTR:
+			/* intr_ok is set if we are including .profile or $ENV.
+			 * If user ^C's out, we don't want to kill the shell...
+			 */
+			if (intr_ok && (exstat - 128) != SIGTERM)
+				return 1;
+			/* FALLTHROUGH */
+		case LEXIT:
+		case LLEAVE:
+		case LSHELL:
+			unwind(i);
+			/* NOTREACHED */
+		default:
+			internal_errorf("%s: %d", __func__, i);
+			/* NOTREACHED */
+		}
+	}
+	if (argv) {
+		genv->loc->argv = argv;
+		genv->loc->argc = argc;
+	}
+	s = pushs(SFILE, ATEMP);
+	s->u.shf = shf;
+	s->file = str_save(name, ATEMP);
+	i = shell(s, false);
+	quitenv(s->u.shf);
+	if (old_argv) {
+		genv->loc->argv = old_argv;
+		genv->loc->argc = old_argc;
+	}
+	return i & 0xff;	/* & 0xff to ensure value not -1 */
+}
+
+/*
+ * spawn a command into a shell optionally keeping track of line
+ * number.
+ */
+int
+command(const char *comm, int line)
+{
+	Source *s;
+
+	s = pushs(SSTRING, ATEMP);
+	s->start = s->str = comm;
+	s->line = line;
+	return shell(s, false);
+}
+
+/*
+ * run the commands from the input source, returning status.
+ */
+int
+shell(Source *volatile s, volatile int toplevel)
+{
+	struct op *t;
+	volatile int wastty = s->flags & SF_TTY;
+	volatile int attempts = 13;
+	volatile int interactive = Flag(FTALKING) && toplevel;
+	Source *volatile old_source = source;
+	int i;
+
+	newenv(E_PARSE);
+	if (interactive)
+		really_exit = 0;
+	i = sigsetjmp(genv->jbuf, 0);
+	if (i) {
+		switch (i) {
+		case LINTR: /* we get here if SIGINT not caught or ignored */
+		case LERROR:
+		case LSHELL:
+			if (interactive) {
+				c_fc_reset();
+				if (i == LINTR)
+					shellf("\n");
+				/* Reset any eof that was read as part of a
+				 * multiline command.
+				 */
+				if (Flag(FIGNOREEOF) && s->type == SEOF &&
+				    wastty)
+					s->type = SSTDIN;
+				/* Used by exit command to get back to
+				 * top level shell.  Kind of strange since
+				 * interactive is set if we are reading from
+				 * a tty, but to have stopped jobs, one only
+				 * needs FMONITOR set (not FTALKING/SF_TTY)...
+				 */
+				/* toss any input we have so far */
+				s->start = s->str = null;
+				break;
+			}
+			/* FALLTHROUGH */
+		case LEXIT:
+		case LLEAVE:
+		case LRETURN:
+			source = old_source;
+			quitenv(NULL);
+			unwind(i);	/* keep on going */
+			/* NOTREACHED */
+		default:
+			source = old_source;
+			quitenv(NULL);
+			internal_errorf("%s: %d", __func__, i);
+			/* NOTREACHED */
+		}
+	}
+
+	while (1) {
+		if (trap)
+			runtraps(0);
+
+		if (s->next == NULL) {
+			if (Flag(FVERBOSE))
+				s->flags |= SF_ECHO;
+			else
+				s->flags &= ~SF_ECHO;
+		}
+
+		if (interactive) {
+			got_sigwinch = 1;
+			j_notify();
+			mcheck();
+			set_prompt(PS1);
+		}
+
+		t = compile(s);
+		if (t != NULL && t->type == TEOF) {
+			if (wastty && Flag(FIGNOREEOF) && --attempts > 0) {
+				shellf("Use `exit' to leave ksh\n");
+				s->type = SSTDIN;
+			} else if (wastty && !really_exit &&
+			    j_stopped_running()) {
+				really_exit = 1;
+				s->type = SSTDIN;
+			} else {
+				/* this for POSIX, which says EXIT traps
+				 * shall be taken in the environment
+				 * immediately after the last command
+				 * executed.
+				 */
+				if (toplevel)
+					unwind(LEXIT);
+				break;
+			}
+		}
+
+		if (t && (!Flag(FNOEXEC) || (s->flags & SF_TTY)))
+			exstat = execute(t, 0, NULL);
+
+		if (t != NULL && t->type != TEOF && interactive && really_exit)
+			really_exit = 0;
+
+		reclaim();
+	}
+	quitenv(NULL);
+	source = old_source;
+	return exstat;
+}
+
+/* return to closest error handler or shell(), exit if none found */
+void
+unwind(int i)
+{
+	/* ordering for EXIT vs ERR is a bit odd (this is what at&t ksh does) */
+	if (i == LEXIT || (Flag(FERREXIT) && (i == LERROR || i == LINTR) &&
+	    sigtraps[SIGEXIT_].trap)) {
+		if (trap)
+			runtraps(0);
+		runtrap(&sigtraps[SIGEXIT_]);
+		i = LLEAVE;
+	} else if (Flag(FERREXIT) && (i == LERROR || i == LINTR)) {
+		if (trap)
+			runtraps(0);
+		runtrap(&sigtraps[SIGERR_]);
+		i = LLEAVE;
+	}
+	while (1) {
+		switch (genv->type) {
+		case E_PARSE:
+		case E_FUNC:
+		case E_INCL:
+		case E_LOOP:
+		case E_ERRH:
+			siglongjmp(genv->jbuf, i);
+			/* NOTREACHED */
+
+		case E_NONE:
+			if (i == LINTR)
+				genv->flags |= EF_FAKE_SIGDIE;
+			/* FALLTHROUGH */
+
+		default:
+			quitenv(NULL);
+			/*
+			 * quitenv() may have reclaimed the memory
+			 * used by source which will end badly when
+			 * we jump to a function that expects it to
+			 * be valid
+			 */
+			source = NULL;
+		}
+	}
+}
+
+void
+newenv(int type)
+{
+	struct env *ep;
+
+	ep = alloc(sizeof(*ep), ATEMP);
+	ep->type = type;
+	ep->flags = 0;
+	ainit(&ep->area);
+	ep->loc = genv->loc;
+	ep->savefd = NULL;
+	ep->oenv = genv;
+	ep->temps = NULL;
+	genv = ep;
+}
+
+void
+quitenv(struct shf *shf)
+{
+	struct env *ep = genv;
+	int fd;
+
+	if (ep->oenv && ep->oenv->loc != ep->loc)
+		popblock();
+	if (ep->savefd != NULL) {
+		for (fd = 0; fd < NUFILE; fd++)
+			/* if ep->savefd[fd] < 0, means fd was closed */
+			if (ep->savefd[fd])
+				restfd(fd, ep->savefd[fd]);
+		if (ep->savefd[2]) /* Clear any write errors */
+			shf_reopen(2, SHF_WR, shl_out);
+	}
+
+	/* Bottom of the stack.
+	 * Either main shell is exiting or cleanup_parents_env() was called.
+	 */
+	if (ep->oenv == NULL) {
+		if (ep->type == E_NONE) {	/* Main shell exiting? */
+			if (Flag(FTALKING))
+				hist_finish();
+			j_exit();
+			if (ep->flags & EF_FAKE_SIGDIE) {
+				int sig = exstat - 128;
+
+				/* ham up our death a bit (at&t ksh
+				 * only seems to do this for SIGTERM)
+				 * Don't do it for SIGQUIT, since we'd
+				 * dump a core..
+				 */
+				if ((sig == SIGINT || sig == SIGTERM) &&
+				    getpgrp() == kshpid) {
+					setsig(&sigtraps[sig], SIG_DFL,
+					    SS_RESTORE_CURR|SS_FORCE);
+					kill(0, sig);
+				}
+			}
+		}
+		if (shf)
+			shf_close(shf);
+		reclaim();
+		exit(exstat);
+	}
+	if (shf)
+		shf_close(shf);
+	reclaim();
+
+	genv = genv->oenv;
+	afree(ep, ATEMP);
+}
+
+/* Called after a fork to cleanup stuff left over from parents environment */
+void
+cleanup_parents_env(void)
+{
+	struct env *ep;
+	int fd;
+
+	/* Don't clean up temporary files - parent will probably need them.
+	 * Also, can't easily reclaim memory since variables, etc. could be
+	 * anywhere.
+	 */
+
+	/* close all file descriptors hiding in savefd */
+	for (ep = genv; ep; ep = ep->oenv) {
+		if (ep->savefd) {
+			for (fd = 0; fd < NUFILE; fd++)
+				if (ep->savefd[fd] > 0)
+					close(ep->savefd[fd]);
+			afree(ep->savefd, &ep->area);
+			ep->savefd = NULL;
+		}
+	}
+	genv->oenv = NULL;
+}
+
+/* Called just before an execve cleanup stuff temporary files */
+void
+cleanup_proc_env(void)
+{
+	struct env *ep;
+
+	for (ep = genv; ep; ep = ep->oenv)
+		remove_temps(ep->temps);
+}
+
+/* remove temp files and free ATEMP Area */
+static void
+reclaim(void)
+{
+	remove_temps(genv->temps);
+	genv->temps = NULL;
+	afreeall(&genv->area);
+}
+
+static void
+remove_temps(struct temp *tp)
+{
+
+	for (; tp != NULL; tp = tp->next)
+		if (tp->pid == procpid) {
+			unlink(tp->name);
+		}
+}
+
+/* Returns true if name refers to a restricted shell */
+static int
+is_restricted(char *name)
+{
+	char *p;
+
+	if ((p = strrchr(name, '/')))
+		name = p + 1;
+	/* accepts rsh, rksh, rpdksh, pdrksh */
+	if (strcmp(name, "rsh") && \
+		strcmp(name, "rksh") && \
+		strcmp(name, "rpdksh") && \
+		strcmp(name, "pdrksh"))
+		return(0);
+	else
+		return(1);
+
+}
diff --git a/misc.c b/misc.c
@@ -0,0 +1,1147 @@
+/*	$OpenBSD: misc.c,v 1.72 2018/11/20 08:12:26 deraadt Exp $	*/
+
+/*
+ * Miscellaneous functions
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <grp.h>
+#include <sys/auxv.h>
+
+#include "sh.h"
+#include "charclass.h"
+
+short ctypes [UCHAR_MAX+1];	/* type bits for unsigned char */
+static int dropped_privileges;
+
+static int	do_gmatch(const unsigned char *, const unsigned char *,
+		    const unsigned char *, const unsigned char *);
+static const unsigned char *cclass(const unsigned char *, int);
+
+/*
+ * Fast character classes
+ */
+void
+setctypes(const char *s, int t)
+{
+	int i;
+
+	if (t & C_IFS) {
+		for (i = 0; i < UCHAR_MAX+1; i++)
+			ctypes[i] &= ~C_IFS;
+		ctypes[0] |= C_IFS; /* include \0 in C_IFS */
+	}
+	while (*s != 0)
+		ctypes[(unsigned char) *s++] |= t;
+}
+
+void
+initctypes(void)
+{
+	int c;
+
+	for (c = 'a'; c <= 'z'; c++)
+		ctypes[c] |= C_ALPHA;
+	for (c = 'A'; c <= 'Z'; c++)
+		ctypes[c] |= C_ALPHA;
+	ctypes['_'] |= C_ALPHA;
+	setctypes(" \t\n|&;<>()", C_LEX1); /* \0 added automatically */
+	setctypes("*@#!$-?", C_VAR1);
+	setctypes(" \t\n", C_IFSWS);
+	setctypes("=-+?", C_SUBOP1);
+	setctypes("#%", C_SUBOP2);
+	setctypes(" \n\t\"#$&'()*;<>?[\\`|", C_QUOTE);
+}
+
+/* convert uint64_t to base N string */
+
+char *
+u64ton(uint64_t n, int base)
+{
+	char *p;
+	static char buf [20];
+
+	p = &buf[sizeof(buf)];
+	*--p = '\0';
+	do {
+		*--p = "0123456789ABCDEF"[n%base];
+		n /= base;
+	} while (n != 0);
+	return p;
+}
+
+char *
+str_save(const char *s, Area *ap)
+{
+	size_t len;
+	char *p;
+
+	if (!s)
+		return NULL;
+	len = strlen(s)+1;
+	p = alloc(len, ap);
+	strlcpy(p, s, len);
+	return (p);
+}
+
+/* Allocate a string of size n+1 and copy upto n characters from the possibly
+ * null terminated string s into it.  Always returns a null terminated string
+ * (unless n < 0).
+ */
+char *
+str_nsave(const char *s, int n, Area *ap)
+{
+	char *ns;
+
+	if (n < 0)
+		return 0;
+	ns = alloc(n + 1, ap);
+	ns[0] = '\0';
+	return strncat(ns, s, n);
+}
+
+/* called from expand.h:XcheckN() to grow buffer */
+char *
+Xcheck_grow_(XString *xsp, char *xp, size_t more)
+{
+	char *old_beg = xsp->beg;
+
+	xsp->len += more > xsp->len ? more : xsp->len;
+	xsp->beg = aresize(xsp->beg, xsp->len + 8, xsp->areap);
+	xsp->end = xsp->beg + xsp->len;
+	return xsp->beg + (xp - old_beg);
+}
+
+const struct option sh_options[] = {
+	/* Special cases (see parse_args()): -A, -o, -s.
+	 * Options are sorted by their longnames - the order of these
+	 * entries MUST match the order of sh_flag F* enumerations in sh.h.
+	 */
+	{ "allexport",	'a',		OF_ANY },
+	{ "braceexpand",  0,		OF_ANY }, /* non-standard */
+	{ "bgnice",	  0,		OF_ANY },
+	{ NULL,	'c',	    OF_CMDLINE },
+	{ "csh-history",  0,		OF_ANY }, /* non-standard */
+#ifdef EMACS
+	{ "emacs",	  0,		OF_ANY },
+#endif
+	{ "errexit",	'e',		OF_ANY },
+#ifdef EMACS
+	{ "gmacs",	  0,		OF_ANY },
+#endif
+	{ "ignoreeof",	  0,		OF_ANY },
+	{ "interactive",'i',	    OF_CMDLINE },
+	{ "keyword",	'k',		OF_ANY },
+	{ "login",	'l',	    OF_CMDLINE },
+	{ "markdirs",	'X',		OF_ANY },
+	{ "monitor",	'm',		OF_ANY },
+	{ "noclobber",	'C',		OF_ANY },
+	{ "noexec",	'n',		OF_ANY },
+	{ "noglob",	'f',		OF_ANY },
+	{ "nohup",	  0,		OF_ANY },
+	{ "nolog",	  0,		OF_ANY }, /* no effect */
+	{ "notify",	'b',		OF_ANY },
+	{ "nounset",	'u',		OF_ANY },
+	{ "physical",	  0,		OF_ANY }, /* non-standard */
+	{ "posix",	  0,		OF_ANY }, /* non-standard */
+	{ "privileged",	'p',		OF_ANY },
+	{ "restricted",	'r',	    OF_CMDLINE },
+	{ "sh",		  0,		OF_ANY }, /* non-standard */
+	{ "stdin",	's',	    OF_CMDLINE }, /* pseudo non-standard */
+	{ "trackall",	'h',		OF_ANY },
+	{ "verbose",	'v',		OF_ANY },
+#ifdef VI
+	{ "vi",		  0,		OF_ANY },
+	{ "viraw",	  0,		OF_ANY }, /* no effect */
+	{ "vi-show8",	  0,		OF_ANY }, /* non-standard */
+	{ "vi-tabcomplete",  0,		OF_ANY }, /* non-standard */
+	{ "vi-esccomplete",  0,		OF_ANY }, /* non-standard */
+#endif
+	{ "xtrace",	'x',		OF_ANY },
+	/* Anonymous flags: used internally by shell only
+	 * (not visible to user)
+	 */
+	{ NULL,	0,		OF_INTERNAL }, /* FTALKING_I */
+};
+
+/*
+ * translate -o option into F* constant (also used for test -o option)
+ */
+int
+option(const char *n)
+{
+	unsigned int ele;
+
+	for (ele = 0; ele < NELEM(sh_options); ele++)
+		if (sh_options[ele].name && strcmp(sh_options[ele].name, n) == 0)
+			return ele;
+
+	return -1;
+}
+
+struct options_info {
+	int opt_width;
+	struct {
+		const char *name;
+		int	flag;
+	} opts[NELEM(sh_options)];
+};
+
+static char *options_fmt_entry(void *arg, int i, char *buf, int buflen);
+static void printoptions(int verbose);
+
+/* format a single select menu item */
+static char *
+options_fmt_entry(void *arg, int i, char *buf, int buflen)
+{
+	struct options_info *oi = (struct options_info *) arg;
+
+	shf_snprintf(buf, buflen, "%-*s %s",
+	    oi->opt_width, oi->opts[i].name,
+	    Flag(oi->opts[i].flag) ? "on" : "off");
+	return buf;
+}
+
+static void
+printoptions(int verbose)
+{
+	unsigned int ele;
+
+	if (verbose) {
+		struct options_info oi;
+		unsigned int n;
+		int len;
+
+		/* verbose version */
+		shprintf("Current option settings\n");
+
+		for (ele = n = oi.opt_width = 0; ele < NELEM(sh_options); ele++) {
+			if (sh_options[ele].name) {
+				len = strlen(sh_options[ele].name);
+				oi.opts[n].name = sh_options[ele].name;
+				oi.opts[n++].flag = ele;
+				if (len > oi.opt_width)
+					oi.opt_width = len;
+			}
+		}
+		print_columns(shl_stdout, n, options_fmt_entry, &oi,
+		    oi.opt_width + 5, 1);
+	} else {
+		/* short version ala ksh93 */
+		shprintf("set");
+		for (ele = 0; ele < NELEM(sh_options); ele++) {
+			if (sh_options[ele].name)
+				shprintf(" %co %s",
+					 Flag(ele) ? '-' : '+',
+					 sh_options[ele].name);
+		}
+		shprintf("\n");
+	}
+}
+
+char *
+getoptions(void)
+{
+	unsigned int ele;
+	char m[(int) FNFLAGS + 1];
+	char *cp = m;
+
+	for (ele = 0; ele < NELEM(sh_options); ele++)
+		if (sh_options[ele].c && Flag(ele))
+			*cp++ = sh_options[ele].c;
+	*cp = 0;
+	return str_save(m, ATEMP);
+}
+
+/* change a Flag(*) value; takes care of special actions */
+void
+change_flag(enum sh_flag f,
+    int what,		/* flag to change */
+    int newval)		/* what is changing the flag (command line vs set) */
+{
+	int oldval;
+
+	oldval = Flag(f);
+	Flag(f) = newval;
+	if (f == FMONITOR) {
+		if (what != OF_CMDLINE && newval != oldval)
+			j_change();
+	} else
+	if (0
+#ifdef VI
+	    || f == FVI
+#endif /* VI */
+#ifdef EMACS
+	    || f == FEMACS || f == FGMACS
+#endif /* EMACS */
+	   )
+	{
+		if (newval) {
+#ifdef VI
+			Flag(FVI) = 0;
+#endif /* VI */
+#ifdef EMACS
+			Flag(FEMACS) = Flag(FGMACS) = 0;
+#endif /* EMACS */
+			Flag(f) = newval;
+		}
+	} else
+	/* Turning off -p? */
+	if (f == FPRIVILEGED && oldval && !newval && getauxval(AT_SECURE) &&
+	    !dropped_privileges) {
+		gid_t gid = getgid();
+
+		setresgid(gid, gid, gid);
+		setgroups(1, &gid);
+		setresuid(ksheuid, ksheuid, ksheuid);
+
+		dropped_privileges = 1;
+	} else if (f == FPOSIX && newval) {
+		Flag(FBRACEEXPAND) = 0;
+	}
+	/* Changing interactive flag? */
+	if (f == FTALKING) {
+		if ((what == OF_CMDLINE || what == OF_SET) && procpid == kshpid)
+			Flag(FTALKING_I) = newval;
+	}
+}
+
+/* parse command line & set command arguments.  returns the index of
+ * non-option arguments, -1 if there is an error.
+ */
+int
+parse_args(char **argv,
+    int what,			/* OF_CMDLINE or OF_SET */
+    int *setargsp)
+{
+	static char cmd_opts[NELEM(sh_options) + 3]; /* o:\0 */
+	static char set_opts[NELEM(sh_options) + 5]; /* Ao;s\0 */
+	char *opts;
+	char *array = NULL;
+	Getopt go;
+	int i, optc, sortargs = 0, arrayset = 0;
+	unsigned int ele;
+
+	/* First call?  Build option strings... */
+	if (cmd_opts[0] == '\0') {
+		char *p, *q;
+
+		/* see cmd_opts[] declaration */
+		strlcpy(cmd_opts, "o:", sizeof cmd_opts);
+		p = cmd_opts + strlen(cmd_opts);
+		/* see set_opts[] declaration */
+		strlcpy(set_opts, "A:o;s", sizeof set_opts);
+		q = set_opts + strlen(set_opts);
+		for (ele = 0; ele < NELEM(sh_options); ele++) {
+			if (sh_options[ele].c) {
+				if (sh_options[ele].flags & OF_CMDLINE)
+					*p++ = sh_options[ele].c;
+				if (sh_options[ele].flags & OF_SET)
+					*q++ = sh_options[ele].c;
+			}
+		}
+		*p = '\0';
+		*q = '\0';
+	}
+
+	if (what == OF_CMDLINE) {
+		char *p;
+		/* Set FLOGIN before parsing options so user can clear
+		 * flag using +l.
+		 */
+		Flag(FLOGIN) = (argv[0][0] == '-' ||
+		    ((p = strrchr(argv[0], '/')) && *++p == '-'));
+		opts = cmd_opts;
+	} else
+		opts = set_opts;
+	ksh_getopt_reset(&go, GF_ERROR|GF_PLUSOPT);
+	while ((optc = ksh_getopt(argv, &go, opts)) != -1) {
+		int set = (go.info & GI_PLUS) ? 0 : 1;
+		switch (optc) {
+		case 'A':
+			arrayset = set ? 1 : -1;
+			array = go.optarg;
+			break;
+
+		case 'o':
+			if (go.optarg == NULL) {
+				/* lone -o: print options
+				 *
+				 * Note that on the command line, -o requires
+				 * an option (ie, can't get here if what is
+				 * OF_CMDLINE).
+				 */
+				printoptions(set);
+				break;
+			}
+			i = option(go.optarg);
+			if (i != -1 && set == Flag(i))
+				/* Don't check the context if the flag
+				 * isn't changing - makes "set -o interactive"
+				 * work if you're already interactive.  Needed
+				 * if the output of "set +o" is to be used.
+				 */
+				;
+			else if (i != -1 && (sh_options[i].flags & what))
+				change_flag((enum sh_flag) i, what, set);
+			else {
+				bi_errorf("%s: bad option", go.optarg);
+				return -1;
+			}
+			break;
+
+		case '?':
+			return -1;
+
+		default:
+			/* -s: sort positional params (at&t ksh stupidity) */
+			if (what == OF_SET && optc == 's') {
+				sortargs = 1;
+				break;
+			}
+			for (ele = 0; ele < NELEM(sh_options); ele++)
+				if (optc == sh_options[ele].c &&
+				    (what & sh_options[ele].flags)) {
+					change_flag((enum sh_flag) ele, what,
+					    set);
+					break;
+				}
+			if (ele == NELEM(sh_options)) {
+				internal_errorf("%s: `%c'", __func__, optc);
+				return -1; /* not reached */
+			}
+		}
+	}
+	if (!(go.info & GI_MINUSMINUS) && argv[go.optind] &&
+	    (argv[go.optind][0] == '-' || argv[go.optind][0] == '+') &&
+	    argv[go.optind][1] == '\0') {
+		/* lone - clears -v and -x flags */
+		if (argv[go.optind][0] == '-' && !Flag(FPOSIX))
+			Flag(FVERBOSE) = Flag(FXTRACE) = 0;
+		/* set skips lone - or + option */
+		go.optind++;
+	}
+	if (setargsp)
+		/* -- means set $#/$* even if there are no arguments */
+		*setargsp = !arrayset && ((go.info & GI_MINUSMINUS) ||
+		    argv[go.optind]);
+
+	if (arrayset && (!*array || *skip_varname(array, false))) {
+		bi_errorf("%s: is not an identifier", array);
+		return -1;
+	}
+	if (sortargs) {
+		for (i = go.optind; argv[i]; i++)
+			;
+		qsortp((void **) &argv[go.optind], (size_t) (i - go.optind),
+		    xstrcmp);
+	}
+	if (arrayset) {
+		set_array(array, arrayset, argv + go.optind);
+		for (; argv[go.optind]; go.optind++)
+			;
+	}
+
+	return go.optind;
+}
+
+/* parse a decimal number: returns 0 if string isn't a number, 1 otherwise */
+int
+getn(const char *as, int *ai)
+{
+	char *p;
+	long n;
+
+	n = strtol(as, &p, 10);
+
+	if (!*as || *p || INT_MIN >= n || n >= INT_MAX)
+		return 0;
+
+	*ai = (int)n;
+	return 1;
+}
+
+/* getn() that prints error */
+int
+bi_getn(const char *as, int *ai)
+{
+	int rv = getn(as, ai);
+
+	if (!rv)
+		bi_errorf("%s: bad number", as);
+	return rv;
+}
+
+/* -------- gmatch.c -------- */
+
+/*
+ * int gmatch(string, pattern)
+ * char *string, *pattern;
+ *
+ * Match a pattern as in sh(1).
+ * pattern character are prefixed with MAGIC by expand.
+ */
+
+int
+gmatch(const char *s, const char *p, int isfile)
+{
+	const char *se, *pe;
+
+	if (s == NULL || p == NULL)
+		return 0;
+	se = s + strlen(s);
+	pe = p + strlen(p);
+	/* isfile is false iff no syntax check has been done on
+	 * the pattern.  If check fails, just to a strcmp().
+	 */
+	if (!isfile && !has_globbing(p, pe)) {
+		size_t len = pe - p + 1;
+		char tbuf[64];
+		char *t = len <= sizeof(tbuf) ? tbuf :
+		    alloc(len, ATEMP);
+		debunk(t, p, len);
+		return !strcmp(t, s);
+	}
+	return do_gmatch((const unsigned char *) s, (const unsigned char *) se,
+	    (const unsigned char *) p, (const unsigned char *) pe);
+}
+
+/* Returns if p is a syntacticly correct globbing pattern, false
+ * if it contains no pattern characters or if there is a syntax error.
+ * Syntax errors are:
+ *	- [ with no closing ]
+ *	- imbalanced $(...) expression
+ *	- [...] and *(...) not nested (eg, [a$(b|]c), *(a[b|c]d))
+ */
+/*XXX
+- if no magic,
+	if dest given, copy to dst
+	return ?
+- if magic && (no globbing || syntax error)
+	debunk to dst
+	return ?
+- return ?
+*/
+int
+has_globbing(const char *xp, const char *xpe)
+{
+	const unsigned char *p = (const unsigned char *) xp;
+	const unsigned char *pe = (const unsigned char *) xpe;
+	int c;
+	int nest = 0, bnest = 0;
+	int saw_glob = 0;
+	int in_bracket = 0; /* inside [...] */
+
+	for (; p < pe; p++) {
+		if (!ISMAGIC(*p))
+			continue;
+		if ((c = *++p) == '*' || c == '?')
+			saw_glob = 1;
+		else if (c == '[') {
+			if (!in_bracket) {
+				saw_glob = 1;
+				in_bracket = 1;
+				if (ISMAGIC(p[1]) && p[2] == '!')
+					p += 2;
+				if (ISMAGIC(p[1]) && p[2] == ']')
+					p += 2;
+			}
+			/* XXX Do we need to check ranges here? POSIX Q */
+		} else if (c == ']') {
+			if (in_bracket) {
+				if (bnest)		/* [a*(b]) */
+					return 0;
+				in_bracket = 0;
+			}
+		} else if ((c & 0x80) && strchr("*+?@! ", c & 0x7f)) {
+			saw_glob = 1;
+			if (in_bracket)
+				bnest++;
+			else
+				nest++;
+		} else if (c == '|') {
+			if (in_bracket && !bnest)	/* *(a[foo|bar]) */
+				return 0;
+		} else if (c == /*(*/ ')') {
+			if (in_bracket) {
+				if (!bnest--)		/* *(a[b)c] */
+					return 0;
+			} else if (nest)
+				nest--;
+		}
+		/* else must be a MAGIC-MAGIC, or MAGIC-!, MAGIC--, MAGIC-]
+			 MAGIC-{, MAGIC-,, MAGIC-} */
+	}
+	return saw_glob && !in_bracket && !nest;
+}
+
+/* Function must return either 0 or 1 (assumed by code for 0x80|'!') */
+static int
+do_gmatch(const unsigned char *s, const unsigned char *se,
+    const unsigned char *p, const unsigned char *pe)
+{
+	int sc, pc;
+	const unsigned char *prest, *psub, *pnext;
+	const unsigned char *srest;
+
+	if (s == NULL || p == NULL)
+		return 0;
+	while (p < pe) {
+		pc = *p++;
+		sc = s < se ? *s : '\0';
+		s++;
+		if (!ISMAGIC(pc)) {
+			if (sc != pc)
+				return 0;
+			continue;
+		}
+		switch (*p++) {
+		case '[':
+			if (sc == 0 || (p = cclass(p, sc)) == NULL)
+				return 0;
+			break;
+
+		case '?':
+			if (sc == 0)
+				return 0;
+			break;
+
+		case '*':
+			if (p == pe)
+				return 1;
+			s--;
+			do {
+				if (do_gmatch(s, se, p, pe))
+					return 1;
+			} while (s++ < se);
+			return 0;
+
+		  /*
+		   * [*+?@!](pattern|pattern|..)
+		   *
+		   * Not ifdef'd KSH as this is needed for ${..%..}, etc.
+		   */
+		case 0x80|'+': /* matches one or more times */
+		case 0x80|'*': /* matches zero or more times */
+			if (!(prest = pat_scan(p, pe, 0)))
+				return 0;
+			s--;
+			/* take care of zero matches */
+			if (p[-1] == (0x80 | '*') &&
+			    do_gmatch(s, se, prest, pe))
+				return 1;
+			for (psub = p; ; psub = pnext) {
+				pnext = pat_scan(psub, pe, 1);
+				for (srest = s; srest <= se; srest++) {
+					if (do_gmatch(s, srest, psub, pnext - 2) &&
+					    (do_gmatch(srest, se, prest, pe) ||
+					    (s != srest && do_gmatch(srest,
+					    se, p - 2, pe))))
+						return 1;
+				}
+				if (pnext == prest)
+					break;
+			}
+			return 0;
+
+		case 0x80|'?': /* matches zero or once */
+		case 0x80|'@': /* matches one of the patterns */
+		case 0x80|' ': /* simile for @ */
+			if (!(prest = pat_scan(p, pe, 0)))
+				return 0;
+			s--;
+			/* Take care of zero matches */
+			if (p[-1] == (0x80 | '?') &&
+			    do_gmatch(s, se, prest, pe))
+				return 1;
+			for (psub = p; ; psub = pnext) {
+				pnext = pat_scan(psub, pe, 1);
+				srest = prest == pe ? se : s;
+				for (; srest <= se; srest++) {
+					if (do_gmatch(s, srest, psub, pnext - 2) &&
+					    do_gmatch(srest, se, prest, pe))
+						return 1;
+				}
+				if (pnext == prest)
+					break;
+			}
+			return 0;
+
+		case 0x80|'!': /* matches none of the patterns */
+			if (!(prest = pat_scan(p, pe, 0)))
+				return 0;
+			s--;
+			for (srest = s; srest <= se; srest++) {
+				int matched = 0;
+
+				for (psub = p; ; psub = pnext) {
+					pnext = pat_scan(psub, pe, 1);
+					if (do_gmatch(s, srest, psub,
+					    pnext - 2)) {
+						matched = 1;
+						break;
+					}
+					if (pnext == prest)
+						break;
+				}
+				if (!matched &&
+				    do_gmatch(srest, se, prest, pe))
+					return 1;
+			}
+			return 0;
+
+		default:
+			if (sc != p[-1])
+				return 0;
+			break;
+		}
+	}
+	return s == se;
+}
+
+static int
+posix_cclass(const unsigned char *pattern, int test, const unsigned char **ep)
+{
+	struct cclass *cc;
+	const unsigned char *colon;
+	size_t len;
+	int rval = 0;
+
+	if ((colon = strchr(pattern, ':')) == NULL || colon[1] != MAGIC) {
+		*ep = pattern - 2;
+		return -1;
+	}
+	*ep = colon + 3; /* skip MAGIC */
+	len = (size_t)(colon - pattern);
+
+	for (cc = cclasses; cc->name != NULL; cc++) {
+		if (!strncmp(pattern, cc->name, len) && cc->name[len] == '\0') {
+			if (cc->isctype(test))
+				rval = 1;
+			break;
+		}
+	}
+	if (cc->name == NULL) {
+		rval = -2;	/* invalid character class */
+	}
+	return rval;
+}
+
+static const unsigned char *
+cclass(const unsigned char *p, int sub)
+{
+	int c, d, rv, not, found = 0;
+	const unsigned char *orig_p = p;
+
+	if ((not = (ISMAGIC(*p) && *++p == '!')))
+		p++;
+	do {
+		/* check for POSIX character class (e.g. [[:alpha:]]) */
+		if ((p[0] == MAGIC && p[1] == '[' && p[2] == ':') ||
+		    (p[0] == '[' && p[1] == ':')) {
+			do {
+				const char *pp = p + (*p == MAGIC) + 2;
+				rv = posix_cclass(pp, sub, &p);
+				switch (rv) {
+				case 1:
+					found = 1;
+					break;
+				case -2:
+					return NULL;
+				}
+			} while (rv != -1 && p[0] == MAGIC && p[1] == '[' && p[2] == ':');
+			if (p[0] == MAGIC && p[1] == ']')
+				break;
+		}
+
+		c = *p++;
+		if (ISMAGIC(c)) {
+			c = *p++;
+			if ((c & 0x80) && !ISMAGIC(c)) {
+				c &= 0x7f;/* extended pattern matching: *+?@! */
+				/* XXX the ( char isn't handled as part of [] */
+				if (c == ' ') /* simile for @: plain (..) */
+					c = '(' /*)*/;
+			}
+		}
+		if (c == '\0')
+			/* No closing ] - act as if the opening [ was quoted */
+			return sub == '[' ? orig_p : NULL;
+		if (ISMAGIC(p[0]) && p[1] == '-' &&
+		    (!ISMAGIC(p[2]) || p[3] != ']')) {
+			p += 2; /* MAGIC- */
+			d = *p++;
+			if (ISMAGIC(d)) {
+				d = *p++;
+				if ((d & 0x80) && !ISMAGIC(d))
+					d &= 0x7f;
+			}
+			/* POSIX says this is an invalid expression */
+			if (c > d)
+				return NULL;
+		} else
+			d = c;
+		if (c == sub || (c <= sub && sub <= d))
+			found = 1;
+	} while (!(ISMAGIC(p[0]) && p[1] == ']'));
+
+	return (found != not) ? p+2 : NULL;
+}
+
+/* Look for next ) or | (if match_sep) in *(foo|bar) pattern */
+const unsigned char *
+pat_scan(const unsigned char *p, const unsigned char *pe, int match_sep)
+{
+	int nest = 0;
+
+	for (; p < pe; p++) {
+		if (!ISMAGIC(*p))
+			continue;
+		if ((*++p == /*(*/ ')' && nest-- == 0) ||
+		    (*p == '|' && match_sep && nest == 0))
+			return ++p;
+		if ((*p & 0x80) && strchr("*+?@! ", *p & 0x7f))
+			nest++;
+	}
+	return NULL;
+}
+
+/*
+ * quick sort of array of generic pointers to objects.
+ */
+void
+qsortp(void **base,			/* base address */
+    size_t n,				/* elements */
+    int (*f) (const void *, const void *)) /* compare function */
+{
+	qsort(base, n, sizeof(char *), f);
+}
+
+int
+xstrcmp(const void *p1, const void *p2)
+{
+	return (strcmp(*(char **)p1, *(char **)p2));
+}
+
+/* Initialize a Getopt structure */
+void
+ksh_getopt_reset(Getopt *go, int flags)
+{
+	go->optind = 1;
+	go->optarg = NULL;
+	go->p = 0;
+	go->flags = flags;
+	go->info = 0;
+	go->buf[1] = '\0';
+}
+
+
+/* getopt() used for shell built-in commands, the getopts command, and
+ * command line options.
+ * A leading ':' in options means don't print errors, instead return '?'
+ * or ':' and set go->optarg to the offending option character.
+ * If GF_ERROR is set (and option doesn't start with :), errors result in
+ * a call to bi_errorf().
+ *
+ * Non-standard features:
+ *	- ';' is like ':' in options, except the argument is optional
+ *	  (if it isn't present, optarg is set to 0).
+ *	  Used for 'set -o'.
+ *	- ',' is like ':' in options, except the argument always immediately
+ *	  follows the option character (optarg is set to the null string if
+ *	  the option is missing).
+ *	  Used for 'read -u2', 'print -u2' and fc -40.
+ *	- '#' is like ':' in options, expect that the argument is optional
+ *	  and must start with a digit or be the string "unlimited".  If the
+ *	  argument doesn't match, it is assumed to be missing and normal option
+ *	  processing continues (optarg is set to 0 if the option is missing).
+ *	  Used for 'typeset -LZ4' and 'ulimit -adunlimited'.
+ *	- accepts +c as well as -c IF the GF_PLUSOPT flag is present.  If an
+ *	  option starting with + is accepted, the GI_PLUS flag will be set
+ *	  in go->info.
+ */
+int
+ksh_getopt(char **argv, Getopt *go, const char *options)
+{
+	char c;
+	char *o;
+
+	if (go->p == 0 || (c = argv[go->optind - 1][go->p]) == '\0') {
+		char *arg = argv[go->optind], flag = arg ? *arg : '\0';
+
+		go->p = 1;
+		if (flag == '-' && arg[1] == '-' && arg[2] == '\0') {
+			go->optind++;
+			go->p = 0;
+			go->info |= GI_MINUSMINUS;
+			return -1;
+		}
+		if (arg == NULL ||
+		    ((flag != '-' ) && /* neither a - nor a + (if + allowed) */
+		    (!(go->flags & GF_PLUSOPT) || flag != '+')) ||
+		    (c = arg[1]) == '\0') {
+			go->p = 0;
+			return -1;
+		}
+		go->optind++;
+		go->info &= ~(GI_MINUS|GI_PLUS);
+		go->info |= flag == '-' ? GI_MINUS : GI_PLUS;
+	}
+	go->p++;
+	if (c == '?' || c == ':' || c == ';' || c == ',' || c == '#' ||
+	    !(o = strchr(options, c))) {
+		if (options[0] == ':') {
+			go->buf[0] = c;
+			go->optarg = go->buf;
+		} else {
+			warningf(true, "%s%s-%c: unknown option",
+			    (go->flags & GF_NONAME) ? "" : argv[0],
+			    (go->flags & GF_NONAME) ? "" : ": ", c);
+			if (go->flags & GF_ERROR)
+				bi_errorf(NULL);
+		}
+		return '?';
+	}
+	/* : means argument must be present, may be part of option argument
+	 *   or the next argument
+	 * ; same as : but argument may be missing
+	 * , means argument is part of option argument, and may be null.
+	 */
+	if (*++o == ':' || *o == ';') {
+		if (argv[go->optind - 1][go->p])
+			go->optarg = argv[go->optind - 1] + go->p;
+		else if (argv[go->optind])
+			go->optarg = argv[go->optind++];
+		else if (*o == ';')
+			go->optarg = NULL;
+		else {
+			if (options[0] == ':') {
+				go->buf[0] = c;
+				go->optarg = go->buf;
+				return ':';
+			}
+			warningf(true, "%s%s-`%c' requires argument",
+			    (go->flags & GF_NONAME) ? "" : argv[0],
+			    (go->flags & GF_NONAME) ? "" : ": ", c);
+			if (go->flags & GF_ERROR)
+				bi_errorf(NULL);
+			return '?';
+		}
+		go->p = 0;
+	} else if (*o == ',') {
+		/* argument is attached to option character, even if null */
+		go->optarg = argv[go->optind - 1] + go->p;
+		go->p = 0;
+	} else if (*o == '#') {
+		/* argument is optional and may be attached or unattached
+		 * but must start with a digit.  optarg is set to 0 if the
+		 * argument is missing.
+		 */
+		if (argv[go->optind - 1][go->p]) {
+			if (digit(argv[go->optind - 1][go->p]) ||
+			    !strcmp(&argv[go->optind - 1][go->p], "unlimited")) {
+				go->optarg = argv[go->optind - 1] + go->p;
+				go->p = 0;
+			} else
+				go->optarg = NULL;
+		} else {
+			if (argv[go->optind] && (digit(argv[go->optind][0]) ||
+			    !strcmp(argv[go->optind], "unlimited"))) {
+				go->optarg = argv[go->optind++];
+				go->p = 0;
+			} else
+				go->optarg = NULL;
+		}
+	}
+	return c;
+}
+
+/* print variable/alias value using necessary quotes
+ * (POSIX says they should be suitable for re-entry...)
+ * No trailing newline is printed.
+ */
+void
+print_value_quoted(const char *s)
+{
+	const char *p;
+	int inquote = 0;
+
+	/* Test if any quotes are needed */
+	for (p = s; *p; p++)
+		if (ctype(*p, C_QUOTE))
+			break;
+	if (!*p) {
+		shprintf("%s", s);
+		return;
+	}
+	for (p = s; *p; p++) {
+		if (*p == '\'') {
+			shprintf(inquote ? "'\\'" : "\\'");
+			inquote = 0;
+		} else {
+			if (!inquote) {
+				shprintf("'");
+				inquote = 1;
+			}
+			shf_putc(*p, shl_stdout);
+		}
+	}
+	if (inquote)
+		shprintf("'");
+}
+
+/* Print things in columns and rows - func() is called to format the ith
+ * element
+ */
+void
+print_columns(struct shf *shf, int n, char *(*func) (void *, int, char *, int),
+    void *arg, int max_width, int prefcol)
+{
+	char *str = alloc(max_width + 1, ATEMP);
+	int i;
+	int r, c;
+	int rows, cols;
+	int nspace;
+	int col_width;
+
+	/* max_width + 1 for the space.  Note that no space
+	 * is printed after the last column to avoid problems
+	 * with terminals that have auto-wrap.
+	 */
+	cols = x_cols / (max_width + 1);
+	if (!cols)
+		cols = 1;
+	rows = (n + cols - 1) / cols;
+	if (prefcol && n && cols > rows) {
+		int tmp = rows;
+
+		rows = cols;
+		cols = tmp;
+		if (rows > n)
+			rows = n;
+	}
+
+	col_width = max_width;
+	if (cols == 1)
+		col_width = 0; /* Don't pad entries in single column output. */
+	nspace = (x_cols - max_width * cols) / cols;
+	if (nspace <= 0)
+		nspace = 1;
+	for (r = 0; r < rows; r++) {
+		for (c = 0; c < cols; c++) {
+			i = c * rows + r;
+			if (i < n) {
+				shf_fprintf(shf, "%-*s",
+				    col_width,
+				    (*func)(arg, i, str, max_width + 1));
+				if (c + 1 < cols)
+					shf_fprintf(shf, "%*s", nspace, "");
+			}
+		}
+		shf_putchar('\n', shf);
+	}
+	afree(str, ATEMP);
+}
+
+/* Strip any nul bytes from buf - returns new length (nbytes - # of nuls) */
+int
+strip_nuls(char *buf, int nbytes)
+{
+	char *dst;
+
+	if ((dst = memchr(buf, '\0', nbytes))) {
+		char *end = buf + nbytes;
+		char *p, *q;
+
+		for (p = dst; p < end; p = q) {
+			/* skip a block of nulls */
+			while (++p < end && *p == '\0')
+				;
+			/* find end of non-null block */
+			if (!(q = memchr(p, '\0', end - p)))
+				q = end;
+			memmove(dst, p, q - p);
+			dst += q - p;
+		}
+		*dst = '\0';
+		return dst - buf;
+	}
+	return nbytes;
+}
+
+/* Like read(2), but if read fails due to non-blocking flag, resets flag
+ * and restarts read.
+ */
+int
+blocking_read(int fd, char *buf, int nbytes)
+{
+	int ret;
+	int tried_reset = 0;
+
+	while ((ret = read(fd, buf, nbytes)) < 0) {
+		if (!tried_reset && errno == EAGAIN) {
+			int oerrno = errno;
+			if (reset_nonblock(fd) > 0) {
+				tried_reset = 1;
+				continue;
+			}
+			errno = oerrno;
+		}
+		break;
+	}
+	return ret;
+}
+
+/* Reset the non-blocking flag on the specified file descriptor.
+ * Returns -1 if there was an error, 0 if non-blocking wasn't set,
+ * 1 if it was.
+ */
+int
+reset_nonblock(int fd)
+{
+	int flags;
+
+	if ((flags = fcntl(fd, F_GETFL)) < 0)
+		return -1;
+	if (!(flags & O_NONBLOCK))
+		return 0;
+	flags &= ~O_NONBLOCK;
+	if (fcntl(fd, F_SETFL, flags) < 0)
+		return -1;
+	return 1;
+}
+
+
+/* Like getcwd(), except bsize is ignored if buf is 0 (PATH_MAX is used) */
+char *
+ksh_get_wd(char *buf, int bsize)
+{
+	char *b;
+	char *ret;
+
+	/* Note: we could just use plain getcwd(), but then we'd had to
+	 * inject possibly allocated space into the ATEMP area. */
+	/* Assume getcwd() available */
+	if (!buf) {
+		bsize = PATH_MAX;
+		b = alloc(PATH_MAX + 1, ATEMP);
+	} else
+		b = buf;
+
+	ret = getcwd(b, bsize);
+
+	if (!buf) {
+		if (ret)
+			ret = aresize(b, strlen(b) + 1, ATEMP);
+		else
+			afree(b, ATEMP);
+	}
+
+	return ret;
+}
diff --git a/path.c b/path.c
@@ -0,0 +1,266 @@
+/*	$OpenBSD: path.c,v 1.22 2018/01/06 16:28:58 millert Exp $	*/
+
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+/*
+ *	Contains a routine to search a : separated list of
+ *	paths (a la CDPATH) and make appropriate file names.
+ *	Also contains a routine to simplify .'s and ..'s out of
+ *	a path name.
+ *
+ *	Larry Bouzane (larry@cs.mun.ca)
+ */
+
+static char	*do_phys_path(XString *, char *, const char *);
+
+/*
+ *	Makes a filename into result using the following algorithm.
+ *	- make result NULL
+ *	- if file starts with '/', append file to result & set cdpathp to NULL
+ *	- if file starts with ./ or ../ append cwd and file to result
+ *	  and set cdpathp to NULL
+ *	- if the first element of cdpathp doesnt start with a '/' xx or '.' xx
+ *	  then cwd is appended to result.
+ *	- the first element of cdpathp is appended to result
+ *	- file is appended to result
+ *	- cdpathp is set to the start of the next element in cdpathp (or NULL
+ *	  if there are no more elements.
+ *	The return value indicates whether a non-null element from cdpathp
+ *	was appended to result.
+ */
+int
+make_path(const char *cwd, const char *file,
+    char **cdpathp,		/* & of : separated list */
+    XString *xsp,
+    int *phys_pathp)
+{
+	int	rval = 0;
+	int	use_cdpath = 1;
+	char	*plist;
+	int	len;
+	int	plen = 0;
+	char	*xp = Xstring(*xsp, xp);
+
+	if (!file)
+		file = null;
+
+	if (file[0] == '/') {
+		*phys_pathp = 0;
+		use_cdpath = 0;
+	} else {
+		if (file[0] == '.') {
+			char c = file[1];
+
+			if (c == '.')
+				c = file[2];
+			if (c == '/' || c == '\0')
+				use_cdpath = 0;
+		}
+
+		plist = *cdpathp;
+		if (!plist)
+			use_cdpath = 0;
+		else if (use_cdpath) {
+			char *pend;
+
+			for (pend = plist; *pend && *pend != ':'; pend++)
+				;
+			plen = pend - plist;
+			*cdpathp = *pend ? ++pend : NULL;
+		}
+
+		if ((use_cdpath == 0 || !plen || plist[0] != '/') &&
+		    (cwd && *cwd)) {
+			len = strlen(cwd);
+			XcheckN(*xsp, xp, len);
+			memcpy(xp, cwd, len);
+			xp += len;
+			if (cwd[len - 1] != '/')
+				Xput(*xsp, xp, '/');
+		}
+		*phys_pathp = Xlength(*xsp, xp);
+		if (use_cdpath && plen) {
+			XcheckN(*xsp, xp, plen);
+			memcpy(xp, plist, plen);
+			xp += plen;
+			if (plist[plen - 1] != '/')
+				Xput(*xsp, xp, '/');
+			rval = 1;
+		}
+	}
+
+	len = strlen(file) + 1;
+	XcheckN(*xsp, xp, len);
+	memcpy(xp, file, len);
+
+	if (!use_cdpath)
+		*cdpathp = NULL;
+
+	return rval;
+}
+
+/*
+ * Simplify pathnames containing "." and ".." entries.
+ * ie, simplify_path("/a/b/c/./../d/..") returns "/a/b"
+ */
+void
+simplify_path(char *path)
+{
+	char	*cur;
+	char	*t;
+	int	isrooted;
+	char	*very_start = path;
+	char	*start;
+
+	if (!*path)
+		return;
+
+	if ((isrooted = (path[0] == '/')))
+		very_start++;
+
+	/* Before			After
+	 *  /foo/			/foo
+	 *  /foo/../../bar		/bar
+	 *  /foo/./blah/..		/foo
+	 *  .				.
+	 *  ..				..
+	 *  ./foo			foo
+	 *  foo/../../../bar		../../bar
+	 */
+
+	for (cur = t = start = very_start; ; ) {
+		/* treat multiple '/'s as one '/' */
+		while (*t == '/')
+			t++;
+
+		if (*t == '\0') {
+			if (cur == path)
+				/* convert empty path to dot */
+				*cur++ = '.';
+			*cur = '\0';
+			break;
+		}
+
+		if (t[0] == '.') {
+			if (!t[1] || t[1] == '/') {
+				t += 1;
+				continue;
+			} else if (t[1] == '.' && (!t[2] || t[2] == '/')) {
+				if (!isrooted && cur == start) {
+					if (cur != very_start)
+						*cur++ = '/';
+					*cur++ = '.';
+					*cur++ = '.';
+					start = cur;
+				} else if (cur != start)
+					while (--cur > start && *cur != '/')
+						;
+				t += 2;
+				continue;
+			}
+		}
+
+		if (cur != very_start)
+			*cur++ = '/';
+
+		/* find/copy next component of pathname */
+		while (*t && *t != '/')
+			*cur++ = *t++;
+	}
+}
+
+
+void
+set_current_wd(char *path)
+{
+	int len;
+	char *p = path;
+
+	if (!p && !(p = ksh_get_wd(NULL, 0)))
+		p = null;
+
+	len = strlen(p) + 1;
+
+	if (len > current_wd_size)
+		current_wd = aresize(current_wd, current_wd_size = len, APERM);
+	memcpy(current_wd, p, len);
+	if (p != path && p != null)
+		afree(p, ATEMP);
+}
+
+char *
+get_phys_path(const char *path)
+{
+	XString xs;
+	char *xp;
+
+	Xinit(xs, xp, strlen(path) + 1, ATEMP);
+
+	xp = do_phys_path(&xs, xp, path);
+
+	if (!xp)
+		return NULL;
+
+	if (Xlength(xs, xp) == 0)
+		Xput(xs, xp, '/');
+	Xput(xs, xp, '\0');
+
+	return Xclose(xs, xp);
+}
+
+static char *
+do_phys_path(XString *xsp, char *xp, const char *path)
+{
+	const char *p, *q;
+	int len, llen;
+	int savepos;
+	char lbuf[PATH_MAX];
+
+	Xcheck(*xsp, xp);
+	for (p = path; p; p = q) {
+		while (*p == '/')
+			p++;
+		if (!*p)
+			break;
+		len = (q = strchr(p, '/')) ? (size_t)(q - p) : strlen(p);
+		if (len == 1 && p[0] == '.')
+			continue;
+		if (len == 2 && p[0] == '.' && p[1] == '.') {
+			while (xp > Xstring(*xsp, xp)) {
+				xp--;
+				if (*xp == '/')
+					break;
+			}
+			continue;
+		}
+
+		savepos = Xsavepos(*xsp, xp);
+		Xput(*xsp, xp, '/');
+		XcheckN(*xsp, xp, len + 1);
+		memcpy(xp, p, len);
+		xp += len;
+		*xp = '\0';
+
+		llen = readlink(Xstring(*xsp, xp), lbuf, sizeof(lbuf) - 1);
+		if (llen < 0) {
+			/* EINVAL means it wasn't a symlink... */
+			if (errno != EINVAL)
+				return NULL;
+			continue;
+		}
+		lbuf[llen] = '\0';
+
+		/* If absolute path, start from scratch.. */
+		xp = lbuf[0] == '/' ? Xstring(*xsp, xp) :
+		    Xrestpos(*xsp, xp, savepos);
+		if (!(xp = do_phys_path(xsp, xp, lbuf)))
+			return NULL;
+	}
+	return xp;
+}
diff --git a/reallocarray.c b/reallocarray.c
@@ -0,0 +1,38 @@
+/*	$OpenBSD: reallocarray.c,v 1.3 2015/09/13 08:31:47 guenther Exp $	*/
+/*
+ * Copyright (c) 2008 Otto Moerbeek <otto@drijf.net>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+/*
+ * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX
+ * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW
+ */
+#define MUL_NO_OVERFLOW	((size_t)1 << (sizeof(size_t) * 4))
+
+void *
+reallocarray(void *optr, size_t nmemb, size_t size)
+{
+	if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
+	    nmemb > 0 && SIZE_MAX / nmemb < size) {
+		errno = ENOMEM;
+		return NULL;
+	}
+	return realloc(optr, size * nmemb);
+}
diff --git a/sh.1 b/sh.1
@@ -0,0 +1,2241 @@
+.\"	$OpenBSD: sh.1,v 1.151 2018/12/16 13:08:35 schwarze Exp $
+.\"
+.\" Copyright (c) 2015 Jason McIntyre <jmc@openbsd.org>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate: December 16 2018 $
+.Dt SH 1
+.Os
+.Sh NAME
+.Nm sh
+.Nd command language interpreter
+.Sh SYNOPSIS
+.Nm sh
+.Op Fl abCefhimnuvx
+.Op Fl o Ar option
+.Op Fl c Ar string | Fl s | Ar file
+.Sh DESCRIPTION
+The
+.Nm
+utility is a
+.Em command language interpreter :
+it reads one or more commands,
+either from the command line or from a file
+(a shell script),
+and then sets about executing those commands.
+Thus it is the
+main interface between the user and the operating system.
+.Pp
+This version of
+.Nm
+is actually
+.Nm ksh
+in disguise.
+As such, it also supports the features described in
+.Xr ksh 1 .
+This manual page describes only the parts
+relevant to a POSIX compliant
+.Nm .
+If portability is a concern,
+use only those features described in this page.
+.Pp
+The shell receives input as follows:
+.Pp
+.Bl -tag -width "-c stringXXX" -offset indent -compact
+.It Fl c Ar string
+Read commands from
+.Ar string .
+.It Fl s
+Read commands from standard input
+(the default).
+.It Ar file
+Read commands from
+.Ar file .
+.El
+.Pp
+The options below can be specified with a
+.Sq Cm +
+rather than
+.Sq Fl ,
+meaning to unset the option.
+They can also be set or unset using the
+.Ic set
+command.
+Some options have equivalent long names,
+indicated at the start of the description,
+which can be used with the
+.Fl o
+option.
+.Bl -tag -width Ds
+.It Fl a
+Allexport.
+Variable assignments are exported to all child processes
+of the running shell.
+If the assignment precedes a command it does not persist
+after that command has finished running,
+unless the command is a special builtin
+or one of the builtins
+.Ic getopts
+or
+.Ic read
+makes the assignment.
+.It Fl b
+Notify.
+The user is given notice asynchronously when background jobs complete.
+.It Fl C
+Noclobber.
+Do not permit the redirection operator
+.Pq Sq >
+to clobber (overwrite) existing files.
+.It Fl e
+Errexit.
+Exit the shell immediately should an error occur or a command fail.
+For pipelines and
+.Cm &&
+and
+.Cm ||
+constructs, only exit if the last component fails.
+Errexit is ignored for
+.Ic while ,
+.Ic until ,
+.Ic if ,
+and
+.Ic elif
+lists and pipelines beginning
+.Sq !\& .
+.It Fl f
+Noglob.
+Do not expand file name patterns.
+.It Fl h
+When a utility is first executed,
+hash (record) its location
+so that future invocations do not need to search for it.
+.It Fl i
+Enable behaviour convenient for an interactive shell.
+This option is set by default
+if the session is attached to a terminal.
+.It Fl m
+Monitor.
+Fully enable job control:
+enable the
+.Ic bg
+and
+.Ic fg
+builtins;
+report completion status when jobs finish;
+report when a foreground process stops;
+and report when a job changes status.
+The processes of a job share their own process group.
+This option is set by default for interactive shells.
+.It Fl n
+Noexec.
+Read commands but do not execute them \(en
+useful for checking syntax errors in scripts.
+This option is ignored for interactive shells.
+.It Fl o Ar option
+Specify an option by its long name.
+Those described below have no equivalent option letter:
+.Pp
+.Bl -tag -width "ignoreeof" -offset 3n -compact
+.It ignoreeof
+Ignore an end-of-file
+.Pq Sq ^D .
+EOF normally logs a user out,
+so setting this can prevent accidental logouts
+(the user will need to explicitly use the
+.Ic exit
+command).
+.It nolog
+Do not enter function definitions into command history.
+.It posix
+Enable POSIX mode
+(see
+.Sx STANDARDS ) .
+.It vi
+Enable
+.Xr vi 1
+command line editing.
+.El
+.It Fl u
+Nounset.
+If a command references an unset parameter,
+write an error to standard output instead of executing the command.
+This option is ignored for the special parameters
+.Sq *
+and
+.Sq @ .
+If the shell is not interactive,
+immediately exit.
+.It Fl v
+Verbose.
+Write input to standard error after reading it.
+.It Fl x
+Xtrace.
+Write a trace for each command to standard error after expanding it,
+and before executing it.
+.El
+.Sh BUILTINS
+The shell has a number of
+.Em built-ins
+available:
+utilities that are included as part of the shell.
+The shell does not need to search for them
+and can execute them directly.
+.Pp
+A number of built-ins are special in that
+a syntax error can cause a running shell to abort,
+and, after the built-in completes,
+variable assignments remain in the current environment.
+The following built-ins are special:
+.Ic .\& , :\& , break , continue ,
+.Ic eval , exec , exit , export ,
+.Ic readonly , return , set , shift ,
+.Ic times , trap ,
+and
+.Ic unset .
+.Pp
+The built-ins available to
+.Nm
+are listed below.
+Unless otherwise indicated,
+they exit 0 on success,
+and >0 if an error occurs.
+.Bl -tag -width 2n
+.It Ic .\& Ar file
+Execute the commands in
+.Ar file ,
+in the current environment.
+The actual file need not be executable,
+and its location is determined by searching
+.Ev PATH
+if there are no slashes in the filename.
+The exit status is that of the last command returned,
+or zero if no commands were executed.
+If no readable file can be found,
+a non-interactive shell will abort;
+an interactive shell writes an error message
+and returns a non-zero exit status.
+.It Ic :\& Op Ar arg ...
+The
+.Ic :\&
+command does nothing \(en
+it is a placeholder for when a command is required.
+Its exit status is always zero.
+.It Ic alias Op Ar name Ns Oo = Ns Ar value Oc Ar ...
+Define an alias
+.Ar name
+to
+.Ar value ;
+when the shell encounters a command name that is an alias,
+its value is substituted.
+If
+.Ar value
+ends in a blank,
+the next word is checked for alias substitution too.
+If only a
+.Ar name
+is specified,
+display the value of that alias;
+if no arguments are given,
+list all aliases and their values.
+Aliases are visible in the current environment and that of subshells,
+but not by the parent process of the current shell
+or by utilities invoked by it.
+.It Ic bg Op Ar id ...
+Select a job by
+.Ar id
+(see the
+.Ic jobs
+command, below)
+to run in the background.
+The default job is
+.Qq %+ .
+.It Ic break Op Ar n
+Exit from the innermost
+.Ic for , while ,
+or
+.Ic until
+loop,
+or from loop level
+.Ar n .
+.It Ic cd Oo Fl L | P Oc Op Ar dir
+Change the current working directory to
+.Ar dir ,
+or
+.Ev $HOME
+by default.
+If
+.Ar dir
+is set to
+.Sq - ,
+change to the previous working directory and
+print the (now current) working directory.
+If
+.Ar dir
+does not begin with a slash or dot,
+.Ev CDPATH
+is searched for the directory.
+.Pp
+The options to the
+.Ic cd
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl L
+Do not resolve symbolic links before processing
+.Qq ..
+components.
+.It Fl P
+Resolve symbolic links before processing
+.Qq ..
+components.
+.El
+.It Ic command Oo Fl p | V | v Oc Ar command Op Ar arg ...
+Invoke
+.Ar command
+(and any optional arguments),
+overriding any functions with the same name,
+and without any of the properties that special built-ins have.
+.Pp
+The options to
+.Ic command
+are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl p
+Use a default value for
+.Ev PATH
+to search for the command.
+.It Fl V
+Do not invoke
+.Ar command ,
+but identify how the shell will interpret it
+(such as a function or special built-in).
+.It Fl v
+Do not invoke
+.Ar command ,
+but identify the pathname the shell will use to run it.
+For aliases, a command to define that alias is printed.
+For shell reserved words, shell functions, and built-in utilities,
+just the name is printed.
+.El
+.Pp
+The exit status is that of
+.Ar command ,
+or 126 if
+.Ar command
+could not be invoked,
+or 127 if an error occurred in
+.Ic command
+itself or
+.Ar command
+could not be found.
+If the options
+.Fl V
+or
+.Fl v
+are given,
+the exit status is 0 on success,
+or >0 if an error occurs.
+.It Ic continue Op Ar n
+Go directly to the next iteration of the innermost
+.Ic for , while ,
+or
+.Ic until
+loop,
+or from loop level
+.Ar n .
+.It Ic eval Op Ar arg ...
+Concatenate the arguments given
+and interpret them as a command.
+The exit status is that of the resulting command,
+zero if no arguments are given,
+or >0 if the resulting command could not be correctly parsed.
+.It Ic exec Op Ar command Op Ar arg ...
+Replace the shell with
+.Ar command
+(and any optional arguments),
+without creating a new process.
+The exit status is that of
+.Ar command ,
+or 126 if
+.Ar command
+could not be invoked,
+or 127 if
+.Ar command
+could not be found.
+If no command is given but a redirection happens,
+the exit status is 1\(en125;
+otherwise
+.Ic exec
+returns 0.
+.It Ic exit Op Ar n
+Exit the shell with exit status
+.Ar n ,
+or that of the last command executed.
+.It Ic export Oo Fl p Oc Ar name Ns Oo = Ns Ar value Oc Ar ...
+Make the variable
+.Ar name
+visible to subsequently run commands,
+optionally setting it to
+.Ar value .
+.Pp
+The options to the
+.Ic export
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl p
+List all exported variables in a manner that can be reinput to the shell.
+.El
+.It Ic false
+Return a false (non-zero) value.
+.It Xo
+.Ic fc
+.Op Fl lnr
+.Op Fl e Ar editor
+.Op Fl s Op Ar old Ns = Ns Ar new
+.Op Ar first Op Ar last
+.Xc
+Edit commands from command history using
+.Xr ed 1 .
+After editing,
+the new commands are executed by the shell.
+.Pp
+The options to the
+.Ic fc
+command are as follows:
+.Pp
+.Bl -tag -width "-s [old=new]" -offset 3n -compact
+.It Fl e Ar editor
+Edit commands using
+.Ar editor .
+See also
+.Ev FCEDIT .
+.It Fl l
+List the command history.
+.It Fl ln
+List the command history without command numbers.
+.It Fl r
+Edit or list
+.Pq Fl lr
+commands in reverse order.
+.It Fl s Op Ar old Ns = Ns Ar new
+Reexecute a single command
+without invoking an editor.
+The first occurrence of the string
+.Ar old
+in the command is replaced by
+.Ar new .
+.El
+.Pp
+A range of commands can be specified,
+.Ar first
+to
+.Ar last .
+Their format can be numerical,
+to select by command number;
+.Sq - Ns Ar n ,
+to select a command executed that number of commands previous;
+or a string which matches the beginning of the command.
+If no range is given,
+the last command in command history is edited,
+or reexecuted
+.Pq Fl s ,
+or the previous 16 commands in command history are listed
+.Pq Fl l .
+If
+.Ar first
+is newer than
+.Ar last ,
+commands are processed in reverse order
+(as if
+.Fl r
+had been given);
+if either are out of range,
+the oldest or newest values are used.
+.It Ic fg Op Ar id ...
+Select a job by
+.Ar id
+(see the
+.Ic jobs
+command, below)
+to run in the foreground.
+The default job is
+.Qq %+ .
+.It Ic getopts Ar optstring name Op Ar arg ...
+When invoked,
+.Ic getopts
+processes the positional parameters
+(or any
+.Ar arg
+passed to it)
+as a list of options and option arguments.
+.Ic getopts
+sets the variable
+.Ar name
+to the option found,
+.Ev OPTARG
+to its argument,
+and
+.Ev OPTIND
+to the index of the next variable to be processed.
+.Pp
+The string
+.Ar optstring
+contains a list of acceptable options;
+a colon following an option indicates it requires an argument.
+If an option not recognised by
+.Ar optstring
+is found,
+.Ar name
+is set to
+.Sq ?\& ;
+if the first character of
+.Ar optstring
+is a colon,
+.Ev OPTARG
+is set to the unsupported option,
+otherwise an error message is displayed.
+.It Ic hash Op Fl r | Ar utility
+Add
+.Ar utility
+to the hash list
+or remove
+.Pq Fl r
+all utilities from the hash list.
+Without arguments, show the utilities currently hashed.
+.It Ic jobs Oo Fl l | p Oc Op Ar id ...
+Display the status of all jobs in the current shell environment,
+or those selected by
+.Ar id .
+.Pp
+The options to the
+.Ic jobs
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl l
+Additionally display the process group ID.
+.It Fl p
+Display only the process group ID.
+.El
+.Pp
+Job
+.Ar id
+can be selected in one of the following ways:
+.Pp
+.Bl -tag -width "%?string" -offset 3n -compact
+.It %%
+The current job.
+.It %+
+The current job.
+.It %-
+The previous job.
+.It % Ns Ar n
+Job number
+.Ar n .
+.It % Ns Ar string
+Job with command matching
+.Ar string .
+.It %? Ns Ar string
+Job with command containing
+.Ar string .
+.El
+.It Xo
+.Ic kill
+.Op Fl l Op Ar signal
+.Op Fl s Ar signal
+.Oo Fl Ar signal Oc Ar pid ...
+.Xc
+Send a signal,
+by default
+.Dv SIGTERM ,
+to the process with ID
+.Ar pid .
+.Pp
+The options to the
+.Ic kill
+command are as follows:
+.Pp
+.Bl -tag -width "-l [signal]" -offset 3n -compact
+.It Fl l Op Ar signal
+List all supported signals,
+or the signal name corresponding to
+.Ar signal
+number or the exit status of a command killed by a signal.
+.It Fl s Ar signal
+Send the process
+.Ar signal
+name.
+.It Fl Ar signal
+Send the process
+.Ar signal
+name or number.
+.It Ar pid
+A process ID,
+process group ID,
+or a job ID (see
+.Ic jobs ,
+above).
+The process ID 0 signals all processes in the current process group.
+.El
+.Pp
+The supported signal numbers are:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It " 0"
+Do not signal a process,
+but determine whether an ID exists.
+.It " 1"
+.Dv SIGHUP :
+Terminal line hangup.
+.It " 2"
+.Dv SIGINT :
+Interrupt a program.
+.It " 3"
+.Dv SIGQUIT :
+Quit a program.
+.It " 6"
+.Dv SIGABRT :
+Call
+.Xr abort 3 .
+.It " 9"
+.Dv SIGKILL :
+Kill a program.
+Cannot be caught or ignored.
+.It "14"
+.Dv SIGALRM :
+Real-time timer expired.
+.It "15"
+.Dv SIGTERM :
+Software termination signal.
+.El
+.It Ic pwd Op Fl L | P
+Print the current working directory.
+.Pp
+The options to the
+.Ic pwd
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl L
+Print the logical path to the current working directory
+i.e. display symbolic links followed.
+.It Fl P
+Print the physical path to the current working directory
+i.e. display symbolic links resolved.
+.El
+.Pp
+If both options are given,
+the last specified is used;
+if none are given,
+the default is
+.Fl L .
+.It Ic read Oo Fl r Oc Ar name ...
+Read a line from standard input.
+The line is split into fields,
+with each field assigned to a variable,
+.Ar name ,
+in turn
+(first field assigned to first variable, and so on).
+If there are more fields than variables,
+the last variable will contain all the remaining fields.
+If there are more variables than fields,
+the remaining variables are set to empty strings.
+A backslash in the input line causes the shell to prompt for further input.
+.Pp
+The options to the
+.Ic read
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl r
+Ignore backslash sequences.
+.El
+.It Ic readonly Oo Fl p Oc Ar name Ns Op = Ns Ar value
+Mark variable
+.Ar name
+as readonly,
+and optionally set it to
+.Ar value .
+Readonly variables cannot be later assigned values or unset.
+.Pp
+The options to the
+.Ic readonly
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl p
+Display the names and values of all readonly variables
+in a manner which can be reinput to the shell.
+.El
+.It Ic return Op Ar n
+Exit the current function or
+.Ic .\&
+script with exit status
+.Ar n ,
+or that of the last command executed.
+.It Xo
+.Ic set
+.Op Fl abCefhmnuvx
+.Op Fl o Op Ar option
+.Op Ar arg ...
+.Xc
+Set options and positional parameters.
+Without options or arguments,
+display the names and values of all shell variables.
+.Pp
+The options are described in the options description
+at the beginning of this manual.
+The sequence
+.Ql set -o
+displays the current option settings;
+the sequence
+.Ql set +o
+displays,
+in a format suitable to be reinput to the shell,
+a command suitable to achieve the current option settings.
+.Pp
+Any arguments are assigned to the positional parameters,
+with the special parameter
+.Sq #
+set to the number of positional parameters.
+The sequence
+.Ql set --
+indicates an end to option processing
+(i.e. only arguments follow);
+.Ql set --
+by itself unsets all positional parameters
+and sets
+.Sq #
+to zero.
+.It Ic shift Op Ar n
+Shift the positional parameters
+.Ar n
+times
+(by default once).
+Parameter 1 takes the value of parameter
+.Sq 1+ Ns Ar n ,
+parameter 2 takes
+.Sq 2+ Ns Ar n ,
+and so on.
+Parameters
+.Sq #
+to
+.Sq Po #\(mi Ns Ar n Pc Ns +1
+and downwards are unset and
+.Sq #
+is updated to the new number of positional parameters.
+If
+.Ar n
+is 0,
+no change occurs.
+.It Ic times
+Display accumulated process times for the shell (user and system)
+and all child processes (user and system).
+.It Ic trap Op Ar action signal ...
+Perform
+.Ar action
+whenever
+.Ar signal
+is caught.
+Without arguments,
+display a list of all traps and actions,
+in a format suitable to be reinput to the shell.
+.Pp
+If
+.Ar action
+is
+.Sq -
+or an integer,
+reset
+.Ar signal
+to its default value;
+if it is empty
+.Pq Qq ,
+ignore
+.Ar signal .
+If
+.Ar signal
+is
+.Qq EXIT
+or 0,
+perform
+.Ar action
+when the shell exits;
+otherwise
+.Ar signal
+should be a signal name
+(without the SIG prefix)
+or number.
+.It Ic true
+Return a true (zero) value.
+.It Ic type Ar command ...
+For each
+.Ar command ,
+show how the shell would interpret it.
+.It Ic ulimit Op Fl f Ar n
+Limit the maximum size of a file that can be created to
+.Ar n
+blocks.
+Without arguments,
+display the current file size limit.
+.It Ic umask Oo Fl S Oc Op Ar mask
+Set the file mode creation mask to
+.Ar mask .
+The creation mask determines the default permissions
+a newly created file or directory will have.
+If
+.Ar mask
+is not specified,
+display the current creation mask.
+.Pp
+The options to the
+.Ic umask
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl S
+Display symbolic output.
+.El
+.Pp
+See
+.Xr chmod 1
+for the format of
+.Ar mask .
+.It Ic unalias Oo Fl a Oc Ar name ...
+Remove the alias definition of alias
+.Ar name .
+.Pp
+The options to the
+.Ic unalias
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl a
+Remove all alias definitions.
+.El
+.It Ic unset Oo Fl fv Oc Ar name ...
+Unset variable or function
+.Ar name .
+.Pp
+The options to the
+.Ic unset
+command are as follows:
+.Pp
+.Bl -tag -width Ds -offset 3n -compact
+.It Fl f
+Treat
+.Ar name
+as a function.
+.It Fl v
+Treat
+.Ar name
+as a variable (the default).
+.El
+.It Ic wait Op Ar pid ...
+Wait until all the processes specified by process or job ID
+.Ar pid
+have terminated.
+If no
+.Ar pid
+is specified,
+wait until all processes have terminated.
+The exit status is 0 on success,
+1\(en126 if an error occurs,
+or 127 if
+.Ar pid
+was unknown.
+.El
+.Sh COMMAND HISTORY AND COMMAND LINE EDITING
+When a shell is interactive,
+it keeps a record of commands run in a
+.Em command history ,
+either internally in memory or in a file,
+as determined by
+.Dv HISTFILE .
+The command line and all the commands in command history
+can be edited using commands similar to those of
+.Xr vi 1 .
+.Pp
+There are two modes,
+.Em interactive
+and
+.Em command .
+The shell starts in interactive mode.
+In this mode text is entered normally.
+A
+.Aq newline
+executes the current command line.
+The command line, unless empty, is entered into command history.
+The
+.Aq ESC
+key is used to enter command mode,
+where commands similar to those used by
+.Xr vi 1
+are available.
+A Ctrl-L sequence
+.Pq ^L
+can be used in this mode to
+redraw the current command line.
+.Pp
+Where noted,
+some commands may be preceded by a numerical
+.Ar count ,
+which causes the command to be repeated that number of times.
+The term
+.Em word
+is used to denote a sequence of letters, digits, or underscores;
+.Em bigword
+denotes a sequence of whitespace delineated characters.
+.Pp
+The commands for command mode:
+.Bl -tag -width "<newline>"
+.It Ic =
+Display the possible shell word expansion.
+.It Ic \e
+Perform pathname expansion on the current word,
+matching the largest possible unique expansion,
+then enter insert mode.
+.It Ic *
+Perform pathname expansion on the current word,
+substituting every possible expansion,
+then enter insert mode.
+.It Ic @ Ns Ar c
+Perform the commands defined by the alias
+.No _ Ns Ar c ,
+where
+.Ar c
+is a single letter alphabetical character.
+.It Oo Ar count Oc Ns Ic ~
+Convert the character from lowercase to upper or vice versa.
+.It Oo Ar count Oc Ns Ic .\&
+Repeat the most recent non-motion command.
+If no
+.Ar count
+is given, use that of the repeated command,
+if any.
+.It Oo Ar n Oc Ns Ic v
+Use
+.Xr vi 1
+to edit command number
+.Ar n
+in command history,
+or the current command if none given.
+.It Xo
+.Oo Ar count Oc Ns Ic l ,
+.Oo Ar count Oc Ns Aq space
+.Xc
+Move right.
+.It Oo Ar count Oc Ns Ic h
+Move left.
+.It Oo Ar count Oc Ns Ic w
+Move to the start of the next word.
+.It Oo Ar count Oc Ns Ic W
+Move to the start of the next big word.
+.It Oo Ar count Oc Ns Ic e
+Move to the end of the current word,
+or the end of the next word if the cursor is currently
+at the end of a word.
+.It Oo Ar count Oc Ns Ic E
+Move to the end of the current bigword,
+or the end of the next bigword if the cursor is currently
+at the end of a bigword.
+.It Oo Ar count Oc Ns Ic b
+Move to the start of the current word,
+or the start of the next word if the cursor is currently
+at the start of a word.
+.It Oo Ar count Oc Ns Ic B
+Move to the start of the current bigword,
+or the start of the next bigword if the cursor is currently
+at the start of a bigword.
+.It Ic ^
+Move to the first non-blank character.
+.It Ic $
+Move to the end of the current line.
+.It Ic 0
+Move to the beginning of the current line.
+.It Oo Ar count Oc Ns Ic |\&
+Move to the beginning of the current line
+or the character position specified by
+.Ar count .
+.It Oo Ar count Oc Ns Ic f Ns Ar c
+Move to the next instance of the
+character
+.Ar c .
+.It Oo Ar count Oc Ns Ic F Ns Ar c
+Move to the last instance of the
+character
+.Ar c .
+.It Oo Ar count Oc Ns Ic t Ns Ar c
+Move to the character before the next instance of the
+character
+.Ar c .
+.It Oo Ar count Oc Ns Ic T Ns Ar c
+Move to the character after the last instance of the
+character
+.Ar c .
+.It Oo Ar count Oc Ns Ic ;\&
+Repeat the last
+.Ic f , F , t ,
+or
+.Ic T
+command.
+Ignore any
+.Ar count
+specified with the last command.
+.It Oo Ar count Oc Ns Ic ,\&
+Repeat the last
+.Ic f , F , t ,
+or
+.Ic T
+command,
+but in the opposite direction.
+Ignore any
+.Ar count
+specified with the last command.
+.It Ic a
+Enter insert mode after the current cursor position.
+.It Ic A
+Enter insert mode after the end of the current line.
+.It Ic i
+Enter insert mode at the current cursor position.
+.It Ic I
+Enter insert mode at the beginning of the current line.
+.It Ic R
+Enter insert mode at the current cursor position,
+replacing any characters thereafter.
+.It Oo Ar count Oc Ns Ic c Ns Ar motion
+Delete the characters between the cursor and the motion command specified,
+then enter insert mode.
+A special motion command,
+.Ic c ,
+may be used to delete the entire line.
+The
+.Ar count
+argument is ignored for the commands
+.Ic 0 , ^ , $ ,
+and
+.Ic c .
+If the motion moves towards the beginning of the line
+the character under the cursor is not deleted;
+if it moves towards the end of the line
+it is deleted.
+.It Ic C
+Delete the characters between the cursor and the line end,
+then enter insert mode.
+.It Ic S
+Clear the entire line,
+then enter insert mode.
+.It Oo Ar count Oc Ns Ic r
+Replace the character under the cursor with the next typed character.
+With a
+.Ar count ,
+replace the current character
+and the corresponding number of following characters.
+.It Oo Ar count Oc Ns Ic _
+After the cursor,
+append a
+.Aq space
+and the
+.Ar count Ns th
+bigword (by default the last entered)
+from the previous input line,
+then enter insert mode.
+.It Oo Ar count Oc Ns Ic x
+Delete the character under the cursor,
+placing it in the save buffer.
+.It Oo Ar count Oc Ns Ic X
+Delete the character before the cursor,
+placing it in the save buffer.
+.It Oo Ar count Oc Ns Ic d Ns Ar motion
+Delete the characters between the cursor and the motion command specified,
+placing them in the save buffer.
+A special motion command,
+.Ic d ,
+may be used to delete the entire line.
+If the motion moves towards the beginning of the line
+the character under the cursor is not deleted.
+.It Oo Ar count Oc Ns Ic D
+Delete the characters between the cursor and the line end,
+placing them in the save buffer.
+.It Oo Ar count Oc Ns Ic y Ns Ar motion
+Yank (copy) the characters between the cursor and the motion command specified,
+placing them in the save buffer.
+A special motion command,
+.Ic y ,
+may be used to yank the entire line.
+If the motion moves towards the beginning of the line
+the character under the cursor is not yanked.
+.It Oo Ar count Oc Ns Ic Y
+Yank (copy) the characters between the cursor and the line end,
+placing them in the save buffer.
+.It Oo Ar count Oc Ns Ic p
+Paste the contents of the save buffer after the cursor.
+.It Oo Ar count Oc Ns Ic P
+Paste the contents of the save buffer before the cursor.
+.It Oo Ar count Oc Ns Ic u
+Undo the last change to the edit line.
+.It Oo Ar count Oc Ns Ic U
+Undo all changes to the edit line.
+.It Xo
+.Oo Ar count Oc Ns Ic k ,
+.Oo Ar count Oc Ns Ic -\&
+.Xc
+Replace the current command line with the previous entry in history.
+.It Xo
+.Oo Ar count Oc Ns Ic j ,
+.Oo Ar count Oc Ns Ic +\&
+.Xc
+Replace the current command line with the next entry in history.
+.It Oo Ar n Oc Ns Ic G
+Replace the current command line with command number
+.Ar n
+in command history,
+or the oldest command if none given.
+.It / Ns Ar pattern
+Moving backwards through history,
+replace the current command line with the first that matches
+.Ar pattern .
+A
+.Sq ^
+at the beginning of the pattern searches only for entries beginning with
+.Ar pattern .
+An empty pattern matches the last search.
+.It ? Ns Ar pattern
+As above,
+but searching forwards.
+.It Ic n
+Repeat the most recent pattern search.
+.It Ic N
+Repeat the most recent pattern search,
+but in the opposite direction.
+.El
+.Sh SHELL GRAMMAR
+The shell reads its input as described above.
+After that it follows a fairly simple chain of operations
+to parse that input:
+.Bl -dash
+.It
+The shell breaks the input into
+.Em words
+and
+.Em operators .
+Words are the command text the user wishes run;
+operators are special characters which describe
+how the shell should interact with the commands.
+.It
+The shell
+.Em expands
+the command text according to the rules of expansion.
+.It
+Words are subject to
+.Em field splitting ,
+where the command text is separated into commands
+and arguments to commands.
+.It
+The shell performs any
+.Em redirection .
+.It
+The shell runs the commands.
+Argument names are assigned to
+.Em positional parameters ,
+with the command name itself assigned parameter 0.
+.It
+If the command is not being run in the background,
+the shell waits for it to complete
+and collects its exit status.
+.El
+.Ss Quoting
+Some characters have special meaning to the shell and need
+.Em quoting
+if the user wants to indicate to the shell not to interpret them as such.
+The following characters need quoting if their literal meaning is desired:
+.Bd -literal -offset indent
+| & ; < > ( ) $ \` \e " \(aq <space> <tab> <newline>
+* ? [ # ~ = %
+.Ed
+.Pp
+A backslash
+.Pq \e
+can be used to quote any character except a newline.
+If a newline follows a backslash the shell removes them both,
+effectively making the following line part of the current one.
+.Pp
+A group of characters can be enclosed within single quotes
+.Pq \(aq
+to quote every character within the quotes.
+.Pp
+A group of characters can be enclosed within double quotes
+.Pq \&"
+to quote every character within the quotes
+except a backquote
+.Pq \`
+or a dollar sign
+.Pq $ ,
+both of which retain their special meaning.
+A backslash
+.Pq \e
+within double quotes retains its special meaning,
+but only when followed by a backquote, dollar sign,
+double quote, newline, or another backslash.
+An at sign
+.Pq @
+within double quotes has a special meaning
+(see
+.Sx SPECIAL PARAMETERS ,
+below).
+.Pp
+Similarly command words need to be quoted
+if they are not to be interpreted as such.
+.Ss Expansion
+Shell
+.Em variables
+are arbitrary names assigned values using the
+.Sq =
+operator;
+the values can be retrieved using the syntax
+.No $ Ns Ar variable .
+Shell
+.Em parameters
+are variable names,
+numbers,
+or any of the characters listed in
+.Sx SPECIAL PARAMETERS .
+.Pp
+The shell is able to
+.Em expand
+certain elements of its syntax,
+allowing for a more concise notation
+and providing a convenience to the user.
+.Pp
+Firstly, tilde expansion occurs on words beginning with the
+.Sq ~
+character.
+Any characters following the tilde,
+up to the next colon, slash, or blank,
+are taken as a login name
+and substituted with that user's home directory,
+as defined in
+.Xr passwd 5 .
+A tilde by itself is expanded to the contents of the variable
+.Ev HOME .
+This notation can be used in variable assignments,
+in the assignment half,
+immediately after the equals sign or a colon,
+up to the next slash or colon, if any.
+.Pp
+.Dl PATH=~alice:~bob/jobs
+.Pp
+Parameter expansion happens after tildes have been expanded,
+with the value of the parameter being substituted.
+The basic format is:
+.Pp
+.D1 $ Ns Brq Ar parameter
+.Pp
+The braces are optional
+except for positional parameters 10 and higher,
+or where the parameter name is followed by other characters
+that would prevent it from being expanded.
+If parameter expansion occurs within double quotes,
+neither pathname expansion nor field splitting happens afterwards.
+.Pp
+Some special forms of parameter expansion are available.
+In the formats below,
+.Ar word
+itself is subject to expansion,
+and, if omitted,
+the empty string is used.
+If the colon is omitted,
+.Ar word
+is substituted only if
+.Ar parameter
+is unset (not if it is empty).
+.Bl -tag -width Ds
+.It $ Ns Brq Ar parameter Ns :- Ns Op Ar word
+Substitute
+.Ar parameter .
+If
+.Ar parameter
+is unset or empty,
+substitute
+.Ar word .
+.It $ Ns Brq Ar parameter Ns := Ns Op Ar word
+Substitute
+.Ar parameter .
+If
+.Ar parameter
+is unset or empty,
+first assign the value of
+.Ar word
+to
+.Ar parameter .
+.It $ Ns Brq Ar parameter Ns :? Ns Op Ar word
+Substitute
+.Ar parameter .
+If
+.Ar parameter
+is unset or empty,
+the result of the expansion of
+.Ar word
+is written to standard error
+and the shell exits with a non-zero exit status.
+If
+.Ar word
+is omitted,
+the string
+.Qq parameter null or not set
+is used.
+.It $ Ns Brq Ar parameter Ns :+ Ns Op Ar word
+Substitute
+.Ar word .
+If
+.Ar parameter
+is unset or empty,
+substitute the empty string.
+.It $ Ns Brq # Ns Ar parameter
+The length, in characters, of
+.Ar parameter .
+.It $ Ns Brq Ar parameter Ns % Ns Op Ar word
+Substitute
+.Ar parameter ,
+deleting the smallest possible suffix matching
+.Ar word .
+.It $ Ns Brq Ar parameter Ns %% Ns Op Ar word
+Substitute
+.Ar parameter ,
+deleting the largest possible suffix matching
+.Ar word .
+.It $ Ns Brq Ar parameter Ns # Ns Op Ar word
+Substitute
+.Ar parameter ,
+deleting the smallest possible prefix matching
+.Ar word .
+.It $ Ns Brq Ar parameter Ns ## Ns Op Ar word
+Substitute
+.Ar parameter ,
+deleting the largest possible prefix matching
+.Ar word .
+.El
+.Pp
+Command expansion has a command executed in a subshell
+and the results output in its place.
+The basic format is:
+.Pp
+.D1 $ Ns Pq Ar command
+or
+.D1 \` Ns Ar command Ns \`
+.Pp
+The results are subject to field splitting and pathname expansion;
+no other form of expansion happens.
+If
+.Ar command
+is contained within double quotes,
+field splitting does not happen either.
+Within backquotes,
+a backslash is treated literally unless it follows
+a dollar sign, backquote, or another backslash.
+Commands can be nested,
+though the backquoted version requires backslashes before the backquotes.
+If
+.Ar command
+is run in a subshell in the bracketed version,
+the syntax is identical to that of arithmetic expansion.
+In that case the shell attempts arithmetic expansion first,
+then attempts command substitution if that fails.
+Or a non-ambiguous version can be used:
+.Pp
+.D1 "$( (" Ns Ar command Ns ") )"
+.Pp
+Arithmetic expansion works similarly,
+with an arithmetic expression being evaluated and substituted.
+The format is:
+.Pp
+.D1 $ Ns Pq Pq Ar expression
+.Pp
+Where
+.Ar expression
+is an integer, parameter name, or array reference,
+optionally combined with any of the operators described below,
+listed and grouped according to precedence:
+.Bl -tag -width Ds
+.It ()\&
+Operators within brackets have highest precedence.
+Compare 3+2*4, which is 11,
+since multiplication has higher precedence than addition,
+and (3+2)*4, which is 20.
+.It + - ~ !\&
+Unary plus
+(indicates a positive value; integers are positive by default),
+unary minus (indicates a negative value),
+bitwise NOT,
+and logical NOT
+(the result is 1 if the argument is zero, or 0 otherwise), respectively.
+.It * / %
+Multiplication, division, and modulus (remainder), respectively.
+.It + -
+Addition and subtraction, respectively.
+.It << >>
+Shift left or right, respectively.
+.It < <= > >=
+Less than, less than or equal to,
+greater than, and greater than or equal to, respectively.
+The result is 1 if true, or 0 otherwise.
+.It == !=
+Equal (the result is 1 if both arguments are equal, and 0 otherwise)
+and not equal (the result is 0 if both arguments are equal, and 1 otherwise),
+respectively.
+.It &
+Bitwise AND.
+.It ^
+Bitwise exclusive OR.
+.It |
+Bitwise inclusive OR.
+.It &&
+Logical AND.
+The result is 1 if both arguments are non-zero, or 0 otherwise.
+.It ||
+Logical OR.
+The result is 1 if either argument is non-zero, or 0 otherwise.
+.It Ar expression ? Ns Ar expr1 : Ns Ar expr2
+The result is
+.Ar expr1
+if
+.Ar expression
+is non-zero,
+or
+.Ar expr2
+otherwise.
+.It = *= /= %= += -= <<= >>= &= ^= |=
+Assignment.
+The notation
+.Ar var Ns *= Ns Ar expression
+is equivalent to
+.Ar var Ns = Ns Ar var Ns * Ns Ar expression .
+.El
+.Pp
+After the various types of expansion listed above have been carried out,
+the shell subjects everything that did not occur in double quotes to
+.Em field splitting ,
+where words are broken up according to the value of the
+.Ev IFS
+variable.
+Each character of
+.Ev IFS
+is used to split fields;
+any
+.Ev IFS
+characters at the beginning and end of input are ignored.
+If
+.Ev IFS
+is unset, the default value consisting of
+.Aq space ,
+.Aq tab
+and
+.Aq newline
+is used; if the value of
+.Ev IFS
+is empty, no field splitting is performed.
+.Pp
+After field splitting,
+the shell matches filename patterns.
+.Bl -tag -width Ds
+.It ?
+A question mark matches any single character.
+.It *
+An asterisk matches multiple characters.
+.It [..]
+Matches any character enclosed in the brackets.
+The sense is negated if the first character is
+.Sq !\& .
+A closing bracket can be included in the list of characters to match
+by listing it as the first character after the opening bracket
+or by quoting it.
+Similarly a
+.Sq -
+should be specified last or quoted so that the shell does not think
+it is a character range (see below).
+.It [[: Ns Ar class Ns :]]
+Matches any character in the following character classes:
+.Bd -literal -offset indent
+alnum	alpha	blank	cntrl
+digit	graph	lower	print
+punct	space	upper	xdigit
+.Ed
+.It Bq Ar x Ns - Ns Ar y
+Matches any character in the range between
+.Ar x
+and
+.Ar y ,
+inclusive.
+.El
+.Pp
+Slashes and full stops do not match the patterns above
+because of their use as path and filename characters.
+.Ss Redirection
+Redirection is used to open, close, or otherwise manipulate files,
+using redirection operators in combination with numerical
+.Em file descriptors .
+A minimum of ten (0\-9) descriptors are supported;
+by convention
+standard input is file descriptor 0,
+standard output file descriptor 1,
+and standard error file descriptor 2.
+In the examples given below,
+.Ar n
+represents a numerical file descriptor.
+The target for redirection is
+.Ar file
+and it is subject to all forms of expansion as listed above,
+except pathname expansion.
+If any part of the file descriptor or redirection operator is quoted,
+they are not recognised.
+.Bl -tag -width Ds
+.It Oo Ar n Oc Ns < Ns Ar file
+Open
+.Ar file
+for reading on file descriptor
+.Ar n ,
+by default standard input.
+.It Oo Ar n Oc Ns > Ns Ar file
+Write to
+.Ar file
+with file descriptor
+.Ar n ,
+by default standard output.
+If
+.Ar file
+does not exist,
+create it;
+if it does exist,
+truncate it to be empty before beginning to write to it.
+.It Oo Ar n Oc Ns >| Ns Ar file
+As above, but forces clobbering
+(see the
+.Fl C
+option).
+.It Oo Ar n Oc Ns >> Ns Ar file
+Append to
+.Ar file
+with file descriptor
+.Ar n ,
+by default standard output.
+If
+.Ar file
+does not exist,
+create it.
+.It Oo Ar n Oc Ns <<
+This form of redirection,
+called a
+.Em here document ,
+is used to copy a block of lines
+to a temporary file until a line matching
+.Ar delimiter
+is read.
+When the command is executed, standard input is redirected from the
+temporary file to file descriptor
+.Ar n ,
+or standard input by default.
+The basic format is:
+.Bd -unfilled -offset indent
+.Oo Ar n Oc Ns << Ns Ar delimiter
+text
+text
+\&...
+.Ar delimiter
+.Ed
+.Pp
+Provided
+.Ar delimiter
+doesn't contain any quoted characters,
+parameter, command, and arithmetic expansions are performed on
+the text block,
+and backslashes escape the special meaning of
+.Sq $ ,
+.Sq \` ,
+and
+.Sq \e .
+If multiple here documents are used on the same command line,
+they are saved and processed in order.
+.It Oo Ar n Oc Ns <<-
+Same as
+.Ic << ,
+except leading tabs are stripped from lines in
+.Ar block .
+.It Oo Ar n Oc Ns <& Ns Ar file
+Make file descriptor
+.Ar n ,
+by default standard input,
+a copy of the file descriptor denoted by
+.Ar file .
+If
+.Ar file
+is
+.Sq - ,
+close file descriptor
+.Ar n
+or standard input.
+.It Oo Ar n Oc Ns >& Ns Ar file
+Make file descriptor
+.Ar n ,
+by default standard output,
+a copy of the file descriptor denoted by
+.Ar file .
+If
+.Ar file
+is
+.Sq - ,
+close file descriptor
+.Ar n
+or standard output.
+.It Oo Ar n Oc Ns <> Ns Ar file
+Open
+.Ar file
+for reading and writing on file descriptor
+.Ar n ,
+by default standard input.
+If
+.Ar file
+does not exist,
+create it.
+.El
+.Sh COMMANDS
+The shell first expands
+any words that are not variable assignments or redirections,
+with the first field being the command name
+and any successive fields arguments to that command.
+It sets up redirections, if any,
+and then expands variable assignments, if any.
+It then attempts to run the command.
+.Pp
+Firstly, it determines whether the command name contains any slashes.
+If it does not, and the shell implements the command as a special built-in,
+it then invokes the built-in.
+If not, but it is a non POSIX standard command,
+implemented as a shell function,
+it then invokes that.
+If not, but it is one of the commands
+.Ic alias , bg , cd , command ,
+.Ic false , fc , fg , getopts ,
+.Ic jobs , kill , newgrp , pwd ,
+.Ic read , true , umask , unalias ,
+or
+.Ic wait ,
+it then invokes that.
+.Pp
+Failing that, the value of
+.Ev PATH
+is used to search for the command.
+If it finds a match,
+and it is a POSIX standard command,
+implemented as a built-in or function,
+it then invokes it.
+Otherwise
+it attempts to execute the command in an environment separate from the shell.
+If it is unable to execute the command,
+it tries to run it as a shell script.
+.Pp
+Finally, if the command name does contain a slash,
+and it finds a match in
+.Ev PATH ,
+it attempts to execute the command in an environment separate from the shell.
+If it is unable to execute the command,
+it tries to run it as a shell script.
+.Pp
+A series of one or more commands separated by
+.Sq ;\&
+constitute a
+.Em sequential list ,
+where commands are executed in the order given.
+The exit status of a sequential list is that of the last command executed.
+The format for a sequential list is:
+.Pp
+.D1 Ar command No \&; Op Ar command ...
+.Pp
+A series of one or more commands separated by
+.Sq &
+constitute an
+.Em asynchronous list ,
+where the shell executes the command in a subshell
+and runs the next command without waiting for the previous one to finish.
+The exit status of an asynchronous list is always zero.
+The format for an asynchronous list is:
+.Pp
+.D1 Ar command No & Op Ar command ...
+.Pp
+A series of commands separated by
+.Sq |
+constitute a
+.Em pipeline ,
+where the output of one command
+is used as input for the next command.
+The exit status of a pipeline is that of the last command;
+if a pipeline begins
+.Sq !\&
+the exit status is inverted.
+The format for a pipeline is:
+.Pp
+.D1 Oo !\& Oc Ar command | command Op | Ar ...
+.Pp
+A series of commands separated by
+.Sq &&
+constitute an
+.Em AND list ,
+where a command is only executed if the exit status of the previous command was
+zero.
+The exit status of an AND list is that of the last command.
+The format for an AND list is:
+.Pp
+.D1 Ar command No && Ar command Op && Ar ...
+.Pp
+A series of commands separated by
+.Sq ||
+constitute an
+.Em OR list ,
+where a command is only executed if the exit status of the previous command was
+non-zero.
+The exit status of an OR list is that of the last command.
+The format for an OR list is:
+.Pp
+.D1 Ar command No || Ar command Op || Ar ...
+.Pp
+A series of commands separated by
+.Sq &&
+and
+.Sq ||
+constitute an
+.Em AND-OR list ,
+where
+.Sq &&
+and
+.Sq ||
+have equal precedence and are evaluated in the order they are given.
+The AND-OR list can be terminated with
+.Sq ;\&
+or
+.Sq &
+to have them execute sequentially or asynchronously, respectively.
+.Pp
+Command lists,
+as described above,
+can be enclosed within
+.Sq ()
+to have them executed in a subshell,
+or within
+.Sq {}
+to have them executed in the current environment:
+.Pp
+.D1 Pq Ar command ...
+.D1 Brq \& Ar command ... ; No \&
+.Pp
+Any redirections specified after the closing bracket apply to all commands
+within the brackets.
+An operator such as
+.Sq ;\&
+or a newline are needed to terminate a command list within curly braces.
+.Pp
+The shell has grammatical constructs
+which allow it to work its way (loop) through lists
+or evaluate things conditionally.
+.Pp
+A
+.Em for loop
+executes a series of commands for each item in a list.
+Its format is:
+.Bd -unfilled -offset indent
+.No for Ar name Op in Op Ar word ...
+do
+.No "   " Ar command
+.No "   " Ar ...
+done
+.Ed
+.Pp
+Firstly
+.Ar word ...
+is expanded to generate a list of items.
+The variable
+.Ar name
+is set to each item, in turn,
+and the commands are executed for each item.
+The construct
+.Qq in word ...
+can be omitted,
+which is equivalent to: in \&"$@\&".
+The exit status is that of the last command executed.
+If there are no items,
+.Ar command
+is not executed and the exit status is zero.
+.Pp
+A
+.Em while loop
+continuously executes a set of commands
+as long as the command or command list being tested in
+.Ar condition
+has a zero exit status.
+Its format is:
+.Bd -unfilled -offset indent
+.No while Ar condition
+do
+.No "   " Ar command
+.No "   " Ar ...
+done
+.Ed
+.Pp
+Multiple commands may be given by grouping them in lists,
+as described above,
+or by separating them with newlines.
+The exit status is zero if the commands after
+.Qq do
+were never executed
+or otherwise the exit status of the last command executed.
+.Pp
+An
+.Em until loop
+continuously executes a set of commands
+as long as the command or command list being tested in
+.Ar condition
+has a non-zero exit status.
+Its format is:
+.Bd -unfilled -offset indent
+.No until Ar condition
+do
+.No "   " Ar command
+.No "   " Ar ...
+done
+.Ed
+.Pp
+Multiple commands may be given by grouping them in lists,
+as described above,
+or by separating them with newlines.
+The exit status is zero if the commands after
+.Qq do
+were never executed
+or otherwise the exit status is that of the last command executed.
+.Pp
+A
+.Em case conditional
+is used to run commands whenever a pattern is matched.
+Its format is:
+.Bd -unfilled -offset indent
+.No case Ar word No in
+.No "   " Po Ar pattern Oo | Ar pattern ... Oc Pc Ar command Ns ;;
+.No "   " Ar ...
+esac
+.Ed
+.Pp
+In this case
+.Ar pattern
+is matched against the string resulting from the expansion of
+.Ar word .
+Multiple commands may be given by grouping them in lists,
+as described above,
+or by separating them with newlines.
+The initial
+.Sq (\&
+is optional,
+as is the terminating
+.Sq ;;
+for the final command.
+The exit status is zero if no patterns are matched
+or otherwise the exit status of the last command executed.
+.Pp
+An
+.Em if conditional
+is used to execute commands depending on the exit status of the command or
+command list being tested.
+Its format is:
+.Bd -unfilled -offset indent
+.No if Ar conditional
+then
+.No "   " Ar command
+.No "   " Ar ...
+.No elif Ar conditional
+then
+.No "   " Ar command
+.No "   " Ar ...
+.No else
+.No "   " Ar command
+.No "   " Ar ...
+fi
+.Ed
+.Pp
+Firstly the command(s) following
+.Qq if
+is executed;
+if its exit status is zero,
+the commands in the
+.Qq then
+block are executed and the conditional completes.
+Otherwise the commands in the
+.Qq elif
+block are executed;
+if the exit status is zero,
+the commands in the
+.Qq then
+block are executed and the conditional completes.
+Otherwise the next
+.Qq elif
+block, if any, is tried.
+If nothing from an
+.Qq if
+or
+.Qq elif
+block returns zero,
+the commands in the
+.Qq else
+block are run and the conditional completes.
+The
+.Qq elif
+and
+.Qq else
+blocks are optional.
+.Pp
+Multiple commands may be given by grouping them in lists,
+as described above,
+or by separating them with newlines.
+The exit status is zero if nothing is executed from an
+.Qq if
+or
+.Qq elif
+block
+or otherwise the exit status of the last command executed.
+.Pp
+Functions allow the user to define a group of commands,
+executed whenever the function is invoked.
+Its format is:
+.Bd -unfilled -offset indent
+.Ar function Ns () Ar command-list
+.Ed
+.Pp
+The above simply defines a function;
+nothing is executed until the function is invoked.
+Commands may specify redirections
+and positional parameters are changed,
+for the duration of the function,
+to those passed to it.
+The special parameter
+.Sq #
+is temporarily changed too,
+though
+.Sq 0
+is not.
+After the function finishes,
+the positional parameters and
+.Sq #
+are restored to their original values.
+The exit status of a function definition is 0 if successful
+or >0 otherwise.
+The exit status of a function is that of the last command
+executed by the function.
+.Sh SPECIAL PARAMETERS
+Some parameters have special meaning to the shell
+and are listed below.
+.Bl -tag -width Ds
+.It 0
+The name of the shell or shell script.
+.It 1 ... n
+The
+.Em positional parameters .
+These parameters are set when a shell, shell script,
+or shell function is invoked.
+Each argument passed to a shell or shell script
+is assigned a positional parameter,
+starting at 1,
+and assigned sequentially.
+When a shell function is invoked,
+any arguments passed to it are temporarily reassigned to the
+positional parameters;
+when the function completes,
+the values are restored.
+Positional parameters 10 and above should be enclosed in {}.
+Positional parameters can be reassigned using the
+.Ic set
+command.
+.It @
+All positional parameters.
+Within double quotes,
+each parameter is output as a separate field.
+The resulting list completely matches what was passed to the shell.
+So "1 2" "3" is output as two parameters, "1 2" and "3".
+.It *
+All positional parameters.
+Within double quotes,
+all parameters are output as one field,
+separated by the first character of
+.Ev IFS
+(by default a space).
+The resulting list of words is amalgamated,
+losing the sense of how they were passed to the shell.
+So "1 2" "3" is output as one parameter, "1 2 3".
+.It #
+The number of positional parameters.
+.It ?
+The exit status of the most recent command.
+.It -
+The current shell options.
+.It $
+The process ID of the current shell.
+Subshells have the same PID as the current shell.
+.It !
+The process ID of the most recent background command.
+.El
+.Sh ENVIRONMENT
+The following environment variables affect the execution of
+.Nm :
+.Bl -tag -width "POSIXLY_CORRECT"
+.It Ev CDPATH
+Colon separated list of directories used by the
+.Ic cd
+command.
+If unset or empty,
+the current working directory is used.
+.It Ev ENV
+Pathname to a file containing commands to be executed
+when an interactive shell is started.
+.It Ev FCEDIT
+Editor for the
+.Ic fc
+builtin.
+The default is
+.Xr ed 1 .
+.It Ev HISTFILE
+Pathname to a file to be used to record command history.
+The default is to not write command history to a file.
+.It Ev HISTSIZE
+The maximum number of commands stored in history.
+The default is 500.
+.It Ev HOME
+Pathname to a user's home directory.
+.It Ev IFS
+A list of characters to be used for field splitting.
+.It Ev LINENO
+The current line number in a script or function,
+starting at 1.
+This variable should not be set by users.
+.It Ev MAIL
+Pathname to a user's mailbox file.
+If set,
+.Nm
+reports the arrival of new mail
+(ascertained by checking a file's modification time)
+every
+.Ev MAILCHECK
+seconds.
+.Ev MAIL
+is overridden by
+.Ev MAILPATH .
+.It Ev MAILCHECK
+How often,
+in seconds,
+to check for new mail in either
+.Ev MAIL
+or
+.Ev MAILPATH .
+The default is 600 (10 minutes).
+If set to 0,
+check before issuing each prompt.
+.It Ev MAILPATH
+Pathname to a colon separated list of mailboxes.
+If set,
+.Nm
+reports the arrival of new mail
+(ascertained by checking a file's modification time)
+every
+.Ev MAILCHECK
+seconds.
+The default notification message
+.Pq Qq you have mail in $_
+can be changed per mailbox by appending
+.No % Ns Ar message
+to a pathname.
+.Ev MAILPATH
+overrides
+.Ev MAIL .
+.It Ev OLDPWD
+Pathname to the previous working directory.
+.It Ev OPTARG
+An option argument for the
+.Ic getopts
+command.
+.It Ev OPTIND
+An index to the next option for the
+.Ic getopts
+command.
+.It Ev PATH
+Pathname to a colon separated list of directories
+used to search for the location of executable files.
+A pathname of
+.Sq .\&
+represents the current working directory.
+The default value of
+.Ev PATH
+on
+.Ox
+is:
+.Bd -literal -offset 2n
+/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11R6/bin:/usr/local/bin
+.Ed
+.It Ev POSIXLY_CORRECT
+Enable POSIX mode
+(see
+.Sx STANDARDS ) .
+.It Ev PPID
+The shell's parent process ID.
+Subshells have the same
+.Ev PPID
+as the current shell.
+.It Ev PS1
+User prompt displayed every time an interactive shell
+is ready to read a command.
+A
+.Sq !\&
+in the prompt is expanded to the number of the next command in history
+to be typed.
+.It Ev PS2
+Newline prompt displayed in an interactive shell
+when a newline has been entered
+before the command line completes.
+The default value is
+.Sq >\ \& .
+.It Ev PS4
+Trace prompt displayed in an interactive shell
+before each command is traced
+(see the
+.Fl x
+option).
+The default is
+.Sq +\ \& .
+.It PWD
+The absolute pathname to the current working directory.
+.El
+.Sh ASYNCHRONOUS EVENTS
+The following signals affect the execution of
+.Nm :
+.Bl -tag -width "SIGQUITXXX"
+.It Dv SIGINT
+If a shell is interactive
+and in command line editing mode,
+editing is terminated on the current line
+and the command being edited is not entered into command history.
+Otherwise the signal is caught
+but no action is taken.
+.It Dv SIGQUIT
+Ignored if a shell is interactive.
+.It Dv SIGTERM
+Ignored if a shell is interactive.
+.It Dv SIGTSTP
+Ignored if a shell is interactive
+and the
+.Ic monitor
+option
+.Pq Fl m
+is set.
+.It Dv SIGTTIN
+Ignored if a shell is interactive
+and the
+.Ic monitor
+option
+.Pq Fl m
+is set.
+.It Dv SIGTTOU
+Ignored if a shell is interactive
+and the
+.Ic monitor
+option
+.Pq Fl m
+is set.
+.El
+.Sh EXIT STATUS
+The
+.Nm
+utility exits with one of:
+.Bl -tag -width "1-125"
+.It 0
+The script being executed contained only blank lines or comments.
+.It 1\(en125
+A non-interactive shell detected an error other than
+.Ar file
+not found.
+.It 126
+A command was found but was not executable.
+.It 127
+A non-interactive shell returned
+.Ar file
+not found.
+.El
+.Pp
+Otherwise
+.Nm
+returns the exit status of the last command it invoked.
+.Sh SEE ALSO
+.Xr csh 1 ,
+.Xr ed 1 ,
+.Xr ksh 1 ,
+.Xr vi 1 ,
+.Xr script 7
+.Sh STANDARDS
+The
+.Nm
+utility is compliant with the
+.St -p1003.1-2008
+specification,
+except where noted below:
+.Bl -dash
+.It
+The flag
+.Op Fl h
+is documented by POSIX as hashing
+.Qq utilities invoked by functions as those functions are defined ;
+this implementation hashes utilities after first invocation
+(and functions be damned).
+.It
+POSIX says mail notifications via
+.Ev MAIL
+and
+.Ev MAILPATH
+should happen if a file is created,
+as well as if its modification time changes.
+This implementation of
+.Nm
+does not provide notification when these files are created.
+.It
+The built-in
+.Ic newgrp
+is unsupported.
+.It
+The
+.Ic break
+and
+.Ic continue
+built-ins should exit/return from the outermost loop if the argument
+.Ar n
+is greater than the level of loops.
+.It
+The default value for the
+.Ev PS1
+user prompt contains the machine's hostname,
+followed by
+.Sq $\ \&
+for normal users and
+.Sq #\ \&
+for root;
+POSIX does not include the hostname.
+.El
+.Pp
+Enabling POSIX mode changes some behaviour to make
+.Nm
+adhere more strictly to the
+.St -p1003.1-2008
+specification.
diff --git a/sh.h b/sh.h
@@ -0,0 +1,606 @@
+/*	$OpenBSD: sh.h,v 1.75 2019/02/20 23:59:17 schwarze Exp $	*/
+
+/*
+ * Public Domain Bourne/Korn shell
+ */
+
+/* $From: sh.h,v 1.2 1994/05/19 18:32:40 michael Exp michael $ */
+
+#include "config.h"	/* system and option configuration info */
+
+/* Start of common headers */
+
+#include <limits.h>
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <signal.h>
+#include <time.h>
+#include <stdbool.h>
+#include <fcntl.h>
+
+/* end of common headers */
+
+#define	NELEM(a) (sizeof(a) / sizeof((a)[0]))
+#define	BIT(i)	(1<<(i))	/* define bit in flag */
+
+#define	NUFILE	32		/* Number of user-accessible files */
+#define	FDBASE	10		/* First file usable by Shell */
+
+#define BITS(t)	(CHAR_BIT * sizeof(t))
+
+/* Make MAGIC a char that might be printed to make bugs more obvious, but
+ * not a char that is used often.  Also, can't use the high bit as it causes
+ * portability problems (calling strchr(x, 0x80|'x') is error prone).
+ */
+#define	MAGIC		(7)	/* prefix for *?[!{,} during expand */
+#define ISMAGIC(c)	((unsigned char)(c) == MAGIC)
+
+#define	LINE	4096		/* input line size */
+
+extern	const char *kshname;	/* $0 */
+extern	pid_t	kshpid;		/* $$, shell pid */
+extern	pid_t	procpid;	/* pid of executing process */
+extern	uid_t	ksheuid;	/* effective uid of shell */
+extern	int	exstat;		/* exit status */
+extern	int	subst_exstat;	/* exit status of last $(..)/`..` */
+extern	const char *safe_prompt; /* safe prompt if PS1 substitution fails */
+extern	char	username[];	/* username for \u prompt expansion */
+extern	int	disable_subst;	/* disable substitution during evaluation */
+
+/*
+ * Area-based allocation built on malloc/free
+ */
+typedef struct Area {
+	struct link *freelist;	/* free list */
+} Area;
+
+extern	Area	aperm;		/* permanent object space */
+#define	APERM	&aperm
+#define	ATEMP	&genv->area
+
+#ifdef KSH_DEBUG
+# define kshdebug_init()	kshdebug_init_()
+# define kshdebug_printf(a)	kshdebug_printf_ a
+# define kshdebug_dump(a)	kshdebug_dump_ a
+#else /* KSH_DEBUG */
+# define kshdebug_init()
+# define kshdebug_printf(a)
+# define kshdebug_dump(a)
+#endif /* KSH_DEBUG */
+
+/*
+ * parsing & execution environment
+ */
+struct env {
+	short	type;			/* environment type - see below */
+	short	flags;			/* EF_* */
+	Area	area;			/* temporary allocation area */
+	struct	block *loc;		/* local variables and functions */
+	short  *savefd;			/* original redirected fd's */
+	struct	env *oenv;		/* link to previous environment */
+	sigjmp_buf jbuf;		/* long jump back to env creator */
+	struct temp *temps;		/* temp files */
+};
+extern	struct env	*genv;
+
+/* struct env.type values */
+#define	E_NONE	0		/* dummy environment */
+#define	E_PARSE	1		/* parsing command # */
+#define	E_FUNC	2		/* executing function # */
+#define	E_INCL	3		/* including a file via . # */
+#define	E_EXEC	4		/* executing command tree */
+#define	E_LOOP	5		/* executing for/while # */
+#define	E_ERRH	6		/* general error handler # */
+/* # indicates env has valid jbuf (see unwind()) */
+
+/* struct env.flag values */
+#define EF_FUNC_PARSE	BIT(0)	/* function being parsed */
+#define EF_BRKCONT_PASS	BIT(1)	/* set if E_LOOP must pass break/continue on */
+#define EF_FAKE_SIGDIE	BIT(2)	/* hack to get info from unwind to quitenv */
+
+/* Do breaks/continues stop at env type e? */
+#define STOP_BRKCONT(t)	((t) == E_NONE || (t) == E_PARSE \
+			 || (t) == E_FUNC || (t) == E_INCL)
+/* Do returns stop at env type e? */
+#define STOP_RETURN(t)	((t) == E_FUNC || (t) == E_INCL)
+
+/* values for siglongjmp(e->jbuf, 0) */
+#define LRETURN	1		/* return statement */
+#define	LEXIT	2		/* exit statement */
+#define LERROR	3		/* errorf() called */
+#define LLEAVE	4		/* untrappable exit/error */
+#define LINTR	5		/* ^C noticed */
+#define	LBREAK	6		/* break statement */
+#define	LCONTIN	7		/* continue statement */
+#define LSHELL	8		/* return to interactive shell() */
+#define LAEXPR	9		/* error in arithmetic expression */
+
+/* option processing */
+#define OF_CMDLINE	0x01	/* command line */
+#define OF_SET		0x02	/* set builtin */
+#define OF_SPECIAL	0x04	/* a special variable changing */
+#define OF_INTERNAL	0x08	/* set internally by shell */
+#define OF_ANY		(OF_CMDLINE | OF_SET | OF_SPECIAL | OF_INTERNAL)
+
+struct option {
+    const char	*name;	/* long name of option */
+    char	c;	/* character flag (if any) */
+    short	flags;	/* OF_* */
+};
+extern const struct option sh_options[];
+
+/*
+ * flags (the order of these enums MUST match the order in misc.c(options[]))
+ */
+enum sh_flag {
+	FEXPORT = 0,	/* -a: export all */
+	FBRACEEXPAND,	/* enable {} globbing */
+	FBGNICE,	/* bgnice */
+	FCOMMAND,	/* -c: (invocation) execute specified command */
+	FCSHHISTORY,	/* csh-style history enabled */
+#ifdef EMACS
+	FEMACS,		/* emacs command editing */
+#endif
+	FERREXIT,	/* -e: quit on error */
+#ifdef EMACS
+	FGMACS,		/* gmacs command editing */
+#endif
+	FIGNOREEOF,	/* eof does not exit */
+	FTALKING,	/* -i: interactive */
+	FKEYWORD,	/* -k: name=value anywhere */
+	FLOGIN,		/* -l: a login shell */
+	FMARKDIRS,	/* mark dirs with / in file name completion */
+	FMONITOR,	/* -m: job control monitoring */
+	FNOCLOBBER,	/* -C: don't overwrite existing files */
+	FNOEXEC,	/* -n: don't execute any commands */
+	FNOGLOB,	/* -f: don't do file globbing */
+	FNOHUP,		/* -H: don't kill running jobs when login shell exits */
+	FNOLOG,		/* don't save functions in history (ignored) */
+	FNOTIFY,	/* -b: asynchronous job completion notification */
+	FNOUNSET,	/* -u: using an unset var is an error */
+	FPHYSICAL,	/* -o physical: don't do logical cd's/pwd's */
+	FPOSIX,		/* -o posix: be posixly correct */
+	FPRIVILEGED,	/* -p: use suid_profile */
+	FRESTRICTED,	/* -r: restricted shell */
+	FSH,		/* -o sh: favor sh behaviour */
+	FSTDIN,		/* -s: (invocation) parse stdin */
+	FTRACKALL,	/* -h: create tracked aliases for all commands */
+	FVERBOSE,	/* -v: echo input */
+#ifdef VI
+	FVI,		/* vi command editing */
+	FVIRAW,		/* always read in raw mode (ignored) */
+	FVISHOW8,	/* display chars with 8th bit set as is (versus M-) */
+	FVITABCOMPLETE,	/* enable tab as file name completion char */
+	FVIESCCOMPLETE,	/* enable ESC as file name completion in command mode */
+#endif
+	FXTRACE,	/* -x: execution trace */
+	FTALKING_I,	/* (internal): initial shell was interactive */
+	FNFLAGS /* (place holder: how many flags are there) */
+};
+
+#define Flag(f)	(shell_flags[(int) (f)])
+
+extern	char shell_flags[FNFLAGS];
+
+extern	char	null[];	/* null value for variable */
+
+enum temp_type {
+	TT_HEREDOC_EXP,	/* expanded heredoc */
+	TT_HIST_EDIT	/* temp file used for history editing (fc -e) */
+};
+typedef enum temp_type Temp_type;
+/* temp/heredoc files.  The file is removed when the struct is freed. */
+struct temp {
+	struct temp	*next;
+	struct shf	*shf;
+	int		pid;		/* pid of process parsed here-doc */
+	Temp_type	type;
+	char		*name;
+};
+
+/*
+ * stdio and our IO routines
+ */
+
+#define shl_spare	(&shf_iob[0])	/* for c_read()/c_print() */
+#define shl_stdout	(&shf_iob[1])
+#define shl_out		(&shf_iob[2])
+extern int shl_stdout_ok;
+
+/*
+ * trap handlers
+ */
+typedef struct trap {
+	int	signal;		/* signal number */
+	const char *name;	/* short name */
+	const char *mess;	/* descriptive name */
+	char   *trap;		/* trap command */
+	volatile sig_atomic_t set; /* trap pending */
+	int	flags;		/* TF_* */
+	sig_t cursig;		/* current handler (valid if TF_ORIG_* set) */
+	sig_t shtrap;		/* shell signal handler */
+} Trap;
+
+/* values for Trap.flags */
+#define TF_SHELL_USES	BIT(0)	/* shell uses signal, user can't change */
+#define TF_USER_SET	BIT(1)	/* user has (tried to) set trap */
+#define TF_ORIG_IGN	BIT(2)	/* original action was SIG_IGN */
+#define TF_ORIG_DFL	BIT(3)	/* original action was SIG_DFL */
+#define TF_EXEC_IGN	BIT(4)	/* restore SIG_IGN just before exec */
+#define TF_EXEC_DFL	BIT(5)	/* restore SIG_DFL just before exec */
+#define TF_DFL_INTR	BIT(6)	/* when received, default action is LINTR */
+#define TF_TTY_INTR	BIT(7)	/* tty generated signal (see j_waitj) */
+#define TF_CHANGED	BIT(8)	/* used by runtrap() to detect trap changes */
+#define TF_FATAL	BIT(9)	/* causes termination if not trapped */
+
+/* values for setsig()/setexecsig() flags argument */
+#define SS_RESTORE_MASK	0x3	/* how to restore a signal before an exec() */
+#define SS_RESTORE_CURR	0	/* leave current handler in place */
+#define SS_RESTORE_ORIG	1	/* restore original handler */
+#define SS_RESTORE_DFL	2	/* restore to SIG_DFL */
+#define SS_RESTORE_IGN	3	/* restore to SIG_IGN */
+#define SS_FORCE	BIT(3)	/* set signal even if original signal ignored */
+#define SS_USER		BIT(4)	/* user is doing the set (ie, trap command) */
+#define SS_SHTRAP	BIT(5)	/* trap for internal use (CHLD,ALRM,WINCH) */
+
+#define SIGEXIT_	0	/* for trap EXIT */
+#define SIGERR_		NSIG	/* for trap ERR */
+
+extern	volatile sig_atomic_t trap;	/* traps pending? */
+extern	volatile sig_atomic_t intrsig;	/* pending trap interrupts command */
+extern	volatile sig_atomic_t fatal_trap;	/* received a fatal signal */
+extern	volatile sig_atomic_t got_sigwinch;
+extern	Trap	sigtraps[NSIG+1];
+
+/*
+ * TMOUT support
+ */
+/* values for ksh_tmout_state */
+enum tmout_enum {
+	TMOUT_EXECUTING	= 0,	/* executing commands */
+	TMOUT_READING,		/* waiting for input */
+	TMOUT_LEAVING		/* have timed out */
+};
+extern unsigned int ksh_tmout;
+extern enum tmout_enum ksh_tmout_state;
+
+/* For "You have stopped jobs" message */
+extern int really_exit;
+
+/*
+ * fast character classes
+ */
+#define	C_ALPHA	 BIT(0)		/* a-z_A-Z */
+/* was	C_DIGIT */
+#define	C_LEX1	 BIT(2)		/* \0 \t\n|&;<>() */
+#define	C_VAR1	 BIT(3)		/* *@#!$-? */
+#define	C_IFSWS	 BIT(4)		/* \t \n (IFS white space) */
+#define	C_SUBOP1 BIT(5)		/* "=-+?" */
+#define	C_SUBOP2 BIT(6)		/* "#%" */
+#define	C_IFS	 BIT(7)		/* $IFS */
+#define	C_QUOTE	 BIT(8)		/*  \n\t"#$&'()*;<>?[\`| (needing quoting) */
+
+extern	short ctypes [];
+
+#define	ctype(c, t)	!!(ctypes[(unsigned char)(c)]&(t))
+#define	letter(c)	ctype(c, C_ALPHA)
+#define	digit(c)	isdigit((unsigned char)(c))
+#define	letnum(c)	(ctype(c, C_ALPHA) || isdigit((unsigned char)(c)))
+
+extern int ifs0;	/* for "$*" */
+
+/* Argument parsing for built-in commands and getopts command */
+
+/* Values for Getopt.flags */
+#define GF_ERROR	BIT(0)	/* call errorf() if there is an error */
+#define GF_PLUSOPT	BIT(1)	/* allow +c as an option */
+#define GF_NONAME	BIT(2)	/* don't print argv[0] in errors */
+
+/* Values for Getopt.info */
+#define GI_MINUS	BIT(0)	/* an option started with -... */
+#define GI_PLUS		BIT(1)	/* an option started with +... */
+#define GI_MINUSMINUS	BIT(2)	/* arguments were ended with -- */
+
+typedef struct {
+	int		optind;
+	int		uoptind;/* what user sees in $OPTIND */
+	char		*optarg;
+	int		flags;	/* see GF_* */
+	int		info;	/* see GI_* */
+	unsigned int	p;	/* 0 or index into argv[optind - 1] */
+	char		buf[2];	/* for bad option OPTARG value */
+} Getopt;
+
+extern Getopt builtin_opt;	/* for shell builtin commands */
+extern Getopt user_opt;		/* parsing state for getopts builtin command */
+
+/* This for co-processes */
+
+typedef int Coproc_id; /* something that won't (realistically) wrap */
+struct coproc {
+	int	read;		/* pipe from co-process's stdout */
+	int	readw;		/* other side of read (saved temporarily) */
+	int	write;		/* pipe to co-process's stdin */
+	Coproc_id id;		/* id of current output pipe */
+	int	njobs;		/* number of live jobs using output pipe */
+	void	*job;		/* 0 or job of co-process using input pipe */
+};
+extern struct coproc coproc;
+
+/* Used in jobs.c and by coprocess stuff in exec.c */
+extern sigset_t		sm_default, sm_sigchld;
+
+extern const char ksh_version[];
+
+/* name of called builtin function (used by error functions) */
+extern char	*builtin_argv0;
+extern int	builtin_flag;	/* flags of called builtin (SPEC_BI, etc.) */
+
+/* current working directory, and size of memory allocated for same */
+extern char	*current_wd;
+extern int	current_wd_size;
+
+/* Minimum required space to work with on a line - if the prompt leaves less
+ * space than this on a line, the prompt is truncated.
+ */
+#define MIN_EDIT_SPACE	7
+/* Minimum allowed value for x_cols: 2 for prompt, 3 for " < " at end of line
+ */
+#define MIN_COLS	(2 + MIN_EDIT_SPACE + 3)
+extern	int	x_cols;	/* tty columns */
+
+/* These to avoid bracket matching problems */
+#define OPAREN	'('
+#define CPAREN	')'
+#define OBRACK	'['
+#define CBRACK	']'
+#define OBRACE	'{'
+#define CBRACE	'}'
+
+/* Determine the location of the system (common) profile */
+#define KSH_SYSTEM_PROFILE "/etc/profile"
+
+/* Used by v_evaluate() and setstr() to control action when error occurs */
+#define KSH_UNWIND_ERROR	0x0	/* unwind the stack (longjmp) */
+#define KSH_RETURN_ERROR	0x1	/* return 1/0 for success/failure */
+#define KSH_IGNORE_RDONLY	0x4	/* ignore the read-only flag */
+
+#include "shf.h"
+#include "table.h"
+#include "tree.h"
+#include "expand.h"
+#include "lex.h"
+
+/* alloc.c */
+Area *	ainit(Area *);
+void	afreeall(Area *);
+void *	alloc(size_t, Area *);
+void *	areallocarray(void *, size_t, size_t, Area *);
+void *	aresize(void *, size_t, Area *);
+void	afree(void *, Area *);
+/* c_ksh.c */
+int	c_cd(char **);
+int	c_pwd(char **);
+int	c_print(char **);
+int	c_whence(char **);
+int	c_command(char **);
+int	c_type(char **);
+int	c_typeset(char **);
+int	c_alias(char **);
+int	c_unalias(char **);
+int	c_let(char **);
+int	c_jobs(char **);
+int	c_fgbg(char **);
+int	c_kill(char **);
+void	getopts_reset(int);
+int	c_getopts(char **);
+int	c_bind(char **);
+/* c_sh.c */
+int	c_label(char **);
+int	c_shift(char **);
+int	c_umask(char **);
+int	c_dot(char **);
+int	c_wait(char **);
+int	c_read(char **);
+int	c_eval(char **);
+int	c_trap(char **);
+int	c_brkcont(char **);
+int	c_exitreturn(char **);
+int	c_set(char **);
+int	c_unset(char **);
+int	c_ulimit(char **);
+int	c_times(char **);
+int	timex(struct op *, int, volatile int *);
+void	timex_hook(struct op *, char ** volatile *);
+int	c_exec(char **);
+int	c_builtin(char **);
+/* c_test.c */
+int	c_test(char **);
+/* edit.c: most prototypes in edit.h */
+void	x_init(void);
+int	x_read(char *, size_t);
+void	set_editmode(const char *);
+/* emacs.c: most prototypes in edit.h */
+int	x_bind(const char *, const char *, int, int);
+/* eval.c */
+char *	substitute(const char *, int);
+char **	eval(char **, int);
+char *	evalstr(char *cp, int);
+char *	evalonestr(char *cp, int);
+char	*debunk(char *, const char *, size_t);
+void	expand(char *, XPtrV *, int);
+int	glob_str(char *, XPtrV *, int);
+/* exec.c */
+int	execute(struct op * volatile, volatile int, volatile int *);
+int	shcomexec(char **);
+struct tbl * findfunc(const char *, unsigned int, int);
+int	define(const char *, struct op *);
+void	builtin(const char *, int (*)(char **));
+struct tbl *	findcom(const char *, int);
+void	flushcom(int);
+char *	search(const char *, const char *, int, int *);
+int	search_access(const char *, int, int *);
+int	pr_menu(char *const *);
+int	pr_list(char *const *);
+/* expr.c */
+int	evaluate(const char *, int64_t *, int, bool);
+int	v_evaluate(struct tbl *, const char *, volatile int, bool);
+/* history.c */
+void	init_histvec(void);
+void	hist_init(Source *);
+void	hist_finish(void);
+void	histsave(int, const char *, int);
+int	c_fc(char **);
+void	c_fc_reset(void);
+void	sethistcontrol(const char *);
+void	sethistsize(int);
+void	sethistfile(const char *);
+char **	histpos(void);
+int	histnum(int);
+int	findhist(int, int, const char *, int);
+int	findhistrel(const char *);
+char  **hist_get_newest(int);
+
+/* io.c */
+void	errorf(const char *, ...)
+	    __attribute__((__noreturn__, __format__ (printf, 1, 2)));
+void	warningf(bool, const char *, ...)
+	    __attribute__((__format__ (printf, 2, 3)));
+void	bi_errorf(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	internal_errorf(const char *, ...)
+	    __attribute__((__noreturn__, __format__ (printf, 1, 2)));
+void	internal_warningf(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	error_prefix(int);
+void	shellf(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	shprintf(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+#ifdef KSH_DEBUG
+void	kshdebug_init_(void);
+void	kshdebug_printf_(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	kshdebug_dump_(const char *, const void *, int);
+#endif /* KSH_DEBUG */
+int	can_seek(int);
+void	initio(void);
+int	ksh_dup2(int, int, int);
+int	savefd(int);
+void	restfd(int, int);
+void	openpipe(int *);
+void	closepipe(int *);
+int	check_fd(char *, int, const char **);
+void	coproc_init(void);
+void	coproc_read_close(int);
+void	coproc_readw_close(int);
+void	coproc_write_close(int);
+int	coproc_getfd(int, const char **);
+void	coproc_cleanup(int);
+struct temp *maketemp(Area *, Temp_type, struct temp **);
+/* jobs.c */
+void	j_init(int);
+void	j_exit(void);
+void	j_change(void);
+int	exchild(struct op *, int, volatile int *, int);
+void	startlast(void);
+int	waitlast(void);
+int	waitfor(const char *, int *);
+int	j_kill(const char *, int);
+int	j_resume(const char *, int);
+int	j_jobs(const char *, int, int);
+int	j_njobs(void);
+void	j_notify(void);
+pid_t	j_async(void);
+int	j_stopped_running(void);
+/* mail.c */
+void	mcheck(void);
+void	mcset(int64_t);
+void	mbset(char *);
+void	mpset(char *);
+/* main.c */
+int	include(const char *, int, char **, int);
+int	command(const char *, int);
+int	shell(Source *volatile, int volatile);
+void	unwind(int) __attribute__((__noreturn__));
+void	newenv(int);
+void	quitenv(struct shf *);
+void	cleanup_parents_env(void);
+void	cleanup_proc_env(void);
+/* misc.c */
+void	setctypes(const char *, int);
+void	initctypes(void);
+char *	u64ton(uint64_t, int);
+char *	str_save(const char *, Area *);
+char *	str_nsave(const char *, int, Area *);
+int	option(const char *);
+char *	getoptions(void);
+void	change_flag(enum sh_flag, int, int);
+int	parse_args(char **, int, int *);
+int	getn(const char *, int *);
+int	bi_getn(const char *, int *);
+int	gmatch(const char *, const char *, int);
+int	has_globbing(const char *, const char *);
+const unsigned char *pat_scan(const unsigned char *, const unsigned char *,
+    int);
+void	qsortp(void **, size_t, int (*)(const void *, const void *));
+int	xstrcmp(const void *, const void *);
+void	ksh_getopt_reset(Getopt *, int);
+int	ksh_getopt(char **, Getopt *, const char *);
+void	print_value_quoted(const char *);
+void	print_columns(struct shf *, int, char *(*)(void *, int, char *, int),
+    void *, int, int prefcol);
+int	strip_nuls(char *, int);
+int	blocking_read(int, char *, int);
+int	reset_nonblock(int);
+char	*ksh_get_wd(char *, int);
+/* path.c */
+int	make_path(const char *, const char *, char **, XString *, int *);
+void	simplify_path(char *);
+char	*get_phys_path(const char *);
+void	set_current_wd(char *);
+/* syn.c */
+void	initkeywords(void);
+struct op * compile(Source *);
+/* trap.c */
+void	inittraps(void);
+void	alarm_init(void);
+Trap *	gettrap(const char *, int);
+void	trapsig(int);
+void	intrcheck(void);
+int	fatal_trap_check(void);
+int	trap_pending(void);
+void	runtraps(int intr);
+void	runtrap(Trap *);
+void	cleartraps(void);
+void	restoresigs(void);
+void	settrap(Trap *, char *);
+int	block_pipe(void);
+void	restore_pipe(int);
+int	setsig(Trap *, sig_t, int);
+void	setexecsig(Trap *, int);
+/* var.c */
+void	newblock(void);
+void	popblock(void);
+void	initvar(void);
+struct tbl *	global(const char *);
+struct tbl *	local(const char *, bool);
+char *	str_val(struct tbl *);
+int64_t	intval(struct tbl *);
+int	setstr(struct tbl *, const char *, int);
+struct tbl *setint_v(struct tbl *, struct tbl *, bool);
+void	setint(struct tbl *, int64_t);
+int	getint(struct tbl *, int64_t *, bool);
+struct tbl *typeset(const char *, int, int, int, int);
+void	unset(struct tbl *, int);
+char  * skip_varname(const char *, int);
+char	*skip_wdvarname(const char *, int);
+int	is_wdvarname(const char *, int);
+int	is_wdvarassign(const char *);
+char **	makenv(void);
+void	change_random(void);
+int	array_ref_len(const char *);
+char *	arrayname(const char *);
+void    set_array(const char *, int, char **);
+/* vi.c: see edit.h */
diff --git a/shf.c b/shf.c
@@ -0,0 +1,998 @@
+/*	$OpenBSD: shf.c,v 1.33 2018/03/15 16:51:29 anton Exp $	*/
+
+/*
+ *  Shell file I/O routines
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+/* flags to shf_emptybuf() */
+#define EB_READSW	0x01	/* about to switch to reading */
+#define EB_GROW		0x02	/* grow buffer if necessary (STRING+DYNAMIC) */
+
+/*
+ * Replacement stdio routines.  Stdio is too flakey on too many machines
+ * to be useful when you have multiple processes using the same underlying
+ * file descriptors.
+ */
+
+static int	shf_fillbuf(struct shf *);
+static int	shf_emptybuf(struct shf *, int);
+
+/* Open a file.  First three args are for open(), last arg is flags for
+ * this package.  Returns NULL if file could not be opened, or if a dup
+ * fails.
+ */
+struct shf *
+shf_open(const char *name, int oflags, int mode, int sflags)
+{
+	struct shf *shf;
+	int bsize = sflags & SHF_UNBUF ? (sflags & SHF_RD ? 1 : 0) : SHF_BSIZE;
+	int fd;
+
+	/* Done before open so if alloca fails, fd won't be lost. */
+	shf = alloc(sizeof(struct shf) + bsize, ATEMP);
+	shf->areap = ATEMP;
+	shf->buf = (unsigned char *) &shf[1];
+	shf->bsize = bsize;
+	shf->flags = SHF_ALLOCS;
+	/* Rest filled in by reopen. */
+
+	fd = open(name, oflags, mode);
+	if (fd < 0) {
+		afree(shf, shf->areap);
+		return NULL;
+	}
+	if ((sflags & SHF_MAPHI) && fd < FDBASE) {
+		int nfd;
+
+		nfd = fcntl(fd, F_DUPFD, FDBASE);
+		close(fd);
+		if (nfd < 0) {
+			afree(shf, shf->areap);
+			return NULL;
+		}
+		fd = nfd;
+	}
+	sflags &= ~SHF_ACCMODE;
+	sflags |= (oflags & O_ACCMODE) == O_RDONLY ? SHF_RD :
+	    ((oflags & O_ACCMODE) == O_WRONLY ? SHF_WR : SHF_RDWR);
+
+	return shf_reopen(fd, sflags, shf);
+}
+
+/* Set up the shf structure for a file descriptor.  Doesn't fail. */
+struct shf *
+shf_fdopen(int fd, int sflags, struct shf *shf)
+{
+	int bsize = sflags & SHF_UNBUF ? (sflags & SHF_RD ? 1 : 0) : SHF_BSIZE;
+
+	/* use fcntl() to figure out correct read/write flags */
+	if (sflags & SHF_GETFL) {
+		int flags = fcntl(fd, F_GETFL);
+
+		if (flags < 0)
+			/* will get an error on first read/write */
+			sflags |= SHF_RDWR;
+		else {
+			switch (flags & O_ACCMODE) {
+			case O_RDONLY:
+				sflags |= SHF_RD;
+				break;
+			case O_WRONLY:
+				sflags |= SHF_WR;
+				break;
+			case O_RDWR:
+				sflags |= SHF_RDWR;
+				break;
+			}
+		}
+	}
+
+	if (!(sflags & (SHF_RD | SHF_WR)))
+		internal_errorf("%s: missing read/write", __func__);
+
+	if (shf) {
+		if (bsize) {
+			shf->buf = alloc(bsize, ATEMP);
+			sflags |= SHF_ALLOCB;
+		} else
+			shf->buf = NULL;
+	} else {
+		shf = alloc(sizeof(struct shf) + bsize, ATEMP);
+		shf->buf = (unsigned char *) &shf[1];
+		sflags |= SHF_ALLOCS;
+	}
+	shf->areap = ATEMP;
+	shf->fd = fd;
+	shf->rp = shf->wp = shf->buf;
+	shf->rnleft = 0;
+	shf->rbsize = bsize;
+	shf->wnleft = 0; /* force call to shf_emptybuf() */
+	shf->wbsize = sflags & SHF_UNBUF ? 0 : bsize;
+	shf->flags = sflags;
+	shf->errno_ = 0;
+	shf->bsize = bsize;
+	if (sflags & SHF_CLEXEC)
+		fcntl(fd, F_SETFD, FD_CLOEXEC);
+	return shf;
+}
+
+/* Set up an existing shf (and buffer) to use the given fd */
+struct shf *
+shf_reopen(int fd, int sflags, struct shf *shf)
+{
+	int bsize = sflags & SHF_UNBUF ? (sflags & SHF_RD ? 1 : 0) : SHF_BSIZE;
+
+	/* use fcntl() to figure out correct read/write flags */
+	if (sflags & SHF_GETFL) {
+		int flags = fcntl(fd, F_GETFL);
+
+		if (flags < 0)
+			/* will get an error on first read/write */
+			sflags |= SHF_RDWR;
+		else {
+			switch (flags & O_ACCMODE) {
+			case O_RDONLY:
+				sflags |= SHF_RD;
+				break;
+			case O_WRONLY:
+				sflags |= SHF_WR;
+				break;
+			case O_RDWR:
+				sflags |= SHF_RDWR;
+				break;
+			}
+		}
+	}
+
+	if (!(sflags & (SHF_RD | SHF_WR)))
+		internal_errorf("%s: missing read/write", __func__);
+	if (!shf || !shf->buf || shf->bsize < bsize)
+		internal_errorf("%s: bad shf/buf/bsize", __func__);
+
+	/* assumes shf->buf and shf->bsize already set up */
+	shf->fd = fd;
+	shf->rp = shf->wp = shf->buf;
+	shf->rnleft = 0;
+	shf->rbsize = bsize;
+	shf->wnleft = 0; /* force call to shf_emptybuf() */
+	shf->wbsize = sflags & SHF_UNBUF ? 0 : bsize;
+	shf->flags = (shf->flags & (SHF_ALLOCS | SHF_ALLOCB)) | sflags;
+	shf->errno_ = 0;
+	if (sflags & SHF_CLEXEC)
+		fcntl(fd, F_SETFD, FD_CLOEXEC);
+	return shf;
+}
+
+/* Open a string for reading or writing.  If reading, bsize is the number
+ * of bytes that can be read.  If writing, bsize is the maximum number of
+ * bytes that can be written.  If shf is not null, it is filled in and
+ * returned, if it is null, shf is allocated.  If writing and buf is null
+ * and SHF_DYNAMIC is set, the buffer is allocated (if bsize > 0, it is
+ * used for the initial size).  Doesn't fail.
+ * When writing, a byte is reserved for a trailing null - see shf_sclose().
+ */
+struct shf *
+shf_sopen(char *buf, int bsize, int sflags, struct shf *shf)
+{
+	/* can't have a read+write string */
+	if (!(sflags & (SHF_RD | SHF_WR)) ||
+	    (sflags & (SHF_RD | SHF_WR)) == (SHF_RD | SHF_WR))
+		internal_errorf("%s: flags 0x%x", __func__, sflags);
+
+	if (!shf) {
+		shf = alloc(sizeof(struct shf), ATEMP);
+		sflags |= SHF_ALLOCS;
+	}
+	shf->areap = ATEMP;
+	if (!buf && (sflags & SHF_WR) && (sflags & SHF_DYNAMIC)) {
+		if (bsize <= 0)
+			bsize = 64;
+		sflags |= SHF_ALLOCB;
+		buf = alloc(bsize, shf->areap);
+	}
+	shf->fd = -1;
+	shf->buf = shf->rp = shf->wp = (unsigned char *) buf;
+	shf->rnleft = bsize;
+	shf->rbsize = bsize;
+	shf->wnleft = bsize - 1;	/* space for a '\0' */
+	shf->wbsize = bsize;
+	shf->flags = sflags | SHF_STRING;
+	shf->errno_ = 0;
+	shf->bsize = bsize;
+
+	return shf;
+}
+
+/* Flush and close file descriptor, free the shf structure */
+int
+shf_close(struct shf *shf)
+{
+	int ret = 0;
+
+	if (shf->fd >= 0) {
+		ret = shf_flush(shf);
+		if (close(shf->fd) < 0)
+			ret = EOF;
+	}
+	if (shf->flags & SHF_ALLOCS)
+		afree(shf, shf->areap);
+	else if (shf->flags & SHF_ALLOCB)
+		afree(shf->buf, shf->areap);
+
+	return ret;
+}
+
+/* Flush and close file descriptor, don't free file structure */
+int
+shf_fdclose(struct shf *shf)
+{
+	int ret = 0;
+
+	if (shf->fd >= 0) {
+		ret = shf_flush(shf);
+		if (close(shf->fd) < 0)
+			ret = EOF;
+		shf->rnleft = 0;
+		shf->rp = shf->buf;
+		shf->wnleft = 0;
+		shf->fd = -1;
+	}
+
+	return ret;
+}
+
+/* Close a string - if it was opened for writing, it is null terminated;
+ * returns a pointer to the string and frees shf if it was allocated
+ * (does not free string if it was allocated).
+ */
+char *
+shf_sclose(struct shf *shf)
+{
+	unsigned char *s = shf->buf;
+
+	/* null terminate */
+	if (shf->flags & SHF_WR) {
+		shf->wnleft++;
+		shf_putc('\0', shf);
+	}
+	if (shf->flags & SHF_ALLOCS)
+		afree(shf, shf->areap);
+	return (char *) s;
+}
+
+/* Un-read what has been read but not examined, or write what has been
+ * buffered.  Returns 0 for success, EOF for (write) error.
+ */
+int
+shf_flush(struct shf *shf)
+{
+	if (shf->flags & SHF_STRING)
+		return (shf->flags & SHF_WR) ? EOF : 0;
+
+	if (shf->fd < 0)
+		internal_errorf("%s: no fd", __func__);
+
+	if (shf->flags & SHF_ERROR) {
+		errno = shf->errno_;
+		return EOF;
+	}
+
+	if (shf->flags & SHF_READING) {
+		shf->flags &= ~(SHF_EOF | SHF_READING);
+		if (shf->rnleft > 0) {
+			lseek(shf->fd, (off_t) -shf->rnleft, SEEK_CUR);
+			shf->rnleft = 0;
+			shf->rp = shf->buf;
+		}
+		return 0;
+	} else if (shf->flags & SHF_WRITING)
+		return shf_emptybuf(shf, 0);
+
+	return 0;
+}
+
+/* Write out any buffered data.  If currently reading, flushes the read
+ * buffer.  Returns 0 for success, EOF for (write) error.
+ */
+static int
+shf_emptybuf(struct shf *shf, int flags)
+{
+	int ret = 0;
+
+	if (!(shf->flags & SHF_STRING) && shf->fd < 0)
+		internal_errorf("%s: no fd", __func__);
+
+	if (shf->flags & SHF_ERROR) {
+		errno = shf->errno_;
+		return EOF;
+	}
+
+	if (shf->flags & SHF_READING) {
+		if (flags & EB_READSW) /* doesn't happen */
+			return 0;
+		ret = shf_flush(shf);
+		shf->flags &= ~SHF_READING;
+	}
+	if (shf->flags & SHF_STRING) {
+		unsigned char	*nbuf;
+
+		/* Note that we assume SHF_ALLOCS is not set if SHF_ALLOCB
+		 * is set... (changing the shf pointer could cause problems)
+		 */
+		if (!(flags & EB_GROW) || !(shf->flags & SHF_DYNAMIC) ||
+		    !(shf->flags & SHF_ALLOCB))
+			return EOF;
+		/* allocate more space for buffer */
+		nbuf = areallocarray(shf->buf, 2, shf->wbsize, shf->areap);
+		shf->rp = nbuf + (shf->rp - shf->buf);
+		shf->wp = nbuf + (shf->wp - shf->buf);
+		shf->rbsize += shf->wbsize;
+		shf->wnleft += shf->wbsize;
+		shf->wbsize *= 2;
+		shf->buf = nbuf;
+	} else {
+		if (shf->flags & SHF_WRITING) {
+			int ntowrite = shf->wp - shf->buf;
+			unsigned char *buf = shf->buf;
+			int n;
+
+			while (ntowrite > 0) {
+				n = write(shf->fd, buf, ntowrite);
+				if (n < 0) {
+					if (errno == EINTR &&
+					    !(shf->flags & SHF_INTERRUPT))
+						continue;
+					shf->flags |= SHF_ERROR;
+					shf->errno_ = errno;
+					shf->wnleft = 0;
+					if (buf != shf->buf) {
+						/* allow a second flush
+						 * to work */
+						memmove(shf->buf, buf,
+						    ntowrite);
+						shf->wp = shf->buf + ntowrite;
+					}
+					return EOF;
+				}
+				buf += n;
+				ntowrite -= n;
+			}
+			if (flags & EB_READSW) {
+				shf->wp = shf->buf;
+				shf->wnleft = 0;
+				shf->flags &= ~SHF_WRITING;
+				return 0;
+			}
+		}
+		shf->wp = shf->buf;
+		shf->wnleft = shf->wbsize;
+	}
+	shf->flags |= SHF_WRITING;
+
+	return ret;
+}
+
+/* Fill up a read buffer.  Returns EOF for a read error, 0 otherwise. */
+static int
+shf_fillbuf(struct shf *shf)
+{
+	if (shf->flags & SHF_STRING)
+		return 0;
+
+	if (shf->fd < 0)
+		internal_errorf("%s: no fd", __func__);
+
+	if (shf->flags & (SHF_EOF | SHF_ERROR)) {
+		if (shf->flags & SHF_ERROR)
+			errno = shf->errno_;
+		return EOF;
+	}
+
+	if ((shf->flags & SHF_WRITING) && shf_emptybuf(shf, EB_READSW) == EOF)
+		return EOF;
+
+	shf->flags |= SHF_READING;
+
+	shf->rp = shf->buf;
+	while (1) {
+		shf->rnleft = blocking_read(shf->fd, (char *) shf->buf,
+		    shf->rbsize);
+		if (shf->rnleft < 0 && errno == EINTR &&
+		    !(shf->flags & SHF_INTERRUPT))
+			continue;
+		break;
+	}
+	if (shf->rnleft <= 0) {
+		if (shf->rnleft < 0) {
+			shf->flags |= SHF_ERROR;
+			shf->errno_ = errno;
+			shf->rnleft = 0;
+			shf->rp = shf->buf;
+			return EOF;
+		}
+		shf->flags |= SHF_EOF;
+	}
+	return 0;
+}
+
+/* Read a buffer from shf.  Returns the number of bytes read into buf,
+ * if no bytes were read, returns 0 if end of file was seen, EOF if
+ * a read error occurred.
+ */
+int
+shf_read(char *buf, int bsize, struct shf *shf)
+{
+	int orig_bsize = bsize;
+	int ncopy;
+
+	if (!(shf->flags & SHF_RD))
+		internal_errorf("%s: flags %x", __func__, shf->flags);
+
+	if (bsize <= 0)
+		internal_errorf("%s: bsize %d", __func__, bsize);
+
+	while (bsize > 0) {
+		if (shf->rnleft == 0 &&
+		    (shf_fillbuf(shf) == EOF || shf->rnleft == 0))
+			break;
+		ncopy = shf->rnleft;
+		if (ncopy > bsize)
+			ncopy = bsize;
+		memcpy(buf, shf->rp, ncopy);
+		buf += ncopy;
+		bsize -= ncopy;
+		shf->rp += ncopy;
+		shf->rnleft -= ncopy;
+	}
+	/* Note: fread(3S) returns 0 for errors - this doesn't */
+	return orig_bsize == bsize ? (shf_error(shf) ? EOF : 0) :
+	    orig_bsize - bsize;
+}
+
+/* Read up to a newline or EOF.  The newline is put in buf; buf is always
+ * null terminated.  Returns NULL on read error or if nothing was read before
+ * end of file, returns a pointer to the null byte in buf otherwise.
+ */
+char *
+shf_getse(char *buf, int bsize, struct shf *shf)
+{
+	unsigned char *end;
+	int ncopy;
+	char *orig_buf = buf;
+
+	if (!(shf->flags & SHF_RD))
+		internal_errorf("%s: flags %x", __func__, shf->flags);
+
+	if (bsize <= 0)
+		return NULL;
+
+	--bsize;	/* save room for null */
+	do {
+		if (shf->rnleft == 0) {
+			if (shf_fillbuf(shf) == EOF)
+				return NULL;
+			if (shf->rnleft == 0) {
+				*buf = '\0';
+				return buf == orig_buf ? NULL : buf;
+			}
+		}
+		end = (unsigned char *) memchr((char *) shf->rp, '\n',
+		    shf->rnleft);
+		ncopy = end ? end - shf->rp + 1 : shf->rnleft;
+		if (ncopy > bsize)
+			ncopy = bsize;
+		memcpy(buf, (char *) shf->rp, ncopy);
+		shf->rp += ncopy;
+		shf->rnleft -= ncopy;
+		buf += ncopy;
+		bsize -= ncopy;
+	} while (!end && bsize);
+	*buf = '\0';
+	return buf;
+}
+
+/* Returns the char read.  Returns EOF for error and end of file. */
+int
+shf_getchar(struct shf *shf)
+{
+	if (!(shf->flags & SHF_RD))
+		internal_errorf("%s: flags %x", __func__, shf->flags);
+
+	if (shf->rnleft == 0 && (shf_fillbuf(shf) == EOF || shf->rnleft == 0))
+		return EOF;
+	--shf->rnleft;
+	return *shf->rp++;
+}
+
+/* Put a character back in the input stream.  Returns the character if
+ * successful, EOF if there is no room.
+ */
+int
+shf_ungetc(int c, struct shf *shf)
+{
+	if (!(shf->flags & SHF_RD))
+		internal_errorf("%s: flags %x", __func__, shf->flags);
+
+	if ((shf->flags & SHF_ERROR) || c == EOF ||
+	    (shf->rp == shf->buf && shf->rnleft))
+		return EOF;
+
+	if ((shf->flags & SHF_WRITING) && shf_emptybuf(shf, EB_READSW) == EOF)
+		return EOF;
+
+	if (shf->rp == shf->buf)
+		shf->rp = shf->buf + shf->rbsize;
+	if (shf->flags & SHF_STRING) {
+		/* Can unget what was read, but not something different - we
+		 * don't want to modify a string.
+		 */
+		if (shf->rp[-1] != c)
+			return EOF;
+		shf->flags &= ~SHF_EOF;
+		shf->rp--;
+		shf->rnleft++;
+		return c;
+	}
+	shf->flags &= ~SHF_EOF;
+	*--(shf->rp) = c;
+	shf->rnleft++;
+	return c;
+}
+
+/* Write a character.  Returns the character if successful, EOF if
+ * the char could not be written.
+ */
+int
+shf_putchar(int c, struct shf *shf)
+{
+	if (!(shf->flags & SHF_WR))
+		internal_errorf("%s: flags %x", __func__, shf->flags);
+
+	if (c == EOF)
+		return EOF;
+
+	if (shf->flags & SHF_UNBUF) {
+		char cc = c;
+		int n;
+
+		if (shf->fd < 0)
+			internal_errorf("%s: no fd", __func__);
+		if (shf->flags & SHF_ERROR) {
+			errno = shf->errno_;
+			return EOF;
+		}
+		while ((n = write(shf->fd, &cc, 1)) != 1)
+			if (n < 0) {
+				if (errno == EINTR &&
+				    !(shf->flags & SHF_INTERRUPT))
+					continue;
+				shf->flags |= SHF_ERROR;
+				shf->errno_ = errno;
+				return EOF;
+			}
+	} else {
+		/* Flush deals with strings and sticky errors */
+		if (shf->wnleft == 0 && shf_emptybuf(shf, EB_GROW) == EOF)
+			return EOF;
+		shf->wnleft--;
+		*shf->wp++ = c;
+	}
+
+	return c;
+}
+
+/* Write a string.  Returns the length of the string if successful, EOF if
+ * the string could not be written.
+ */
+int
+shf_puts(const char *s, struct shf *shf)
+{
+	if (!s)
+		return EOF;
+
+	return shf_write(s, strlen(s), shf);
+}
+
+/* Write a buffer.  Returns nbytes if successful, EOF if there is an error. */
+int
+shf_write(const char *buf, int nbytes, struct shf *shf)
+{
+	int orig_nbytes = nbytes;
+	int n;
+	int ncopy;
+
+	if (!(shf->flags & SHF_WR))
+		internal_errorf("%s: flags %x", __func__, shf->flags);
+
+	if (nbytes < 0)
+		internal_errorf("%s: nbytes %d", __func__, nbytes);
+
+	/* Don't buffer if buffer is empty and we're writting a large amount. */
+	if ((ncopy = shf->wnleft) &&
+	    (shf->wp != shf->buf || nbytes < shf->wnleft)) {
+		if (ncopy > nbytes)
+			ncopy = nbytes;
+		memcpy(shf->wp, buf, ncopy);
+		nbytes -= ncopy;
+		buf += ncopy;
+		shf->wp += ncopy;
+		shf->wnleft -= ncopy;
+	}
+	if (nbytes > 0) {
+		/* Flush deals with strings and sticky errors */
+		if (shf_emptybuf(shf, EB_GROW) == EOF)
+			return EOF;
+		if (nbytes > shf->wbsize) {
+			ncopy = nbytes;
+			if (shf->wbsize)
+				ncopy -= nbytes % shf->wbsize;
+			nbytes -= ncopy;
+			while (ncopy > 0) {
+				n = write(shf->fd, buf, ncopy);
+				if (n < 0) {
+					if (errno == EINTR &&
+					    !(shf->flags & SHF_INTERRUPT))
+						continue;
+					shf->flags |= SHF_ERROR;
+					shf->errno_ = errno;
+					shf->wnleft = 0;
+					/* Note: fwrite(3S) returns 0 for
+					 * errors - this doesn't */
+					return EOF;
+				}
+				buf += n;
+				ncopy -= n;
+			}
+		}
+		if (nbytes > 0) {
+			memcpy(shf->wp, buf, nbytes);
+			shf->wp += nbytes;
+			shf->wnleft -= nbytes;
+		}
+	}
+
+	return orig_nbytes;
+}
+
+int
+shf_fprintf(struct shf *shf, const char *fmt, ...)
+{
+	va_list args;
+	int n;
+
+	va_start(args, fmt);
+	n = shf_vfprintf(shf, fmt, args);
+	va_end(args);
+
+	return n;
+}
+
+int
+shf_snprintf(char *buf, int bsize, const char *fmt, ...)
+{
+	struct shf shf;
+	va_list args;
+	int n;
+
+	if (!buf || bsize <= 0)
+		internal_errorf("%s: buf %lx, bsize %d",
+			__func__, (long) buf, bsize);
+
+	shf_sopen(buf, bsize, SHF_WR, &shf);
+	va_start(args, fmt);
+	n = shf_vfprintf(&shf, fmt, args);
+	va_end(args);
+	shf_sclose(&shf); /* null terminates */
+	return n;
+}
+
+char *
+shf_smprintf(const char *fmt, ...)
+{
+	struct shf shf;
+	va_list args;
+
+	shf_sopen(NULL, 0, SHF_WR|SHF_DYNAMIC, &shf);
+	va_start(args, fmt);
+	shf_vfprintf(&shf, fmt, args);
+	va_end(args);
+	return shf_sclose(&shf); /* null terminates */
+}
+
+#define	FL_HASH		0x001	/* `#' seen */
+#define FL_PLUS		0x002	/* `+' seen */
+#define FL_RIGHT	0x004	/* `-' seen */
+#define FL_BLANK	0x008	/* ` ' seen */
+#define FL_SHORT	0x010	/* `h' seen */
+#define FL_LONG		0x020	/* `l' seen */
+#define FL_LLONG	0x040	/* `ll' seen */
+#define FL_ZERO		0x080	/* `0' seen */
+#define FL_DOT		0x100	/* '.' seen */
+#define FL_UPPER	0x200	/* format character was uppercase */
+#define FL_NUMBER	0x400	/* a number was formated %[douxefg] */
+
+int
+shf_vfprintf(struct shf *shf, const char *fmt, va_list args)
+{
+	char		c, *s;
+	int		tmp = 0;
+	int		field, precision;
+	int		len;
+	int		flags;
+	unsigned long long	llnum;
+					/* %#o produces the longest output */
+	char		numbuf[(BITS(long long) + 2) / 3 + 1];
+	/* this stuff for dealing with the buffer */
+	int		nwritten = 0;
+
+	if (!fmt)
+		return 0;
+
+	while ((c = *fmt++)) {
+		if (c != '%') {
+			shf_putc(c, shf);
+			nwritten++;
+			continue;
+		}
+		/*
+		 *	This will accept flags/fields in any order - not
+		 *  just the order specified in printf(3), but this is
+		 *  the way _doprnt() seems to work (on bsd and sysV).
+		 *  The only restriction is that the format character must
+		 *  come last :-).
+		 */
+		flags = field = precision = 0;
+		for ( ; (c = *fmt++) ; ) {
+			switch (c) {
+			case '#':
+				flags |= FL_HASH;
+				continue;
+
+			case '+':
+				flags |= FL_PLUS;
+				continue;
+
+			case '-':
+				flags |= FL_RIGHT;
+				continue;
+
+			case ' ':
+				flags |= FL_BLANK;
+				continue;
+
+			case '0':
+				if (!(flags & FL_DOT))
+					flags |= FL_ZERO;
+				continue;
+
+			case '.':
+				flags |= FL_DOT;
+				precision = 0;
+				continue;
+
+			case '*':
+				tmp = va_arg(args, int);
+				if (flags & FL_DOT)
+					precision = tmp;
+				else if ((field = tmp) < 0) {
+					field = -field;
+					flags |= FL_RIGHT;
+				}
+				continue;
+
+			case 'l':
+				if (*fmt == 'l') {
+					fmt++;
+					flags |= FL_LLONG;
+				} else
+					flags |= FL_LONG;
+				continue;
+
+			case 'h':
+				flags |= FL_SHORT;
+				continue;
+			}
+			if (digit(c)) {
+				tmp = c - '0';
+				while (c = *fmt++, digit(c))
+					tmp = tmp * 10 + c - '0';
+				--fmt;
+				if (tmp < 0)		/* overflow? */
+					tmp = 0;
+				if (flags & FL_DOT)
+					precision = tmp;
+				else
+					field = tmp;
+				continue;
+			}
+			break;
+		}
+
+		if (precision < 0)
+			precision = 0;
+
+		if (!c)		/* nasty format */
+			break;
+
+		if (c >= 'A' && c <= 'Z') {
+			flags |= FL_UPPER;
+			c = c - 'A' + 'a';
+		}
+
+		switch (c) {
+		case 'p': /* pointer */
+			flags &= ~(FL_LLONG | FL_SHORT);
+			flags |= FL_LONG;
+			/* aaahhh... */
+		case 'd':
+		case 'i':
+		case 'o':
+		case 'u':
+		case 'x':
+			flags |= FL_NUMBER;
+			s = &numbuf[sizeof(numbuf)];
+			if (flags & FL_LLONG)
+				llnum = va_arg(args, unsigned long long);
+			else if (flags & FL_LONG) {
+				if (c == 'd' || c == 'i')
+					llnum = va_arg(args, long);
+				else
+					llnum = va_arg(args, unsigned long);
+			} else {
+				if (c == 'd' || c == 'i')
+					llnum = va_arg(args, int);
+				else
+					llnum = va_arg(args, unsigned int);
+			}
+			switch (c) {
+			case 'd':
+			case 'i':
+				if (0 > (long long) llnum)
+					llnum = - (long long) llnum, tmp = 1;
+				else
+					tmp = 0;
+				/* aaahhhh..... */
+
+			case 'u':
+				do {
+					*--s = llnum % 10 + '0';
+					llnum /= 10;
+				} while (llnum);
+
+				if (c != 'u') {
+					if (tmp)
+						*--s = '-';
+					else if (flags & FL_PLUS)
+						*--s = '+';
+					else if (flags & FL_BLANK)
+						*--s = ' ';
+				}
+				break;
+
+			case 'o':
+				do {
+					*--s = (llnum & 0x7) + '0';
+					llnum >>= 3;
+				} while (llnum);
+
+				if ((flags & FL_HASH) && *s != '0')
+					*--s = '0';
+				break;
+
+			case 'p':
+			case 'x':
+			    {
+				const char *digits = (flags & FL_UPPER) ?
+				    "0123456789ABCDEF" :
+				    "0123456789abcdef";
+				do {
+					*--s = digits[llnum & 0xf];
+					llnum >>= 4;
+				} while (llnum);
+
+				if (flags & FL_HASH) {
+					*--s = (flags & FL_UPPER) ? 'X' : 'x';
+					*--s = '0';
+				}
+			    }
+			}
+			len = &numbuf[sizeof(numbuf)] - s;
+			if (flags & FL_DOT) {
+				if (precision > len) {
+					field = precision;
+					flags |= FL_ZERO;
+				} else
+					precision = len; /* no loss */
+			}
+			break;
+
+		case 's':
+			if (!(s = va_arg(args, char *)))
+				s = "(null %s)";
+			len = strlen(s);
+			break;
+
+		case 'c':
+			flags &= ~FL_DOT;
+			numbuf[0] = va_arg(args, int);
+			s = numbuf;
+			len = 1;
+			break;
+
+		case '%':
+		default:
+			numbuf[0] = c;
+			s = numbuf;
+			len = 1;
+			break;
+		}
+
+		/*
+		 *	At this point s should point to a string that is
+		 *  to be formatted, and len should be the length of the
+		 *  string.
+		 */
+		if (!(flags & FL_DOT) || len < precision)
+			precision = len;
+		if (field > precision) {
+			field -= precision;
+			if (!(flags & FL_RIGHT)) {
+				field = -field;
+				/* skip past sign or 0x when padding with 0 */
+				if ((flags & FL_ZERO) && (flags & FL_NUMBER)) {
+					if (*s == '+' || *s == '-' || *s ==' ') {
+						shf_putc(*s, shf);
+						s++;
+						precision--;
+						nwritten++;
+					} else if (*s == '0') {
+						shf_putc(*s, shf);
+						s++;
+						nwritten++;
+						if (--precision > 0 &&
+						    (*s | 0x20) == 'x') {
+							shf_putc(*s, shf);
+							s++;
+							precision--;
+							nwritten++;
+						}
+					}
+					c = '0';
+				} else
+					c = flags & FL_ZERO ? '0' : ' ';
+				if (field < 0) {
+					nwritten += -field;
+					for ( ; field < 0 ; field++)
+						shf_putc(c, shf);
+				}
+			} else
+				c = ' ';
+		} else
+			field = 0;
+
+		if (precision > 0) {
+			nwritten += precision;
+			for ( ; precision-- > 0 ; s++)
+				shf_putc(*s, shf);
+		}
+		if (field > 0) {
+			nwritten += field;
+			for ( ; field > 0 ; --field)
+				shf_putc(c, shf);
+		}
+	}
+
+	return shf_error(shf) ? EOF : nwritten;
+}
diff --git a/shf.h b/shf.h
@@ -0,0 +1,79 @@
+/*	$OpenBSD: shf.h,v 1.8 2015/12/14 06:09:43 mmcc Exp $	*/
+
+#ifndef SHF_H
+# define SHF_H
+
+/*
+ * Shell file I/O routines
+ */
+
+#define SHF_BSIZE	512
+
+#define shf_getc(shf) ((shf)->rnleft > 0 ? (shf)->rnleft--, *(shf)->rp++ : \
+			shf_getchar(shf))
+#define shf_putc(c, shf)	((shf)->wnleft == 0 ? shf_putchar((c), (shf)) : \
+				    ((shf)->wnleft--, *(shf)->wp++ = (c)))
+#define shf_eof(shf)		((shf)->flags & SHF_EOF)
+#define shf_error(shf)		((shf)->flags & SHF_ERROR)
+#define shf_clearerr(shf)	((shf)->flags &= ~(SHF_EOF | SHF_ERROR))
+
+/* Flags passed to shf_*open() */
+#define SHF_RD		0x0001
+#define SHF_WR		0x0002
+#define SHF_RDWR	  (SHF_RD|SHF_WR)
+#define SHF_ACCMODE	  0x0003	/* mask */
+#define SHF_GETFL	0x0004		/* use fcntl() to figure RD/WR flags */
+#define SHF_UNBUF	0x0008		/* unbuffered I/O */
+#define SHF_CLEXEC	0x0010		/* set close on exec flag */
+#define SHF_MAPHI	0x0020		/* make fd > FDBASE (and close orig)
+					 * (shf_open() only) */
+#define SHF_DYNAMIC	0x0040		/* string: increase buffer as needed */
+#define SHF_INTERRUPT	0x0080		/* EINTR in read/write causes error */
+/* Flags used internally */
+#define SHF_STRING	0x0100		/* a string, not a file */
+#define SHF_ALLOCS	0x0200		/* shf and shf->buf were alloc()ed */
+#define SHF_ALLOCB	0x0400		/* shf->buf was alloc()ed */
+#define SHF_ERROR	0x0800		/* read()/write() error */
+#define SHF_EOF		0x1000		/* read eof (sticky) */
+#define SHF_READING	0x2000		/* currently reading: rnleft,rp valid */
+#define SHF_WRITING	0x4000		/* currently writing: wnleft,wp valid */
+
+
+struct shf {
+	int flags;		/* see SHF_* */
+	unsigned char *rp;	/* read: current position in buffer */
+	int rbsize;		/* size of buffer (1 if SHF_UNBUF) */
+	int rnleft;		/* read: how much data left in buffer */
+	unsigned char *wp;	/* write: current position in buffer */
+	int wbsize;		/* size of buffer (0 if SHF_UNBUF) */
+	int wnleft;		/* write: how much space left in buffer */
+	unsigned char *buf;	/* buffer */
+	int fd;			/* file descriptor */
+	int errno_;		/* saved value of errno after error */
+	int bsize;		/* actual size of buf */
+	Area *areap;		/* area shf/buf were allocated in */
+};
+
+extern struct shf shf_iob[];
+
+struct shf *shf_open(const char *, int, int, int);
+struct shf *shf_fdopen(int, int, struct shf *);
+struct shf *shf_reopen(int, int, struct shf *);
+struct shf *shf_sopen(char *, int, int, struct shf *);
+int	    shf_close(struct shf *);
+int	    shf_fdclose(struct shf *);
+char	   *shf_sclose(struct shf *);
+int	    shf_flush(struct shf *);
+int	    shf_read(char *, int, struct shf *);
+char	   *shf_getse(char *, int, struct shf *);
+int	    shf_getchar(struct shf *s);
+int	    shf_ungetc(int, struct shf *);
+int	    shf_putchar(int, struct shf *);
+int	    shf_puts(const char *, struct shf *);
+int	    shf_write(const char *, int, struct shf *);
+int	    shf_fprintf(struct shf *, const char *, ...);
+int	    shf_snprintf(char *, int, const char *, ...);
+char	    *shf_smprintf(const char *, ...);
+int	    shf_vfprintf(struct shf *, const char *, va_list);
+
+#endif /* SHF_H */
diff --git a/stdlib.h b/stdlib.h
@@ -0,0 +1,45 @@
+#ifndef _COMPAT_STDLIB_H_
+#define _COMPAT_STDLIB_H_
+
+#include_next <stdlib.h>
+
+/*	$OpenBSD: stdlib.h,v 1.75 2018/11/21 06:57:04 otto Exp $	*/
+/*	$NetBSD: stdlib.h,v 1.25 1995/12/27 21:19:08 jtc Exp $	*/
+
+/*-
+ * Copyright (c) 1990 The Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * 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 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)stdlib.h	5.13 (Berkeley) 6/4/91
+ */
+
+void	*reallocarray(void *, size_t, size_t);
+
+long long
+	 strtonum(const char *, long long, long long, const char **);
+
+#endif
diff --git a/string.h b/string.h
@@ -0,0 +1,45 @@
+#ifndef _COMPAT_STRING_H_
+#define _COMPAT_STRING_H_
+
+#include_next <string.h>
+
+/*	$OpenBSD: string.h,v 1.32 2017/09/05 03:16:13 schwarze Exp $	*/
+/*	$NetBSD: string.h,v 1.6 1994/10/26 00:56:30 cgd Exp $	*/
+
+/*-
+ * Copyright (c) 1990 The Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * 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 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)string.h	5.10 (Berkeley) 3/9/91
+ */
+
+size_t	 strlcat(char *, const char *, size_t)
+		__attribute__ ((__bounded__(__string__,1,3)));
+size_t	 strlcpy(char *, const char *, size_t)
+		__attribute__ ((__bounded__(__string__,1,3)));
+
+#endif
diff --git a/strlcat.c b/strlcat.c
@@ -0,0 +1,55 @@
+/*	$OpenBSD: strlcat.c,v 1.19 2019/01/25 00:19:25 millert Exp $	*/
+
+/*
+ * Copyright (c) 1998, 2015 Todd C. Miller <millert@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <string.h>
+
+/*
+ * Appends src to string dst of size dsize (unlike strncat, dsize is the
+ * full size of dst, not space left).  At most dsize-1 characters
+ * will be copied.  Always NUL terminates (unless dsize <= strlen(dst)).
+ * Returns strlen(src) + MIN(dsize, strlen(initial dst)).
+ * If retval >= dsize, truncation occurred.
+ */
+size_t
+strlcat(char *dst, const char *src, size_t dsize)
+{
+	const char *odst = dst;
+	const char *osrc = src;
+	size_t n = dsize;
+	size_t dlen;
+
+	/* Find the end of dst and adjust bytes left but don't go past end. */
+	while (n-- != 0 && *dst != '\0')
+		dst++;
+	dlen = dst - odst;
+	n = dsize - dlen;
+
+	if (n-- == 0)
+		return(dlen + strlen(src));
+	while (*src != '\0') {
+		if (n != 0) {
+			*dst++ = *src;
+			n--;
+		}
+		src++;
+	}
+	*dst = '\0';
+
+	return(dlen + (src - osrc));	/* count does not include NUL */
+}
diff --git a/strlcpy.c b/strlcpy.c
@@ -0,0 +1,50 @@
+/*	$OpenBSD: strlcpy.c,v 1.16 2019/01/25 00:19:25 millert Exp $	*/
+
+/*
+ * Copyright (c) 1998, 2015 Todd C. Miller <millert@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <string.h>
+
+/*
+ * Copy string src to buffer dst of size dsize.  At most dsize-1
+ * chars will be copied.  Always NUL terminates (unless dsize == 0).
+ * Returns strlen(src); if retval >= dsize, truncation occurred.
+ */
+size_t
+strlcpy(char *dst, const char *src, size_t dsize)
+{
+	const char *osrc = src;
+	size_t nleft = dsize;
+
+	/* Copy as many bytes as will fit. */
+	if (nleft != 0) {
+		while (--nleft != 0) {
+			if ((*dst++ = *src++) == '\0')
+				break;
+		}
+	}
+
+	/* Not enough room in dst, add NUL and traverse rest of src. */
+	if (nleft == 0) {
+		if (dsize != 0)
+			*dst = '\0';		/* NUL-terminate dst */
+		while (*src++)
+			;
+	}
+
+	return(src - osrc - 1);	/* count does not include NUL */
+}
diff --git a/strtonum.c b/strtonum.c
@@ -0,0 +1,65 @@
+/*	$OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $	*/
+
+/*
+ * Copyright (c) 2004 Ted Unangst and Todd Miller
+ * All rights reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+
+#define	INVALID		1
+#define	TOOSMALL	2
+#define	TOOLARGE	3
+
+long long
+strtonum(const char *numstr, long long minval, long long maxval,
+    const char **errstrp)
+{
+	long long ll = 0;
+	int error = 0;
+	char *ep;
+	struct errval {
+		const char *errstr;
+		int err;
+	} ev[4] = {
+		{ NULL,		0 },
+		{ "invalid",	EINVAL },
+		{ "too small",	ERANGE },
+		{ "too large",	ERANGE },
+	};
+
+	ev[0].err = errno;
+	errno = 0;
+	if (minval > maxval) {
+		error = INVALID;
+	} else {
+		ll = strtoll(numstr, &ep, 10);
+		if (numstr == ep || *ep != '\0')
+			error = INVALID;
+		else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval)
+			error = TOOSMALL;
+		else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval)
+			error = TOOLARGE;
+	}
+	if (errstrp != NULL)
+		*errstrp = ev[error].errstr;
+	errno = ev[error].err;
+	if (error)
+		ll = 0;
+
+	return (ll);
+}
diff --git a/syn.c b/syn.c
@@ -0,0 +1,901 @@
+/*	$OpenBSD: syn.c,v 1.39 2018/04/24 08:25:16 kn Exp $	*/
+
+/*
+ * shell parser (C version)
+ */
+
+#include <string.h>
+
+#include "sh.h"
+#include "c_test.h"
+
+struct nesting_state {
+	int	start_token;	/* token than began nesting (eg, FOR) */
+	int	start_line;	/* line nesting began on */
+};
+
+static void	yyparse(void);
+static struct op *pipeline(int);
+static struct op *andor(void);
+static struct op *c_list(int);
+static struct ioword *synio(int);
+static void	musthave(int, int);
+static struct op *nested(int, int, int);
+static struct op *get_command(int);
+static struct op *dogroup(void);
+static struct op *thenpart(void);
+static struct op *elsepart(void);
+static struct op *caselist(void);
+static struct op *casepart(int);
+static struct op *function_body(char *, int);
+static char **	wordlist(void);
+static struct op *block(int, struct op *, struct op *, char **);
+static struct op *newtp(int);
+static void	syntaxerr(const char *) __attribute__((__noreturn__));
+static void	nesting_push(struct nesting_state *, int);
+static void	nesting_pop(struct nesting_state *);
+static int	assign_command(char *);
+static int	inalias(struct source *);
+static int	dbtestp_isa(Test_env *, Test_meta);
+static const char *dbtestp_getopnd(Test_env *, Test_op, int);
+static int	dbtestp_eval(Test_env *, Test_op, const char *, const char *,
+		    int);
+static void	dbtestp_error(Test_env *, int, const char *);
+
+static	struct	op	*outtree; /* yyparse output */
+
+static struct nesting_state nesting;	/* \n changed to ; */
+
+static	int	reject;		/* token(cf) gets symbol again */
+static	int	symbol;		/* yylex value */
+
+#define	token(cf) \
+	((reject) ? (reject = false, symbol) : (symbol = yylex(cf)))
+#define	tpeek(cf) \
+	((reject) ? (symbol) : (reject = true, symbol = yylex(cf)))
+
+static void
+yyparse(void)
+{
+	int c;
+
+	reject = false;
+
+	outtree = c_list(source->type == SSTRING);
+	c = tpeek(0);
+	if (c == 0 && !outtree)
+		outtree = newtp(TEOF);
+	else if (c != '\n' && c != 0)
+		syntaxerr(NULL);
+}
+
+static struct op *
+pipeline(int cf)
+{
+	struct op *t, *p, *tl = NULL;
+
+	t = get_command(cf);
+	if (t != NULL) {
+		while (token(0) == '|') {
+			if ((p = get_command(CONTIN)) == NULL)
+				syntaxerr(NULL);
+			if (tl == NULL)
+				t = tl = block(TPIPE, t, p, NULL);
+			else
+				tl = tl->right = block(TPIPE, tl->right, p, NULL);
+		}
+		reject = true;
+	}
+	return (t);
+}
+
+static struct op *
+andor(void)
+{
+	struct op *t, *p;
+	int c;
+
+	t = pipeline(0);
+	if (t != NULL) {
+		while ((c = token(0)) == LOGAND || c == LOGOR) {
+			if ((p = pipeline(CONTIN)) == NULL)
+				syntaxerr(NULL);
+			t = block(c == LOGAND? TAND: TOR, t, p, NULL);
+		}
+		reject = true;
+	}
+	return (t);
+}
+
+static struct op *
+c_list(int multi)
+{
+	struct op *t = NULL, *p, *tl = NULL;
+	int c;
+	int have_sep;
+
+	while (1) {
+		p = andor();
+		/* Token has always been read/rejected at this point, so
+		 * we don't worry about what flags to pass token()
+		 */
+		c = token(0);
+		have_sep = 1;
+		if (c == '\n' && (multi || inalias(source))) {
+			if (!p) /* ignore blank lines */
+				continue;
+		} else if (!p)
+			break;
+		else if (c == '&' || c == COPROC)
+			p = block(c == '&' ? TASYNC : TCOPROC,
+				  p, NULL, NULL);
+		else if (c != ';')
+			have_sep = 0;
+		if (!t)
+			t = p;
+		else if (!tl)
+			t = tl = block(TLIST, t, p, NULL);
+		else
+			tl = tl->right = block(TLIST, tl->right, p, NULL);
+		if (!have_sep)
+			break;
+	}
+	reject = true;
+	return t;
+}
+
+static struct ioword *
+synio(int cf)
+{
+	struct ioword *iop;
+	int ishere;
+
+	if (tpeek(cf) != REDIR)
+		return NULL;
+	reject = false;
+	iop = yylval.iop;
+	ishere = (iop->flag&IOTYPE) == IOHERE;
+	musthave(LWORD, ishere ? HEREDELIM : 0);
+	if (ishere) {
+		iop->delim = yylval.cp;
+		if (*ident != 0) /* unquoted */
+			iop->flag |= IOEVAL;
+		if (herep >= &heres[HERES])
+			yyerror("too many <<'s\n");
+		*herep++ = iop;
+	} else
+		iop->name = yylval.cp;
+	return iop;
+}
+
+static void
+musthave(int c, int cf)
+{
+	if ((token(cf)) != c)
+		syntaxerr(NULL);
+}
+
+static struct op *
+nested(int type, int smark, int emark)
+{
+	struct op *t;
+	struct nesting_state old_nesting;
+
+	nesting_push(&old_nesting, smark);
+	t = c_list(true);
+	musthave(emark, KEYWORD|ALIAS);
+	nesting_pop(&old_nesting);
+	return (block(type, t, NULL, NULL));
+}
+
+static struct op *
+get_command(int cf)
+{
+	struct op *t;
+	int c, iopn = 0, syniocf;
+	struct ioword *iop, **iops;
+	XPtrV args, vars;
+	struct nesting_state old_nesting;
+
+	iops = areallocarray(NULL, NUFILE + 1,
+	    sizeof(struct ioword *), ATEMP);
+	XPinit(args, 16);
+	XPinit(vars, 16);
+
+	syniocf = KEYWORD|ALIAS;
+	switch (c = token(cf|KEYWORD|ALIAS|VARASN)) {
+	default:
+		reject = true;
+		afree(iops, ATEMP);
+		XPfree(args);
+		XPfree(vars);
+		return NULL; /* empty line */
+
+	case LWORD:
+	case REDIR:
+		reject = true;
+		syniocf &= ~(KEYWORD|ALIAS);
+		t = newtp(TCOM);
+		t->lineno = source->line;
+		while (1) {
+			cf = (t->u.evalflags ? ARRAYVAR : 0) |
+			    (XPsize(args) == 0 ? ALIAS|VARASN : CMDWORD);
+			switch (tpeek(cf)) {
+			case REDIR:
+				if (iopn >= NUFILE)
+					yyerror("too many redirections\n");
+				iops[iopn++] = synio(cf);
+				break;
+
+			case LWORD:
+				reject = false;
+				/* the iopn == 0 and XPsize(vars) == 0 are
+				 * dubious but at&t ksh acts this way
+				 */
+				if (iopn == 0 && XPsize(vars) == 0 &&
+				    XPsize(args) == 0 &&
+				    assign_command(ident))
+					t->u.evalflags = DOVACHECK;
+				if ((XPsize(args) == 0 || Flag(FKEYWORD)) &&
+				    is_wdvarassign(yylval.cp))
+					XPput(vars, yylval.cp);
+				else
+					XPput(args, yylval.cp);
+				break;
+
+			case '(':
+				/* Check for "> foo (echo hi)", which at&t ksh
+				 * allows (not POSIX, but not disallowed)
+				 */
+				afree(t, ATEMP);
+				if (XPsize(args) == 0 && XPsize(vars) == 0) {
+					reject = false;
+					goto Subshell;
+				}
+				/* Must be a function */
+				if (iopn != 0 || XPsize(args) != 1 ||
+				    XPsize(vars) != 0)
+					syntaxerr(NULL);
+				reject = false;
+				/*(*/
+				musthave(')', 0);
+				t = function_body(XPptrv(args)[0], false);
+				goto Leave;
+
+			default:
+				goto Leave;
+			}
+		}
+	  Leave:
+		break;
+
+	  Subshell:
+	case '(':
+		t = nested(TPAREN, '(', ')');
+		break;
+
+	case '{': /*}*/
+		t = nested(TBRACE, '{', '}');
+		break;
+
+	case MDPAREN:
+	  {
+		static const char let_cmd[] = {
+			CHAR, 'l', CHAR, 'e',
+			CHAR, 't', EOS
+		};
+		/* Leave KEYWORD in syniocf (allow if (( 1 )) then ...) */
+		t = newtp(TCOM);
+		t->lineno = source->line;
+		reject = false;
+		XPput(args, wdcopy(let_cmd, ATEMP));
+		musthave(LWORD,LETEXPR);
+		XPput(args, yylval.cp);
+		break;
+	  }
+
+	case DBRACKET: /* [[ .. ]] */
+		/* Leave KEYWORD in syniocf (allow if [[ -n 1 ]] then ...) */
+		t = newtp(TDBRACKET);
+		reject = false;
+		{
+			Test_env te;
+
+			te.flags = TEF_DBRACKET;
+			te.pos.av = &args;
+			te.isa = dbtestp_isa;
+			te.getopnd = dbtestp_getopnd;
+			te.eval = dbtestp_eval;
+			te.error = dbtestp_error;
+
+			test_parse(&te);
+		}
+		break;
+
+	case FOR:
+	case SELECT:
+		t = newtp((c == FOR) ? TFOR : TSELECT);
+		musthave(LWORD, ARRAYVAR);
+		if (!is_wdvarname(yylval.cp, true))
+			yyerror("%s: bad identifier\n",
+			    c == FOR ? "for" : "select");
+		t->str = str_save(ident, ATEMP);
+		nesting_push(&old_nesting, c);
+		t->vars = wordlist();
+		t->left = dogroup();
+		nesting_pop(&old_nesting);
+		break;
+
+	case WHILE:
+	case UNTIL:
+		nesting_push(&old_nesting, c);
+		t = newtp((c == WHILE) ? TWHILE : TUNTIL);
+		t->left = c_list(true);
+		t->right = dogroup();
+		nesting_pop(&old_nesting);
+		break;
+
+	case CASE:
+		t = newtp(TCASE);
+		musthave(LWORD, 0);
+		t->str = yylval.cp;
+		nesting_push(&old_nesting, c);
+		t->left = caselist();
+		nesting_pop(&old_nesting);
+		break;
+
+	case IF:
+		nesting_push(&old_nesting, c);
+		t = newtp(TIF);
+		t->left = c_list(true);
+		t->right = thenpart();
+		musthave(FI, KEYWORD|ALIAS);
+		nesting_pop(&old_nesting);
+		break;
+
+	case BANG:
+		syniocf &= ~(KEYWORD|ALIAS);
+		t = pipeline(0);
+		if (t == NULL)
+			syntaxerr(NULL);
+		t = block(TBANG, NULL, t, NULL);
+		break;
+
+	case TIME:
+		syniocf &= ~(KEYWORD|ALIAS);
+		t = pipeline(0);
+		if (t) {
+			if (t->str) {
+				t->str = str_save(t->str, ATEMP);
+			} else {
+				t->str = alloc(2, ATEMP);
+				t->str[0] = '\0'; /* TF_* flags */
+				t->str[1] = '\0';
+			}
+		}
+		t = block(TTIME, t, NULL, NULL);
+		break;
+
+	case FUNCTION:
+		musthave(LWORD, 0);
+		t = function_body(yylval.cp, true);
+		break;
+	}
+
+	while ((iop = synio(syniocf)) != NULL) {
+		if (iopn >= NUFILE)
+			yyerror("too many redirections\n");
+		iops[iopn++] = iop;
+	}
+
+	if (iopn == 0) {
+		afree(iops, ATEMP);
+		t->ioact = NULL;
+	} else {
+		iops[iopn++] = NULL;
+		iops = areallocarray(iops, iopn,
+		    sizeof(struct ioword *), ATEMP);
+		t->ioact = iops;
+	}
+
+	if (t->type == TCOM || t->type == TDBRACKET) {
+		XPput(args, NULL);
+		t->args = (char **) XPclose(args);
+		XPput(vars, NULL);
+		t->vars = (char **) XPclose(vars);
+	} else {
+		XPfree(args);
+		XPfree(vars);
+	}
+
+	return t;
+}
+
+static struct op *
+dogroup(void)
+{
+	int c;
+	struct op *list;
+
+	c = token(CONTIN|KEYWORD|ALIAS);
+	/* A {...} can be used instead of do...done for for/select loops
+	 * but not for while/until loops - we don't need to check if it
+	 * is a while loop because it would have been parsed as part of
+	 * the conditional command list...
+	 */
+	if (c == DO)
+		c = DONE;
+	else if (c == '{')
+		c = '}';
+	else
+		syntaxerr(NULL);
+	list = c_list(true);
+	musthave(c, KEYWORD|ALIAS);
+	return list;
+}
+
+static struct op *
+thenpart(void)
+{
+	struct op *t;
+
+	musthave(THEN, KEYWORD|ALIAS);
+	t = newtp(0);
+	t->left = c_list(true);
+	if (t->left == NULL)
+		syntaxerr(NULL);
+	t->right = elsepart();
+	return (t);
+}
+
+static struct op *
+elsepart(void)
+{
+	struct op *t;
+
+	switch (token(KEYWORD|ALIAS|VARASN)) {
+	case ELSE:
+		if ((t = c_list(true)) == NULL)
+			syntaxerr(NULL);
+		return (t);
+
+	case ELIF:
+		t = newtp(TELIF);
+		t->left = c_list(true);
+		t->right = thenpart();
+		return (t);
+
+	default:
+		reject = true;
+	}
+	return NULL;
+}
+
+static struct op *
+caselist(void)
+{
+	struct op *t, *tl;
+	int c;
+
+	c = token(CONTIN|KEYWORD|ALIAS);
+	/* A {...} can be used instead of in...esac for case statements */
+	if (c == IN)
+		c = ESAC;
+	else if (c == '{')
+		c = '}';
+	else
+		syntaxerr(NULL);
+	t = tl = NULL;
+	while ((tpeek(CONTIN|KEYWORD|ESACONLY)) != c) { /* no ALIAS here */
+		struct op *tc = casepart(c);
+		if (tl == NULL)
+			t = tl = tc, tl->right = NULL;
+		else
+			tl->right = tc, tl = tc;
+	}
+	musthave(c, KEYWORD|ALIAS);
+	return (t);
+}
+
+static struct op *
+casepart(int endtok)
+{
+	struct op *t;
+	int c;
+	XPtrV ptns;
+
+	XPinit(ptns, 16);
+	t = newtp(TPAT);
+	c = token(CONTIN|KEYWORD); /* no ALIAS here */
+	if (c != '(')
+		reject = true;
+	do {
+		musthave(LWORD, 0);
+		XPput(ptns, yylval.cp);
+	} while ((c = token(0)) == '|');
+	reject = true;
+	XPput(ptns, NULL);
+	t->vars = (char **) XPclose(ptns);
+	musthave(')', 0);
+
+	t->left = c_list(true);
+	/* Note: Posix requires the ;; */
+	if ((tpeek(CONTIN|KEYWORD|ALIAS)) != endtok)
+		musthave(BREAK, CONTIN|KEYWORD|ALIAS);
+	return (t);
+}
+
+static struct op *
+function_body(char *name,
+    int ksh_func)		/* function foo { ... } vs foo() { .. } */
+{
+	char *sname, *p;
+	struct op *t;
+	int old_func_parse;
+
+	sname = wdstrip(name);
+	/* Check for valid characters in name.  posix and ksh93 say only
+	 * allow [a-zA-Z_0-9] but this allows more as old pdksh's have
+	 * allowed more (the following were never allowed:
+	 *	nul space nl tab $ ' " \ ` ( ) & | ; = < >
+	 *  C_QUOTE covers all but = and adds # [ ? *)
+	 */
+	for (p = sname; *p; p++)
+		if (ctype(*p, C_QUOTE) || *p == '=')
+			yyerror("%s: invalid function name\n", sname);
+
+	t = newtp(TFUNCT);
+	t->str = sname;
+	t->u.ksh_func = ksh_func;
+	t->lineno = source->line;
+
+	/* Note that POSIX allows only compound statements after foo(), sh and
+	 * at&t ksh allow any command, go with the later since it shouldn't
+	 * break anything.  However, for function foo, at&t ksh only accepts
+	 * an open-brace.
+	 */
+	if (ksh_func) {
+		musthave('{', CONTIN|KEYWORD|ALIAS); /* } */
+		reject = true;
+	}
+
+	old_func_parse = genv->flags & EF_FUNC_PARSE;
+	genv->flags |= EF_FUNC_PARSE;
+	if ((t->left = get_command(CONTIN)) == NULL) {
+		/*
+		 * Probably something like foo() followed by eof or ;.
+		 * This is accepted by sh and ksh88.
+		 * To make "typeset -f foo" work reliably (so its output can
+		 * be used as input), we pretend there is a colon here.
+		 */
+		t->left = newtp(TCOM);
+		t->left->args = areallocarray(NULL, 2, sizeof(char *), ATEMP);
+		t->left->args[0] = alloc(3, ATEMP);
+		t->left->args[0][0] = CHAR;
+		t->left->args[0][1] = ':';
+		t->left->args[0][2] = EOS;
+		t->left->args[1] = NULL;
+		t->left->vars = alloc(sizeof(char *), ATEMP);
+		t->left->vars[0] = NULL;
+		t->left->lineno = 1;
+	}
+	if (!old_func_parse)
+		genv->flags &= ~EF_FUNC_PARSE;
+
+	return t;
+}
+
+static char **
+wordlist(void)
+{
+	int c;
+	XPtrV args;
+
+	XPinit(args, 16);
+	/* Posix does not do alias expansion here... */
+	if ((c = token(CONTIN|KEYWORD|ALIAS)) != IN) {
+		if (c != ';') /* non-POSIX, but at&t ksh accepts a ; here */
+			reject = true;
+		return NULL;
+	}
+	while ((c = token(0)) == LWORD)
+		XPput(args, yylval.cp);
+	if (c != '\n' && c != ';')
+		syntaxerr(NULL);
+	XPput(args, NULL);
+	return (char **) XPclose(args);
+}
+
+/*
+ * supporting functions
+ */
+
+static struct op *
+block(int type, struct op *t1, struct op *t2, char **wp)
+{
+	struct op *t;
+
+	t = newtp(type);
+	t->left = t1;
+	t->right = t2;
+	t->vars = wp;
+	return (t);
+}
+
+const	struct tokeninfo {
+	const char *name;
+	short	val;
+	short	reserved;
+} tokentab[] = {
+	/* Reserved words */
+	{ "if",		IF,	true },
+	{ "then",	THEN,	true },
+	{ "else",	ELSE,	true },
+	{ "elif",	ELIF,	true },
+	{ "fi",		FI,	true },
+	{ "case",	CASE,	true },
+	{ "esac",	ESAC,	true },
+	{ "for",	FOR,	true },
+	{ "select",	SELECT,	true },
+	{ "while",	WHILE,	true },
+	{ "until",	UNTIL,	true },
+	{ "do",		DO,	true },
+	{ "done",	DONE,	true },
+	{ "in",		IN,	true },
+	{ "function",	FUNCTION, true },
+	{ "time",	TIME,	true },
+	{ "{",		'{',	true },
+	{ "}",		'}',	true },
+	{ "!",		BANG,	true },
+	{ "[[",		DBRACKET, true },
+	/* Lexical tokens (0[EOF], LWORD and REDIR handled specially) */
+	{ "&&",		LOGAND,	false },
+	{ "||",		LOGOR,	false },
+	{ ";;",		BREAK,	false },
+	{ "((",		MDPAREN, false },
+	{ "|&",		COPROC,	false },
+	/* and some special cases... */
+	{ "newline",	'\n',	false },
+	{ 0 }
+};
+
+void
+initkeywords(void)
+{
+	struct tokeninfo const *tt;
+	struct tbl *p;
+
+	ktinit(&keywords, APERM, 32); /* must be 2^n (currently 20 keywords) */
+	for (tt = tokentab; tt->name; tt++) {
+		if (tt->reserved) {
+			p = ktenter(&keywords, tt->name, hash(tt->name));
+			p->flag |= DEFINED|ISSET;
+			p->type = CKEYWD;
+			p->val.i = tt->val;
+		}
+	}
+}
+
+static void
+syntaxerr(const char *what)
+{
+	char redir[6];	/* 2<<- is the longest redirection, I think */
+	const char *s;
+	struct tokeninfo const *tt;
+	int c;
+
+	if (!what)
+		what = "unexpected";
+	reject = true;
+	c = token(0);
+    Again:
+	switch (c) {
+	case 0:
+		if (nesting.start_token) {
+			c = nesting.start_token;
+			source->errline = nesting.start_line;
+			what = "unmatched";
+			goto Again;
+		}
+		/* don't quote the EOF */
+		yyerror("syntax error: unexpected EOF\n");
+		/* NOTREACHED */
+
+	case LWORD:
+		s = snptreef(NULL, 32, "%S", yylval.cp);
+		break;
+
+	case REDIR:
+		s = snptreef(redir, sizeof(redir), "%R", yylval.iop);
+		break;
+
+	default:
+		for (tt = tokentab; tt->name; tt++)
+			if (tt->val == c)
+			    break;
+		if (tt->name)
+			s = tt->name;
+		else {
+			if (c > 0 && c < 256) {
+				redir[0] = c;
+				redir[1] = '\0';
+			} else
+				shf_snprintf(redir, sizeof(redir),
+					"?%d", c);
+			s = redir;
+		}
+	}
+	yyerror("syntax error: `%s' %s\n", s, what);
+}
+
+static void
+nesting_push(struct nesting_state *save, int tok)
+{
+	*save = nesting;
+	nesting.start_token = tok;
+	nesting.start_line = source->line;
+}
+
+static void
+nesting_pop(struct nesting_state *saved)
+{
+	nesting = *saved;
+}
+
+static struct op *
+newtp(int type)
+{
+	struct op *t;
+
+	t = alloc(sizeof(*t), ATEMP);
+	t->type = type;
+	t->u.evalflags = 0;
+	t->args = t->vars = NULL;
+	t->ioact = NULL;
+	t->left = t->right = NULL;
+	t->str = NULL;
+	return (t);
+}
+
+struct op *
+compile(Source *s)
+{
+	nesting.start_token = 0;
+	nesting.start_line = 0;
+	herep = heres;
+	source = s;
+	yyparse();
+	return outtree;
+}
+
+/* This kludge exists to take care of sh/at&t ksh oddity in which
+ * the arguments of alias/export/readonly/typeset have no field
+ * splitting, file globbing, or (normal) tilde expansion done.
+ * at&t ksh seems to do something similar to this since
+ *	$ touch a=a; typeset a=[ab]; echo "$a"
+ *	a=[ab]
+ *	$ x=typeset; $x a=[ab]; echo "$a"
+ *	a=a
+ *	$
+ */
+static int
+assign_command(char *s)
+{
+	if (Flag(FPOSIX) || !*s)
+		return 0;
+	return (strcmp(s, "alias") == 0) ||
+	    (strcmp(s, "export") == 0) ||
+	    (strcmp(s, "readonly") == 0) ||
+	    (strcmp(s, "typeset") == 0);
+}
+
+/* Check if we are in the middle of reading an alias */
+static int
+inalias(struct source *s)
+{
+	for (; s && s->type == SALIAS; s = s->next)
+		if (!(s->flags & SF_ALIASEND))
+			return 1;
+	return 0;
+}
+
+
+/* Order important - indexed by Test_meta values
+ * Note that ||, &&, ( and ) can't appear in as unquoted strings
+ * in normal shell input, so these can be interpreted unambiguously
+ * in the evaluation pass.
+ */
+static const char dbtest_or[] = { CHAR, '|', CHAR, '|', EOS };
+static const char dbtest_and[] = { CHAR, '&', CHAR, '&', EOS };
+static const char dbtest_not[] = { CHAR, '!', EOS };
+static const char dbtest_oparen[] = { CHAR, '(', EOS };
+static const char dbtest_cparen[] = { CHAR, ')', EOS };
+const char *const dbtest_tokens[] = {
+	dbtest_or, dbtest_and, dbtest_not,
+	dbtest_oparen, dbtest_cparen
+};
+const char db_close[] = { CHAR, ']', CHAR, ']', EOS };
+const char db_lthan[] = { CHAR, '<', EOS };
+const char db_gthan[] = { CHAR, '>', EOS };
+
+/* Test if the current token is a whatever.  Accepts the current token if
+ * it is.  Returns 0 if it is not, non-zero if it is (in the case of
+ * TM_UNOP and TM_BINOP, the returned value is a Test_op).
+ */
+static int
+dbtestp_isa(Test_env *te, Test_meta meta)
+{
+	int c = tpeek(ARRAYVAR | (meta == TM_BINOP ? 0 : CONTIN));
+	int uqword = 0;
+	char *save = NULL;
+	int ret = 0;
+
+	/* unquoted word? */
+	uqword = c == LWORD && *ident;
+
+	if (meta == TM_OR)
+		ret = c == LOGOR;
+	else if (meta == TM_AND)
+		ret = c == LOGAND;
+	else if (meta == TM_NOT)
+		ret = uqword && strcmp(yylval.cp, dbtest_tokens[(int) TM_NOT]) == 0;
+	else if (meta == TM_OPAREN)
+		ret = c == '(' /*)*/;
+	else if (meta == TM_CPAREN)
+		ret = c == /*(*/ ')';
+	else if (meta == TM_UNOP || meta == TM_BINOP) {
+		if (meta == TM_BINOP && c == REDIR &&
+		    (yylval.iop->flag == IOREAD || yylval.iop->flag == IOWRITE)) {
+			ret = 1;
+			save = wdcopy(yylval.iop->flag == IOREAD ?
+			    db_lthan : db_gthan, ATEMP);
+		} else if (uqword && (ret = (int) test_isop(te, meta, ident)))
+			save = yylval.cp;
+	} else /* meta == TM_END */
+		ret = uqword && strcmp(yylval.cp, db_close) == 0;
+	if (ret) {
+		reject = false;
+		if (meta != TM_END) {
+			if (!save)
+				save = wdcopy(dbtest_tokens[(int) meta], ATEMP);
+			XPput(*te->pos.av, save);
+		}
+	}
+	return ret;
+}
+
+static const char *
+dbtestp_getopnd(Test_env *te, Test_op op, int do_eval)
+{
+	int c = tpeek(ARRAYVAR);
+
+	if (c != LWORD)
+		return NULL;
+
+	reject = false;
+	XPput(*te->pos.av, yylval.cp);
+
+	return null;
+}
+
+static int
+dbtestp_eval(Test_env *te, Test_op op, const char *opnd1, const char *opnd2,
+    int do_eval)
+{
+	return 1;
+}
+
+static void
+dbtestp_error(Test_env *te, int offset, const char *msg)
+{
+	te->flags |= TEF_ERROR;
+
+	if (offset < 0) {
+		reject = true;
+		/* Kludgy to say the least... */
+		symbol = LWORD;
+		yylval.cp = *(XPptrv(*te->pos.av) + XPsize(*te->pos.av) +
+		    offset);
+	}
+	syntaxerr(msg);
+}
diff --git a/sys/queue.h b/sys/queue.h
@@ -0,0 +1,534 @@
+/*	$OpenBSD: queue.h,v 1.45 2018/07/12 14:22:54 sashan Exp $	*/
+/*	$NetBSD: queue.h,v 1.11 1996/05/16 05:17:14 mycroft Exp $	*/
+
+/*
+ * Copyright (c) 1991, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * 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 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)queue.h	8.5 (Berkeley) 8/20/94
+ */
+
+#ifndef	_SYS_QUEUE_H_
+#define	_SYS_QUEUE_H_
+
+/*
+ * This file defines five types of data structures: singly-linked lists,
+ * lists, simple queues, tail queues and XOR simple queues.
+ *
+ *
+ * A singly-linked list is headed by a single forward pointer. The elements
+ * are singly linked for minimum space and pointer manipulation overhead at
+ * the expense of O(n) removal for arbitrary elements. New elements can be
+ * added to the list after an existing element or at the head of the list.
+ * Elements being removed from the head of the list should use the explicit
+ * macro for this purpose for optimum efficiency. A singly-linked list may
+ * only be traversed in the forward direction.  Singly-linked lists are ideal
+ * for applications with large datasets and few or no removals or for
+ * implementing a LIFO queue.
+ *
+ * A list is headed by a single forward pointer (or an array of forward
+ * pointers for a hash table header). The elements are doubly linked
+ * so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before
+ * or after an existing element or at the head of the list. A list
+ * may only be traversed in the forward direction.
+ *
+ * A simple queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are singly
+ * linked to save space, so elements can only be removed from the
+ * head of the list. New elements can be added to the list before or after
+ * an existing element, at the head of the list, or at the end of the
+ * list. A simple queue may only be traversed in the forward direction.
+ *
+ * A tail queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are doubly
+ * linked so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before or
+ * after an existing element, at the head of the list, or at the end of
+ * the list. A tail queue may be traversed in either direction.
+ *
+ * An XOR simple queue is used in the same way as a regular simple queue.
+ * The difference is that the head structure also includes a "cookie" that
+ * is XOR'd with the queue pointer (first, last or next) to generate the
+ * real pointer value.
+ *
+ * For details on the use of these macros, see the queue(3) manual page.
+ */
+
+#if defined(QUEUE_MACRO_DEBUG) || (defined(_KERNEL) && defined(DIAGNOSTIC))
+#define _Q_INVALID ((void *)-1)
+#define _Q_INVALIDATE(a) (a) = _Q_INVALID
+#else
+#define _Q_INVALIDATE(a)
+#endif
+
+/*
+ * Singly-linked List definitions.
+ */
+#define SLIST_HEAD(name, type)						\
+struct name {								\
+	struct type *slh_first;	/* first element */			\
+}
+
+#define	SLIST_HEAD_INITIALIZER(head)					\
+	{ NULL }
+
+#define SLIST_ENTRY(type)						\
+struct {								\
+	struct type *sle_next;	/* next element */			\
+}
+
+/*
+ * Singly-linked List access methods.
+ */
+#define	SLIST_FIRST(head)	((head)->slh_first)
+#define	SLIST_END(head)		NULL
+#define	SLIST_EMPTY(head)	(SLIST_FIRST(head) == SLIST_END(head))
+#define	SLIST_NEXT(elm, field)	((elm)->field.sle_next)
+
+#define	SLIST_FOREACH(var, head, field)					\
+	for((var) = SLIST_FIRST(head);					\
+	    (var) != SLIST_END(head);					\
+	    (var) = SLIST_NEXT(var, field))
+
+#define	SLIST_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = SLIST_FIRST(head);				\
+	    (var) && ((tvar) = SLIST_NEXT(var, field), 1);		\
+	    (var) = (tvar))
+
+/*
+ * Singly-linked List functions.
+ */
+#define	SLIST_INIT(head) {						\
+	SLIST_FIRST(head) = SLIST_END(head);				\
+}
+
+#define	SLIST_INSERT_AFTER(slistelm, elm, field) do {			\
+	(elm)->field.sle_next = (slistelm)->field.sle_next;		\
+	(slistelm)->field.sle_next = (elm);				\
+} while (0)
+
+#define	SLIST_INSERT_HEAD(head, elm, field) do {			\
+	(elm)->field.sle_next = (head)->slh_first;			\
+	(head)->slh_first = (elm);					\
+} while (0)
+
+#define	SLIST_REMOVE_AFTER(elm, field) do {				\
+	(elm)->field.sle_next = (elm)->field.sle_next->field.sle_next;	\
+} while (0)
+
+#define	SLIST_REMOVE_HEAD(head, field) do {				\
+	(head)->slh_first = (head)->slh_first->field.sle_next;		\
+} while (0)
+
+#define SLIST_REMOVE(head, elm, type, field) do {			\
+	if ((head)->slh_first == (elm)) {				\
+		SLIST_REMOVE_HEAD((head), field);			\
+	} else {							\
+		struct type *curelm = (head)->slh_first;		\
+									\
+		while (curelm->field.sle_next != (elm))			\
+			curelm = curelm->field.sle_next;		\
+		curelm->field.sle_next =				\
+		    curelm->field.sle_next->field.sle_next;		\
+	}								\
+	_Q_INVALIDATE((elm)->field.sle_next);				\
+} while (0)
+
+/*
+ * List definitions.
+ */
+#define LIST_HEAD(name, type)						\
+struct name {								\
+	struct type *lh_first;	/* first element */			\
+}
+
+#define LIST_HEAD_INITIALIZER(head)					\
+	{ NULL }
+
+#define LIST_ENTRY(type)						\
+struct {								\
+	struct type *le_next;	/* next element */			\
+	struct type **le_prev;	/* address of previous next element */	\
+}
+
+/*
+ * List access methods.
+ */
+#define	LIST_FIRST(head)		((head)->lh_first)
+#define	LIST_END(head)			NULL
+#define	LIST_EMPTY(head)		(LIST_FIRST(head) == LIST_END(head))
+#define	LIST_NEXT(elm, field)		((elm)->field.le_next)
+
+#define LIST_FOREACH(var, head, field)					\
+	for((var) = LIST_FIRST(head);					\
+	    (var)!= LIST_END(head);					\
+	    (var) = LIST_NEXT(var, field))
+
+#define	LIST_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = LIST_FIRST(head);				\
+	    (var) && ((tvar) = LIST_NEXT(var, field), 1);		\
+	    (var) = (tvar))
+
+/*
+ * List functions.
+ */
+#define	LIST_INIT(head) do {						\
+	LIST_FIRST(head) = LIST_END(head);				\
+} while (0)
+
+#define LIST_INSERT_AFTER(listelm, elm, field) do {			\
+	if (((elm)->field.le_next = (listelm)->field.le_next) != NULL)	\
+		(listelm)->field.le_next->field.le_prev =		\
+		    &(elm)->field.le_next;				\
+	(listelm)->field.le_next = (elm);				\
+	(elm)->field.le_prev = &(listelm)->field.le_next;		\
+} while (0)
+
+#define	LIST_INSERT_BEFORE(listelm, elm, field) do {			\
+	(elm)->field.le_prev = (listelm)->field.le_prev;		\
+	(elm)->field.le_next = (listelm);				\
+	*(listelm)->field.le_prev = (elm);				\
+	(listelm)->field.le_prev = &(elm)->field.le_next;		\
+} while (0)
+
+#define LIST_INSERT_HEAD(head, elm, field) do {				\
+	if (((elm)->field.le_next = (head)->lh_first) != NULL)		\
+		(head)->lh_first->field.le_prev = &(elm)->field.le_next;\
+	(head)->lh_first = (elm);					\
+	(elm)->field.le_prev = &(head)->lh_first;			\
+} while (0)
+
+#define LIST_REMOVE(elm, field) do {					\
+	if ((elm)->field.le_next != NULL)				\
+		(elm)->field.le_next->field.le_prev =			\
+		    (elm)->field.le_prev;				\
+	*(elm)->field.le_prev = (elm)->field.le_next;			\
+	_Q_INVALIDATE((elm)->field.le_prev);				\
+	_Q_INVALIDATE((elm)->field.le_next);				\
+} while (0)
+
+#define LIST_REPLACE(elm, elm2, field) do {				\
+	if (((elm2)->field.le_next = (elm)->field.le_next) != NULL)	\
+		(elm2)->field.le_next->field.le_prev =			\
+		    &(elm2)->field.le_next;				\
+	(elm2)->field.le_prev = (elm)->field.le_prev;			\
+	*(elm2)->field.le_prev = (elm2);				\
+	_Q_INVALIDATE((elm)->field.le_prev);				\
+	_Q_INVALIDATE((elm)->field.le_next);				\
+} while (0)
+
+/*
+ * Simple queue definitions.
+ */
+#define SIMPLEQ_HEAD(name, type)					\
+struct name {								\
+	struct type *sqh_first;	/* first element */			\
+	struct type **sqh_last;	/* addr of last next element */		\
+}
+
+#define SIMPLEQ_HEAD_INITIALIZER(head)					\
+	{ NULL, &(head).sqh_first }
+
+#define SIMPLEQ_ENTRY(type)						\
+struct {								\
+	struct type *sqe_next;	/* next element */			\
+}
+
+/*
+ * Simple queue access methods.
+ */
+#define	SIMPLEQ_FIRST(head)	    ((head)->sqh_first)
+#define	SIMPLEQ_END(head)	    NULL
+#define	SIMPLEQ_EMPTY(head)	    (SIMPLEQ_FIRST(head) == SIMPLEQ_END(head))
+#define	SIMPLEQ_NEXT(elm, field)    ((elm)->field.sqe_next)
+
+#define SIMPLEQ_FOREACH(var, head, field)				\
+	for((var) = SIMPLEQ_FIRST(head);				\
+	    (var) != SIMPLEQ_END(head);					\
+	    (var) = SIMPLEQ_NEXT(var, field))
+
+#define	SIMPLEQ_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = SIMPLEQ_FIRST(head);				\
+	    (var) && ((tvar) = SIMPLEQ_NEXT(var, field), 1);		\
+	    (var) = (tvar))
+
+/*
+ * Simple queue functions.
+ */
+#define	SIMPLEQ_INIT(head) do {						\
+	(head)->sqh_first = NULL;					\
+	(head)->sqh_last = &(head)->sqh_first;				\
+} while (0)
+
+#define SIMPLEQ_INSERT_HEAD(head, elm, field) do {			\
+	if (((elm)->field.sqe_next = (head)->sqh_first) == NULL)	\
+		(head)->sqh_last = &(elm)->field.sqe_next;		\
+	(head)->sqh_first = (elm);					\
+} while (0)
+
+#define SIMPLEQ_INSERT_TAIL(head, elm, field) do {			\
+	(elm)->field.sqe_next = NULL;					\
+	*(head)->sqh_last = (elm);					\
+	(head)->sqh_last = &(elm)->field.sqe_next;			\
+} while (0)
+
+#define SIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	if (((elm)->field.sqe_next = (listelm)->field.sqe_next) == NULL)\
+		(head)->sqh_last = &(elm)->field.sqe_next;		\
+	(listelm)->field.sqe_next = (elm);				\
+} while (0)
+
+#define SIMPLEQ_REMOVE_HEAD(head, field) do {			\
+	if (((head)->sqh_first = (head)->sqh_first->field.sqe_next) == NULL) \
+		(head)->sqh_last = &(head)->sqh_first;			\
+} while (0)
+
+#define SIMPLEQ_REMOVE_AFTER(head, elm, field) do {			\
+	if (((elm)->field.sqe_next = (elm)->field.sqe_next->field.sqe_next) \
+	    == NULL)							\
+		(head)->sqh_last = &(elm)->field.sqe_next;		\
+} while (0)
+
+#define SIMPLEQ_CONCAT(head1, head2) do {				\
+	if (!SIMPLEQ_EMPTY((head2))) {					\
+		*(head1)->sqh_last = (head2)->sqh_first;		\
+		(head1)->sqh_last = (head2)->sqh_last;			\
+		SIMPLEQ_INIT((head2));					\
+	}								\
+} while (0)
+
+/*
+ * XOR Simple queue definitions.
+ */
+#define XSIMPLEQ_HEAD(name, type)					\
+struct name {								\
+	struct type *sqx_first;	/* first element */			\
+	struct type **sqx_last;	/* addr of last next element */		\
+	unsigned long sqx_cookie;					\
+}
+
+#define XSIMPLEQ_ENTRY(type)						\
+struct {								\
+	struct type *sqx_next;	/* next element */			\
+}
+
+/*
+ * XOR Simple queue access methods.
+ */
+#define XSIMPLEQ_XOR(head, ptr)	    ((__typeof(ptr))((head)->sqx_cookie ^ \
+					(unsigned long)(ptr)))
+#define	XSIMPLEQ_FIRST(head)	    XSIMPLEQ_XOR(head, ((head)->sqx_first))
+#define	XSIMPLEQ_END(head)	    NULL
+#define	XSIMPLEQ_EMPTY(head)	    (XSIMPLEQ_FIRST(head) == XSIMPLEQ_END(head))
+#define	XSIMPLEQ_NEXT(head, elm, field)    XSIMPLEQ_XOR(head, ((elm)->field.sqx_next))
+
+
+#define XSIMPLEQ_FOREACH(var, head, field)				\
+	for ((var) = XSIMPLEQ_FIRST(head);				\
+	    (var) != XSIMPLEQ_END(head);				\
+	    (var) = XSIMPLEQ_NEXT(head, var, field))
+
+#define	XSIMPLEQ_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = XSIMPLEQ_FIRST(head);				\
+	    (var) && ((tvar) = XSIMPLEQ_NEXT(head, var, field), 1);	\
+	    (var) = (tvar))
+
+/*
+ * XOR Simple queue functions.
+ */
+#define	XSIMPLEQ_INIT(head) do {					\
+	arc4random_buf(&(head)->sqx_cookie, sizeof((head)->sqx_cookie)); \
+	(head)->sqx_first = XSIMPLEQ_XOR(head, NULL);			\
+	(head)->sqx_last = XSIMPLEQ_XOR(head, &(head)->sqx_first);	\
+} while (0)
+
+#define XSIMPLEQ_INSERT_HEAD(head, elm, field) do {			\
+	if (((elm)->field.sqx_next = (head)->sqx_first) ==		\
+	    XSIMPLEQ_XOR(head, NULL))					\
+		(head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next); \
+	(head)->sqx_first = XSIMPLEQ_XOR(head, (elm));			\
+} while (0)
+
+#define XSIMPLEQ_INSERT_TAIL(head, elm, field) do {			\
+	(elm)->field.sqx_next = XSIMPLEQ_XOR(head, NULL);		\
+	*(XSIMPLEQ_XOR(head, (head)->sqx_last)) = XSIMPLEQ_XOR(head, (elm)); \
+	(head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next);	\
+} while (0)
+
+#define XSIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	if (((elm)->field.sqx_next = (listelm)->field.sqx_next) ==	\
+	    XSIMPLEQ_XOR(head, NULL))					\
+		(head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next); \
+	(listelm)->field.sqx_next = XSIMPLEQ_XOR(head, (elm));		\
+} while (0)
+
+#define XSIMPLEQ_REMOVE_HEAD(head, field) do {				\
+	if (((head)->sqx_first = XSIMPLEQ_XOR(head,			\
+	    (head)->sqx_first)->field.sqx_next) == XSIMPLEQ_XOR(head, NULL)) \
+		(head)->sqx_last = XSIMPLEQ_XOR(head, &(head)->sqx_first); \
+} while (0)
+
+#define XSIMPLEQ_REMOVE_AFTER(head, elm, field) do {			\
+	if (((elm)->field.sqx_next = XSIMPLEQ_XOR(head,			\
+	    (elm)->field.sqx_next)->field.sqx_next)			\
+	    == XSIMPLEQ_XOR(head, NULL))				\
+		(head)->sqx_last = 					\
+		    XSIMPLEQ_XOR(head, &(elm)->field.sqx_next);		\
+} while (0)
+
+
+/*
+ * Tail queue definitions.
+ */
+#define TAILQ_HEAD(name, type)						\
+struct name {								\
+	struct type *tqh_first;	/* first element */			\
+	struct type **tqh_last;	/* addr of last next element */		\
+}
+
+#define TAILQ_HEAD_INITIALIZER(head)					\
+	{ NULL, &(head).tqh_first }
+
+#define TAILQ_ENTRY(type)						\
+struct {								\
+	struct type *tqe_next;	/* next element */			\
+	struct type **tqe_prev;	/* address of previous next element */	\
+}
+
+/*
+ * Tail queue access methods.
+ */
+#define	TAILQ_FIRST(head)		((head)->tqh_first)
+#define	TAILQ_END(head)			NULL
+#define	TAILQ_NEXT(elm, field)		((elm)->field.tqe_next)
+#define TAILQ_LAST(head, headname)					\
+	(*(((struct headname *)((head)->tqh_last))->tqh_last))
+/* XXX */
+#define TAILQ_PREV(elm, headname, field)				\
+	(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
+#define	TAILQ_EMPTY(head)						\
+	(TAILQ_FIRST(head) == TAILQ_END(head))
+
+#define TAILQ_FOREACH(var, head, field)					\
+	for((var) = TAILQ_FIRST(head);					\
+	    (var) != TAILQ_END(head);					\
+	    (var) = TAILQ_NEXT(var, field))
+
+#define	TAILQ_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = TAILQ_FIRST(head);					\
+	    (var) != TAILQ_END(head) &&					\
+	    ((tvar) = TAILQ_NEXT(var, field), 1);			\
+	    (var) = (tvar))
+
+
+#define TAILQ_FOREACH_REVERSE(var, head, headname, field)		\
+	for((var) = TAILQ_LAST(head, headname);				\
+	    (var) != TAILQ_END(head);					\
+	    (var) = TAILQ_PREV(var, headname, field))
+
+#define	TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar)	\
+	for ((var) = TAILQ_LAST(head, headname);			\
+	    (var) != TAILQ_END(head) &&					\
+	    ((tvar) = TAILQ_PREV(var, headname, field), 1);		\
+	    (var) = (tvar))
+
+/*
+ * Tail queue functions.
+ */
+#define	TAILQ_INIT(head) do {						\
+	(head)->tqh_first = NULL;					\
+	(head)->tqh_last = &(head)->tqh_first;				\
+} while (0)
+
+#define TAILQ_INSERT_HEAD(head, elm, field) do {			\
+	if (((elm)->field.tqe_next = (head)->tqh_first) != NULL)	\
+		(head)->tqh_first->field.tqe_prev =			\
+		    &(elm)->field.tqe_next;				\
+	else								\
+		(head)->tqh_last = &(elm)->field.tqe_next;		\
+	(head)->tqh_first = (elm);					\
+	(elm)->field.tqe_prev = &(head)->tqh_first;			\
+} while (0)
+
+#define TAILQ_INSERT_TAIL(head, elm, field) do {			\
+	(elm)->field.tqe_next = NULL;					\
+	(elm)->field.tqe_prev = (head)->tqh_last;			\
+	*(head)->tqh_last = (elm);					\
+	(head)->tqh_last = &(elm)->field.tqe_next;			\
+} while (0)
+
+#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	if (((elm)->field.tqe_next = (listelm)->field.tqe_next) != NULL)\
+		(elm)->field.tqe_next->field.tqe_prev =			\
+		    &(elm)->field.tqe_next;				\
+	else								\
+		(head)->tqh_last = &(elm)->field.tqe_next;		\
+	(listelm)->field.tqe_next = (elm);				\
+	(elm)->field.tqe_prev = &(listelm)->field.tqe_next;		\
+} while (0)
+
+#define	TAILQ_INSERT_BEFORE(listelm, elm, field) do {			\
+	(elm)->field.tqe_prev = (listelm)->field.tqe_prev;		\
+	(elm)->field.tqe_next = (listelm);				\
+	*(listelm)->field.tqe_prev = (elm);				\
+	(listelm)->field.tqe_prev = &(elm)->field.tqe_next;		\
+} while (0)
+
+#define TAILQ_REMOVE(head, elm, field) do {				\
+	if (((elm)->field.tqe_next) != NULL)				\
+		(elm)->field.tqe_next->field.tqe_prev =			\
+		    (elm)->field.tqe_prev;				\
+	else								\
+		(head)->tqh_last = (elm)->field.tqe_prev;		\
+	*(elm)->field.tqe_prev = (elm)->field.tqe_next;			\
+	_Q_INVALIDATE((elm)->field.tqe_prev);				\
+	_Q_INVALIDATE((elm)->field.tqe_next);				\
+} while (0)
+
+#define TAILQ_REPLACE(head, elm, elm2, field) do {			\
+	if (((elm2)->field.tqe_next = (elm)->field.tqe_next) != NULL)	\
+		(elm2)->field.tqe_next->field.tqe_prev =		\
+		    &(elm2)->field.tqe_next;				\
+	else								\
+		(head)->tqh_last = &(elm2)->field.tqe_next;		\
+	(elm2)->field.tqe_prev = (elm)->field.tqe_prev;			\
+	*(elm2)->field.tqe_prev = (elm2);				\
+	_Q_INVALIDATE((elm)->field.tqe_prev);				\
+	_Q_INVALIDATE((elm)->field.tqe_next);				\
+} while (0)
+
+#define TAILQ_CONCAT(head1, head2, field) do {				\
+	if (!TAILQ_EMPTY(head2)) {					\
+		*(head1)->tqh_last = (head2)->tqh_first;		\
+		(head2)->tqh_first->field.tqe_prev = (head1)->tqh_last;	\
+		(head1)->tqh_last = (head2)->tqh_last;			\
+		TAILQ_INIT((head2));					\
+	}								\
+} while (0)
+
+#endif	/* !_SYS_QUEUE_H_ */
diff --git a/sys/time.h b/sys/time.h
@@ -0,0 +1,79 @@
+#ifndef _COMPAT_SYS_TIME_H_
+#define _COMPAT_SYS_TIME_H_
+
+/*	$OpenBSD: time.h,v 1.40 2019/01/19 01:53:44 cheloha Exp $	*/
+/*	$NetBSD: time.h,v 1.18 1996/04/23 10:29:33 mycroft Exp $	*/
+
+/*
+ * Copyright (c) 1982, 1986, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * 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 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)time.h	8.2 (Berkeley) 7/10/94
+ */
+
+/* Operations on timevals. */
+#define	timerclear(tvp)		(tvp)->tv_sec = (tvp)->tv_usec = 0
+#define	timerisset(tvp)		((tvp)->tv_sec || (tvp)->tv_usec)
+#define	timercmp(tvp, uvp, cmp)						\
+	(((tvp)->tv_sec == (uvp)->tv_sec) ?				\
+	    ((tvp)->tv_usec cmp (uvp)->tv_usec) :			\
+	    ((tvp)->tv_sec cmp (uvp)->tv_sec))
+#define	timeradd(tvp, uvp, vvp)						\
+	do {								\
+		(vvp)->tv_sec = (tvp)->tv_sec + (uvp)->tv_sec;		\
+		(vvp)->tv_usec = (tvp)->tv_usec + (uvp)->tv_usec;	\
+		if ((vvp)->tv_usec >= 1000000) {			\
+			(vvp)->tv_sec++;				\
+			(vvp)->tv_usec -= 1000000;			\
+		}							\
+	} while (0)
+#define	timersub(tvp, uvp, vvp)						\
+	do {								\
+		(vvp)->tv_sec = (tvp)->tv_sec - (uvp)->tv_sec;		\
+		(vvp)->tv_usec = (tvp)->tv_usec - (uvp)->tv_usec;	\
+		if ((vvp)->tv_usec < 0) {				\
+			(vvp)->tv_sec--;				\
+			(vvp)->tv_usec += 1000000;			\
+		}							\
+	} while (0)
+
+/* Operations on timespecs. */
+#define	timespeccmp(tsp, usp, cmp)					\
+	(((tsp)->tv_sec == (usp)->tv_sec) ?				\
+	    ((tsp)->tv_nsec cmp (usp)->tv_nsec) :			\
+	    ((tsp)->tv_sec cmp (usp)->tv_sec))
+#define	timespecsub(tsp, usp, vsp)					\
+	do {								\
+		(vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec;		\
+		(vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec;	\
+		if ((vsp)->tv_nsec < 0) {				\
+			(vsp)->tv_sec--;				\
+			(vsp)->tv_nsec += 1000000000L;			\
+		}							\
+	} while (0)
+
+#endif
diff --git a/table.c b/table.c
@@ -0,0 +1,249 @@
+/*	$OpenBSD: table.c,v 1.25 2018/01/16 22:52:32 jca Exp $	*/
+
+/*
+ * dynamic hashed associative table for commands and variables
+ */
+
+#include <limits.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "sh.h"
+
+#define	INIT_TBLS	8	/* initial table size (power of 2) */
+
+struct table taliases;	/* tracked aliases */
+struct table builtins;	/* built-in commands */
+struct table aliases;	/* aliases */
+struct table keywords;	/* keywords */
+struct table homedirs;	/* homedir() cache */
+
+char *search_path;	/* copy of either PATH or def_path */
+const char *def_path;	/* path to use if PATH not set */
+char *tmpdir;		/* TMPDIR value */
+const char *prompt;
+int cur_prompt;		/* PS1 or PS2 */
+int current_lineno;	/* LINENO value */
+
+static void	texpand(struct table *, int);
+static int	tnamecmp(const void *, const void *);
+
+
+unsigned int
+hash(const char *n)
+{
+	unsigned int h = 0;
+
+	while (*n != '\0')
+		h = 33*h + (unsigned char)(*n++);
+	return h;
+}
+
+void
+ktinit(struct table *tp, Area *ap, int tsize)
+{
+	tp->areap = ap;
+	tp->tbls = NULL;
+	tp->size = tp->nfree = 0;
+	if (tsize)
+		texpand(tp, tsize);
+}
+
+static void
+texpand(struct table *tp, int nsize)
+{
+	int i;
+	struct tbl *tblp, **p;
+	struct tbl **ntblp, **otblp = tp->tbls;
+	int osize = tp->size;
+
+	ntblp = areallocarray(NULL, nsize, sizeof(struct tbl *), tp->areap);
+	for (i = 0; i < nsize; i++)
+		ntblp[i] = NULL;
+	tp->size = nsize;
+	tp->nfree = 7*nsize/10;	/* table can get 70% full */
+	tp->tbls = ntblp;
+	if (otblp == NULL)
+		return;
+	for (i = 0; i < osize; i++)
+		if ((tblp = otblp[i]) != NULL) {
+			if ((tblp->flag&DEFINED)) {
+				for (p = &ntblp[hash(tblp->name) &
+				    (tp->size-1)]; *p != NULL; p--)
+					if (p == ntblp) /* wrap */
+						p += tp->size;
+				*p = tblp;
+				tp->nfree--;
+			} else if (!(tblp->flag & FINUSE)) {
+				afree(tblp, tp->areap);
+			}
+		}
+	afree(otblp, tp->areap);
+}
+
+/* table */
+/* name to enter */
+/* hash(n) */
+struct tbl *
+ktsearch(struct table *tp, const char *n, unsigned int h)
+{
+	struct tbl **pp, *p;
+
+	if (tp->size == 0)
+		return NULL;
+
+	/* search for name in hashed table */
+	for (pp = &tp->tbls[h & (tp->size-1)]; (p = *pp) != NULL; pp--) {
+		if (*p->name == *n && strcmp(p->name, n) == 0 &&
+		    (p->flag&DEFINED))
+			return p;
+		if (pp == tp->tbls) /* wrap */
+			pp += tp->size;
+	}
+
+	return NULL;
+}
+
+/* table */
+/* name to enter */
+/* hash(n) */
+struct tbl *
+ktenter(struct table *tp, const char *n, unsigned int h)
+{
+	struct tbl **pp, *p;
+	int len;
+
+	if (tp->size == 0)
+		texpand(tp, INIT_TBLS);
+  Search:
+	/* search for name in hashed table */
+	for (pp = &tp->tbls[h & (tp->size-1)]; (p = *pp) != NULL; pp--) {
+		if (*p->name == *n && strcmp(p->name, n) == 0)
+			return p;	/* found */
+		if (pp == tp->tbls) /* wrap */
+			pp += tp->size;
+	}
+
+	if (tp->nfree <= 0) {	/* too full */
+		if (tp->size <= INT_MAX/2)
+			texpand(tp, 2*tp->size);
+		else
+			internal_errorf("too many vars");
+		goto Search;
+	}
+
+	/* create new tbl entry */
+	len = strlen(n) + 1;
+	p = alloc(offsetof(struct tbl, name[0]) + len,
+				 tp->areap);
+	p->flag = 0;
+	p->type = 0;
+	p->areap = tp->areap;
+	p->u2.field = 0;
+	p->u.array = NULL;
+	memcpy(p->name, n, len);
+
+	/* enter in tp->tbls */
+	tp->nfree--;
+	*pp = p;
+	return p;
+}
+
+void
+ktdelete(struct tbl *p)
+{
+	p->flag = 0;
+}
+
+void
+ktwalk(struct tstate *ts, struct table *tp)
+{
+	ts->left = tp->size;
+	ts->next = tp->tbls;
+}
+
+struct tbl *
+ktnext(struct tstate *ts)
+{
+	while (--ts->left >= 0) {
+		struct tbl *p = *ts->next++;
+		if (p != NULL && (p->flag&DEFINED))
+			return p;
+	}
+	return NULL;
+}
+
+static int
+tnamecmp(const void *p1, const void *p2)
+{
+	char *name1 = (*(struct tbl **)p1)->name;
+	char *name2 = (*(struct tbl **)p2)->name;
+	return strcmp(name1, name2);
+}
+
+struct tbl **
+ktsort(struct table *tp)
+{
+	int i;
+	struct tbl **p, **sp, **dp;
+
+	p = areallocarray(NULL, tp->size + 1,
+	    sizeof(struct tbl *), ATEMP);
+	sp = tp->tbls;		/* source */
+	dp = p;			/* dest */
+	for (i = 0; i < tp->size; i++)
+		if ((*dp = *sp++) != NULL && (((*dp)->flag&DEFINED) ||
+		    ((*dp)->flag&ARRAY)))
+			dp++;
+	i = dp - p;
+	qsortp((void**)p, (size_t)i, tnamecmp);
+	p[i] = NULL;
+	return p;
+}
+
+#ifdef PERF_DEBUG /* performance debugging */
+
+void tprintinfo(struct table *tp);
+
+void
+tprintinfo(struct table *tp)
+{
+	struct tbl *te;
+	char *n;
+	unsigned int h;
+	int ncmp;
+	int totncmp = 0, maxncmp = 0;
+	int nentries = 0;
+	struct tstate ts;
+
+	shellf("table size %d, nfree %d\n", tp->size, tp->nfree);
+	shellf("    Ncmp name\n");
+	ktwalk(&ts, tp);
+	while ((te = ktnext(&ts))) {
+		struct tbl **pp, *p;
+
+		h = hash(n = te->name);
+		ncmp = 0;
+
+		/* taken from ktsearch() and added counter */
+		for (pp = &tp->tbls[h & (tp->size-1)]; (p = *pp); pp--) {
+			ncmp++;
+			if (*p->name == *n && strcmp(p->name, n) == 0 &&
+			    (p->flag&DEFINED))
+				break; /* return p; */
+			if (pp == tp->tbls) /* wrap */
+				pp += tp->size;
+		}
+		shellf("    %4d %s\n", ncmp, n);
+		totncmp += ncmp;
+		nentries++;
+		if (ncmp > maxncmp)
+			maxncmp = ncmp;
+	}
+	if (nentries)
+		shellf("  %d entries, worst ncmp %d, avg ncmp %d.%02d\n",
+		    nentries, maxncmp,
+		    totncmp / nentries,
+		    (totncmp % nentries) * 100 / nentries);
+}
+#endif /* PERF_DEBUG */
diff --git a/table.h b/table.h
@@ -0,0 +1,195 @@
+/*	$OpenBSD: table.h,v 1.15 2018/06/18 17:03:58 millert Exp $	*/
+
+/* $From: table.h,v 1.3 1994/05/31 13:34:34 michael Exp $ */
+
+#include <inttypes.h>
+
+/*
+ * generic hashed associative table for commands and variables.
+ */
+
+struct table {
+	Area   *areap;		/* area to allocate entries */
+	int	size, nfree;	/* hash size (always 2^^n), free entries */
+	struct	tbl **tbls;	/* hashed table items */
+};
+
+struct tbl {			/* table item */
+	int	flag;		/* flags */
+	int	type;		/* command type (see below), base (if INTEGER),
+				 * or offset from val.s of value (if EXPORT) */
+	Area	*areap;		/* area to allocate from */
+	union {
+		char *s;	/* string */
+		int64_t i;	/* integer */
+		int (*f)(char **);	/* int function */
+		struct op *t;	/* "function" tree */
+	} val;			/* value */
+	int	index;		/* index for an array */
+	union {
+	    int	field;		/* field with for -L/-R/-Z */
+	    int errno_;		/* CEXEC/CTALIAS */
+	} u2;
+	union {
+		struct tbl *array;	/* array values */
+		char *fpath;		/* temporary path to undef function */
+	} u;
+	char	name[4];	/* name -- variable length */
+};
+
+/* common flag bits */
+#define	ALLOC		BIT(0)	/* val.s has been allocated */
+#define	DEFINED		BIT(1)	/* is defined in block */
+#define	ISSET		BIT(2)	/* has value, vp->val.[si] */
+#define	EXPORT		BIT(3)	/* exported variable/function */
+#define	TRACE		BIT(4)	/* var: user flagged, func: execution tracing */
+/* (start non-common flags at 8) */
+/* flag bits used for variables */
+#define	SPECIAL		BIT(8)	/* PATH, IFS, SECONDS, etc */
+#define	INTEGER		BIT(9)	/* val.i contains integer value */
+#define	RDONLY		BIT(10)	/* read-only variable */
+#define	LOCAL		BIT(11)	/* for local typeset() */
+#define ARRAY		BIT(13)	/* array */
+#define LJUST		BIT(14)	/* left justify */
+#define RJUST		BIT(15)	/* right justify */
+#define ZEROFIL		BIT(16)	/* 0 filled if RJUSTIFY, strip 0s if LJUSTIFY */
+#define LCASEV		BIT(17)	/* convert to lower case */
+#define UCASEV_AL	BIT(18)/* convert to upper case / autoload function */
+#define INT_U		BIT(19)	/* unsigned integer */
+#define INT_L		BIT(20)	/* long integer (no-op) */
+#define IMPORT		BIT(21)	/* flag to typeset(): no arrays, must have = */
+#define LOCAL_COPY	BIT(22)	/* with LOCAL - copy attrs from existing var */
+#define EXPRINEVAL	BIT(23)	/* contents currently being evaluated */
+#define EXPRLVALUE	BIT(24)	/* useable as lvalue (temp flag) */
+/* flag bits used for taliases/builtins/aliases/keywords/functions */
+#define KEEPASN		BIT(8)	/* keep command assignments (eg, var=x cmd) */
+#define FINUSE		BIT(9)	/* function being executed */
+#define FDELETE		BIT(10)	/* function deleted while it was executing */
+#define FKSH		BIT(11)	/* function defined with function x (vs x()) */
+#define SPEC_BI		BIT(12)	/* a POSIX special builtin */
+#define REG_BI		BIT(13)	/* a POSIX regular builtin */
+/* Attributes that can be set by the user (used to decide if an unset param
+ * should be repoted by set/typeset).  Does not include ARRAY or LOCAL.
+ */
+#define USERATTRIB	(EXPORT|INTEGER|RDONLY|LJUST|RJUST|ZEROFIL\
+			 |LCASEV|UCASEV_AL|INT_U|INT_L)
+
+/* command types */
+#define	CNONE	0		/* undefined */
+#define	CSHELL	1		/* built-in */
+#define	CFUNC	2		/* function */
+#define	CEXEC	4		/* executable command */
+#define	CALIAS	5		/* alias */
+#define	CKEYWD	6		/* keyword */
+#define CTALIAS	7		/* tracked alias */
+
+/* Flags for findcom()/comexec() */
+#define FC_SPECBI	BIT(0)	/* special builtin */
+#define FC_FUNC		BIT(1)	/* function builtin */
+#define FC_REGBI	BIT(2)	/* regular builtin */
+#define FC_UNREGBI	BIT(3)	/* un-regular builtin (!special,!regular) */
+#define FC_BI		(FC_SPECBI|FC_REGBI|FC_UNREGBI)
+#define FC_PATH		BIT(4)	/* do path search */
+#define FC_DEFPATH	BIT(5)	/* use default path in path search */
+
+
+#define AF_ARGV_ALLOC	0x1	/* argv[] array allocated */
+#define AF_ARGS_ALLOCED	0x2	/* argument strings allocated */
+#define AI_ARGV(a, i)	((i) == 0 ? (a).argv[0] : (a).argv[(i) - (a).skip])
+#define AI_ARGC(a)	((a).argc_ - (a).skip)
+
+/* Argument info.  Used for $#, $* for shell, functions, includes, etc. */
+struct arg_info {
+	int flags;	/* AF_* */
+	char **argv;
+	int argc_;
+	int skip;	/* first arg is argv[0], second is argv[1 + skip] */
+};
+
+/*
+ * activation record for function blocks
+ */
+struct block {
+	Area	area;		/* area to allocate things */
+	/*struct arg_info argi;*/
+	char	**argv;
+	int	argc;
+	int	flags;		/* see BF_* */
+	struct	table vars;	/* local variables */
+	struct	table funs;	/* local functions */
+	Getopt	getopts_state;
+#if 1
+	char *	error;		/* error handler */
+	char *	exit;		/* exit handler */
+#else
+	Trap	error, exit;
+#endif
+	struct	block *next;	/* enclosing block */
+};
+
+/* Values for struct block.flags */
+#define BF_DOGETOPTS	BIT(0)	/* save/restore getopts state */
+
+/*
+ * Used by ktwalk() and ktnext() routines.
+ */
+struct tstate {
+	int left;
+	struct tbl **next;
+};
+
+extern	struct table taliases;	/* tracked aliases */
+extern	struct table builtins;	/* built-in commands */
+extern	struct table aliases;	/* aliases */
+extern	struct table keywords;	/* keywords */
+extern	struct table homedirs;	/* homedir() cache */
+
+struct builtin {
+	const char   *name;
+	int  (*func)(char **);
+};
+
+/* these really are externs! Look in table.c for them */
+extern const struct builtin shbuiltins [], kshbuiltins [];
+
+/* var spec values */
+#define	V_NONE			0
+#define	V_PATH			1
+#define	V_IFS			2
+#define	V_SECONDS		3
+#define	V_OPTIND		4
+#define	V_MAIL			5
+#define	V_MAILPATH		6
+#define	V_MAILCHECK		7
+#define	V_RANDOM		8
+#define	V_HISTCONTROL		9
+#define	V_HISTSIZE		10
+#define	V_HISTFILE		11
+#define	V_VISUAL		12
+#define	V_EDITOR		13
+#define	V_COLUMNS		14
+#define	V_POSIXLY_CORRECT	15
+#define	V_TMOUT			16
+#define	V_TMPDIR		17
+#define	V_LINENO		18
+#define	V_TERM			19
+
+/* values for set_prompt() */
+#define PS1	0		/* command */
+#define PS2	1		/* command continuation */
+
+extern char *search_path;	/* copy of either PATH or def_path */
+extern const char *def_path;	/* path to use if PATH not set */
+extern char *tmpdir;		/* TMPDIR value */
+extern const char *prompt;
+extern int cur_prompt;		/* PS1 or PS2 */
+extern int current_lineno;	/* LINENO value */
+
+unsigned int	hash(const char *);
+void		ktinit(struct table *, Area *, int);
+struct tbl *	ktsearch(struct table *, const char *, unsigned int);
+struct tbl *	ktenter(struct table *, const char *, unsigned int);
+void		ktdelete(struct tbl *);
+void		ktwalk(struct tstate *, struct table *);
+struct tbl *	ktnext(struct tstate *);
+struct tbl **	ktsort(struct table *);
diff --git a/trap.c b/trap.c
@@ -0,0 +1,440 @@
+/*	$OpenBSD: trap.c,v 1.33 2018/12/08 21:03:51 jca Exp $	*/
+
+/*
+ * signal handling
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+
+Trap sigtraps[NSIG + 1] = {
+	{ SIGEXIT_, "EXIT", "Signal 0" },
+	{ 1 , "HUP", "Hangup" },
+	{ 2 , "INT", "Interrupt" },
+	{ 3 , "QUIT", "Quit" },
+	{ 4 , "ILL", "Illegal instruction" },
+	{ 5 , "TRAP", "Trace trap" },
+	{ 6 , "ABRT", "Abort" },
+	{ 7 , "BUS", "Bus error" },
+	{ 8 , "FPE", "Floating point exception" },
+	{ 9 , "KILL", "Killed" },
+	{ 10 , "USR1", "User defined signal 1" },
+	{ 11 , "SEGV", "Memory fault" },
+	{ 12 , "USR2", "User defined signal 2" },
+	{ 13 , "PIPE", "Broken pipe" },
+	{ 14 , "ALRM", "Alarm clock" },
+	{ 15 , "TERM", "Terminated" },
+	{ 16 , "STKFLT", "Stack fault" },
+	{ 17 , "CHLD", "Child exited" },
+	{ 18 , "CONT", "Continued" },
+	{ 19 , "STOP", "Stopped (signal)" },
+	{ 20 , "TSTP", "Stopped" },
+	{ 21 , "TTIN", "Stopped (tty input)" },
+	{ 22 , "TTOU", "Stopped (tty output)" },
+	{ 23 , "URG", "Urgent I/O condition" },
+	{ 24 , "XCPU", "CPU time limit exceeded" },
+	{ 25 , "XFSZ", "File size limit exceeded" },
+	{ 26 , "VTALRM", "Virtual timer expired" },
+	{ 27 , "PROF", "Profiling timer expired" },
+	{ 28 , "WINCH", "Window size change" },
+	{ 29 , "IO", "I/O possible" },
+	{ 30 , "PWR", "Power-fail/Restart" },
+	{ 31 , "UNUSED", "Unused" },
+	{ SIGERR_,  "ERR",  "Error handler" }
+};
+
+static struct sigaction Sigact_ign, Sigact_trap;
+
+void
+inittraps(void)
+{
+	sigemptyset(&Sigact_ign.sa_mask);
+	Sigact_ign.sa_flags = 0; /* interruptible */
+	Sigact_ign.sa_handler = SIG_IGN;
+	Sigact_trap = Sigact_ign;
+	Sigact_trap.sa_handler = trapsig;
+
+	sigtraps[SIGINT].flags |= TF_DFL_INTR | TF_TTY_INTR;
+	sigtraps[SIGQUIT].flags |= TF_DFL_INTR | TF_TTY_INTR;
+	sigtraps[SIGTERM].flags |= TF_DFL_INTR;/* not fatal for interactive */
+	sigtraps[SIGHUP].flags |= TF_FATAL;
+	sigtraps[SIGCHLD].flags |= TF_SHELL_USES;
+
+	/* these are always caught so we can clean up any temporary files. */
+	setsig(&sigtraps[SIGINT], trapsig, SS_RESTORE_ORIG);
+	setsig(&sigtraps[SIGQUIT], trapsig, SS_RESTORE_ORIG);
+	setsig(&sigtraps[SIGTERM], trapsig, SS_RESTORE_ORIG);
+	setsig(&sigtraps[SIGHUP], trapsig, SS_RESTORE_ORIG);
+}
+
+static void alarm_catcher(int sig);
+
+void
+alarm_init(void)
+{
+	sigtraps[SIGALRM].flags |= TF_SHELL_USES;
+	setsig(&sigtraps[SIGALRM], alarm_catcher,
+		SS_RESTORE_ORIG|SS_FORCE|SS_SHTRAP);
+}
+
+static void
+alarm_catcher(int sig)
+{
+	int errno_ = errno;
+
+	if (ksh_tmout_state == TMOUT_READING) {
+		int left = alarm(0);
+
+		if (left == 0) {
+			ksh_tmout_state = TMOUT_LEAVING;
+			intrsig = 1;
+		} else
+			alarm(left);
+	}
+	errno = errno_;
+}
+
+Trap *
+gettrap(const char *name, int igncase)
+{
+	int i;
+	Trap *p;
+
+	if (digit(*name)) {
+		int n;
+
+		if (getn(name, &n) && 0 <= n && n < NSIG)
+			return &sigtraps[n];
+		return NULL;
+	}
+
+	if (igncase && strncasecmp(name, "SIG", 3) == 0)
+		name += 3;
+	if (!igncase && strncmp(name, "SIG", 3) == 0)
+		name += 3;
+
+	for (p = sigtraps, i = NSIG+1; --i >= 0; p++)
+		if (p->name) {
+			if (igncase && strcasecmp(p->name, name) == 0)
+				return p;
+			if (!igncase && strcmp(p->name, name) == 0)
+				return p;
+		}
+	return NULL;
+}
+
+/*
+ * trap signal handler
+ */
+void
+trapsig(int i)
+{
+	Trap *p = &sigtraps[i];
+	int errno_ = errno;
+
+	trap = p->set = 1;
+	if (p->flags & TF_DFL_INTR)
+		intrsig = 1;
+	if ((p->flags & TF_FATAL) && !p->trap) {
+		fatal_trap = 1;
+		intrsig = 1;
+	}
+	if (p->shtrap)
+		(*p->shtrap)(i);
+	errno = errno_;
+}
+
+/* called when we want to allow the user to ^C out of something - won't
+ * work if user has trapped SIGINT.
+ */
+void
+intrcheck(void)
+{
+	if (intrsig)
+		runtraps(TF_DFL_INTR|TF_FATAL);
+}
+
+/* called after EINTR to check if a signal with normally causes process
+ * termination has been received.
+ */
+int
+fatal_trap_check(void)
+{
+	int i;
+	Trap *p;
+
+	/* todo: should check if signal is fatal, not the TF_DFL_INTR flag */
+	for (p = sigtraps, i = NSIG+1; --i >= 0; p++)
+		if (p->set && (p->flags & (TF_DFL_INTR|TF_FATAL)))
+			/* return value is used as an exit code */
+			return 128 + p->signal;
+	return 0;
+}
+
+/* Returns the signal number of any pending traps: ie, a signal which has
+ * occurred for which a trap has been set or for which the TF_DFL_INTR flag
+ * is set.
+ */
+int
+trap_pending(void)
+{
+	int i;
+	Trap *p;
+
+	for (p = sigtraps, i = NSIG+1; --i >= 0; p++)
+		if (p->set && ((p->trap && p->trap[0]) ||
+		    ((p->flags & (TF_DFL_INTR|TF_FATAL)) && !p->trap)))
+			return p->signal;
+	return 0;
+}
+
+/*
+ * run any pending traps.  If intr is set, only run traps that
+ * can interrupt commands.
+ */
+void
+runtraps(int flag)
+{
+	int i;
+	Trap *p;
+
+	if (ksh_tmout_state == TMOUT_LEAVING) {
+		ksh_tmout_state = TMOUT_EXECUTING;
+		warningf(false, "timed out waiting for input");
+		unwind(LEXIT);
+	} else
+		/* XXX: this means the alarm will have no effect if a trap
+		 * is caught after the alarm() was started...not good.
+		 */
+		ksh_tmout_state = TMOUT_EXECUTING;
+	if (!flag)
+		trap = 0;
+	if (flag & TF_DFL_INTR)
+		intrsig = 0;
+	if (flag & TF_FATAL)
+		fatal_trap = 0;
+	for (p = sigtraps, i = NSIG+1; --i >= 0; p++)
+		if (p->set && (!flag ||
+		    ((p->flags & flag) && p->trap == NULL)))
+			runtrap(p);
+}
+
+void
+runtrap(Trap *p)
+{
+	int	i = p->signal;
+	char	*trapstr = p->trap;
+	int	oexstat;
+	int	old_changed = 0;
+
+	p->set = 0;
+	if (trapstr == NULL) { /* SIG_DFL */
+		if (p->flags & TF_FATAL) {
+			/* eg, SIGHUP */
+			exstat = 128 + i;
+			unwind(LLEAVE);
+		}
+		if (p->flags & TF_DFL_INTR) {
+			/* eg, SIGINT, SIGQUIT, SIGTERM, etc. */
+			exstat = 128 + i;
+			unwind(LINTR);
+		}
+		return;
+	}
+	if (trapstr[0] == '\0') /* SIG_IGN */
+		return;
+	if (i == SIGEXIT_ || i == SIGERR_) {	/* avoid recursion on these */
+		old_changed = p->flags & TF_CHANGED;
+		p->flags &= ~TF_CHANGED;
+		p->trap = NULL;
+	}
+	oexstat = exstat;
+	/* Note: trapstr is fully parsed before anything is executed, thus
+	 * no problem with afree(p->trap) in settrap() while still in use.
+	 */
+	command(trapstr, current_lineno);
+	exstat = oexstat;
+	if (i == SIGEXIT_ || i == SIGERR_) {
+		if (p->flags & TF_CHANGED)
+			/* don't clear TF_CHANGED */
+			afree(trapstr, APERM);
+		else
+			p->trap = trapstr;
+		p->flags |= old_changed;
+	}
+}
+
+/* clear pending traps and reset user's trap handlers; used after fork(2) */
+void
+cleartraps(void)
+{
+	int i;
+	Trap *p;
+
+	trap = 0;
+	intrsig = 0;
+	fatal_trap = 0;
+	for (i = NSIG+1, p = sigtraps; --i >= 0; p++) {
+		p->set = 0;
+		if ((p->flags & TF_USER_SET) && (p->trap && p->trap[0]))
+			settrap(p, NULL);
+	}
+}
+
+/* restore signals just before an exec(2) */
+void
+restoresigs(void)
+{
+	int i;
+	Trap *p;
+
+	for (i = NSIG+1, p = sigtraps; --i >= 0; p++)
+		if (p->flags & (TF_EXEC_IGN|TF_EXEC_DFL))
+			setsig(p, (p->flags & TF_EXEC_IGN) ? SIG_IGN : SIG_DFL,
+			    SS_RESTORE_CURR|SS_FORCE);
+}
+
+void
+settrap(Trap *p, char *s)
+{
+	sig_t f;
+
+	afree(p->trap, APERM);
+	p->trap = str_save(s, APERM); /* handles s == 0 */
+	p->flags |= TF_CHANGED;
+	f = !s ? SIG_DFL : s[0] ? trapsig : SIG_IGN;
+
+	p->flags |= TF_USER_SET;
+	if ((p->flags & (TF_DFL_INTR|TF_FATAL)) && f == SIG_DFL)
+		f = trapsig;
+	else if (p->flags & TF_SHELL_USES) {
+		if (!(p->flags & TF_ORIG_IGN) || Flag(FTALKING)) {
+			/* do what user wants at exec time */
+			p->flags &= ~(TF_EXEC_IGN|TF_EXEC_DFL);
+			if (f == SIG_IGN)
+				p->flags |= TF_EXEC_IGN;
+			else
+				p->flags |= TF_EXEC_DFL;
+		}
+
+		/* assumes handler already set to what shell wants it
+		 * (normally trapsig, but could be j_sigchld() or SIG_IGN)
+		 */
+		return;
+	}
+
+	/* todo: should we let user know signal is ignored? how? */
+	setsig(p, f, SS_RESTORE_CURR|SS_USER);
+}
+
+/* Called by c_print() when writing to a co-process to ensure SIGPIPE won't
+ * kill shell (unless user catches it and exits)
+ */
+int
+block_pipe(void)
+{
+	int restore_dfl = 0;
+	Trap *p = &sigtraps[SIGPIPE];
+
+	if (!(p->flags & (TF_ORIG_IGN|TF_ORIG_DFL))) {
+		setsig(p, SIG_IGN, SS_RESTORE_CURR);
+		if (p->flags & TF_ORIG_DFL)
+			restore_dfl = 1;
+	} else if (p->cursig == SIG_DFL) {
+		setsig(p, SIG_IGN, SS_RESTORE_CURR);
+		restore_dfl = 1; /* restore to SIG_DFL */
+	}
+	return restore_dfl;
+}
+
+/* Called by c_print() to undo whatever block_pipe() did */
+void
+restore_pipe(int restore_dfl)
+{
+	if (restore_dfl)
+		setsig(&sigtraps[SIGPIPE], SIG_DFL, SS_RESTORE_CURR);
+}
+
+/* Set action for a signal.  Action may not be set if original
+ * action was SIG_IGN, depending on the value of flags and
+ * FTALKING.
+ */
+int
+setsig(Trap *p, sig_t f, int flags)
+{
+	struct sigaction sigact;
+
+	if (p->signal == SIGEXIT_ || p->signal == SIGERR_)
+		return 1;
+
+	/* First time setting this signal?  If so, get and note the current
+	 * setting.
+	 */
+	if (!(p->flags & (TF_ORIG_IGN|TF_ORIG_DFL))) {
+		sigaction(p->signal, &Sigact_ign, &sigact);
+		p->flags |= sigact.sa_handler == SIG_IGN ?
+		    TF_ORIG_IGN : TF_ORIG_DFL;
+		p->cursig = SIG_IGN;
+	}
+
+	/* Generally, an ignored signal stays ignored, except if
+	 *	- the user of an interactive shell wants to change it
+	 *	- the shell wants for force a change
+	 */
+	if ((p->flags & TF_ORIG_IGN) && !(flags & SS_FORCE) &&
+	    (!(flags & SS_USER) || !Flag(FTALKING)))
+		return 0;
+
+	setexecsig(p, flags & SS_RESTORE_MASK);
+
+	/* This is here 'cause there should be a way of clearing shtraps, but
+	 * don't know if this is a sane way of doing it.  At the moment,
+	 * all users of shtrap are lifetime users (SIGCHLD, SIGALRM, SIGWINCH).
+	 */
+	if (!(flags & SS_USER))
+		p->shtrap = NULL;
+	if (flags & SS_SHTRAP) {
+		p->shtrap = f;
+		f = trapsig;
+	}
+
+	if (p->cursig != f) {
+		p->cursig = f;
+		sigemptyset(&sigact.sa_mask);
+		sigact.sa_flags = 0 /* interruptible */;
+		sigact.sa_handler = f;
+		sigaction(p->signal, &sigact, NULL);
+	}
+
+	return 1;
+}
+
+/* control what signal is set to before an exec() */
+void
+setexecsig(Trap *p, int restore)
+{
+	/* XXX debugging */
+	if (!(p->flags & (TF_ORIG_IGN|TF_ORIG_DFL)))
+		internal_errorf("%s: unset signal %d(%s)",
+		    __func__, p->signal, p->name);
+
+	/* restore original value for exec'd kids */
+	p->flags &= ~(TF_EXEC_IGN|TF_EXEC_DFL);
+	switch (restore & SS_RESTORE_MASK) {
+	case SS_RESTORE_CURR: /* leave things as they currently are */
+		break;
+	case SS_RESTORE_ORIG:
+		p->flags |= p->flags & TF_ORIG_IGN ? TF_EXEC_IGN : TF_EXEC_DFL;
+		break;
+	case SS_RESTORE_DFL:
+		p->flags |= TF_EXEC_DFL;
+		break;
+	case SS_RESTORE_IGN:
+		p->flags |= TF_EXEC_IGN;
+		break;
+	}
+}
diff --git a/tree.c b/tree.c
@@ -0,0 +1,687 @@
+/*	$OpenBSD: tree.c,v 1.34 2018/04/09 17:53:36 tobias Exp $	*/
+
+/*
+ * command tree climbing
+ */
+
+#include <string.h>
+
+#include "sh.h"
+
+#define INDENT	4
+
+#define tputc(c, shf)	shf_putchar(c, shf);
+static void	ptree(struct op *, int, struct shf *);
+static void	pioact(struct shf *, int, struct ioword *);
+static void	tputC(int, struct shf *);
+static void	tputS(char *, struct shf *);
+static void	vfptreef(struct shf *, int, const char *, va_list);
+static struct ioword **iocopy(struct ioword **, Area *);
+static void     iofree(struct ioword **, Area *);
+
+/*
+ * print a command tree
+ */
+
+static void
+ptree(struct op *t, int indent, struct shf *shf)
+{
+	char **w;
+	struct ioword **ioact;
+	struct op *t1;
+
+ Chain:
+	if (t == NULL)
+		return;
+	switch (t->type) {
+	case TCOM:
+		if (t->vars)
+			for (w = t->vars; *w != NULL; )
+				fptreef(shf, indent, "%S ", *w++);
+		else
+			fptreef(shf, indent, "#no-vars# ");
+		if (t->args)
+			for (w = t->args; *w != NULL; )
+				fptreef(shf, indent, "%S ", *w++);
+		else
+			fptreef(shf, indent, "#no-args# ");
+		break;
+	case TEXEC:
+		t = t->left;
+		goto Chain;
+	case TPAREN:
+		fptreef(shf, indent + 2, "( %T) ", t->left);
+		break;
+	case TPIPE:
+		fptreef(shf, indent, "%T| ", t->left);
+		t = t->right;
+		goto Chain;
+	case TLIST:
+		fptreef(shf, indent, "%T%;", t->left);
+		t = t->right;
+		goto Chain;
+	case TOR:
+	case TAND:
+		fptreef(shf, indent, "%T%s %T",
+		    t->left, (t->type==TOR) ? "||" : "&&", t->right);
+		break;
+	case TBANG:
+		fptreef(shf, indent, "! ");
+		t = t->right;
+		goto Chain;
+	case TDBRACKET:
+	  {
+		int i;
+
+		fptreef(shf, indent, "[[");
+		for (i = 0; t->args[i]; i++)
+			fptreef(shf, indent, " %S", t->args[i]);
+		fptreef(shf, indent, " ]] ");
+		break;
+	  }
+	case TSELECT:
+		fptreef(shf, indent, "select %s ", t->str);
+		/* FALLTHROUGH */
+	case TFOR:
+		if (t->type == TFOR)
+			fptreef(shf, indent, "for %s ", t->str);
+		if (t->vars != NULL) {
+			fptreef(shf, indent, "in ");
+			for (w = t->vars; *w; )
+				fptreef(shf, indent, "%S ", *w++);
+			fptreef(shf, indent, "%;");
+		}
+		fptreef(shf, indent + INDENT, "do%N%T", t->left);
+		fptreef(shf, indent, "%;done ");
+		break;
+	case TCASE:
+		fptreef(shf, indent, "case %S in", t->str);
+		for (t1 = t->left; t1 != NULL; t1 = t1->right) {
+			fptreef(shf, indent, "%N(");
+			for (w = t1->vars; *w != NULL; w++)
+				fptreef(shf, indent, "%S%c", *w,
+				    (w[1] != NULL) ? '|' : ')');
+			fptreef(shf, indent + INDENT, "%;%T%N;;", t1->left);
+		}
+		fptreef(shf, indent, "%Nesac ");
+		break;
+	case TIF:
+	case TELIF:
+		/* 3 == strlen("if ") */
+		fptreef(shf, indent + 3, "if %T", t->left);
+		for (;;) {
+			t = t->right;
+			if (t->left != NULL) {
+				fptreef(shf, indent, "%;");
+				fptreef(shf, indent + INDENT, "then%N%T",
+				    t->left);
+			}
+			if (t->right == NULL || t->right->type != TELIF)
+				break;
+			t = t->right;
+			fptreef(shf, indent, "%;");
+			/* 5 == strlen("elif ") */
+			fptreef(shf, indent + 5, "elif %T", t->left);
+		}
+		if (t->right != NULL) {
+			fptreef(shf, indent, "%;");
+			fptreef(shf, indent + INDENT, "else%;%T", t->right);
+		}
+		fptreef(shf, indent, "%;fi ");
+		break;
+	case TWHILE:
+	case TUNTIL:
+		/* 6 == strlen("while"/"until") */
+		fptreef(shf, indent + 6, "%s %T",
+		    (t->type==TWHILE) ? "while" : "until",
+		    t->left);
+		fptreef(shf, indent, "%;do");
+		fptreef(shf, indent + INDENT, "%;%T", t->right);
+		fptreef(shf, indent, "%;done ");
+		break;
+	case TBRACE:
+		fptreef(shf, indent + INDENT, "{%;%T", t->left);
+		fptreef(shf, indent, "%;} ");
+		break;
+	case TCOPROC:
+		fptreef(shf, indent, "%T|& ", t->left);
+		break;
+	case TASYNC:
+		fptreef(shf, indent, "%T& ", t->left);
+		break;
+	case TFUNCT:
+		fptreef(shf, indent,
+		    t->u.ksh_func ? "function %s %T" : "%s() %T",
+		    t->str, t->left);
+		break;
+	case TTIME:
+		fptreef(shf, indent, "time %T", t->left);
+		break;
+	default:
+		fptreef(shf, indent, "<botch>");
+		break;
+	}
+	if ((ioact = t->ioact) != NULL) {
+		int	need_nl = 0;
+
+		while (*ioact != NULL)
+			pioact(shf, indent, *ioact++);
+		/* Print here documents after everything else... */
+		for (ioact = t->ioact; *ioact != NULL; ) {
+			struct ioword *iop = *ioact++;
+
+			/* heredoc is 0 when tracing (set -x) */
+			if ((iop->flag & IOTYPE) == IOHERE && iop->heredoc) {
+				tputc('\n', shf);
+				shf_puts(iop->heredoc, shf);
+				fptreef(shf, indent, "%s",
+				    evalstr(iop->delim, 0));
+				need_nl = 1;
+			}
+		}
+		/* Last delimiter must be followed by a newline (this often
+		 * leads to an extra blank line, but its not worth worrying
+		 * about)
+		 */
+		if (need_nl)
+			tputc('\n', shf);
+	}
+}
+
+static void
+pioact(struct shf *shf, int indent, struct ioword *iop)
+{
+	int flag = iop->flag;
+	int type = flag & IOTYPE;
+	int expected;
+
+	expected = (type == IOREAD || type == IORDWR || type == IOHERE) ? 0 :
+	    (type == IOCAT || type == IOWRITE) ? 1 :
+	    (type == IODUP && (iop->unit == !(flag & IORDUP))) ? iop->unit :
+	    iop->unit + 1;
+	if (iop->unit != expected)
+		tputc('0' + iop->unit, shf);
+
+	switch (type) {
+	case IOREAD:
+		fptreef(shf, indent, "< ");
+		break;
+	case IOHERE:
+		if (flag&IOSKIP)
+			fptreef(shf, indent, "<<- ");
+		else
+			fptreef(shf, indent, "<< ");
+		break;
+	case IOCAT:
+		fptreef(shf, indent, ">> ");
+		break;
+	case IOWRITE:
+		if (flag&IOCLOB)
+			fptreef(shf, indent, ">| ");
+		else
+			fptreef(shf, indent, "> ");
+		break;
+	case IORDWR:
+		fptreef(shf, indent, "<> ");
+		break;
+	case IODUP:
+		if (flag & IORDUP)
+			fptreef(shf, indent, "<&");
+		else
+			fptreef(shf, indent, ">&");
+		break;
+	}
+	/* name/delim are 0 when printing syntax errors */
+	if (type == IOHERE) {
+		if (iop->delim)
+			fptreef(shf, indent, "%S ", iop->delim);
+	} else if (iop->name)
+		fptreef(shf, indent, (iop->flag & IONAMEXP) ? "%s " : "%S ",
+		    iop->name);
+}
+
+
+/*
+ * variants of fputc, fputs for ptreef and snptreef
+ */
+
+static void
+tputC(int c, struct shf *shf)
+{
+	if ((c&0x60) == 0) {		/* C0|C1 */
+		tputc((c&0x80) ? '$' : '^', shf);
+		tputc(((c&0x7F)|0x40), shf);
+	} else if ((c&0x7F) == 0x7F) {	/* DEL */
+		tputc((c&0x80) ? '$' : '^', shf);
+		tputc('?', shf);
+	} else
+		tputc(c, shf);
+}
+
+static void
+tputS(char *wp, struct shf *shf)
+{
+	int c, quoted=0;
+
+	/* problems:
+	 *	`...` -> $(...)
+	 *	'foo' -> "foo"
+	 * could change encoding to:
+	 *	OQUOTE ["'] ... CQUOTE ["']
+	 *	COMSUB [(`] ...\0	(handle $ ` \ and maybe " in `...` case)
+	 */
+	while (1)
+		switch ((c = *wp++)) {
+		case EOS:
+			return;
+		case CHAR:
+			tputC(*wp++, shf);
+			break;
+		case QCHAR:
+			c = *wp++;
+			if (!quoted || (c == '"' || c == '`' || c == '$'))
+				tputc('\\', shf);
+			tputC(c, shf);
+			break;
+		case COMSUB:
+			tputc('$', shf);
+			tputc('(', shf);
+			while (*wp != 0)
+				tputC(*wp++, shf);
+			tputc(')', shf);
+			wp++;
+			break;
+		case EXPRSUB:
+			tputc('$', shf);
+			tputc('(', shf);
+			tputc('(', shf);
+			while (*wp != 0)
+				tputC(*wp++, shf);
+			tputc(')', shf);
+			tputc(')', shf);
+			wp++;
+			break;
+		case OQUOTE:
+			quoted = 1;
+			tputc('"', shf);
+			break;
+		case CQUOTE:
+			quoted = 0;
+			tputc('"', shf);
+			break;
+		case OSUBST:
+			tputc('$', shf);
+			if (*wp++ == '{')
+				tputc('{', shf);
+			while ((c = *wp++) != 0)
+				tputC(c, shf);
+			break;
+		case CSUBST:
+			if (*wp++ == '}')
+				tputc('}', shf);
+			break;
+		case OPAT:
+			tputc(*wp++, shf);
+			tputc('(', shf);
+			break;
+		case SPAT:
+			tputc('|', shf);
+			break;
+		case CPAT:
+			tputc(')', shf);
+			break;
+		}
+}
+
+void
+fptreef(struct shf *shf, int indent, const char *fmt, ...)
+{
+  va_list	va;
+
+  va_start(va, fmt);
+  vfptreef(shf, indent, fmt, va);
+  va_end(va);
+}
+
+char *
+snptreef(char *s, int n, const char *fmt, ...)
+{
+  va_list va;
+  struct shf shf;
+
+  shf_sopen(s, n, SHF_WR | (s ? 0 : SHF_DYNAMIC), &shf);
+
+  va_start(va, fmt);
+  vfptreef(&shf, 0, fmt, va);
+  va_end(va);
+
+  return shf_sclose(&shf); /* null terminates */
+}
+
+static void
+vfptreef(struct shf *shf, int indent, const char *fmt, va_list va)
+{
+	int c;
+
+	while ((c = *fmt++)) {
+		if (c == '%') {
+			int64_t n;
+			char *p;
+			int neg;
+
+			switch ((c = *fmt++)) {
+			case 'c':
+				tputc(va_arg(va, int), shf);
+				break;
+			case 'd': /* decimal */
+				n = va_arg(va, int);
+				neg = n < 0;
+				p = u64ton(neg ? -n : n, 10);
+				if (neg)
+					*--p = '-';
+				while (*p)
+					tputc(*p++, shf);
+				break;
+			case 's':
+				p = va_arg(va, char *);
+				while (*p)
+					tputc(*p++, shf);
+				break;
+			case 'S':	/* word */
+				p = va_arg(va, char *);
+				tputS(p, shf);
+				break;
+			case 'u': /* unsigned decimal */
+				p = u64ton(va_arg(va, unsigned int), 10);
+				while (*p)
+					tputc(*p++, shf);
+				break;
+			case 'T':	/* format tree */
+				ptree(va_arg(va, struct op *), indent, shf);
+				break;
+			case ';':	/* newline or ; */
+			case 'N':	/* newline or space */
+				if (shf->flags & SHF_STRING) {
+					if (c == ';')
+						tputc(';', shf);
+					tputc(' ', shf);
+				} else {
+					int i;
+
+					tputc('\n', shf);
+					for (i = indent; i >= 8; i -= 8)
+						tputc('\t', shf);
+					for (; i > 0; --i)
+						tputc(' ', shf);
+				}
+				break;
+			case 'R':
+				pioact(shf, indent, va_arg(va, struct ioword *));
+				break;
+			default:
+				tputc(c, shf);
+				break;
+			}
+		} else
+			tputc(c, shf);
+	}
+}
+
+/*
+ * copy tree (for function definition)
+ */
+
+struct op *
+tcopy(struct op *t, Area *ap)
+{
+	struct op *r;
+	char **tw, **rw;
+
+	if (t == NULL)
+		return NULL;
+
+	r = alloc(sizeof(struct op), ap);
+
+	r->type = t->type;
+	r->u.evalflags = t->u.evalflags;
+
+	r->str = t->type == TCASE ? wdcopy(t->str, ap) : str_save(t->str, ap);
+
+	if (t->vars == NULL)
+		r->vars = NULL;
+	else {
+		for (tw = t->vars; *tw++ != NULL; )
+			;
+		rw = r->vars = areallocarray(NULL, tw - t->vars + 1,
+		    sizeof(*tw), ap);
+		for (tw = t->vars; *tw != NULL; )
+			*rw++ = wdcopy(*tw++, ap);
+		*rw = NULL;
+	}
+
+	if (t->args == NULL)
+		r->args = NULL;
+	else {
+		for (tw = t->args; *tw++ != NULL; )
+			;
+		rw = r->args = areallocarray(NULL, tw - t->args + 1,
+		    sizeof(*tw), ap);
+		for (tw = t->args; *tw != NULL; )
+			*rw++ = wdcopy(*tw++, ap);
+		*rw = NULL;
+	}
+
+	r->ioact = (t->ioact == NULL) ? NULL : iocopy(t->ioact, ap);
+
+	r->left = tcopy(t->left, ap);
+	r->right = tcopy(t->right, ap);
+	r->lineno = t->lineno;
+
+	return r;
+}
+
+char *
+wdcopy(const char *wp, Area *ap)
+{
+	size_t len = wdscan(wp, EOS) - wp;
+	return memcpy(alloc(len, ap), wp, len);
+}
+
+/* return the position of prefix c in wp plus 1 */
+char *
+wdscan(const char *wp, int c)
+{
+	int nest = 0;
+
+	while (1)
+		switch (*wp++) {
+		case EOS:
+			return (char *) wp;
+		case CHAR:
+		case QCHAR:
+			wp++;
+			break;
+		case COMSUB:
+		case EXPRSUB:
+			while (*wp++ != 0)
+				;
+			break;
+		case OQUOTE:
+		case CQUOTE:
+			break;
+		case OSUBST:
+			nest++;
+			while (*wp++ != '\0')
+				;
+			break;
+		case CSUBST:
+			wp++;
+			if (c == CSUBST && nest == 0)
+				return (char *) wp;
+			nest--;
+			break;
+		case OPAT:
+			nest++;
+			wp++;
+			break;
+		case SPAT:
+		case CPAT:
+			if (c == wp[-1] && nest == 0)
+				return (char *) wp;
+			if (wp[-1] == CPAT)
+				nest--;
+			break;
+		default:
+			internal_warningf(
+			    "%s: unknown char 0x%x (carrying on)",
+			    __func__, wp[-1]);
+		}
+}
+
+/* return a copy of wp without any of the mark up characters and
+ * with quote characters (" ' \) stripped.
+ * (string is allocated from ATEMP)
+ */
+char *
+wdstrip(const char *wp)
+{
+	struct shf shf;
+	int c;
+
+	shf_sopen(NULL, 32, SHF_WR | SHF_DYNAMIC, &shf);
+
+	/* problems:
+	 *	`...` -> $(...)
+	 *	x${foo:-"hi"} -> x${foo:-hi}
+	 *	x${foo:-'hi'} -> x${foo:-hi}
+	 */
+	while (1)
+		switch ((c = *wp++)) {
+		case EOS:
+			return shf_sclose(&shf); /* null terminates */
+		case CHAR:
+		case QCHAR:
+			shf_putchar(*wp++, &shf);
+			break;
+		case COMSUB:
+			shf_putchar('$', &shf);
+			shf_putchar('(', &shf);
+			while (*wp != 0)
+				shf_putchar(*wp++, &shf);
+			shf_putchar(')', &shf);
+			break;
+		case EXPRSUB:
+			shf_putchar('$', &shf);
+			shf_putchar('(', &shf);
+			shf_putchar('(', &shf);
+			while (*wp != 0)
+				shf_putchar(*wp++, &shf);
+			shf_putchar(')', &shf);
+			shf_putchar(')', &shf);
+			break;
+		case OQUOTE:
+			break;
+		case CQUOTE:
+			break;
+		case OSUBST:
+			shf_putchar('$', &shf);
+			if (*wp++ == '{')
+			    shf_putchar('{', &shf);
+			while ((c = *wp++) != 0)
+				shf_putchar(c, &shf);
+			break;
+		case CSUBST:
+			if (*wp++ == '}')
+				shf_putchar('}', &shf);
+			break;
+		case OPAT:
+			shf_putchar(*wp++, &shf);
+			shf_putchar('(', &shf);
+			break;
+		case SPAT:
+			shf_putchar('|', &shf);
+			break;
+		case CPAT:
+			shf_putchar(')', &shf);
+			break;
+		}
+}
+
+static	struct ioword **
+iocopy(struct ioword **iow, Area *ap)
+{
+	struct ioword **ior;
+	int i;
+
+	for (ior = iow; *ior++ != NULL; )
+		;
+	ior = areallocarray(NULL, ior - iow + 1, sizeof(*ior), ap);
+
+	for (i = 0; iow[i] != NULL; i++) {
+		struct ioword *p, *q;
+
+		p = iow[i];
+		q = alloc(sizeof(*p), ap);
+		ior[i] = q;
+		*q = *p;
+		if (p->name != NULL)
+			q->name = wdcopy(p->name, ap);
+		if (p->delim != NULL)
+			q->delim = wdcopy(p->delim, ap);
+		if (p->heredoc != NULL)
+			q->heredoc = str_save(p->heredoc, ap);
+	}
+	ior[i] = NULL;
+
+	return ior;
+}
+
+/*
+ * free tree (for function definition)
+ */
+
+void
+tfree(struct op *t, Area *ap)
+{
+	char **w;
+
+	if (t == NULL)
+		return;
+
+	afree(t->str, ap);
+
+	if (t->vars != NULL) {
+		for (w = t->vars; *w != NULL; w++)
+			afree(*w, ap);
+		afree(t->vars, ap);
+	}
+
+	if (t->args != NULL) {
+		for (w = t->args; *w != NULL; w++)
+			afree(*w, ap);
+		afree(t->args, ap);
+	}
+
+	if (t->ioact != NULL)
+		iofree(t->ioact, ap);
+
+	tfree(t->left, ap);
+	tfree(t->right, ap);
+
+	afree(t, ap);
+}
+
+static	void
+iofree(struct ioword **iow, Area *ap)
+{
+	struct ioword **iop;
+	struct ioword *p;
+
+	for (iop = iow; (p = *iop++) != NULL; ) {
+		afree(p->name, ap);
+		afree(p->delim, ap);
+		afree(p->heredoc, ap);
+		afree(p, ap);
+	}
+	afree(iow, ap);
+}
diff --git a/tree.h b/tree.h
@@ -0,0 +1,145 @@
+/*	$OpenBSD: tree.h,v 1.12 2015/10/15 22:53:50 mmcc Exp $	*/
+
+/*
+ * command trees for compile/execute
+ */
+
+/* $From: tree.h,v 1.3 1994/05/31 13:34:34 michael Exp $ */
+
+/*
+ * Description of a command or an operation on commands.
+ */
+struct op {
+	short	type;			/* operation type, see below */
+	union { /* WARNING: newtp(), tcopy() use evalflags = 0 to clear union */
+		short	evalflags;	/* TCOM: arg expansion eval() flags */
+		short	ksh_func;	/* TFUNC: function x (vs x()) */
+	} u;
+	char  **args;			/* arguments to a command */
+	char  **vars;			/* variable assignments */
+	struct ioword	**ioact;	/* IO actions (eg, < > >>) */
+	struct op *left, *right;	/* descendents */
+	char   *str;			/* word for case; identifier for for,
+					 * select, and functions;
+					 * path to execute for TEXEC;
+					 * time hook for TCOM.
+					 */
+	int	lineno;			/* TCOM/TFUNC: LINENO for this */
+};
+
+/* Tree.type values */
+#define	TEOF		0
+#define	TCOM		1	/* command */
+#define	TPAREN		2	/* (c-list) */
+#define	TPIPE		3	/* a | b */
+#define	TLIST		4	/* a ; b */
+#define	TOR		5	/* || */
+#define	TAND		6	/* && */
+#define TBANG		7	/* ! */
+#define TDBRACKET	8	/* [[ .. ]] */
+#define	TFOR		9
+#define TSELECT		10
+#define	TCASE		11
+#define	TIF		12
+#define	TWHILE		13
+#define	TUNTIL		14
+#define	TELIF		15
+#define	TPAT		16	/* pattern in case */
+#define	TBRACE		17	/* {c-list} */
+#define	TASYNC		18	/* c & */
+#define	TFUNCT		19	/* function name { command; } */
+#define	TTIME		20	/* time pipeline */
+#define	TEXEC		21	/* fork/exec eval'd TCOM */
+#define TCOPROC		22	/* coprocess |& */
+
+/*
+ * prefix codes for words in command tree
+ */
+#define	EOS	0		/* end of string */
+#define	CHAR	1		/* unquoted character */
+#define	QCHAR	2		/* quoted character */
+#define	COMSUB	3		/* $() substitution (0 terminated) */
+#define EXPRSUB	4		/* $(()) substitution (0 terminated) */
+#define	OQUOTE	5		/* opening " or ' */
+#define	CQUOTE	6		/* closing " or ' */
+#define	OSUBST	7		/* opening ${ subst (followed by { or X) */
+#define	CSUBST	8		/* closing } of above (followed by } or X) */
+#define OPAT	9		/* open pattern: *(, @(, etc. */
+#define SPAT	10		/* separate pattern: | */
+#define CPAT	11		/* close pattern: ) */
+
+/*
+ * IO redirection
+ */
+struct ioword {
+	int	unit;	/* unit affected */
+	int	flag;	/* action (below) */
+	char	*name;	/* file name (unused if heredoc) */
+	char	*delim;	/* delimiter for <<,<<- */
+	char	*heredoc;/* content of heredoc */
+};
+
+/* ioword.flag - type of redirection */
+#define	IOTYPE	0xF		/* type: bits 0:3 */
+#define	IOREAD	0x1		/* < */
+#define	IOWRITE	0x2		/* > */
+#define	IORDWR	0x3		/* <>: todo */
+#define	IOHERE	0x4		/* << (here file) */
+#define	IOCAT	0x5		/* >> */
+#define	IODUP	0x6		/* <&/>& */
+#define	IOEVAL	BIT(4)		/* expand in << */
+#define	IOSKIP	BIT(5)		/* <<-, skip ^\t* */
+#define	IOCLOB	BIT(6)		/* >|, override -o noclobber */
+#define IORDUP	BIT(7)		/* x<&y (as opposed to x>&y) */
+#define IONAMEXP BIT(8)		/* name has been expanded */
+
+/* execute/exchild flags */
+#define	XEXEC	BIT(0)		/* execute without forking */
+#define	XFORK	BIT(1)		/* fork before executing */
+#define	XBGND	BIT(2)		/* command & */
+#define	XPIPEI	BIT(3)		/* input is pipe */
+#define	XPIPEO	BIT(4)		/* output is pipe */
+#define	XPIPE	(XPIPEI|XPIPEO)	/* member of pipe */
+#define	XXCOM	BIT(5)		/* `...` command */
+#define	XPCLOSE	BIT(6)		/* exchild: close close_fd in parent */
+#define	XCCLOSE	BIT(7)		/* exchild: close close_fd in child */
+#define XERROK	BIT(8)		/* non-zero exit ok (for set -e) */
+#define XCOPROC BIT(9)		/* starting a co-process */
+#define XTIME	BIT(10)		/* timing TCOM command */
+
+/*
+ * flags to control expansion of words (assumed by t->evalflags to fit
+ * in a short)
+ */
+#define	DOBLANK	BIT(0)		/* perform blank interpretation */
+#define	DOGLOB	BIT(1)		/* expand [?* */
+#define	DOPAT	BIT(2)		/* quote *?[ */
+#define	DOTILDE	BIT(3)		/* normal ~ expansion (first char) */
+#define DONTRUNCOMMAND BIT(4)	/* do not run $(command) things */
+#define DOASNTILDE BIT(5)	/* assignment ~ expansion (after =, :) */
+#define DOBRACE_ BIT(6)		/* used by expand(): do brace expansion */
+#define DOMAGIC_ BIT(7)		/* used by expand(): string contains MAGIC */
+#define DOTEMP_	BIT(8)		/* ditto : in word part of ${..[%#=?]..} */
+#define DOVACHECK BIT(9)	/* var assign check (for typeset, set, etc) */
+#define DOMARKDIRS BIT(10)	/* force markdirs behaviour */
+
+/*
+ * The arguments of [[ .. ]] expressions are kept in t->args[] and flags
+ * indicating how the arguments have been munged are kept in t->vars[].
+ * The contents of t->vars[] are stuffed strings (so they can be treated
+ * like all other t->vars[]) in which the second character is the one that
+ * is examined.  The DB_* defines are the values for these second characters.
+ */
+#define DB_NORM	1		/* normal argument */
+#define DB_OR	2		/* || -> -o conversion */
+#define DB_AND	3		/* && -> -a conversion */
+#define DB_BE	4		/* an inserted -BE */
+#define DB_PAT	5		/* a pattern argument */
+
+void	fptreef(struct shf *, int, const char *, ...);
+char *	snptreef(char *, int, const char *, ...);
+struct op *	tcopy(struct op *, Area *);
+char *	wdcopy(const char *, Area *);
+char *	wdscan(const char *, int);
+char *	wdstrip(const char *);
+void	tfree(struct op *, Area *);
diff --git a/tty.c b/tty.c
@@ -0,0 +1,59 @@
+/*	$OpenBSD: tty.c,v 1.17 2018/03/15 16:51:29 anton Exp $	*/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "sh.h"
+#include "tty.h"
+
+int		tty_fd = -1;	/* dup'd tty file descriptor */
+int		tty_devtty;	/* true if tty_fd is from /dev/tty */
+struct termios	tty_state;	/* saved tty state */
+
+void
+tty_close(void)
+{
+	if (tty_fd >= 0) {
+		close(tty_fd);
+		tty_fd = -1;
+	}
+}
+
+/* Initialize tty_fd.  Used for saving/resetting tty modes upon
+ * foreground job completion and for setting up tty process group.
+ */
+void
+tty_init(int init_ttystate)
+{
+	int	do_close = 1;
+	int	tfd;
+
+	tty_close();
+	tty_devtty = 1;
+
+	tfd = open("/dev/tty", O_RDWR, 0);
+	if (tfd < 0) {
+		tty_devtty = 0;
+		warningf(false, "No controlling tty (open /dev/tty: %s)",
+		    strerror(errno));
+
+		do_close = 0;
+		if (isatty(0))
+			tfd = 0;
+		else if (isatty(2))
+			tfd = 2;
+		else {
+			warningf(false, "Can't find tty file descriptor");
+			return;
+		}
+	}
+	if ((tty_fd = fcntl(tfd, F_DUPFD_CLOEXEC, FDBASE)) < 0) {
+		warningf(false, "%s: dup of tty fd failed: %s",
+		    __func__, strerror(errno));
+	} else if (init_ttystate)
+		tcgetattr(tty_fd, &tty_state);
+	if (do_close)
+		close(tfd);
+}
diff --git a/tty.h b/tty.h
@@ -0,0 +1,21 @@
+/*	$OpenBSD: tty.h,v 1.6 2015/09/25 11:58:14 nicm Exp $	*/
+
+/*
+	tty.h -- centralized definitions for a variety of terminal interfaces
+
+	created by DPK, Oct. 1986
+
+	Rearranged to work with autoconf, added TTY_state, get_tty/set_tty
+						Michael Rendell, May '94
+
+	last edit:	30-Jul-1987	D A Gwyn
+*/
+
+#include <termios.h>
+
+extern int		tty_fd;		/* dup'd tty file descriptor */
+extern int		tty_devtty;	/* true if tty_fd is from /dev/tty */
+extern struct termios	tty_state;	/* saved tty state */
+
+extern void	tty_init(int);
+extern void	tty_close(void);
diff --git a/unvis.c b/unvis.c
@@ -0,0 +1,284 @@
+/*	$OpenBSD: unvis.c,v 1.17 2015/09/13 11:32:51 guenther Exp $ */
+/*-
+ * Copyright (c) 1989, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * 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 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/types.h>
+#include <ctype.h>
+#include <vis.h>
+
+/*
+ * decode driven by state machine
+ */
+#define	S_GROUND	0	/* haven't seen escape char */
+#define	S_START		1	/* start decoding special sequence */
+#define	S_META		2	/* metachar started (M) */
+#define	S_META1		3	/* metachar more, regular char (-) */
+#define	S_CTRL		4	/* control char started (^) */
+#define	S_OCTAL2	5	/* octal digit 2 */
+#define	S_OCTAL3	6	/* octal digit 3 */
+
+#define	isoctal(c)	(((u_char)(c)) >= '0' && ((u_char)(c)) <= '7')
+
+/*
+ * unvis - decode characters previously encoded by vis
+ */
+int
+unvis(char *cp, char c, int *astate, int flag)
+{
+
+	if (flag & UNVIS_END) {
+		if (*astate == S_OCTAL2 || *astate == S_OCTAL3) {
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		} 
+		return (*astate == S_GROUND ? UNVIS_NOCHAR : UNVIS_SYNBAD);
+	}
+
+	switch (*astate) {
+
+	case S_GROUND:
+		*cp = 0;
+		if (c == '\\') {
+			*astate = S_START;
+			return (0);
+		} 
+		*cp = c;
+		return (UNVIS_VALID);
+
+	case S_START:
+		switch(c) {
+		case '-':
+			*cp = 0;
+			*astate = S_GROUND;
+			return (0);
+		case '\\':
+		case '"':
+			*cp = c;
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case '0': case '1': case '2': case '3':
+		case '4': case '5': case '6': case '7':
+			*cp = (c - '0');
+			*astate = S_OCTAL2;
+			return (0);
+		case 'M':
+			*cp = (char) 0200;
+			*astate = S_META;
+			return (0);
+		case '^':
+			*astate = S_CTRL;
+			return (0);
+		case 'n':
+			*cp = '\n';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case 'r':
+			*cp = '\r';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case 'b':
+			*cp = '\b';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case 'a':
+			*cp = '\007';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case 'v':
+			*cp = '\v';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case 't':
+			*cp = '\t';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case 'f':
+			*cp = '\f';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case 's':
+			*cp = ' ';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case 'E':
+			*cp = '\033';
+			*astate = S_GROUND;
+			return (UNVIS_VALID);
+		case '\n':
+			/*
+			 * hidden newline
+			 */
+			*astate = S_GROUND;
+			return (UNVIS_NOCHAR);
+		case '$':
+			/*
+			 * hidden marker
+			 */
+			*astate = S_GROUND;
+			return (UNVIS_NOCHAR);
+		}
+		*astate = S_GROUND;
+		return (UNVIS_SYNBAD);
+		 
+	case S_META:
+		if (c == '-')
+			*astate = S_META1;
+		else if (c == '^')
+			*astate = S_CTRL;
+		else {
+			*astate = S_GROUND;
+			return (UNVIS_SYNBAD);
+		}
+		return (0);
+		 
+	case S_META1:
+		*astate = S_GROUND;
+		*cp |= c;
+		return (UNVIS_VALID);
+		 
+	case S_CTRL:
+		if (c == '?')
+			*cp |= 0177;
+		else
+			*cp |= c & 037;
+		*astate = S_GROUND;
+		return (UNVIS_VALID);
+
+	case S_OCTAL2:	/* second possible octal digit */
+		if (isoctal(c)) {
+			/* 
+			 * yes - and maybe a third 
+			 */
+			*cp = (*cp << 3) + (c - '0');
+			*astate = S_OCTAL3;	
+			return (0);
+		} 
+		/* 
+		 * no - done with current sequence, push back passed char 
+		 */
+		*astate = S_GROUND;
+		return (UNVIS_VALIDPUSH);
+
+	case S_OCTAL3:	/* third possible octal digit */
+		*astate = S_GROUND;
+		if (isoctal(c)) {
+			*cp = (*cp << 3) + (c - '0');
+			return (UNVIS_VALID);
+		}
+		/*
+		 * we were done, push back passed char
+		 */
+		return (UNVIS_VALIDPUSH);
+
+	default:	
+		/* 
+		 * decoder in unknown state - (probably uninitialized) 
+		 */
+		*astate = S_GROUND;
+		return (UNVIS_SYNBAD);
+	}
+}
+
+/*
+ * strunvis - decode src into dst 
+ *
+ *	Number of chars decoded into dst is returned, -1 on error.
+ *	Dst is null terminated.
+ */
+
+int
+strunvis(char *dst, const char *src)
+{
+	char c;
+	char *start = dst;
+	int state = 0;
+
+	while ((c = *src++)) {
+	again:
+		switch (unvis(dst, c, &state, 0)) {
+		case UNVIS_VALID:
+			dst++;
+			break;
+		case UNVIS_VALIDPUSH:
+			dst++;
+			goto again;
+		case 0:
+		case UNVIS_NOCHAR:
+			break;
+		default:
+			*dst = '\0';
+			return (-1);
+		}
+	}
+	if (unvis(dst, c, &state, UNVIS_END) == UNVIS_VALID)
+		dst++;
+	*dst = '\0';
+	return (dst - start);
+}
+
+ssize_t
+strnunvis(char *dst, const char *src, size_t sz)
+{
+	char c, p;
+	char *start = dst, *end = dst + sz - 1;
+	int state = 0;
+
+	if (sz > 0)
+		*end = '\0';
+	while ((c = *src++)) {
+	again:
+		switch (unvis(&p, c, &state, 0)) {
+		case UNVIS_VALID:
+			if (dst < end)
+				*dst = p;
+			dst++;
+			break;
+		case UNVIS_VALIDPUSH:
+			if (dst < end)
+				*dst = p;
+			dst++;
+			goto again;
+		case 0:
+		case UNVIS_NOCHAR:
+			break;
+		default:
+			if (dst <= end)
+				*dst = '\0';
+			return (-1);
+		}
+	}
+	if (unvis(&p, c, &state, UNVIS_END) == UNVIS_VALID) {
+		if (dst < end)
+			*dst = p;
+		dst++;
+	}
+	if (dst <= end)
+		*dst = '\0';
+	return (dst - start);
+}
diff --git a/var.c b/var.c
@@ -0,0 +1,1232 @@
+/*	$OpenBSD: var.c,v 1.70 2018/06/18 21:46:05 millert Exp $	*/
+
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#ifndef SMALL
+# include <term.h>
+# include <curses.h>
+#endif
+
+#include "sh.h"
+
+/*
+ * Variables
+ *
+ * WARNING: unreadable code, needs a rewrite
+ *
+ * if (flag&INTEGER), val.i contains integer value, and type contains base.
+ * otherwise, (val.s + type) contains string value.
+ * if (flag&EXPORT), val.s contains "name=value" for E-Z exporting.
+ */
+static	struct tbl vtemp;
+static	struct table specials;
+static char	*formatstr(struct tbl *, const char *);
+static void	export(struct tbl *, const char *);
+static int	special(const char *);
+static void	unspecial(const char *);
+static void	getspec(struct tbl *);
+static void	setspec(struct tbl *);
+static void	unsetspec(struct tbl *);
+static struct tbl *arraysearch(struct tbl *, int);
+
+/*
+ * create a new block for function calls and simple commands
+ * assume caller has allocated and set up genv->loc
+ */
+void
+newblock(void)
+{
+	struct block *l;
+	static char *const empty[] = {null};
+
+	l = alloc(sizeof(struct block), ATEMP);
+	l->flags = 0;
+	ainit(&l->area); /* todo: could use genv->area (l->area => l->areap) */
+	if (!genv->loc) {
+		l->argc = 0;
+		l->argv = (char **) empty;
+	} else {
+		l->argc = genv->loc->argc;
+		l->argv = genv->loc->argv;
+	}
+	l->exit = l->error = NULL;
+	ktinit(&l->vars, &l->area, 0);
+	ktinit(&l->funs, &l->area, 0);
+	l->next = genv->loc;
+	genv->loc = l;
+}
+
+/*
+ * pop a block handling special variables
+ */
+void
+popblock(void)
+{
+	struct block *l = genv->loc;
+	struct tbl *vp, **vpp = l->vars.tbls, *vq;
+	int i;
+
+	genv->loc = l->next;	/* pop block */
+	for (i = l->vars.size; --i >= 0; )
+		if ((vp = *vpp++) != NULL && (vp->flag&SPECIAL)) {
+			if ((vq = global(vp->name))->flag & ISSET)
+				setspec(vq);
+			else
+				unsetspec(vq);
+		}
+	if (l->flags & BF_DOGETOPTS)
+		user_opt = l->getopts_state;
+	afreeall(&l->area);
+	afree(l, ATEMP);
+}
+
+/* called by main() to initialize variable data structures */
+void
+initvar(void)
+{
+	static const struct {
+		const char *name;
+		int v;
+	} names[] = {
+		{ "COLUMNS",		V_COLUMNS },
+		{ "IFS",		V_IFS },
+		{ "OPTIND",		V_OPTIND },
+		{ "PATH",		V_PATH },
+		{ "POSIXLY_CORRECT",	V_POSIXLY_CORRECT },
+		{ "TMPDIR",		V_TMPDIR },
+		{ "HISTCONTROL",	V_HISTCONTROL },
+		{ "HISTFILE",		V_HISTFILE },
+		{ "HISTSIZE",		V_HISTSIZE },
+		{ "EDITOR",		V_EDITOR },
+		{ "VISUAL",		V_VISUAL },
+		{ "MAIL",		V_MAIL },
+		{ "MAILCHECK",		V_MAILCHECK },
+		{ "MAILPATH",		V_MAILPATH },
+		{ "RANDOM",		V_RANDOM },
+		{ "SECONDS",		V_SECONDS },
+		{ "TMOUT",		V_TMOUT },
+		{ "LINENO",		V_LINENO },
+		{ "TERM",		V_TERM },
+		{ NULL,	0 }
+	};
+	int i;
+	struct tbl *tp;
+
+	ktinit(&specials, APERM, 32); /* must be 2^n (currently 19 specials) */
+	for (i = 0; names[i].name; i++) {
+		tp = ktenter(&specials, names[i].name, hash(names[i].name));
+		tp->flag = DEFINED|ISSET;
+		tp->type = names[i].v;
+	}
+}
+
+/* Used to calculate an array index for global()/local().  Sets *arrayp to
+ * non-zero if this is an array, sets *valp to the array index, returns
+ * the basename of the array.
+ */
+static const char *
+array_index_calc(const char *n, bool *arrayp, int *valp)
+{
+	const char *p;
+	int len;
+
+	*arrayp = false;
+	p = skip_varname(n, false);
+	if (p != n && *p == '[' && (len = array_ref_len(p))) {
+		char *sub, *tmp;
+		int64_t rval;
+
+		/* Calculate the value of the subscript */
+		*arrayp = true;
+		tmp = str_nsave(p+1, len-2, ATEMP);
+		sub = substitute(tmp, 0);
+		afree(tmp, ATEMP);
+		n = str_nsave(n, p - n, ATEMP);
+		evaluate(sub, &rval, KSH_UNWIND_ERROR, true);
+		if (rval < 0 || rval > INT_MAX)
+			errorf("%s: subscript %" PRIi64 " out of range",
+			    n, rval);
+		*valp = rval;
+		afree(sub, ATEMP);
+	}
+	return n;
+}
+
+/*
+ * Search for variable, if not found create globally.
+ */
+struct tbl *
+global(const char *n)
+{
+	struct block *l = genv->loc;
+	struct tbl *vp;
+	long	 num;
+	int c;
+	unsigned int h;
+	bool	 array;
+	int	 val;
+
+	/* Check to see if this is an array */
+	n = array_index_calc(n, &array, &val);
+	h = hash(n);
+	c = (unsigned char)n[0];
+	if (!letter(c)) {
+		if (array)
+			errorf("bad substitution");
+		vp = &vtemp;
+		vp->flag = DEFINED;
+		vp->type = 0;
+		vp->areap = ATEMP;
+		*vp->name = c;
+		if (digit(c)) {
+			errno = 0;
+			num = strtol(n, NULL, 10);
+			if (errno == 0 && num <= l->argc)
+				/* setstr can't fail here */
+				setstr(vp, l->argv[num], KSH_RETURN_ERROR);
+			vp->flag |= RDONLY;
+			return vp;
+		}
+		vp->flag |= RDONLY;
+		if (n[1] != '\0')
+			return vp;
+		vp->flag |= ISSET|INTEGER;
+		switch (c) {
+		case '$':
+			vp->val.i = kshpid;
+			break;
+		case '!':
+			/* If no job, expand to nothing */
+			if ((vp->val.i = j_async()) == 0)
+				vp->flag &= ~(ISSET|INTEGER);
+			break;
+		case '?':
+			vp->val.i = exstat;
+			break;
+		case '#':
+			vp->val.i = l->argc;
+			break;
+		case '-':
+			vp->flag &= ~INTEGER;
+			vp->val.s = getoptions();
+			break;
+		default:
+			vp->flag &= ~(ISSET|INTEGER);
+		}
+		return vp;
+	}
+	for (l = genv->loc; ; l = l->next) {
+		vp = ktsearch(&l->vars, n, h);
+		if (vp != NULL) {
+			if (array)
+				return arraysearch(vp, val);
+			else
+				return vp;
+		}
+		if (l->next == NULL)
+			break;
+	}
+	vp = ktenter(&l->vars, n, h);
+	if (array)
+		vp = arraysearch(vp, val);
+	vp->flag |= DEFINED;
+	if (special(n))
+		vp->flag |= SPECIAL;
+	return vp;
+}
+
+/*
+ * Search for local variable, if not found create locally.
+ */
+struct tbl *
+local(const char *n, bool copy)
+{
+	struct block *l = genv->loc;
+	struct tbl *vp;
+	unsigned int h;
+	bool	 array;
+	int	 val;
+
+	/* Check to see if this is an array */
+	n = array_index_calc(n, &array, &val);
+	h = hash(n);
+	if (!letter(*n)) {
+		vp = &vtemp;
+		vp->flag = DEFINED|RDONLY;
+		vp->type = 0;
+		vp->areap = ATEMP;
+		return vp;
+	}
+	vp = ktenter(&l->vars, n, h);
+	if (copy && !(vp->flag & DEFINED)) {
+		struct block *ll = l;
+		struct tbl *vq = NULL;
+
+		while ((ll = ll->next) && !(vq = ktsearch(&ll->vars, n, h)))
+			;
+		if (vq) {
+			vp->flag |= vq->flag &
+			    (EXPORT | INTEGER | RDONLY | LJUST | RJUST |
+			    ZEROFIL | LCASEV | UCASEV_AL | INT_U | INT_L);
+			if (vq->flag & INTEGER)
+				vp->type = vq->type;
+			vp->u2.field = vq->u2.field;
+		}
+	}
+	if (array)
+		vp = arraysearch(vp, val);
+	vp->flag |= DEFINED;
+	if (special(n))
+		vp->flag |= SPECIAL;
+	return vp;
+}
+
+/* get variable string value */
+char *
+str_val(struct tbl *vp)
+{
+	char *s;
+
+	if ((vp->flag&SPECIAL))
+		getspec(vp);
+	if (!(vp->flag&ISSET))
+		s = null;		/* special to dollar() */
+	else if (!(vp->flag&INTEGER))	/* string source */
+		s = vp->val.s + vp->type;
+	else {				/* integer source */
+		/* worst case number length is when base=2, so use
+		 * minus base # number BITS(int64_t) NUL */
+		char strbuf[1 + 2 + 1 + BITS(int64_t) + 1];
+		const char *digits = (vp->flag & UCASEV_AL) ?
+		    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" :
+		    "0123456789abcdefghijklmnopqrstuvwxyz";
+		uint64_t n;
+		unsigned int base;
+
+		s = strbuf + sizeof(strbuf);
+		if (vp->flag & INT_U)
+			n = (uint64_t) vp->val.i;
+		else
+			n = (vp->val.i < 0) ? -vp->val.i : vp->val.i;
+		base = (vp->type == 0) ? 10 : vp->type;
+		if (base < 2 || base > strlen(digits))
+			base = 10;
+
+		*--s = '\0';
+		do {
+			*--s = digits[n % base];
+			n /= base;
+		} while (n != 0);
+		if (base != 10) {
+			*--s = '#';
+			*--s = digits[base % 10];
+			if (base >= 10)
+				*--s = digits[base / 10];
+		}
+		if (!(vp->flag & INT_U) && vp->val.i < 0)
+			*--s = '-';
+		if (vp->flag & (RJUST|LJUST)) /* case already dealt with */
+			s = formatstr(vp, s);
+		else
+			s = str_save(s, ATEMP);
+	}
+	return s;
+}
+
+/* get variable integer value, with error checking */
+int64_t
+intval(struct tbl *vp)
+{
+	int64_t num;
+	int base;
+
+	base = getint(vp, &num, false);
+	if (base == -1)
+		/* XXX check calls - is error here ok by POSIX? */
+		errorf("%s: bad number", str_val(vp));
+	return num;
+}
+
+/* set variable to string value */
+int
+setstr(struct tbl *vq, const char *s, int error_ok)
+{
+	const char *fs = NULL;
+	int no_ro_check = error_ok & KSH_IGNORE_RDONLY;
+	error_ok &= ~KSH_IGNORE_RDONLY;
+	if ((vq->flag & RDONLY) && !no_ro_check) {
+		warningf(true, "%s: is read only", vq->name);
+		if (!error_ok)
+			errorf(NULL);
+		return 0;
+	}
+	if (!(vq->flag&INTEGER)) { /* string dest */
+		if ((vq->flag&ALLOC)) {
+			/* debugging */
+			if (s >= vq->val.s &&
+			    s <= vq->val.s + strlen(vq->val.s))
+				internal_errorf("%s: %s=%s: assigning to self",
+				    __func__, vq->name, s);
+			afree(vq->val.s, vq->areap);
+		}
+		vq->flag &= ~(ISSET|ALLOC);
+		vq->type = 0;
+		if (s && (vq->flag & (UCASEV_AL|LCASEV|LJUST|RJUST)))
+			fs = s = formatstr(vq, s);
+		if ((vq->flag&EXPORT))
+			export(vq, s);
+		else {
+			vq->val.s = str_save(s, vq->areap);
+			vq->flag |= ALLOC;
+		}
+	} else {		/* integer dest */
+		if (!v_evaluate(vq, s, error_ok, true))
+			return 0;
+	}
+	vq->flag |= ISSET;
+	if ((vq->flag&SPECIAL))
+		setspec(vq);
+	afree((void *)fs, ATEMP);
+	return 1;
+}
+
+/* set variable to integer */
+void
+setint(struct tbl *vq, int64_t n)
+{
+	if (!(vq->flag&INTEGER)) {
+		struct tbl *vp = &vtemp;
+		vp->flag = (ISSET|INTEGER);
+		vp->type = 0;
+		vp->areap = ATEMP;
+		vp->val.i = n;
+		/* setstr can't fail here */
+		setstr(vq, str_val(vp), KSH_RETURN_ERROR);
+	} else
+		vq->val.i = n;
+	vq->flag |= ISSET;
+	if ((vq->flag&SPECIAL))
+		setspec(vq);
+}
+
+int
+getint(struct tbl *vp, int64_t *nump, bool arith)
+{
+	char *s;
+	int c;
+	int base, neg;
+	int have_base = 0;
+	int64_t num;
+
+	if (vp->flag&SPECIAL)
+		getspec(vp);
+	/* XXX is it possible for ISSET to be set and val.s to be 0? */
+	if (!(vp->flag&ISSET) || (!(vp->flag&INTEGER) && vp->val.s == NULL))
+		return -1;
+	if (vp->flag&INTEGER) {
+		*nump = vp->val.i;
+		return vp->type;
+	}
+	s = vp->val.s + vp->type;
+	if (s == NULL)	/* redundant given initial test */
+		s = null;
+	base = 10;
+	num = 0;
+	neg = 0;
+	if (arith && *s == '0' && *(s+1)) {
+		s++;
+		if (*s == 'x' || *s == 'X') {
+			s++;
+			base = 16;
+		} else if (vp->flag & ZEROFIL) {
+			while (*s == '0')
+				s++;
+		} else
+			base = 8;
+		have_base++;
+	}
+	for (c = (unsigned char)*s++; c ; c = (unsigned char)*s++) {
+		if (c == '-') {
+			neg++;
+		} else if (c == '#') {
+			base = (int) num;
+			if (have_base || base < 2 || base > 36)
+				return -1;
+			num = 0;
+			have_base = 1;
+		} else if (letnum(c)) {
+			if (isdigit(c))
+				c -= '0';
+			else if (islower(c))
+				c -= 'a' - 10; /* todo: assumes ascii */
+			else if (isupper(c))
+				c -= 'A' - 10; /* todo: assumes ascii */
+			else
+				c = -1; /* _: force error */
+			if (c < 0 || c >= base)
+				return -1;
+			num = num * base + c;
+		} else
+			return -1;
+	}
+	if (neg)
+		num = -num;
+	*nump = num;
+	return base;
+}
+
+/* convert variable vq to integer variable, setting its value from vp
+ * (vq and vp may be the same)
+ */
+struct tbl *
+setint_v(struct tbl *vq, struct tbl *vp, bool arith)
+{
+	int base;
+	int64_t num;
+
+	if ((base = getint(vp, &num, arith)) == -1)
+		return NULL;
+	if (!(vq->flag & INTEGER) && (vq->flag & ALLOC)) {
+		vq->flag &= ~ALLOC;
+		afree(vq->val.s, vq->areap);
+	}
+	vq->val.i = num;
+	if (vq->type == 0) /* default base */
+		vq->type = base;
+	vq->flag |= ISSET|INTEGER;
+	if (vq->flag&SPECIAL)
+		setspec(vq);
+	return vq;
+}
+
+static char *
+formatstr(struct tbl *vp, const char *s)
+{
+	int olen, nlen;
+	char *p, *q;
+
+	olen = strlen(s);
+
+	if (vp->flag & (RJUST|LJUST)) {
+		if (!vp->u2.field)	/* default field width */
+			vp->u2.field = olen;
+		nlen = vp->u2.field;
+	} else
+		nlen = olen;
+
+	p = alloc(nlen + 1, ATEMP);
+	if (vp->flag & (RJUST|LJUST)) {
+		int slen;
+
+		if (vp->flag & RJUST) {
+			const char *r = s + olen;
+			/* strip trailing spaces (at&t ksh uses r[-1] == ' ') */
+			while (r > s && isspace((unsigned char)r[-1]))
+				--r;
+			slen = r - s;
+			if (slen > vp->u2.field) {
+				s += slen - vp->u2.field;
+				slen = vp->u2.field;
+			}
+			shf_snprintf(p, nlen + 1,
+				((vp->flag & ZEROFIL) && digit(*s)) ?
+					  "%0*s%.*s" : "%*s%.*s",
+				vp->u2.field - slen, null, slen, s);
+		} else {
+			/* strip leading spaces/zeros */
+			while (isspace((unsigned char)*s))
+				s++;
+			if (vp->flag & ZEROFIL)
+				while (*s == '0')
+					s++;
+			shf_snprintf(p, nlen + 1, "%-*.*s",
+				vp->u2.field, vp->u2.field, s);
+		}
+	} else
+		memcpy(p, s, olen + 1);
+
+	if (vp->flag & UCASEV_AL) {
+		for (q = p; *q; q++)
+			if (islower((unsigned char)*q))
+				*q = toupper((unsigned char)*q);
+	} else if (vp->flag & LCASEV) {
+		for (q = p; *q; q++)
+			if (isupper((unsigned char)*q))
+				*q = tolower((unsigned char)*q);
+	}
+
+	return p;
+}
+
+/*
+ * make vp->val.s be "name=value" for quick exporting.
+ */
+static void
+export(struct tbl *vp, const char *val)
+{
+	char *xp;
+	char *op = (vp->flag&ALLOC) ? vp->val.s : NULL;
+	int namelen = strlen(vp->name);
+	int vallen = strlen(val) + 1;
+
+	vp->flag |= ALLOC;
+	xp = alloc(namelen + 1 + vallen, vp->areap);
+	memcpy(vp->val.s = xp, vp->name, namelen);
+	xp += namelen;
+	*xp++ = '=';
+	vp->type = xp - vp->val.s; /* offset to value */
+	memcpy(xp, val, vallen);
+	afree(op, vp->areap);
+}
+
+/*
+ * lookup variable (according to (set&LOCAL)),
+ * set its attributes (INTEGER, RDONLY, EXPORT, TRACE, LJUST, RJUST, ZEROFIL,
+ * LCASEV, UCASEV_AL), and optionally set its value if an assignment.
+ */
+struct tbl *
+typeset(const char *var, int set, int clr, int field, int base)
+{
+	struct tbl *vp;
+	struct tbl *vpbase, *t;
+	char *tvar;
+	const char *val;
+
+	/* check for valid variable name, search for value */
+	val = skip_varname(var, false);
+	if (val == var)
+		return NULL;
+	if (*val == '[') {
+		int len;
+
+		len = array_ref_len(val);
+		if (len == 0)
+			return NULL;
+		/* IMPORT is only used when the shell starts up and is
+		 * setting up its environment.  Allow only simple array
+		 * references at this time since parameter/command substitution
+		 * is preformed on the [expression], which would be a major
+		 * security hole.
+		 */
+		if (set & IMPORT) {
+			int i;
+			for (i = 1; i < len - 1; i++)
+				if (!digit(val[i]))
+					return NULL;
+		}
+		val += len;
+	}
+	if (*val == '=')
+		tvar = str_nsave(var, val++ - var, ATEMP);
+	else {
+		/* Importing from original environment: must have an = */
+		if (set & IMPORT)
+			return NULL;
+		tvar = (char *) var;
+		val = NULL;
+	}
+
+	/* Prevent typeset from creating a local PATH/ENV/SHELL */
+	if (Flag(FRESTRICTED) && (strcmp(tvar, "PATH") == 0 ||
+	    strcmp(tvar, "ENV") == 0 || strcmp(tvar, "SHELL") == 0))
+		errorf("%s: restricted", tvar);
+
+	vp = (set&LOCAL) ? local(tvar, (set & LOCAL_COPY) ? true : false) :
+	    global(tvar);
+	set &= ~(LOCAL|LOCAL_COPY);
+
+	vpbase = (vp->flag & ARRAY) ? global(arrayname(var)) : vp;
+
+	/* only allow export flag to be set.  at&t ksh allows any attribute to
+	 * be changed, which means it can be truncated or modified
+	 * (-L/-R/-Z/-i).
+	 */
+	if ((vpbase->flag&RDONLY) &&
+	    (val || clr || (set & ~EXPORT)))
+		/* XXX check calls - is error here ok by POSIX? */
+		errorf("%s: is read only", tvar);
+	if (val)
+		afree(tvar, ATEMP);
+
+	/* most calls are with set/clr == 0 */
+	if (set | clr) {
+		int ok = 1;
+		/* XXX if x[0] isn't set, there will be problems: need to have
+		 * one copy of attributes for arrays...
+		 */
+		for (t = vpbase; t; t = t->u.array) {
+			int fake_assign;
+			int error_ok = KSH_RETURN_ERROR;
+			char *s = NULL;
+			char *free_me = NULL;
+
+			fake_assign = (t->flag & ISSET) && (!val || t != vp) &&
+			    ((set & (UCASEV_AL|LCASEV|LJUST|RJUST|ZEROFIL)) ||
+			    ((t->flag & INTEGER) && (clr & INTEGER)) ||
+			    (!(t->flag & INTEGER) && (set & INTEGER)));
+			if (fake_assign) {
+				if (t->flag & INTEGER) {
+					s = str_val(t);
+					free_me = NULL;
+				} else {
+					s = t->val.s + t->type;
+					free_me = (t->flag & ALLOC) ? t->val.s :
+					    NULL;
+				}
+				t->flag &= ~ALLOC;
+			}
+			if (!(t->flag & INTEGER) && (set & INTEGER)) {
+				t->type = 0;
+				t->flag &= ~ALLOC;
+			}
+			if (!(t->flag & RDONLY) && (set & RDONLY)) {
+				/* allow var to be initialized read-only */
+				error_ok |= KSH_IGNORE_RDONLY;
+			}
+			t->flag = (t->flag | set) & ~clr;
+			/* Don't change base if assignment is to be done,
+			 * in case assignment fails.
+			 */
+			if ((set & INTEGER) && base > 0 && (!val || t != vp))
+				t->type = base;
+			if (set & (LJUST|RJUST|ZEROFIL))
+				t->u2.field = field;
+			if (fake_assign) {
+				if (!setstr(t, s, error_ok)) {
+					/* Somewhat arbitrary action here:
+					 * zap contents of variable, but keep
+					 * the flag settings.
+					 */
+					ok = 0;
+					if (t->flag & INTEGER)
+						t->flag &= ~ISSET;
+					else {
+						if (t->flag & ALLOC)
+							afree(t->val.s, t->areap);
+						t->flag &= ~(ISSET|ALLOC);
+						t->type = 0;
+					}
+				}
+				afree(free_me, t->areap);
+			}
+		}
+		if (!ok)
+		    errorf(NULL);
+	}
+
+	if (val != NULL) {
+		if (vp->flag&INTEGER) {
+			/* do not zero base before assignment */
+			setstr(vp, val, KSH_UNWIND_ERROR | KSH_IGNORE_RDONLY);
+			/* Done after assignment to override default */
+			if (base > 0)
+				vp->type = base;
+		} else
+			/* setstr can't fail (readonly check already done) */
+			setstr(vp, val, KSH_RETURN_ERROR | KSH_IGNORE_RDONLY);
+	}
+
+	/* only x[0] is ever exported, so use vpbase */
+	if ((vpbase->flag&EXPORT) && !(vpbase->flag&INTEGER) &&
+	    vpbase->type == 0)
+		export(vpbase, (vpbase->flag&ISSET) ? vpbase->val.s : null);
+
+	return vp;
+}
+
+/* Unset a variable.  array_ref is set if there was an array reference in
+ * the name lookup (eg, x[2]).
+ */
+void
+unset(struct tbl *vp, int array_ref)
+{
+	if (vp->flag & ALLOC)
+		afree(vp->val.s, vp->areap);
+	if ((vp->flag & ARRAY) && !array_ref) {
+		struct tbl *a, *tmp;
+
+		/* Free up entire array */
+		for (a = vp->u.array; a; ) {
+			tmp = a;
+			a = a->u.array;
+			if (tmp->flag & ALLOC)
+				afree(tmp->val.s, tmp->areap);
+			afree(tmp, tmp->areap);
+		}
+		vp->u.array = NULL;
+	}
+	/* If foo[0] is being unset, the remainder of the array is kept... */
+	vp->flag &= SPECIAL | (array_ref ? ARRAY|DEFINED : 0);
+	if (vp->flag & SPECIAL)
+		unsetspec(vp);	/* responsible for `unspecial'ing var */
+}
+
+/* return a pointer to the first char past a legal variable name (returns the
+ * argument if there is no legal name, returns * a pointer to the terminating
+ * null if whole string is legal).
+ */
+char *
+skip_varname(const char *s, int aok)
+{
+	int alen;
+
+	if (s && letter(*s)) {
+		while (*++s && letnum(*s))
+			;
+		if (aok && *s == '[' && (alen = array_ref_len(s)))
+			s += alen;
+	}
+	return (char *) s;
+}
+
+/* Return a pointer to the first character past any legal variable name.  */
+char *
+skip_wdvarname(const char *s,
+    int aok)				/* skip array de-reference? */
+{
+	if (s[0] == CHAR && letter(s[1])) {
+		do {
+			s += 2;
+		} while (s[0] == CHAR && letnum(s[1]));
+		if (aok && s[0] == CHAR && s[1] == '[') {
+			/* skip possible array de-reference */
+			const char *p = s;
+			char c;
+			int depth = 0;
+
+			while (1) {
+				if (p[0] != CHAR)
+					break;
+				c = p[1];
+				p += 2;
+				if (c == '[')
+					depth++;
+				else if (c == ']' && --depth == 0) {
+					s = p;
+					break;
+				}
+			}
+		}
+	}
+	return (char *) s;
+}
+
+/* Check if coded string s is a variable name */
+int
+is_wdvarname(const char *s, int aok)
+{
+	char *p = skip_wdvarname(s, aok);
+
+	return p != s && p[0] == EOS;
+}
+
+/* Check if coded string s is a variable assignment */
+int
+is_wdvarassign(const char *s)
+{
+	char *p = skip_wdvarname(s, true);
+
+	return p != s && p[0] == CHAR && p[1] == '=';
+}
+
+/*
+ * Make the exported environment from the exported names in the dictionary.
+ */
+char **
+makenv(void)
+{
+	struct block *l;
+	XPtrV env;
+	struct tbl *vp, **vpp;
+	int i;
+
+	XPinit(env, 64);
+	for (l = genv->loc; l != NULL; l = l->next)
+		for (vpp = l->vars.tbls, i = l->vars.size; --i >= 0; )
+			if ((vp = *vpp++) != NULL &&
+			    (vp->flag&(ISSET|EXPORT)) == (ISSET|EXPORT)) {
+				struct block *l2;
+				struct tbl *vp2;
+				unsigned int h = hash(vp->name);
+
+				/* unexport any redefined instances */
+				for (l2 = l->next; l2 != NULL; l2 = l2->next) {
+					vp2 = ktsearch(&l2->vars, vp->name, h);
+					if (vp2 != NULL)
+						vp2->flag &= ~EXPORT;
+				}
+				if ((vp->flag&INTEGER)) {
+					/* integer to string */
+					char *val;
+					val = str_val(vp);
+					vp->flag &= ~(INTEGER|RDONLY);
+					/* setstr can't fail here */
+					setstr(vp, val, KSH_RETURN_ERROR);
+				}
+				XPput(env, vp->val.s);
+			}
+	XPput(env, NULL);
+	return (char **) XPclose(env);
+}
+
+/*
+ * Called after a fork in parent to bump the random number generator.
+ * Done to ensure children will not get the same random number sequence
+ * if the parent doesn't use $RANDOM.
+ */
+void
+change_random(void)
+{
+	rand();
+}
+
+/*
+ * handle special variables with side effects - PATH, SECONDS.
+ */
+
+/* Test if name is a special parameter */
+static int
+special(const char *name)
+{
+	struct tbl *tp;
+
+	tp = ktsearch(&specials, name, hash(name));
+	return tp && (tp->flag & ISSET) ? tp->type : V_NONE;
+}
+
+/* Make a variable non-special */
+static void
+unspecial(const char *name)
+{
+	struct tbl *tp;
+
+	tp = ktsearch(&specials, name, hash(name));
+	if (tp)
+		ktdelete(tp);
+}
+
+static	struct	timespec seconds;	/* time SECONDS last set */
+static	int	user_lineno;		/* what user set $LINENO to */
+
+static void
+getspec(struct tbl *vp)
+{
+	switch (special(vp->name)) {
+	case V_SECONDS:
+		vp->flag &= ~SPECIAL;
+		/* On start up the value of SECONDS is used before seconds
+		 * has been set - don't do anything in this case
+		 * (see initcoms[] in main.c).
+		 */
+		if (vp->flag & ISSET) {
+			struct timespec difference, now;
+
+			clock_gettime(CLOCK_MONOTONIC, &now);
+			timespecsub(&now, &seconds, &difference);
+			setint(vp, (int64_t)difference.tv_sec);
+		}
+		vp->flag |= SPECIAL;
+		break;
+	case V_RANDOM:
+		vp->flag &= ~SPECIAL;
+		setint(vp, (int64_t) (rand() & 0x7fff));
+		vp->flag |= SPECIAL;
+		break;
+	case V_HISTSIZE:
+		vp->flag &= ~SPECIAL;
+		setint(vp, (int64_t) histsize);
+		vp->flag |= SPECIAL;
+		break;
+	case V_OPTIND:
+		vp->flag &= ~SPECIAL;
+		setint(vp, (int64_t) user_opt.uoptind);
+		vp->flag |= SPECIAL;
+		break;
+	case V_LINENO:
+		vp->flag &= ~SPECIAL;
+		setint(vp, (int64_t) current_lineno + user_lineno);
+		vp->flag |= SPECIAL;
+		break;
+	}
+}
+
+static void
+setspec(struct tbl *vp)
+{
+	char *s;
+
+	switch (special(vp->name)) {
+	case V_PATH:
+		afree(search_path, APERM);
+		search_path = str_save(str_val(vp), APERM);
+		flushcom(1);	/* clear tracked aliases */
+		break;
+	case V_IFS:
+		setctypes(s = str_val(vp), C_IFS);
+		ifs0 = *s;
+		break;
+	case V_OPTIND:
+		vp->flag &= ~SPECIAL;
+		getopts_reset((int) intval(vp));
+		vp->flag |= SPECIAL;
+		break;
+	case V_POSIXLY_CORRECT:
+		change_flag(FPOSIX, OF_SPECIAL, 1);
+		break;
+	case V_TMPDIR:
+		afree(tmpdir, APERM);
+		tmpdir = NULL;
+		/* Use tmpdir iff it is an absolute path, is writable and
+		 * searchable and is a directory...
+		 */
+		{
+			struct stat statb;
+
+			s = str_val(vp);
+			if (s[0] == '/' && access(s, W_OK|X_OK) == 0 &&
+			    stat(s, &statb) == 0 && S_ISDIR(statb.st_mode))
+				tmpdir = str_save(s, APERM);
+		}
+		break;
+	case V_HISTCONTROL:
+		sethistcontrol(str_val(vp));
+		break;
+	case V_HISTSIZE:
+		vp->flag &= ~SPECIAL;
+		sethistsize((int) intval(vp));
+		vp->flag |= SPECIAL;
+		break;
+	case V_HISTFILE:
+		sethistfile(str_val(vp));
+		break;
+	case V_VISUAL:
+		set_editmode(str_val(vp));
+		break;
+	case V_EDITOR:
+		if (!(global("VISUAL")->flag & ISSET))
+			set_editmode(str_val(vp));
+		break;
+	case V_COLUMNS:
+		{
+			int64_t l;
+
+			if (getint(vp, &l, false) == -1) {
+				x_cols = MIN_COLS;
+				break;
+			}
+			if (l <= MIN_COLS || l > INT_MAX)
+				x_cols = MIN_COLS;
+			else
+				x_cols = l;
+		}
+		break;
+	case V_MAIL:
+		mbset(str_val(vp));
+		break;
+	case V_MAILPATH:
+		mpset(str_val(vp));
+		break;
+	case V_MAILCHECK:
+		vp->flag &= ~SPECIAL;
+		mcset(intval(vp));
+		vp->flag |= SPECIAL;
+		break;
+	case V_RANDOM:
+		vp->flag &= ~SPECIAL;
+		srand((unsigned int)intval(vp));
+		vp->flag |= SPECIAL;
+		break;
+	case V_SECONDS:
+		vp->flag &= ~SPECIAL;
+		clock_gettime(CLOCK_MONOTONIC, &seconds);
+		seconds.tv_sec -= intval(vp);
+		vp->flag |= SPECIAL;
+		break;
+	case V_TMOUT:
+		/* at&t ksh seems to do this (only listen if integer) */
+		if (vp->flag & INTEGER)
+			ksh_tmout = vp->val.i >= 0 ? vp->val.i : 0;
+		break;
+	case V_LINENO:
+		vp->flag &= ~SPECIAL;
+		/* The -1 is because line numbering starts at 1. */
+		user_lineno = (unsigned int) intval(vp) - current_lineno - 1;
+		vp->flag |= SPECIAL;
+		break;
+	case V_TERM:
+#ifndef SMALL
+		{
+			int ret;
+
+			vp->flag &= ~SPECIAL;
+			if (setupterm(str_val(vp), shl_out->fd, &ret) == ERR)
+				del_curterm(cur_term);
+			vp->flag |= SPECIAL;
+		}
+#endif
+		break;
+	}
+}
+
+static void
+unsetspec(struct tbl *vp)
+{
+	switch (special(vp->name)) {
+	case V_PATH:
+		afree(search_path, APERM);
+		search_path = str_save(def_path, APERM);
+		flushcom(1);	/* clear tracked aliases */
+		break;
+	case V_IFS:
+		setctypes(" \t\n", C_IFS);
+		ifs0 = ' ';
+		break;
+	case V_TMPDIR:
+		/* should not become unspecial */
+		afree(tmpdir, APERM);
+		tmpdir = NULL;
+		break;
+	case V_MAIL:
+		mbset(NULL);
+		break;
+	case V_MAILPATH:
+		mpset(NULL);
+		break;
+	case V_HISTCONTROL:
+		sethistcontrol(NULL);
+		break;
+	case V_LINENO:
+	case V_MAILCHECK:	/* at&t ksh leaves previous value in place */
+	case V_RANDOM:
+	case V_SECONDS:
+	case V_TMOUT:		/* at&t ksh leaves previous value in place */
+		unspecial(vp->name);
+		break;
+
+	  /* at&t ksh man page says OPTIND, OPTARG and _ lose special meaning,
+	   * but OPTARG does not (still set by getopts) and _ is also still
+	   * set in various places.
+	   * Don't know what at&t does for:
+	   *		MAIL, MAILPATH, HISTSIZE, HISTFILE,
+	   * Unsetting these in at&t ksh does not loose the `specialness':
+	   *    no effect: IFS, COLUMNS, PATH, TMPDIR,
+	   *		VISUAL, EDITOR,
+	   * pdkshisms: no effect:
+	   *		POSIXLY_CORRECT (use set +o posix instead)
+	   */
+	}
+}
+
+/*
+ * Search for (and possibly create) a table entry starting with
+ * vp, indexed by val.
+ */
+static struct tbl *
+arraysearch(struct tbl *vp, int val)
+{
+	struct tbl *prev, *curr, *new;
+	size_t namelen = strlen(vp->name) + 1;
+
+	vp->flag |= ARRAY|DEFINED;
+	vp->index = 0;
+	/* The table entry is always [0] */
+	if (val == 0)
+		return vp;
+	prev = vp;
+	curr = vp->u.array;
+	while (curr && curr->index < val) {
+		prev = curr;
+		curr = curr->u.array;
+	}
+	if (curr && curr->index == val) {
+		if (curr->flag&ISSET)
+			return curr;
+		else
+			new = curr;
+	} else
+		new = alloc(sizeof(struct tbl) + namelen,
+		    vp->areap);
+	strlcpy(new->name, vp->name, namelen);
+	new->flag = vp->flag & ~(ALLOC|DEFINED|ISSET|SPECIAL);
+	new->type = vp->type;
+	new->areap = vp->areap;
+	new->u2.field = vp->u2.field;
+	new->index = val;
+	if (curr != new) {		/* not reusing old array entry */
+		prev->u.array = new;
+		new->u.array = curr;
+	}
+	return new;
+}
+
+/* Return the length of an array reference (eg, [1+2]) - cp is assumed
+ * to point to the open bracket.  Returns 0 if there is no matching closing
+ * bracket.
+ */
+int
+array_ref_len(const char *cp)
+{
+	const char *s = cp;
+	int c;
+	int depth = 0;
+
+	while ((c = *s++) && (c != ']' || --depth))
+		if (c == '[')
+			depth++;
+	if (!c)
+		return 0;
+	return s - cp;
+}
+
+/*
+ * Make a copy of the base of an array name
+ */
+char *
+arrayname(const char *str)
+{
+	const char *p;
+
+	if ((p = strchr(str, '[')) == 0)
+		/* Shouldn't happen, but why worry? */
+		return (char *) str;
+
+	return str_nsave(str, p - str, ATEMP);
+}
+
+/* Set (or overwrite, if !reset) the array variable var to the values in vals.
+ */
+void
+set_array(const char *var, int reset, char **vals)
+{
+	struct tbl *vp, *vq;
+	int i;
+
+	/* to get local array, use "typeset foo; set -A foo" */
+	vp = global(var);
+
+	/* Note: at&t ksh allows set -A but not set +A of a read-only var */
+	if ((vp->flag&RDONLY))
+		errorf("%s: is read only", var);
+	/* This code is quite non-optimal */
+	if (reset > 0)
+		/* trash existing values and attributes */
+		unset(vp, 0);
+	/* todo: would be nice for assignment to completely succeed or
+	 * completely fail.  Only really effects integer arrays:
+	 * evaluation of some of vals[] may fail...
+	 */
+	for (i = 0; vals[i]; i++) {
+		vq = arraysearch(vp, i);
+		/* would be nice to deal with errors here... (see above) */
+		setstr(vq, vals[i], KSH_RETURN_ERROR);
+	}
+}
diff --git a/version.c b/version.c
@@ -0,0 +1,10 @@
+/*	$OpenBSD: version.c,v 1.12 1999/07/14 13:37:24 millert Exp $	*/
+
+/*
+ * value of $KSH_VERSION (or $SH_VERSION)
+ */
+
+#include "sh.h"
+
+const char ksh_version [] =
+	"@(#)PD KSH v5.2.14 99/07/13.2";
diff --git a/vi.c b/vi.c
@@ -0,0 +1,2229 @@
+/*	$OpenBSD: vi.c,v 1.56 2018/03/15 16:51:29 anton Exp $	*/
+
+/*
+ *	vi command editing
+ *	written by John Rochester (initially for nsh)
+ *	bludgeoned to fit pdksh by Larry Bouzane, Jeff Sparkes & Eric Gisin
+ *
+ */
+#include "config.h"
+#ifdef VI
+
+#include <sys/stat.h>		/* completion */
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "sh.h"
+#include "edit.h"
+
+#define CTRL(c)		(c & 0x1f)
+
+struct edstate {
+	char	*cbuf;		/* main buffer to build the command line */
+	int	cbufsize;	/* number of bytes allocated for cbuf */
+	int	linelen;	/* current number of bytes in cbuf */
+	int	winleft;	/* first byte# in cbuf to be displayed */
+	int	cursor;		/* byte# in cbuf having the cursor */
+};
+
+
+static int	vi_hook(int);
+static void	vi_reset(char *, size_t);
+static int	nextstate(int);
+static int	vi_insert(int);
+static int	vi_cmd(int, const char *);
+static int	domove(int, const char *, int);
+static int	redo_insert(int);
+static void	yank_range(int, int);
+static int	bracktype(int);
+static void	save_cbuf(void);
+static void	restore_cbuf(void);
+static void	edit_reset(char *, size_t);
+static int	putbuf(const char *, int, int);
+static void	del_range(int, int);
+static int	findch(int, int, int, int);
+static int	forwword(int);
+static int	backword(int);
+static int	endword(int);
+static int	Forwword(int);
+static int	Backword(int);
+static int	Endword(int);
+static int	grabhist(int, int);
+static int	grabsearch(int, int, int, char *);
+static void	redraw_line(int);
+static void	refresh(int);
+static int	outofwin(void);
+static void	rewindow(void);
+static int	newcol(int, int);
+static void	display(char *, char *, int);
+static void	ed_mov_opt(int, char *);
+static int	expand_word(int);
+static int	complete_word(int, int);
+static int	print_expansions(struct edstate *);
+static int	char_len(int);
+static void	x_vi_zotc(int);
+static void	vi_pprompt(int);
+static void	vi_error(void);
+static void	vi_macro_reset(void);
+static int	x_vi_putbuf(const char *, size_t);
+static int	isu8cont(unsigned char);
+
+#define C_	0x1		/* a valid command that isn't a M_, E_, U_ */
+#define M_	0x2		/* movement command (h, l, etc.) */
+#define E_	0x4		/* extended command (c, d, y) */
+#define X_	0x8		/* long command (@, f, F, t, T, etc.) */
+#define U_	0x10		/* an UN-undoable command (that isn't a M_) */
+#define B_	0x20		/* bad command (^@) */
+#define Z_	0x40		/* repeat count defaults to 0 (not 1) */
+#define S_	0x80		/* search (/, ?) */
+
+#define is_bad(c)	(classify[(c)&0x7f]&B_)
+#define is_cmd(c)	(classify[(c)&0x7f]&(M_|E_|C_|U_))
+#define is_move(c)	(classify[(c)&0x7f]&M_)
+#define is_extend(c)	(classify[(c)&0x7f]&E_)
+#define is_long(c)	(classify[(c)&0x7f]&X_)
+#define is_undoable(c)	(!(classify[(c)&0x7f]&U_))
+#define is_srch(c)	(classify[(c)&0x7f]&S_)
+#define is_zerocount(c)	(classify[(c)&0x7f]&Z_)
+
+const unsigned char	classify[128] = {
+   /*       0       1       2       3       4       5       6       7        */
+   /*   0   ^@     ^A      ^B      ^C      ^D      ^E      ^F      ^G        */
+	    B_,     0,      0,      0,      0,      C_|U_,  C_|Z_,  0,
+   /*  01   ^H     ^I      ^J      ^K      ^L      ^M      ^N      ^O        */
+	    M_,     C_|Z_,  0,      0,      C_|U_,  0,      C_,     0,
+   /*  02   ^P     ^Q      ^R      ^S      ^T      ^U      ^V      ^W        */
+	    C_,     0,      C_|U_,  0,      0,      0,      C_,     0,
+   /*  03   ^X     ^Y      ^Z      ^[      ^\      ^]      ^^      ^_        */
+	    C_,     0,      0,      C_|Z_,  0,      0,      0,      0,
+   /*  04  <space>  !       "       #       $       %       &       '        */
+	    M_,     0,      0,      C_,     M_,     M_,     0,      0,
+   /*  05   (       )       *       +       ,       -       .       /        */
+	    0,      0,      C_,     C_,     M_,     C_,     0,      C_|S_,
+   /*  06   0       1       2       3       4       5       6       7        */
+	    M_,     0,      0,      0,      0,      0,      0,      0,
+   /*  07   8       9       :       ;       <       =       >       ?        */
+	    0,      0,      0,      M_,     0,      C_,     0,      C_|S_,
+   /* 010   @       A       B       C       D       E       F       G        */
+	    C_|X_,  C_,     M_,     C_,     C_,     M_,     M_|X_,  C_|U_|Z_,
+   /* 011   H       I       J       K       L       M       N       O        */
+	    0,      C_,     0,      0,      0,      0,      C_|U_,  0,
+   /* 012   P       Q       R       S       T       U       V       W        */
+	    C_,     0,      C_,     C_,     M_|X_,  C_,     0,      M_,
+   /* 013   X       Y       Z       [       \       ]       ^       _        */
+	    C_,     C_|U_,  0,      0,      C_|Z_,  0,      M_,     C_|Z_,
+   /* 014   `       a       b       c       d       e       f       g        */
+	    0,      C_,     M_,     E_,     E_,     M_,     M_|X_,  C_|Z_,
+   /* 015   h       i       j       k       l       m       n       o        */
+	    M_,     C_,     C_|U_,  C_|U_,  M_,     0,      C_|U_,  0,
+   /* 016   p       q       r       s       t       u       v       w        */
+	    C_,     0,      X_,     C_,     M_|X_,  C_|U_,  C_|U_|Z_,M_,
+   /* 017   x       y       z       {       |       }       ~      ^?        */
+	    C_,     E_|U_,  0,      0,      M_|Z_,  0,      C_,     0
+};
+
+#define MAXVICMD	3
+#define SRCHLEN		40
+
+#define INSERT		1
+#define REPLACE		2
+
+#define VNORMAL		0		/* command, insert or replace mode */
+#define VARG1		1		/* digit prefix (first, eg, 5l) */
+#define VEXTCMD		2		/* cmd + movement (eg, cl) */
+#define VARG2		3		/* digit prefix (second, eg, 2c3l) */
+#define VXCH		4		/* f, F, t, T, @ */
+#define VFAIL		5		/* bad command */
+#define VCMD		6		/* single char command (eg, X) */
+#define VREDO		7		/* . */
+#define VLIT		8		/* ^V */
+#define VSEARCH		9		/* /, ? */
+
+static char		undocbuf[LINE];
+
+static struct edstate	*save_edstate(struct edstate *old);
+static void		restore_edstate(struct edstate *old, struct edstate *new);
+static void		free_edstate(struct edstate *old);
+
+static struct edstate	ebuf;
+static struct edstate	undobuf = { undocbuf, LINE, 0, 0, 0 };
+
+static struct edstate	*es;			/* current editor state */
+static struct edstate	*undo;
+
+static char	ibuf[LINE];		/* input buffer */
+static int	first_insert;		/* set when starting in insert mode */
+static int	saved_inslen;		/* saved inslen for first insert */
+static int	inslen;			/* length of input buffer */
+static int	srchlen;		/* number of bytes in search pattern */
+static char	ybuf[LINE];		/* yank buffer */
+static int	yanklen;		/* length of yank buffer */
+static int	fsavecmd = ' ';		/* last find command */
+static int	fsavech;		/* character to find */
+static char	lastcmd[MAXVICMD];	/* last non-move command */
+static int	lastac;			/* argcnt for lastcmd */
+static int	lastsearch = ' ';	/* last search command */
+static char	srchpat[SRCHLEN];	/* last search pattern */
+static int	insert;			/* mode: INSERT, REPLACE, or 0 */
+static int	hnum;			/* position in history */
+static int	ohnum;			/* history line copied (after mod) */
+static int	hlast;			/* 1 past last position in history */
+static int	modified;		/* buffer has been "modified" */
+static int	state;
+
+/* Information for keeping track of macros that are being expanded.
+ * The format of buf is the alias contents followed by a null byte followed
+ * by the name (letter) of the alias.  The end of the buffer is marked by
+ * a double null.  The name of the alias is stored so recursive macros can
+ * be detected.
+ */
+struct macro_state {
+    unsigned char	*p;	/* current position in buf */
+    unsigned char	*buf;	/* pointer to macro(s) being expanded */
+    int			len;	/* how much data in buffer */
+};
+static struct macro_state macro;
+
+enum expand_mode { NONE, EXPAND, COMPLETE, PRINT };
+static enum expand_mode expanded = NONE;/* last input was expanded */
+
+int
+x_vi(char *buf, size_t len)
+{
+	int	c;
+
+	vi_reset(buf, len > LINE ? LINE : len);
+	vi_pprompt(1);
+	x_flush();
+	while (1) {
+		if (macro.p) {
+			c = (unsigned char)*macro.p++;
+			/* end of current macro? */
+			if (!c) {
+				/* more macros left to finish? */
+				if (*macro.p++)
+					continue;
+				/* must be the end of all the macros */
+				vi_macro_reset();
+				c = x_getc();
+			}
+		} else
+			c = x_getc();
+
+		if (c == -1)
+			break;
+		if (state != VLIT) {
+			if (c == edchars.intr || c == edchars.quit) {
+				/* pretend we got an interrupt */
+				x_vi_zotc(c);
+				x_flush();
+				trapsig(c == edchars.intr ? SIGINT : SIGQUIT);
+				x_mode(false);
+				unwind(LSHELL);
+			} else if (c == edchars.eof) {
+				if (es->linelen == 0) {
+					x_vi_zotc(edchars.eof);
+					c = -1;
+					break;
+				}
+				continue;
+			}
+		}
+		if (vi_hook(c))
+			break;
+		x_flush();
+	}
+
+	x_putc('\r'); x_putc('\n'); x_flush();
+
+	if (c == -1 || len <= (size_t)es->linelen)
+		return -1;
+
+	if (es->cbuf != buf)
+		memmove(buf, es->cbuf, es->linelen);
+
+	buf[es->linelen++] = '\n';
+
+	return es->linelen;
+}
+
+static int
+vi_hook(int ch)
+{
+	static char	curcmd[MAXVICMD], locpat[SRCHLEN];
+	static int	cmdlen, argc1, argc2;
+
+	switch (state) {
+
+	case VNORMAL:
+		if (insert != 0) {
+			if (ch == CTRL('v')) {
+				state = VLIT;
+				ch = '^';
+			}
+			switch (vi_insert(ch)) {
+			case -1:
+				vi_error();
+				state = VNORMAL;
+				break;
+			case 0:
+				if (state == VLIT) {
+					es->cursor--;
+					refresh(0);
+				} else
+					refresh(insert != 0);
+				break;
+			case 1:
+				return 1;
+			}
+		} else {
+			if (ch == '\r' || ch == '\n')
+				return 1;
+			cmdlen = 0;
+			argc1 = 0;
+			if (ch >= '1' && ch <= '9') {
+				argc1 = ch - '0';
+				state = VARG1;
+			} else {
+				curcmd[cmdlen++] = ch;
+				state = nextstate(ch);
+				if (state == VSEARCH) {
+					save_cbuf();
+					es->cursor = 0;
+					es->linelen = 0;
+					if (ch == '/') {
+						if (putbuf("/", 1, 0) != 0)
+							return -1;
+					} else if (putbuf("?", 1, 0) != 0)
+						return -1;
+					refresh(0);
+				}
+			}
+		}
+		break;
+
+	case VLIT:
+		if (is_bad(ch)) {
+			del_range(es->cursor, es->cursor + 1);
+			vi_error();
+		} else
+			es->cbuf[es->cursor++] = ch;
+		refresh(1);
+		state = VNORMAL;
+		break;
+
+	case VARG1:
+		if (isdigit(ch))
+			argc1 = argc1 * 10 + ch - '0';
+		else {
+			curcmd[cmdlen++] = ch;
+			state = nextstate(ch);
+		}
+		break;
+
+	case VEXTCMD:
+		argc2 = 0;
+		if (ch >= '1' && ch <= '9') {
+			argc2 = ch - '0';
+			state = VARG2;
+			return 0;
+		} else {
+			curcmd[cmdlen++] = ch;
+			if (ch == curcmd[0])
+				state = VCMD;
+			else if (is_move(ch))
+				state = nextstate(ch);
+			else
+				state = VFAIL;
+		}
+		break;
+
+	case VARG2:
+		if (isdigit(ch))
+			argc2 = argc2 * 10 + ch - '0';
+		else {
+			if (argc1 == 0)
+				argc1 = argc2;
+			else
+				argc1 *= argc2;
+			curcmd[cmdlen++] = ch;
+			if (ch == curcmd[0])
+				state = VCMD;
+			else if (is_move(ch))
+				state = nextstate(ch);
+			else
+				state = VFAIL;
+		}
+		break;
+
+	case VXCH:
+		if (ch == CTRL('['))
+			state = VNORMAL;
+		else {
+			curcmd[cmdlen++] = ch;
+			state = VCMD;
+		}
+		break;
+
+	case VSEARCH:
+		if (ch == '\r' || ch == '\n' /*|| ch == CTRL('[')*/ ) {
+			restore_cbuf();
+			/* Repeat last search? */
+			if (srchlen == 0) {
+				if (!srchpat[0]) {
+					vi_error();
+					state = VNORMAL;
+					refresh(0);
+					return 0;
+				}
+			} else {
+				locpat[srchlen] = '\0';
+				(void) strlcpy(srchpat, locpat, sizeof srchpat);
+			}
+			state = VCMD;
+		} else if (ch == edchars.erase || ch == CTRL('h')) {
+			if (srchlen != 0) {
+				do {
+					srchlen--;
+					es->linelen -= char_len(
+					    (unsigned char)locpat[srchlen]);
+				} while (srchlen > 0 &&
+				    isu8cont(locpat[srchlen]));
+				es->cursor = es->linelen;
+				refresh(0);
+				return 0;
+			}
+			restore_cbuf();
+			state = VNORMAL;
+			refresh(0);
+		} else if (ch == edchars.kill) {
+			srchlen = 0;
+			es->linelen = 1;
+			es->cursor = 1;
+			refresh(0);
+			return 0;
+		} else if (ch == edchars.werase) {
+			struct edstate new_es, *save_es;
+			int i;
+			int n = srchlen;
+
+			new_es.cursor = n;
+			new_es.cbuf = locpat;
+
+			save_es = es;
+			es = &new_es;
+			n = backword(1);
+			es = save_es;
+
+			for (i = srchlen; --i >= n; )
+				es->linelen -= char_len((unsigned char)locpat[i]);
+			srchlen = n;
+			es->cursor = es->linelen;
+			refresh(0);
+			return 0;
+		} else {
+			if (srchlen == SRCHLEN - 1)
+				vi_error();
+			else {
+				locpat[srchlen++] = ch;
+				if ((ch & 0x80) && Flag(FVISHOW8)) {
+					if (es->linelen + 2 > es->cbufsize)
+						vi_error();
+					es->cbuf[es->linelen++] = 'M';
+					es->cbuf[es->linelen++] = '-';
+					ch &= 0x7f;
+				}
+				if (ch < ' ' || ch == 0x7f) {
+					if (es->linelen + 2 > es->cbufsize)
+						vi_error();
+					es->cbuf[es->linelen++] = '^';
+					es->cbuf[es->linelen++] = ch ^ '@';
+				} else {
+					if (es->linelen >= es->cbufsize)
+						vi_error();
+					es->cbuf[es->linelen++] = ch;
+				}
+				es->cursor = es->linelen;
+				refresh(0);
+			}
+			return 0;
+		}
+		break;
+	}
+
+	switch (state) {
+	case VCMD:
+		state = VNORMAL;
+		switch (vi_cmd(argc1, curcmd)) {
+		case -1:
+			vi_error();
+			refresh(0);
+			break;
+		case 0:
+			if (insert != 0)
+				inslen = 0;
+			refresh(insert != 0);
+			break;
+		case 1:
+			refresh(0);
+			return 1;
+		case 2:
+			/* back from a 'v' command - don't redraw the screen */
+			return 1;
+		}
+		break;
+
+	case VREDO:
+		state = VNORMAL;
+		if (argc1 != 0)
+			lastac = argc1;
+		switch (vi_cmd(lastac, lastcmd)) {
+		case -1:
+			vi_error();
+			refresh(0);
+			break;
+		case 0:
+			if (insert != 0) {
+				if (lastcmd[0] == 's' || lastcmd[0] == 'c' ||
+				    lastcmd[0] == 'C') {
+					if (redo_insert(1) != 0)
+						vi_error();
+				} else {
+					if (redo_insert(lastac) != 0)
+						vi_error();
+				}
+			}
+			refresh(0);
+			break;
+		case 1:
+			refresh(0);
+			return 1;
+		case 2:
+			/* back from a 'v' command - can't happen */
+			break;
+		}
+		break;
+
+	case VFAIL:
+		state = VNORMAL;
+		vi_error();
+		break;
+	}
+	return 0;
+}
+
+static void
+vi_reset(char *buf, size_t len)
+{
+	state = VNORMAL;
+	ohnum = hnum = hlast = histnum(-1) + 1;
+	insert = INSERT;
+	saved_inslen = inslen;
+	first_insert = 1;
+	inslen = 0;
+	modified = 1;
+	vi_macro_reset();
+	edit_reset(buf, len);
+}
+
+static int
+nextstate(int ch)
+{
+	if (is_extend(ch))
+		return VEXTCMD;
+	else if (is_srch(ch))
+		return VSEARCH;
+	else if (is_long(ch))
+		return VXCH;
+	else if (ch == '.')
+		return VREDO;
+	else if (is_cmd(ch))
+		return VCMD;
+	else
+		return VFAIL;
+}
+
+static int
+vi_insert(int ch)
+{
+	int	tcursor;
+
+	if (ch == edchars.erase || ch == CTRL('h')) {
+		if (insert == REPLACE) {
+			if (es->cursor == undo->cursor) {
+				vi_error();
+				return 0;
+			}
+		} else {
+			if (es->cursor == 0) {
+				/* x_putc(BEL); no annoying bell here */
+				return 0;
+			}
+		}
+		tcursor = es->cursor - 1;
+		while(tcursor > 0 && isu8cont(es->cbuf[tcursor]))
+			tcursor--;
+		if (insert == INSERT)
+			memmove(es->cbuf + tcursor, es->cbuf + es->cursor,
+			    es->linelen - es->cursor);
+		if (insert == REPLACE && es->cursor < undo->linelen)
+			memcpy(es->cbuf + tcursor, undo->cbuf + tcursor,
+			    es->cursor - tcursor);
+		else
+			es->linelen -= es->cursor - tcursor;
+		if (inslen < es->cursor - tcursor)
+			inslen = 0;
+		else
+			inslen -= es->cursor - tcursor;
+		es->cursor = tcursor;
+		expanded = NONE;
+		return 0;
+	}
+	if (ch == edchars.kill) {
+		if (es->cursor != 0) {
+			inslen = 0;
+			memmove(es->cbuf, &es->cbuf[es->cursor],
+			    es->linelen - es->cursor);
+			es->linelen -= es->cursor;
+			es->cursor = 0;
+		}
+		expanded = NONE;
+		return 0;
+	}
+	if (ch == edchars.werase) {
+		if (es->cursor != 0) {
+			tcursor = backword(1);
+			memmove(&es->cbuf[tcursor], &es->cbuf[es->cursor],
+			    es->linelen - es->cursor);
+			es->linelen -= es->cursor - tcursor;
+			if (inslen < es->cursor - tcursor)
+				inslen = 0;
+			else
+				inslen -= es->cursor - tcursor;
+			es->cursor = tcursor;
+		}
+		expanded = NONE;
+		return 0;
+	}
+	/* If any chars are entered before escape, trash the saved insert
+	 * buffer (if user inserts & deletes char, ibuf gets trashed and
+	 * we don't want to use it)
+	 */
+	if (first_insert && ch != CTRL('['))
+		saved_inslen = 0;
+	switch (ch) {
+	case '\0':
+		return -1;
+
+	case '\r':
+	case '\n':
+		return 1;
+
+	case CTRL('['):
+		expanded = NONE;
+		if (first_insert) {
+			first_insert = 0;
+			if (inslen == 0) {
+				inslen = saved_inslen;
+				return redo_insert(0);
+			}
+			lastcmd[0] = 'a';
+			lastac = 1;
+		}
+		if (lastcmd[0] == 's' || lastcmd[0] == 'c' ||
+		    lastcmd[0] == 'C')
+			return redo_insert(0);
+		else
+			return redo_insert(lastac - 1);
+
+	/* { Begin nonstandard vi commands */
+	case CTRL('x'):
+		expand_word(0);
+		break;
+
+	case CTRL('f'):
+		complete_word(0, 0);
+		break;
+
+	case CTRL('e'):
+		print_expansions(es);
+		break;
+
+	case CTRL('i'):
+		if (Flag(FVITABCOMPLETE)) {
+			complete_word(0, 0);
+			break;
+		}
+		/* FALLTHROUGH */
+	/* End nonstandard vi commands } */
+
+	default:
+		if (es->linelen >= es->cbufsize - 1)
+			return -1;
+		ibuf[inslen++] = ch;
+		if (insert == INSERT) {
+			memmove(&es->cbuf[es->cursor+1], &es->cbuf[es->cursor],
+			    es->linelen - es->cursor);
+			es->linelen++;
+		}
+		es->cbuf[es->cursor++] = ch;
+		if (insert == REPLACE && es->cursor > es->linelen)
+			es->linelen++;
+		expanded = NONE;
+	}
+	return 0;
+}
+
+static int
+vi_cmd(int argcnt, const char *cmd)
+{
+	int		ncursor;
+	int		cur, c1, c2, c3 = 0;
+	int		any;
+	struct edstate	*t;
+
+	if (argcnt == 0 && !is_zerocount(*cmd))
+		argcnt = 1;
+
+	if (is_move(*cmd)) {
+		if ((cur = domove(argcnt, cmd, 0)) >= 0) {
+			if (cur == es->linelen && cur != 0)
+				while (isu8cont(es->cbuf[--cur]))
+					continue;
+			es->cursor = cur;
+		} else
+			return -1;
+	} else {
+		/* Don't save state in middle of macro.. */
+		if (is_undoable(*cmd) && !macro.p) {
+			undo->winleft = es->winleft;
+			memmove(undo->cbuf, es->cbuf, es->linelen);
+			undo->linelen = es->linelen;
+			undo->cursor = es->cursor;
+			lastac = argcnt;
+			memmove(lastcmd, cmd, MAXVICMD);
+		}
+		switch (*cmd) {
+
+		case CTRL('l'):
+		case CTRL('r'):
+			redraw_line(1);
+			break;
+
+		case '@':
+			{
+				static char alias[] = "_\0";
+				struct tbl *ap;
+				int	olen, nlen;
+				char	*p, *nbuf;
+
+				/* lookup letter in alias list... */
+				alias[1] = cmd[1];
+				ap = ktsearch(&aliases, alias, hash(alias));
+				if (!cmd[1] || !ap || !(ap->flag & ISSET))
+					return -1;
+				/* check if this is a recursive call... */
+				if ((p = (char *) macro.p))
+					while ((p = strchr(p, '\0')) && p[1])
+						if (*++p == cmd[1])
+							return -1;
+				/* insert alias into macro buffer */
+				nlen = strlen(ap->val.s) + 1;
+				olen = !macro.p ? 2 :
+				    macro.len - (macro.p - macro.buf);
+				nbuf = alloc(nlen + 1 + olen, APERM);
+				memcpy(nbuf, ap->val.s, nlen);
+				nbuf[nlen++] = cmd[1];
+				if (macro.p) {
+					memcpy(nbuf + nlen, macro.p, olen);
+					afree(macro.buf, APERM);
+					nlen += olen;
+				} else {
+					nbuf[nlen++] = '\0';
+					nbuf[nlen++] = '\0';
+				}
+				macro.p = macro.buf = (unsigned char *) nbuf;
+				macro.len = nlen;
+			}
+			break;
+
+		case 'a':
+			modified = 1; hnum = hlast;
+			if (es->linelen != 0)
+				while (isu8cont(es->cbuf[++es->cursor]))
+					continue;
+			insert = INSERT;
+			break;
+
+		case 'A':
+			modified = 1; hnum = hlast;
+			del_range(0, 0);
+			es->cursor = es->linelen;
+			insert = INSERT;
+			break;
+
+		case 'S':
+			es->cursor = domove(1, "^", 1);
+			del_range(es->cursor, es->linelen);
+			modified = 1; hnum = hlast;
+			insert = INSERT;
+			break;
+
+		case 'Y':
+			cmd = "y$";
+			/* ahhhhhh... */
+		case 'c':
+		case 'd':
+		case 'y':
+			if (*cmd == cmd[1]) {
+				c1 = *cmd == 'c' ? domove(1, "^", 1) : 0;
+				c2 = es->linelen;
+			} else if (!is_move(cmd[1]))
+				return -1;
+			else {
+				if ((ncursor = domove(argcnt, &cmd[1], 1)) < 0)
+					return -1;
+				if (*cmd == 'c' &&
+				    (cmd[1]=='w' || cmd[1]=='W') &&
+				    !isspace((unsigned char)es->cbuf[es->cursor])) {
+					while (isspace(
+					    (unsigned char)es->cbuf[--ncursor]))
+						;
+					ncursor++;
+				}
+				if (ncursor > es->cursor) {
+					c1 = es->cursor;
+					c2 = ncursor;
+				} else {
+					c1 = ncursor;
+					c2 = es->cursor;
+					if (cmd[1] == '%')
+						c2++;
+				}
+			}
+			if (*cmd != 'c' && c1 != c2)
+				yank_range(c1, c2);
+			if (*cmd != 'y') {
+				del_range(c1, c2);
+				es->cursor = c1;
+			}
+			if (*cmd == 'c') {
+				modified = 1; hnum = hlast;
+				insert = INSERT;
+			}
+			break;
+
+		case 'p':
+			modified = 1; hnum = hlast;
+			if (es->linelen != 0)
+				es->cursor++;
+			while (putbuf(ybuf, yanklen, 0) == 0 && --argcnt > 0)
+				;
+			if (es->cursor != 0)
+				es->cursor--;
+			if (argcnt != 0)
+				return -1;
+			break;
+
+		case 'P':
+			modified = 1; hnum = hlast;
+			any = 0;
+			while (putbuf(ybuf, yanklen, 0) == 0 && --argcnt > 0)
+				any = 1;
+			if (any && es->cursor != 0)
+				es->cursor--;
+			if (argcnt != 0)
+				return -1;
+			break;
+
+		case 'C':
+			modified = 1; hnum = hlast;
+			del_range(es->cursor, es->linelen);
+			insert = INSERT;
+			break;
+
+		case 'D':
+			yank_range(es->cursor, es->linelen);
+			del_range(es->cursor, es->linelen);
+			if (es->cursor != 0)
+				es->cursor--;
+			break;
+
+		case 'g':
+			if (!argcnt)
+				argcnt = hlast;
+			/* FALLTHROUGH */
+		case 'G':
+			if (!argcnt)
+				argcnt = 1;
+			else
+				argcnt = hlast - (source->line - argcnt);
+			if (grabhist(modified, argcnt - 1) < 0)
+				return -1;
+			else {
+				modified = 0;
+				hnum = argcnt - 1;
+			}
+			break;
+
+		case 'i':
+			modified = 1; hnum = hlast;
+			insert = INSERT;
+			break;
+
+		case 'I':
+			modified = 1; hnum = hlast;
+			es->cursor = domove(1, "^", 1);
+			insert = INSERT;
+			break;
+
+		case 'j':
+		case '+':
+		case CTRL('n'):
+			if (grabhist(modified, hnum + argcnt) < 0)
+				return -1;
+			else {
+				modified = 0;
+				hnum += argcnt;
+			}
+			break;
+
+		case 'k':
+		case '-':
+		case CTRL('p'):
+			if (grabhist(modified, hnum - argcnt) < 0)
+				return -1;
+			else {
+				modified = 0;
+				hnum -= argcnt;
+			}
+			break;
+
+		case 'r':
+			if (es->linelen == 0)
+				return -1;
+			modified = 1; hnum = hlast;
+			if (cmd[1] == 0)
+				vi_error();
+			else {
+				c1 = 0;
+				for (cur = es->cursor;
+				    cur < es->linelen; cur++) {
+					if (!isu8cont(es->cbuf[cur]))
+						c1++;
+					if (c1 > argcnt)
+						break;
+				}
+				if (argcnt > c1)
+					return -1;
+
+				del_range(es->cursor, cur);
+				while (argcnt-- > 0)
+					putbuf(&cmd[1], 1, 0);
+				while (es->cursor > 0)
+					if (!isu8cont(es->cbuf[--es->cursor]))
+						break;
+				es->cbuf[es->linelen] = '\0';
+			}
+			break;
+
+		case 'R':
+			modified = 1; hnum = hlast;
+			insert = REPLACE;
+			break;
+
+		case 's':
+			if (es->linelen == 0)
+				return -1;
+			modified = 1; hnum = hlast;
+			for (cur = es->cursor; cur < es->linelen; cur++)
+				if (!isu8cont(es->cbuf[cur]))
+					if (argcnt-- == 0)
+						break;
+			del_range(es->cursor, cur);
+			insert = INSERT;
+			break;
+
+		case 'v':
+			if (es->linelen == 0 && argcnt == 0)
+				return -1;
+			if (!argcnt) {
+				if (modified) {
+					es->cbuf[es->linelen] = '\0';
+					source->line++;
+					histsave(source->line, es->cbuf, 1);
+				} else
+					argcnt = source->line + 1
+						- (hlast - hnum);
+			}
+			shf_snprintf(es->cbuf, es->cbufsize,
+			    argcnt ? "%s %d" : "%s",
+			    "fc -e ${VISUAL:-${EDITOR:-vi}} --",
+			    argcnt);
+			es->linelen = strlen(es->cbuf);
+			return 2;
+
+		case 'x':
+			if (es->linelen == 0)
+				return -1;
+			modified = 1; hnum = hlast;
+			for (cur = es->cursor; cur < es->linelen; cur++)
+				if (!isu8cont(es->cbuf[cur]))
+					if (argcnt-- == 0)
+						break;
+			yank_range(es->cursor, cur);
+			del_range(es->cursor, cur);
+			break;
+
+		case 'X':
+			if (es->cursor == 0)
+				return -1;
+			modified = 1; hnum = hlast;
+			for (cur = es->cursor; cur > 0; cur--)
+				if (!isu8cont(es->cbuf[cur]))
+					if (argcnt-- == 0)
+						break;
+			yank_range(cur, es->cursor);
+			del_range(cur, es->cursor);
+			es->cursor = cur;
+			break;
+
+		case 'u':
+			t = es;
+			es = undo;
+			undo = t;
+			break;
+
+		case 'U':
+			if (!modified)
+				return -1;
+			if (grabhist(modified, ohnum) < 0)
+				return -1;
+			modified = 0;
+			hnum = ohnum;
+			break;
+
+		case '?':
+			if (hnum == hlast)
+				hnum = -1;
+			/* ahhh */
+		case '/':
+			c3 = 1;
+			srchlen = 0;
+			lastsearch = *cmd;
+			/* FALLTHROUGH */
+		case 'n':
+		case 'N':
+			if (lastsearch == ' ')
+				return -1;
+			if (lastsearch == '?')
+				c1 = 1;
+			else
+				c1 = 0;
+			if (*cmd == 'N')
+				c1 = !c1;
+			if ((c2 = grabsearch(modified, hnum,
+			    c1, srchpat)) < 0) {
+				if (c3) {
+					restore_cbuf();
+					refresh(0);
+				}
+				return -1;
+			} else {
+				modified = 0;
+				hnum = c2;
+				ohnum = hnum;
+			}
+			break;
+		case '_': {
+			int	inspace;
+			char	*p, *sp;
+
+			if (histnum(-1) < 0)
+				return -1;
+			p = *histpos();
+#define issp(c)		(isspace((unsigned char)(c)) || (c) == '\n')
+			if (argcnt) {
+				while (*p && issp(*p))
+					p++;
+				while (*p && --argcnt) {
+					while (*p && !issp(*p))
+						p++;
+					while (*p && issp(*p))
+						p++;
+				}
+				if (!*p)
+					return -1;
+				sp = p;
+			} else {
+				sp = p;
+				inspace = 0;
+				while (*p) {
+					if (issp(*p))
+						inspace = 1;
+					else if (inspace) {
+						inspace = 0;
+						sp = p;
+					}
+					p++;
+				}
+				p = sp;
+			}
+			modified = 1; hnum = hlast;
+			if (es->cursor != es->linelen)
+				es->cursor++;
+			while (*p && !issp(*p)) {
+				argcnt++;
+				p++;
+			}
+			if (putbuf(" ", 1, 0) != 0)
+				argcnt = -1;
+			else if (putbuf(sp, argcnt, 0) != 0)
+				argcnt = -1;
+			if (argcnt < 0) {
+				if (es->cursor != 0)
+					es->cursor--;
+				return -1;
+			}
+			insert = INSERT;
+			}
+			break;
+
+		case '~': {
+			char	*p;
+			unsigned char c;
+			int	i;
+
+			if (es->linelen == 0)
+				return -1;
+			for (i = 0; i < argcnt; i++) {
+				p = &es->cbuf[es->cursor];
+				c = (unsigned char)*p;
+				if (islower(c)) {
+					modified = 1; hnum = hlast;
+					*p = toupper(c);
+				} else if (isupper(c)) {
+					modified = 1; hnum = hlast;
+					*p = tolower(c);
+				}
+				if (es->cursor < es->linelen - 1)
+					es->cursor++;
+			}
+			break;
+			}
+
+		case '#':
+		    {
+			int ret = x_do_comment(es->cbuf, es->cbufsize,
+			    &es->linelen);
+			if (ret >= 0)
+				es->cursor = 0;
+			return ret;
+		    }
+
+		case '=':			/* at&t ksh */
+		case CTRL('e'):			/* Nonstandard vi/ksh */
+			print_expansions(es);
+			break;
+
+
+		case CTRL('i'):			/* Nonstandard vi/ksh */
+			if (!Flag(FVITABCOMPLETE))
+				return -1;
+			complete_word(1, argcnt);
+			break;
+
+		case CTRL('['):			/* some annoying at&t ksh's */
+			if (!Flag(FVIESCCOMPLETE))
+				return -1;
+		case '\\':			/* at&t ksh */
+		case CTRL('f'):			/* Nonstandard vi/ksh */
+			complete_word(1, argcnt);
+			break;
+
+
+		case '*':			/* at&t ksh */
+		case CTRL('x'):			/* Nonstandard vi/ksh */
+			expand_word(1);
+			break;
+		}
+		if (insert == 0 && es->cursor >= es->linelen)
+			while (es->cursor > 0)
+				if (!isu8cont(es->cbuf[--es->cursor]))
+					break;
+	}
+	return 0;
+}
+
+static int
+domove(int argcnt, const char *cmd, int sub)
+{
+	int	bcount, i = 0, t;
+	int	ncursor = 0;
+
+	switch (*cmd) {
+
+	case 'b':
+	case 'B':
+		if (!sub && es->cursor == 0)
+			return -1;
+		ncursor = (*cmd == 'b' ? backword : Backword)(argcnt);
+		break;
+
+	case 'e':
+	case 'E':
+		if (!sub && es->cursor + 1 >= es->linelen)
+			return -1;
+		ncursor = (*cmd == 'e' ? endword : Endword)(argcnt);
+		if (!sub)
+			while (isu8cont((unsigned char)es->cbuf[--ncursor]))
+				continue;
+		break;
+
+	case 'f':
+	case 'F':
+	case 't':
+	case 'T':
+		fsavecmd = *cmd;
+		fsavech = cmd[1];
+		/* drop through */
+
+	case ',':
+	case ';':
+		if (fsavecmd == ' ')
+			return -1;
+		i = fsavecmd == 'f' || fsavecmd == 'F';
+		t = fsavecmd > 'a';
+		if (*cmd == ',')
+			t = !t;
+		if ((ncursor = findch(fsavech, argcnt, t, i)) < 0)
+			return -1;
+		if (sub && t)
+			ncursor++;
+		break;
+
+	case 'h':
+	case CTRL('h'):
+		if (!sub && es->cursor == 0)
+			return -1;
+		for (ncursor = es->cursor; ncursor > 0; ncursor--)
+			if (!isu8cont(es->cbuf[ncursor]))
+				if (argcnt-- == 0)
+					break;
+		break;
+
+	case ' ':
+	case 'l':
+		if (!sub && es->cursor + 1 >= es->linelen)
+			return -1;
+		for (ncursor = es->cursor; ncursor < es->linelen; ncursor++)
+			if (!isu8cont(es->cbuf[ncursor]))
+				if (argcnt-- == 0)
+					break;
+		break;
+
+	case 'w':
+	case 'W':
+		if (!sub && es->cursor + 1 >= es->linelen)
+			return -1;
+		ncursor = (*cmd == 'w' ? forwword : Forwword)(argcnt);
+		break;
+
+	case '0':
+		ncursor = 0;
+		break;
+
+	case '^':
+		ncursor = 0;
+		while (ncursor < es->linelen - 1 &&
+		    isspace((unsigned char)es->cbuf[ncursor]))
+			ncursor++;
+		break;
+
+	case '|':
+		ncursor = argcnt;
+		if (ncursor > es->linelen)
+			ncursor = es->linelen;
+		if (ncursor)
+			ncursor--;
+		while (isu8cont(es->cbuf[ncursor]))
+			ncursor--;
+		break;
+
+	case '$':
+		ncursor = es->linelen;
+		break;
+
+	case '%':
+		ncursor = es->cursor;
+		while (ncursor < es->linelen &&
+		    (i = bracktype(es->cbuf[ncursor])) == 0)
+			ncursor++;
+		if (ncursor == es->linelen)
+			return -1;
+		bcount = 1;
+		do {
+			if (i > 0) {
+				if (++ncursor >= es->linelen)
+					return -1;
+			} else {
+				if (--ncursor < 0)
+					return -1;
+			}
+			t = bracktype(es->cbuf[ncursor]);
+			if (t == i)
+				bcount++;
+			else if (t == -i)
+				bcount--;
+		} while (bcount != 0);
+		if (sub && i > 0)
+			ncursor++;
+		break;
+
+	default:
+		return -1;
+	}
+	return ncursor;
+}
+
+static int
+redo_insert(int count)
+{
+	while (count-- > 0)
+		if (putbuf(ibuf, inslen, insert==REPLACE) != 0)
+			return -1;
+	if (es->cursor > 0)
+		while (isu8cont(es->cbuf[--es->cursor]))
+			continue;
+	insert = 0;
+	return 0;
+}
+
+static void
+yank_range(int a, int b)
+{
+	yanklen = b - a;
+	if (yanklen != 0)
+		memmove(ybuf, &es->cbuf[a], yanklen);
+}
+
+static int
+bracktype(int ch)
+{
+	switch (ch) {
+
+	case '(':
+		return 1;
+
+	case '[':
+		return 2;
+
+	case '{':
+		return 3;
+
+	case ')':
+		return -1;
+
+	case ']':
+		return -2;
+
+	case '}':
+		return -3;
+
+	default:
+		return 0;
+	}
+}
+
+/*
+ *	Non user interface editor routines below here
+ */
+
+static int	cur_col;		/* current display column */
+static int	pwidth;			/* display columns needed for prompt */
+static int	prompt_trunc;		/* how much of prompt to truncate */
+static int	prompt_skip;		/* how much of prompt to skip */
+static int	winwidth;		/* available column positions */
+static char	*wbuf[2];		/* current & previous window buffer */
+static int	wbuf_len;		/* length of window buffers (x_cols-3)*/
+static int	win;			/* number of window buffer in use */
+static char	morec;			/* more character at right of window */
+static char	holdbuf[LINE];		/* place to hold last edit buffer */
+static int	holdlen;		/* length of holdbuf */
+
+static void
+save_cbuf(void)
+{
+	memmove(holdbuf, es->cbuf, es->linelen);
+	holdlen = es->linelen;
+	holdbuf[holdlen] = '\0';
+}
+
+static void
+restore_cbuf(void)
+{
+	es->cursor = 0;
+	es->linelen = holdlen;
+	memmove(es->cbuf, holdbuf, holdlen);
+}
+
+/* return a new edstate */
+static struct edstate *
+save_edstate(struct edstate *old)
+{
+	struct edstate *new;
+
+	new = alloc(sizeof(struct edstate), APERM);
+	new->cbuf = alloc(old->cbufsize, APERM);
+	memcpy(new->cbuf, old->cbuf, old->linelen);
+	new->cbufsize = old->cbufsize;
+	new->linelen = old->linelen;
+	new->cursor = old->cursor;
+	new->winleft = old->winleft;
+	return new;
+}
+
+static void
+restore_edstate(struct edstate *new, struct edstate *old)
+{
+	memcpy(new->cbuf, old->cbuf, old->linelen);
+	new->linelen = old->linelen;
+	new->cursor = old->cursor;
+	new->winleft = old->winleft;
+	free_edstate(old);
+}
+
+static void
+free_edstate(struct edstate *old)
+{
+	afree(old->cbuf, APERM);
+	afree(old, APERM);
+}
+
+
+
+static void
+edit_reset(char *buf, size_t len)
+{
+	const char *p;
+
+	es = &ebuf;
+	es->cbuf = buf;
+	es->cbufsize = len;
+	undo = &undobuf;
+	undo->cbufsize = len;
+
+	es->linelen = undo->linelen = 0;
+	es->cursor = undo->cursor = 0;
+	es->winleft = undo->winleft = 0;
+
+	cur_col = pwidth = promptlen(prompt, &p);
+	prompt_skip = p - prompt;
+	if (pwidth > x_cols - 3 - MIN_EDIT_SPACE) {
+		cur_col = x_cols - 3 - MIN_EDIT_SPACE;
+		prompt_trunc = pwidth - cur_col;
+		pwidth -= prompt_trunc;
+	} else
+		prompt_trunc = 0;
+	if (!wbuf_len || wbuf_len != x_cols - 3) {
+		wbuf_len = x_cols - 3;
+		wbuf[0] = aresize(wbuf[0], wbuf_len, APERM);
+		wbuf[1] = aresize(wbuf[1], wbuf_len, APERM);
+	}
+	(void) memset(wbuf[0], ' ', wbuf_len);
+	(void) memset(wbuf[1], ' ', wbuf_len);
+	winwidth = x_cols - pwidth - 3;
+	win = 0;
+	morec = ' ';
+	holdlen = 0;
+}
+
+/*
+ * this is used for calling x_escape() in complete_word()
+ */
+static int
+x_vi_putbuf(const char *s, size_t len)
+{
+	return putbuf(s, len, 0);
+}
+
+static int
+putbuf(const char *buf, int len, int repl)
+{
+	if (len == 0)
+		return 0;
+	if (repl) {
+		if (es->cursor + len >= es->cbufsize)
+			return -1;
+		if (es->cursor + len > es->linelen)
+			es->linelen = es->cursor + len;
+	} else {
+		if (es->linelen + len >= es->cbufsize)
+			return -1;
+		memmove(&es->cbuf[es->cursor + len], &es->cbuf[es->cursor],
+		    es->linelen - es->cursor);
+		es->linelen += len;
+	}
+	memmove(&es->cbuf[es->cursor], buf, len);
+	es->cursor += len;
+	return 0;
+}
+
+static void
+del_range(int a, int b)
+{
+	if (es->linelen != b)
+		memmove(&es->cbuf[a], &es->cbuf[b], es->linelen - b);
+	es->linelen -= b - a;
+}
+
+static int
+findch(int ch, int cnt, int forw, int incl)
+{
+	int	ncursor;
+
+	if (es->linelen == 0)
+		return -1;
+	ncursor = es->cursor;
+	while (cnt--) {
+		do {
+			if (forw) {
+				if (++ncursor == es->linelen)
+					return -1;
+			} else {
+				if (--ncursor < 0)
+					return -1;
+			}
+		} while (es->cbuf[ncursor] != ch);
+	}
+	if (!incl) {
+		if (forw)
+			ncursor--;
+		else
+			ncursor++;
+	}
+	return ncursor;
+}
+
+/* Move right one character, and then to the beginning of the next word. */
+static int
+forwword(int argcnt)
+{
+	int ncursor, skip_space, want_letnum;
+	unsigned char uc;
+
+	ncursor = es->cursor;
+	while (ncursor < es->linelen && argcnt--) {
+		skip_space = 0;
+		want_letnum = -1;
+		ncursor--;
+		while (++ncursor < es->linelen) {
+			uc = es->cbuf[ncursor];
+			if (isspace(uc)) {
+				skip_space = 1;
+				continue;
+			} else if (skip_space)
+				break;
+			if (uc & 0x80)
+				continue;
+			if (want_letnum == -1)
+				want_letnum = letnum(uc);
+			else if (want_letnum != letnum(uc))
+				break;
+		}
+	}
+	return ncursor;
+}
+
+/* Move left one character, and then to the beginning of the word. */
+static int
+backword(int argcnt)
+{
+	int ncursor, skip_space, want_letnum;
+	unsigned char uc;
+
+	ncursor = es->cursor;
+	while (ncursor > 0 && argcnt--) {
+		skip_space = 1;
+		want_letnum = -1;
+		while (ncursor-- > 0) {
+			uc = es->cbuf[ncursor];
+			if (isspace(uc)) {
+				if (skip_space)
+					continue;
+				else
+					break;
+			}
+			skip_space = 0;
+			if (uc & 0x80)
+				continue;
+			if (want_letnum == -1)
+				want_letnum = letnum(uc);
+			else if (want_letnum != letnum(uc))
+				break;
+		}
+		ncursor++;
+	}
+	return ncursor;
+}
+
+/* Move right one character, and then to the byte after the word. */
+static int
+endword(int argcnt)
+{
+	int ncursor, skip_space, want_letnum;
+	unsigned char uc;
+
+	ncursor = es->cursor;
+	while (ncursor < es->linelen && argcnt--) {
+		skip_space = 1;
+		want_letnum = -1;
+		while (++ncursor < es->linelen) {
+			uc = es->cbuf[ncursor];
+			if (isspace(uc)) {
+				if (skip_space)
+					continue;
+				else
+					break;
+			}
+			skip_space = 0;
+			if (uc & 0x80)
+				continue;
+			if (want_letnum == -1)
+				want_letnum = letnum(uc);
+			else if (want_letnum != letnum(uc))
+				break;
+		}
+	}
+	return ncursor;
+}
+
+/* Move right one character, and then to the beginning of the next big word. */
+static int
+Forwword(int argcnt)
+{
+	int	ncursor;
+
+	ncursor = es->cursor;
+	while (ncursor < es->linelen && argcnt--) {
+		while (!isspace((unsigned char)es->cbuf[ncursor]) &&
+		    ncursor < es->linelen)
+			ncursor++;
+		while (isspace((unsigned char)es->cbuf[ncursor]) &&
+		    ncursor < es->linelen)
+			ncursor++;
+	}
+	return ncursor;
+}
+
+/* Move left one character, and then to the beginning of the big word. */
+static int
+Backword(int argcnt)
+{
+	int	ncursor;
+
+	ncursor = es->cursor;
+	while (ncursor > 0 && argcnt--) {
+		while (--ncursor >= 0 &&
+		    isspace((unsigned char)es->cbuf[ncursor]))
+			;
+		while (ncursor >= 0 &&
+		    !isspace((unsigned char)es->cbuf[ncursor]))
+			ncursor--;
+		ncursor++;
+	}
+	return ncursor;
+}
+
+/* Move right one character, and then to the byte after the big word. */
+static int
+Endword(int argcnt)
+{
+	int	ncursor;
+
+	ncursor = es->cursor;
+	while (ncursor < es->linelen && argcnt--) {
+		while (++ncursor < es->linelen &&
+		    isspace((unsigned char)es->cbuf[ncursor]))
+			;
+		while (ncursor < es->linelen &&
+		    !isspace((unsigned char)es->cbuf[ncursor]))
+			ncursor++;
+	}
+	return ncursor;
+}
+
+static int
+grabhist(int save, int n)
+{
+	char	*hptr;
+
+	if (n < 0 || n > hlast)
+		return -1;
+	if (n == hlast) {
+		restore_cbuf();
+		ohnum = n;
+		return 0;
+	}
+	(void) histnum(n);
+	if ((hptr = *histpos()) == NULL) {
+		internal_warningf("%s: bad history array", __func__);
+		return -1;
+	}
+	if (save)
+		save_cbuf();
+	if ((es->linelen = strlen(hptr)) >= es->cbufsize)
+		es->linelen = es->cbufsize - 1;
+	memmove(es->cbuf, hptr, es->linelen);
+	es->cursor = 0;
+	ohnum = n;
+	return 0;
+}
+
+static int
+grabsearch(int save, int start, int fwd, char *pat)
+{
+	char	*hptr;
+	int	hist;
+	int	anchored;
+
+	if ((start == 0 && fwd == 0) || (start >= hlast-1 && fwd == 1))
+		return -1;
+	if (fwd)
+		start++;
+	else
+		start--;
+	anchored = *pat == '^' ? (++pat, 1) : 0;
+	if ((hist = findhist(start, fwd, pat, anchored)) < 0) {
+		/* if (start != 0 && fwd && match(holdbuf, pat) >= 0) { */
+		/* XXX should strcmp be strncmp? */
+		if (start != 0 && fwd && strcmp(holdbuf, pat) >= 0) {
+			restore_cbuf();
+			return 0;
+		} else
+			return -1;
+	}
+	if (save)
+		save_cbuf();
+	histnum(hist);
+	hptr = *histpos();
+	if ((es->linelen = strlen(hptr)) >= es->cbufsize)
+		es->linelen = es->cbufsize - 1;
+	memmove(es->cbuf, hptr, es->linelen);
+	es->cursor = 0;
+	return hist;
+}
+
+static void
+redraw_line(int newline)
+{
+	(void) memset(wbuf[win], ' ', wbuf_len);
+	if (newline) {
+		x_putc('\r');
+		x_putc('\n');
+	}
+	vi_pprompt(0);
+	cur_col = pwidth;
+	morec = ' ';
+}
+
+static void
+refresh(int leftside)
+{
+	if (outofwin())
+		rewindow();
+	display(wbuf[1 - win], wbuf[win], leftside);
+	win = 1 - win;
+}
+
+static int
+outofwin(void)
+{
+	int	cur, col;
+
+	if (es->cursor < es->winleft)
+		return 1;
+	col = 0;
+	cur = es->winleft;
+	while (cur < es->cursor)
+		col = newcol((unsigned char) es->cbuf[cur++], col);
+	if (col >= winwidth)
+		return 1;
+	return 0;
+}
+
+static void
+rewindow(void)
+{
+	int	tcur, tcol;
+	int	holdcur1, holdcol1;
+	int	holdcur2, holdcol2;
+
+	holdcur1 = holdcur2 = tcur = 0;
+	holdcol1 = holdcol2 = tcol = 0;
+	while (tcur < es->cursor) {
+		if (tcol - holdcol2 > winwidth / 2) {
+			holdcur1 = holdcur2;
+			holdcol1 = holdcol2;
+			holdcur2 = tcur;
+			holdcol2 = tcol;
+		}
+		tcol = newcol((unsigned char) es->cbuf[tcur++], tcol);
+	}
+	while (tcol - holdcol1 > winwidth / 2)
+		holdcol1 = newcol((unsigned char) es->cbuf[holdcur1++],
+		    holdcol1);
+	es->winleft = holdcur1;
+}
+
+/* Printing the byte ch at display column col moves to which column? */
+static int
+newcol(int ch, int col)
+{
+	if (ch == '\t')
+		return (col | 7) + 1;
+	if (isu8cont(ch))
+		return col;
+	return col + char_len(ch);
+}
+
+/* Display wb1 assuming that wb2 is currently displayed. */
+static void
+display(char *wb1, char *wb2, int leftside)
+{
+	char	*twb1;	/* pointer into the buffer to display */
+	char	*twb2;	/* pointer into the previous display buffer */
+	static int lastb = -1; /* last byte# written from wb1, if UTF-8 */
+	int	 cur;	/* byte# in the main command line buffer */
+	int	 col;	/* display column loop variable */
+	int	 ncol;	/* display column of the cursor */
+	int	 cnt;	/* remaining display columns to fill */
+	int	 moreright;
+	char	 mc;	/* new "more character" at the right of window */
+	unsigned char ch;
+
+	/*
+	 * Fill the current display buffer with data from cbuf.
+	 * In this first loop, col does not include the prompt.
+	 */
+
+	ncol = col = 0;
+	cur = es->winleft;
+	moreright = 0;
+	twb1 = wb1;
+	while (col < winwidth && cur < es->linelen) {
+		if (cur == es->cursor && leftside)
+			ncol = col + pwidth;
+		if ((ch = es->cbuf[cur]) == '\t') {
+			do {
+				*twb1++ = ' ';
+			} while (++col < winwidth && (col & 7) != 0);
+		} else {
+			if ((ch & 0x80) && Flag(FVISHOW8)) {
+				*twb1++ = 'M';
+				if (++col < winwidth) {
+					*twb1++ = '-';
+					col++;
+				}
+				ch &= 0x7f;
+			}
+			if (col < winwidth) {
+				if (ch < ' ' || ch == 0x7f) {
+					*twb1++ = '^';
+					if (++col < winwidth) {
+						*twb1++ = ch ^ '@';
+						col++;
+					}
+				} else {
+					*twb1++ = ch;
+					if (!isu8cont(ch))
+						col++;
+				}
+			}
+		}
+		if (cur == es->cursor && !leftside)
+			ncol = col + pwidth - 1;
+		cur++;
+	}
+	if (cur == es->cursor)
+		ncol = col + pwidth;
+
+	/* Pad the current display buffer to the right margin. */
+
+	if (col < winwidth) {
+		while (col < winwidth) {
+			*twb1++ = ' ';
+			col++;
+		}
+	} else
+		moreright++;
+	*twb1 = ' ';
+
+	/*
+	 * Update the terminal display with data from wb1.
+	 * In this final loop, col includes the prompt.
+	 */
+
+	col = pwidth;
+	cnt = winwidth;
+	for (twb1 = wb1, twb2 = wb2; cnt; twb1++, twb2++) {
+		if (*twb1 != *twb2) {
+
+			/*
+			 * When a byte changes in the middle of a UTF-8
+			 * character, back up to the start byte, unless
+			 * the previous byte was the last one written.
+			 */
+
+			if (col > 0 && isu8cont(*twb1)) {
+				col--;
+				if (lastb >= 0 && twb1 == wb1 + lastb + 1)
+					cur_col = col;
+				else while (twb1 > wb1 && isu8cont(*twb1)) {
+					twb1--;
+					twb2--;
+				}
+			}
+
+			if (cur_col != col)
+				ed_mov_opt(col, wb1);
+
+			/*
+			 * Always write complete characters, and
+			 * advance all pointers accordingly.
+			 */
+
+			x_putc(*twb1);
+			while (isu8cont(twb1[1])) {
+				x_putc(*++twb1);
+				twb2++;
+			}
+			lastb = *twb1 & 0x80 ? twb1 - wb1 : -1;
+			cur_col++;
+		} else if (isu8cont(*twb1))
+			continue;
+
+		/*
+		 * For changed continuation bytes, we backed up.
+		 * For unchanged ones, we jumped to the next byte.
+		 * So, getting here, we had a real column.
+		 */
+
+		col++;
+		cnt--;
+	}
+
+	/* Update the "more character". */
+
+	if (es->winleft > 0 && moreright)
+		/* POSIX says to use * for this but that is a globbing
+		 * character and may confuse people; + is more innocuous
+		 */
+		mc = '+';
+	else if (es->winleft > 0)
+		mc = '<';
+	else if (moreright)
+		mc = '>';
+	else
+		mc = ' ';
+	if (mc != morec) {
+		ed_mov_opt(pwidth + winwidth + 1, wb1);
+		x_putc(mc);
+		cur_col++;
+		morec = mc;
+		lastb = -1;
+	}
+
+	/* Move the cursor to its new position. */
+
+	if (cur_col != ncol) {
+		ed_mov_opt(ncol, wb1);
+		lastb = -1;
+	}
+}
+
+/* Move the display cursor to display column number col. */
+static void
+ed_mov_opt(int col, char *wb)
+{
+	int ci;
+
+	/* The cursor is already at the right place. */
+
+	if (cur_col == col)
+		return;
+
+	/* The cursor is too far right. */
+
+	if (cur_col > col) {
+		if (cur_col > 2 * col + 1) {
+			/* Much too far right, redraw from scratch. */
+			x_putc('\r');
+			vi_pprompt(0);
+			cur_col = pwidth;
+		} else {
+			/* Slightly too far right, back up. */
+			do {
+				x_putc('\b');
+			} while (--cur_col > col);
+			return;
+		}
+	}
+
+	/* Advance the cursor. */
+
+	for (ci = pwidth; ci < col || isu8cont(*wb);
+	     ci = newcol((unsigned char)*wb++, ci))
+		if (ci > cur_col || (ci == cur_col && !isu8cont(*wb)))
+			x_putc(*wb);
+	cur_col = ci;
+}
+
+
+/* replace word with all expansions (ie, expand word*) */
+static int
+expand_word(int command)
+{
+	static struct edstate *buf;
+	int rval = 0;
+	int nwords;
+	int start, end;
+	char **words;
+	int i;
+
+	/* Undo previous expansion */
+	if (command == 0 && expanded == EXPAND && buf) {
+		restore_edstate(es, buf);
+		buf = NULL;
+		expanded = NONE;
+		return 0;
+	}
+	if (buf) {
+		free_edstate(buf);
+		buf = NULL;
+	}
+
+	nwords = x_cf_glob(XCF_COMMAND_FILE|XCF_FULLPATH,
+	    es->cbuf, es->linelen, es->cursor,
+	    &start, &end, &words, NULL);
+	if (nwords == 0) {
+		vi_error();
+		return -1;
+	}
+
+	buf = save_edstate(es);
+	expanded = EXPAND;
+	del_range(start, end);
+	es->cursor = start;
+	for (i = 0; i < nwords; ) {
+		if (x_escape(words[i], strlen(words[i]), x_vi_putbuf) != 0) {
+			rval = -1;
+			break;
+		}
+		if (++i < nwords && putbuf(" ", 1, 0) != 0) {
+			rval = -1;
+			break;
+		}
+	}
+	i = buf->cursor - end;
+	if (rval == 0 && i > 0)
+		es->cursor += i;
+	modified = 1; hnum = hlast;
+	insert = INSERT;
+	lastac = 0;
+	refresh(0);
+	return rval;
+}
+
+static int
+complete_word(int command, int count)
+{
+	static struct edstate *buf;
+	int rval = 0;
+	int nwords;
+	int start, end;
+	char **words;
+	char *match;
+	int match_len;
+	int is_unique;
+	int is_command;
+
+	/* Undo previous completion */
+	if (command == 0 && expanded == COMPLETE && buf) {
+		print_expansions(buf);
+		expanded = PRINT;
+		return 0;
+	}
+	if (command == 0 && expanded == PRINT && buf) {
+		restore_edstate(es, buf);
+		buf = NULL;
+		expanded = NONE;
+		return 0;
+	}
+	if (buf) {
+		free_edstate(buf);
+		buf = NULL;
+	}
+
+	/* XCF_FULLPATH for count 'cause the menu printed by print_expansions()
+	 * was done this way.
+	 */
+	nwords = x_cf_glob(XCF_COMMAND_FILE | (count ? XCF_FULLPATH : 0),
+	    es->cbuf, es->linelen, es->cursor,
+	    &start, &end, &words, &is_command);
+	if (nwords == 0) {
+		vi_error();
+		return -1;
+	}
+	if (count) {
+		int i;
+
+		count--;
+		if (count >= nwords) {
+			vi_error();
+			x_print_expansions(nwords, words, is_command);
+			x_free_words(nwords, words);
+			redraw_line(0);
+			return -1;
+		}
+		/*
+		 * Expand the count'th word to its basename
+		 */
+		if (is_command) {
+			match = words[count] +
+			    x_basename(words[count], NULL);
+			/* If more than one possible match, use full path */
+			for (i = 0; i < nwords; i++)
+				if (i != count &&
+				    strcmp(words[i] + x_basename(words[i],
+				    NULL), match) == 0) {
+					match = words[count];
+					break;
+				}
+		} else
+			match = words[count];
+		match_len = strlen(match);
+		is_unique = 1;
+		/* expanded = PRINT;	next call undo */
+	} else {
+		match = words[0];
+		match_len = x_longest_prefix(nwords, words);
+		expanded = COMPLETE;	/* next call will list completions */
+		is_unique = nwords == 1;
+	}
+
+	buf = save_edstate(es);
+	del_range(start, end);
+	es->cursor = start;
+
+	/* escape all shell-sensitive characters and put the result into
+	 * command buffer */
+	rval = x_escape(match, match_len, x_vi_putbuf);
+
+	if (rval == 0 && is_unique) {
+		/* If exact match, don't undo.  Allows directory completions
+		 * to be used (ie, complete the next portion of the path).
+		 */
+		expanded = NONE;
+
+		/* If not a directory, add a space to the end... */
+		if (match_len > 0 && match[match_len - 1] != '/')
+			rval = putbuf(" ", 1, 0);
+	}
+	x_free_words(nwords, words);
+
+	modified = 1; hnum = hlast;
+	insert = INSERT;
+	lastac = 0;	 /* prevent this from being redone... */
+	refresh(0);
+
+	return rval;
+}
+
+static int
+print_expansions(struct edstate *e)
+{
+	int nwords;
+	int start, end;
+	char **words;
+	int is_command;
+
+	nwords = x_cf_glob(XCF_COMMAND_FILE|XCF_FULLPATH,
+	    e->cbuf, e->linelen, e->cursor,
+	    &start, &end, &words, &is_command);
+	if (nwords == 0) {
+		vi_error();
+		return -1;
+	}
+	x_print_expansions(nwords, words, is_command);
+	x_free_words(nwords, words);
+	redraw_line(0);
+	return 0;
+}
+
+/*
+ * The number of bytes needed to encode byte c.
+ * Control bytes get "M-" or "^" prepended.
+ * This function does not handle tabs.
+ */
+static int
+char_len(int c)
+{
+	int len = 1;
+
+	if ((c & 0x80) && Flag(FVISHOW8)) {
+		len += 2;
+		c &= 0x7f;
+	}
+	if (c < ' ' || c == 0x7f)
+		len++;
+	return len;
+}
+
+/* Similar to x_zotc(emacs.c), but no tab weirdness */
+static void
+x_vi_zotc(int c)
+{
+	if (Flag(FVISHOW8) && (c & 0x80)) {
+		x_puts("M-");
+		c &= 0x7f;
+	}
+	if (c < ' ' || c == 0x7f) {
+		x_putc('^');
+		c ^= '@';
+	}
+	x_putc(c);
+}
+
+static void
+vi_pprompt(int full)
+{
+	pprompt(prompt + (full ? 0 : prompt_skip), prompt_trunc);
+}
+
+static void
+vi_error(void)
+{
+	/* Beem out of any macros as soon as an error occurs */
+	vi_macro_reset();
+	x_putc(BEL);
+	x_flush();
+}
+
+static void
+vi_macro_reset(void)
+{
+	if (macro.p) {
+		afree(macro.buf, APERM);
+		memset((char *) &macro, 0, sizeof(macro));
+	}
+}
+
+static int
+isu8cont(unsigned char c)
+{
+	return !Flag(FVISHOW8) && (c & (0x80 | 0x40)) == 0x80;
+}
+#endif	/* VI */
diff --git a/vis.c b/vis.c
@@ -0,0 +1,241 @@
+/*	$OpenBSD: vis.c,v 1.25 2015/09/13 11:32:51 guenther Exp $ */
+/*-
+ * Copyright (c) 1989, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * 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 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/types.h>
+#include <errno.h>
+#include <ctype.h>
+#include <limits.h>
+#include <string.h>
+#include <stdlib.h>
+#include <vis.h>
+
+#define	isoctal(c)	(((u_char)(c)) >= '0' && ((u_char)(c)) <= '7')
+#define	isvisible(c,flag)						\
+	(((c) == '\\' || (flag & VIS_ALL) == 0) &&			\
+	(((u_int)(c) <= UCHAR_MAX && isascii((u_char)(c)) &&		\
+	(((c) != '*' && (c) != '?' && (c) != '[' && (c) != '#') ||	\
+		(flag & VIS_GLOB) == 0) && isgraph((u_char)(c))) ||	\
+	((flag & VIS_SP) == 0 && (c) == ' ') ||				\
+	((flag & VIS_TAB) == 0 && (c) == '\t') ||			\
+	((flag & VIS_NL) == 0 && (c) == '\n') ||			\
+	((flag & VIS_SAFE) && ((c) == '\b' ||				\
+		(c) == '\007' || (c) == '\r' ||				\
+		isgraph((u_char)(c))))))
+
+/*
+ * vis - visually encode characters
+ */
+char *
+vis(char *dst, int c, int flag, int nextc)
+{
+	if (isvisible(c, flag)) {
+		if ((c == '"' && (flag & VIS_DQ) != 0) ||
+		    (c == '\\' && (flag & VIS_NOSLASH) == 0))
+			*dst++ = '\\';
+		*dst++ = c;
+		*dst = '\0';
+		return (dst);
+	}
+
+	if (flag & VIS_CSTYLE) {
+		switch(c) {
+		case '\n':
+			*dst++ = '\\';
+			*dst++ = 'n';
+			goto done;
+		case '\r':
+			*dst++ = '\\';
+			*dst++ = 'r';
+			goto done;
+		case '\b':
+			*dst++ = '\\';
+			*dst++ = 'b';
+			goto done;
+		case '\a':
+			*dst++ = '\\';
+			*dst++ = 'a';
+			goto done;
+		case '\v':
+			*dst++ = '\\';
+			*dst++ = 'v';
+			goto done;
+		case '\t':
+			*dst++ = '\\';
+			*dst++ = 't';
+			goto done;
+		case '\f':
+			*dst++ = '\\';
+			*dst++ = 'f';
+			goto done;
+		case ' ':
+			*dst++ = '\\';
+			*dst++ = 's';
+			goto done;
+		case '\0':
+			*dst++ = '\\';
+			*dst++ = '0';
+			if (isoctal(nextc)) {
+				*dst++ = '0';
+				*dst++ = '0';
+			}
+			goto done;
+		}
+	}
+	if (((c & 0177) == ' ') || (flag & VIS_OCTAL) ||
+	    ((flag & VIS_GLOB) && (c == '*' || c == '?' || c == '[' || c == '#'))) {
+		*dst++ = '\\';
+		*dst++ = ((u_char)c >> 6 & 07) + '0';
+		*dst++ = ((u_char)c >> 3 & 07) + '0';
+		*dst++ = ((u_char)c & 07) + '0';
+		goto done;
+	}
+	if ((flag & VIS_NOSLASH) == 0)
+		*dst++ = '\\';
+	if (c & 0200) {
+		c &= 0177;
+		*dst++ = 'M';
+	}
+	if (iscntrl((u_char)c)) {
+		*dst++ = '^';
+		if (c == 0177)
+			*dst++ = '?';
+		else
+			*dst++ = c + '@';
+	} else {
+		*dst++ = '-';
+		*dst++ = c;
+	}
+done:
+	*dst = '\0';
+	return (dst);
+}
+
+/*
+ * strvis, strnvis, strvisx - visually encode characters from src into dst
+ *	
+ *	Dst must be 4 times the size of src to account for possible
+ *	expansion.  The length of dst, not including the trailing NULL,
+ *	is returned. 
+ *
+ *	Strnvis will write no more than siz-1 bytes (and will NULL terminate).
+ *	The number of bytes needed to fully encode the string is returned.
+ *
+ *	Strvisx encodes exactly len bytes from src into dst.
+ *	This is useful for encoding a block of data.
+ */
+int
+strvis(char *dst, const char *src, int flag)
+{
+	char c;
+	char *start;
+
+	for (start = dst; (c = *src);)
+		dst = vis(dst, c, flag, *++src);
+	*dst = '\0';
+	return (dst - start);
+}
+
+int
+strnvis(char *dst, const char *src, size_t siz, int flag)
+{
+	char *start, *end;
+	char tbuf[5];
+	int c, i;
+
+	i = 0;
+	for (start = dst, end = start + siz - 1; (c = *src) && dst < end; ) {
+		if (isvisible(c, flag)) {
+			if ((c == '"' && (flag & VIS_DQ) != 0) ||
+			    (c == '\\' && (flag & VIS_NOSLASH) == 0)) {
+				/* need space for the extra '\\' */
+				if (dst + 1 >= end) {
+					i = 2;
+					break;
+				}
+				*dst++ = '\\';
+			}
+			i = 1;
+			*dst++ = c;
+			src++;
+		} else {
+			i = vis(tbuf, c, flag, *++src) - tbuf;
+			if (dst + i <= end) {
+				memcpy(dst, tbuf, i);
+				dst += i;
+			} else {
+				src--;
+				break;
+			}
+		}
+	}
+	if (siz > 0)
+		*dst = '\0';
+	if (dst + i > end) {
+		/* adjust return value for truncation */
+		while ((c = *src))
+			dst += vis(tbuf, c, flag, *++src) - tbuf;
+	}
+	return (dst - start);
+}
+
+int
+stravis(char **outp, const char *src, int flag)
+{
+	char *buf;
+	int len, serrno;
+
+	buf = reallocarray(NULL, 4, strlen(src) + 1);
+	if (buf == NULL)
+		return -1;
+	len = strvis(buf, src, flag);
+	serrno = errno;
+	*outp = realloc(buf, len + 1);
+	if (*outp == NULL) {
+		*outp = buf;
+		errno = serrno;
+	}
+	return (len);
+}
+
+int
+strvisx(char *dst, const char *src, size_t len, int flag)
+{
+	char c;
+	char *start;
+
+	for (start = dst; len > 1; len--) {
+		c = *src;
+		dst = vis(dst, c, flag, *++src);
+	}
+	if (len)
+		dst = vis(dst, *src, flag, '\0');
+	*dst = '\0';
+	return (dst - start);
+}
diff --git a/vis.h b/vis.h
@@ -0,0 +1,88 @@
+/*	$OpenBSD: vis.h,v 1.15 2015/07/20 01:52:27 millert Exp $	*/
+/*	$NetBSD: vis.h,v 1.4 1994/10/26 00:56:41 cgd Exp $	*/
+
+/*-
+ * Copyright (c) 1990 The Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * 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 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)vis.h	5.9 (Berkeley) 4/3/91
+ */
+
+#ifndef _VIS_H_
+#define	_VIS_H_
+
+/*
+ * to select alternate encoding format
+ */
+#define	VIS_OCTAL	0x01	/* use octal \ddd format */
+#define	VIS_CSTYLE	0x02	/* use \[nrft0..] where appropriate */
+
+/*
+ * to alter set of characters encoded (default is to encode all
+ * non-graphic except space, tab, and newline).
+ */
+#define	VIS_SP		0x04	/* also encode space */
+#define	VIS_TAB		0x08	/* also encode tab */
+#define	VIS_NL		0x10	/* also encode newline */
+#define	VIS_WHITE	(VIS_SP | VIS_TAB | VIS_NL)
+#define	VIS_SAFE	0x20	/* only encode "unsafe" characters */
+#define	VIS_DQ		0x200	/* backslash-escape double quotes */
+#define	VIS_ALL		0x400	/* encode all characters */
+
+/*
+ * other
+ */
+#define	VIS_NOSLASH	0x40	/* inhibit printing '\' */
+#define	VIS_GLOB	0x100	/* encode glob(3) magics and '#' */
+
+/*
+ * unvis return codes
+ */
+#define	UNVIS_VALID	 1	/* character valid */
+#define	UNVIS_VALIDPUSH	 2	/* character valid, push back passed char */
+#define	UNVIS_NOCHAR	 3	/* valid sequence, no character produced */
+#define	UNVIS_SYNBAD	-1	/* unrecognized escape sequence */
+#define	UNVIS_ERROR	-2	/* decoder in unknown state (unrecoverable) */
+
+/*
+ * unvis flags
+ */
+#define	UNVIS_END	1	/* no more characters */
+
+char	*vis(char *, int, int, int);
+int	strvis(char *, const char *, int);
+int	stravis(char **, const char *, int);
+int	strnvis(char *, const char *, size_t, int)
+		__attribute__ ((__bounded__(__string__,1,3)));
+int	strvisx(char *, const char *, size_t, int)
+		__attribute__ ((__bounded__(__string__,1,3)));
+int	strunvis(char *, const char *);
+int	unvis(char *, char, int *, int);
+ssize_t strnunvis(char *, const char *, size_t)
+		__attribute__ ((__bounded__(__string__,1,3)));
+
+#endif /* !_VIS_H_ */