about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPeter Stephenson <pws@users.sourceforge.net>2003-02-06 12:21:49 +0000
committerPeter Stephenson <pws@users.sourceforge.net>2003-02-06 12:21:49 +0000
commit5c1f3b65a6f5abeae8459f41adb8fd2316971515 (patch)
tree21a82daa1abab96c967d731c7afe2a3a2bd07fff
parent809ab19dff75185a805b4cbb31a6b89f225167f4 (diff)
downloadzsh-5c1f3b65a6f5abeae8459f41adb8fd2316971515.tar.gz
zsh-5c1f3b65a6f5abeae8459f41adb8fd2316971515.tar.xz
zsh-5c1f3b65a6f5abeae8459f41adb8fd2316971515.zip
18202: New TCP function system plus small error message change in ztcp.
-rw-r--r--ChangeLog8
-rw-r--r--Doc/Makefile.in6
-rw-r--r--Doc/Zsh/manual.yo8
-rw-r--r--Doc/Zsh/modules.yo2
-rw-r--r--Doc/Zsh/tcpsys.yo694
-rw-r--r--Doc/Zsh/zftpsys.yo2
-rw-r--r--Doc/zsh.yo2
-rw-r--r--Doc/zshtcpsys.yo3
-rw-r--r--Functions/TCP/tcp_alias156
-rw-r--r--Functions/TCP/tcp_close134
-rw-r--r--Functions/TCP/tcp_command3
-rw-r--r--Functions/TCP/tcp_expect115
-rw-r--r--Functions/TCP/tcp_fd_handler35
-rw-r--r--Functions/TCP/tcp_log94
-rw-r--r--Functions/TCP/tcp_open197
-rw-r--r--Functions/TCP/tcp_output65
-rw-r--r--Functions/TCP/tcp_proxy31
-rw-r--r--Functions/TCP/tcp_read207
-rw-r--r--Functions/TCP/tcp_rename43
-rw-r--r--Functions/TCP/tcp_send67
-rw-r--r--Functions/TCP/tcp_sess39
-rw-r--r--Functions/TCP/tcp_spam97
-rw-r--r--Functions/TCP/tcp_talk50
-rw-r--r--Functions/TCP/tcp_wait11
-rw-r--r--Functions/TCP/zgprintf70
-rw-r--r--Src/Modules/tcp.c4
-rw-r--r--Src/Modules/tcp.mdd1
27 files changed, 2139 insertions, 5 deletions
diff --git a/ChangeLog b/ChangeLog
index 413752da6..582faf109 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,13 @@
 2003-02-06  Peter Stephenson  <pws@csr.com>
 
