summaryrefslogtreecommitdiff
path: root/Functions
diff options
context:
space:
mode:
Diffstat (limited to 'Functions')
-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
17 files changed, 1414 insertions, 0 deletions
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