+	* 18202: Functions/TCP/*, Doc/Makefile.in, Doc/zsh.yo,
+	Doc/zshtcpsys.yo, Doc/Zsh/manual.yo, Doc/Zsh/modules.yo,
+	Doc/zsh/tcpsys.yo, Doc/Zsh/zftpsys.yo, Src/Modules/tcp.c,
+	Src/Modules/tcp.mdd:  New set of TCP functions tcp_* which
+	run on top of ztcp, documented in zshtcpsys manual.  Also
+	sneaked in more informative error message in zsh/net/tcp
+	for failure to bind to a port.
+
 	* Greg Klanderman <gak@klanderman.net>: 18191:
 	Src/Zle/compresult.c: `compctl -y' didn't obey the listpacked
 	and listrowsfirst options.
diff --git a/Doc/Makefile.in b/Doc/Makefile.in
index 7ed9d02d4..e5f691f08 100644
--- a/Doc/Makefile.in
+++ b/Doc/Makefile.in
@@ -47,7 +47,7 @@ TEXI2HTML = texi2html -expand info -split chapter
 # man pages to install
 MAN = zsh.1 zshbuiltins.1 zshcompctl.1 zshcompwid.1 zshcompsys.1 \
 zshcontrib.1 zshexpn.1 zshmisc.1 zshmodules.1 \
-zshoptions.1 zshparam.1 zshzftpsys.1 zshzle.1 zshall.1
+zshoptions.1 zshparam.1 zshtcpsys.1 zshzftpsys.1 zshzle.1 zshall.1
 
 # yodl documentation
 
@@ -72,7 +72,7 @@ Zsh/filelist.yo Zsh/files.yo Zsh/func.yo Zsh/grammar.yo Zsh/manual.yo \
 Zsh/index.yo Zsh/intro.yo Zsh/invoke.yo Zsh/jobs.yo Zsh/metafaq.yo \
 Zsh/modules.yo Zsh/modlist.yo Zsh/modmenu.yo Zsh/manmodmenu.yo $(MODDOCSRC) \
 Zsh/options.yo Zsh/params.yo Zsh/prompt.yo Zsh/redirect.yo Zsh/restricted.yo \
-Zsh/seealso.yo Zsh/zftpsys.yo Zsh/zle.yo
+Zsh/seealso.yo Zsh/tcpsys.yo Zsh/zftpsys.yo Zsh/zle.yo
 
 # ========== DEPENDENCIES FOR BUILDING ==========
 
@@ -182,6 +182,8 @@ zshoptions.1: Zsh/options.yo
 
 zshparam.1: Zsh/params.yo
 
+zshtcpsys.1: Zsh/tcpsys.yo
+
 zshzftpsys.1: Zsh/zftpsys.yo
 
 zshzle.1: Zsh/zle.yo
diff --git a/Doc/Zsh/manual.yo b/Doc/Zsh/manual.yo
index fb23e6fcf..9fb81d99a 100644
--- a/Doc/Zsh/manual.yo
+++ b/Doc/Zsh/manual.yo
@@ -33,6 +33,7 @@ menu(Completion Widgets)
 menu(Completion System)
 menu(Completion Using compctl)
 menu(Zsh Modules)
+menu(TCP Function System)
 menu(Zftp Function System)
 menu(User Contributions)
 
@@ -137,6 +138,13 @@ Zsh Modules
 
 includefile(Zsh/manmodmenu.yo)
 
+TCP Function System
+
+menu(TCP Functions)
+menu(TCP Parameters)
+menu(TCP Examples)
+menu(TCP Bugs)
+
 Zftp Function System
 
 menu(Installation)
diff --git a/Doc/Zsh/modules.yo b/Doc/Zsh/modules.yo
index 828b97d37..cbec478e3 100644
--- a/Doc/Zsh/modules.yo
+++ b/Doc/Zsh/modules.yo
@@ -1,4 +1,4 @@
-texinode(Zsh Modules)(Zftp Function System)(Completion Using compctl)(Top)
+texinode(Zsh Modules)(TCP Function System)(Completion Using compctl)(Top)
 chapter(Zsh Modules)
 cindex(modules)
 sect(Description)
diff --git a/Doc/Zsh/tcpsys.yo b/Doc/Zsh/tcpsys.yo
new file mode 100644
index 000000000..40c92c1cb
--- /dev/null
+++ b/Doc/Zsh/tcpsys.yo
@@ -0,0 +1,694 @@
+texinode(TCP Function System)(Zftp Function System)(Zsh Modules)(Top)
+chapter(TCP Function System)
+cindex(TCP function system)
+cindex(ztcp, function system based on)
+sect(Description)
+
+A module tt(zsh/net/tcp) is provided to provide network I/O over
+TCP/IP from within the shell; see its description in
+ifzman(\
+zmanref(zshmodules)
+)\
+ifnzman(\
+noderef(Zsh Modules)
+).  This manual page describes a function suite based on the module.  The
+functions will usually be installed at the same time as the module if that
+is present on your system, in which case they will be available for
+autoloading in the default function search path.  In addition to the
+tt(zsh/net/tcp) module, the tt(zsh/zselect) module is used to implement
+timeouts on read operations.  For troubleshooting tips, consult the
+corresponding advice for the tt(zftp) functions described in
+ifzman(\
+zmanref(zshftpsys)
+)\
+ifnzman(\
+noderef(Zftp Function System)
+).
+
+There are functions corresponding to the basic I/O operations open, close,
+read and send, named tt(tcp_open) etc., as well as a function
+tt(tcp_expect) for pattern match analysis of data read as input.  The
+system makes it easy to receive data from and send data to multiple named
+sessions at once.  In addition, it can be linked with the shell's line
+editor in such a way that input data is automatically shown at the
+terminal.  Other facilities available including logging, filtering and
+configurable output prompts.
+
+To use the system where it is available, it should be enough to
+`tt(autoload -U tcp_open)' and run tt(tcp_open) as documented below to
+start a session.  The tt(tcp_open) function will autoload the remaining
+functions.
+
+startmenu()
+menu(TCP Functions)
+menu(TCP Parameters)
+menu(TCP Examples)
+menu(TCP Bugs)
+endmenu()
+
+texinode(TCP Functions)(TCP Parameters)()(TCP Function System)
+sect(TCP User Functions)
+
+subsect(Basic I/O)
+
+startitem()
+findex(tcp_open)
+xitem(tt(tcp_open [-qz]) var(host port) tt([) var(sess) tt(]))
+xitem(tt(tcp_open [-qz] [ -s) var(sess) tt(| -l) var(sess)tt(,... ] ... ))
+item(tt(tcp_open [-qz] [-a) var(fd) tt(| -f) var(fd) tt(] [) var(sess) tt(]))(
+Open a new session.  In the first and simplest form, open a TCP connection
+to host var(host) at port var(port); numeric and symbolic forms are
+understood for both.
+
+If var(sess) is given, this becomes the name of the session which can be
+used to refer to multiple different TCP connections.  If var(sess) is
+not given, the function will invent a numeric name value (note this is
+em(not) the same as the file descriptor to which the session is attached).
+It is recommended that session names not include `funny' characters, where
+funny characters are not well-defined but certainly do not include
+alphanumerics or underscores, and certainly do include whitespace.
+
+In the second case, one or more sessions to be opened are given by name.
+A single session name is given after tt(-s) and a comma-separated list
+after tt(-l); both options may be repeated as many times as necessary.
+The host and port are read from the file tt(.ztcp_sessions) in the same
+directory as the user's zsh initialisation files, i.e. usually the home
+directory, but tt($ZDOTDIR) if that is set.  The file consists of lines
+each giving a session name and the corresponding host and port, in that
+order (note the session name comes first, not last), separated by
+whitespace.
+
+The third form allows passive and fake TCP connections.  If the option
+tt(-a) is used, its argument is a file descriptor open for listening for
+connections.  No function front-end is provided to open such a file
+descriptor, but a call to `tt(ztcp -l) var(port)' will create one with the
+file descriptor stored in the parameter tt($REPLY).  The listening port can
+be closed with `tt(ztcp -c) var(fd)'.  A call to `tt(tcp_open -a) var(fd)'
+will block until a remote TCP connection is made to var(port) on the local
+machine.  At this point, a session is created in the usual way and is
+largely indistinguishable from an active connection created with one of the
+first two forms.
+
+If the option tt(-f) is used, its argument is a file descriptor which is
+used directly as if it were a TCP session.  How well the remainder of the
+TCP function system copes with this depends on what actually underlies this
+file descriptor.  A regular file is likely to be unusable; a FIFO (pipe) of
+some sort will work better, but note that it is not a good idea for two
+different sessions to attempt to read from the same FIFO at once.
+
+If the option tt(-q) is given with any of the three forms, tt(tcp_open)
+will not print informational messages, although it will in any case exit
+with an appropriate status.
+
+If the line editor (zle) is in use, which it usually is if and only if the
+shell is interactive, tt(tcp_open) installs a handler inside tt(zle) which
+will check for new data at the same time as it checks for keyboard input.
+This is convenient as the shell consumes no CPU time while waiting; the
+test is performed by the operating systems.  However, if incoming data
+is only to be read explicitly, the option tt(-z) to any of the forms of
+tt(tcp_open) prevents the handler from being installed.   Note this is not
+necessary for executing complete sets of send and read commands from a
+function, as zle is not active at this point.  Generally speaking, the
+handler is only active when the shell is waiting for input at a command
+prompt or in the tt(vared) builtin.  The option has no effect if zle is not
+active; `tt([[ -o zle]])' will test for this.
+
+The first session to be opened becomes the current session; subsequent
+calls to tt(tcp_open) will not change this.  The current session is stored
+in the parameter tt($TCP_SESS); see below for more detail about the
+parameters used by the system.
+)
+findex(tcp_close)
+item(tt(tcp_close [-qn] [ -a | -l) var(sess)tt(,... |) var(sess) tt(... ]))(
+Close the named sessions, or the current session if none is given,
+or all open sessions if tt(-a) is given.  The options tt(-l) and tt(-s) are
+both handled for consistency with tt(tcp_open), although the latter is
+redundant.
+
+If the session being closed is the current one, tt($TCP_SESS) is unset,
+leaving no current session, even if there are other sessions still open.
+
+If the session was opened with tt(tcp_open -f), the file descriptor is
+closed so long as it is in the range 0 to 9 accessible directly from the
+command line.  If the option tt(-n) is given, no attempt will be made to
+close file descriptors in this case.  The tt(-n) option is not used for
+genuine tt(ztcp) session; the file descriptors are always closed with the
+session.
+
+If the option tt(-q) is given, no informational messages will be printed.
+)
+findex(tcp_read)
+xitem(tt(tcp_read [-bdq] [ -t) var(TO) tt(] [ -T) var(TO) tt(]))
+item(    tt([ -a | -u) var(fd) tt(... | -l) var(sess)tt(,... | -s) var(sess) tt(...]))(
+Perform a read operation on the current session, or on a list of sessions
+if any are given (the first form), or all open sessions (the second form).
+Any of the tt(-u), tt(-l) or tt(-s) options may be repeated or mixed
+together.  The tt(-u) option specifies a file descriptor directly (only
+those managed by this system are useful), the other two specify sessions as
+described for tt(tcp_open) above.  If tt(-a) is given, all sessions ares
+examined for new data.
+
+The function checks for new data available on all the sessions listed.
+Unless the tt(-b) option is given, it will not block waiting for new data.
+Any one line of data from any of the available sessions will be read,
+stored in the parameter tt($TCP_LINE), and displayed to standard output
+unless tt($TCP_SILENT) contains a non-empty string.  When printed to
+standard output the string tt($TCP_PROMPT) will be shown at the start of
+the line; the default form for this includes the name of the session being
+read.  See below for more information on these parameters.  In this mode,
+tt(tcp_read) can be called repeatedly until it returns status 2 which
+indicates all pending input from all specified sessions has been handled.
+
+With the option tt(-b), equivalent to an infinite timeout, the function
+will block until a line is available to read from one of the specified
+sessions.  However, only a single line is returned.
+
+The option tt(-d) indicates that all pending input should be drained.  In
+this case tt(tcp_read) may process multiple lines in the manner given
+above; only the last is stored in tt($TCP_LINE), but the complete set is
+stored in the array tt($tcp_lines).  This is cleared at the start of each
+call to tt(tcp_read).
+
+The options tt(-t) and tt(-T) specify a timeout in seconds, which may be a
+floating point number for increased accuracy.  With tt(-t) the timeout is
+applied before each line read.  With tt(-T), the timeout applies to the
+overall operation, possibly including multiple read operations if the
+option tt(-d) is present; without this option, there is no distinction
+between tt(-t) and tt(-T).
+
+The function does not print informational messages, but if the option
+tt(-q) is given, no error message is printed for a non-existent session.
+
+A return value of 2 indicates a timeout or no data to read.  Any other
+non-zero return value indicates some error condition.
+
+See tt(tcp_log) for how to control where data is sent by tt(tcp_read).
+)
+findex(tcp_send)
+xitem(tt(tcp_send [-nq] [ -s) var(sess) tt(| -l) var(sess)tt(,... ]) var(data) tt(...))
+item(tt(tcp_send [-nq] -a) var(data) tt(...))(
+Send the supplied data strings to all the specified sessions in turn.  The
+underlying operation differs little from a `tt(print -r)' to the session's
+file descriptor, although it attempts to prevent the shell from dying owing
+to a tt(SIGPIPE) caused by an attempt to write to a defunct session.
+
+The option tt(-n) prevents tt(tcp_send) from putting a newline at the end
+of the data strings.
+
+The remaining options all behave as for tt(tcp_read).
+
+The data arguments are not further processed once they have been passed to
+tt(tcp_send); they are simply passed down to tt(print -r).
+
+If the parameter tt($TCP_OUTPUT) is a non-empty string and logging is
+enabled then the data sent to each session will be echoed to the log
+file(s) with tt($TCP_OUTPUT) in front where appropriate, much in the manner
+of tt($TCP_PROMPT).
+)
+enditem()
+
+subsect(Session Management)
+
+startitem()
+findex(tcp_alias)
+xitem(tt(tcp_alias [-q]) var(alias)tt(=)var(sess) tt(...))
+xitem(tt(tcp_alias [-q] [) var(alias) tt(] ...))
+item(tt(tcp_alias -d [-q]) var(alias) tt(...))(
+This function is not particularly well tested.
+
+The first form creates an alias for a session name; var(alias) can then be
+used to refer to the existing session var(sess).  As many aliases may be
+listed as required.
+
+The second form lists any aliases specified, or all aliases if none.
+
+The third form deletes all the aliases listed.  The underlying sessions are
+not affected.
+
+The option tt(-q) suppresses an inconsistently chosen subset of error
+messages.
+)
+findex(tcp_log)
+item(tt(tcp_log [-asc] [ -n | -N ] [) var(logfile) tt(]))(
+With an argument var(logfile), all future input from tt(tcp_read) will be
+logged to the named file.  Unless tt(-a) (append) is given, this file will
+first be truncated or created empty.  With no arguments, show the current
+status of logging.
+
+With the option tt(-s), per-session logging is enabled.  Input from
+tt(tcp_read) is output to the file var(logfile).var(sess).  As the
+session is automatically discriminated by the filename, the contents are
+raw (no tt($TCP_PROMPT)).  The option  tt(-a) applies as above.
+Per-session logging and logging of all data in one file are not mutually
+exclusive.
+
+The option tt(-c) closes all logging, both complete and per-session logs.
+
+The options tt(-n) and tt(-N) respectively turn off or restore output of
+data read by tt(tcp_read) to standard output; hence `tt(tcp_log -cn)' turns
+off all output by tt(tcp_read).
+
+The function is purely a convenient front end to setting the parameters
+tt($TCP_LOG), tt($TCP_LOG_SESS), tt($TCP_SILENT), which are described below.
+)
+findex(tcp_rename)
+item(tt(tcp_rename) var(old) var(new))(
+Rename session var(old) to session var(new).  The old name becomes invalid.
+)
+findex(tcp_sess)
+item(tt(tcp_sess [) var(sess) tt([) var(command)  tt(... ] ]))(
+With no arguments, list all the open sessions and associated file
+descriptors.  The current session is marked with a star.  For use in
+functions, direct access to the parameters tt($tcp_by_name), tt($tcp_by_fd)
+and tt($TCP_SESS) is probably more convenient; see below.
+
+With a var(sess) argument, set the current session to var(sess).
+This is equivalent to changing tt($TCP_SESS) directly.
+
+With additional arguments, temporarily set the current session while
+executing the string tt(command ...).  The first argument is re-evaluated
+so as to expand aliases etc., but the remaining arguments are passed
+through as the appear to tt(tcp_sess).  The original session is restored
+when tt(tcp_sess) exits.
+)
+enditem()
+
+subsect(Advanced I/O)
+
+startitem()
+findex(tcp_command)
+item(tt(tcp_command) var(send-options) tt(...) var(send-arguments) tt(...))(
+This is a convenient front-end to tt(tcp_send).  All arguments are passed
+to tt(tcp_send), then the function pauses waiting for data.  While data is
+arriving at least every tt($TCP_TIMEOUT) (default 0.3) seconds, data is
+handled and printed out according to the current settings.  Status 0 is
+always returned.
+
+This is generally only useful for interactive use, to prevent the display
+becomming fragmented by output returned from the connection.  Within a
+programe or function it is generally better to handle reading data by a
+more explicit method.
+)
+findex(tcp_expect)
+xitem(tt(tcp_expect [ -q ] [ -p) var(var) tt(] [ -t ) var(to) tt(| -T) var(TO)tt(]))
+item(tt(    [ -a | -s) var(sess) tt(... | -l) var(sess)tt(,... ]) var(pattern) ...)(
+Wait for input matching any of the given var(pattern)s from any of the
+specified sessions.  Input is ignored until an input line matches one of
+the given patterns; at this point status zero is returned, the matching
+line is stored in tt($TCP_LINE), and the full set of lines read during the
+call to tt(tcp_expect) is stored in the array tt($tcp_expect_lines).
+
+Sessions are specified in the same way as tt(tcp_read): the default is to
+use the current session, otherwise the sessions specified by tt(-a),
+tt(-s), or tt(-l) are used.
+
+Each var(pattern) is a standard zsh extended-globbing pattern; note that it
+needs to be quoted to avoid it being expanded immediately by filename
+generation.  It must match the full line, so to match a substring there
+must be a `tt(*)' at the start and end.  The line matched against includes
+the tt($TCP_PROMPT) added by tt(tcp_read).  It is possible to include the
+globbing flags `tt(#b)' or `tt(#m)' in the patterns to make backreferences
+available in the parameters tt($MATCH), tt($match), etc., as described in
+the base zsh documentation on pattern matching.
+
+Unlike tt(tcp_read), the default behaviour of tt(tcp_expect) is to block
+indefinitely until the required input is found.  This can be modified by
+specifying a timeout with tt(-t) or tt(-T); these function as in
+tt(tcp_read), specifying a per-read or overall timeout, respectively, in
+seconds, as an integer or floating-point number.  As tt(tcp_read), the
+function returns status 2 if a timeout occurs.
+
+The function returns as soon as any one of the patterns given match.  If
+the caller needs to know which of the patterns matched, the option tt(-p)
+var(var) can be used; on return, tt($var) is set to the number of the
+pattern using ordinary zsh indexing, i.e. the first is 1, and so on.  Note
+tha absence of a `tt($)' in front of var(var).  To avoid clashes, the
+parameter cannot begin with `tt(_expect)'.
+
+The option tt(-q) is passed directly down to tt(tcp_read).
+
+As all input is done via tt(tcp_read), all the usual rules about output of
+lines read apply.  One exception is that the parameter tt($tcp_lines) will
+only reflect the line actually matched by tt(tcp_expect); use
+tt($tcp_expect_lines) for the full set of lines read during the function
+call.
+)
+findex(tcp_proxy)
+item(tt(tcp_proxy))(
+This is a simple-minded function to accept a TCP connection and execute a
+command with I/O redirected to the connection.  Extreme caution should be
+taken as there is no security whatsoever and this can leave your computer
+open to the world.  Ideally, it should only be used behind a firewall.
+
+The first argument is a TCP port on which the function will listen.
+
+The remaining arguments give a command and its arguments to execute with
+standard input, standard output and standard error redirected to the
+file descriptor on which the TCP session has been accepted.
+If no command is given, a new zsh is started.  This gives everyone on
+your network direct access to your account, which in many cases will be a
+bad thing.
+
+The command is run in the background, so tt(tcp_proxy) can then accept new
+connections.  It continues to accept new connections until interrupted.
+)
+findex(tcp_spam)
+item(tt(tcp_spam [-rtv] [ -a | -s ) var(sess) tt(| -l) var(sess)tt(,... ]) var(cmd) tt(...))(
+Execute `var(cmd) tt(...)' for each session in turn.  Note this executes
+the command and arguments; it does not send the command line as data
+unless the tt(-t) (transmit) option is given.
+
+The sessions may be selected explicitly with the standard tt(-a), tt(-s) or
+tt(-l) options, or may be chosen implicitly.  If none of the three options
+is given the rules are: first, if the array tt($tcp_spam_list) is set, this
+is taken as the list of sessions, otherwise all sessions are taken.
+Second, any sessions given in the array tt($tcp_no_spam_list) are removed
+from the list of sessions.
+
+Normally, any sessions added by the `tt(-a)' flag or when all sessions are
+chosen implicitly are spammed in alphabetic order; sessions given by the
+tt($tcp_spam_list) array or on the command line are spammed in the order
+given.  The tt(-r) flag reverses the order however it was arrived it.
+
+The tt(-v) flag specifies that a tt($TCP_PROMPT) will be output before each
+session.  This is output after any modfication to TCP_SESS by the
+user-defined tt(tcp_on_spam) function described below.  (Obviously that
+function is able to generate its own output.)
+)
+findex(tcp_talk)
+item(tt(tcp_talk))(
+This is a fairly simple-minded attempt to force input to the line editor to
+go straight to the default TCP_SESSION.
+
+An escape string, tt($TCP_TALK_ESCAPE), default `:', is used to allow
+access to normal shell operation.  If it is on its own at the start of the
+line, or followed only by whitespace, the line editor returns to normal
+operation.  Otherwise, the string and any following whitespace are skipped
+and the remainder of the line executed as shell input without any change of
+the line editor's operating mode.
+
+The current implementation is somewhat deficient in terms of use of the
+command history.  For this reason, many users will prefer to use some form
+of alternative approach for sending data easily to the current session.
+One simple approach is to alias some special character (such as `tt(%)') to
+`tt(tcp_command --)'.
+)
+findex(tcp_wait)
+item(tt(tcp_wait))(
+The sole argument is an integer or floating point number which gives the
+seconds to delay.  The shell will do nothing for that period except wait
+for input on all TCP sessions by calling tt(tcp_read -a).  This is similar
+to the interactive behaviour at the command prompt when zle handlers are
+installed.
+)
+enditem()
+
+sect(TCP User-defined Function)
+
+Certain functions, if defined by the user, will be called by the function
+system in certain contexts.  This facility depends on the module
+tt(zsh/parameter), which is usually available in interactive shells as the
+completion system depends on it.  None of the functions need by defined;
+they simply provide convenient hooks when necessary.
+
+Typically, these are called after the requested action has been taken, so
+that the various parameters will reflect the new state.
+
+startitem()
+findex(tcp_on_alias)
+item(tt(tcp_on_alias) var(alias) var(fd))(
+When an alias is defined, this function will be called with two arguments:
+the name of the alias, and the file descriptor of the corresponding session.
+)
+findex(tcp_on_close)
+item(tt(tcp_on_close) var(sess) var(fd))(
+This is called with the name of a session being closed and the file
+descriptor which corresponded to that session.  Both will be invalid by
+the time the function is called.
+)
+findex(tcp_on_open)
+item(tt(tcp_on_open) var(sess) var(fd))(
+This is called after a new session has been defined with the session name
+and file descriptor as arguments.
+)
+findex(tcp_on_rename)
+item(tt(tcp_on_rename) var(oldsess) var(fd) var(newsess))(
+This is called after a session has been renamed with the three arguments
+old session name, file descriptor, new session name.
+)
+findex(tcp_on_spam)
+item(tt(tcp_on_spam) var(sess) var(command) tt(...))(
+This is called once for each session spammed, just em(before) a command is
+executed for a session by tt(tcp_spam).  The arguments are the session name
+followed by the command list to be executed.  If tt(tcp_spam) was called
+with the option tt(-t), the first command will be tt(tcp_send).
+
+This function is called after tt($TCP_SESS) is set to reflect the session
+to be spammed, but before any use of it is made.  Hence it is possible to
+alter the value of tt($TCP_SESS) within this function.  For example, the
+session arguments to tt(tcp_spam) could include extra information to be
+stripped off and processed in tt(tcp_on_spam).
+
+If the function sets the parameter tt($REPLY) to `tt(done)', the command
+line is not executed; in addition, no prompt is printed for the tt(-v)
+option to tt(tcp_spam).
+)
+findex(tcp_on_unalias)
+item(tt(tcp_on_unalias) var(alias) var(fd))(
+This is called with the name of an alias and the corresponding session's
+file descriptor after an alias has been deleted.
+)
+enditem()
+
+sect(TCP Utility Functions)
+
+The following functions are used by the TCP function system but will rarely
+if ever need to be called directly.
+
+startitem()
+findex(tcp_fd_handler)
+item(tt(tcp_fd_handler))(
+This is the function installed by tt(tcp_open) for handling input from
+within the line editor, if that is required.  It is in the format
+documented for the builtin `tt(zle -F)' in
+ifzman(\
+zmanref(zshzle)
+)\
+ifnzman(\
+noderef(Zle Builtins)
+).
+)
+findex(tcp_output)
+item(tt(tcp_output [ -q ] -P) var(prompt) tt(-F) var(fd) tt(-S) var(sess))(
+This function is used for both logging and handling output to standard
+output, from within tt(tcp_read) and (if tt($TCP_OUTPUT) is set)
+tt(tcp_send).
+
+The var(prompt) to use is specified by tt(-P); the default is the empty
+string.  It can contain `tt(%s)' which is replaced by the session name, or
+`tt(%f)' which is replaced by the session's file descriptor; `tt(%%)' is
+replaced by a single `tt(%)'.
+
+The option tt(-q) suppresses output to standard output, but not to any log
+files which are configured.
+
+The tt(-S) and tt(-F) options are used to pass in the session name and file
+descriptor for possible replacement in the prompt.
+)
+findex(zgprintf)
+item(tt(zgprintf) tt(-rPR -%)var(X)tt(=)var(subst) var(fmt) tt([) var(val) tt(... ]))(
+This function is used for performing tt(%)-replacement in prompts supplied
+to tt(tcp_output).  The var(fmt) string is printed to standard output.
+The option tt(-%)var(X)tt(=)var(subst) specifies that any occurrence
+of tt(%)var(X) in the var(fmt) string should be replaced by var(subst).
+These arguments may be repeated for arbitrary var(X).
+
+The option tt(-r) specifies that the normal tt(print) conventions are not
+to be used, as with the corresponding argument to the tt(print) builtin.
+
+The option tt(-R) specifies that the output is to be left in the parameter
+tt($REPLY) instead of being printed.
+
+The option tt(-P) specifies that unhandled tt(%)-escapes should be
+formatted by a call to tt(printf).  Each is assumed to consume exactly one
+additional var(val) argument.  This option is only minimally implemented.
+)
+enditem()
+
+texinode(TCP Parameters)(TCP Examples)(TCP Functions)(TCP Function System)
+sect(TCP User Parameters)
+
+Parameters follow the usual convention that uppercase is used for scalars
+and integers, while lowercase is used for normal and associative array.
+It is always safe for user code to read these parameters; some parameters
+may also be set, which are noted explicitly.  Other are included in this
+group as they are set by the function system for the user's benefit,
+i.e. setting them is typically not useful but is benign.
+
+It is often also useful to make settable parameters local to a function.
+For example, `tt(local TCP_SILENT=1)' specifies that data read during the
+function call will not be printed to standard output, regardless of the
+setting outside the function.  Likewise, `tt(local TCP_SESS=)var(sess)'
+sets a session for the duration of a function.
+
+startitem()
+findex(tcp_expect_lines)
+item(tt(tcp_expect_lines))(
+Array.  The set of lines read during the last call to tt(tcp_expect),
+including the last (tt($TCP_LINE)).
+)
+findex(tcp_filter)
+item(tt(tcp_filter))(
+Array. May be set directly.  A set of extended globbing patterns which,
+if matched in tt(tcp_output), will cause the line not to be printed to
+standard output.  The patterns should be defined as described for the
+arguments to tt(tcp_expect).  Output of line to log files is not affected.
+)
+findex(TCP_LINE)
+item(tt(TCP_LINE))(
+The last line read by tt(tcp_read), and hence also tt(tcp_expect).
+)
+findex(TCP_LINE_FD)
+item(tt(TCP_LINE_FD))(
+The file descriptor from which tt($TCP_LINE) was read.
+tt(${tcp_by_fd[$TCP_LINE_FD]}) will give the corresponding session name.
+)
+findex(tcp_lines)
+item(tt(tcp_lines))(
+Array. The set of lines read during the last call to tt(tcp_read),
+including the last (tt($TCP_LINE)).
+)
+findex(TCP_LOG)
+item(tt(TCP_LOG))(
+May be set directly, although it is also controlled by tt(tcp_log).
+The name of a file to which output from all sessions will be sent.
+The output is proceeded by the usual tt($TCP_PROMPT).  If it is not an
+absolute path name, it will follow the user's current directory.
+)
+findex(TCP_LOG_SESS)
+item(tt(TCP_LOG_SESS))(
+May be set directly, although it is also controlled by tt(tcp_log).
+The prefix for a set of files to which output from each session separately
+will be sent; the full filename is tt(${TCP_LOG_SESS}.)var(sess).
+Output to each file is raw; no prompt is added.  If it is not an absolute
+path name, it will follow the user's current directory.
+)
+findex(tcp_nospam_list)
+item(tt(tcp_nospam_list))(
+Array.  May be set directly.  See tt(tcp_spam) for how this is used.
+)
+findex(TCP_OUTPUT)
+item(tt(TCP_OUTPUT))(
+May be set directly.  If a non-empty string, any data sent to a session by
+tt(tcp_send) will be logged.  The prompt has the same format as
+tt(TCP_PROMPT) and the same rules for its use apply:  it is used in a file
+specified by tt($TCP_LOG), but not in a file generated from
+tt($TCP_LOG_SESS).
+)
+findex(TCP_PROMPT)
+item(tt(TCP_PROMPT))(
+May be set directly.  Used as the prefix for data read by tt(tcp_read)
+which is printed to standard output or to the log file given by
+tt($TCP_LOG), if any.  Any `tt(%s)', `tt(%f)' or `tt(%%)' occurring in the
+string will be replaced by the name of the session, the session's
+underlying file descriptor, or a single `tt(%)', respectively.
+)
+findex(TCP_READ_DEBUG)
+item(tt(TCP_READ_DEBUG))(
+May be set directly.  If this has non-zero length, tt(tcp_read) will give
+some limited diagnostics about data being read.
+)
+findex(TCP_SESS)
+item(tt(TCP_SESS))(
+May be set directly.  The current session; must refer to one of the
+sessions established by tt(tcp_open).
+)
+findex(TCP_SILENT)
+item(tt(TCP_SILENT))(
+May be set directly, although it is also controlled by tt(tcp_log).
+If of non-zero length, data read by tt(tcp_read) will not be written to
+standard output, though may still be written to a log file.
+)
+findex(tcp_spam_list)
+item(tt(tcp_spam_list))(
+Array.  May be set directly.  See the description of the function
+tt(tcp_spam) for how this is used.
+)
+findex(TCP_TALK_ESCAPE)
+item(tt(TCP_TALK_ESCAPE))(
+May be set directly.  See the description of the function tt(tcp_talk) for
+how this is used.
+)
+findex(TCP_TIMEOUT)
+item(tt(TCP_TIMEOUT))(
+May be set directly.  Currently this is only used by the function
+tt(tcp_command), see above.
+)
+enditem()
+
+sect(TCP Utility Parameters)
+
+These parameters are controlled by the function system; they may be read
+directly, but should not usually be set by user code.
+
+startitem()
+findex(tcp_aliases)
+item(tt(tcp_aliases))(
+Associative array.  The keys are the names of sessions established with
+tt(tcp_open); each value is a space-separated list of aliases which refer
+to that session.
+)
+findex(tcp_by_fd)
+item(tt(tcp_by_fd))(
+Associative array.  The keys are session file descriptors; each
+value is the name of that session.
+)
+findex(tcp_by_name)
+item(tt(tcp_by_name))(
+Associative array.  The keys are the names of sessions; each value is the
+file descriptor associated with that session.
+)
+enditem()
+
+texinode(TCP Examples)(TCP Bugs)(TCP Parameters)(TCP Function System)
+sect(TCP Examples)
+
+Here is a trivial example using a remote calculator.
+
+TO create a calculator server on port 7337 (see the tt(dc) manual page for
+quite how infuriating the underlying command is):
+
+example(tcp_proxy 7337 dc)
+
+To connect to this from the same host with a session also named `tt(dc)':
+
+example(tcp_open localhost 7337 dc)
+
+To send a command to the remote session and wait a short while for output
+(assuming tt(dc) is the current session):
+
+example(tcp_command 2 4 + p)
+
+To close the session:
+
+example(tcp_close)
+
+The tt(tcp_proxy) needs to be killed to be stopped.  Note this will not
+usually kill any connections which have already been accepted, and also
+that the port is not immediately available for reuse.
+
+The following chunk of code puts a list of sessions into an xterm header,
+with the current session followed by a star.
+
+example(print -n "\033]2;TCP:" ${(k)tcp_by_name:/$TCP_SESS/$TCP_SESS\*} "\a")
+
+texinode(TCP Bugs)()(TCP Examples)(TCP Function System)
+sect(TCP Bugs)
+
+The function tt(tcp_read) uses the shell's normal tt(read) builtin.  As
+this reads a complete line at once, data arriving without a terminating
+newline can cause the function to block indefinitely.
+
+Though the function suite works well for interactive use and for data
+arriving in small amounts, the performance when large amounts of data are
+being exchanged is likely to be extremely poor.
diff --git a/Doc/Zsh/zftpsys.yo b/Doc/Zsh/zftpsys.yo
index b8ffa562b..95cb6f5d2 100644
--- a/Doc/Zsh/zftpsys.yo
+++ b/Doc/Zsh/zftpsys.yo
@@ -1,4 +1,4 @@
-texinode(Zftp Function System)(User Contributions)(Zsh Modules)(Top)
+texinode(Zftp Function System)(User Contributions)(TCP Function System)(Top)
 chapter(Zftp Function System)
 cindex(zftp function system)
 cindex(FTP, functions for using shell as client)
diff --git a/Doc/zsh.yo b/Doc/zsh.yo
index e6bf38310..5258555a8 100644
--- a/Doc/zsh.yo
+++ b/Doc/zsh.yo
@@ -63,6 +63,7 @@ ifnzman(includefile(Zsh/compwid.yo))
 ifnzman(includefile(Zsh/compsys.yo))
 ifnzman(includefile(Zsh/compctl.yo))
 ifnzman(includefile(Zsh/modules.yo))
+ifnzman(includefile(Zsh/tcpsys.yo))
 ifnzman(includefile(Zsh/zftpsys.yo))
 ifnzman(includefile(Zsh/contrib.yo))
 ifzshall(\
@@ -78,6 +79,7 @@ source(zshcompwid)
 source(zshcompsys)
 source(zshcompctl)
 source(zshmodules)
+source(zshtcpsys)
 source(zshzftpsys)
 source(zshcontrib)
 manpage(ZSHALL)(1)(date())(zsh version())
diff --git a/Doc/zshtcpsys.yo b/Doc/zshtcpsys.yo
new file mode 100644
index 000000000..52cd86153
--- /dev/null
+++ b/Doc/zshtcpsys.yo
@@ -0,0 +1,3 @@
+manpage(ZSHTCPSYS)(1)(date())(zsh version())
+manpagename(zshtcpsys)(zsh tcpletion system)
+includefile(Zsh/tcpsys.yo)
diff --git a/Functions/TCP/tcp_alias b/Functions/TCP/tcp_alias
new file mode 100644
index 000000000..9d6a28da0
--- /dev/null
+++ b/Functions/TCP/tcp_alias
@@ -0,0 +1,156 @@
+# Create an alias for a TCP session.
+#
+# The syntax is similar to the `alias' builtin.  Aliases with a trailing
+# `=' are assigned, while those without are listed.
+#
+# The alias can be used to refer to the session, however any output
+# from the session will be shown using information for the base
+# session name.  Likewise, any other reference to the session's file
+# descriptor will cause the original session name rather than the alias to
+# be used.
+#
+# It is an error to attempt to create an alias for a non-existent session.
+# The alias will be removed when the session is closed.
+#
+# An alias can be reused without the session having to be closed.
+# However, a base session name cannot be used as an alias.  If this
+# becomes necessary, the base session should be renamed with tcp_rename
+# first.
+#
+# With no arguments, list aliases.
+#
+# With the option -d, delete the alias.  No value is allowed in this case.
+#
+# With the option -q (quiet), just return status 1 on failure.  This
+# does not apply to bad syntax, which is always reported.  Bad syntax
+# includes deleting aliases when supplying a value.
+
+emulate -L zsh
+setopt extendedglob cbases
+
+local opt quiet base value alias delete arg match mbegin mend fd array
+integer stat index
+
+while getopts "qd" opt; do
+  case $opt in
+    (q) quiet=1
+	;;
+    (d) delete=1
+	;;
+    (*) return 1
+	;;
+  esac
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+if (( ! $# )); then
+  if (( ${#tcp_aliases} )); then
+    for fd value in ${(kv)tcp_aliases}; do
+      for alias in ${=value}; do
+	print -r - \
+	"${alias}: alias for session ${tcp_by_fd[$fd]:-unnamed fd $fd}"
+      done
+    done
+  fi
+  return 0
+fi
+
+for arg in $*; do
+  if [[ $arg = (#b)([^=]##)=(*) ]]; then
+    if [[ -n $delete ]]; then
+      print "$0: value given with deletion command." >&2
+      stat=1
+      continue
+    fi
+    alias=$match[1]
+    base=$match[2]
+    if [[ -z $base ]]; then
+      # hmm, this is very nearly a syntax error...
+      [[ -z $quiet ]] && print "$0: empty value for alias $alias" >&2
+      stat=1
+      continue
+    fi
+    if [[ ${+tcp_by_name} -eq 0 || -z ${tcp_by_name[$base]} ]]; then
+      [[ -z $quiet ]] && print "$0: no base session \`$base' for alias"
+      stat=1
+      continue
+    fi
+    if [[ -n ${tcp_by_name[$alias]} ]]; then
+      # already exists, OK if this is an alias...
+      fd=${tcp_by_name[$alias]}
+      array=(${=tcp_aliases[$fd]})
+      if [[ -n ${array[(r)$alias]} ]]; then
+	# yes, we're OK; delete the old alias.
+	unset "tcp_by_name[$alias]"
+	index=${array[(i)$alias]}
+	array=(${array[1,index-1]} ${array[index+1,-1]})
+	if [[ -z "$array" ]]; then
+	  unset "tcp_aliase[$fd]"
+	else
+	  tcp_aliases[$fd]="$array"
+	fi
+      else
+	# oops
+	if [[ -z $quiet ]]; then
+	  print "$0: \`$alias' is already a session name." >&2
+	fi
+	stat=1
+	continue
+      fi
+    fi
+    (( ! ${+tcp_aliases} )) && typeset -gA tcp_aliases
+    fd=${tcp_by_name[$base]}
+    if [[ -n ${tcp_aliases[$fd]} ]]; then
+      tcp_aliases[$fd]+=" $alias"
+    else
+      tcp_aliases[$fd]=$alias
+    fi
+    tcp_by_name[$alias]=$fd
+    if zmodload -i zsh/parameter; then
+      if (( ${+functions[tcp_on_alias]} )); then
+	tcp_on_alias $alias $fd
+      fi
+    fi
+  else
+    alias=$arg
+    fd=${tcp_by_name[$alias]}
+    if [[ -z $fd ]]; then
+      print "$0: no such alias \`$alias'" >&2
+      stat=1
+      continue
+    fi
+    # OK if this is an alias...
+    array=(${=tcp_aliases[$fd]})
+    if [[ -n ${array[(r)$alias]} ]]; then
+      # yes, we're OK
+      if [[ -n $delete ]]; then
+	unset "tcp_by_name[$alias]"
+	index=${array[(i)$alias]}
+	array=(${array[1,index-1]} ${array[index+1,-1]})
+	if [[ -z "$array" ]]; then
+	  unset "tcp_aliases[$fd]"
+	else
+	  tcp_aliases[$fd]="$array"
+	fi
+
+	if zmodload -i zsh/parameter; then
+	  if (( ${+functions[tcp_on_unalias]} )); then
+	    tcp_on_unalias $alias $fd
+	  fi
+	fi
+      else
+	print -r - \
+	  "${alias}: alias for session ${tcp_by_fd[$fd]:-unnamed fd $fd}"
+      fi
+    else
+      # oops
+      if [[ -z $quiet ]]; then
+	print "$0: \`$alias' is a session name." >&2
+      fi
+      stat=1
+	continue
+    fi
+  fi
+done
+
+return $stat
diff --git a/Functions/TCP/tcp_close b/Functions/TCP/tcp_close
new file mode 100644
index 000000000..61508f4c6
--- /dev/null
+++ b/Functions/TCP/tcp_close
@@ -0,0 +1,134 @@
+# Usage:
+#   tcp_close [-q] [ -a | session ... ]
+# -a means all sessions.
+# -n means don't close a fake session's fd.
+# -q means quiet.
+#
+# Accepts the -s and -l arguments for consistenty with other functions,
+# but there is no particular gain in using them
+emulate -L zsh
+setopt extendedglob cbases
+
+local all quiet opt alias noclose
+local -a sessnames
+
+while getopts "aql:ns:" opt; do
+    case $opt in
+	(a) all=1
+	    ;;
+	(q) quiet=1
+	    ;;
+	(l) sessnames+=(${(s.,.)OPTARG})
+	    ;;
+	(n) noclose=1
+	    ;;
+	(s) sessnames+=($OPTARG)
+	    ;;
+	(*) return 1
+	    ;;
+    esac
+done
+
+(( OPTIND > 1 )) && shift $(( OPTIND - 1))
+
+if [[ -n $all ]]; then
+    if (( $# )); then
+	print "Usage: $0 [ -q ] [ -a | [ session ... ] ]" >&2
+	return 1
+    fi
+    sessnames=(${(k)tcp_by_name})
+    if (( ! ${#sessnames} )); then
+	[[ -z $quiet ]] && print "No TCP sessions open." >&2
+	return 1
+    fi
+fi
+
+sessnames+=($*)
+
+if (( ! ${#sessnames} )); then
+    sessnames+=($TCP_SESS)
+fi
+
+if (( ! ${#sessnames} )); then
+    [[ -z $quiet ]] && print "No current TCP session." >&2
+    return 1
+fi
+
+local tcp_sess fd
+integer stat curstat
+
+# Check to see if the fd is opened for a TCP session, or was opened
+# to a pre-existing fd.  We could remember this from tcp_open.
+local -A ztcp_fds
+local line match mbegin mend
+
+if zmodload -e zsh/net/tcp; then
+    ztcp | while read line; do
+        if [[ $line = (#b)*fd\ ([0-9]##) ]]; then
+	    ztcp_fds[$match[1]]=1
+	fi
+    done
+fi
+
+for tcp_sess in $sessnames; do
+    curstat=0
+    fd=${tcp_by_name[$tcp_sess]}
+    if [[ -z $fd ]]; then
+	print "No TCP session $tcp_sess!" >&2
+	stat=1
+	continue
+    fi
+    # We need the base name if this is an alias.
+    tcp_sess=${tcp_by_fd[$fd]}
+    if [[ -z $tcp_sess ]]; then
+	if [[ -z $quiet ]]; then
+	    print "Aaargh!  Session for fd $fd has disappeared!" >&2
+	fi
+	stat=1
+	continue
+    fi
+
+    if [[ ${+tcp_aliases} -ne 0 && -n ${tcp_aliases[$fd]} ]]; then
+	for alias in ${=tcp_aliases[$fd]}; do
+	    if (( ${+functions[tcp_on_unalias]} )); then
+		tcp_on_unalias $alias $fd
+	    fi
+	    unset "tcp_by_name[$alias]"
+	done
+	unset "tcp_aliases[$fd]"
+    fi
+
+    # Don't return just because the zle handler couldn't be uninstalled...
+    if [[ -o zle ]]; then
+	zle -F $fd || print "[Ignoring...]" >&2
+    fi
+
+    if [[ -n $ztcp_fds[$fd] ]]; then
+        # It's a ztcp session.
+	if ! ztcp -c $fd; then
+	    stat=1
+	    curstat=1
+	fi
+    elif [[ -z $noclose ]]; then
+        # It's not, just close it normally.
+        # Careful: syntax for closing fd's is quite strict.
+	if [[ ${#fd} -gt 1 ]]; then
+	    [[ -z $quiet ]] && print "Can't close fd $fd; will leave open." >&2
+	else
+	    eval "exec $fd>&-"
+	fi
+    fi
+
+    unset "tcp_by_name[$tcp_sess]"
+    unset "tcp_by_fd[$fd]"
+    if [[ -z $quiet && $curstat -eq 0 ]]; then
+	print "Session $tcp_sess successfully closed."
+    fi
+    [[ $tcp_sess = $TCP_SESS ]] && unset TCP_SESS
+
+    if (( ${+functions[tcp_on_close]} )); then
+	tcp_on_close $tcp_sess $fd
+    fi
+done
+
+return $stat
diff --git a/Functions/TCP/tcp_command b/Functions/TCP/tcp_command
new file mode 100644
index 000000000..8a4f02504
--- /dev/null
+++ b/Functions/TCP/tcp_command
@@ -0,0 +1,3 @@
+tcp_send $* || return 1
+tcp_read -d -t ${TCP_TIMEOUT:=0.3}
+return 0
diff --git a/Functions/TCP/tcp_expect b/Functions/TCP/tcp_expect
new file mode 100644
index 000000000..14963a3e6
--- /dev/null
+++ b/Functions/TCP/tcp_expect
@@ -0,0 +1,115 @@
+# Expect one of a series of regular expressions from $TCP_SESS.
+# Can make backreferences to be handled by $match.  Returns 0 for
+# successful match, 1 for error, 2 for timeout.
+#
+# This function has no facility for conditionally calling code based
+# the regular expression found.  This should be done in the calling code
+# by testing $TCP_LINE, which contains the line which matched the
+# regular expression.  The complete set of lines read while waiting for
+# this line is available in the array $tcp_expect_lines (including $TCP_LINE
+# itself which will be the final element).  Alternatively, use -p pind
+# which sets $pind to the index of the pattern which matched.  It
+# will be set to 0 otherwise.
+#
+# Many of the options are passed straight down to tcp_read.
+#
+# Options:
+#   -a     Run tcp_expect across all sessions; the first pattern matched
+#          from any session is used.  The TCP output prompt can be
+#          used to decide which session matched.
+#   -l list
+#          Comma-separated list of sessions as for tcp_read.
+#   -p pv  If the Nth of a series of patterns matches, set the parameter
+#          whose name is given by $pv to N; in the case of a timeout,
+#          set it to -1; otherwise (unless the function exited prematurely),
+#	   set it to 0.
+#	   To avoid namespace clashes, the parameter's name must
+#	   not begin with `_expect'.
+#   -q     Quiet, passed down to tcp_read.  Bad option and argument
+#          usage is always reported.
+#   -s sess
+#          Expect from session sess.  May be repeated for multiple sessions.
+#   -t to  Timeout in seconds (may be floating point) per read operation.
+#          tcp_expect will only time out if every read operation takes longer
+#          than to
+#   -T TO  Overall timeout; tcp_expect will time out if the overall operation
+#          takes longer than this many seconds.
+emulate -L zsh
+setopt extendedglob
+
+# Get extra accuracy by making SECONDS floating point locally
+typeset -F SECONDS
+
+# Variables are all named _expect_* to avoid problems with the -p param.
+local _expect_opt _expect_pvar
+local -a _expect_read_args
+float _expect_to1 _expect_to_all _expect_to _expect_new_to
+integer _expect_i _expect_stat
+
+while getopts "al:p:qs:t:T:" _expect_opt; do
+  case $_expect_opt in
+    (a) _expect_read_args+=(-a)
+	;;
+    (l) _expect_read_args+=(-l $OPTARG)
+        ;;
+    (p) _expect_pvar=$OPTARG
+	if [[ $_expect_pvar != [a-zA-Z_][a-zA-Z_0-9]# ]]; then
+	  print "invalid parameter name: $_expect_pvar" >&2
+	  return 1
+	fi
+	if [[ $_expect_pvar = _expect* ]]; then
+	  print "$0: parameter names staring \`_expect' are reserved."
+	  return 1
+	fi
+	eval "$_expect_pvar=0"
+	;;
+    (q) _expect_read_args+=(-q)
+        ;;
+    (s) _expect_read_args+=(-s $OPTARG)
+        ;;
+    (t) _expect_to1=$OPTARG
+	;;
+    (T) _expect_to_all=$(( SECONDS + $OPTARG ))
+        ;;
+    (\?) return 1
+	 ;;
+    (*) print Unhandled option $_expect_opt, complain >&2
+	return 1
+	;;
+  esac
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+tcp_expect_lines=()
+while true; do
+  if (( _expect_to_all || _expect_to1 )); then
+    _expect_to=0
+    (( _expect_to1 )) && (( _expect_to = _expect_to1 ))
+    if (( _expect_to_all )); then
+      # overall timeout, see if it has already triggered
+      if (( (_expect_new_to = (_expect_to_all - SECONDS)) <= 0 )); then
+	[[ -n $_expect_pvar ]] && eval "$_expect_pvar=-1"
+	return 2
+      fi
+      if (( _expect_to <= 0 || _expect_new_to < _expect_to )); then
+	_expect_to=$_expect_new_to
+      fi
+    fi
+    tcp_read $_expect_read_args -t $_expect_to
+    _expect_stat=$?
+  else
+    tcp_read $_expect_read_args -b
+    _expect_stat=$?
+  fi
+  if (( _expect_stat )); then
+    [[ -n $_expect_pvar ]] && eval "$_expect_pvar=-1"
+    return $_expect_stat
+  fi
+  tcp_expect_lines+=($TCP_LINE)
+  for (( _expect_i = 1; _expect_i <= $#; _expect_i++ )); do
+    if [[ "$TCP_LINE" = ${~argv[_expect_i]} ]]; then
+      [[ -n $_expect_pvar ]] && eval "$_expect_pvar=\$_expect_i"
+      return 0
+    fi
+  done
+done
diff --git a/Functions/TCP/tcp_fd_handler b/Functions/TCP/tcp_fd_handler
new file mode 100644
index 000000000..012fd4d87
--- /dev/null
+++ b/Functions/TCP/tcp_fd_handler
@@ -0,0 +1,35 @@
+local line name=${tcp_by_fd[$1]}
+if [[ -n $name ]]
+then
+  local TCP_INVALIDATE_ZLE
+  if (( $# > 2 )); then
+    zle -I
+    ## debugging only
+    # print "Flags on the play:" ${argv[3,-1]}
+  else
+    TCP_INVALIDATE_ZLE=1
+  fi
+  if ! tcp_read -d -u $1; then
+    [[ -n $TCP_INVALIDATE_ZLE ]] && zle -I
+    print "[TCP fd $1 (session $name) gone awol; removing from poll list]" >& 2
+    zle -F $1
+    return 1
+  fi
+  return 0
+else
+  zle -I
+  # Handle fds not in the TCP set similarly.
+  # This does the drain thing, to try and get as much data out as possible.
+  if ! read line <&$1; then
+    print "[Reading on $1 failed; removing from poll list]" >& 2
+    zle -F $1
+    return 1
+  fi
+  line="fd$1:$line"
+  local newline
+  while read -t newline <&$1; do
+    line="${line}
+fd$1:$newline"
+  done
+fi
+print -r - $line
diff --git a/Functions/TCP/tcp_log b/Functions/TCP/tcp_log
new file mode 100644
index 000000000..e8ebaca23
--- /dev/null
+++ b/Functions/TCP/tcp_log
@@ -0,0 +1,94 @@
+# Log TCP output.
+#
+# Argument:  Output filename.
+#
+# Options:
+#   -a    Append.  Otherwise the existing file is truncated without warning.
+#	  (N.B.: even if logging was already active to it!)
+#   -s    Per-session logs.  Output to <filename>1, <filename>2, etc.
+#   -c    Close logging.
+#   -n/-N Turn off or on normal output; output only goes to the logfile, if
+#         any.  Otherwise, output also appears interactively.  This
+#         can be given with -c (or any other option), then no output
+#         goes anywhere.  However, input is still handled by the usual
+#         mechanisms --- $tcp_lines and $TCP_LINE are still set, hence
+#         tcp_expect still works.  Equivalent to (un)setting TCP_SILENT.
+#
+# With no options and no arguments, print the current configuration.
+#
+# Per-session logs are raw output, otherwise $TCP_PROMPT is prepended
+# to each line.
+
+emulate -L zsh
+setopt cbases extendedglob
+
+local opt append sess close
+integer activity
+while getopts "ascnN" opt; do
+  (( activity++ ))
+  case $opt in
+    # append to existing file
+    a) append=1
+       ;;
+    # per-session
+    s) sess=1
+       ;;
+    # close
+    c) close=1
+       ;;
+    # turn off interactive output
+    n) TCP_SILENT=1
+       ;;
+    # turn on interactive output
+    N) unset TCP_SILENT
+       ;;
+    # incorrect option
+    \?) return 1
+	;;
+    # correct option I forgot about
+    *) print "$0: option -$opt not handled, oops." >&2
+       return 1
+       ;;
+  esac
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1)) 
+
+if [[ -n $close ]]; then
+  if (( $# )); then
+    print "$0: too many arguments for -c" >&2
+    return 1
+  fi
+  unset TCP_LOG TCP_LOG_SESS
+  return 0
+fi
+
+if (( $# == 0 && ! activity )); then
+  print "\
+Per-session log: ${TCP_LOG_SESS:-<none>}
+Overall log:     ${TCP_LOG:-<none>}
+Silent?          ${${TCP_SILENT:+yes}:-no}"
+  return 0
+fi
+
+if (( $# != 1 )); then
+  print "$0: wrong number of arguments" >&2
+  return 1
+fi
+
+if [[ -n $sess ]]; then
+  TCP_LOG_SESS=$1
+  if [[ -z $append ]]; then
+    local sesslogs
+    integer i
+    sesslogs=(${TCP_LOG_SESS}*(N))
+    # yes, i know i can do this with multios
+    for (( i = 1; i <= $#sesslogs; i++ )); do
+      : >$sesslogs[$i]
+    done
+  fi
+else
+  TCP_LOG=$1
+  [[ -z $append ]] && : >$TCP_LOG
+fi
+
+return 0
diff --git a/Functions/TCP/tcp_open b/Functions/TCP/tcp_open
new file mode 100644
index 000000000..d9d5a96da
--- /dev/null
+++ b/Functions/TCP/tcp_open
@@ -0,0 +1,197 @@
+# Open a TCP session, add it to the list, handle it with zle if that's running.
+# Unless using -a, -f, -l or -s, first two arguments are host and port.
+#
+# Remaining argument, if any, is the name of the session, which mustn't
+# clash with an existing one.  If none is given, the number of the
+# connection is used (i.e. first connection is 1, etc.), or the first
+# available integer if that is already in use.
+#
+# Session names, whether provided on the command line or in the
+# .ztcp_sessions file should not be `clever'.  A clever name is one
+# with characters that won't work.  This includes whitespace and an
+# inconsistent set of punctuation characters.  If in doubt, stick
+# to alphanumeric, underscore and non-initial hyphen.
+#
+# -a fd   Accept a connection on fd and make that the session.
+#         This will block until a successful incoming connection is received.
+#
+#         fd is probably a value returned by ztcp -l; no front-end
+#         is currently provided for that but it should simply be
+#         a matter of calling `ztcp -l port' and storing $REPLY, then
+#         closing the listened port with `ztcp -c $stored_fd'.
+#
+# -f fd   `Fake' tcp connection on the given file descriptor.  This
+#         could be, for example, a file descriptor already opened to
+#         a named pipe.  It should not be a regular file, however.
+#         Note that it is not a good idea for two different sessions
+#         to be attempting to read from the same named pipe, so if
+#         both ends of the pipe are to be handled by zsh, at least
+#         one should use the `-z' option.
+#
+# -l sesslist
+# -s sessname
+#         Open by session name or comma separated list; either may
+#         be repeated as many times as necessary.  The session must be
+#	  listed in the file ${ZDOTDIR:-$HOME}/.ztcp_sessions.  Lines in
+#	  this file look exactly like a tcp_open command line except the
+#	  session name is at the start, for example
+#           sess1 pwspc 2811
+#         has the effect of
+#           tcp_open pwspc 2811 sess1
+#         Remaining arguments (other than options) to tcp_open are
+#         not allowed.  Options in .ztcp_sessions are not handled.
+#	  The file must be edited by hand.
+#
+# -z      Don't install a zle handler for reading on the file descriptor.
+#	  Otherwise, if zle is enabled, the file descriptor will
+#         be tested while at the shell prompt and any input automatically
+#         printed in the same way as job control notification.
+#
+# If a session is successfully opened, and if the function `tcp_on_open'
+# exists, it is run with the arguments session_name, session_fd.
+
+emulate -L zsh
+setopt extendedglob cbases
+
+zmodload -i zsh/net/tcp || return 1
+autoload -U zgprintf tcp_alias tcp_close tcp_command tcp_expect tcp_fd_handler
+autoload -U tcp_log tcp_output tcp_proxy tcp_read tcp_rename tcp_send
+autoload -U tcp_sess tcp_spam tcp_talk tcp_wait
+
+local opt accept fake nozle sessfile sess quiet
+local -a sessnames sessargs
+integer stat
+
+while getopts "a:f:l:qs:z" opt; do
+    case $opt in
+	(a) accept=$OPTARG
+            if [[ $accept != [[:digit:]]## ]]; then
+		print "option -a takes a file descriptor" >&2
+		return 1
+	    fi
+	    ;;
+	(f) fake=$OPTARG
+	    if [[ $fake != [[:digit:]]## ]]; then
+		print "option -f takes a file descriptor" >&2
+		return 1
+	    fi
+	    ;;
+	(l) sessnames+=(${(s.,.)OPTARG})
+            ;;
+	(q) quiet=1
+            ;;
+	(s) sessnames+=($OPTARG)
+            ;;
+	(z) nozle=1
+            ;;
+	(*) return 1
+            ;;
+    esac
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+(( ${+tcp_by_fd} ))   || typeset -gA tcp_by_fd
+(( ${+tcp_by_name} )) || typeset -gA tcp_by_name 
+typeset -A sessassoc
+
+if (( ${#sessnames} )); then
+    if [[ $# -ne 0 || -n $accept || -n $fake ]]; then
+	print "Incompatible arguments with \`-s' option." >&2
+	return 1
+    fi
+    for sess in ${sessnames}; do
+	sessassoc[$sess]=
+    done
+
+    sessfile=${ZDOTDIR:-$HOME}/.ztcp_sessions
+    if [[ ! -r $sessfile ]]; then
+	print "No session file: $sessfile" >&2
+	return 1
+    fi
+    while read -A sessargs; do
+	[[ ${sessargs[1]} = '#'* ]] && continue
+	if ((  ${+sessassoc[${sessargs[1]}]} )); then
+	    sessassoc[${sessargs[1]}]="${sessargs[2,-1]}"
+	fi
+    done < $sessfile
+    for sess in ${sessnames}; do
+	if [[ -z $sessassoc[$sess] ]]; then
+	    print "Couldn't find session $sess in $sessfile." >&2
+	    return 1
+	fi
+    done
+else
+    if [[ -z $accept && -z $fake ]]; then
+	if (( $# < 2 )); then
+	    set -- wrong number of arguments
+	else
+	    host=$1 port=$2
+	    shift $(( $# > 1 ? 2 : 1 ))
+	fi
+    fi
+    if [[ -n $1 ]]; then
+	sessnames=($1)
+	shift
+    else
+	sessnames=($(( ${#tcp_by_fd} + 1 )))
+	while [[ -n $tcp_by_name[$sessnames[1]] ]]; do
+	    (( sessnames[1]++ ))
+	done
+    fi
+    sessassoc[$sessnames[1]]="$host $port"
+fi
+
+if (( $# )); then
+  print "Usage: $0 [-z] [-a fd | -f fd | host port [ session ] ]
+  $0 [-z] [ -s session | -l sesslist ] ..." >&2
+  return 1
+fi
+
+local REPLY fd
+for sess in $sessnames; do
+    if [[ -n $tcp_by_name[$sess] ]]; then
+	print "Session \`$sess' already exists." >&2
+	return 1
+    fi
+
+    sessargs=()
+    if [[ -n $fake ]]; then
+	fd=$fake;
+    else
+	if [[ -n $accept ]]; then
+	    ztcp -a $accept || return 1
+	else
+	    sessargs=(${=sessassoc[$sess]})
+	    ztcp $sessargs || return 1
+	fi
+	fd=$REPLY
+    fi
+
+    tcp_by_fd[$fd]=$sess
+    tcp_by_name[$sess]=$fd
+
+    [[ -o zle && -z $nozle ]] && zle -F $fd tcp_fd_handler
+    if [[ -z $quiet ]]; then
+	if (( ${#sessargs} )); then
+	    print "Session $sess" \
+"(host $sessargs[1], port $sessargs[2] fd $fd) opened OK."
+	else
+	    print "Session $sess (fd $fd) opened OK."
+	fi
+    fi
+
+    # needed for new completion system, so I'm not too sanguine
+    # about requiring this here...
+    if zmodload -i zsh/parameter; then
+	if (( ${+functions[tcp_on_open]} )); then
+	    tcp_on_open $sess $fd
+	fi
+    fi
+done
+
+if [[ -z $TCP_SESS ]]; then
+    [[ -z $quiet ]] && print "Setting default TCP session $sessnames[1]"
+    TCP_SESS=$sessnames[1]
+fi
+
+return $stat
diff --git a/Functions/TCP/tcp_output b/Functions/TCP/tcp_output
new file mode 100644
index 000000000..b22b79412
--- /dev/null
+++ b/Functions/TCP/tcp_output
@@ -0,0 +1,65 @@
+emulate -L zsh
+setopt extendedglob
+
+local opt tprompt sess read_fd tpat quiet
+
+while getopts "F:P:qS:" opt; do
+  case $opt in
+    (F) read_fd=$OPTARG
+	;;
+    (P) tprompt=$OPTARG
+	;;
+    (q) quiet=1
+	;;
+    (S) sess=$OPTARG
+	;;
+    (*) [[ $opt != \? ]] && print -r "Can't handle option $opt" >&2
+	return 1
+	;;
+  esac
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+# Per-session logs don't have the session discriminator in front.
+if [[ -n $TCP_LOG_SESS ]]; then
+  print -r -- "$*" >>${TCP_LOG_SESS}.$sess
+fi
+# Always add the TCP prompt.  We used only to do this with
+# multiple sessions, but it seems always to be useful to know
+# where data is coming from; also, it allows more predictable
+# behaviour in tcp_expect.
+if [[ -n $tprompt ]]; then
+  zgprintf -R -%s=$sess -%f=$read_fd -- $tprompt
+  # We will pass this back up.
+  REPLY="$REPLY$*"
+else
+  REPLY="$*"
+fi
+if [[ -n $TCP_LOG ]]; then
+  print -r -- $REPLY >>${TCP_LOG}
+fi
+
+if [[ -z $quiet ]]; then
+  local skip=
+  if [[ ${#tcp_filter} -ne 0 ]]; then
+    # Allow tcp_filter to be an associative array, though
+    # it doesn't *need* to be.
+    for tpat in ${(v)tcp_filter}; do
+      [[ $REPLY = ${~tpat} ]] && skip=1 && break
+    done
+  fi
+  if [[ -z $skip ]]; then
+    # Check flag passed down probably from tcp_fd_handler:
+    # if we have output, we are in zle and need to fix the display first.
+    # (The shell is supposed to be smart enough that you can replace
+    # all the following with
+    #   [[ -o zle ]] && zle -I
+    # but I haven't dared try it yet.)
+    if [[ -n $TCP_INVALIDATE_ZLE ]]; then
+      zle -I
+      # Only do this the first time.
+      unset TCP_INVALIDATE_ZLE
+    fi
+    print -r -- $REPLY
+  fi
+fi
diff --git a/Functions/TCP/tcp_proxy b/Functions/TCP/tcp_proxy
new file mode 100644
index 000000000..3f19bd3de
--- /dev/null
+++ b/Functions/TCP/tcp_proxy
@@ -0,0 +1,31 @@
+# Listen on the given port and for every connection, start a new 
+# command (defaults to $SHELL) in the background on the accepted fd.
+# WARNING: this can leave your host open to the universe.  For use
+# in a restricted fashion on a secure network.
+#
+# Remote logins are much more efficient...
+
+local TCP_LISTEN_FD
+trap '[[ -n $TCP_LISTEN_FD ]] && ztcp -c $TCP_LISTEN_FD; return 1' \
+    HUP INT TERM EXIT PIPE
+
+if [[ $1 != <-> ]]; then
+    print "Usage: $0 port [cmd args... ]" >&2
+    return 1
+fi
+
+integer port=$1
+shift
+ztcp -l $port || return 1
+TCP_LISTEN_FD=$REPLY
+
+(( $# )) || set -- ${SHELL:-zsh}
+local cmd=$1
+shift
+
+while ztcp -a $TCP_LISTEN_FD; do
+    # hack to expand aliases without screwing up arguments
+    eval $cmd '$*  <&$REPLY >&$REPLY 2>&$REPLY &'
+    # Close the session fd; we don't need it here any more.
+    ztcp -c $REPLY
+done
diff --git a/Functions/TCP/tcp_read b/Functions/TCP/tcp_read
new file mode 100644
index 000000000..97da8bf21
--- /dev/null
+++ b/Functions/TCP/tcp_read
@@ -0,0 +1,207 @@
+# Helper function for reading input from a TCP connection.
+# Actually, the input doesn't need to be a TCP connection at all, it
+# is simply an input file descriptor.  However, it must be contained
+# in ${tcp_by_fd[$TCP_SESS]}.  This is set set by tcp_open, but may be
+# set by hand.  (Note, however, the blocking/timeout behaviour is usually
+# not implemented for reading from regular files.)
+#
+# The default behaviour is simply to read any single available line from
+# the input fd and print it.  If a line is read, it is stored in the
+# parameter $TCP_LINE; this always contains the last line successfully
+# read.  Any chunk of lines read in are stored in the array $tcp_lines;
+# this always contains a complete list of all lines read in by a single
+# execution of this function and hence may be empty.  The fd corresponding
+# to $TCP_LINE is stored in $TCP_LINE_FD (this can be turned into a
+# session by looking up in $tcp_by_fd).
+#
+# Printed lines are preceded by $TCP_PROMPT.  This may contain two
+# percent escapes: %s for the current session, %f for the current file
+# descriptor.  The default is `T[%s]:'.  The prompt is not printed
+# to per-session logs where the source is unambiguous.
+#
+# The function returns 0 if a read succeeded, even if (using -d) a
+# subsequent read failed.
+#
+# The behaviour is modified by the following options.
+#
+# -a     Read from all fds, not just the one given by TCP_SESS.
+#
+# -b	 The first read blocks until some data is available for reading.
+#
+# -d     Drain all pending input; loop until no data is available.
+#
+# -l sess1,sess2,...
+#        Gives a list of sessions to read on.  Equivalent to
+#        -u ${tcp_by_name[sess1]} -u ${tcp_by_name[sess2]} ...
+#	 Multiple -l options also work.
+#
+# -q     Quiet; if $TCP_SESS is not set, just return 1, but don't print
+#        an error message.
+#
+# -s sess
+#        Gives a single session; the option may be repeated.
+#
+# -t TO  On each read (the only read unless -d was also given), time out
+#        if nothing was available after TO seconds (may be floating point).
+#        Otherwise,  the function will return immediately when no data is
+#	 available.
+#
+#        If combined with -b, the function will always wait for the
+#        first data to become available; hence this is not useful unless
+#        -d is specified along with -b, in which case the timeout applies
+#        to data after the first line.
+# -u fd  Read from fd instead of the default session; may be repeated for
+#        multiple sessions.  Can be a comma-separated list, too.
+# -T TO  This sets an overall timeout, again in seconds.
+
+emulate -L zsh
+setopt extendedglob cbases
+# set -x
+
+zmodload -i zsh/mathfunc
+
+local opt drain line quiet block read_fd all sess
+local -A read_fds
+read_fds=()
+float timeout timeout_all endtime
+integer stat
+
+while getopts "abdl:qs:t:T:u:" opt; do
+  case $opt in
+    # Read all sessions.
+    (a) all=1
+	;;
+    # Block until we receive something.
+    (b) block=1
+	;;
+    # Drain all pending input.
+    (d) drain=1
+	;;
+    (l) for sess in ${(s.,.)OPTARG}; do
+	  read_fd=${tcp_by_name[$sess]}
+	  if [[ -z $read_fd ]]; then
+	    print "$0: no such session: $sess" >&2
+	    return 1
+	  fi
+	  read_fds[$read_fd]=1
+	done
+	;;
+
+    # Don't print an error mesage if there is no TCP connection,
+    # just return 1.
+    (q) quiet=1
+	;;
+    # Add a single session to the list
+    (s) read_fd=${tcp_by_name[$OPTARG]}
+        if [[ -z $read_fd ]]; then
+	    print "$0: no such session: $sess" >&2
+	    return 1
+	fi
+	read_fds[$read_fd]=1
+        ;;
+    # Per-read timeout: wait this many seconds before
+    # each read.
+    (t) timeout=$OPTARG
+        [[ -n $TCP_READ_DEBUG ]] && print "Timeout per-operations is $timeout" >&2
+	;;
+    # Overall timeout: return after this many seconds.
+    (T) timeout_all=$OPTARG
+	;;
+    # Read from given fd(s).
+    (u) for read_fd in ${(s.,.)OPTARG}; do
+	  if [[ $read_fd != (0x[[:xdigit:]]##|[[:digit:]]##) ]]; then
+	    print "Bad fd in $OPTARG" >&2
+	    return 1
+	  fi
+	  read_fds[$((read_fd))]=1
+	done
+	;;
+    (*) [[ $opt != \? ]] && print Unhandled option, complain: $opt >&2
+	return 1
+       ;;
+  esac
+done
+
+if [[ -n $all ]]; then
+  read_fds=(${(kv)tcp_by_fd})
+elif (( ! $#read_fds )); then
+  if [[ -z $TCP_SESS ]]; then
+    [[ -z $quiet ]] && print "No tcp connection open." >&2
+    return 1
+  elif [[ -z $tcp_by_name[$TCP_SESS] ]]; then
+    print "TCP session $TCP_SESS has gorn!" >&2
+    return 1
+  fi
+  read_fds[$tcp_by_name[$TCP_SESS]]=1
+fi
+
+tcp_lines=()
+local helper_stat=2 skip tpat reply REPLY
+float newtimeout
+
+# Get extra accuracy by making SECONDS floating point locally
+typeset -F SECONDS
+
+if (( timeout_all )); then
+  (( endtime = SECONDS + timeout_all ))
+fi
+
+zmodload -i zsh/zselect
+
+if [[ -n $block ]]; then
+  if (( timeout_all )); then
+    # zselect -t uses 100ths of a second
+    zselect -t $(( int(100*timeout_all + 0.5) )) ${(k)read_fds} || 
+      return $helper_stat
+  else
+    zselect ${(k)read_fds} || return $helper_stat
+  fi
+fi
+
+while (( ${#read_fds} )); do
+  if [[ -n $block ]]; then
+    # We already have data waiting this time through.
+    unset block
+  else
+    if (( timeout_all )); then
+      (( (newtimeout = endtime - SECONDS) <= 0 )) && return 2
+      if (( ! timeout || newtimeout < timeout )); then
+	(( timeout = newtimeout ))
+      fi
+    fi
+    if (( timeout )); then
+      if [[ -n $TCP_READ_DEBUG ]]; then
+	print "[tcp_read: selecting timeout $timeout on ${(k)read_fds}]" >&2
+      fi
+      zselect -t $(( int(timeout*100 + 0.5) )) ${(k)read_fds} ||
+        return $helper_stat
+    else
+      if [[ -n $TCP_READ_DEBUG ]]; then
+	print "[tcp_read: selecting no timeout on ${(k)read_fds}]" >&2
+      fi
+      zselect -t 0 ${(k)read_fds} || return $helper_stat
+    fi
+  fi
+  if [[ -n $TCP_READ_DEBUG ]]; then
+    print "[tcp_read: returned fds ${reply}]" >&2
+  fi
+  for read_fd in ${reply[2,-1]}; do
+    if ! read -r line <&$read_fd; then
+      unset "read_fds[$read_fd]"
+      stat=1
+      continue
+    fi
+
+    helper_stat=0
+    sess=${tcp_by_fd[$read_fd]}
+    tcp_output -P "${TCP_PROMPT:=<-[%s] }" -S $sess -F $read_fd \
+      ${TCP_SILENT:+-q} "$line"
+    # REPLY is now set to the line with an appropriate prompt.
+    tcp_lines+=($REPLY)
+    TCP_LINE=$REPLY TCP_LINE_FD=$read_fd
+    # Only handle one line from one device at a time unless draining.
+    [[ -z $drain ]] && return $stat
+  done
+done
+
+return $stat
diff --git a/Functions/TCP/tcp_rename b/Functions/TCP/tcp_rename
new file mode 100644
index 000000000..8d926ca0d
--- /dev/null
+++ b/Functions/TCP/tcp_rename
@@ -0,0 +1,43 @@
+# Rename session OLD (defaults to current session) to session NEW.
+# Does not handle aliases; use tcp_alias for all alias redefinitions.
+
+local old new
+
+if (( $# == 1 )); then
+  old=$TCP_SESS
+  new=$1
+elif (( $# == 2 )); then
+  old=$1
+  new=$2
+else
+  print "Usage: $0 OLD NEW" >&2
+  return 1
+fi
+
+local fd=$tcp_by_name[$old]
+if [[ -z $fd ]]; then
+  print "No such session: $old" >&2
+  return 1
+fi
+if [[ -n $tcp_by_name[$new] ]]; then
+  print "Session $new already exists." >&2
+  return 1
+fi
+# Can't rename an alias
+if [[ $tcp_by_fd[$fd] != $old ]]; then
+  print "Use tcp_alias to redefine an alias." >&2
+  return 1
+fi
+
+tcp_by_name[$new]=$fd
+unset "tcp_by_name[$old]"
+
+tcp_by_fd[$fd]=$new
+
+[[ $TCP_SESS = $old ]] && TCP_SESS=$new
+
+if zmodload -i zsh/parameter; then
+  if (( ${+functions[tcp_on_rename]} )); then
+    tcp_on_rename $new $fd $old
+  fi
+fi
diff --git a/Functions/TCP/tcp_send b/Functions/TCP/tcp_send
new file mode 100644
index 000000000..e7dfca771
--- /dev/null
+++ b/Functions/TCP/tcp_send
@@ -0,0 +1,67 @@
+emulate -L zsh
+setopt extendedglob cbases
+
+local opt quiet all sess fd nonewline
+local -a sessions write_fds
+
+while getopts "al:nqs:" opt; do
+    case $opt in
+	(a) all=1
+	    ;;
+	(n) nonewline=-n
+	    ;;
+	(q) quiet=1
+	    ;;
+	(l) for sess in ${(s.,.)OPTARG}; do
+	        if [[ -z ${tcp_by_name[$sess]} ]]; then
+		    print "$0: no such session: $sess" >&2
+		    return 1
+		fi
+		sessions+=($sess)
+	    done
+	    ;;
+	(s) if [[ -z $tcp_by_name[$OPTARG] ]]; then
+                print "No such session: $OPTARG" >&2
+		return 1
+	    fi
+	    sessions+=($OPTARG)
+	    ;;
+	(*) [[ $opt != '?' ]] && print Unhandled option, complain: $opt >&2
+            return 1
+	    ;;
+    esac
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+if [[ -n $all ]]; then
+    sessions=(${(k)tcp_by_name})
+elif (( ! ${#sessions} )); then
+    sessions=($TCP_SESS)
+fi
+if (( ! $#sessions )); then
+    if [[ -z $quiet ]]; then
+	print "No current TCP session open." >&2
+    fi
+    return 1
+fi
+
+# Writing on a TCP connection closed by the remote end can cause SIGPIPE.
+# The following test is reasonably robust, though in principle we can
+# mistake a SIGPIPE owing to another fd.  That doesn't seem like a big worry.
+# `emulate -L zsh' will already have set localtraps.
+local TCP_FD_CLOSED
+trap 'TCP_FD_CLOSED=1' PIPE
+
+local TCP_SESS
+
+for TCP_SESS in $sessions; do
+    fd=${tcp_by_name[$TCP_SESS]}
+    print $nonewline -r -- $* >&$fd
+    if [[ $? -ne 0 || -n $TCP_FD_CLOSED ]]; then
+	print "Session ${TCP_SESS}: fd $fd unusable." >&2
+	unset TCP_FD_CLOSED
+    fi
+    if [[ -n $TCP_OUTPUT ]]; then
+	tcp_output -P "$TCP_OUTPUT" -S $TCP_SESS -F $fd -q "${(j. .)*}"
+    fi
+done
diff --git a/Functions/TCP/tcp_sess b/Functions/TCP/tcp_sess
new file mode 100644
index 000000000..ee3d268b3
--- /dev/null
+++ b/Functions/TCP/tcp_sess
@@ -0,0 +1,39 @@
+# try to disguise parameters from the eval'd command in case it's a function.
+integer __myfd=1
+
+if [[ -n $1 ]]; then
+  if [[ -z $tcp_by_name[$1] ]]; then
+    print no such session: $1
+    __myfd=2
+  elif [[ -n $2 ]]; then
+    local TCP_SESS=$1
+    shift
+    # A bit tricky: make sure the first argument gets re-evaluated,
+    # so as to get aliases etc. to work, but make sure the remainder
+    # don't, so as not to bugger up quoting.  This ought to work the
+    # vast majority of the time, anyway.
+    local __cmd=$1
+    shift
+    eval $__cmd \$\*
+    return
+  else
+    TCP_SESS=$1
+    return 0;
+  fi
+fi
+
+# Print out the list of sessions, first the number, than the corresponding
+# file descriptor.  The current session, if any, is marked with an asterisk.
+local cur name fd
+for name in ${(ko)tcp_by_name}; do
+  fd=${tcp_by_name[$name]}
+  # mark current session with an asterisk
+  if [[ ${TCP_SESS} = $name ]]; then
+    cur=" *"
+  else
+    cur=
+  fi
+  print "sess:$name; fd:$fd$cur" >&$__myfd
+done
+
+return $(( __myfd - 1 ))
diff --git a/Functions/TCP/tcp_spam b/Functions/TCP/tcp_spam
new file mode 100644
index 000000000..f5c612bee
--- /dev/null
+++ b/Functions/TCP/tcp_spam
@@ -0,0 +1,97 @@
+# SPAM is a registered trademark of Hormel Foods Corporation.
+#
+# -a all connections, override $tcp_spam_list and $tcp_no_spam_list.
+#    If not given and tcp_spam_list is set to a list of sessions,
+#    only those will be spammed.  If tcp_no_spam_list is set, those
+#    will (also) be excluded from spamming.
+# -l sess1,sess2    give comma separated list of sessions to spam
+# -r reverse, spam in opposite order (default is alphabetic, -r means
+#    omegapsiic).  Note tcp_spam_list is not sorted (but may be reversed).
+# -t transmit, send data to slave rather than executing command for eac
+#    session.
+# -v verbose, list session being spammed in turn
+#
+# If the function tcp_on_spam is defined, it is called for each link
+# with the first argument set to the session name, and the remainder the
+# command line to be executed.  If it sets the parameter REPLY to `done',
+# the command line will not then be executed by tcp_spam, else it will.
+
+emulate -L zsh
+setopt extendedglob
+
+local TCP_SESS cmd opt verbose reverse sesslist transmit all
+local match mbegin mend REPLY
+local -a sessions
+
+while getopts "al:rtv" opt; do
+    case $opt in
+	(a) all=1
+	    ;;
+	(l) sessions+=(${(s.,.)OPTARG})
+	    ;;
+	(r) reverse=1
+	    ;;
+	(s) sessions+=($OPTARG)
+	    ;;
+	(t) transmit=-t
+	    ;;
+	(v) verbose=1
+	    ;;
+	(*) [[ $opt != '?' ]] && print "Option $opt not handled." >&2
+	    print "Sorry, spam's off." >&2
+	    return 1
+	    ;;
+    esac
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+local name
+if [[ -n $all ]]; then
+    sessions=(${(ko)tcp_by_name})
+elif (( ! ${#sessions} )); then
+  if (( ${#tcp_spam_list} )); then
+    sessions=($tcp_spam_list)
+  else
+    sessions=(${(ko)tcp_by_name})
+  fi
+  if (( ${#tcp_no_spam_list} )); then
+    for name in ${tcp_no_spam_list}; do
+      sessions=(${sessions:#$name})
+    done
+  fi
+fi
+
+if [[ -n $reverse ]]; then
+  local tmp
+  integer i
+  for (( i = 1; i <= ${#sessions}/2; i++ )); do
+    tmp=${sessions[i]}
+    sessions[i]=${sessions[-i]}
+    sessions[-i]=$tmp
+  done
+fi
+
+if (( ! ${#sessions} )); then
+  print "No connections to spam." >&2
+  return 1
+fi
+
+if [[ -n $transmit ]]; then
+  cmd=tcp_send
+else
+  cmd=$1
+  shift
+fi
+
+: ${TCP_PROMPT:=T[%s]:}
+
+for TCP_SESS in $sessions; do
+  REPLY=
+  if (( ${+functions[tcp_on_spam]} )); then
+    tcp_on_spam $TCP_SESS $cmd $*
+    [[ $REPLY = done ]] && continue
+  fi
+  [[ -n $verbose ]] && zgprintf -R -%s=$TCP_SESS \
+    -%f=${tcp_by_name[$TCP_SESS]} -- $TCP_PROMPT
+  eval $cmd '$*'
+done
diff --git a/Functions/TCP/tcp_talk b/Functions/TCP/tcp_talk
new file mode 100644
index 000000000..9376b9436
--- /dev/null
+++ b/Functions/TCP/tcp_talk
@@ -0,0 +1,50 @@
+# Make line editor input go straight to the current TCP session.
+# Returns when the string $TCP_TALK_ESCAPE (default :) is read on its own.
+# Otherwise, $TCP_TALK_ESCAPE followed by whitespace at the start of a line
+# is stripped off and the rest of the line passed to the shell.
+#
+# History is not currently handled, because this is difficult.
+
+: ${TCP_TALK_ESCAPE:=:}
+
+tcp-accept-line-or-exit() {
+  emulate -L zsh
+  setopt extendedglob
+  local match mbegin mend
+
+  if [[ $BUFFER = ${TCP_TALK_ESCAPE}[[:blank:]]#(#b)(*) ]]; then
+    if [[ -z $match[1] ]]; then
+      BUFFER=
+      zle -A .accept-line accept-line
+      PS1=$TCP_SAVE_PS1
+      unset TCP_SAVE_PS1
+      zle -I
+      print '\r[Normal keyboard input restored]' >&2
+    else
+      BUFFER=$match[1]
+    fi
+    zle .accept-line
+  else
+    # BUGS: is deleted from the command line and doesn't appear in
+    # the history.
+
+    # The following attempt to get the BUFFER into the history falls
+    # foul of the fact that we need to accept the current line first.
+    # But we don't actually want to accept the current line at all.
+    # print -s -r - $BUFFER
+
+    # This is my function to send data over a TCP connection; replace
+    # it with something else or nothing.
+    tcp_send $BUFFER
+    BUFFER=
+  fi
+}
+
+TCP_SAVE_PS1=${PS1##\[T*\]}
+if [[ -o prompt_subst ]]; then
+  PS1="T[\$TCP_SESS]$TCP_SAVE_PS1"
+else
+  PS1="[T]$TCP_SAVE_PS1"
+fi
+zle -N tcp-accept-line-or-exit
+zle -A tcp-accept-line-or-exit accept-line
diff --git a/Functions/TCP/tcp_wait b/Functions/TCP/tcp_wait
new file mode 100644
index 000000000..d18068a66
--- /dev/null
+++ b/Functions/TCP/tcp_wait
@@ -0,0 +1,11 @@
+# Wait for given number of seconds, reading any data from
+# all TCP connections while doing so.
+
+typeset -F SECONDS to end
+
+(( to = $1, end = SECONDS + to ))
+while (( SECONDS < end )); do
+  tcp_read -a -T $to
+  (( to = end - SECONDS ))
+done
+return
diff --git a/Functions/TCP/zgprintf b/Functions/TCP/zgprintf
new file mode 100644
index 000000000..c448b35a2
--- /dev/null
+++ b/Functions/TCP/zgprintf
@@ -0,0 +1,70 @@
+# Generalised printf.
+# Arguments of the form -%X=... give the output to be used with
+# the directive %x.
+#
+# -P indicates that any unhandled directives are to be
+# passed to printf.  With this option, any %-escapes passed to printf
+# are assumed to consume exactly one argument from the command line.
+# Unused command line arguments are ignored.  This is only minimally
+# implemented.
+#
+# -R indicates the value is to be put into REPLY rather than printed.
+#
+# -r indicates that print formatting (backslash escapes etc.) should
+# not be replied to the result.  When using -R, no print formatting
+# is applied in any case.
+
+emulate -L zsh
+setopt extendedglob
+
+local opt printf fmt usereply match mbegin mend raw c
+typeset -A chars
+chars=(% %)
+
+while getopts "%:PrR" opt; do
+  case $opt in
+    (%) if [[ $OPTARG != ?=* ]]; then
+	  print -r "Bad % option: should be -%${OPTARG[1]}=..." >&2
+	  return 1
+	fi
+	chars[${OPTARG[1]}]=${OPTARG[3,-1]}
+	;;
+    (P) printf=1
+	;;
+    (r) raw=-r
+	;;
+    (R) usereply=1
+        ;;
+  esac
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+[[ -z $usereply ]] && local REPLY
+REPLY=
+
+if (( $# )); then
+  fmt=$1
+  shift
+fi
+
+while [[ $fmt = (#b)([^%]#)%([-0-9.*]#?)(*) ]]; do
+  REPLY+=$match[1]
+  c=$match[2]
+  fmt=$match[3]
+  if [[ -n ${chars[$c]} ]]; then
+    REPLY+=${chars[$c]}
+  elif [[ -n $P ]]; then
+    # hmmm, we need sprintf...
+    # TODO: %ld etc.
+    REPLY+=`printf "%$c" $1`
+    (( $? )) && return 1
+    shift
+  else
+    print -r "Format not handled: %$c" >&2
+    return 1
+  fi
+done
+
+REPLY+=$fmt
+[[ -z $usereply ]] && print -n $raw - $REPLY
+return 0
diff --git a/Src/Modules/tcp.c b/Src/Modules/tcp.c
index 96dde66e3..58ab8c090 100644
--- a/Src/Modules/tcp.c
+++ b/Src/Modules/tcp.c
@@ -433,7 +433,9 @@ bin_ztcp(char *nam, char **args, Options ops, int func)
 
 	if (bind(sess->fd, (struct sockaddr *)&sess->sock.in, sizeof(struct sockaddr_in)))
 	{
-	    zwarnnam(nam, "could not bind to %s: %e", "0.0.0.0", errno);
+	    char buf[DIGBUFSIZE];
+	    convbase(buf, (zlong)lport, 10);
+	    zwarnnam(nam, "could not bind to port %s: %e", buf, errno);
 	    tcp_close(sess);
 	    return 1;
 	}
diff --git a/Src/Modules/tcp.mdd b/Src/Modules/tcp.mdd
index 041d9138a..88874cd7d 100644
--- a/Src/Modules/tcp.mdd
+++ b/Src/Modules/tcp.mdd
@@ -1,6 +1,7 @@
 name=zsh/net/tcp
 link=dynamic
 load=no
+functions='Functions/TCP/*'
 
 objects="tcp.o"
 autobins="ztcp"