about summary refs log tree commit diff
diff options
context:
space:
mode:
authorTanaka Akira <akr@users.sourceforge.net>1999-04-15 18:09:05 +0000
committerTanaka Akira <akr@users.sourceforge.net>1999-04-15 18:09:05 +0000
commit9003d99d16c46b5679da7fcf1f2a41adef495ff9 (patch)
tree95244be534cc37c03a628068faf7712da6317196
parentf13624e0f8a3c28c90aa0ce8ee36b639a491e4a9 (diff)
downloadzsh-9003d99d16c46b5679da7fcf1f2a41adef495ff9.tar.gz
zsh-9003d99d16c46b5679da7fcf1f2a41adef495ff9.tar.xz
zsh-9003d99d16c46b5679da7fcf1f2a41adef495ff9.zip
zsh-3.1.5-pws-3 zsh-3.1.5-pws-3
-rw-r--r--Config/version.mk4
-rw-r--r--Doc/Zsh/builtins.yo25
-rw-r--r--Doc/Zsh/expn.yo50
-rw-r--r--Doc/Zsh/guide.yo1
-rw-r--r--Doc/Zsh/mod_stat.yo10
-rw-r--r--Doc/Zsh/mod_zle.yo2
-rw-r--r--Doc/Zsh/modules.yo5
-rw-r--r--Functions/cdmatch22
-rwxr-xr-xMisc/globtests1
-rwxr-xr-xMisc/globtests.ksh1
-rwxr-xr-xMisc/lete2ctl49
-rw-r--r--Misc/zftp-functions1281
-rw-r--r--Src/Modules/example.c62
-rw-r--r--Src/Modules/stat.c77
-rw-r--r--Src/Modules/zftp.c2596
-rw-r--r--Src/Modules/zftp.mdd3
-rw-r--r--Src/Zle/comp1.c2
-rw-r--r--Src/Zle/zle_main.c2
-rw-r--r--Src/Zle/zle_params.c3
-rw-r--r--Src/Zle/zle_thingy.c4
-rw-r--r--Src/Zle/zle_tricky.c66
-rw-r--r--Src/Zle/zle_vi.c2
-rw-r--r--Src/Zle/zle_word.c2
-rw-r--r--Src/builtin.c4
-rw-r--r--Src/cond.c20
-rw-r--r--Src/exec.c52
-rw-r--r--Src/glob.c422
-rw-r--r--Src/hashtable.c3
-rw-r--r--Src/init.c5
-rw-r--r--Src/mem.c8
-rw-r--r--Src/module.c328
-rw-r--r--Src/params.c189
-rw-r--r--Src/parse.c91
-rw-r--r--Src/signals.c2
-rw-r--r--Src/subst.c117
-rw-r--r--Src/text.c21
-rw-r--r--Src/utils.c47
-rw-r--r--Src/zsh.export17
-rw-r--r--Src/zsh.h69
-rw-r--r--configure.in2
-rw-r--r--patchlist.txt83
41 files changed, 5309 insertions, 421 deletions
diff --git a/Config/version.mk b/Config/version.mk
index 5ed093b1b..5b8c88350 100644
--- a/Config/version.mk
+++ b/Config/version.mk
@@ -27,5 +27,5 @@
 # This must also serve as a shell script, so do not add spaces around the
 # `=' signs.
 
-VERSION=3.1.5
-VERSION_DATE='October 29, 1998'
+VERSION=3.1.5-pws-3
+VERSION_DATE='December 12, 1998'
diff --git a/Doc/Zsh/builtins.yo b/Doc/Zsh/builtins.yo
index 1bbc9c36a..4eae549e5 100644
--- a/Doc/Zsh/builtins.yo
+++ b/Doc/Zsh/builtins.yo
@@ -512,7 +512,7 @@ sitem([var(mm)tt(:)]var(ss))(minutes and seconds)
 endsitem()
 )
 findex(local)
-item(tt(local) [ {tt(PLUS())|tt(-)}tt(LRZilrtu) [var(n)]] [ var(name)[tt(=)var(value)] ] ...)(
+item(tt(local) [ {tt(PLUS())|tt(-)}tt(ALRUZailrtu) [var(n)]] [ var(name)[tt(=)var(value)] ] ...)(
 Same as tt(typeset), except that the options tt(-x) and
 tt(-f) are not permitted.
 )
@@ -878,7 +878,7 @@ Equivalent to tt(whence -v).
 findex(typeset)
 cindex(parameters, setting)
 cindex(parameters, declaring)
-item(tt(typeset) [ {tt(PLUS())|tt(-)}tt(LRUZfilrtuxm) [var(n)]] [ var(name)[tt(=)var(value)] ... ])(
+item(tt(typeset) [ {tt(PLUS())|tt(-)}tt(ALRUZafilrtuxm) [var(n)]] [ var(name)[tt(=)var(value)] ... ])(
 Set attributes and values for shell parameters.
 When invoked inside a function a new parameter is created which will be
 unset when the function completes.  The new parameter will not be
@@ -887,6 +887,9 @@ exported provided no parameter of that name already exists.
 The following attributes are valid:
 
 startitem()
+item(tt(-A))(
+Declare var(name) to be an em(A)ssociation parameter (also known as a hash).
+)
 item(tt(-L))(
 Left justify and remove leading blanks from var(value).
 If var(n) is nonzero, it defines the width of the field;
@@ -916,6 +919,10 @@ If var(n) is nonzero it defines the width of the field;
 otherwise it is determined by the width of the value of the
 first assignment.
 )
+item(tt(-a))(
+On its own, this option produces a list of all array parameters.
+If any non-options are provided, the tt(typeset) command is silently ignored.
+)
 item(tt(-f))(
 The names refer to functions rather than parameters.  No assignments
 can be made, and the only other valid flags are tt(-t)
@@ -1115,7 +1122,10 @@ xitem(tt(zmodload) tt(-u) [ tt(-i) ] var(name) ...)
 xitem(tt(zmodload) tt(-d) [ tt(-L) ] [ var(name) [ var(dep) ... ] ])
 xitem(tt(zmodload) tt(-du) var(name) [ var(dep) ... ])
 xitem(tt(zmodload) tt(-a) [ tt(-iL) ] [ var(name) [ var(builtin) ... ] ])
-item(tt(zmodload) tt(-au) [ tt(-i) ] var(builtin) ...)(
+xitem(tt(zmodload) tt(-au) [ tt(-i) ] var(builtin) ...)
+xitem(tt(zmodload) tt(-c) [ tt(-iI) ] [ var(name) [ var(cond) ... ] ])
+xitem(tt(zmodload) tt(-cu) [ tt(-iI) ] var(cond) ...)
+item(tt(zmodload) tt(-c) [ tt(-IL) ])(
 tt(zmodload) performs operations relating to zsh's loadable modules.
 This feature is not available on all operating systems,
 or on all installations on a particular operating system.
@@ -1182,5 +1192,14 @@ together with the tt(-u) option it removes builtins defined with
 tt(zmodload -a).  This is only possible if the builtin is not yet
 loaded.  tt(-i) suppresses the error if the builtin is already
 removed (or never existed).
+
+The tt(-c) option is used to define autoloaded condition codes. The
+var(cond) strings give the names of the conditions defined by the
+module. The optional tt(-I) option is used to define infix condition
+names. Without this option prefix condition names are defined.
+
+Together with the tt(-u) option definitions for autoloaded conditions
+are removed. If given no condition names all defined names are listed
+(as a series of tt(zmodload) commands if the tt(-L) option is given).
 )
 enditem()
diff --git a/Doc/Zsh/expn.yo b/Doc/Zsh/expn.yo
index ba8a1d239..5341ec61e 100644
--- a/Doc/Zsh/expn.yo
+++ b/Doc/Zsh/expn.yo
@@ -312,8 +312,10 @@ noderef(Parameters)
 for a description of parameters.
 In the expansions discussed below that require a pattern, the form of
 the pattern is the same as that used for filename generation;
-see noderef(Filename Generation).  In addition to the following
-operations, the file modifiers described in
+see noderef(Filename Generation).  Note that this pattern, along with
+the replacement text of a substitution, is itself subject to
+parameter, command and arithmetic substitution.  In addition to the
+following operations, the file modifiers described in
 noderef(Modifiers) in noderef(History Expansion) can be
 applied:  for example, tt(${i:s/foo/bar/}) performs string
 substitution on the value of parameter tt($i).
@@ -398,6 +400,37 @@ is used, matching is performed on each array elements separately, and
 the matched array elements are removed (use the tt((M)) flag to
 remove the non-matched elements).
 )
+xitem(tt(${)var(name)tt(/)var(pattern)tt(/)var(repl)tt(}))
+item(tt(${)var(name)tt(//)var(pattern)tt(/)var(repl)tt(}))(
+Substitute the longest possible match of var(pattern) in the value of
+variable var(name) with the string var(repl).  The first form
+substitutes just the first occurrence, the second all occurrences.
+The var(pattern) may begin with a var(#), in which case the
+var(pattern) must match at the start of the string, or var(%), in
+which case it must match at the end of the string.  The var(repl) may
+be an empty string, in which case the final tt(/) may also be omitted.
+To quote the final tt(/) in other cases it should be preceded by two
+backslashes (i.e., a quoted backslash); this is not necessary if the
+tt(/) occurs inside a substituted paramter.  Substitution of an array
+is as described for tt(#) and tt(%) above.
+
+The first tt(/) may be preceded by a tt(:), in which case the match
+will only succeed if it matches the entire word.  Note also the
+effect of the tt(I) and tt(S) parameter expansion flags below:  the
+flags tt(M), tt(R), tt(B), tt(E) and tt(N) are not useful, however.
+
+For example,
+
+nofill(tt(foo="twinkle twinkle little star" sub="t*e" rep="spy"))
+nofill(tt(print ${foo//${~sub}/$rep}))
+nofill(tt(print ${(S)foo//${~sub}/$rep}))
+
+Here, the tt(~) ensures that the text of tt($sub) is treated as a
+pattern rather than a plain string.  In the first case, the longest
+match for tt(t*e) is substituted and the result is `tt(spy star)',
+while in the second case, the shortest matches are taken and the
+result is `tt(spy spy lispy star)'.
+)
 item(tt(${#)var(spec)tt(}))(
 If var(spec) is one of the above substitutions, substitute
 the length in characters of the result instead of
@@ -549,10 +582,21 @@ for `tt(ps:\n:)'.
 item(tt(S))(
 (This and all remaining flags are used with the tt(${)...tt(#)...tt(}) or
 tt(${)...tt(%)...tt(}) forms.)
-Search substrings as well as beginnings or ends.
+Search substrings as well as beginnings or ends; with tt(#) start
+from the beginning and with tt(%) start from the end of the string.
+With substitution via tt(${)...tt(/)...tt(}) or
+tt(${)...tt(//)...tt(}), specifies that the shortest instead of the
+longest match should be replaced.
 )
 item(tt(I:)var(expr)tt(:))(
 Search the var(expr)th match (where var(expr) evaluates to a number).
+This only applies when searching for substrings, either with the tt(S)
+flag, or with tt(${)...tt(/)...tt(}) (only the var(expr)th match is
+substituted) or tt(${)...tt(//)...tt(}) (all matches from the
+var(expr)th on are substituted).  The var(expr)th match is counted
+such that there is either one or zero matches from each starting
+position in the string, although for global subsitution matches
+overlapping previous replacements are ignored.
 )
 item(tt(M))(
 Include the matched portion in the result.
diff --git a/Doc/Zsh/guide.yo b/Doc/Zsh/guide.yo
index d38a0d308..f54e6bfdb 100644
--- a/Doc/Zsh/guide.yo
+++ b/Doc/Zsh/guide.yo
@@ -114,6 +114,7 @@ menu(The example Module)
 menu(The files Module)
 menu(The sched Module)
 menu(The stat Module)
+menu(The zftp Module)
 menu(The zle Module)
 endmenu()
 texinode(The Z Shell Guide)(Introduction)(Top)(Top)
diff --git a/Doc/Zsh/mod_stat.yo b/Doc/Zsh/mod_stat.yo
index 22fbe2a73..a8551d8e9 100644
--- a/Doc/Zsh/mod_stat.yo
+++ b/Doc/Zsh/mod_stat.yo
@@ -1,4 +1,4 @@
-texinode(The stat Module)(The zle Module)(The sched Module)(Zsh Modules)
+texinode(The stat Module)(The zftp Module)(The sched Module)(Zsh Modules)
 sect(The stat Module)
 The tt(stat) module makes available one builtin command:
 
@@ -6,7 +6,8 @@ startitem()
 findex(stat)
 cindex(files, listing)
 cindex(files, examining)
-item(tt(stat) [ tt(-gnNlLtTrs) ] [ tt(-f) var(fd) ] [ tt(-A) var(array) ] \
+item(tt(stat) [ tt(-gnNlLtTrs) ] [ tt(-f) var(fd) ] \
+    [ tt(-H) var(hash) ] [ tt(-A) var(array) ] \
     [ tt(-F) var(fmt) ] [ tt(PLUS())var(element) ] [ var(file) ... ])(
 The command acts as a front end to the tt(stat) system call (see
 manref(stat)(2)).
@@ -91,6 +92,11 @@ appropriate array element and in the latter case the file name
 appears as a separate array element preceding all the others.
 Other formatting options are respected.
 )
+item(tt(-H) var(hash))(
+Similar to tt(-A), but instead assign the values to var(hash).  The keys
+are the elements listed above.  If the tt(-n) option is provided then the
+name of the file is included in the hash with key tt(name).
+)
 item(tt(-f) var(fd))(
 Use the file on file descriptor var(fd) instead of
 named files; no list of file names is allowed in this case.
diff --git a/Doc/Zsh/mod_zle.yo b/Doc/Zsh/mod_zle.yo
index b7c6a934f..48dd1935f 100644
--- a/Doc/Zsh/mod_zle.yo
+++ b/Doc/Zsh/mod_zle.yo
@@ -1,4 +1,4 @@
-texinode(The zle Module)()(The stat Module)(Zsh Modules)
+texinode(The zle Module)()(The zftp Module)(Zsh Modules)
 sect(The zle Module)
 The tt(zle) module contains the Zsh Line Editor.  See
 ifzman(zmanref(zshzle))\
diff --git a/Doc/Zsh/modules.yo b/Doc/Zsh/modules.yo
index b44b00e6b..300553845 100644
--- a/Doc/Zsh/modules.yo
+++ b/Doc/Zsh/modules.yo
@@ -36,6 +36,9 @@ A builtin that provides a timed execution facility within the shell.
 item(tt(stat))(
 A builtin command interface to the tt(stat) system call.
 )
+item(tt(zftp))(
+A builtin FTP client.
+)
 item(tt(zle))(
 The Zsh Line Editor, including the tt(bindkey) and tt(vared) builtins.
 )
@@ -50,6 +53,7 @@ menu(The example Module)
 menu(The files Module)
 menu(The sched Module)
 menu(The stat Module)
+menu(The zftp Module)
 menu(The zle Module)
 endmenu()
 includefile(Zsh/mod_cap.yo)
@@ -61,4 +65,5 @@ includefile(Zsh/mod_example.yo)
 includefile(Zsh/mod_files.yo)
 includefile(Zsh/mod_sched.yo)
 includefile(Zsh/mod_stat.yo)
+includefile(Zsh/mod_zftp.yo)
 includefile(Zsh/mod_zle.yo)
diff --git a/Functions/cdmatch2 b/Functions/cdmatch2
index c70357a34..8a3e9591f 100644
--- a/Functions/cdmatch2
+++ b/Functions/cdmatch2
@@ -2,7 +2,7 @@
 # second argument of cd and pushd.
 
 emulate -R zsh				# Requires zsh 3.0-pre4 or later
-setopt localoptions
+setopt localoptions extendedglob
 local from
 
 read -Ac from
diff --git a/Misc/globtests b/Misc/globtests
index b7d5fd0bd..9a12a11e1 100755
--- a/Misc/globtests
+++ b/Misc/globtests
@@ -112,5 +112,6 @@ t fooxx         ((#i)FOOX)x
 f fooxx         ((#i)FOOX)X
 f BAR           (bar|(#i)foo)
 t FOO           (bar|(#i)foo)
+t Modules       (#i)*m*
 EOT
 print "$failed tests failed."
diff --git a/Misc/globtests.ksh b/Misc/globtests.ksh
index da5145845..839637af4 100755
--- a/Misc/globtests.ksh
+++ b/Misc/globtests.ksh
@@ -96,5 +96,6 @@ t fooxx         @((#i)FOOX)x
 f fooxx         @((#i)FOOX)X
 f BAR           @(bar|(#i)foo)
 t FOO           @(bar|(#i)foo)
+t Modules       (#i)*m*
 EOT
 print "$failed tests failed."
diff --git a/Misc/lete2ctl b/Misc/lete2ctl
index 273ea4674..ca00b8aee 100755
--- a/Misc/lete2ctl
+++ b/Misc/lete2ctl
@@ -9,7 +9,6 @@
 # Runs as a filter.  Should ignore anything which isn't a "complete".
 # It expects each "complete" statement to be the first thing on a line.
 # All the examples in the tcsh manual give sensible results.
-# Author:  Peter Stephenson <pws@ibmth.df.unipi.it>
 #
 # Option:
 # -x (exact): only applies in the case of command disambiguation (is
@@ -39,11 +38,6 @@
 # (5) Make sure all command names with wildcards are processed together --
 #     they need to be lumped into one "compctl -C" or "compctl -D"
 #     statement for zsh.
-# (6) Group completion (complete's g flag) is not built into zsh, so
-#     you need perl to be available to generate the groups.  If this
-#     script is useful, I assume that's not a problem.
-# (7) I don't know what `completing completions' means, so the X
-#     flag to complete is not handled.
 
 # Handle options
 if (@ARGV) {
@@ -119,13 +113,6 @@ sub gettype {
 # Nothing (n) can be handled by returning nothing.  (C.f. King Lear, I.i.)
     if ($c =~ /[abcjuv]/) {
 	$ret = "-$c";
-    } elsif ($c eq 'C') {
-	if (defined($glob)) {
-	    $ret = "-W $glob -/g '*(.*)'";
-	    undef($glob);
-	} else {
-	    $ret = '-c';
-	}
     } elsif ($c eq 'S') {
 	$ret = '-k signals';
     } elsif ($c eq 'd') {
@@ -134,42 +121,18 @@ sub gettype {
 	} else {
 	    $ret = '-/';
 	}
-    } elsif ($c eq 'D') {
-	if (defined($glob)) {
-	    $ret = "-W $glob -/";
-	    undef($glob);
-	} else {
-	    $ret = '-/';
-	}
     } elsif ($c eq 'e') {
 	$ret = '-E';
     } elsif ($c eq 'f' && !$glob) {
 	$ret = '-f';
-    } elsif ($c eq 'F') {
-	if (defined($glob)) {
-	    $ret = "-W $glob -f";
-	    undef($glob);
-	} else {
-	    $ret = '-f';
-	}
-    } elsif ($c eq 'g') {
-	$ret = "-s '\$(perl -e '\\''while ((\$name) = getgrent)\n" .
-	    "{ print \$name, \"\\n\"; }'\\'')'";
     } elsif ($c eq 'l') {
 	$ret = q!-k "(`limit | awk '{print $1}'`)"!;
     } elsif ($c eq 'p') {
-        $ret = "-W $glob -f", undef($glob) if defined($glob);
+	$ret = "-W $glob -f", undef($glob) if defined($glob);
     } elsif ($c eq 's') {
-        $ret = '-p';
+	$ret = '-p';
     } elsif ($c eq 't') {
 	$qual = '.';
-    } elsif ($c eq 'T') {
-        if (defined($glob)) {
-            $ret = "-W $glob -g '*(.)'";
-            undef($glob);
-        } else {
-            $ret = "-g '*(.)'";
-        }
     } elsif ($c eq 'x') {
 	$glob =~ s/'/'\\''/g;
 	$ret = "-X '$glob'";
@@ -227,7 +190,7 @@ $" = " - ";
 
 while (<>) {
     if (/^\s*complete\s/) {
-	undef(@stuff);
+	undef(@stuff); 
 	$default = '';
 	$_ = $';
 	while (/\\$/) {
@@ -248,7 +211,7 @@ while (<>) {
 	    # Loop over remaining arguments to "complete".
 	    $sep = substr($word,1,1);
 	    $sep =~ s/(\W)/\\$1/g;
-	    @split = split(/$sep/,$word,4);
+	    @split = split(/$sep/,$word);
 	    for ($i = 0; $i < 3; $i++) {
 		while ($split[$i] =~ /\\$/) {
 		    substr($split[$i],-1,1) = "";
@@ -262,9 +225,7 @@ while (<>) {
 		 # The "complete" catch-all:  treat this as compctl\'s
 		 # default (requiring no pattern matching).
 		$default .= &gettype($type) . ' ';
-		defined($suffix) &&
-		    (defined($defsuf) ? ($defsuf .= $suffix)
-		     : ($defsuf = $suffix));
+		defined($suffix) && ($defsuf .= $suffix);
 	    } else {
 		$pat = &getpat($pat,$arg);
 		$type = &gettype($type);
diff --git a/Misc/zftp-functions b/Misc/zftp-functions
new file mode 100644
index 000000000..a07e46d72
--- /dev/null
+++ b/Misc/zftp-functions
@@ -0,0 +1,1281 @@
+# zftp is a loadable module implementing an FTP client as a builtin
+# command so that you can use the shell command language and line
+# editing to make life easier.  If your system has dynamically
+# load libraries and zsh was compiled to use them, it is probably
+# somewhere where it can be loaded at run time.  Otherwise, it depends
+# whether the shell was compiled with zftp already built into it.
+#
+# Here is a suite of functions, plus assorted other code, to make
+# zftp work smoothly.
+#
+# Completion is implemented in a fairly natural way, except that
+# very little support has been provided for non-UNIX remote hosts.
+# On such machines, the safest thing to do is only try to complete
+# files in the current directory; this should be OK.
+#
+# Remote globbing for commands which retrieve files is also
+# implemented.  This can be done in two different ways.  The default
+# is for zsh to do the globbing locally.  The advantage is that full
+# zsh pattern matching (respecting the setting of extendedglob) is
+# possible, and no assumption (apart from the restrictions on
+# directory handling noted above) is made about the behaviour of the
+# server.  The disadvantage is that the entire filename list for the
+# current directory must be retrieved, and then zsh must laboriously
+# do pattern matching against every file, so it is potentially slow
+# for large directories.  Only the non-directory part of file names is
+# globbed.
+#
+# The alternative will be used if $zfrglob has non-zero length.
+# Zsh then sends the pattern to the server for globbing.  Best of
+# luck.
+#
+# To support remote globbing, some functions have been aliased
+# with 'noglob' in front.  Currently, this has a dire effect on
+# completion unless the completeinaliases option is set, so
+# it is set below.  This can conceivably cause you problems
+# if you expect completion for aliases automatically to give you
+# completion for the base command.  I suspect that most people
+# don't even know that happens.
+#
+# The following functions are provided.
+#
+# General status changing and displaying functions:
+# zfparams
+#   Simple front end to `zftp params', except it will automatically
+#   query host, user and password.  These are then stored to be
+#   used with a `zfopen' with no arguments.
+# zfopen [ host [ user ... ] ]
+#   Open a connection and login.  Unless the option -1 (once)
+#   is given, will store the parameters for the open (including
+#   a password which is prompted for and not echoed) so that
+#   if you call zfopen subsequently without arguments it will
+#   reopen the same connection.
+# zfanon anonftphost
+#   Open a connection for anonymous FTP.  Tries to guess an
+#   email address to use as the password, unless $EMAIL_ADDR is
+#   already set.  The first time, will tell you what it has guessed.
+#   It's rude to set EMAIL_ADDR=mozilla.
+# zfcd [ dir | old new ]
+#   Change directory on the server.  This tries to mimic the behaviour
+#   of the shell's cd.  In particular,
+#    zfcd           change to '~' on server, if it interprets it
+#    zfcd -         change to previous directory of current connection
+#    zfcd OLD NEW   change directory from fooOLDbar to fooNEWbar
+#   One piece of magic is builtin:  an initial part of the directory
+#   matching $HOME is translated back to `~'.  Most UNIX servers
+#   recognise the usual shell convention.  So things like `zfcd $PWD'
+#   is useful provide you are under your home directory and the
+#   structure on the remote machine mirrors that on the local.
+# zfhere
+#   Synonym for `zfcd $PWD', see above.
+# zfdir [args]
+#   Show a long diretory list of the remote connection.  Any
+#   arguments are passed on to the server, apart from options.
+#   Currently this always uses a pager to show the directory
+#   list.  Caching is implemented:  zfdir on its own always shows
+#   the current diretory, which is cached; zfdir with some other
+#   directory arguments shows that, which is cached separately
+#   and can be reviewed with `zfdir -r'.  Other options:
+#    -f  force reget, overriding the cache, in case something's changed
+#    -d  delete the cache, but don't show anything.
+#   To pass options to the server, use e.g. `zfdir -- -C'.
+#   This also has the zfcd ~ hack.
+# zfls [args]
+#   Short list of the long directory, depending on what [args]
+#   do to the server.  No options, no caching, no pager.
+# zftype [ a[scii] | i[mage] | b[inary] ]
+#   Set or display the transfer type; currently only ASCII
+#   and image (same as binary) types are supported.
+# zfclose
+#   Close the connection.
+# zfstat
+#   Print the zftp status from local variables; doesn't do any network
+#   operations unless -v is supplied, in which case the server is
+#   asked for its views on the status, too.
+#
+# Functions for retrieving data:
+#   All accept the following options:
+#    -G   Don't do remote globbing (see above); the default is to do it.
+#    -t   Try to set local files to the same time as the remote ones.
+#         Unfortunately we only know the remote time in GMT, so it's
+#         a little tricky and you need perl 5 (installed as `perl')
+#         for this to work.  Suggestions welcome.
+# zfget file1 file2 ...
+#   Retrieve each file from the server.  The remote file is the
+#   full name given, the local file is the non-directory part of that
+#   (assuming UNIX file paths).
+# zfuget file1 file2 ..
+#   Get with update.  Check remote and local sizes and times and
+#   retrieve files which are newer on the server.  Will query
+#   hard cases, which are where the remote file is newer but a
+#   different size, or is older but the same size.  With option -s
+#   (silent) assumes it's best to retrieve the files in both those
+#   cases.  With -v (may be combined with -s), print the information
+#   about the files being considered.
+# zfcget file1 ...
+#   Assuming file1 was incompletely retrieved, try to get the rest of
+#   it.  This relies on a normal UNIX server behaviour which is not
+#   as specified in the FTP standard and hence is not universal.
+# zfgcp file1 file2
+# zfgcp file1 file2 ... dir
+#   Get with the behaviour of cp, i.e. copy remote file1 to local
+#   file2, or get remote fileN into local diretory dir.
+#
+# Function for sending data:
+# zfput file1 file2 ...
+#   Put the local files onto the server under the same name.  The
+#   local files are exactly as given; the remote files are the
+#   non-diretory parts of that. 
+# zfuput file1 file2 ..
+#   Put the local files onto the server, with update.  Works
+#   similarly to zfuget.
+#
+# Utility functions:
+# zftp_chpwd
+#   Show the new directory when it changes; try to put it into
+#   an xterm on shelltool header.  Works best alongside chpwd.
+# zftp_progress
+#   Show the percentage of a file retrieved as it is coming; if the
+#   size is not available show the size transferred so far.  The
+#   percentage may be wrong if sending data from a local pipe.
+#   If you transfer files in the background, you should undefine
+#   this before the transfer.  It is smart enough not to print
+#   anything when stderr is not a terminal.
+# zfcd_match
+#   Function for remote directory completion.
+# zfget_match
+#   Function for remote filename completion.
+# zfrglob varname
+#   This is used for the remote globbing.  The pattern resides
+#   in $varname (note extra level of indirection), and on return
+#   $varname will contain the list of matching files.
+# zfrtime locfile remfile [ time ]
+#   This sad thing does the setting of local file times to those
+#   of the remote, see horror story above.
+
+zmodload -ia zftp
+
+alias zfcd='noglob zfcd'
+alias zfget='noglob zfget'
+alias zfls='noglob zfls'
+alias zfdir='noglob zfdir'
+alias zfuget='noglob zfuget'
+# only way of getting that noglob out of the way at the moment
+setopt completealiases
+
+#
+# zftp completions
+#
+compctl -f -x 'p[1]' \
+  -k '(open params user login type ascii binary mode put putat
+  get getat append appendat ls dir local remote mkdir rmdir delete
+  close quit)'  - \
+  'w[1,cd][1,ls][1,dir][1,rmdir]' -K zfcd_match -S/ -q - \
+  'W[1,get*]' -K zfget_match - 'w[1,delete][1,remote]' -K zfget_match - \
+  'w[1,open][1,params]' -k hosts -- zftp
+compctl -K zfcd_match -S/ -q zfcd zfdir zfls
+compctl -K zfget_match zfget zfgcp zfuget zfcget
+compctl -k hosts zfopen zfparams
+
+function zfanon {
+  local opt optlist once
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        1) once=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  if [[ -z $EMAIL_ADDR ]]; then
+    # Exercise in futility.  There's a poem by Wallace Stevens
+    # called something like `N ways of looking at a blackbird',
+    # where N is somewhere around 0x14 to 0x18.  Now zftp is
+    # ashamed to prsent `N ways of looking at a hostname'.
+    local domain host
+    # First, maybe we've already got it.  Zen-like.
+    if [[ $HOST = *.* ]]; then
+      # assume this is the full host name
+      host=$HOST
+    elif [[ -f /etc/resolv.conf ]]; then
+      # Next, maybe we've got resolv.conf.
+      domain=$(awk '/domain/ { print $2 }' /etc/resolv.conf)
+      [[ -n $domain ]] && host=$HOST.$domain
+    fi
+    # Next, maybe we've got nlsookup.  May not work on LINUX.
+    [[ -z $host ]] && host=$(nslookup $HOST | awk '/Name:/ { print $2 }')
+    if [[ -z $host ]]; then
+      # we're running out of ideas, but this should work.
+      # after all, i wrote it...
+      # don't want user to know about this, too embarrassed.
+      local oldvb=$ZFTP_VERBOSE oldtm=$ZFTP_TMOUT
+      ZFTP_VERBOSE=
+      ZFTP_TMOUT=5
+      if zftp open $host >& /dev/null; then
+        host=$ZFTP_HOST
+        zftp close $host
+      fi
+      ZFTP_VERBOSE=$oldvb
+      ZFTP_TMOUT=$oldtm
+    fi
+    if [[ -z $host ]]; then
+      print "Can't get your hostname.  Define \$EMAIL_ADDR by hand."
+      return 1;
+    fi
+    EMAIL_ADDR="$USER@$host"
+    print "Using $EMAIL_ADDR as anonymous FTP password."
+  fi
+  
+  if [[ $once = 1 ]]; then
+    zftp open $1 anonymous $EMAIL_ADDR
+  else
+    zftp params $1 anonymous $EMAIL_ADDR
+    zftp open
+  fi
+}
+
+function zfcd {
+  # zfcd:  change directory on the remote server.
+  #
+  #  Currently has the following features:
+  # --- an initial string matching $HOME in the directory is turned back into ~
+  #     to be re-interpreted by the remote server.
+  # --- zfcd with no arguments changes directory to '~'
+  # --- `zfcd old new' and `zfcd -' work analagously to cd
+  # --- if the connection is not currently open, it will try to
+  #     re-open it with the stored parameters as set by zfopen.
+  #     If the connection timed out, however, it won't know until
+  #     too late.  In that case, just try the same zfcd command again
+  #     (but now `zfcd -' and `zfcd old new' won't work).
+  
+  # hack: if directory begins with $HOME, turn it back into ~
+  # there are two reasons for this:
+  #   first, a ~ on the command line gets expanded even with noglob.
+  #     (I suppose this is correct, but I wouldn't like to swear to it.)
+  #   second, we can no do 'zfcd $PWD' and the like, and that will
+  #     work just as long as the directory structures under the home match.
+  
+  # Autoopen:  if not already open, hope there are parameters set up to
+  # do so.  If not, we get the right error message, so no harm done.
+  [[ -z $ZFTP_HOST ]] && { zfopen  || return 1; }
+  
+  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
+  
+  if (( $# == 0 )); then
+    # Emulate `cd' behaviour
+    set -- '~'
+  elif [[ $# -eq 1 && $1 = - ]]; then
+    # Emulate `cd -' behaviour.
+    set -- $zflastdir
+  elif [[ $# -eq 2 ]]; then
+    # Emulate `cd old new' behaviour.
+    # We have to find a character not in $1 or $2; ! is a good bet.
+    eval set -- "\${ZFTP_PWD:s!$1!$2!}"
+  fi
+  
+  # We have to remember the current directory before changing it
+  # if we want to keep it.
+  local lastdir=$ZFTP_PWD
+  
+  zftp cd "$@"  &&  zflastdir=$lastdir
+}
+
+function zfcd_match {
+  # see zfcd for details of this hack
+  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
+  
+  # error messages only
+  local ZFTP_VERBOSE=45
+  # should we redirect 2>/dev/null or let the user see it?
+  
+  if [[ $ZFTP_SYSTEM = UNIX* ]]; then
+    # hoo, aren't we lucky: this makes things so much easier
+    setopt localoptions rcexpandparam
+    local dir
+    if [[ $1 = ?*/* ]]; then
+      dir=${1%/*}
+    elif [[ $1 = /* ]]; then
+      dir=/
+    fi
+    # If we're using -F, we get away with using a directory
+    # to list, but not a glob.  Don't ask me why.
+    # I hate having to rely on awk here.
+    reply=($(zftp ls -F $dir | 
+    awk '/\/$/ { print substr($1, 0, length($1)-1) }'))
+    if [[ $dir = / ]]; then
+      reply=(${dir}$reply)
+    elif [[ -n $dir ]]; then
+      reply=($dir/$reply)
+    fi
+  else
+    # I simply don't know what to do here.
+    # Just use the list of files for the current directory.
+    zfget_match $*
+  fi
+  
+}
+
+function zfcget {
+  # Continuation get of files from remote server.
+  # For each file, if it's shorter here, try to get the remainder from
+  # over there.  This requires the server to support the REST command
+  # in the way many do but RFC959 doesn't specify.
+  # Options:
+  #   -G   don't to remote globbing, else do
+  #   -t   update the local file times to the same time as the remote.
+  #        Currently this only works if you have the `perl' command,
+  #        and that perl is version 5 with the standard library.
+  #        See the function zfrtime for more gory details.
+  
+  setopt localoptions
+  unsetopt ksharrays shwordsplit
+  
+  local loc rem stat=0 optlist opt nglob remlist locst remst
+  local tmpfile=${TMPPREFIX}zfcget$$ rstat tsize time
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        G) nglob=1
+  	 ;;
+        t) time=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  for remlist in $*; do
+    # zfcd directory hack to put the front back to ~
+    if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then
+      remlist="~${remlist#$HOME}"
+    fi
+    if [[ $nglob != 1 ]]; then
+      zfrglob remlist
+    fi
+    if (( $#remlist )); then
+      for rem in $remlist; do
+        loc=${rem:t}
+        if [[ ! -f $loc ]]; then
+  	# File does not yet exist
+  	zftp get $rem >$loc || stat=$?
+        else
+  	# Compare the sizes.
+  	locst=($(zftp local $loc))
+  	zftp remote $rem >$tmpfile
+  	rstat=$?
+  	remst=($(<$tmpfile))
+  	rm -f $tmpfile
+  	if [[ $rstat = 2 ]]; then
+  	  print "Server does not support SIZE command.\n" \
+  	  "Assuming you know what you're doing..." 2>&1
+  	  zftp getat $rem $locst[1] >>$loc || stat=$?
+  	  continue
+  	elif [[ $rstat = 1 ]]; then
+  	  print "Remote file not found: $rem" 2>&1
+  	  continue
+  	fi
+  	if [[ $locst[1] -gt $remst[1] ]]; then
+  	  print "Local file is larger!" 2>&1
+  	  continue;
+  	elif [[ $locst[1] == $remst[1] ]]; then
+  	  print "Files are already the same size." 2>&1
+  	  continue
+  	else
+  	  if zftp getat $rem $locst[1] >>$loc; then
+  	    [[ $time = 1 ]] && zfrtime $loc $rem $remst[2]
+  	  else
+  	    stat=1
+  	  fi
+  	fi
+        fi
+      done
+    fi
+  done
+  
+  return $stat
+}
+
+function zfclose {
+  zftp close
+}
+
+function zfdir {
+  # Long directory of remote server.
+  # The remote directory is cached.  In fact, two caches are kept:
+  # one of the standard listing of the current directory, i.e. zfdir
+  # with no arguments, and another for everything else.
+  # To access the appropriate cache, just use zfdir with the same
+  # arguments as previously.  zfdir -r will also re-use the `everything
+  # else' cache; you can always reuse the current directory cache just
+  # with zfdir on its own.
+  #
+  # The current directory cache is emptied when the directory changes;
+  # the other is kept until a new zfdir with a non-empty argument list.
+  # Both are removed when the connection is closed.
+  #
+  # zfdir -f will force the existing cache to be ignored, e.g. if you know
+  #          or suspect the directory has changed.
+  # zfdir -d will remove both caches without listing anything.
+  # If you need to pass -r, -f or -d to the dir itself, use zfdir -- -d etc.
+  
+  setopt localoptions unset
+  unsetopt shwordsplit ksharrays
+  
+  local file opt optlist redir i newargs force
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        r) redir=1
+  	 ;;
+        f) force=1
+  	 ;;
+        d) [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir
+  	 [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir
+  	 zftp_fcache=()
+  	 return 0
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  # directory hack, see zfcd
+  for (( i = 1; i <= $#argv; i++ )); do
+    if [[ $argv[$i] = $HOME || $argv[$i] = $HOME/* ]]; then
+      argv[$i]="~${argv[$i]#$HOME}"
+    fi
+  done
+  
+  if [[ $# -eq 0 ]]; then
+    # Cache it in the current directory file.  This means that repeated
+    # calls to zfdir with no arguments always use a cached file.
+    [[ -z $zfcurdir ]] && zfcurdir=${TMPPREFIX}zfcurdir$$
+    file=$zfcurdir
+  else
+    # Last directly looked at was not the current one, or at least
+    # had non-standard arguments.
+    [[ -z $zfotherdir ]] && zfotherdir=${TMPPREFIX}zfotherdir$$
+    file=$zfotherdir
+    newargs="$*"
+    if [[ -f $file && $redir != 1 && $force -ne 1 ]]; then
+      # Don't use the cached file if the arguments changed.
+      [[ $newargs = $zfotherargs ]] || rm -f $file
+    fi
+    zfotherargs=$newargs
+  fi
+  
+  if [[ $force -eq 1 ]]; then
+    rm -f $file
+    # if it looks like current directory has changed, better invalidate
+    # the filename cache, too.
+    (( $# == 0 )) && zftp_fcache=()
+  fi
+  
+  if [[ -n $file && -f $file ]]; then
+    eval ${PAGER:-more} \$file
+  else
+    zftp dir $* | tee $file | eval ${PAGER-:more}
+  fi
+}
+
+function zfgcp {
+  # ZFTP get as copy:  i.e. first arguments are remote, last is local.
+  # Supposed to work exactly like a normal copy otherwise, i.e.
+  #  zfcp rfile lfile
+  # or
+  #  zfcp rfile1 rfile2 rfile3 ... ldir
+  # Options:
+  #   -G   don't to remote globbing, else do
+  #   -t   update the local file times to the same time as the remote.
+  #        Currently this only works if you have the `perl' command,
+  #        and that perl is version 5 with the standard library.
+  #        See the function zfrtime for more gory details.
+  
+  setopt localoptions
+  unsetopt shwordsplit
+  
+  local opt optlist nglob remlist rem loc stat=0 time
+  
+  while [[ $1 == -* ]]; do
+    if [[ $1 == - || $1 == -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $opt in
+        G) nglob=1
+  	 ;;
+        t) time=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  # hmm, we should really check this after expanding the glob,
+  # but we shouldn't expand the last argument remotely anyway.
+  if [[ $# -gt 2 && ! -d $argv[-1] ]]; then
+    print "zfgcp:  last argument must be a directory." 2>&1
+    return 1
+  elif [[ $# == 1 ]]; then
+    print "zfgcp:  not enough arguments." 2>&1
+    return 1
+  fi
+  
+  if [[ -d $argv[-1] ]]; then
+    local dir=$argv[-1]
+    argv[-1]=
+    for remlist in $*; do
+      # zfcd directory hack to put the front back to ~
+      if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then
+        remlist="~${remlist#$HOME}"
+      fi
+      if [[ $nglob != 1 ]]; then
+        zfrglob remlist
+      fi
+      if (( $#remlist )); then
+        for rem in $remlist; do
+  	loc=$dir/${rem:t}
+  	if zftp get $rem >$loc; then
+  	  [[ $time = 1 ]] && zfrtime $rem $loc
+  	else
+  	  stat=1
+  	fi
+        done
+      fi
+    done
+  else
+    zftp get $1 >$2 || stat=$?
+  fi
+  return $stat
+}
+
+function zfget {
+  # Get files from remote server.  Options:
+  #   -G   don't to remote globbing, else do
+  #   -t   update the local file times to the same time as the remote.
+  #        Currently this only works if you have the `perl' command,
+  #        and that perl is version 5 with the standard library.
+  #        See the function zfrtime for more gory details.
+  
+  local loc rem stat=0 optlist opt nglob remlist time
+  
+  while [[ $1 == -* ]]; do
+    if [[ $1 == - || $1 == -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $opt in
+        G) nglob=1
+  	 ;;
+        t) time=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  for remlist in $*; do
+    # zfcd directory hack to put the front back to ~
+    if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then
+      remlist="~${remlist#$HOME}"
+    fi
+    if [[ $nglob != 1 ]]; then
+      zfrglob remlist
+    fi
+    if (( $#remlist )); then
+      for rem in $remlist; do
+        loc=${rem:t}
+        if zftp get $rem >$loc; then
+  	[[ $time = 1 ]] && zfrtime $rem $loc
+        else
+  	stat=1
+        fi
+      done
+    fi
+  done
+  
+  return $stat
+}
+
+function zfget_match {
+  # the zfcd hack:  this may not be necessary here
+  if [[ $1 == $HOME || $1 == $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
+  
+  
+  if [[ $ZFTP_SYSTEM == UNIX* && $1 == */* ]]; then
+    # On the first argument to ls, we usually get away with a glob.
+    reply=($(zftp ls "$1*$2"))
+  else
+    if (( $#zftp_fcache == 0 )); then
+      # Always cache the current directory and use it
+      # even if the system is UNIX.
+      zftp_fcache=($(zftp ls))
+    fi
+    reply=($zftp_fcache);
+  fi
+}
+
+function zfhere {
+  # Change to the directory corresponding to $PWD on the server.
+  # See zfcd for how this works.
+  zfcd $PWD
+}
+
+function zfls {
+  # directory hack, see zfcd
+  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
+  zftp ls $*
+}
+
+function zfopen {
+  # Use zftp params to set parameters for open, rather than sending
+  # them straight to open.  That way they are stored for a future open
+  # command.
+  #
+  # With option -1 (just this 1ce), don't do that.
+  
+  local optlist opt once
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        1) once=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  # This is where we should try and do same name-lookupage in
+  # both .netrc and .ncftp/bookmarks .  We could even try saving
+  # the info in their for new hosts, like ncftp does.
+  
+  if [[ $once = 1 ]]; then
+    zftp open $*
+  else
+    # set parameters, but only if there was at least a host
+    (( $# > 0 )) && zfparams $*
+    # now call with no parameters
+    zftp open
+  fi
+}
+
+function zfparams {
+    # Set to prompt for any user or password if not given.
+    # Don't worry about accounts here.
+    if (( $# > 0 )); then
+      (( $# < 2 )) && 2='?'
+      (( $# < 3 )) && 3='?'
+    fi
+    zftp params $*
+}
+
+function zfput {
+  # Simple put:  dump every file under the same name, but stripping
+  # off any directory parts.
+  local loc rem stat=0
+  for loc in $*; do
+    rem=${loc:t}
+    zftp put $rem <$loc
+    [[ $? == 0 ]] || stat=$?
+  done
+  return $stat
+}
+
+function zfrglob {
+  # Do the remote globbing for zfput, etc.
+  # We have two choices:
+  #  (1) Get the entire file list and match it one by one
+  #      locally against the pattern.
+  #      Causes problems if we are globbing directories (rare, presumably).
+  #      But: we can cache the current directory, which
+  #      we need for completion anyway.  Works on any OS if you
+  #      stick with a single directory.  This is the default.
+  #  (2) Use remote globbing, i.e. pass it to ls at the site.
+  #      Faster, but only works with UNIX, and only basic globbing.
+  #      We do this if $zfrglob is non-null.
+  
+  # There is only one argument, the variable containing the
+  # pattern to be globbed.  We set this back to an array containing
+  # all the matches.
+  setopt localoptions unset
+  unsetopt ksharrays
+  
+  local pat dir nondir files i
+  
+  eval pat=\$$1
+  
+  # Check if we really need to do anything.  Look for standard
+  # globbing characters, and if extendedglob is set and we are
+  # using zsh for the actual pattern matching also look for
+  # extendedglob characters.
+  if [[ $remlist != *[][*?]* &&
+    ( -n $zfrglob || ! -o extendedglob || $remlist != *[(|)~#^]* ) ]]; then
+    return 0
+  fi
+  
+  if [[ $zfrglob != '' ]]; then
+    eval "$1=(\$(zftp ls \"$pat\" 2>/dev/null))"
+  else
+    if [[ $ZFTP_SYSTEM = UNIX* && $pat = */* ]]; then
+      # not the current directory and we know how to handle paths
+      if [[ $pat = ?*/* ]]; then
+        # careful not to remove too many slashes
+        dir=${pat%/*}
+      else
+        dir=/
+      fi
+      nondir=${pat##*/}
+      files=($(zftp ls "$dir" 2>/dev/null))
+    else
+      # we just have to do an ls and hope that's right
+      nondir=$pat
+      if (( $#zftp_fcache == 0 )); then
+        zftp_fcache=($(zftp ls))
+      fi
+      files=($zftp_fcache)
+    fi
+    # now we want to see which of the $files match $nondir
+    for (( i = 1; i <= $#files; i++)); do
+      # empty words are elided in array assignment
+      [[ $files[$i] = ${~nondir} ]] || files[$i]=''
+    done
+    eval "$1=(\$files)"
+  fi
+}
+
+function zfrtime {
+  # Set the modification time of file LOCAL to that of REMOTE.
+  # If the optional TIME is passed, it should be in the FTP format
+  # CCYYMMDDhhmmSS, i.e. no dot before the seconds, and in GMT.
+  # This is what both `zftp remote' and `zftp local' return.
+  #
+  # Unfortunately, since the time returned from FTP is GMT and
+  # your file needs to be set in local time, we need to do some
+  # hacking around with time.  At the moment this requires perl 5
+  # with the standard library.
+  
+  setopt localoptions unset
+  unsetopt ksharrays
+  
+  local time gmtime loctime
+  
+  if [[ -n $3 ]]; then
+    time=$3
+  else
+    time=($(zftp remote $2 2>/dev/null))
+    [[ -n $time ]] && time=$time[2]
+  fi
+  [[ -z $time ]] && return 1
+  
+  # Now's the real *!@**!?!.  We have the date in GMT and want to turn
+  # it into local time for touch to handle.  It's just too nasty
+  # to handle in zsh; do it in perl.
+  if perl -mTime::Local -e '($file, $t) = @ARGV;
+  $yr = substr($t, 0, 4) - 1900;
+  $mon = substr($t, 4, 2) - 1;
+  $mday = substr($t, 6, 2) + 0;
+  $hr = substr($t, 8, 2) + 0;
+  $min = substr($t, 10, 2) + 0;
+  $sec = substr($t, 12, 2) + 0;
+  $time = Time::Local::timegm($sec, $min, $hr, $mday, $mon, $yr);
+  utime $time, $time, $file and return 0;' $1 $time 2>/dev/null; then
+    print "Setting time for $1 failed.  Need perl 5." 2>1
+  fi
+  
+  # If it wasn't for the GMT/local time thing, it would be this simple.
+  #
+  # time="${time[1,12]}.${time[13,14]}"
+  #
+  # touch -t $time $1
+  
+}
+
+function zfstat {
+  # Give a zftp status report using local variables.
+  # With option -v, connect the remote host and ask it what it
+  # thinks the status is.  
+  
+  setopt localoptions unset
+  unsetopt ksharrays
+  
+  local i stat=0 opt optlist verbose
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $opt in
+        v) verbose=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  # hack:  in case the status from a subshell process hasn't been
+  # fixed yet
+  zftp type >&/dev/null
+  
+  if [[ -n $ZFTP_HOST ]]; then
+    print "Host:\t\t$ZFTP_HOST"
+    print "IP:\t\t$ZFTP_IP"
+    [[ -n $ZFTP_SYSTEM ]] && print "System type:\t$ZFTP_SYSTEM"
+    if [[ -n $ZFTP_USER ]]; then
+      print "User:\t\t$ZFTP_USER "
+      [[ -n $ZFTP_ACCOUNT ]] && print "Account:\t$AFTP_ACCOUNT"
+      print "Directory:\t$ZFTP_PWD"
+      print -n "Transfer type:\t"
+      if [[ $ZFTP_TYPE = "I" ]]; then
+        print Image
+      elif [[ $ZFTP_TYPE = "A" ]]; then
+        print Ascii
+      else
+        print Unknown
+      fi
+      print -n "Transfer mode:\t"
+      if [[ $ZFTP_MODE = "S" ]]; then
+        print Stream
+      elif [[ $ZFTP_MODE = "B" ]]; then
+        print Block
+      else
+        print Unknown
+      fi
+    else
+      print "No user logged in."
+    fi
+  else
+    print "Not connected."
+    stat=1
+  fi
+  
+  # things which may be set even if not connected:
+  [[ -n $ZFTP_REPLY ]] && print "Last reply:\t$ZFTP_REPLY"
+  print "Verbosity:\t$ZFTP_VERBOSE"
+  print -n "Preferences:\t"
+  for (( i = 1; i <= ${#ZFTP_PREFS}; i++ )); do
+    case $ZFTP_PREFS[$i] in
+      [pP]) print -n "Passive "
+  	  ;;
+      [sS]) print -n "Sendport "
+  	  ;;
+      [dD]) print -n "Dumb "
+  	  ;;
+      *) print -n "$ZFTP_PREFS[$i]???"
+    esac
+  done
+  print
+  
+  if [[ -n $ZFTP_HOST && $verbose = 1 ]]; then
+    print "Status of remote server:"
+    # make sure we print the reply
+    local ZFTP_VERBOSE=045
+    zftp quote STAT
+  fi
+  
+  return $stat
+}
+
+function zftp_chpwd {
+  # You may want to alter chpwd to call this when $ZFTP_USER is set.
+  # If so, call it with a non-zero first argument so it doesn't
+  # print the new FTP directory.
+  
+  # Cancel the filename cache for the current directory.
+  zftp_fcache=()
+  # ...and also empty the stored directory listing cache.
+  # As this function is called when we close the connection, this
+  # is the only place we need to do these two things.
+  [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir
+  
+  if [[ -z $ZFTP_USER ]]; then
+    # last call, after an FTP logout
+  
+    # delete the non-current cached directory
+    [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir
+    zfotherargs=
+  
+    # don't keep zflastdir between opens
+    zflastdir=
+  
+    # return the display to standard
+    # uncomment the following line if you have a chpwd which shows directories
+    # chpwd
+  else
+    [[ -z $zflastdir ]] && zflastdir=$ZFTP_PWD
+    local args
+    if [[ -t 1 && -t 2 ]]; then
+      local str="$ZFTP_HOST:$ZFTP_PWD"
+      [[ -z $1 ]] && print $str
+      [[ ${#str} -lt 70 ]] && str="%m: %~  $str"
+      case $TERM in
+        sun-cmd) print -n -P "\033]l$str\033\\"
+  	       ;;
+        xterm) print -n -P "\033]2;$str\a"
+  	     ;;
+      esac
+    fi
+  fi
+}
+
+function zftp_progress {
+  # Basic progress metre, showing the percent of the file transferred.
+  # You want growing bars?  You gotta write growing bars.
+  
+  # Don't show progress unless stderr is a terminal
+  [[ ! -t 2 ]] && return 0
+  
+  if [[ $ZFTP_TRANSFER = *F ]]; then
+    print 1>&2
+  elif [[ -n $ZFTP_TRANSFER ]]; then
+    if [[ -n $ZFTP_SIZE ]]; then
+      local frac="$(( ZFTP_COUNT * 100 / ZFTP_SIZE ))%"
+      print -n "\r$ZFTP_FILE ($ZFTP_SIZE bytes): $ZFTP_TRANSFER $frac" 1>&2
+    else
+      print -n "\r$ZFTP_FILE: $ZFTP_TRANSFER $ZFTP_COUNT" 1>&2
+    fi
+  fi
+}
+
+function zftype {
+  local type
+  
+  if (( $# == 0 )); then
+    type=$(zftp type)
+    if [[ $type = I ]]; then
+      print "Current type is image (binary)"
+      return 0
+    elif [[ $type = A ]]; then
+      print "Current type is ASCII"
+      return 0
+    else
+      return 1
+    fi
+  else
+    if [[ $1 == [aA]([sS][cC]([iI][iI]|)|) ]]; then
+      type=A
+    elif [[ $1 == [iI]([mM]([aA][gG][eE]|)|) ||
+      $1 == [bB]([iI][nN]([aA][rR][yY]|)|) ]]; then
+      type=I
+    else
+      print "Type not recognised:  $1" 2>&1
+      return 1
+    fi
+    zftp type $type
+  fi
+}
+
+function zfuget {
+  # Get a list of files from the server with update.
+  # In other words, only retrieve files which are newer than local
+  # ones.  This depends on the clocks being adjusted correctly
+  # (i.e. if one is fifteen minutes out, for the next fifteen minutes
+  # updates may not be correctly calculated).  However, difficult
+  # cases --- where the files are the same size, but the remote is newer,
+  # or have different sizes, but the local is newer -- are prompted for.
+  #
+  # Files are globbed on the remote host --- assuming, of course, they
+  # haven't already been globbed local, so use 'noglob' e.g. as
+  # `alias zfuget="noglob zfuget"'.
+  #
+  # Options:
+  #  -G    Glob:     turn off globbing
+  #  -v    verbose:  print more about the files listed.
+  #  -s    silent:   don't ask, just guess.  The guesses are:
+  #                - if the files have different sizes but remote is older ) grab
+  #                - if they have the same size but remote is newer        )
+  #                  which is safe if the remote files are always the right ones.
+  #   -t   time:     update the local file times to the same time as the remote.
+  #                  Currently this only works if you have the `perl' command,
+  #                  and that perl is version 5 with the standard library.
+  #                  See the function zfrtime for more gory details.
+  
+  setopt localoptions
+  unsetopt ksharrays shwordsplit
+  
+  local loc rem stat=0 locstats remstats doit tmpfile=${TMPPREFIX}zfuget$$
+  local rstat remlist verbose optlist opt bad i silent nglob time
+  
+  zfuget_print_time() {
+    local tim=$1
+    print -n "$tim[1,4]/$tim[5,6]/$tim[7,8] $tim[9,10]:$tim[11,12].$tim[13,14]"
+    print -n GMT
+  }
+  
+  zfuget_print () {
+    print -n "\nremote $rem ("
+    zfuget_print_time $remstats[2]
+    print -n ", $remstats[1] bytes)\nlocal $loc ("
+    zfuget_print_time $locstats[2]
+    print ", $locstats[1] bytes)"
+  }
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        v) verbose=1
+  	 ;;
+        s) silent=1
+  	 ;;
+        G) nglob=1
+  	 ;;
+        t) time=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  [[ -n $bad ]] && return 1
+  
+  for remlist in $*; do
+    # zfcd directory hack to put the front back to ~
+    if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then
+      remlist="~${remlist#$HOME}"
+    fi
+    if [[ $nglob != 1 ]]; then
+      zfrglob remlist
+    fi
+    if (( $#remlist )); then
+      for rem in $remlist; do
+        loc=${rem:t}
+        doit=y
+        remstats=()
+        if [[ -f $loc ]]; then
+  	zftp local $loc >$tmpfile
+  	locstats=($(<$tmpfile))
+  	zftp remote $rem >$tmpfile
+  	rstat=$?
+  	remstats=($(<$tmpfile))
+  	rm -f $tmpfile
+  	if [[ $rstat = 2 ]]; then
+  	  print "Server does not implement full command set required." 1>&2
+  	  return 1
+  	elif [[ $rstat = 1 ]]; then
+  	  print "File not found on server: $rem" 1>&2
+  	  stat=1
+  	  continue
+  	fi
+  	[[ $verbose = 1 ]] && zfuget_print
+  	if (( $locstats[1] != $remstats[1] )); then
+  	  # Files have different sizes
+  	  if [[ $locstats[2] > $remstats[2] && $silent != 1 ]]; then
+  	    [[ $verbose != 1 ]] && zfuget_print
+  	    print "Local file $loc more recent than remote," 1>&2
+  	    print -n "but sizes are different.  Transfer anyway [y/n]? " 1>&2
+  	    read -q doit
+  	  fi
+  	else
+  	  # Files have same size
+  	  if [[ $locstats[2] < $remstats[2] ]]; then
+  	    if [[ $silent != 1 ]]; then
+  	      [[ $verbose != 1 ]] && zfuget_print
+  	      print "Local file $loc has same size as remote," 1>&2
+  	      print -n "but local file is older. Transfer anyway [y/n]? " 1>&2
+  	      read -q doit
+  	    fi
+  	  else
+  	    # presumably same file, so don't get it.
+  	    [[ $verbose = 1 ]] && print Not transferring
+  	    doit=n
+  	  fi
+  	fi
+        else
+  	[[ $verbose = 1 ]] && print New file $loc
+        fi
+        if [[ $doit = y ]]; then
+  	if zftp get $rem >$loc; then
+  	  if [[ $time = 1 ]]; then
+  	    # if $remstats is set, it's second element is the remote time
+  	    zfrtime $loc $rem $remstats[2]
+  	  fi
+  	else
+  	  stat=$?
+  	fi
+  	
+        fi
+      done
+    fi
+  done
+  return $stat
+}
+
+function zfuput {
+  # Put a list of files from the server with update.
+  # See zfuget for details.
+  #
+  # Options:
+  #  -v    verbose:  print more about the files listed.
+  #  -s    silent:   don't ask, just guess.  The guesses are:
+  #                - if the files have different sizes but remote is older ) grab
+  #                - if they have the same size but remote is newer        )
+  #                  which is safe if the remote files are always the right ones.
+  
+  setopt localoptions
+  unsetopt ksharrays shwordsplit
+  
+  local loc rem stat=0 locstats remstats doit tmpfile=${TMPPREFIX}zfuput$$
+  local rstat verbose optlist opt bad i silent
+  
+  zfuput_print_time() {
+    local tim=$1
+    print -n "$tim[1,4]/$tim[5,6]/$tim[7,8] $tim[9,10]:$tim[11,12].$tim[13,14]"
+    print -n GMT
+  }
+  
+  zfuput_print () {
+    print -n "\nremote $rem ("
+    zfuput_print_time $remstats[2]
+    print -n ", $remstats[1] bytes)\nlocal $loc ("
+    zfuput_print_time $locstats[2]
+    print ", $locstats[1] bytes)"
+  }
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        v) verbose=1
+  	 ;;
+        s) silent=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  [[ -n $bad ]] && return 1
+  
+  if [[ $ZFTP_VERBOSE = *5* ]]; then
+    # should we turn it off locally?
+    print "Messages with code 550 are harmless." >&2
+  fi
+  
+  for loc in $*; do
+    rem=${loc:t}
+    doit=y
+    remstats=()
+    if [[ ! -f $loc ]]; then
+      print "$loc: file not found" >&2
+      stat=1
+      continue
+    fi
+    zftp local $loc >$tmpfile
+    locstats=($(<$tmpfile))
+    zftp remote $rem >$tmpfile
+    rstat=$?
+    remstats=($(<$tmpfile))
+    rm -f $tmpfile
+    if [[ $rstat = 2 ]]; then
+      print "Server does not implement full command set required." 1>&2
+      return 1
+    elif [[ $rstat = 1 ]]; then
+      [[ $verbose = 1 ]] && print New file $loc
+    else
+      [[ $verbose = 1 ]] && zfuput_print
+      if (( $locstats[1] != $remstats[1] )); then
+        # Files have different sizes
+        if [[ $locstats[2] < $remstats[2] && $silent != 1 ]]; then
+  	[[ $verbose != 1 ]] && zfuput_print
+  	print "Remote file $rem more recent than local," 1>&2
+  	print -n "but sizes are different.  Transfer anyway [y/n]? " 1>&2
+  	read -q doit
+        fi
+      else
+        # Files have same size
+        if [[ $locstats[2] > $remstats[2] ]]; then
+  	if [[ $silent != 1 ]]; then
+  	  [[ $verbose != 1 ]] && zfuput_print
+  	  print "Remote file $rem has same size as local," 1>&2
+  	  print -n "but remote file is older. Transfer anyway [y/n]? " 1>&2
+  	  read -q doit
+  	fi
+        else
+  	# presumably same file, so don't get it.
+  	[[ $verbose = 1 ]] && print Not transferring
+  	doit=n
+        fi
+      fi
+    fi
+    if [[ $doit = y ]]; then
+      zftp put $rem <$loc || stat=$?
+    fi
+  done
+  return $stat
+}
diff --git a/Src/Modules/example.c b/Src/Modules/example.c
index 45ef3c28f..a71806c3a 100644
--- a/Src/Modules/example.c
+++ b/Src/Modules/example.c
@@ -49,6 +49,53 @@ bin_example(char *nam, char **args, char *ops, int func)
     return 0;
 }
 
+/**/
+static int
+cond_p_len(Conddef c, char **a)
+{
+    char *s1 = a[0], *s2 = a[1];
+
+    singsub(&s1);
+    untokenize(s1);
+    if (s2) {
+	singsub(&s2);
+	untokenize(s2);
+	return strlen(s1) == matheval(s2);
+    } else {
+	return !s1[0];
+    }
+}
+
+/**/
+static int
+cond_i_ex(Conddef c, char **a)
+{
+    char *s1 = a[0], *s2 = a[1];
+
+    singsub(&s1);
+    untokenize(s1);
+    singsub(&s2);
+    untokenize(s2);
+    return !strcmp("example", dyncat(s1, s2));
+}
+
+/**/
+static int
+ex_wrapper(List list, FuncWrap w, char *name)
+{
+    if (strncmp(name, "example", 7))
+	return 1;
+    else {
+	int ogd = opts[GLOBDOTS];
+
+	opts[GLOBDOTS] = 1;
+	runshfunc(list, w, name);
+	opts[GLOBDOTS] = ogd;
+
+	return 0;
+    }
+}
+
 /*
  * boot_example is executed when the module is loaded.
  */
@@ -57,11 +104,22 @@ static struct builtin bintab[] = {
     BUILTIN("example", 0, bin_example, 0, -1, 0, "flags", NULL),
 };
 
+static struct conddef cotab[] = {
+    CONDDEF("len", 0, 1, 2, cond_p_len),
+    CONDDEF("ex", CONDF_INFIX, 0, 0, cond_i_ex),
+};
+
+static struct funcwrap wrapper[] = {
+    WRAPDEF(ex_wrapper),
+};
+
 /**/
 int
 boot_example(Module m)
 {
-    return !addbuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
+    return !(addbuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab)) |
+	     addconddefs(m->nam, cotab, sizeof(cotab)/sizeof(*cotab)) |
+	     !addwrapper(m, wrapper));
 }
 
 #ifdef MODULE
@@ -71,6 +129,8 @@ int
 cleanup_example(Module m)
 {
     deletebuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
+    deleteconddefs(m->nam, cotab, sizeof(cotab)/sizeof(*cotab));
+    deletewrapper(m, wrapper);
     return 0;
 }
 #endif
diff --git a/Src/Modules/stat.c b/Src/Modules/stat.c
index 64664abaf..769b42b1a 100644
--- a/Src/Modules/stat.c
+++ b/Src/Modules/stat.c
@@ -34,11 +34,13 @@ enum statnum { ST_DEV, ST_INO, ST_MODE, ST_NLINK, ST_UID, ST_GID,
 		   ST_RDEV, ST_SIZE, ST_ATIM, ST_MTIM, ST_CTIM,
 		   ST_BLKSIZE, ST_BLOCKS, ST_READLINK, ST_COUNT };
 enum statflags { STF_NAME = 1,  STF_FILE = 2, STF_STRING = 4, STF_RAW = 8,
-		     STF_PICK = 16, STF_ARRAY = 32, STF_GMT = 64 };
+		     STF_PICK = 16, STF_ARRAY = 32, STF_GMT = 64,
+		     STF_HASH = 128 };
 static char *statelts[] = { "device", "inode", "mode", "nlink",
 				"uid", "gid", "rdev", "size", "atime",
 				"mtime", "ctime", "blksize", "blocks",
 				"link", NULL };
+#define HNAMEKEY "name"
 
 /**/
 static void
@@ -287,6 +289,8 @@ statprint(struct stat *sbuf, char *outbuf, char *fname, int iwhich, int flags)
  *        file names are returned as a separate array element, type names as
  *        prefix to element.  Note the formatting deliberately contains
  *        fewer frills when -A is used.
+ *  -H hash:  as for -A array, but returns a hash with the keys being those
+ *        from stat -l
  *  -F fmt: specify a $TIME-like format for printing times; the default
  *        is the (CTIME-like) "%a %b %e %k:%M:%S".  This option implies
  *        -s as it is not useful for numerical times.
@@ -305,6 +309,7 @@ static int
 bin_stat(char *name, char **args, char *ops, int func)
 {
     char **aptr, *arrnam = NULL, **array = NULL, **arrptr = NULL;
+    char *hashnam = NULL, **hash = NULL, **hashptr = NULL;
     int len, iwhich = -1, ret = 0, flags = 0, arrsize = 0, fd = 0;
     struct stat statbuf;
     int found = 0, nargs;
@@ -352,6 +357,16 @@ bin_stat(char *name, char **args, char *ops, int func)
 		    }
 		    flags |= STF_ARRAY;
 		    break;
+		} else if (*arg == 'H') {
+		    if (arg[1]) {
+			hashnam = arg+1;
+		    } else if (!(hashnam = *++args)) {
+			zerrnam(name, "missing parameter name\n",
+				NULL, 0);
+			return 1;
+		    }
+		    flags |= STF_HASH;
+		    break;
 		} else if (*arg == 'f') {
 		    char *sfd;
 		    ops['f'] = 1;
@@ -385,6 +400,15 @@ bin_stat(char *name, char **args, char *ops, int func)
 	}
     }
 
+    if ((flags & STF_ARRAY) && (flags & STF_HASH)) {
+    	/* We don't implement setting multiple variables at once */
+	zwarnnam(name, "both array and hash requested", NULL, 0);
+	return 1;
+	/* Alternate method would be to make -H blank arrnam etc etc *
+	 * and so get 'silent loss' of earlier choice, which would   *
+	 * be similar to stat -A foo -A bar filename                 */
+    }
+
     if (ops['l']) {
 	/* list types and return:  can also list to array */
 	if (arrnam) {
@@ -435,7 +459,7 @@ bin_stat(char *name, char **args, char *ops, int func)
     if (ops['g'])
 	flags |= STF_GMT;
 
-    if (!arrnam) {
+    if (!(arrnam || hashnam)) {
 	if (nargs > 1)
 	    flags |= STF_FILE;
 	if (!(flags & STF_PICK))
@@ -444,9 +468,20 @@ bin_stat(char *name, char **args, char *ops, int func)
 
     if (ops['N'] || ops['f'])
 	flags &= ~STF_FILE;
-    if (ops['T'])
+    if (ops['T'] || ops['H'])
 	flags &= ~STF_NAME;
 
+    if (hashnam) {
+    	if (nargs > 1) {
+	    zwarnnam(name, "only one file allowed with -H", NULL, 0);
+	    return 1;
+	}
+	arrsize = (flags & STF_PICK) ? 1 : ST_COUNT;
+	if (flags & STF_FILE)
+	    arrsize++;
+	hashptr = hash = (char **)zcalloc((arrsize+1)*2*sizeof(char *));
+    }
+
     if (arrnam) {
 	arrsize = (flags & STF_PICK) ? 1 : ST_COUNT;
 	if (flags & STF_FILE)
@@ -473,13 +508,20 @@ bin_stat(char *name, char **args, char *ops, int func)
 	if (flags & STF_FILE)
 	    if (arrnam)
 		*arrptr++ = ztrdup(*args);
-	    else
+	    else if (hashnam) {
+	    	*hashptr++ = ztrdup(HNAMEKEY);
+		*hashptr++ = ztrdup(*args);
+	    } else
 		printf("%s%s", *args, (flags & STF_PICK) ? " " : ":\n");
 	if (iwhich > -1) {
 	    statprint(&statbuf, outbuf, *args, iwhich, flags);
 	    if (arrnam)
 		*arrptr++ = ztrdup(outbuf);
-	    else
+	    else if (hashnam) {
+		/* STF_NAME explicitly turned off for ops['H'] above */
+	    	*hashptr++ = ztrdup(statelts[iwhich]);
+		*hashptr++ = ztrdup(outbuf);
+	    } else
 		printf("%s\n", outbuf);
 	} else {
 	    int i;
@@ -487,28 +529,39 @@ bin_stat(char *name, char **args, char *ops, int func)
 		statprint(&statbuf, outbuf, *args, i, flags);
 		if (arrnam)
 		    *arrptr++= ztrdup(outbuf);
-		else
+		else if (hashnam) {
+		    /* STF_NAME explicitly turned off for ops['H'] above */
+		    *hashptr++ = ztrdup(statelts[i]);
+		    *hashptr++ = ztrdup(outbuf);
+		} else
 		    printf("%s\n", outbuf);
 	    }
 	}
 	if (ops['f'])
 	    break;
 
-	if (!arrnam && args[1] && !(flags & STF_PICK))
+	if (!arrnam && !hashnam && args[1] && !(flags & STF_PICK))
 	    putchar('\n');
     }
 
     if (arrnam)
-	if (ret) {
-	    for (aptr = array; *aptr; aptr++)
-		zsfree(*aptr);
-	    zfree(array, arrsize+1);
-	} else {
+	if (ret)
+	    freearray(array);
+	else {
 	    setaparam(arrnam, array);
 	    if (errflag)
 		return 1;
 	}
 
+    if (hashnam)
+    	if (ret)
+	    freearray(hash);
+	else {
+	    sethparam(hashnam, hash);
+	    if (errflag)
+		return 1;
+	}
+
     return ret;
 }
 
diff --git a/Src/Modules/zftp.c b/Src/Modules/zftp.c
new file mode 100644
index 000000000..ca0843419
--- /dev/null
+++ b/Src/Modules/zftp.c
@@ -0,0 +1,2596 @@
+/*
+ * zftp.c - builtin FTP client
+ *
+ * This file is part of zsh, the Z shell.
+ *
+ * Copyright (c) 1998 Peter Stephenson
+ * All rights reserved.
+ *
+ * Permission is hereby granted, without written agreement and without
+ * license or royalty fees, to use, copy, modify, and distribute this
+ * software and to distribute modified versions of this software for any
+ * purpose, provided that the above copyright notice and the following
+ * two paragraphs appear in all copies of this software.
+ *
+ * In no event shall Peter Stephenson or the Zsh Development Group be liable
+ * to any party for direct, indirect, special, incidental, or consequential
+ * damages arising out of the use of this software and its documentation,
+ * even if Peter Stephenson and the Zsh Development Group have been advised of
+ * the possibility of such damage.
+ *
+ * Peter Stephenson and the Zsh Development Group specifically disclaim any
+ * warranties, including, but not limited to, the implied warranties of
+ * merchantability and fitness for a particular purpose.  The software
+ * provided hereunder is on an "as is" basis, and Peter Stephenson and the
+ * Zsh Development Group have no obligation to provide maintenance,
+ * support, updates, enhancements, or modifications.
+ *
+ */
+
+/*
+ * TODO:
+ *   can signal handling be improved?
+ *   error messages may need tidying up.
+ *   maybe we should block CTRL-c on some more operations,
+ *     otherwise you can get the connection closed prematurely.
+ *   some way of turning off progress reports when backgrounded
+ *     would be nice, but the shell doesn't make it easy to find that out.
+ *   the progress reports 100% a bit prematurely:  the data may still
+ *     be in transit, and we are stuck waiting for a message from the
+ *     server.  but there's really nothing else to do.  it's worst
+ *     with small files.
+ *   proxy/gateway connections if i knew what to do
+ *   options to specify e.g. a non-standard port
+ *   optimizing things out is hard in general when you don't know what
+ *     the shell's going to want, but they may be places to second guess
+ *     the user.  Some of the variables could be made special and so
+ *     only retrieve things like the current directory when necessary.
+ *     But it's much neater to have ordinary variables, which the shell
+ *     can manage without our interference, and getting the directory
+ *     just every time it changes isn't so bad.  The user can always
+ *     toggle the `Dumb' preference if it's feeling clever.
+ */
+#include "zftp.mdh"
+#include "zftp.pro"
+
+#include <sys/socket.h>
+#include <netdb.h>
+#include <netinet/in_systm.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <arpa/inet.h>
+/* it's a TELNET based protocol, but don't think I like doing this */
+#include <arpa/telnet.h>
+
+/* bet there are machines which have neither INADDR_NONE nor in_addr_t. */
+#ifndef INADDR_NONE
+#define INADDR_NONE (in_addr_t)-1
+#endif
+
+/*
+ * For FTP block mode
+ *
+ * The server on our AIX machine here happily accepts block mode, takes the
+ * first connection, then at the second complains that it's got nowhere
+ * to send data.  The same problem happens with ncftp, it's not just
+ * me.  And a lot of servers don't even support block mode. So I'm not sure
+ * how widespread the supposed ability to leave open the data fd between
+ * transfers.  Therefore, I've closed all connections after the transfer.
+ * But then what's the point in block mode?  I only implemented it because
+ * it says in RFC959 that you need it to be able to restart transfers
+ * later in the file.  However, it turns out that's not true for
+ * most servers --- but our AIX machine happily accepts the REST
+ * command and then dumps the whole file onto you.  Sigh.
+ *
+ * Note on block sizes:
+ * Not quite sure how to optimize this:  in principle
+ * we should handle blocks up to 65535 bytes, which
+ * is pretty big, and should presumably send blocks
+ * which are smaller to be on the safe side.
+ * Currently we send 32768 and use that also as
+ * the maximum to receive.  No-one's complained yet.  Of course,
+ * no-one's *used* it yet apart from me, but even so.
+ */
+
+struct zfheader {
+    char flags;
+    unsigned char bytes[2];
+};
+
+enum {
+    ZFHD_MARK = 16,		/* restart marker */
+    ZFHD_ERRS = 32,		/* suspected errors in block */
+    ZFHD_EOFB = 64,		/* block is end of record */
+    ZFHD_EORB = 128		/* block is end of file */
+};
+
+typedef int (*readwrite_t)(int, char *, size_t, int);
+
+struct zftpcmd {
+    const char *nam;
+    int (*fun) _((char *, char **, int));
+    int min, max, flags;
+};
+
+enum {
+    ZFTP_CONN  = 0x0001,	/* must be connected */
+    ZFTP_LOGI  = 0x0002,	/* must be logged in */
+    ZFTP_TBIN  = 0x0004,	/* set transfer type image */
+    ZFTP_TASC  = 0x0008,	/* set transfer type ASCII */
+    ZFTP_NLST  = 0x0010,	/* use NLST rather than LIST */
+    ZFTP_DELE  = 0x0020,	/* a delete rather than a make */
+    ZFTP_SITE  = 0x0040,	/* a site rather than a quote */
+    ZFTP_APPE  = 0x0080,	/* append rather than overwrite */
+    ZFTP_HERE  = 0x0100,	/* here rather than over there */
+    ZFTP_CDUP  = 0x0200,	/* CDUP rather than CWD */
+    ZFTP_REST  = 0x0400,	/* restart: set point in remote file */
+    ZFTP_RECV  = 0x0800		/* receive rather than send */
+};
+
+typedef struct zftpcmd *Zftpcmd;
+
+static struct zftpcmd zftpcmdtab[] = {
+    { "open", zftp_open, 0, 4, 0 },
+    { "params", zftp_params, 0, 4, 0 },
+    { "login", zftp_login, 0, 3, ZFTP_CONN },
+    { "user", zftp_login, 0, 3, ZFTP_CONN },
+    { "cd", zftp_cd, 1, 1, ZFTP_CONN|ZFTP_LOGI },
+    { "cdup", zftp_cd, 0, 0, ZFTP_CONN|ZFTP_LOGI|ZFTP_CDUP },
+    { "dir", zftp_dir, 0, -1, ZFTP_CONN|ZFTP_LOGI },
+    { "ls", zftp_dir, 0, -1, ZFTP_CONN|ZFTP_LOGI|ZFTP_NLST },
+    { "type", zftp_type, 0, 1, ZFTP_CONN|ZFTP_LOGI },
+    { "ascii", zftp_type, 0, 0, ZFTP_CONN|ZFTP_LOGI|ZFTP_TASC },
+    { "binary", zftp_type, 0, 0, ZFTP_CONN|ZFTP_LOGI|ZFTP_TBIN },
+    { "mode", zftp_mode, 0, 1, ZFTP_CONN|ZFTP_LOGI },
+    { "local", zftp_local, 0, -1, ZFTP_HERE },
+    { "remote", zftp_local, 1, -1, ZFTP_CONN|ZFTP_LOGI },
+    { "get", zftp_getput, 1, -1, ZFTP_CONN|ZFTP_LOGI|ZFTP_RECV },
+    { "getat", zftp_getput, 2, 2, ZFTP_CONN|ZFTP_LOGI|ZFTP_RECV|ZFTP_REST },
+    { "put", zftp_getput, 1, -1, ZFTP_CONN|ZFTP_LOGI },
+    { "putat", zftp_getput, 2, 2, ZFTP_CONN|ZFTP_LOGI|ZFTP_REST },
+    { "append", zftp_getput, 1, -1, ZFTP_CONN|ZFTP_LOGI|ZFTP_APPE },
+    { "appendat", zftp_getput, 2, 2, ZFTP_CONN|ZFTP_LOGI|ZFTP_APPE|ZFTP_REST },
+    { "delete", zftp_delete, 1, -1, ZFTP_CONN|ZFTP_LOGI },
+    { "mkdir", zftp_mkdir, 1, 1, ZFTP_CONN|ZFTP_LOGI },
+    { "rmdir", zftp_mkdir, 1, 1, ZFTP_CONN|ZFTP_LOGI|ZFTP_DELE },
+    { "rename", zftp_rename, 2, 2, ZFTP_CONN|ZFTP_LOGI },
+    { "quote", zftp_quote, 1, -1, ZFTP_CONN },
+    { "site", zftp_quote, 1, -1, ZFTP_CONN|ZFTP_SITE },
+    { "close", zftp_close, 0, 0, ZFTP_CONN },
+    { "quit", zftp_close, 0, 0, ZFTP_CONN },
+    { 0, 0, 0, 0}
+};
+
+static struct builtin bintab[] = {
+    BUILTIN("zftp", 0, bin_zftp, 1, -1, 0, NULL, NULL),
+};
+
+/*
+ * these are the non-special params to unset when a connection
+ * closes.  any special params are handled, well, specially.
+ * currently there aren't any, which is the way I like it.
+ */
+static char *zfparams[] = {
+    "ZFTP_HOST", "ZFTP_IP", "ZFTP_SYSTEM", "ZFTP_USER",
+    "ZFTP_ACCOUNT", "ZFTP_PWD", "ZFTP_TYPE", "ZFTP_MODE", NULL
+};
+
+/* flags for zfsetparam */
+
+enum {
+    ZFPM_READONLY = 0x01,	/* make parameter readonly */
+    ZFPM_IFUNSET  = 0x02,	/* only set if not already set */
+    ZFPM_INTEGER  = 0x04	/* passed pointer to long */
+};
+
+/*
+ * Basic I/O variables for control connection:
+ * zcfd != -1 is a flag that there is a connection open.
+ */
+static int zcfd = -1;
+static FILE *zcin;
+static struct sockaddr_in zsock;
+
+/*
+ * zcfinish = 0 keep going
+ *            1 line finished, alles klar
+ *            2 EOF
+ */
+static int zcfinish;
+/* zfclosing is set if zftp_close() is active */
+static int zfclosing;
+
+/*
+ * Now stuff for data connection
+ */
+static int zdfd = -1;
+static struct sockaddr_in zdsock;
+
+/*
+ * Stuff about last message:  last line of message and status code.
+ * The reply is also stored in $ZFTP_REPLY; we keep these separate
+ * for convenience.
+ */
+static char *lastmsg, lastcodestr[4];
+static int lastcode;
+
+/* flag for remote system is UNIX --- useful to know as things are simpler */
+static int zfis_unix, zfpassive_conn;
+
+/* remote system has size, mdtm commands */
+enum {
+    ZFCP_UNKN = 0,		/* dunno if it works on this server */
+    ZFCP_YUPP = 1,		/* it does */
+    ZFCP_NOPE = 2		/* it doesn't */
+};
+
+static int zfhas_size, zfhas_mdtm;
+
+/*
+ * We keep an fd open for communication between the main shell
+ * and forked off bits and pieces.  This allows us to know
+ * if something happend in a subshell:  mode changed, type changed,
+ * connection was closed.  If something too substantial happened
+ * in a subshell --- connection opened, ZFTP_USER and ZFTP_PWD changed
+ * --- we don't try to track it because it's too complicated.
+ */
+enum {
+    ZFST_ASCI = 0x00,		/* type for next transfer is ASCII */
+    ZFST_IMAG = 0x01,		/* type for next transfer is image */
+
+    ZFST_TMSK = 0x01,		/* mask for type flags */
+    ZFST_TBIT = 0x01,		/* number of bits in type flags */
+
+    ZFST_CASC = 0x00,		/* current type is ASCII - default */
+    ZFST_CIMA = 0x02,		/* current type is image */
+
+    ZFST_STRE = 0x00,		/* stream mode - default */
+    ZFST_BLOC = 0x04,		/* block mode */
+
+    ZFST_MMSK = 0x04,		/* mask for mode flags */
+
+    ZFST_LOGI = 0x08,		/* user logged in */
+    ZFST_NOPS = 0x10,		/* server doesn't understand PASV */
+    ZFST_NOSZ = 0x20,		/* server doesn't send `(XXXX bytes)' reply */
+    ZFST_TRSZ = 0x40,		/* tried getting 'size' from reply */
+    ZFST_CLOS = 0x80		/* connection closed */
+};
+#define ZFST_TYPE(x) (x & ZFST_TMSK)
+/*
+ * shift current type flags to match type flags: should be by
+ * the number of bits in the type flags
+ */
+#define ZFST_CTYP(x) ((x >> ZFST_TBIT) & ZFST_TMSK)
+#define ZFST_MODE(x) (x & ZFST_MMSK)
+
+static int zfstatfd = -1, zfstatus;
+
+/* Preferences, read in from the `zftp_prefs' array variable */
+enum {
+    ZFPF_SNDP = 0x01,		/* Use send port mode */
+    ZFPF_PASV = 0x02,		/* Try using passive mode */
+    ZFPF_DUMB = 0x04		/* Don't do clever things with variables */
+};
+
+/* The flags as stored internally. */
+int zfprefs;
+
+
+/* zfuserparams is the storage area for zftp_params() */
+char **zfuserparams;
+
+/*
+ * Bits and pieces for dealing with SIGALRM (and SIGPIPE, but that's
+ * easier).  The complication is that SIGALRM may already be handled
+ * by the user setting TMOUT and possibly setting their own trap --- in
+ * fact, it's always handled by the shell when it's interactive.  It's
+ * too difficult to use zsh's own signal handler --- either it would
+ * need rewriting to use a C function as a trap, or we would need a
+ * hack to make it callback via a hidden builtin from a function --- so
+ * just install our own, and use settrap() to restore the behaviour
+ * afterwards if necessary.  However, the more that could be done by
+ * the main shell code, the better I would like it.
+ *
+ * Since we don't want to go through the palaver of changing between
+ * the main zsh signal handler and ours every time we start or stop the
+ * alarm, we keep the flag zfalarmed set to 1 while zftp is rigged to
+ * handle alarms.  This is tested at the end of bin_zftp(), which is
+ * the entry point for all functions, and that restores the original
+ * handler for SIGALRM.  To turn off the alarm temporarily in the zftp
+ * code we then just call alarm(0).
+ *
+ * If we could rely on having select() or some replacement, we would
+ * only need the alarm during zftp_open().
+ */
+
+/* flags for alarm set, alarm gone off */
+int zfalarmed, zfdrrrring;
+/* remember old alarm status */
+time_t oaltime;
+unsigned int oalremain;
+
+/*
+ * Where to jump to when the alarm goes off.  This is much
+ * easier than fiddling with error flags at every turn.
+ * Since we don't expect too many alarm's, the simple setjmp()
+ * mechanism should be good enough.
+ *
+ * gcc -O gives apparently spurious `may be clobbered by longjmp' warnings.
+ */
+jmp_buf zfalrmbuf;
+
+/* The signal handler itself */
+
+/**/
+static RETSIGTYPE
+zfhandler(int sig)
+{
+    if (sig == SIGALRM) {
+	zfdrrrring = 1;
+#ifdef ETIMEDOUT		/* just in case */
+	errno = ETIMEDOUT;
+#else
+	errno = EIO;
+#endif
+	longjmp(zfalrmbuf, 1);
+    }
+    DPUTS(1, "zfhandler caught incorrect signal");
+}
+
+/* Set up for an alarm call */
+
+/**/
+static void
+zfalarm(int tmout)
+{
+    zfdrrrring = 0;
+    /*
+     * We need to do this even if tmout is zero, since there may
+     * be a non-zero timeout set in the main shell which we don't
+     * want to go off.  This could be argued the other way, since
+     * if we don't get that it's probably harmless.  But this looks safer.
+     */
+    if (zfalarmed) {
+	alarm(tmout);
+	return;
+    }
+    signal(SIGALRM, zfhandler);
+    oalremain = alarm(tmout);
+    if (oalremain)
+	oaltime = time(NULL);
+    /*
+     * We'll leave sigtrapped, sigfuncs and TRAPXXX as they are; since the
+     * shell's handler doesn't get the signal, they don't matter.
+     */
+    zfalarmed = 1;
+}
+
+/* Set up for a broken pipe */
+
+/**/
+static void
+zfpipe()
+{
+    /* Just ignore SIGPIPE and rely on getting EPIPE from the write. */
+    signal(SIGPIPE, SIG_IGN);
+}
+
+/* Unset the alarm, see above */
+
+/**/
+static void
+zfunalarm()
+{
+    if (oalremain) {
+	/*
+	 * The alarm was previously set, so set it back, adjusting
+	 * for the time used.  Mostly the alarm was set to zero
+	 * beforehand, so it would probably be best to reinstall
+	 * the proper signal handler before resetting the alarm.
+	 *
+	 * I love the way alarm() uses unsigned int while time_t
+	 * is probably something completely different.
+	 */
+	time_t tdiff = time(NULL) - oaltime;
+	alarm(oalremain < tdiff ? 1 : oalremain - tdiff);
+    } else
+	alarm(0);
+    if (sigtrapped[SIGALRM] || interact) {
+	if (sigfuncs[SIGALRM] || !sigtrapped[SIGALRM])
+	    install_handler(SIGALRM);
+	else
+	    signal_ignore(SIGALRM);
+    } else
+	signal_default(SIGALRM);
+    zfalarmed = 0;
+}
+
+/* Restore SIGPIPE handling to its usual status */
+
+/**/
+static void
+zfunpipe()
+{
+    if (sigtrapped[SIGPIPE]) {
+	if (sigfuncs[SIGPIPE])
+	    install_handler(SIGPIPE);
+	else
+	    signal_ignore(SIGPIPE);
+    } else
+	signal_default(SIGPIPE);
+}
+
+/*
+ * Same as movefd(), but don't mark the fd in the zsh tables,
+ * because we only want it closed by zftp.  However, we still
+ * need to shift the fd's out of the way of the user-visible 0-9.
+ */
+
+/**/
+static int
+zfmovefd(int fd)
+{
+    if (fd != -1 && fd < 10) {
+#ifdef F_DUPFD
+	int fe = fcntl(fd, F_DUPFD, 10);
+#else
+	int fe = zfmovefd(dup(fd));
+#endif
+	close(fd);
+	fd = fe;
+    }
+    return fd;
+}
+
+/*
+ * set a non-special parameter.
+ * if ZFPM_IFUNSET, don't set if it already exists.
+ * if ZFPM_READONLY, make it readonly, but only when creating it.
+ * if ZFPM_INTEGER, val pointer is to long (NB not int), don't free.
+ */
+/**/
+static void
+zfsetparam(char *name, void *val, int flags)
+{
+    Param pm = NULL;
+    int type = (flags & ZFPM_INTEGER) ? PM_INTEGER : PM_SCALAR;
+
+    if (!(pm = (Param) paramtab->getnode(paramtab, name))
+	|| (pm->flags & PM_UNSET)) {
+	/*
+	 * just make it readonly when creating, in case user
+	 * *really* knows what they're doing
+	 */
+	if ((pm = createparam(name, type)) && (flags & ZFPM_READONLY))
+	    pm->flags |= PM_READONLY;
+    } else if (flags & ZFPM_IFUNSET) {
+	pm = NULL;
+    }
+    if (!pm || PM_TYPE(pm->flags) != type) {
+	/* parameters are funny, you just never know */
+	if (type == PM_SCALAR)
+	    zsfree((char *)val);
+	return;
+    }
+    if (type == PM_INTEGER)
+	pm->sets.ifn(pm, *(long *)val);
+    else
+	pm->sets.cfn(pm, (char *)val);
+}
+
+/*
+ * Unset a ZFTP parameter when the connection is closed.
+ * We only do this with connection-specific parameters.
+ */
+
+/**/
+static void
+zfunsetparam(char *name)
+{
+    Param pm;
+
+    if ((pm = (Param) paramtab->getnode(paramtab, name))) {
+	pm->flags &= ~PM_READONLY;
+	unsetparam_pm(pm, 0, 1);
+    }
+}
+
+/*
+ * Join command and arguments to make a proper TELNET command line.
+ * New line is in permanent storage.
+ */
+
+/**/
+static char *
+zfargstring(char *cmd, char **args)
+{
+    int clen = strlen(cmd) + 3;
+    char *line, **aptr;
+
+    for (aptr = args; *aptr; aptr++)
+	clen += strlen(*aptr) + 1;
+    line = zalloc(clen);
+    strcpy(line, cmd);
+    for (aptr = args; *aptr; aptr++) {
+	strcat(line, " ");
+	strcat(line, *aptr);
+    }
+    strcat(line, "\r\n");
+
+    return line;
+}
+
+/*
+ * get a line on the control connection according to TELNET rules
+ * Return status is first digit of FTP reply code
+ */
+
+/**/
+static int
+zfgetline(char *ln, int lnsize, int tmout)
+{
+    int ch, added = 0;
+    /* current line point */
+    char *pcur = ln, cmdbuf[3];
+
+    zcfinish = 0;
+    /* leave room for null byte */
+    lnsize--;
+    /* in case we return unexpectedly before getting anything */
+    ln[0] = '\0';
+
+    if (setjmp(zfalrmbuf)) {
+	alarm(0);
+	zwarnnam("zftp", "timeout getting response", NULL, 0);
+	return 5;
+    }
+    zfalarm(tmout);
+
+    /*
+     * We need to be more careful about errors here; we
+     * should do the stuff with errflag and so forth.
+     * We should probably holdintr() here, since if we don't
+     * get the message, the connection is going to be messed up.
+     * But then we get `frustrated user syndrome'.
+     */
+    for (;;) {
+	ch = fgetc(zcin);
+
+	switch(ch) {
+	case EOF:
+	    if (ferror(zcin) && errno == EINTR) {
+		clearerr(zcin);
+		continue;
+	    }
+	    zcfinish = 2;
+	    break;
+
+	case '\r':
+	    /* always precedes something else */
+	    ch = fgetc(zcin);
+	    if (ch == EOF) {
+		zcfinish = 2;
+		break;
+	    }
+	    if (ch == '\n') {
+		zcfinish = 1;
+		break;
+	    }
+	    if (ch == '\0') {
+		ch = '\r';
+		break;
+	    }
+	    /* not supposed to get here */
+	    ch = '\r';
+	    break;
+
+	case '\n':
+	    /* not supposed to get here */
+	    zcfinish = 1;
+	    break;
+
+	case IAC:
+	    /*
+	     * oh great, now it's sending TELNET commands.  try
+	     * to persuade it not to.
+	     */
+	    ch = fgetc(zcin);
+	    switch (ch) {
+	    case WILL:
+	    case WONT:
+		ch = fgetc(zcin);
+		/* whatever it wants to do, stop it. */
+		cmdbuf[0] = (char)IAC;
+		cmdbuf[1] = (char)DONT;
+		cmdbuf[2] = ch;
+		write(zcfd, cmdbuf, 3);
+		continue;
+
+	    case DO:
+	    case DONT:
+		ch = fgetc(zcin);
+		/* well, tough, we're not going to. */
+		cmdbuf[0] = (char)IAC;
+		cmdbuf[1] = (char)WONT;
+		cmdbuf[2] = ch;
+		write(zcfd, cmdbuf, 3);
+		continue;
+
+	    case EOF:
+		/* strange machine. */
+		zcfinish = 2;
+		break;
+
+	    default:
+		break;
+	    }
+	    break;
+	}
+	
+	if (zcfinish)
+	    break;
+	if (added < lnsize) {
+	    *pcur++ = ch;
+	    added++;
+	}
+	/* junk it if we don't have room, but go on reading */
+    }
+
+    alarm(0);
+
+    *pcur = '\0';
+    /* if zcfinish == 2, at EOF, return that, else 0 */
+    return (zcfinish & 2);
+}
+
+/*
+ * Get a whole message from the server.  A dash after
+ * the first line code means keep reading until we get
+ * a line with the same code followed by a space.
+ *
+ * Note that this returns an FTP status code, the first
+ * digit of the reply.  There is also a pseudocode, 6, which
+ * means `there's no point trying anything, just yet'.
+ * We return it either if the connection is closed, or if
+ * we got a 530 (user not logged in), in which case whatever
+ * you're trying to do isn't going to work.
+ */
+
+/**/
+static int 
+zfgetmsg()
+{
+    char line[256], *ptr, *verbose;
+    int stopit, printing = 0, tmout;
+
+    if (zcfd == -1)
+	return 5;
+    if (!(verbose = getsparam("ZFTP_VERBOSE")))
+	verbose = "";
+    zsfree(lastmsg);
+    lastmsg = NULL;
+
+    tmout = getiparam("ZFTP_TMOUT");
+
+    zfgetline(line, 256, tmout);
+    ptr = line;
+    if (zfdrrrring || !isdigit((int)*ptr) || !isdigit((int)ptr[1]) || 
+	!isdigit((int)ptr[2])) {
+	/* timeout, or not talking FTP.  not really interested. */
+	zcfinish = 2;
+	if (!zfclosing)
+	    zfclose();
+	lastmsg = ztrdup("");
+	strcpy(lastcodestr, "000");
+	zfsetparam("ZFTP_REPLY", ztrdup(lastmsg), ZFPM_READONLY);
+	return 6;
+    }
+    strncpy(lastcodestr, ptr, 3);
+    ptr += 3;
+    lastcodestr[3] = '\0';
+    lastcode = atoi(lastcodestr);
+    zfsetparam("ZFTP_CODE", ztrdup(lastcodestr), ZFPM_READONLY);
+    stopit = (*ptr++ != '-');
+
+    if (strchr(verbose, lastcodestr[0])) {
+	/* print the whole thing verbatim */
+	printing = 1;
+	fputs(line, stderr);
+    }  else if (strchr(verbose, '0') && !stopit) {
+	/* print multiline parts with the code stripped */
+	printing = 2;
+	fputs(ptr, stderr);
+    }
+    if (printing)
+	fputc('\n', stderr);
+
+    while (zcfinish != 2 && !stopit) {
+	zfgetline(line, 256, tmout);
+	ptr = line;
+	if (zfdrrrring) {
+	    line[0] = '\0';
+	    break;
+	}
+
+	if (!strncmp(lastcodestr, line, 3)) {
+	    if (line[3] == ' ') {
+		stopit = 1;
+		ptr += 4;
+	    } else if (line[3] == '-')
+		ptr += 4;
+	} else if (!strncmp("    ", line, 4))
+	    ptr += 4;
+
+	if (printing == 2) {
+	    if (!stopit) {
+		fputs(ptr, stderr);
+		fputc('\n', stderr);
+	    }
+	} else if (printing) {
+	    fputs(line, stderr);
+	    fputc('\n', stderr);
+	}
+    }
+
+    if (printing)
+	fflush(stderr);
+
+    /* The internal message is just the text. */
+    lastmsg = ztrdup(ptr);
+    /*
+     * The parameter is the whole thing, including the code.
+     */
+    zfsetparam("ZFTP_REPLY", ztrdup(line), ZFPM_READONLY);
+    /*
+     * close the connection here if zcfinish == 2, i.e. EOF,
+     * or if we get a 421 (service not available, closing connection),
+     * but don't do it if it's expected (zfclosing set).
+     */
+    if ((zcfinish == 2 || lastcode == 421) && !zfclosing) {
+	zcfinish = 2;		/* don't need to tell server */
+	zfclose();
+	/* unexpected, so tell user */
+	zwarnnam("zftp", "remote server has closed connection", NULL, 0);
+	return 6;		/* pretend it failed, because it did */
+    }
+    if (lastcode == 530) {
+	/* user not logged in */
+	return 6;
+    }
+    /*
+     * May as well handle this here, though it's pretty uncommon.
+     * A 120 is something like "service ready in nnn minutes".
+     * It means we just hang around waiting for another reply.
+     */
+    if (lastcode == 120) {
+	zwarnnam("zftp", "delay expected, waiting: %s", lastmsg, 0);
+	return zfgetmsg();
+    }
+
+    /* first digit of code determines success, failure, not in the mood... */
+    return lastcodestr[0] - '0';
+}
+
+
+/*
+ * Send a command and get the reply.
+ * The command is expected to have the \r\n already tacked on.
+ * Returns the status code for the reply.
+ */
+
+/**/
+static int
+zfsendcmd(char *cmd)
+{
+    /*
+     * We use the fd directly; there's no point even using
+     * stdio with line buffering, since we always send the
+     * complete line in one string anyway.
+     */
+    int ret, tmout;
+
+    if (zcfd == -1)
+	return 5;
+    tmout = getiparam("ZFTP_TMOUT");
+    if (setjmp(zfalrmbuf)) {
+	alarm(0);
+	zwarnnam("zftp", "timeout sending message", NULL, 0);
+	return 5;
+    }
+    zfalarm(tmout);
+    ret = write(zcfd, cmd, strlen(cmd));
+    alarm(0);
+
+    if (ret <= 0) {
+	zwarnnam("zftp send", "failed sending control message", NULL, 0);
+	return 5;		/* FTP status code */
+    }
+
+    return zfgetmsg();
+}
+
+
+/* Set up a data connection, return 1 for failure, 0 for success */
+
+/**/
+static int
+zfopendata(char *name)
+{
+    if (!(zfprefs & (ZFPF_SNDP|ZFPF_PASV))) {
+	zwarnnam(name, "Must set preference S or P to transfer data", NULL, 0);
+	return 1;
+    }
+    zdfd = zfmovefd(socket(AF_INET, SOCK_STREAM, 0));
+    if (zdfd < 0) {
+	zwarnnam(name, "can't get data socket: %e", NULL, errno);
+	return 1;
+    }
+
+    zdsock = zsock;
+    zdsock.sin_family = AF_INET;
+
+    if (!(zfstatus & ZFST_NOPS) && (zfprefs & ZFPF_PASV)) {
+	char *ptr;
+	int i, nums[6], err;
+	unsigned char iaddr[4], iport[2];
+
+	if (zfsendcmd("PASV\r\n") == 6)
+	    return 1;
+	else if (lastcode >= 500 && lastcode <= 504) {
+	    /*
+	     * Fall back to send port mode.  That will
+	     * test the preferences for whether that's OK.
+	     */
+	    zfstatus |= ZFST_NOPS;
+	    zfclosedata();
+	    return zfopendata(name);
+	}
+	/*
+	 * OK, now we need to know what port we're looking at,
+	 * which is cunningly concealed in the reply.
+	 * lastmsg already has the reply code expunged.
+	 */
+	for (ptr = lastmsg; *ptr; ptr++)
+	    if (isdigit(*ptr))
+		break;
+	if (sscanf(ptr, "%d,%d,%d,%d,%d,%d",
+		   nums, nums+1, nums+2, nums+3, nums+4, nums+5) != 6) {
+	    zwarnnam(name, "bad response to PASV: %s", lastmsg, 0);
+	    zfclosedata();
+	    return 1;
+	}
+	for (i = 0; i < 4; i++)
+	    iaddr[i] = STOUC(nums[i]);
+	iport[0] = STOUC(nums[4]);
+	iport[1] = STOUC(nums[5]);
+
+	memcpy(&zdsock.sin_addr, iaddr, sizeof(iaddr));
+	memcpy(&zdsock.sin_port, iport, sizeof(iport));
+
+	/* we should timeout this connect */
+	do {
+	    err = connect(zdfd, (struct sockaddr *)&zdsock, sizeof(zdsock));
+	} while (err && errno == EINTR && !errflag);
+
+	if (err) {
+	    zwarnnam(name, "connect failed: %e", NULL, errno);
+	    zfclosedata();
+	    return 1;
+	}
+
+	zfpassive_conn = 1;
+    } else {
+	char portcmd[40];
+	unsigned char *addr, *port;
+	int ret, len;
+
+	if (!(zfprefs & ZFPF_SNDP)) {
+	    zwarnnam(name, "only sendport mode available for data", NULL, 0);
+	    return 1;
+	}
+
+	zdsock.sin_port = 0;	/* to be set by bind() */
+	len = sizeof(zdsock);
+	/* need to do timeout stuff and probably handle EINTR here */
+	if (bind(zdfd, (struct sockaddr *)&zdsock, sizeof(zdsock)) < 0)
+	    ret = 1;
+	else if (getsockname(zdfd, (struct sockaddr *)&zdsock, &len) < 0)
+	    ret = 2;
+	else if (listen(zdfd, 1) < 0)
+	    ret = 3;
+	else
+	    ret = 0;
+
+	if (ret) {
+	    zwarnnam(name, "failure on data socket: %s: %e",
+		     ret == 3 ? "listen" : ret == 2 ? "getsockname" : "bind",
+		     errno);
+	    zfclosedata();
+	    return 1;
+	}
+
+	addr = (unsigned char *) &zdsock.sin_addr;
+	port = (unsigned char *) &zdsock.sin_port;
+	sprintf(portcmd, "PORT %d,%d,%d,%d,%d,%d\r\n",
+		addr[0],addr[1],addr[2],addr[3],port[0],port[1]);
+	if (zfsendcmd(portcmd) >= 5) {
+	    zwarnnam(name, "port command failed", NULL, 0);
+	    zfclosedata();
+	    return 1;
+	}
+	zfpassive_conn = 0;
+    }
+
+    return 0;
+}
+
+/* Close the data connection. */
+
+/**/
+static void
+zfclosedata(void)
+{
+    if (zdfd == -1)
+	return;
+    close(zdfd);
+    zdfd = -1;
+}
+
+/*
+ * Set up a data connection and use cmd to initiate a transfer.
+ * The actual data fd will be zdfd; the calling routine
+ * must handle the data itself.
+ * rest is a REST command to specify starting somewhere other
+ * then the start of the remote file.
+ * getsize is non-zero if we want to try to find the number
+ * of bytes in the reply to a RETR command.
+ *
+ * Return 0 on success, 1 on failure.
+ */
+
+/**/
+static int
+zfgetdata(char *name, char *rest, char *cmd, int getsize)
+{
+    int len, newfd;
+
+    if (zfopendata(name))
+	return 1;
+
+    /*
+     * Set position in remote file for get/put.
+     * According to RFC959, the restart command needs something
+     * called a marker which has previously been put into the data.
+     * Luckily for the real world, UNIX machines just interpret this
+     * as an offset into the byte stream.
+     *
+     * This has to be sent immediately before the data transfer, i.e.
+     * after all mucking around with types and sizes and so on.
+     */
+    if (rest && zfsendcmd(rest) > 3) {
+	zfclosedata();
+	return 1;
+    }
+
+    if (zfsendcmd(cmd) > 2) {
+	zfclosedata();
+	return 1;
+    }
+    if (getsize || (!(zfstatus & ZFST_TRSZ) && !strncmp(cmd, "RETR", 4))) {
+	/*
+	 * See if we got something like:
+	 *   Opening data connection for nortypix.gif (1234567 bytes).
+	 * On the first RETR, always see if this works,  Then we
+	 * can avoid sending a special SIZE command beforehand.
+	 */
+	char *ptr = strstr(lastmsg, "bytes");
+	zfstatus |= ZFST_NOSZ|ZFST_TRSZ;
+	if (ptr) {
+	    while (ptr > lastmsg && !isdigit(*ptr))
+		ptr--;
+	    while (ptr > lastmsg && isdigit(ptr[-1]))
+		ptr--;
+	    if (isdigit(*ptr)) {
+		zfstatus &= ~ZFST_NOSZ;
+		if (getsize) {
+		    long sz = zstrtol(ptr, NULL, 10);
+		    zfsetparam("ZFTP_SIZE", &sz, ZFPM_READONLY|ZFPM_INTEGER);
+		}
+	    }
+	}
+    }
+
+    if (!zfpassive_conn) {
+	/*
+	 * the current zdfd is the socket we opened, but we need
+	 * to let the server set up a different fd for reading/writing.
+	 * then we can close the fd we were listening for a connection on.
+	 * don't expect me to understand this, i'm only the programmer.
+	 */
+
+	/* accept the connection */
+	len = sizeof(zdsock);
+	newfd = zfmovefd(accept(zdfd, (struct sockaddr *)&zdsock, &len));
+	zfclosedata();
+	if (newfd < 0) {
+	    zwarnnam(name, "unable to accept data.", NULL, 0);
+	    return 1;
+	}
+	zdfd = newfd;		/* this is now the actual data fd */
+    }
+
+
+    /* more options, just to look professional */
+#ifdef SO_LINGER
+    /*
+     * Since data can take arbitrary amounts of time to arrive,
+     * the socket can be made to hang around until it doesn't think
+     * anything is arriving.
+     *
+     * In BSD 4.3, you could only linger for infinity.  Don't
+     * know if this has changed.
+     */
+    {
+	struct linger li;
+
+	li.l_onoff = 1;
+	li.l_linger = 120;
+	setsockopt(zdfd, SOL_SOCKET, SO_LINGER, (char *)&li, sizeof(li));
+    }
+#endif
+#if defined(IP_TOS) && defined(IPTOS_THROUGHPUT)
+    /* try to get high throughput, snigger */
+    {
+	int arg = IPTOS_THROUGHPUT;
+	setsockopt(zdfd, IPPROTO_IP, IP_TOS, (char *)&arg, sizeof(arg));
+    }
+#endif
+#if defined(F_SETFD) && defined(FD_CLOEXEC)
+	/* If the shell execs a program, we don't want this fd left open. */
+	len = FD_CLOEXEC;
+	fcntl(zdfd, F_SETFD, &len);
+#endif
+
+    return 0;
+}
+
+/*
+ * Find out about a local or remote file and pass back the information.
+ *
+ * We could jigger this to use ls like ncftp does as a backup.
+ * But if the server is non-standard enough not to have SIZE and MDTM,
+ * there's a very good chance ls -l isn't going to do great things.
+ *
+ * if fd is >= 0, it is used for an fstat when remote is zero:
+ * this is because on a put we are taking input from fd 0.
+ */
+
+/**/
+static int
+zfstats(char *fnam, int remote, long *retsize, char **retmdtm, int fd)
+{
+    long sz = -1;
+    char *mt = NULL;
+    int ret;
+
+    if (retsize)
+	*retsize = -1;
+    if (retmdtm)
+	*retmdtm = NULL;
+    if (remote) {
+	char *cmd;
+	if ((zfhas_size == ZFCP_NOPE && retsize) ||
+	    (zfhas_mdtm == ZFCP_NOPE && retmdtm))
+	    return 2;
+
+	/*
+	 * File is coming from over there.
+	 * Make sure we get the type right.
+	 */
+	zfsettype(ZFST_TYPE(zfstatus));
+	if (retsize) {
+	    cmd = tricat("SIZE ", fnam, "\r\n");
+	    ret = zfsendcmd(cmd);
+	    zsfree(cmd);
+	    if (ret == 6)
+		return 1;
+	    else if (lastcode < 300) {
+		sz = zstrtol(lastmsg, 0, 10);
+		zfhas_size = ZFCP_YUPP;
+	    } else if (lastcode >= 500 && lastcode <= 504) {
+		zfhas_size = ZFCP_NOPE;
+		return 2;
+	    } else if (lastcode == 550)
+		return 1;
+	    /* if we got a 550 from SIZE, the file doesn't exist */
+	}
+
+	if (retmdtm) {
+	    cmd = tricat("MDTM ", fnam, "\r\n");
+	    ret = zfsendcmd(cmd);
+	    zsfree(cmd);
+	    if (ret == 6)
+		return 1;
+	    else if (lastcode < 300) {
+		mt = ztrdup(lastmsg);
+		zfhas_mdtm = ZFCP_YUPP;
+	    } else if (lastcode >= 500 && lastcode <= 504) {
+		zfhas_mdtm = ZFCP_NOPE;
+		return 2;
+	    } else if (lastcode == 550)
+		return 1;
+	}
+    } else {
+	/* File is over here */
+	struct stat statbuf;
+	struct tm *tm;
+	char tmbuf[20];
+
+	if ((fd == -1 ? stat(fnam, &statbuf) : fstat(fd, &statbuf)) < 0)
+	    return 1;
+	/* make sure it's long, since this has to be a pointer */
+	sz = statbuf.st_size;
+
+	if (retmdtm) {
+	    /* use gmtime() rather than localtime() for consistency */
+	    tm = gmtime(&statbuf.st_mtime);
+	    /*
+	     * FTP format for data is YYYYMMDDHHMMSS
+	     * Using tm directly is easier than worrying about
+	     * incompatible strftime()'s.
+	     */
+	    sprintf(tmbuf, "%04d%02d%02d%02d%02d%02d",
+		    tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday,
+		    tm->tm_hour, tm->tm_min, tm->tm_sec);
+	    mt = ztrdup(tmbuf);
+	}
+    }
+    if (retsize)
+	*retsize = sz;
+    if (retmdtm)
+	*retmdtm = mt;
+    return 0;
+}
+
+/* Set parameters to say what's coming */
+
+/**/
+static void
+zfstarttrans(char *nam, int recv, long sz)
+{
+    long cnt = 0;
+    /*
+     * sz = -1 signifies error getting size.  don't set ZFTP_SIZE if sz is
+     * zero, either: it probably came from an fstat() on a pipe, so it
+     * means we don't know and shouldn't tell the user porkies.
+     */
+    if (sz > 0)
+	zfsetparam("ZFTP_SIZE", &sz, ZFPM_READONLY|ZFPM_INTEGER);
+    zfsetparam("ZFTP_FILE", ztrdup(nam), ZFPM_READONLY);
+    zfsetparam("ZFTP_TRANSFER", ztrdup(recv ? "G" : "P"), ZFPM_READONLY);
+    zfsetparam("ZFTP_COUNT", &cnt, ZFPM_READONLY|ZFPM_INTEGER);
+}
+
+/* Tidy up afterwards */
+
+/**/
+static void
+zfendtrans()
+{
+    zfunsetparam("ZFTP_SIZE");
+    zfunsetparam("ZFTP_FILE");
+    zfunsetparam("ZFTP_TRANSFER");
+    zfunsetparam("ZFTP_COUNT");
+}
+
+/* Read with timeout if recv is set. */
+
+/**/
+static int
+zfread(int fd, char *bf, size_t sz, int tmout)
+{
+    int ret;
+
+    if (!tmout)
+	return read(fd, bf, sz);
+
+    if (setjmp(zfalrmbuf)) {
+	alarm(0);
+	zwarnnam("zftp", "timeout on network read", NULL, 0);
+	return -1;
+    }
+    zfalarm(tmout);
+
+    ret = read(fd, bf, sz);
+
+    /* we don't bother turning off the whole alarm mechanism here */
+    alarm(0);
+    return ret;
+}
+
+/* Write with timeout if recv is not set. */
+
+/**/
+static int
+zfwrite(int fd, char *bf, size_t sz, int tmout)
+{
+    int ret;
+
+    if (!tmout)
+	return write(fd, bf, sz);
+
+    if (setjmp(zfalrmbuf)) {
+	alarm(0);
+	zwarnnam("zftp", "timeout on network write", NULL, 0);
+	return -1;
+    }
+    zfalarm(tmout);
+
+    ret = write(fd, bf, sz);
+
+    /* we don't bother turning off the whole alarm mechanism here */
+    alarm(0);
+    return ret;
+}
+
+static int zfread_eof;
+
+/* Version of zfread when we need to read in block mode. */
+
+/**/
+static int
+zfread_block(int fd, char *bf, size_t sz, int tmout)
+{
+    int n;
+    struct zfheader hdr;
+    size_t blksz, cnt;
+    char *bfptr;
+    do {
+	/* we need the header */
+	do {
+	    n = zfread(fd, (char *)&hdr, sizeof(hdr), tmout);
+	} while (n < 0 && errno == EINTR);
+	if (n != 3 && !zfdrrrring) {
+	    zwarnnam("zftp", "failed to read FTP block header", NULL, 0);
+	    return n;
+	}
+	/* size is stored in network byte order */
+	if (hdr.flags & ZFHD_EOFB)
+	    zfread_eof = 1;
+	blksz = (hdr.bytes[0] << 8) | hdr.bytes[1];
+	if (blksz > sz) {
+	    /*
+	     * See comments in file headers
+	     */
+	    zwarnnam("zftp", "block too large to handle", NULL, 0);
+	    errno = EIO;
+	    return -1;
+	}
+	bfptr = bf;
+	cnt = blksz;
+	while (cnt) {
+	    n = zfread(fd, bfptr, cnt, tmout);
+	    if (n > 0) {
+		bfptr += n;
+		cnt -= n;
+	    } else if (n < 0 && (errflag || zfdrrrring || errno != EINTR))
+		return n;
+	    else
+		break;
+	}
+	if (cnt) {
+	    zwarnnam("zftp", "short data block", NULL, 0);
+	    errno = EIO;
+	    return -1;
+	}
+    } while ((hdr.flags & ZFHD_MARK) && !zfread_eof);
+    return (hdr.flags & ZFHD_MARK) ? 0 : blksz;
+}
+
+/* Version of zfwrite when we need to write in block mode. */
+
+/**/
+static int
+zfwrite_block(int fd, char *bf, size_t sz, int tmout)
+{
+    int n;
+    struct zfheader hdr;
+    size_t cnt;
+    char *bfptr;
+    /* we need the header */
+    do {
+	hdr.bytes[0] = (sz & 0xff00) >> 8;
+	hdr.bytes[1] = sz & 0xff;
+	hdr.flags = sz ? 0 : ZFHD_EOFB;
+	n = zfwrite(fd, (char *)&hdr, sizeof(hdr), tmout);
+    } while (n < 0 && errno == EINTR);
+    if (n != 3 && !zfdrrrring) {
+	zwarnnam("zftp", "failed to write FTP block header", NULL, 0);
+	return n;
+    }
+    bfptr = bf;
+    cnt = sz;
+    while (cnt) {
+	n = zfwrite(fd, bfptr, cnt, tmout);
+	if (n > 0) {
+	    bfptr += n;
+	    cnt -= n;
+	} else if (n < 0 && (errflag || zfdrrrring || errno != EINTR))
+	    return n;
+    }
+
+    return sz;
+}
+
+/*
+ * Move stuff from fdin to fdout, tidying up the data connection
+ * when finished.  The data connection could be either input or output:
+ * recv is 1 for receiving a file, 0 for sending.
+ *
+ * progress is 1 to use a progress meter.
+ * startat says how far in we're starting with a REST command.
+ *
+ * Since we're doing some buffering here anyway, we don't bother
+ * with a stdio layer.
+ */
+
+/**/
+static int
+zfsenddata(char *name, int recv, int progress, long startat)
+{
+#define ZF_BUFSIZE 32768
+#define ZF_ASCSIZE (ZF_BUFSIZE/2)
+    /* ret = 2 signals the local read/write failed, so send abort */
+    int n, ret = 0, gotack = 0, fdin, fdout, fromasc = 0, toasc = 0;
+    int rtmout = 0, wtmout = 0;
+    char lsbuf[ZF_BUFSIZE], *ascbuf = NULL, *optr;
+    long sofar = 0, last_sofar = 0;
+    readwrite_t read_ptr = zfread, write_ptr = zfwrite;
+    List l;
+
+    if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+	/*
+	 * progress to set up:  ZFTP_COUNT is zero.
+	 * We do this here in case we needed to wait for a RETR
+	 * command to tell us how many bytes are coming.
+	 */
+	doshfunc("zftp_progress", l, NULL, 0, 1);
+	/* Now add in the bit of the file we've got/sent already */
+	sofar = last_sofar = startat;
+    }
+    if (recv) {
+	fdin = zdfd;
+	fdout = 1;
+	rtmout = getiparam("ZFTP_TMOUT");
+	if (ZFST_CTYP(zfstatus) == ZFST_ASCI)
+	    fromasc = 1;
+	if (ZFST_MODE(zfstatus) == ZFST_BLOC)
+	    read_ptr = zfread_block;
+    } else {
+	fdin = 0;
+	fdout = zdfd;
+	wtmout = getiparam("ZFTP_TMOUT");
+	if (ZFST_CTYP(zfstatus) == ZFST_ASCI)
+	    toasc = 1;
+	if (ZFST_MODE(zfstatus) == ZFST_BLOC)
+	    write_ptr = zfwrite_block;
+    }
+
+    if (toasc)
+	ascbuf = zalloc(ZF_ASCSIZE);
+    zfpipe();
+    zfread_eof = 0;
+    while (!ret && !zfread_eof) {
+	n = (toasc) ? read_ptr(fdin, ascbuf, ZF_ASCSIZE, rtmout)
+	    : read_ptr(fdin, lsbuf, ZF_BUFSIZE, rtmout);
+	if (n > 0) {
+	    char *iptr;
+	    if (toasc) {
+		/* \n -> \r\n it shouldn't happen to a dog. */
+		char *iptr = ascbuf, *optr = lsbuf;
+		int cnt = n;
+		while (cnt--) {
+		    if (*iptr == '\n') {
+			*optr++ = '\r';
+			n++;
+		    }
+		    *optr++ = *iptr++;
+		}
+	    }
+	    if (fromasc && (iptr = memchr(lsbuf, '\r', n))) {
+		/* \r\n -> \n */
+		char *optr = iptr;
+		int cnt = n - (iptr - lsbuf);
+		while (cnt--) {
+		    if (*iptr != '\r' || iptr[1] != '\n') {
+			*optr++ = *iptr;
+		    } else
+			n--;
+		    iptr++;
+		}
+	    }
+	    optr = lsbuf;
+
+	    sofar += n;
+
+	    for (;;) {
+		/*
+		 * in principle, write can be interrupted after
+		 * safely writing some bytes, and will return the
+		 * number already written, which may not be the
+		 * complete buffer.  so make this robust.  they call me
+		 * `robustness stephenson'.  in my dreams.
+		 */
+		int newn = write_ptr(fdout, optr, n, wtmout);
+		if (newn == n)
+		    break;
+		if (newn < 0) {
+		    /*
+		     * The somewhat contorted test here (for write)
+		     * and below (for read) means:
+		     * real error if
+		     *  - errno is set and it's not just an interrupt, or
+		     *  - errflag is set, probably due to CTRL-c, or
+		     *  - zfdrrrring is set, due to the alarm going off.
+		     * print an error message if
+		     *  - not a timeout, since that was reported, and
+		     *    either
+		     *    - a non-interactive shell, where we don't
+		     *      know what happened otherwise
+		     *    - or both of
+		     *      - not errflag, i.e. CTRL-c or what have you,
+		     *        since the user probably knows about that, and
+		     *      - not a SIGPIPE, since usually people are
+		     *        silent about those when going to pagers
+		     *        (if you quit less or more in the middle
+		     *        and see an error message you think `I
+		     *        shouldn't have done that').
+		     *
+		     * If we didn't print an error message here,
+		     * and were going to do an abort (ret == 2)
+		     * because the error happened on the local end
+		     * of the connection, set ret to 3 and don't print
+		     * the 'aborting...' either.
+		     *
+		     * There must be a better way of doing this.
+		     */
+		    if (errno != EINTR || errflag || zfdrrrring) {
+			if (!zfdrrrring &&
+			    (!interact || (!errflag && errno != EPIPE))) {
+			    ret = recv ? 2 : 1;
+			    zwarnnam(name, "write failed: %e", NULL, errno);
+			} else
+			    ret = recv ? 3 : 1;
+			break;
+		    }
+		    continue;
+		}
+		optr += newn;
+		n -= newn;
+	    }
+	} else if (n < 0) {
+	    if (errno != EINTR || errflag || zfdrrrring) {
+		if (!zfdrrrring &&
+		    (!interact || (!errflag && errno != EPIPE))) {
+		    ret = recv ? 1 : 2;
+		    zwarnnam(name, "read failed: %e", NULL, errno);
+		} else
+		    ret = recv ? 1 : 3;
+		break;
+	    }
+	} else
+	    break;
+	if (!ret && sofar != last_sofar && progress &&
+	    (l = getshfunc("zftp_progress")) != &dummy_list) {
+	    zfsetparam("ZFTP_COUNT", &sofar, ZFPM_READONLY|ZFPM_INTEGER);
+	    doshfunc("zftp_progress", l, NULL, 0, 1);
+	    last_sofar = sofar;
+	}
+    }
+    zfunpipe();
+    /*
+     * At this point any timeout was on the data connection,
+     * so we don't need to force the control connection to close.
+     */
+    zfdrrrring = 0;
+    if (!errflag && !ret && !recv && ZFST_MODE(zfstatus) == ZFST_BLOC) {
+	/* send an end-of-file marker block */
+	ret = (zfwrite_block(fdout, lsbuf, 0, wtmout) < 0);
+    }
+    if (errflag || ret > 1) {
+	/*
+	 * some error occurred, maybe a keyboard interrupt, or
+	 * a local file/pipe handling problem.
+	 * send an abort.
+	 *
+	 * safest to block all signals here?  can get frustrating if
+	 * we're waiting for an abort.  don't I know.  let's start
+	 * off just by blocking SIGINT's.
+	 *
+	 * maybe the timeout for the abort should be shorter than
+	 * for normal commands.  and what about aborting after
+	 * we had a timeout on the data connection, is that
+	 * really a good idea?
+	 */
+	/* RFC 959 says this is what to send */
+	unsigned char msg[4] = { IAC, IP, IAC, SYNCH };
+
+	if (ret == 2)
+	    zwarnnam(name, "aborting data transfer...", NULL, 0);
+
+	holdintr();
+
+	/* the following is black magic, as far as I'm concerned. */
+	/* what are we going to do if it fails?  not a lot, actually. */
+	send(zcfd, (char *)msg, 3, 0);
+	send(zcfd, (char *)msg+3, 1, MSG_OOB);
+
+	zfsendcmd("ABOR\r\n");
+	if (lastcode == 226) {
+	    /*
+	     * 226 is supposed to mean the transfer got sent OK after
+	     * all, and the abort got ignored, at least that's what
+	     * rfc959 seems to be saying.  but in fact what can happen
+	     * is the transfer finishes (at least as far as the
+	     * server's concerned) and it's response is waiting, then
+	     * the abort gets sent, and we need to mop up a response to
+	     * that.  so actually in most cases we get two replies
+	     * anyway.  we could test if we had select() on all hosts.
+	     */
+	    /* gotack = 1; */
+	    /*
+	     * we'd better leave errflag, since we don't know
+	     * where it came from.  maybe the user wants to abort
+	     * a whole script or function.
+	     */
+	} else
+	    ret = 1;
+
+	noholdintr();
+    }
+	
+    if (toasc)
+	zfree(ascbuf, ZF_ASCSIZE);
+    zfclosedata();
+    if (!gotack && zfgetmsg() > 2)
+	ret = 1;
+    return ret != 0;
+}
+
+/* Open a new control connection, i.e. start a new FTP session */
+
+/**/
+static int
+zftp_open(char *name, char **args, int flags)
+{
+    struct in_addr ipaddr;
+    struct protoent *zprotop;
+    struct servent *zservp;
+    struct hostent *zhostp = NULL;
+    char **addrp, tbuf[2] = "X", *fname;
+    int err, len, tmout;
+
+    if (!*args) {
+	if (zfuserparams)
+	    args = zfuserparams;
+	else {
+	    zwarnnam(name, "no host specified", NULL, 0);
+	    return 1;
+	}
+    }
+
+    /*
+     * Close the existing connection if any.
+     * Probably this is the safest thing to do.  It's possible
+     * a `QUIT' will hang, though.
+     */
+    if (zcfd != -1)
+	zfclose();
+
+    /* this is going to give 0.  why bother? */
+    zprotop = getprotobyname("tcp");
+    zservp = getservbyname("ftp", "tcp");
+
+    if (!zprotop || !zservp) {
+	zwarnnam(name, "Somebody stole FTP!", NULL, 0);
+	return 1;
+    }
+
+    /* don't try talking to server yet */
+    zcfinish = 2;
+
+    /*
+     * This sets an alarm for the whole process, getting the host name
+     * as well as connecting.  Arguably you could time them out separately. 
+     */
+    tmout = getiparam("ZFTP_TMOUT");
+    if (setjmp(zfalrmbuf)) {
+	char *hname;
+	alarm(0);
+	if ((hname = getsparam("ZFTP_HOST")) && *hname) 
+	    zwarnnam(name, "timeout connecting to %s", hname, 0);
+	else
+	    zwarnnam(name, "timeout on host name lookup", NULL, 0);
+	zfclose();
+	return 1;
+    }
+    zfalarm(tmout);
+
+    /*
+     * Now this is what I like.  A library which provides decent higher
+     * level functions to do things like converting address types.  It saves
+     * so much trouble.  Pity about the rest of the network interface, though.
+     */
+    ipaddr.s_addr = inet_addr(args[0]);
+    if (ipaddr.s_addr != INADDR_NONE) {
+	/*
+	 * hmmm, we don't necessarily want to do this... maybe the
+	 * user is actively trying to avoid a bad nameserver.
+	 * perhaps better just to set ZFTP_HOST to the dot address, too.
+	 * that way shell functions know how it was opened.
+	 *
+	 * 	zhostp = gethostbyaddr(&ipaddr, sizeof(ipaddr), AF_INET);
+	 *
+	 * or, we could have a `no_lookup' flag.
+	 */
+	zfsetparam("ZFTP_HOST", ztrdup(args[0]), ZFPM_READONLY);
+	zsock.sin_family = AF_INET;
+    } else {
+	zhostp = gethostbyname(args[0]);
+	if (!zhostp || errflag) {
+	    /* should use herror() here if available, but maybe
+	     * needs configure test. on AIX it's present but not
+	     * in headers.
+	     */
+	    zwarnnam(name, "host not found: %s", args[0], 0);
+	    alarm(0);
+	    return 1;
+	}
+	zsock.sin_family = zhostp->h_addrtype;
+	zfsetparam("ZFTP_HOST", ztrdup(zhostp->h_name), ZFPM_READONLY);
+    }
+
+    zsock.sin_port = ntohs(zservp->s_port);
+    zcfd = zfmovefd(socket(zsock.sin_family, SOCK_STREAM, 0));
+    if (zcfd < 0) {
+	zwarnnam(name, "socket failed: %e", NULL, errno);
+	zfunsetparam("ZFTP_HOST");
+	alarm(0);
+	return 1;
+    }
+
+#if defined(F_SETFD) && defined(FD_CLOEXEC)
+    /* If the shell execs a program, we don't want this fd left open. */
+    len = FD_CLOEXEC;
+    fcntl(zcfd, F_SETFD, &len);
+#endif
+
+    /*
+     * now connect the socket.  manual pages all say things like `this is all
+     * explained oh-so-wonderfully in some other manual page'.  not.
+     */
+
+    err = 1;
+
+    if (ipaddr.s_addr != INADDR_NONE) {
+	/* dot address */
+	memcpy(&zsock.sin_addr, &ipaddr, sizeof(ipaddr));
+	do {
+	    err = connect(zcfd, (struct sockaddr *)&zsock, sizeof(zsock));
+	} while (err && errno == EINTR && !errflag);
+    } else {
+	/* host name: try all possible IP's */
+	for (addrp = zhostp->h_addr_list; *addrp; addrp++) {
+	    memcpy(&zsock.sin_addr, *addrp, zhostp->h_length);
+	    do {
+		err = connect(zcfd, (struct sockaddr *)&zsock, sizeof(zsock));
+	    } while (err && errno == EINTR && !errflag);
+	    /* you can check whether it's worth retrying here */
+	}
+    }
+
+    alarm(0);
+
+    if (err < 0) {
+	zwarnnam(name, "connect failed: %e", NULL, errno);
+	zfclose();
+	return 1;
+    }
+    zfsetparam("ZFTP_IP", ztrdup(inet_ntoa(zsock.sin_addr)), ZFPM_READONLY);
+    /* now we can talk to the control connection */
+    zcfinish = 0;
+
+    len = sizeof(zsock);
+    if (getsockname(zcfd, (struct sockaddr *)&zsock, &len) < 0) {
+	zwarnnam(name, "getsockname failed: %e", NULL, errno);
+	zfclose();
+	return 1;
+    }
+    /* nice to get some options right, ignore if they don't work */
+#ifdef SO_OOBINLINE
+    /*
+     * this says we want messages in line.  maybe sophisticated people
+     * do clever things with SIGURG.
+     */
+    len = 1;
+    setsockopt(zcfd, SOL_SOCKET, SO_OOBINLINE, (char *)&len, sizeof(len));
+#endif
+#if defined(IP_TOS) && defined(IPTOS_LOWDELAY)
+    /* for control connection we want low delay.  please don't laugh. */
+    len = IPTOS_LOWDELAY;
+    setsockopt(zcfd, IPPROTO_IP, IP_TOS, (char *)&len, sizeof(len));
+#endif
+
+    /*
+     * We use stdio with line buffering for convenience on input.
+     * On output, we can just dump a complete message to the fd via write().
+     */
+    zcin = fdopen(zcfd, "r");
+
+    if (!zcin) {
+	zwarnnam(name, "file handling error", NULL, 0);
+	zfclose();
+	return 1;
+    }
+
+#ifdef _IONBF
+    setvbuf(zcin, NULL, _IONBF, 0);
+#else
+    setlinebuf(zcin);
+#endif
+
+    /*
+     * now see what the remote server has to say about that.
+     */
+    if (zfgetmsg() >= 4) {
+	zfclose();
+	return 1;
+    }
+
+    zfis_unix = 0;
+    zfhas_size = zfhas_mdtm = ZFCP_UNKN;
+    zdfd = -1;
+    /* initial status: open, ASCII data, stream mode 'n' stuff */
+    zfstatus = 0;
+
+    /* open file for saving the current status */
+    fname = gettempname();
+    zfstatfd = open(fname, O_RDWR|O_CREAT, 0600);
+    DPUTS(zfstatfd == -1, "zfstatfd not created");
+#if defined(F_SETFD) && defined(FD_CLOEXEC)
+    /* If the shell execs a program, we don't want this fd left open. */
+    len = FD_CLOEXEC;
+    fcntl(zfstatfd, F_SETFD, &len);
+#endif
+    unlink(fname);
+
+    /* now find out what system we're connected to */
+    if (!(zfprefs & ZFPF_DUMB) && zfsendcmd("SYST\r\n") == 2) {
+	char *ptr = lastmsg, *eptr, *systype;
+	for (eptr = ptr; *eptr; eptr++)
+	    ;
+	systype = ztrduppfx(ptr, eptr-ptr);
+	if (!strncmp(systype, "UNIX Type: L8", 13)) {
+	    /*
+	     * Use binary for transfers.  This simple test saves much
+	     * hassle for all concerned, particularly me.
+	     */
+	    zfstatus |= ZFST_IMAG;
+	    zfis_unix = 1;
+	}
+	/*
+	 * we could set zfis_unix based just on the UNIX part,
+	 * but I don't really know the consequences of that.
+	 */
+	zfsetparam("ZFTP_SYSTEM", systype, ZFPM_READONLY);
+    } else if (zcfd == -1) {
+	/* final paranoid check */
+	return 1;
+    }
+	
+    tbuf[0] = (ZFST_TYPE(zfstatus) == ZFST_ASCI) ? 'A' : 'I';
+    zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
+    zfsetparam("ZFTP_MODE", ztrdup("S"), ZFPM_READONLY);
+    /* if remaining arguments, use them to log in. */
+    if (zcfd > -1 && *++args)
+	return zftp_login(name, args, flags);
+    /* if something wayward happened, connection was already closed */
+    return zcfd == -1;
+}
+
+/*
+ * Read a parameter string, with a prompt if reading from stdin.
+ * The returned string is on the heap.
+ * If noecho, turn off ECHO mode while reading.
+ */
+
+/**/
+static char *
+zfgetinfo(char *prompt, int noecho)
+{
+    int resettty = 0;
+    /* 256 characters should be enough, hardly worth allocating
+     * a password string byte by byte
+     */
+    char instr[256], *strret;
+    int len;
+
+    /*
+     * Only print the prompt if getting info from a tty.  Of
+     * course, we don't know if stderr has been redirected, but
+     * that seems a minor point.
+     */
+    if (isatty(0)) {
+	if (noecho) {
+	    /* hmmm... all this great big shell and we have to read
+	     * something with no echo by ourselves.
+	     * bin_read() is far to complicated for our needs.
+	     * we could use zread(), but that relies on static
+	     * variables, so someone doesn't want that to happen.
+	     *
+	     * this is modified from setcbreak() in utils.c,
+	     * except I don't see any point in using cbreak mode
+	     */
+	    struct ttyinfo ti;
+
+	    ti = shttyinfo;
+#ifdef HAS_TIO
+	    ti.tio.c_lflag &= ~ECHO;
+#else
+	    ti.sgttyb.sg_flags &= ~ECHO;
+#endif
+	    settyinfo(&ti);
+	    resettty = 1;
+	}
+	fflush(stdin);
+	fputs(prompt, stderr);
+	fflush(stderr);
+    }
+
+    fgets(instr, 256, stdin);
+    if (instr[len = strlen(instr)-1] == '\n')
+	instr[len] = '\0';
+
+    strret = dupstring(instr);
+
+    if (resettty) {
+	/* '\n' didn't get echoed */
+	fputc('\n', stdout);
+	fflush(stdout);
+    	settyinfo(&shttyinfo);
+    }
+
+    return strret;
+}
+
+/*
+ * set params for an open with no arguments.
+ * this allows easy re-opens.
+ */
+
+/**/
+static int
+zftp_params(char *name, char **args, int flags)
+{
+    char *prompts[] = { "Host: ", "User: ", "Password: ", "Account: " };
+    char **aptr, **newarr;
+    int i, j, len;
+
+    if (!*args) {
+	if (zfuserparams) {
+	    for (aptr = zfuserparams, i = 0; *aptr; aptr++, i++) {
+		if (i == 2) {
+		    len = strlen(*aptr);
+		    for (j = 0; j < len; j++)
+			fputc('*', stdout);
+		    fputc('\n', stdout);
+		} else
+		    fprintf(stdout, "%s\n", *aptr);
+	    }
+	}
+	return 0;
+    }
+    if (!strcmp(*args, "-")) {
+	if (zfuserparams)
+	    freearray(zfuserparams);
+	zfuserparams = 0;
+	return 0;
+    }
+    len = arrlen(args);
+    newarr = (char **)zcalloc((len+1)*sizeof(char *));
+    for (aptr = args, i = 0; *aptr && !errflag; aptr++, i++) {
+	char *str;
+	if (!strcmp(*aptr, "?"))
+	    str = zfgetinfo(prompts[i], i == 2);
+	else
+	    str = *aptr;
+	newarr[i] = ztrdup(str);
+    }
+    if (errflag) {
+	/* maybe user CTRL-c'd in the middle somewhere */
+	for (aptr = newarr; *aptr; aptr++)
+	    zsfree(*aptr);
+	zfree(newarr, len+1);
+	return 1;
+    }
+    if (zfuserparams)
+	freearray(zfuserparams);
+    zfuserparams = newarr;
+    return 0;
+}
+
+/* login a user:  often called as part of the open sequence */
+
+/**/
+static int
+zftp_login(char *name, char **args, int flags)
+{
+    char *ucmd, *passwd = NULL, *acct = NULL;
+    char *user;
+    int stopit;
+
+    if ((zfstatus & ZFST_LOGI) && zfsendcmd("REIN\r\n") >= 4)
+	return 1;
+
+    zfstatus &= ~ZFST_LOGI;
+    if (*args) {
+	user = *args++;
+    } else {
+	user = zfgetinfo("User: ", 0);
+    }
+
+    ucmd = tricat("USER ", user, "\r\n");
+    stopit = 0;
+
+    if (zfsendcmd(ucmd) == 6)
+	stopit = 2;
+
+    while (!stopit && !errflag) {
+	switch (lastcode) {
+	case 230: /* user logged in */
+	case 202: /* command not implemented, don't care */
+	    stopit = 1;
+	    break;
+
+	case 331: /* need password */
+	    if (*args)
+		passwd = *args++;
+	    else
+		passwd = zfgetinfo("Password: ", 1);
+	    zsfree(ucmd);
+	    ucmd = tricat("PASS ", passwd, "\r\n");
+	    if (zfsendcmd(ucmd) == 6)
+		stopit = 2;
+	    break;
+
+	case 332: /* need account */
+	case 532:
+	    if (*args)
+		acct = *args++;
+	    else
+		acct = zfgetinfo("Account: ", 0);
+	    zsfree(ucmd);
+	    ucmd = tricat("ACCT ", passwd, "\r\n");
+	    if (zfsendcmd(ucmd) == 6)
+		stopit = 2;
+	    break;
+
+	case 421: /* service not available, so closed anyway */
+	case 501: /* syntax error */
+	case 503: /* bad commands */
+	case 530: /* not logged in */
+	case 550: /* random can't-do-that */
+	default:  /* whatever, should flag this as bad karma */
+	    /* need more diagnostics here */
+	    stopit = 2;
+	    break;
+	}
+    }
+
+    zsfree(ucmd);
+    if (zcfd == -1)
+	return 1;
+    if (stopit == 2 || (lastcode != 230 && lastcode != 202)) {
+	zwarnnam(name, "login failed", NULL, 0);
+	return 1;
+    }
+
+    if (*args) {
+	int cnt;
+	for (cnt = 0; *args; args++)
+	    cnt++;
+	zwarnnam(name, "warning: %d comand arguments not used\n", NULL, cnt);
+    }
+    zfstatus |= ZFST_LOGI;
+    zfsetparam("ZFTP_USER", ztrdup(user), ZFPM_READONLY);
+    if (acct)
+	zfsetparam("ZFTP_ACCOUNT", ztrdup(acct), ZFPM_READONLY);
+
+    /*
+     * Get the directory.  This is possibly an unnecessary overhead, of
+     * course, but when you're being driven by shell functions there's
+     * just no way of telling.
+     */
+    return zfgetcwd();
+}
+
+/* do ls or dir on the remote directory */
+
+/**/
+static int
+zftp_dir(char *name, char **args, int flags)
+{
+    /* maybe should be cleverer about handling arguments */
+    char *cmd;
+    int ret;
+
+    /*
+     * RFC959 says this must be ASCII or EBCDIC, not image format.
+     * I rather suspect on a UNIX server we get away handsomely
+     * with doing everything, including this, as image.
+     */
+    zfsettype(ZFST_ASCI);
+
+    cmd = zfargstring((flags & ZFTP_NLST) ? "NLST" : "LIST", args);
+    ret = zfgetdata(name, NULL, cmd, 0);
+    zsfree(cmd);
+    if (ret)
+	return 1;
+
+    fflush(stdout);		/* since we're now using fd 1 */
+    return zfsenddata(name, 1, 0, 0);
+}
+
+/* change the remote directory */
+
+/**/
+static int
+zftp_cd(char *name, char **args, int flags)
+{
+    /* change directory --- enhance to allow 'zftp cdup' */
+    int ret;
+
+    if ((flags & ZFTP_CDUP) || !strcmp(*args, "..") ||
+	!strcmp(*args, "../")) {
+	ret = zfsendcmd("CDUP\r\n");
+    } else {
+	char *cmd = tricat("CWD ", *args, "\r\n");
+	ret = zfsendcmd(cmd);
+	zsfree(cmd);
+    }
+    if (ret > 2)
+	return 1;
+    /* sometimes the directory may be in the response. usually not. */
+    if (zfgetcwd())
+	return 1;
+
+    return 0;
+}
+
+/* get the remote directory */
+
+/**/
+static int
+zfgetcwd(void)
+{
+    char *ptr, *eptr;
+    int endc;
+    List l;
+
+    if (zfprefs & ZFPF_DUMB)
+	return 1;
+    if (zfsendcmd("PWD\r\n") > 2) {
+	zfunsetparam("ZFTP_PWD");
+	return 1;
+    }
+    ptr = lastmsg;
+    while (*ptr == ' ')
+	ptr++;
+    if (!*ptr)			/* ultra safe */
+	return 1;
+    if (*ptr == '"') {
+	ptr++;
+	endc = '"';
+    } else 
+	endc = ' ';
+    for (eptr = ptr; *eptr && *eptr != endc; eptr++)
+	;
+    zfsetparam("ZFTP_PWD", ztrduppfx(ptr, eptr-ptr), ZFPM_READONLY);
+
+    /*
+     * This isn't so necessary if we're going to have a shell function
+     * front end.  By putting it here, and in close when ZFTP_PWD is unset,
+     * we at least cover the bases.
+     */
+    if ((l = getshfunc("zftp_chpwd")) != &dummy_list)
+	doshfunc("zftp_chpwd", l, NULL, 0, 1);
+
+    return 0;
+}
+
+/*
+ * Set the type for the next transfer, usually image (binary) or ASCII.
+ */
+
+/**/
+static int
+zfsettype(int type)
+{
+    char buf[] = "TYPE X\r\n";
+    if (ZFST_TYPE(type) == ZFST_CTYP(zfstatus))
+	return 0;
+    buf[5] = (ZFST_TYPE(type) == ZFST_ASCI) ? 'A' : 'I';
+    if (zfsendcmd(buf) > 2)
+	return 1;
+    zfstatus &= ~(ZFST_TMSK << ZFST_TBIT);
+    /* shift the type left to set the current type bits */;
+    zfstatus |= type << ZFST_TBIT;
+    return 0;
+}
+
+/*
+ * Print or get a new type for the transfer.
+ * We don't actually set the type at this point.
+ */
+
+/**/
+static int
+zftp_type(char *name, char **args, int flags)
+{
+    char *str, nt, tbuf[2] = "A";
+    if (flags & (ZFTP_TBIN|ZFTP_TASC)) {
+	nt = (flags & ZFTP_TBIN) ? 'I' : 'A';
+    } else if (!(str = *args)) {
+	/*
+	 * Since this is supposed to be a low-level basis for
+	 * an FTP system, just print the single code letter.
+	 */
+	printf("%c\n", (ZFST_TYPE(zfstatus) == ZFST_ASCI) ? 'A' : 'I');
+	fflush(stdout);
+	return 0;
+    } else {
+	nt = toupper(*str);
+	/*
+	 * RFC959 specifies other types, but these are the only
+	 * ones we know what to do with.
+	 */
+	if (str[1] || (nt != 'A' && nt != 'B' && nt != 'I')) {
+	    zwarnnam(name, "transfer type %s not recognised", str, 0);
+	    return 1;
+	}
+	
+	if (nt == 'B')		/* binary = image */
+	    nt = 'I';
+    }
+
+    zfstatus &= ~ZFST_TMSK;
+    zfstatus |= (nt == 'I') ? ZFST_IMAG : ZFST_ASCI;
+    tbuf[0] = nt;
+    zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
+    return 0;
+}
+
+/**/
+static int
+zftp_mode(char *name, char **args, int flags)
+{
+    char *str, cmd[] = "MODE X\r\n";
+    int nt;
+
+    if (!(str = *args)) {
+	printf("%c\n", (ZFST_MODE(zfstatus) == ZFST_STRE) ? 'S' : 'B');
+	fflush(stdout);
+	return 0;
+    }
+    nt = str[0] = toupper(*str);
+    if (str[1] || (nt != 'S' && nt != 'B')) {
+	zwarnnam(name, "transfer mode %s not recognised", str, 0);
+	return 1;
+    }
+    cmd[5] = (char) nt;
+    if (zfsendcmd(cmd) > 2)
+	return 1;
+    zfstatus &= ZFST_MMSK;
+    zfstatus |= (nt == 'S') ? ZFST_STRE : ZFST_BLOC;
+    zfsetparam("ZFTP_MODE", ztrdup(str), ZFPM_READONLY);
+    return 0;
+}
+
+/**/
+static int
+zftp_local(char *name, char **args, int flags)
+{
+    int more = !!args[1], ret = 0, dofd = !*args;
+    while (*args || dofd) {
+	long sz;
+	char *mt;
+	int newret = zfstats(*args, !(flags & ZFTP_HERE), &sz, &mt,
+			     dofd ? 0 : -1);
+	if (newret == 2)	/* at least one is not implemented */
+	    return 2;
+	else if (newret) {
+	    ret = 1;
+	    if (mt)
+		zsfree(mt);
+	    args++;
+	    continue;
+	}
+	if (more) {
+	    fputs(*args, stdout);
+	    fputc(' ', stdout);
+	}
+	printf("%ld %s\n", sz, mt);
+	zsfree(mt);
+	if (dofd)
+	    break;
+	args++;
+    }
+    fflush(stdout);
+
+    return ret;
+}
+
+/*
+ * Generic transfer for get, put and append.
+ *
+ * Get sends all files to stdout, i.e. this is basically cat. It's up to a
+ * shell function driver to turn this into standard FTP-like commands.
+ *
+ * Put/append sends everything from stdin down the drai^H^H^Hata connection. 
+ * Slightly weird with multiple files in that it tries to read
+ * a separate complete file from stdin each time, which is
+ * only even potentially useful interactively.  But the only
+ * real alternative is just to allow one file at a time.
+ */
+
+/**/
+static int
+zftp_getput(char *name, char **args, int flags)
+{
+    int ret = 0, recv = (flags & ZFTP_RECV), getsize = 0, progress = 1;
+    char *cmd = recv ? "RETR " : (flags & ZFTP_APPE) ? "APPE " : "STOR ";
+    List l;
+
+    /*
+     * At this point I'd like to set progress to 0 if we're
+     * backgrounded, since it's hard for the user to find out.
+     * It turns out it's hard enough for us to find out.
+     * The problem is that zsh clears it's job table, so we
+     * just don't know if we're some forked shell in a pipeline
+     * somewhere or in the background.  This seems to me a problem.
+     */
+
+    zfsettype(ZFST_TYPE(zfstatus));
+
+    if (recv)
+	fflush(stdout);		/* since we may be using fd 1 */
+    for (; *args; args++) {
+	char *ln, *rest = NULL;
+	long startat = 0;
+	if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+	    long sz;
+	    /*
+	     * This calls the SIZE command to get the size for remote
+	     * files.  Some servers send the size with the reply to
+	     * the transfer command (i.e. RETR), in which
+	     * case we note the fact and don't call this
+	     * next time.  For that reason, the first call
+	     * of zftp_progress is delayed until zfsenddata().
+	     */
+	    if ((!(zfprefs & ZFPF_DUMB) &&
+		 (zfstatus & (ZFST_NOSZ|ZFST_TRSZ)) != ZFST_TRSZ)
+		|| !recv) {
+		/* the final 0 is a local fd to fstat if recv is zero */
+		zfstats(*args, recv, &sz, NULL, 0);
+		/* even if it doesn't support SIZE, it may tell us */
+		if (recv && sz == -1)
+		    getsize = 1;
+	    } else
+		getsize = 1;
+	    zfstarttrans(*args, recv, sz);
+	}
+
+	if (flags & ZFTP_REST) {
+	    startat = zstrtol(args[1], NULL, 10);
+	    rest = tricat("REST ", args[1], "\r\n");
+	}
+
+	ln = tricat(cmd, *args, "\r\n");
+	/* note zdfd doesn't exist till zfgetdata() creates it */
+	if (zfgetdata(name, rest, ln, getsize) ||
+	    zfsenddata(name, recv, progress, startat))
+	    ret = 1;
+	zsfree(ln);
+	if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+	    /* progress to finish: ZFTP_TRANSFER set to GF or PF */
+	    zfsetparam("ZFTP_TRANSFER", ztrdup(recv ? "GF" : "PF"),
+		       ZFPM_READONLY);
+	    doshfunc("zftp_progress", l, NULL, 0, 1);
+	}
+	if (rest) {
+	    zsfree(rest);
+	    args++;
+	}
+	if (errflag)
+	    break;
+    }
+    zfendtrans();
+    return ret;
+}
+
+/*
+ * Delete a list of files on the server.  We allow a list by analogy with
+ * `rm'.
+ */
+
+/**/
+static int
+zftp_delete(char *name, char **args, int flags)
+{
+    int ret = 0;
+    char *cmd, **aptr;
+    for (aptr = args; *aptr; aptr++) {
+	cmd = tricat("DELE ", *aptr, "\r\n");
+	if (zfsendcmd(cmd) > 2)
+	    ret = 1;
+	zsfree(cmd);
+    }
+    return ret;
+}
+
+/* Create or remove a directory on the server */
+
+/**/
+static int
+zftp_mkdir(char *name, char **args, int flags)
+{
+    int ret;
+    char *cmd = tricat((flags & ZFTP_DELE) ? "RMD " : "MKD ",
+		       *args, "\r\n");
+    ret = (zfsendcmd(cmd) > 2);
+    zsfree(cmd);
+    return ret;
+}
+
+/* Rename a file on the server */
+
+/**/
+static int
+zftp_rename(char *name, char **args, int flags)
+{
+    int ret;
+    char *cmd;
+
+    cmd = tricat("RNFR ", args[0], "\r\n");
+    ret = 1;
+    if (zfsendcmd(cmd) == 3) {
+	zsfree(cmd);
+	cmd = tricat("RNTO ", args[1], "\r\n");
+	if (zfsendcmd(cmd) == 2)
+	    ret = 0;
+    }
+    zsfree(cmd);
+    return ret;
+}
+
+/*
+ * Send random commands, either with SITE or literal.
+ * In the second case, the user better know what they're doing.
+ */
+
+/**/
+static int
+zftp_quote(char *name, char **args, int flags)
+{
+    int ret = 0;
+    char *cmd;
+
+    cmd = (flags & ZFTP_SITE) ? zfargstring("SITE", args)
+	: zfargstring(args[0], args+1);
+    ret = (zfsendcmd(cmd) > 2);
+    zsfree(cmd);
+
+    return ret;
+}
+
+/* Close the connection, ending the session */
+
+/**/
+static int
+zftp_close(char *name, char **args, int flags)
+{
+    char **aptr;
+    List l;
+    zfclosing = 1;
+    if (zcfinish != 2) {
+	/*
+	 * haven't had EOF from server, so send a QUIT and get the response.
+	 * maybe we should set a shorter timeout for this to avoid
+	 * CTRL-c rage.
+	 */
+	zfsendcmd("QUIT\r\n");
+    }
+    if (zcin)
+	fclose(zcin);
+    zcin = NULL;
+    close(zcfd);
+    zcfd = -1;
+
+    /* Write the final status in case this is a subshell */
+    zfstatus |= ZFST_CLOS;
+    lseek(zfstatfd, 0, 0);
+    write(zfstatfd, &zfstatus, sizeof(zfstatus));
+    close(zfstatfd);
+    zfstatfd = -1;
+
+    /* Unset the non-special parameters */
+    for (aptr = zfparams; *aptr; aptr++)
+	zfunsetparam(*aptr);
+
+    /* Now ZFTP_PWD is unset.  It's up to zftp_chpwd to notice. */
+    if ((l = getshfunc("zftp_chpwd")) != &dummy_list)
+	doshfunc("zftp_chpwd", l, NULL, 0, 1);
+
+    /* tidy up status variables, because mess is bad */
+    zfclosing = zfdrrrring = 0;
+
+    return 0;
+}
+
+/* Safe front end to zftp_close() from within the package */
+
+/**/
+static void
+zfclose(void)
+{
+    if (zcfd != -1)
+	zftp_close("zftp close", NULL, 0);
+}
+
+/* The builtin command frontend to the rest of the package */
+
+/**/
+static int
+bin_zftp(char *name, char **args, char *ops, int func)
+{
+    char fullname[11] = "zftp ";
+    char *cnam = *args++, *prefs, *ptr;
+    Zftpcmd zptr;
+    int n, ret;
+
+    for (zptr = zftpcmdtab; zptr->nam; zptr++)
+	if (!strcmp(zptr->nam, cnam))
+	    break;
+
+    if (!zptr->nam) {
+	zwarnnam(name, "no such subcommand: %s", cnam, 0);
+	return 1;
+    }
+
+    /* check number of arguments */
+    for (n = 0; args[n]; n++)
+	;
+    if (n < zptr->min || (zptr->max != -1 && n > zptr->max)) {
+	zwarnnam(name, "wrong no. of arguments for %s", cnam, 0);
+	return 1;
+    }
+
+    strcat(fullname, cnam);
+    if (zfstatfd != -1) {
+	/* Get the status in case it was set by a forked process */
+	int oldstatus = zfstatus;
+	lseek(zfstatfd, 0, 0);
+	read(zfstatfd, &zfstatus, sizeof(zfstatus));
+	if (zcfd != -1 && (zfstatus & ZFST_CLOS)) {
+	    /* got closed in subshell without us knowing */
+	    zcfinish = 2;
+	    zfclose();
+	} else {
+	    /*
+	     * fix up status types: unfortunately they may already
+	     * have been looked at between being changed in the subshell
+	     * and now, but we can't help that.
+	     */
+	    if (ZFST_TYPE(oldstatus) != ZFST_TYPE(zfstatus))
+		zfsetparam("ZFTP_TYPE",
+			   ztrdup(ZFST_TYPE(zfstatus) == ZFST_ASCI ?
+				  "A" : "I"), ZFPM_READONLY);
+	    if (ZFST_MODE(oldstatus) != ZFST_MODE(zfstatus))
+		zfsetparam("ZFTP_MODE",
+			   ztrdup(ZFST_MODE(zfstatus) == ZFST_BLOC ?
+				  "B" : "S"), ZFPM_READONLY);
+	}
+    }
+    if ((zptr->flags & ZFTP_CONN) && zcfd == -1) {
+	zwarnnam(fullname, "not connected.", NULL, 0);
+	return 1;
+    }
+
+    if ((prefs = getsparam("ZFTP_PREFS"))) {
+	zfprefs = 0;
+	for (ptr = prefs; *ptr; ptr++) {
+	    switch (toupper(*ptr)) {
+	    case 'S':
+		/* sendport */
+		zfprefs |= ZFPF_SNDP;
+		break;
+
+	    case 'P':
+		/*
+		 * passive
+		 * If we have already been told to use sendport mode,
+		 * we're never going to use passive mode.
+		 */
+		if (!(zfprefs & ZFPF_SNDP))
+		    zfprefs |= ZFPF_PASV;
+		break;
+
+	    case 'D':
+		/* dumb */
+		zfprefs |= ZFPF_DUMB;
+		break;
+
+	    default:
+		zwarnnam(name, "preference %c not recognized", NULL, *ptr);
+		break;
+	    }
+	}
+    }
+
+    ret = (*zptr->fun)(fullname, args, zptr->flags);
+
+    if (zfalarmed)
+	zfunalarm();
+    if (zfdrrrring) {
+	/* had a timeout, close the connection */
+	zcfinish = 2;		/* don't try sending QUIT */
+	zfclose();
+    }
+    if (zfstatfd != -1) {
+	/* Set the status in case another process needs to know */
+	lseek(zfstatfd, 0, 0);
+	write(zfstatfd, &zfstatus, sizeof(zfstatus));
+    }
+    return ret;
+}
+
+/* The load/unload routines required by the zsh library interface */
+
+/**/
+int
+boot_zftp(Module m)
+{
+    int ret;
+    if ((ret = addbuiltins(m->nam, bintab,
+			   sizeof(bintab)/sizeof(*bintab))) == 1) {
+	/* if successful, set some default parameters */
+	long tmout_def = 60;
+	zfsetparam("ZFTP_VERBOSE", ztrdup("450"), ZFPM_IFUNSET);
+	zfsetparam("ZFTP_TMOUT", &tmout_def, ZFPM_IFUNSET|ZFPM_INTEGER);
+	zfsetparam("ZFTP_PREFS", ztrdup("PS"), ZFPM_IFUNSET);
+	/* default preferences if user deletes variable */
+	zfprefs = ZFPF_SNDP|ZFPF_PASV;
+    }
+    return !ret;
+}
+
+#ifdef MODULE
+
+/**/
+int
+cleanup_zftp(Module m)
+{
+    /*
+     * There are various parameters hanging around, but they're
+     * all non-special so are entirely non-life-threatening.
+     */
+    zfclosedata();
+    zfclose();
+    zsfree(lastmsg);
+    if (zfuserparams)
+	freearray(zfuserparams);
+    deletebuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
+    return 0;
+}
+
+#endif
diff --git a/Src/Modules/zftp.mdd b/Src/Modules/zftp.mdd
new file mode 100644
index 000000000..83051ae54
--- /dev/null
+++ b/Src/Modules/zftp.mdd
@@ -0,0 +1,3 @@
+autobins="zftp"
+
+objects="zftp.o"
diff --git a/Src/Zle/comp1.c b/Src/Zle/comp1.c
index 68aa5ef52..5ffce0da2 100644
--- a/Src/Zle/comp1.c
+++ b/Src/Zle/comp1.c
@@ -44,7 +44,7 @@ Cmlist cmatcher;
 /* pointers to functions required by zle */
 
 /**/
-void (*printcompctlptr) _((char *, Compctl, int));
+void (*printcompctlptr) _((char *, Compctl, int, int));
 
 /**/
 Compctl (*compctl_widgetptr) _((char *, char **));
diff --git a/Src/Zle/zle_main.c b/Src/Zle/zle_main.c
index 254021bed..57b75cd39 100644
--- a/Src/Zle/zle_main.c
+++ b/Src/Zle/zle_main.c
@@ -605,7 +605,7 @@ execzlefunc(Thingy func)
 	} else {
 	  startparamscope();
 	  makezleparams();
-	  doshfunc(l, NULL, 0, 1);
+	  doshfunc(w->u.fnnam, l, NULL, 0, 1);
 	  endparamscope();
 	  lastcmd = 0;
 	}
diff --git a/Src/Zle/zle_params.c b/Src/Zle/zle_params.c
index ed1420829..e0c4e94ec 100644
--- a/Src/Zle/zle_params.c
+++ b/Src/Zle/zle_params.c
@@ -72,6 +72,9 @@ makezleparams(void)
 
     for(zp = zleparams; zp->name; zp++) {
 	Param pm = createparam(zp->name, zp->type | PM_SPECIAL);
+	if (!pm)
+	    pm = (Param) paramtab->getnode(paramtab, zp->name);
+	DPUTS(!pm, "param not set in makezleparams");
 
 	pm->level = locallevel;
 	pm->u.data = zp->data;
diff --git a/Src/Zle/zle_thingy.c b/Src/Zle/zle_thingy.c
index f8de14f17..68329be65 100644
--- a/Src/Zle/zle_thingy.c
+++ b/Src/Zle/zle_thingy.c
@@ -394,7 +394,7 @@ scanlistwidgets(HashNode hn, int list)
 	quotedzputs(t->nam, stdout);
 	if (w->flags & WIDGET_COMP) {
 	    if (printcompctlptr && w->u.cc)
-		printcompctlptr(NULL, w->u.cc, PRINT_LIST);
+		printcompctlptr(NULL, w->u.cc, PRINT_LIST, 0);
 	} else if(strcmp(t->nam, w->u.fnnam)) {
 	    fputc(' ', stdout);
 	    quotedzputs(w->u.fnnam, stdout);
@@ -404,7 +404,7 @@ scanlistwidgets(HashNode hn, int list)
 	if (w->flags & WIDGET_COMP) {
 	    fputs(" -C", stdout);
 	    if (printcompctlptr && w->u.cc)
-		printcompctlptr(NULL, w->u.cc, PRINT_TYPE);
+		printcompctlptr(NULL, w->u.cc, PRINT_TYPE, 0);
 	} else if(strcmp(t->nam, w->u.fnnam)) {
 	    fputs(" (", stdout);
 	    nicezputs(w->u.fnnam, stdout);
diff --git a/Src/Zle/zle_tricky.c b/Src/Zle/zle_tricky.c
index a8998739c..cbc744601 100644
--- a/Src/Zle/zle_tricky.c
+++ b/Src/Zle/zle_tricky.c
@@ -2730,18 +2730,16 @@ maketildelist(void)
 
 /**/
 static int
-getcpat(char *wrd, int cpatindex, char *cpat, int class)
+getcpat(char *str, int cpatindex, char *cpat, int class)
 {
-    char *str, *s, *t, *p;
+    char *s, *t, *p;
     int d = 0;
 
-    if (!wrd || !*wrd)
+    if (!str || !*str)
 	return -1;
 
     cpat = rembslash(cpat);
 
-    str = ztrdup(wrd);
-    untokenize(str);
     if (!cpatindex)
 	cpatindex++, d = 0;
     else if ((d = (cpatindex < 0)))
@@ -2752,23 +2750,14 @@ getcpat(char *wrd, int cpatindex, char *cpat, int class)
 	 d ? s-- : s++) {
 	for (t = s, p = cpat; *t && *p; p++) {
 	    if (class) {
-		if (*p == *s && !--cpatindex) {
-		    zsfree(str);
+		if (*p == *s && !--cpatindex)
 		    return (int)(s - str + 1);
-		}
 	    } else if (*t++ != *p)
 		break;
 	}
-	if (!class && !*p && !--cpatindex) {
-	    zsfree(str);
-	    t += wrd - str;
-	    for (d = 0; --t >= wrd;)
-		if (! INULL(*t))
-		    d++;
-	    return d;
-	}
+	if (!class && !*p && !--cpatindex)
+	    return t - str;
     }
-    zsfree(str);
     return -1;
 }
 
@@ -3336,8 +3325,9 @@ makecomplistext(Compctl occ, char *os, int incmd)
 			break;
 		    case CCT_CURSUF:
 		    case CCT_CURPRE:
-			s = ztrdup(clwpos < clwnum ? clwords[clwpos] : "");
+			s = ztrdup(clwpos < clwnum ? os : "");
 			untokenize(s);
+			if (isset(COMPLETEINWORD)) s[offs] = '\0';
 			sc = rembslash(cc->u.s.s[i]);
 			a = strlen(sc);
 			if (!strncmp(s, sc, a)) {
@@ -3347,10 +3337,13 @@ makecomplistext(Compctl occ, char *os, int incmd)
 			break;
 		    case CCT_CURSUB:
 		    case CCT_CURSUBC:
-			if (clwpos < 0 || clwpos > clwnum)
+			if (clwpos < 0 || clwpos >= clwnum)
 			    t = 0;
 			else {
-			    a = getcpat(clwords[clwpos],
+			    s = ztrdup(os);
+			    untokenize(s);
+			    if (isset(COMPLETEINWORD)) s[offs] = '\0';
+			    a = getcpat(s,
 					cc->u.s.p[i],
 					cc->u.s.s[i],
 					cc->type == CCT_CURSUBC);
@@ -4107,7 +4100,7 @@ makecomplistflags(Compctl cc, char *s, int incmd, int compadd)
 	    /* This flag allows us to use read -l and -c. */
 	    incompctlfunc = 1;
 	    /* Call the function. */
-	    doshfunc(list, args, 0, 1);
+	    doshfunc(cc->func, list, args, 0, 1);
 	    incompctlfunc = 0;
 	    /* And get the result from the reply parameter. */
 	    if ((r = get_user_var("reply")))
@@ -4270,7 +4263,7 @@ makecomplistflags(Compctl cc, char *s, int incmd, int compadd)
 
 	    /* No harm in allowing read -l and -c here, too */
 	    incompctlfunc = 1;
-	    doshfunc(list, args, 0, 1);
+	    doshfunc(cc->ylist, list, args, 0, 1);
 	    incompctlfunc = 0;
 	    uv = "reply";
 	}
@@ -4912,7 +4905,7 @@ do_ambiguous(void)
      * if it is needed.                                                     */
     if (isset(LISTBEEP))
 	feep();
-    if (isset(AUTOLIST) && !amenu && !showinglist)
+    if (isset(AUTOLIST) && !amenu && !showinglist && smatches >= 2)
 	showinglist = -2;
     if (am)
 	lastambig = 1;
@@ -5233,14 +5226,10 @@ listmatches(void)
     Cmgroup g;
     Cmatch *p, m;
     Cexpl *e;
-    int nlines = 0, ncols, colsz, ngr = 0, nlist = 0, longest = 1, pnl = 0;
+    int nlines = 0, ncols, nlist = 0, longest = 1, pnl = 0;
     int of = isset(LISTTYPES), opl = 0;
     int listmax = getiparam("LISTMAX");
 
-    if (smatches < 2) {
-	showinglist = 0;
-	return;
-    }
 #ifdef DEBUG
     /* Sanity check */
     if(!validlist) {
@@ -5310,16 +5299,13 @@ listmatches(void)
 		e++;
 	    }
 	}
-	if (g->lcount)
-	    ngr++;
     }
     longest += 2 + of;
     if ((ncols = (columns + 1) / longest)) {
-	colsz = (nlist + ncols - 1) / ncols;
-	nlines += ngr - 1 + colsz + (nlist == 0);
+	for (g = amatches; g; g = g->next)
+	    nlines += (g->lcount + ncols - 1) / ncols;
     } else {
 	ncols = 1;
-	colsz = 1;
 	opl = 1;
 	for (g = amatches; g; g = g->next) {
 	    char **pp = g->ylist;
@@ -5396,12 +5382,11 @@ listmatches(void)
 		}
 	    }
 	    else {
-		int n = g->lcount, nl = (n + ncols - 1) / ncols, i, a;
-		int nc = (opl ? 1 : (n + colsz - 1) / colsz);
+		int n = g->lcount, nl = (n + ncols - 1) / ncols, nc = nl, i, a;
 		char **pq;
 
 		while (n && nl--) {
-		    i = nc;
+		    i = ncols;
 		    pq = pp;
 		    while (n && i--) {
 			if (pq - g->ylist >= g->lcount)
@@ -5412,7 +5397,7 @@ listmatches(void)
 			    while (a--)
 				putc(' ', shout);
 			}
-			pq += colsz;
+			pq += nc;
 			n--;
 		    }
 		    if (n)
@@ -5422,8 +5407,7 @@ listmatches(void)
 	    }
 	}
 	else if (g->lcount) {
-	    int n = g->lcount, nl = (n + ncols - 1) / ncols, i, j, a;
-	    int nc = (opl ? 1 : (n + colsz - 1) / colsz);
+	    int n = g->lcount, nl = (n + ncols - 1) / ncols, nc = nl, i, j, a;
 	    Cmatch *q;
 
 	    if (n && pnl) {
@@ -5431,7 +5415,7 @@ listmatches(void)
 		pnl = 0;
 	    }
 	    for (p = skipnolist(g->matches); n && nl--;) {
-		i = nc;
+		i = ncols;
 		q = p;
 		while (n && i--) {
 		    if (!(m = *q))
@@ -5460,7 +5444,7 @@ listmatches(void)
 			while (a--)
 			    putc(' ', shout);
 		    if (--n)
-			for (j = colsz; j && *q; j--)
+			for (j = nc; j && *q; j--)
 			    q = skipnolist(q + 1);
 		}
 		if (n) {
diff --git a/Src/Zle/zle_vi.c b/Src/Zle/zle_vi.c
index 46c6a705b..e699e438c 100644
--- a/Src/Zle/zle_vi.c
+++ b/Src/Zle/zle_vi.c
@@ -567,7 +567,9 @@ vioperswapcase(void)
 	}
 	/* go back to the first line of the range */
 	cs = oldcs;
+#if 0
 	vifirstnonblank();
+#endif
     }
     vichgflag = 0;
 }
diff --git a/Src/Zle/zle_word.c b/Src/Zle/zle_word.c
index 923216ef8..afd860066 100644
--- a/Src/Zle/zle_word.c
+++ b/Src/Zle/zle_word.c
@@ -73,7 +73,7 @@ viforwardword(void)
 		cs++;
 	if (wordflag && !n)
 	    return;
-	while (cs != ll && iblank(line[cs]))
+	while (cs != ll && (iblank(line[cs]) || line[cs] == '\n'))
 	    cs++;
     }
 }
diff --git a/Src/builtin.c b/Src/builtin.c
index dbe91a5b1..7e77bc190 100644
--- a/Src/builtin.c
+++ b/Src/builtin.c
@@ -120,7 +120,7 @@ static struct builtin builtins[] =
     BUILTIN("which", 0, bin_whence, 0, -1, 0, "ampsw", "c"),
 
 #ifdef DYNAMIC
-    BUILTIN("zmodload", 0, bin_zmodload, 0, -1, 0, "Laudi", NULL),
+    BUILTIN("zmodload", 0, bin_zmodload, 0, -1, 0, "LaudicI", NULL),
 #endif
 };
 
@@ -994,7 +994,7 @@ cd_new_pwd(int func, LinkNode dir, int chaselinks)
     if ((l = getshfunc("chpwd")) != &dummy_list) {
 	fflush(stdout);
 	fflush(stderr);
-	doshfunc(l, NULL, 0, 1);
+	doshfunc("chpwd", l, NULL, 0, 1);
     }
 
     dirstacksize = getiparam("DIRSTACKSIZE");
diff --git a/Src/cond.c b/Src/cond.c
index 79886a720..ed91f72f3 100644
--- a/Src/cond.c
+++ b/Src/cond.c
@@ -43,6 +43,26 @@ evalcond(Cond c)
 	return evalcond(c->left) && evalcond(c->right);
     case COND_OR:
 	return evalcond(c->left) || evalcond(c->right);
+    case COND_MOD:
+    case COND_MODI:
+	{
+	    Conddef cd;
+
+	    if ((cd = getconddef((c->type == COND_MODI), (char *) c->left, 1))) {
+		if (c->type == COND_MOD) {
+		    int l = arrlen((char **) c->right);
+
+		    if (l < cd->min || (cd->max >= 0 && l > cd->max)) {
+			zerr("unrecognized condition: `-%s'", (char *) c->left, 0);
+			return 0;
+		    }
+		}
+		return cd->handler(cd, (char **) c->right);
+	    }
+	    else
+		zerr("unrecognized condition: `-%s'", (char *) c->left, 0);
+	    return 0;
+	}
     }
     singsub((char **)&c->left);
     untokenize(c->left);
diff --git a/Src/exec.c b/Src/exec.c
index bb70e59c8..a2d74a9f4 100644
--- a/Src/exec.c
+++ b/Src/exec.c
@@ -2602,7 +2602,7 @@ execshfunc(Cmd cmd, Shfunc shf)
 	deletejob(jobtab + thisjob);
     }
 
-    doshfunc(shf->funcdef, cmd->args, shf->flags, 0);
+    doshfunc(shf->nam, shf->funcdef, cmd->args, shf->flags, 0);
 
     if (!list_pipe)
 	deletefilelist(last_file_list);
@@ -2650,14 +2650,13 @@ execautofn(Cmd cmd)
 
 /**/
 void
-doshfunc(List list, LinkList doshargs, int flags, int noreturnval)
+doshfunc(char *name, List list, LinkList doshargs, int flags, int noreturnval)
 /* If noreturnval is nonzero, then reset the current return *
  * value (lastval) to its value before the shell function   *
  * was executed.                                            */
 {
     char **tab, **x, *oargv0 = NULL;
     int xexittr, newexittr, oldzoptind, oldlastval;
-    char *ou;
     void *xexitfn, *newexitfn;
     char saveopts[OPT_SIZE];
     int obreaks = breaks;
@@ -2705,14 +2704,7 @@ doshfunc(List list, LinkList doshargs, int flags, int noreturnval)
 		argzero = ztrdup(argzero);
 	    }
 	}
-	startparamscope();
-	ou = underscore;
-	underscore = ztrdup(underscore);
-	execlist(dupstruct(list), 1, 0);
-	zsfree(underscore);
-	underscore = ou;
-	endparamscope();
-
+	runshfunc(list, wrappers, name);
 	if (retflag) {
 	    retflag = 0;
 	    breaks = obreaks;
@@ -2767,6 +2759,44 @@ doshfunc(List list, LinkList doshargs, int flags, int noreturnval)
     } LASTALLOC;
 }
 
+/* This finally executes a shell function and any function wrappers     *
+ * defined by modules. This works by calling the wrapper function which *
+ * in turn has to call back this function with the arguments it gets.   */
+
+/**/
+void
+runshfunc(List list, FuncWrap wrap, char *name)
+{
+    int cont;
+    char *ou;
+
+    while (wrap) {
+	wrap->module->flags |= MOD_WRAPPER;
+	wrap->count++;
+	cont = wrap->handler(list, wrap->next, name);
+	wrap->count--;
+	if (!wrap->count) {
+	    wrap->module->flags &= ~MOD_WRAPPER;
+#ifdef DYNAMIC
+	    if (wrap->module->flags & MOD_UNLOAD) {
+		wrap->module->flags &= ~MOD_UNLOAD;
+		unload_module(wrap->module, NULL);
+	    }
+#endif
+	}
+	if (!cont)
+	    return;
+	wrap = wrap->next;
+    }
+    startparamscope();
+    ou = underscore;
+    underscore = ztrdup(underscore);
+    execlist(dupstruct(list), 1, 0);
+    zsfree(underscore);
+    underscore = ou;
+    endparamscope();
+}
+
 /* Search fpath for an undefined function.  Finds the file, and returns the *
  * list of its contents.                                                    */
 
diff --git a/Src/glob.c b/Src/glob.c
index ea5d0133c..194d535a4 100644
--- a/Src/glob.c
+++ b/Src/glob.c
@@ -129,6 +129,8 @@ struct comp {
 static char *pptr;		/* current place in string being matched */
 static Comp tail;
 static int first;		/* are leading dots special? */
+static int longest;		/* always match longest piece of path. */
+static int inclosure;		/* see comment in doesmatch() */
 
 /* Add a component to pathbuf: This keeps track of how    *
  * far we are into a file name, since each path component *
@@ -1806,41 +1808,62 @@ matchpat(char *a, char *b)
 /* do the ${foo%%bar}, ${foo#bar} stuff */
 /* please do not laugh at this code. */
 
+struct repldata {
+    int b, e;			/* beginning and end of chunk to replace */
+};
+typedef struct repldata *Repldata;
+
+/* 
+ * List of bits of matches to concatenate with replacement string.
+ * The data is a struct repldata.  It is not used in cases like
+ * ${...//#foo/bar} even though SUB_GLOBAL is set, since the match
+ * is anchored.  It goes on the heap.
+ */
+
+static LinkList repllist;
+
 /* Having found a match in getmatch, decide what part of string
  * to return.  The matched part starts b characters into string s
  * and finishes e characters in: 0 <= b <= e <= strlen(s)
  * (yes, empty matches should work).
- * Bits 3 and higher in fl are used: the flags are
- *   8:		Result is matched portion.
- *  16:		Result is unmatched portion.
- *		(N.B. this should be set for standard ${foo#bar} etc. matches.)
- *  32:		Result is numeric position of start of matched portion.
- *  64:		Result is numeric position of end of matched portion.
- * 128:		Result is length of matched portion.
+ * fl is a set of the SUB_* matches defined in zsh.h from SUB_MATCH onwards;
+ * the lower parts are ignored.
+ * replstr is the replacement string for a substitution
  */
 
 /**/
 static char *
-get_match_ret(char *s, int b, int e, int fl)
+get_match_ret(char *s, int b, int e, int fl, char *replstr)
 {
     char buf[80], *r, *p, *rr;
     int ll = 0, l = strlen(s), bl = 0, t = 0, i;
 
-    if (fl & 8)			/* matched portion */
+    if (replstr) {
+	if ((fl & SUB_GLOBAL) && repllist) {
+	    /* We are replacing the chunk, just add this to the list */
+	    Repldata rd = (Repldata) halloc(sizeof(*rd));
+	    rd->b = b;
+	    rd->e = e;
+	    addlinknode(repllist, rd);
+	    return s;
+	}
+	ll += strlen(replstr);
+    }
+    if (fl & SUB_MATCH)			/* matched portion */
 	ll += 1 + (e - b);
-    if (fl & 16)		/* unmatched portion */
+    if (fl & SUB_REST)		/* unmatched portion */
 	ll += 1 + (l - (e - b));
-    if (fl & 32) {
+    if (fl & SUB_BIND) {
 	/* position of start of matched portion */
 	sprintf(buf, "%d ", b + 1);
 	ll += (bl = strlen(buf));
     }
-    if (fl & 64) {
+    if (fl & SUB_EIND) {
 	/* position of end of matched portion */
 	sprintf(buf + bl, "%d ", e + 1);
 	ll += (bl = strlen(buf));
     }
-    if (fl & 128) {
+    if (fl & SUB_LEN) {
 	/* length of matched portion */
 	sprintf(buf + bl, "%d ", e - b);
 	ll += (bl = strlen(buf));
@@ -1850,13 +1873,13 @@ get_match_ret(char *s, int b, int e, int fl)
 
     rr = r = (char *)ncalloc(ll);
 
-    if (fl & 8) {
+    if (fl & SUB_MATCH) {
 	/* copy matched portion to new buffer */
 	for (i = b, p = s + b; i < e; i++)
 	    *rr++ = *p++;
 	t = 1;
     }
-    if (fl & 16) {
+    if (fl & SUB_REST) {
 	/* Copy unmatched portion to buffer.  If both portions *
 	 * requested, put a space in between (why?)            */
 	if (t)
@@ -1864,6 +1887,9 @@ get_match_ret(char *s, int b, int e, int fl)
 	/* there may be unmatched bits at both beginning and end of string */
 	for (i = 0, p = s; i < b; i++)
 	    *rr++ = *p++;
+	if (replstr)
+	    for (p = replstr; *p; )
+		*rr++ = *p++;
 	for (i = e, p = s + e; i < l; i++)
 	    *rr++ = *p++;
 	t = 1;
@@ -1879,64 +1905,102 @@ get_match_ret(char *s, int b, int e, int fl)
     return r;
 }
 
-/* It is called from paramsubst to get the match for ${foo#bar} etc.
- * Bits of fl determines the required action:
- *   bit 0: match the end instead of the beginning (% or %%)
- *   bit 1: % or # was doubled so get the longest match
- *   bit 2: substring match
- *   bit 3: include the matched portion
- *   bit 4: include the unmatched portion
- *   bit 5: the index of the beginning
- *   bit 6: the index of the end
- *   bit 7: the length of the match
- *   bit 8: match the complete string
+/*
+ * Run the pattern so that we always get the longest possible match.
+ * This eliminates a loop where we gradually shorten the target string
+ * to find same.  We also need to check pptr (the point successfully
+ * reached along the target string) explicitly.
+ *
+ * For this to work, we need the full hairy closure code, so
+ * set inclosure.
+ */
+
+/**/
+static int
+dolongestmatch(char *str, Comp c, int fist)
+{
+    int ret;
+    longest = 1;
+    inclosure++;
+    ret = domatch(str, c, fist);
+    inclosure--;
+    longest = 0;
+    return ret;
+}
+
+/*
+ * This is called from paramsubst to get the match for ${foo#bar} etc.
+ * fl is a set of the SUB_* flags defined in zsh.h
  * *sp points to the string we have to modify. The n'th match will be
  * returned in *sp. ncalloc is used to get memory for the result string.
+ * replstr is the replacement string from a ${.../orig/repl}, in
+ * which case pat is the original.
+ *
+ * n is now ignored unless we are looking for a substring, in
+ * which case the n'th match from the start is counted such that
+ * there is no more than one match from each position.
  */
 
 /**/
 int
-getmatch(char **sp, char *pat, int fl, int n)
+getmatch(char **sp, char *pat, int fl, int n, char *replstr)
 {
     Comp c;
-    char *s = *sp, *t, sav;
-    int i, j, l = strlen(*sp);
+    char *s = *sp, *t, *start, sav;
+    int i, j, l = strlen(*sp), matched;
 
+    MUSTUSEHEAP("getmatch");	/* presumably covered by prefork() test */
+    repllist = NULL;
     c = parsereg(pat);
     if (!c) {
 	zerr("bad pattern: %s", pat, 0);
 	return 1;
     }
-    if (fl & 256) {
+    if (fl & SUB_ALL) {
 	i = domatch(s, c, 0);
-	*sp = get_match_ret(*sp, 0, domatch(s, c, 0) ? l : 0, fl);
-	if (! **sp && (((fl & 8) && !i) || ((fl & 16) && i)))
+	*sp = get_match_ret(*sp, 0, i ? l : 0, fl, i ? replstr : 0);
+	if (! **sp && (((fl & SUB_MATCH) && !i) || ((fl & SUB_REST) && i)))
 	    return 0;
 	return 1;
     }
-    switch (fl & 7) {
+    switch (fl & (SUB_END|SUB_LONG|SUB_SUBSTR)) {
     case 0:
-	/* Smallest possible match at head of string:    *
-	 * start adding characters until we get a match. */
-	for (i = 0, t = s; i <= l; i++, t++) {
-	    sav = *t;
-	    *t = '\0';
-	    if (domatch(s, c, 0) && !--n) {
+    case SUB_LONG:
+	/*
+	 * Largest/smallest possible match at head of string.
+	 * First get the longest match.
+	 */
+	if (dolongestmatch(s, c, 0)) {
+	    char *mpos = pptr;
+	    while (!(fl & SUB_LONG) && pptr > s) {
+		/*
+		 * If we want the shortest, keep backing up to the
+		 * previous character and find the longest up to there.
+		 * That way we can usually reach the shortest in only
+		 * a few attempts.
+		 */
+		t = (pptr > s + 1 && pptr[-2] == Meta) ? pptr - 2 : pptr -1;
+		sav = *t;
+		*t = '\0';
+		if (!dolongestmatch(s, c, 0)) {
+		    *t = sav;
+		    break;
+		}
+		mpos = pptr;
 		*t = sav;
-		*sp = get_match_ret(*sp, 0, i, fl);
-		return 1;
 	    }
-	    if ((*t = sav) == Meta)
-		i++, t++;
+	    *sp = get_match_ret(*sp, 0, mpos-s, fl, replstr);
+	    return 1;
 	}
 	break;
 
-    case 1:
+    case SUB_END:
 	/* Smallest possible match at tail of string:  *
-	 * move back down string until we get a match. */
+	 * move back down string until we get a match. *
+	 * There's no optimization here.               */
 	for (t = s + l; t >= s; t--) {
-	    if (domatch(t, c, 0) && !--n) {
-		*sp = get_match_ret(*sp, t - s, l, fl);
+	    if (domatch(t, c, 0)) {
+		*sp = get_match_ret(*sp, t - s, l, fl, replstr);
 		return 1;
 	    }
 	    if (t > s+1 && t[-2] == Meta)
@@ -1944,29 +2008,13 @@ getmatch(char **sp, char *pat, int fl, int n)
 	}
 	break;
 
-    case 2:
-	/* Largest possible match at head of string:        *
-	 * delete characters from end until we get a match. */
-	for (t = s + l; t > s; t--) {
-	    sav = *t;
-	    *t = '\0';
-	    if (domatch(s, c, 0) && !--n) {
-		*t = sav;
-		*sp = get_match_ret(*sp, 0, t - s, fl);
-		return 1;
-	    }
-	    *t = sav;
-	    if (t >= s+2 && t[-2] == Meta)
-		t--;
-	}
-	break;
-
-    case 3:
+    case (SUB_END|SUB_LONG):
 	/* Largest possible match at tail of string:       *
-	 * move forward along string until we get a match. */
+	 * move forward along string until we get a match. *
+	 * Again there's no optimisation.                  */
 	for (i = 0, t = s; i < l; i++, t++) {
-	    if (domatch(t, c, 0) && !--n) {
-		*sp = get_match_ret(*sp, i, l, fl);
+	    if (domatch(t, c, 0)) {
+		*sp = get_match_ret(*sp, i, l, fl, replstr);
 		return 1;
 	    }
 	    if (*t == Meta)
@@ -1974,110 +2022,147 @@ getmatch(char **sp, char *pat, int fl, int n)
 	}
 	break;
 
-    case 4:
+    case SUB_SUBSTR:
 	/* Smallest at start, but matching substrings. */
-	if (domatch(s + l, c, 0) && !--n) {
-	    *sp = get_match_ret(*sp, 0, 0, fl);
+	if (!(fl & SUB_GLOBAL) && domatch(s + l, c, 0) && !--n) {
+	    *sp = get_match_ret(*sp, 0, 0, fl, replstr);
 	    return 1;
-	}
-	for (i = 1; i <= l; i++) {
-	    for (t = s, j = i; j <= l; j++, t++) {
-		sav = s[j];
-		s[j] = '\0';
-		if (domatch(t, c, 0) && !--n) {
-		    s[j] = sav;
-		    *sp = get_match_ret(*sp, t - s, j, fl);
-		    return 1;
+	} /* fall through */
+    case (SUB_SUBSTR|SUB_LONG):
+	/* longest or smallest at start with substrings */
+	start = s;
+	if (fl & SUB_GLOBAL)
+	    repllist = newlinklist();
+	do {
+	    /* loop over all matches for global substitution */
+	    matched = 0;
+	    for (t = start; t < s + l; t++) {
+		/* Find the longest match from this position. */
+		if (dolongestmatch(t, c, 0) && pptr > t) {
+		    char *mpos = pptr;
+		    while (!(fl & SUB_LONG) && pptr > t) {
+			/* Now reduce to find the smallest match */
+			char *p = (pptr > t + 1 && pptr[-2] == Meta) ?
+			    pptr - 2 : pptr - 1;
+			sav = *p;
+			*p = '\0';
+			if (!dolongestmatch(t, c, 0)) {
+			    *p = sav;
+			    break;
+			}
+			mpos = pptr;
+			*p = sav;
+		    }
+		    if (!--n || (n <= 0 && (fl & SUB_GLOBAL)))
+			*sp = get_match_ret(*sp, t-s, mpos-s, fl, replstr);
+		    if (!(fl & SUB_GLOBAL)) {
+			if (n) {
+			    /*
+			     * Looking for a later match: in this case,
+			     * we can continue looking for matches from
+			     * the next character, even if it overlaps
+			     * with what we just found.
+			     */
+			    continue;
+			} else
+			    return 1;
+		    }
+		    /*
+		     * For a global match, we need to skip the stuff
+		     * which is already marked for replacement.
+		     */
+		    matched = 1;
+		    start = mpos;
+		    break;
 		}
-		if ((s[j] = sav) == Meta)
-		    j++;
 		if (*t == Meta)
 		    t++;
 	    }
-	    if (s[i] == Meta)
-		i++;
+	} while (matched);
+	/*
+	 * check if we can match a blank string, if so do it
+	 * at the start.  Goodness knows if this is a good idea
+	 * with global substitution, so it doesn't happen.
+	 */
+	if ((fl & (SUB_LONG|SUB_GLOBAL)) == SUB_LONG &&
+	    domatch(s + l, c, 0) && !--n) {
+	    *sp = get_match_ret(*sp, 0, 0, fl, replstr);
+	    return 1;
 	}
 	break;
 
-    case 5:
-	/* Smallest at end, matching substrings */
+    case (SUB_END|SUB_SUBSTR):
+	/* Shortest at end with substrings */
 	if (domatch(s + l, c, 0) && !--n) {
-	    *sp = get_match_ret(*sp, l, l, fl);
+	    *sp = get_match_ret(*sp, l, l, fl, replstr);
 	    return 1;
-	}
-	for (i = l; i--;) {
-	    if (i && s[i-1] == Meta)
-		i--;
-	    for (t = s + l, j = i; j >= 0; j--, t--) {
-		sav = *t;
-		*t = '\0';
-		if (domatch(s + j, c, 0) && !--n) {
-		    *t = sav;
-		    *sp = get_match_ret(*sp, j, t - s, fl);
-		    return 1;
-		}
-		*t = sav;
-		if (t >= s+2 && t[-2] == Meta)
-		    t--;
-		if (j >= 2 && s[j-2] == Meta)
-		    j--;
-	    }
-	}
-	break;
-
-    case 6:
-	/* Largest at start, matching substrings. */
-	for (i = l; i; i--) {
-	    for (t = s, j = i; j <= l; j++, t++) {
-		sav = s[j];
-		s[j] = '\0';
-		if (domatch(t, c, 0) && !--n) {
-		    s[j] = sav;
-		    *sp = get_match_ret(*sp, t - s, j, fl);
-		    return 1;
+	} /* fall through */
+    case (SUB_END|SUB_LONG|SUB_SUBSTR):
+	/* Longest/shortest at end, matching substrings.       */
+	for (t = s + l - 1; t >= s; t--) {
+	    if (t > s && t[-1] == Meta)
+		t--;
+	    if (dolongestmatch(t, c, 0) && pptr > t && !--n) {
+		/* Found the longest match */
+		char *mpos = pptr;
+		while (!(fl & SUB_LONG) && pptr > t) {
+		    /* Look for the shortest match */
+		    char *p = (pptr > t+1 && pptr[-2] == Meta) ?
+			pptr-2 : pptr-1;
+		    sav = *p;
+		    *p = '\0';
+		    if (!dolongestmatch(t, c, 0) || pptr == t) {
+			*p = sav;
+			break;
+		    }
+		    *p = sav;
+		    mpos = pptr;
 		}
-		if ((s[j] = sav) == Meta)
-		    j++;
-		if (*t == Meta)
-		    t++;
+		*sp = get_match_ret(*sp, t-s, mpos-s, fl, replstr);
+		return 1;
 	    }
-	    if (i >= 2 && s[i-2] == Meta)
-		i--;
 	}
-	if (domatch(s + l, c, 0) && !--n) {
-	    *sp = get_match_ret(*sp, 0, 0, fl);
+	if ((fl & SUB_LONG) && domatch(s + l, c, 0) && !--n) {
+	    *sp = get_match_ret(*sp, l, l, fl, replstr);
 	    return 1;
 	}
 	break;
+    }
 
-    case 7:
-	/* Largest at end, matching substrings. */
-	for (i = 0; i < l; i++) {
-	    for (t = s + l, j = i; j >= 0; j--, t--) {
-		sav = *t;
-		*t = '\0';
-		if (domatch(s + j, c, 0) && !--n) {
-		    *t = sav;
-		    *sp = get_match_ret(*sp, j, t - s, fl);
-		    return 1;
-		}
-		*t = sav;
-		if (t >= s+2 && t[-2] == Meta)
-		    t--;
-		if (j >= 2 && s[j-2] == Meta)
-		    j--;
-	    }
-	    if (s[i] == Meta)
-		i++;
+    if (repllist && nonempty(repllist)) {
+	/* Put all the bits of a global search and replace together. */
+	LinkNode nd;
+	Repldata rd;
+	int rlen;
+	int lleft = 0;		/* size of returned string */
+
+	i = 0;			/* start of last chunk we got from *sp */
+	rlen = strlen(replstr);
+	for (nd = firstnode(repllist); nd; incnode(nd)) {
+	    rd = (Repldata) getdata(nd);
+	    lleft += rd->b - i; /* previous chunk of *sp */
+	    lleft += rlen;	/* the replaced bit */
+	    i = rd->e;		/* start of next chunk of *sp */
 	}
-	if (domatch(s + l, c, 0) && !--n) {
-	    *sp = get_match_ret(*sp, l, l, fl);
-	    return 1;
+	lleft += l - i;	/* final chunk from *sp */
+	start = t = halloc(lleft+1);
+	i = 0;
+	for (nd = firstnode(repllist); nd; incnode(nd)) {
+	    rd = (Repldata) getdata(nd);
+	    memcpy(t, s + i, rd->b - i);
+	    t += rd->b - i;
+	    memcpy(t, replstr, rlen);
+	    t += rlen;
+	    i = rd->e;
 	}
-	break;
+	memcpy(t, s + i, l - i);
+	start[lleft] = '\0';
+	*sp = start;
+	return 1;
     }
-    /* munge the whole string */
-    *sp = get_match_ret(*sp, 0, 0, fl);
+
+    /* munge the whole string: no match, so no replstr */
+    *sp = get_match_ret(*sp, 0, 0, fl, 0);
     return 1;
 }
 
@@ -2109,9 +2194,15 @@ static int
 excluded(Comp c, char *eptr)
 {
     char *saves = pptr;
-    int savei = first, ret;
+    int savei = first, savel = longest, ret;
 
     first = 0;
+    /*
+     * Even if we've been told always to match the longest string,
+     * i.e. not anchored to the end, we want to match the full string
+     * we've already matched when we're trying to exclude it.
+     */
+    longest = 0;
     if (PATHADDP(c) && pathpos) {
 	VARARR(char, buf, pathpos + strlen(eptr) + 1);
 
@@ -2128,6 +2219,7 @@ excluded(Comp c, char *eptr)
 
     pptr = saves;
     first = savei;
+    longest = savel;
 
     return ret;
 }
@@ -2138,8 +2230,6 @@ struct gclose {
 };
 typedef struct gclose *Gclose;
 
-static int inclosure;		/* see comment in doesmatch() */
-
 /* Add a list of matches that fit the closure.  trystring is a string of
  * the same length as the target string; a non-zero in that string
  * indicates that we have already tried to match the patterns following
@@ -2182,6 +2272,15 @@ addclosures(Comp c, LinkList closlist, int *pdone, char *trystring)
     }
 }
 
+/*
+ * Match characters with case-insensitivity.
+ * Note CHARMATCH(x,y) != CHARMATCH(y,x)
+ */
+#define CHARMATCH(x, y) \
+(x == y || (((c->stat & C_IGNCASE) ? (tulower(x) == tulower(y)) : \
+	     (c->stat & C_LCMATCHUC) ? (islower(y) && tuupper(y) == x) : 0)))
+
+
 /* see if current string in pptr matches c */
 
 /**/
@@ -2219,7 +2318,7 @@ doesmatch(Comp c)
 		    for (; *pptr; pptr++) {
 			if (*pptr == Meta)
 			    pptr++;
-			else if (*pptr == looka)
+			else if (CHARMATCH(*pptr, looka))
 			    break;
 		    }
 		    if (!*(saves = pptr))
@@ -2233,7 +2332,7 @@ doesmatch(Comp c)
 		for (done = 0; ; done++) {
 		    saves = pptr;
 		    if ((done || ONEHASHP(c) || OPTIONALP(c)) &&
-			((!c->next && (!LASTP(c) || !*pptr)) ||
+			((!c->next && (!LASTP(c) || !*pptr || longest)) ||
 			 (c->next && doesmatch(c->next))))
 			return 1;
 		    if (done && OPTIONALP(c))
@@ -2267,7 +2366,7 @@ doesmatch(Comp c)
 		break;
 	    saves = pptr;
 	    /* do we really want this LASTP here?? */
-	    if ((!c->next && (!LASTP(c) || !*pptr)) ||
+	    if ((!c->next && (!LASTP(c) || !*pptr || longest)) ||
 		(c->next && doesmatch(c->next))) {
 		retflag = 1;
 		break;
@@ -2453,7 +2552,7 @@ matchonce(Comp c)
 			if (!ret)
 			    break;
 			if ((ret = ret2 &&
-			     ((!c->next && (!LASTP(c) || !*pptr))
+			     ((!c->next && (!LASTP(c) || !*pptr || longest))
 			      || (c->next && doesmatch(c->next)))) ||
 			    (!c->next && LASTP(c)))
 			    break;
@@ -2485,7 +2584,7 @@ matchonce(Comp c)
 	    if (CLOSUREP(c))
 		return 1;
 	    if (!c->next)	/* no more patterns left */
-		return (!LASTP(c) || !*pptr);
+		return (!LASTP(c) || !*pptr || longest);
 	    /* optimisation when next pattern is not a closure */
 	    if (!CLOSUREP(c->next)) {
 		c = c->next;
@@ -2589,10 +2688,7 @@ matchonce(Comp c)
 	    }
 	    continue;
 	}
-	if (*pptr == *pat ||
-	    (((c->stat & C_IGNCASE) ? (tulower(*pat) == tulower(*pptr)) :
-	      (c->stat & C_LCMATCHUC) ?
-	      (islower(*pat) && tuupper(*pat) == *pptr) : 0))) {
+	if (CHARMATCH(*pptr, *pat)) {
 	    /* just plain old characters */
 	    pptr++;
 	    pat++;
diff --git a/Src/hashtable.c b/Src/hashtable.c
index 5bcfec231..60fb3df80 100644
--- a/Src/hashtable.c
+++ b/Src/hashtable.c
@@ -414,9 +414,10 @@ scanmatchtable(HashTable ht, Comp com, int flags1, int flags2, ScanFunc scanfunc
 	    HashNode hn = st.u.u;
 	    st.u.u = st.u.u->next;
 	    if ((hn->flags & flags1) + !flags1 && !(hn->flags & flags2) &&
-		domatch(hn->nam, com, 0))
+		domatch(hn->nam, com, 0)) {
 		scanfunc(hn, scanflags);
 		match++;
+	    }
 	}
 
     ht->scan = NULL;
diff --git a/Src/init.c b/Src/init.c
index 33496adc6..decc7617e 100644
--- a/Src/init.c
+++ b/Src/init.c
@@ -117,7 +117,7 @@ loop(int toplevel, int justonce)
 		    if (he && he->text)
 			addlinknode(args, he->text);
 		} LASTALLOC;
-		doshfunc(prelist, args, 0, 1);
+		doshfunc("preexec", prelist, args, 0, 1);
 		freelinklist(args, (FreeFunc) NULL);
 		errflag = 0;
 	    }
@@ -592,6 +592,9 @@ setupvals(void)
     createnameddirtable();  /* create hash table for named directories */
     createparamtable();     /* create paramater hash table             */
 
+    condtab = NULL;
+    wrappers = NULL;
+
 #ifdef TIOCGWINSZ
     adjustwinsize();
 #else
diff --git a/Src/mem.c b/Src/mem.c
index 1145f8c5e..32822ab8c 100644
--- a/Src/mem.c
+++ b/Src/mem.c
@@ -244,7 +244,7 @@ halloc(size_t size)
     size = (size + H_ISIZE - 1) & ~(H_ISIZE - 1);
 
 #if defined(ZSH_MEM) && defined(ZSH_MEM_DEBUG)
-    h_m[size < 1024 ? (size / H_ISIZE) : 1024]++;
+    h_m[size < (1024 * H_ISIZE) ? (size / H_ISIZE) : 1024]++;
 #endif
 
     /* find a heap with enough free space */
@@ -319,6 +319,9 @@ hrealloc(char *p, size_t old, size_t new)
 	if (new > old) {
 	    char *ptr = (char *) halloc(new);
 	    memcpy(ptr, p, old);
+#ifdef ZSH_MEM_DEBUG
+	    memset(p, 0xff, old);
+#endif
 	    return ptr;
 	} else
 	    return new ? p : NULL;
@@ -1004,8 +1007,9 @@ zfree(void *p, int sz)
 	long n = (m_lfree->len - M_MIN) & ~(m_pgsz - 1);
 
 	m_lfree->len -= n;
-	if (brk(m_high -= n) == -1)
+	if (brk(m_high -= n) == -1) {
 	    DPUTS(1, "MEM: allocation error at brk.");
+	}
 
 #ifdef ZSH_MEM_DEBUG
 	m_b += n;
diff --git a/Src/module.c b/Src/module.c
index 91687a21d..8ed4f1d3b 100644
--- a/Src/module.c
+++ b/Src/module.c
@@ -90,6 +90,35 @@ addbuiltins(char const *nam, Builtin binl, int size)
     return hadf ? hads : 1;
 }
 
+/* The list of function wrappers defined. */
+
+/**/
+FuncWrap wrappers;
+
+/* This adds a definition for a wrapper. Return value is one in case of *
+ * error and zero if all went fine. */
+
+/**/
+int
+addwrapper(Module m, FuncWrap w)
+{
+    FuncWrap p, q;
+
+    if (w->flags & WRAPF_ADDED)
+	return 1;
+    for (p = wrappers, q = NULL; p; q = p, p = p->next);
+    if (q)
+	q->next = w;
+    else
+	wrappers = w;
+    w->next = NULL;
+    w->flags |= WRAPF_ADDED;
+    w->module = m;
+    w->count = 0;
+
+    return 0;
+}
+
 #ifdef DYNAMIC
 
 /* $module_path ($MODULE_PATH) */
@@ -161,6 +190,31 @@ deletebuiltins(char const *nam, Builtin binl, int size)
     return hadf ? hads : 1;
 }
 
+/* This removes the given wrapper definition from the list. Returned is *
+ * one in case of error and zero otherwise. */
+
+/**/
+int
+deletewrapper(Module m, FuncWrap w)
+{
+    FuncWrap p, q;
+
+    if (w->flags & WRAPF_ADDED) {
+	for (p = wrappers, q = NULL; p && p != w; q = p, p = p->next);
+
+	if (p) {
+	    if (q)
+		q->next = p->next;
+	    else
+		wrappers = p->next;
+	    p->flags &= ~WRAPF_ADDED;
+
+	    return 0;
+	}
+    }
+    return 1;
+}
+
 #ifdef AIXDYNAMIC
 
 #include <sys/ldr.h>
@@ -498,6 +552,8 @@ bin_zmodload(char *nam, char **args, char *ops, int func)
 	return bin_zmodload_dep(nam, args, ops);
     else if(ops['a'])
 	return bin_zmodload_auto(nam, args, ops);
+    else if (ops['c'] || ops['C'])
+	return bin_zmodload_cond(nam, args, ops);
     else
 	return bin_zmodload_load(nam, args, ops);
 }
@@ -632,6 +688,98 @@ bin_zmodload_auto(char *nam, char **args, char *ops)
 
 /**/
 static int
+bin_zmodload_cond(char *nam, char **args, char *ops)
+{
+    int ret = 0;
+
+    if (ops['u']) {
+	/* remove autoloaded conditions */
+	for (; *args; args++) {
+	    Conddef cd = getconddef(ops['I'], *args, 0);
+
+	    if (!cd) {
+		if (!ops['i']) {
+		    zwarnnam(nam, "%s: no such condition", *args, 0);
+		    ret = 1;
+		}
+	    } else if (cd->flags & CONDF_ADDED) {
+		zwarnnam(nam, "%s: condition is already defined", *args, 0);
+		ret = 1;
+	    } else
+		deleteconddef(cd);
+	}
+	return ret;
+    } else if (!*args) {
+	/* list autoloaded conditions */
+	Conddef p;
+
+	for (p = condtab; p; p = p->next) {
+	    if (p->module) {
+		if (ops['L']) {
+		    fputs("zmodload -c", stdout);
+		    if (p->flags & CONDF_INFIX)
+			putchar('I');
+		    printf(" %s %s\n", p->module, p->name);
+		} else {
+		    fputs("post ", stdout);
+		    if (p->flags & CONDF_INFIX)
+			fputs("infix ", stdout);
+		    printf("%s (%s)\n",p->name, p->module);
+		}
+	    }
+	}
+	return 0;
+    } else {
+	/* add autoloaded conditions */
+	char *modnam;
+
+	modnam = *args++;
+	if(isset(RESTRICTED) && strchr(modnam, '/')) {
+	    zwarnnam(nam, "%s: restricted", modnam, 0);
+	    return 1;
+	}
+	do {
+	    char *cnam = *args ? *args++ : modnam;
+	    if (strchr(cnam, '/')) {
+		zwarnnam(nam, "%s: `/' is illegal in a condition", cnam, 0);
+		ret = 1;
+	    } else if (add_autocond(cnam, ops['I'], modnam) && !ops['i']) {
+		zwarnnam(nam, "failed to add condition %s", cnam, 0);
+		ret = 1;
+	    }
+	} while(*args);
+	return ret;
+    }
+}
+
+/**/
+int
+unload_module(Module m, LinkNode node)
+{
+    if (m->handle && cleanup_module(m))
+	return 1;
+    else {
+	if (m->handle)
+	    dlclose(m->handle);
+	m->handle = NULL;
+	if(!m->deps) {
+	    if (!node) {
+		for (node = firstnode(modules); node; incnode(node))
+		    if (m == (Module) getdata(node))
+			break;
+		if (!node)
+		    return 1;
+	    }
+	    remnode(modules, node);
+	    zsfree(m->nam);
+	    zfree(m, sizeof(*m));
+	}
+    }
+    return 0;
+}
+
+/**/
+static int
 bin_zmodload_load(char *nam, char **args, char *ops)
 {
     LinkNode node;
@@ -654,20 +802,13 @@ bin_zmodload_load(char *nam, char **args, char *ops)
 				goto cont;
 			    }
 		}
-
 		m = (Module) getdata(node);
-		if (m->handle && cleanup_module(m))
-		    ret = 1;
-		else {
-		    if (m->handle)
-			dlclose(m->handle);
-		    m->handle = NULL;
-		    if(!m->deps) {
-			remnode(modules, node);
-			zsfree(m->nam);
-			zfree(m, sizeof(*m));
-		    }
+		if (!(m->flags & MOD_WRAPPER)) {
+		    if (unload_module(m, node))
+			ret = 1;
 		}
+		else
+		    m->flags |= MOD_UNLOAD;
 	    } else if (!ops['i']) {
 		zwarnnam(nam, "no such module %s", *args, 0);
 		ret = 1;
@@ -711,3 +852,166 @@ bin_zmodload_load(char *nam, char **args, char *ops)
 }
 
 #endif /* DYNAMIC */
+
+/* The list of module-defined conditions. */
+
+/**/
+Conddef condtab;
+
+/* This gets a condition definition with the given name. The first        *
+ * argument says if we have to look for an infix condition. The last      *
+ * argument is non-zero if we should autoload modules if needed. */
+
+/**/
+Conddef
+getconddef(int inf, char *name, int autol)
+{
+    Conddef p;
+#ifdef DYNAMIC
+    int f = 1;
+#endif
+
+    do {
+	for (p = condtab; p; p = p->next) {
+	    if ((!!inf == !!(p->flags & CONDF_INFIX)) &&
+		!strcmp(name, p->name))
+		break;
+	}
+#ifdef DYNAMIC
+	if (autol && p && p->module) {
+	    /* This is a definition for an autoloaded condition, load the *
+	     * module if we haven't tried that already. */
+	    if (f) {
+		load_module(p->module);
+		f = 0;
+		p = NULL;
+	    } else
+		break;
+	} else
+#endif
+	    break;
+    } while (!p);
+    return p;
+}
+
+#ifdef DYNAMIC
+
+/* This adds the given condition definition. The return value is zero on *
+ * success and 1 on failure. If there is a matching definition for an    *
+ * autoloaded condition, it is removed. */
+
+/**/
+int
+addconddef(Conddef c)
+{
+    Conddef p = getconddef((c->flags & CONDF_INFIX), c->name, 0);
+
+    if (p) {
+	if (!p->module || (p->flags & CONDF_ADDED))
+	    return 1;
+
+	/* There is an autoload definition. */
+
+	deleteconddef(p);
+    }
+    c->next = condtab;
+    condtab = c;
+    return 0;
+}
+
+/* This adds multiple condition definitions. This is like addbuiltins(). */
+
+/**/
+int
+addconddefs(char const *nam, Conddef c, int size)
+{
+    int hads = 0, hadf = 0;
+
+    while (size--) {
+	if (c->flags & CONDF_ADDED)
+	    continue;
+	if (addconddef(c)) {
+	    zwarnnam(nam, "name clash when adding condition `%s'", c->name, 0);
+	    hadf = 1;
+	} else {
+	    c->flags |= CONDF_ADDED;
+	    hads = 2;
+	}
+	c++;
+    }
+    return hadf ? hads : 1;
+}
+
+/* This adds a definition for autoloading a module for a condition. */
+
+/**/
+int
+add_autocond(char *nam, int inf, char *module)
+{
+    Conddef c = zalloc(sizeof(*c));
+
+    c->name = ztrdup(nam);
+    c->flags = (inf  ? CONDF_INFIX : 0);
+    c->module = ztrdup(module);
+
+    if (addconddef(c)) {
+	zsfree(c->name);
+	zsfree(c->module);
+	zfree(c, sizeof(*c));
+
+	return 1;
+    }
+    return 0;
+}
+
+/* This removes the given condition definition from the list(s). If this *
+ * is a definition for a autoloaded condition, the memory is freed. */
+
+/**/
+int
+deleteconddef(Conddef c)
+{
+    Conddef p, q;
+
+    for (p = condtab, q = NULL; p && p != c; q = p, p = p->next);
+
+    if (p) {
+	if (q)
+	    q->next = p->next;
+	else 
+	    condtab = p->next;
+		
+	if (p->module) {
+	    /* autoloaded, free it */
+	    zsfree(p->name);
+	    zsfree(p->module);
+	    zfree(p, sizeof(*p));
+	}
+	return 0;
+    }
+    return -1;
+}
+
+/* This removes multiple condition definitions (like deletebuiltins()). */
+
+/**/
+int
+deleteconddefs(char const *nam, Conddef c, int size)
+{
+    int hads = 0, hadf = 0;
+
+    while (size--) {
+	if (!(c->flags & CONDF_ADDED))
+	    continue;
+	if (deleteconddef(c)) {
+	    zwarnnam(nam, "condition `%s' already deleted", c->name, 0);
+	    hadf = 1;
+	} else
+	    hads = 2;
+	c->flags &= ~CONDF_ADDED;
+	c++;
+    }
+    return hadf ? hads : 1;
+}
+
+#endif /* DYNAMIC */
diff --git a/Src/params.c b/Src/params.c
index 69fd8a904..54699476c 100644
--- a/Src/params.c
+++ b/Src/params.c
@@ -303,7 +303,11 @@ copyparamtable(HashTable ht, char *name)
 
 #define SCANPM_WANTVALS   (1<<0)
 #define SCANPM_WANTKEYS   (1<<1)
-#define SCANPM_WANTINDEX  (1<<2)
+#define SCANPM_WANTINDEX  (1<<2)	/* Useful only if nested arrays */
+#define SCANPM_MATCHKEY   (1<<3)
+#define SCANPM_MATCHVAL   (1<<4)
+#define SCANPM_MATCHMANY  (1<<5)
+#define SCANPM_ISVAR_AT   ((-1)<<15)	/* Only sign bit is significant */
 
 static unsigned numparamvals;
 
@@ -311,13 +315,12 @@ static unsigned numparamvals;
 static void
 scancountparams(HashNode hn, int flags)
 {
-    if (!(((Param)hn)->flags & PM_UNSET)) {
+    ++numparamvals;
+    if ((flags & SCANPM_WANTKEYS) && (flags & SCANPM_WANTVALS))
 	++numparamvals;
-	if ((flags & SCANPM_WANTKEYS) && (flags & SCANPM_WANTVALS))
-	    ++numparamvals;
-    }
 }
 
+static Comp scancomp;
 static char **paramvals;
 
 /**/
@@ -325,33 +328,45 @@ static void
 scanparamvals(HashNode hn, int flags)
 {
     struct value v;
+    if (numparamvals && (flags & (SCANPM_MATCHVAL|SCANPM_MATCHKEY)) &&
+	!(flags & SCANPM_MATCHMANY))
+	return;
     v.pm = (Param)hn;
-    if (!(v.pm->flags & PM_UNSET)) {
-	if (flags & SCANPM_WANTKEYS) {
-	    paramvals[numparamvals++] = v.pm->nam;
-	    if (!(flags & SCANPM_WANTVALS))
-		return;
-	}
-	v.isarr = (PM_TYPE(v.pm->flags) & (PM_ARRAY|PM_HASHED));
-	v.inv = (flags & SCANPM_WANTINDEX);
-	v.a = 0;
-	v.b = -1;
-	paramvals[numparamvals++] = getstrvalue(&v);
+    if ((flags & SCANPM_MATCHKEY) && !domatch(v.pm->nam, scancomp, 0)) {
+	return;
+    }
+    if (flags & SCANPM_WANTKEYS) {
+	paramvals[numparamvals++] = v.pm->nam;
+	if (!(flags & (SCANPM_WANTVALS|SCANPM_MATCHVAL)))
+	    return;
     }
+    v.isarr = (PM_TYPE(v.pm->flags) & (PM_ARRAY|PM_HASHED));
+    v.inv = 0;
+    v.a = 0;
+    v.b = -1;
+    paramvals[numparamvals] = getstrvalue(&v);
+    if (flags & SCANPM_MATCHVAL) {
+	if (domatch(paramvals[numparamvals], scancomp, 0)) {
+	    numparamvals += ((flags & SCANPM_WANTVALS) ? 1 :
+			     !(flags & SCANPM_WANTKEYS));
+	} else if (flags & SCANPM_WANTKEYS)
+	    --numparamvals;	/* Value didn't match, discard key */
+    } else
+	++numparamvals;
 }
 
 /**/
 char **
-paramvalarr(HashTable ht, unsigned flags)
+paramvalarr(HashTable ht, int flags)
 {
     MUSTUSEHEAP("paramvalarr");
     numparamvals = 0;
     if (ht)
-	scanhashtable(ht, 0, 0, 0, scancountparams, flags);
+	scanhashtable(ht, 0, 0, PM_UNSET, scancountparams, flags);
     paramvals = (char **) alloc((numparamvals + 1) * sizeof(char *));
     if (ht) {
 	numparamvals = 0;
-	scanhashtable(ht, 0, 0, 0, scanparamvals, flags);
+	scanhashtable(ht, 0, 0, PM_UNSET, scanparamvals, flags);
     }
     paramvals[numparamvals] = 0;
     return paramvals;
@@ -369,15 +384,10 @@ getvaluearr(Value v)
     else if (PM_TYPE(v->pm->flags) == PM_ARRAY)
 	return v->arr = v->pm->gets.afn(v->pm);
     else if (PM_TYPE(v->pm->flags) == PM_HASHED) {
-	unsigned flags = 0;
-	if (v->a)
-	    flags |= SCANPM_WANTKEYS;
-	if (v->b > v->a)
-	    flags |= SCANPM_WANTVALS;
-	v->arr = paramvalarr(v->pm->gets.hfn(v->pm), flags);
+	v->arr = paramvalarr(v->pm->gets.hfn(v->pm), v->isarr);
 	/* Can't take numeric slices of associative arrays */
 	v->a = 0;
-	v->b = -1;
+	v->b = numparamvals;
 	return v->arr;
     } else
 	return NULL;
@@ -737,7 +747,19 @@ getarg(char **str, int *inv, Value v, int a2, long *w)
 	down = !down;
 	num = -num;
     }
-    *inv = ind;
+    if (v->isarr & SCANPM_WANTKEYS)
+	*inv = (ind || !(v->isarr & SCANPM_WANTVALS));
+    else if (v->isarr & SCANPM_WANTVALS)
+	*inv = 0;
+    else {
+	if (ind) {
+	    v->isarr |= SCANPM_WANTKEYS;
+	    v->isarr &= ~SCANPM_WANTVALS;
+	}
+	if (!down)
+	    v->isarr &= ~SCANPM_MATCHMANY;
+	*inv = ind;
+    }
 
     for (t=s, i=0; *t && ((*t != ']' && *t != Outbrack && *t != ',') || i); t++)
 	if (*t == '[' || *t == Inbrack)
@@ -829,7 +851,21 @@ getarg(char **str, int *inv, Value v, int a2, long *w)
 
 	if ((c = parsereg(s))) {
 	    if (v->isarr) {
-		ta = getarrvalue(v);
+		if (PM_TYPE(v->pm->flags) == PM_HASHED) {
+		    scancomp = c;
+		    if (ind)
+			v->isarr |= SCANPM_MATCHKEY;
+		    else
+			v->isarr |= SCANPM_MATCHVAL;
+		    if (down)
+			v->isarr |= SCANPM_MATCHMANY;
+		    if ((ta = getvaluearr(v)) && *ta) {
+			*inv = v->inv;
+			*w = v->b;
+			return 1;
+		    }
+		} else
+		    ta = getarrvalue(v);
 		if (!ta || !*ta)
 		    return 0;
 		if (down)
@@ -920,8 +956,8 @@ getindex(char **pptr, Value v)
     if (*tbrack == Outbrack)
 	*tbrack = ']';
     if ((s[0] == '*' || s[0] == '@') && s[1] == ']') {
-	if (v->isarr)
-	    v->isarr = (s[0] == '*') ? 1 : -1;
+	if (v->isarr && s[0] == '@')
+	    v->isarr |= SCANPM_ISVAR_AT;
 	v->a = 0;
 	v->b = -1;
 	s += 2;
@@ -941,7 +977,7 @@ getindex(char **pptr, Value v)
 		} else
 		    a = -ztrlen(t + a + strlen(t));
 	    }
-	    if (a > 0 && isset(KSHARRAYS))
+	    if (a > 0 && (isset(KSHARRAYS) || (v->pm->flags & PM_HASHED)))
 		a--;
 	    v->inv = 1;
 	    v->isarr = 0;
@@ -985,6 +1021,13 @@ getindex(char **pptr, Value v)
 Value
 getvalue(char **pptr, int bracks)
 {
+  return fetchvalue(pptr, bracks, 0);
+}
+
+/**/
+Value
+fetchvalue(char **pptr, int bracks, int flags)
+{
     char *s, *t;
     char sav;
     Value v;
@@ -1039,8 +1082,16 @@ getvalue(char **pptr, int bracks)
 	if (!pm || (pm->flags & PM_UNSET))
 	    return NULL;
 	v = (Value) hcalloc(sizeof *v);
-	if (PM_TYPE(pm->flags) & (PM_ARRAY|PM_HASHED))
-	    v->isarr = isvarat ? -1 : 1;
+	if (PM_TYPE(pm->flags) & (PM_ARRAY|PM_HASHED)) {
+	    /* Overload v->isarr as the flag bits for hashed arrays. */
+	    v->isarr = flags | (isvarat ? SCANPM_ISVAR_AT : 0);
+	    /* If no flags were passed, we need something to represent *
+	     * `true' yet differ from an explicit WANTVALS.  This is a *
+	     * bit of a hack, but makes some sense:  When no subscript *
+	     * is provided, all values are substituted.                */
+	    if (!v->isarr)
+		v->isarr = SCANPM_MATCHMANY;
+	}
 	v->pm = pm;
 	v->inv = 0;
 	v->a = 0;
@@ -1079,7 +1130,7 @@ getstrvalue(Value v)
     if (!v)
 	return hcalloc(1);
     HEAPALLOC {
-	if (v->inv) {
+	if (v->inv && !(v->pm->flags & PM_HASHED)) {
 	    sprintf(buf, "%d", v->a);
 	    s = dupstring(buf);
 	    LASTALLOC_RETURN s;
@@ -1087,6 +1138,13 @@ getstrvalue(Value v)
 
 	switch(PM_TYPE(v->pm->flags)) {
 	case PM_HASHED:
+	    /* (!v->isarr) should be impossible unless emulating ksh */
+	    if (!v->isarr && emulation == EMULATE_KSH) {
+		s = dupstring("[0]");
+		if (getindex(&s, v) == 0)
+		    s = getstrvalue(v);
+		LASTALLOC_RETURN s;
+	    } /* else fall through */
 	case PM_ARRAY:
 	    ss = getvaluearr(v);
 	    if (v->isarr)
@@ -1488,6 +1546,39 @@ setaparam(char *s, char **val)
 
 /**/
 Param
+sethparam(char *s, char **kvarr)
+{
+    Value v;
+    Param pm;
+    char *t;
+
+    if (!isident(s)) {
+	zerr("not an identifier: %s", s, 0);
+	freearray(kvarr);
+	errflag = 1;
+	return NULL;
+    }
+    t=ztrdup(s); /* Is this a memory leak? */
+    /* Why does getvalue(s, 1) set s to empty string? */
+    if ((v = getvalue(&t, 1)))
+	if (v->pm->flags & PM_SPECIAL) {
+	    zerr("not overriding a special: %s", s, 0);
+	    freearray(kvarr);
+	    errflag = 1;
+	    return NULL;
+	} else
+	    unsetparam(s);
+
+    pm = createparam(s, PM_HASHED);
+    DPUTS(!pm, "BUG: parameter not created");
+
+    arrhashsetfn(pm, kvarr);
+
+    return pm;
+}
+
+/**/
+Param
 setiparam(char *s, long val)
 {
     Value v;
@@ -2538,24 +2629,28 @@ printparamnode(HashNode hn, int printflags)
 	return;
     }
 
+    quotedzputs(p->nam, stdout);
+    if (printflags & PRINT_KV_PAIR)
+	putchar(' ');
+    else
+	putchar('=');
+
     /* How the value is displayed depends *
      * on the type of the parameter       */
-    quotedzputs(p->nam, stdout);
-    putchar('=');
     switch (PM_TYPE(p->flags)) {
     case PM_SCALAR:
 	/* string: simple output */
 	if (p->gets.cfn && (t = p->gets.cfn(p)))
 	    quotedzputs(t, stdout);
-	putchar('\n');
 	break;
     case PM_INTEGER:
 	/* integer */
-	printf("%ld\n", p->gets.ifn(p));
+	printf("%ld", p->gets.ifn(p));
 	break;
     case PM_ARRAY:
 	/* array */
-	putchar('(');
+	if (!(printflags & PRINT_KV_PAIR))
+	    putchar('(');
 	u = p->gets.afn(p);
 	if(*u) {
 	    quotedzputs(*u++, stdout);
@@ -2564,17 +2659,25 @@ printparamnode(HashNode hn, int printflags)
 		quotedzputs(*u++, stdout);
 	    }
 	}
-	printf(")\n");
+	if (!(printflags & PRINT_KV_PAIR))
+	    putchar(')');
 	break;
     case PM_HASHED:
 	/* association */
-	putchar('(');
+	if (!(printflags & PRINT_KV_PAIR))
+	    putchar('(');
 	{
             HashTable ht = p->gets.hfn(p);
             if (ht)
-		scanhashtable(ht, 0, 0, 0, ht->printnode, 0);
+		scanhashtable(ht, 0, 0, PM_UNSET,
+			      ht->printnode, PRINT_KV_PAIR);
 	}
-	printf(")\n");
+	if (!(printflags & PRINT_KV_PAIR))
+	    putchar(')');
 	break;
     }
+    if (printflags & PRINT_KV_PAIR)
+	putchar(' ');
+    else
+	putchar('\n');
 }
diff --git a/Src/parse.c b/Src/parse.c
index d42be2f2f..9024a834e 100644
--- a/Src/parse.c
+++ b/Src/parse.c
@@ -114,7 +114,7 @@ par_event(void)
     }
     if (tok == ENDINPUT)
 	return NULL;
-    if ((sl = par_sublist()))
+    if ((sl = par_sublist())) {
 	if (tok == ENDINPUT) {
 	    l = (List) make_list();
 	    l->type = Z_SYNC;
@@ -137,6 +137,7 @@ par_event(void)
 	    yylex();
 	} else
 	    l = NULL;
+    }
     if (!l) {
 	if (errflag) {
 	    yyerror();
@@ -181,7 +182,7 @@ par_list(void)
 
     while (tok == SEPER)
 	yylex();
-    if ((sl = par_sublist()))
+    if ((sl = par_sublist())) {
 	if (tok == SEPER || tok == AMPER || tok == AMPERBANG) {
 	    l = (List) make_list();
 	    l->left = sl;
@@ -197,6 +198,7 @@ par_list(void)
 	    l->left = sl;
 	    l->type = Z_SYNC;
 	}
+    }
     return l;
 }
 
@@ -1139,13 +1141,14 @@ par_cond_2(void)
 	condlex();
 	return c;
     }
-    if (tok != STRING)
+    if (tok != STRING) {
 	if (tok && tok != LEXERR && condlex == testlex) {
 	    s1 = tokstr;
 	    condlex();
 	    return par_cond_double("-n", s1);
 	} else
 	    YYERROR;
+    }
     s1 = tokstr;
     if (condlex == testlex)
 	dble = (*s1 == '-' && strspn(s1+1, "abcdefghknoprstuwxzLONGS") == 1
@@ -1165,7 +1168,7 @@ par_cond_2(void)
 	c->ntype = NT_SET(N_COND, NT_STR, NT_STR, 0, 0);
 	return c;
     }
-    if (tok != STRING)
+    if (tok != STRING) {
 	if (tok != LEXERR && condlex == testlex) {
 	    if (!dble)
 		return par_cond_double("-n", s1);
@@ -1173,6 +1176,7 @@ par_cond_2(void)
 		return par_cond_double(s1, "1");
 	} else
 	    YYERROR;
+    }
     s2 = tokstr;
     incond++;			/* parentheses do globbing */
     condlex();
@@ -1180,7 +1184,19 @@ par_cond_2(void)
     if (tok == STRING && !dble) {
 	s3 = tokstr;
 	condlex();
-	return par_cond_triple(s1, s2, s3);
+	if (tok == STRING) {
+	    LinkList l = newlinklist();
+
+	    addlinknode(l, s2);
+	    addlinknode(l, s3);
+
+	    while (tok == STRING) {
+		addlinknode(l, tokstr);
+		condlex();
+	    }
+	    return par_cond_multi(s1, l);
+	} else
+	    return par_cond_triple(s1, s2, s3);
     } else
 	return par_cond_double(s1, s2);
 }
@@ -1312,11 +1328,22 @@ par_cond_double(char *a, char *b)
 {
     Cond n = (Cond) make_cond();
 
-    if (a[0] != '-' || !a[1] || a[2])
-	COND_ERROR("parse error: condition expected: %s", a);
-    n->left = (void *) b;
-    n->type = a[1];
     n->ntype = NT_SET(N_COND, NT_STR, NT_STR, 0, 0);
+    n->left = (void *) b;
+    if (a[0] != '-' || !a[1])
+	COND_ERROR("parse error: condition expected: %s", a);
+    else if (!a[2] && strspn(a+1, "abcdefgknoprstuwxzhLONGS") == 1)
+	n->type = a[1];
+    else {
+	char *d[2];
+
+	n->ntype = NT_SET(N_COND, NT_STR, NT_STR | NT_ARR, 0, 0);
+	n->type = COND_MOD;
+	n->left = (void *) (a + 1);
+	d[0] = b;
+	d[1] = NULL;
+	n->right = (void *) arrdup(d);
+    }
     return n;
 }
 
@@ -1343,6 +1370,9 @@ par_cond_triple(char *a, char *b, char *c)
     Cond n = (Cond) make_cond();
     int t0;
 
+    n->ntype = NT_SET(N_COND, NT_STR, NT_STR, 0, 0);
+    n->left = (void *) a;
+    n->right = (void *) c;
     if ((b[0] == Equals || b[0] == '=') &&
 	(!b[1] || ((b[1] == Equals || b[1] == '=') && !b[2])))
 	n->type = COND_STREQ;
@@ -1351,13 +1381,46 @@ par_cond_triple(char *a, char *b, char *c)
     else if (b[0] == '-') {
 	if ((t0 = get_cond_num(b + 1)) > -1)
 	    n->type = t0 + COND_NT;
-	else
-	    COND_ERROR("unrecognized condition: %s", b);
+	else {
+	    char *d[3];
+
+	    n->ntype = NT_SET(N_COND, NT_STR, NT_STR | NT_ARR, 0, 0);
+	    n->type = COND_MODI;
+	    n->left = (void *) (b + 1);
+	    d[0] = a;
+	    d[1] = c;
+	    d[2] = NULL;
+	    n->right = (void *) arrdup(d);
+	}
+    } else if (a[0] == '-' && a[1]) {
+	char *d[3];
+
+	n->ntype = NT_SET(N_COND, NT_STR, NT_STR | NT_ARR, 0, 0);
+	n->type = COND_MOD;
+	n->left = (void *) (a + 1);
+	d[0] = b;
+	d[1] = c;
+	d[2] = NULL;
+	n->right = (void *) arrdup(d);
     } else
 	COND_ERROR("condition expected: %s", b);
-    n->left = (void *) a;
-    n->right = (void *) c;
-    n->ntype = NT_SET(N_COND, NT_STR, NT_STR, 0, 0);
+    return n;
+}
+
+/**/
+static Cond
+par_cond_multi(char *a, LinkList l)
+{
+    Cond n = (Cond) make_cond();
+
+    n->ntype = NT_SET(N_COND, NT_STR, NT_STR | NT_ARR, 0, 0);
+    if (a[0] != '-' || !a[1])
+	COND_ERROR("condition expected: %s", a);
+    else {
+	n->type = COND_MOD;
+	n->left = (void *) a;
+	n->right = (void *) listarr(l);
+    }
     return n;
 }
 
diff --git a/Src/signals.c b/Src/signals.c
index 5dc19dd22..e637a8ca9 100644
--- a/Src/signals.c
+++ b/Src/signals.c
@@ -712,7 +712,7 @@ dotrapargs(int sig, int *sigtr, void *sigfn)
 	    addlinknode(args, num);
 	} LASTALLOC;
 	trapreturn = -1;
-	doshfunc(sigfn, args, 0, 1);
+	doshfunc(name, sigfn, args, 0, 1);
 	freelinklist(args, (FreeFunc) NULL);
 	zsfree(name);
     } else HEAPALLOC {
diff --git a/Src/subst.c b/Src/subst.c
index cc1ae3027..77f0249e2 100644
--- a/Src/subst.c
+++ b/Src/subst.c
@@ -99,7 +99,7 @@ stringsubst(LinkList list, LinkNode node, int ssub)
     char *str  = str3;
 
     while (!errflag && *str) {
-	if ((qt = *str == Qstring) || *str == String)
+	if ((qt = *str == Qstring) || *str == String) {
 	    if (str[1] == Inpar) {
 		str++;
 		goto comsub;
@@ -125,7 +125,7 @@ stringsubst(LinkList list, LinkNode node, int ssub)
 		str3 = (char *)getdata(node);
 		continue;
 	    }
-	else if ((qt = *str == Qtick) || *str == Tick)
+	} else if ((qt = *str == Qtick) || *str == Tick)
 	  comsub: {
 	    LinkList pl;
 	    char *s, *str2 = str;
@@ -135,8 +135,12 @@ stringsubst(LinkList list, LinkNode node, int ssub)
 	    if (*str == Inpar) {
 		endchar = Outpar;
 		str[-1] = '\0';
+#ifdef DEBUG
 		if (skipparens(Inpar, Outpar, &str))
-		    DPUTS(1, "BUG: parse error in command substitution");
+		    dputs("BUG: parse error in command substitution");
+#else
+		skipparens(Inpar, Outpar, &str);
+#endif
 		str--;
 	    } else {
 		endchar = *str;
@@ -298,7 +302,7 @@ filesub(char **namptr, int assign)
     if (!assign)
 	return;
 
-    if (assign < 3)
+    if (assign < 3) {
 	if ((*namptr)[1] && (sub = strchr(*namptr + 1, Equals))) {
 	    if (assign == 1)
 		for (ptr = *namptr; ptr != sub; ptr++)
@@ -311,6 +315,7 @@ filesub(char **namptr, int assign)
 	    }
 	} else
 	    return;
+    }
 
     ptr = *namptr;
     while ((sub = strchr(ptr, ':'))) {
@@ -691,7 +696,6 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
     char *aptr = *str;
     char *s = aptr, *fstr, *idbeg, *idend, *ostr = (char *) getdata(n);
     int colf;			/* != 0 means we found a colon after the name */
-    int doub = 0;		/* != 0 means we have %%, not %, or ##, not # */
     int isarr = 0;
     int plan9 = isset(RCEXPANDPARAM);
     int globsubst = isset(GLOBSUBST);
@@ -705,11 +709,11 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
     Value v;
     int flags = 0;
     int flnum = 0;
-    int substr = 0;
     int sortit = 0, casind = 0;
     int casmod = 0;
     char *sep = NULL, *spsep = NULL;
     char *premul = NULL, *postmul = NULL, *preone = NULL, *postone = NULL;
+    char *replstr = NULL;	/* replacement string for /orig/repl */
     long prenum = 0, postnum = 0;
     int copied = 0;
     int arrasg = 0;
@@ -717,7 +721,7 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
     int nojoin = 0;
     char inbrace = 0;		/* != 0 means ${...}, otherwise $... */
     char hkeys = 0;		/* 1 means get keys from associative array */
-    char hvals = 1;		/* > hkeys get values of associative array */
+    char hvals = 0;		/* > hkeys get values of associative array */
 
     *s++ = '\0';
     if (!ialnum(*s) && *s != '#' && *s != Pound && *s != '-' &&
@@ -764,22 +768,22 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
 		    nojoin = 1;
 		    break;
 		case 'M':
-		    flags |= 8;
+		    flags |= SUB_MATCH;
 		    break;
 		case 'R':
-		    flags |= 16;
+		    flags |= SUB_REST;
 		    break;
 		case 'B':
-		    flags |= 32;
+		    flags |= SUB_BIND;
 		    break;
 		case 'E':
-		    flags |= 64;
+		    flags |= SUB_EIND;
 		    break;
 		case 'N':
-		    flags |= 128;
+		    flags |= SUB_LEN;
 		    break;
 		case 'S':
-		    substr = 1;
+		    flags |= SUB_SUBSTR;
 		    break;
 		case 'I':
 		    flnum = get_intarg(&s);
@@ -940,7 +944,7 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
 		s++;
 	    } else
 		globsubst = 1;
-	} else if (*s == '+')
+	} else if (*s == '+') {
 	    if (iident(s[1]))
 		chkset = 1, s++;
 	    else if (!inbrace) {
@@ -951,7 +955,7 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
 		zerr("bad substitution", NULL, 0);
 		return NULL;
 	    }
-	else
+	} else
 	    break;
     }
     globsubst = globsubst && !qt;
@@ -974,8 +978,12 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
 	copied = 1;
 	*s = sav;
 	v = (Value) NULL;
-    } else if (!(v = getvalue(&s, (unset(KSHARRAYS) || inbrace) ? 1 : -1)))
-	vunset = 1;
+    } else {
+	/* 2 == SCANPM_WANTKEYS, 1 == SCANPM_WANTVALS, see params.c */
+	if (!(v = fetchvalue(&s, (unset(KSHARRAYS) || inbrace) ? 1 : -1,
+			     (hkeys ? 2 : 0) + ((hvals > hkeys) ? 1 : 0))))
+	    vunset = 1;
+    }
     while (v || ((inbrace || (unset(KSHARRAYS) && vunset)) && isbrack(*s))) {
 	if (!v) {
 	    Param pm;
@@ -1000,13 +1008,8 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
 		break;
 	}
 	if ((isarr = v->isarr)) {
-	    /* No way to reach here with v->inv != 0, so getvaluearr() *
-	     * will definitely be called by getarrvalue().  Slicing of *
-	     * associations isn't done, so use v->a and v->b for flags */
-	    if (PM_TYPE(v->pm->flags) == PM_HASHED) {
-		v->a = hkeys;
-		v->b = hvals;
-	    }
+	    /* No way to get here with v->inv != 0, so getvaluearr() *
+	     * is called by getarrvalue(); needn't test PM_HASHED.   */
 	    aval = getarrvalue(v);
 	} else {
 	    if (v->pm->flags & PM_ARRAY) {
@@ -1124,23 +1127,72 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
 		    *s == '=' || *s == Equals ||
 		    *s == '%' ||
 		    *s == '#' || *s == Pound ||
-		    *s == '?' || *s == Quest)) {
+		    *s == '?' || *s == Quest ||
+		    *s == '/')) {
 
 	if (!flnum)
 	    flnum++;
 	if (*s == '%')
-	    flags |= 1;
+	    flags |= SUB_END;
 
 	/* Check for ${..%%..} or ${..##..} */
 	if ((*s == '%' || *s == '#' || *s == Pound) && *s == s[1]) {
 	    s++;
-	    doub = 1;
+	    /* we have %%, not %, or ##, not # */
+	    flags |= SUB_LONG;
 	}
 	s++;
+	if (s[-1] == '/') {
+	    char *ptr;
+	    /*
+	     * previous flags are irrelevant, except for (S) which
+	     * indicates shortest substring; else look for longest.
+	     */
+	    flags = (flags & SUB_SUBSTR) ? 0 : SUB_LONG;
+	    if (*s == '/') {
+		/* doubled, so replace all occurrences */
+		flags |= SUB_GLOBAL;
+		s++;
+	    }
+	    /* Check for anchored substitution */
+	    if (*s == '%') {
+		/* anchor at tail */
+		flags |= SUB_END;
+		s++;
+	    } else if (*s == '#' || *s == Pound) {
+		/* anchor at head: this is the `normal' case in getmatch */
+		s++;
+	    } else
+		flags |= SUB_SUBSTR;
+	    /*
+	     * Find the / marking the end of the search pattern.
+	     * If there isn't one, we're just going to delete that,
+	     * i.e. replace it with an empty string.
+	     *
+	     * This allows quotation of the slash with '\\/'. Why
+	     * two?  Well, for a non-quoted string we can check for
+	     * Bnull+/, which is what you get from `\/', but inside
+	     * double quotes the Bnull isn't there, so it's not
+	     * consistent.
+	     */
+	    for (ptr = s; *ptr && *ptr != '/'; ptr++)
+		if (*ptr == '\\' && ptr[1] == '/')
+		    chuck(ptr);
+	    replstr = (*ptr && ptr[1]) ? ptr+1 : "";
+	    singsub(&replstr);
+	    untokenize(replstr);
+	    *ptr = '\0';
+	}
 
-	flags |= (doub << 1) | (substr << 2) | (colf << 8);
-	if (!(flags & 0xf8))
-	    flags |= 16;
+	if (colf)
+	    flags |= SUB_ALL;
+	/*
+	 * With no special flags, i.e. just a # or % or whatever,
+	 * the matched portion is removed and we keep the rest.
+	 * We also want the rest when we're doing a substitution.
+	 */
+	if (!(flags & (SUB_MATCH|SUB_REST|SUB_BIND|SUB_EIND|SUB_LEN)))
+	    flags |= SUB_REST;
 
 	if (colf && !vunset)
 	    vunset = (isarr) ? !*aval : !*val || (*val == Nularg && !val[1]);
@@ -1234,6 +1286,7 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
 	case '%':
 	case '#':
 	case Pound:
+	case '/':
 	    if (qt)
 		if (parse_subst_string(s)) {
 		    zerr("parse error in ${...%c...} substitution",
@@ -1247,14 +1300,14 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int ssub)
 		char **pp = aval = (char **)ncalloc(sizeof(char *) * (arrlen(aval) + 1));
 
 		while ((*pp = *ap++)) {
-		    if (getmatch(pp, s, flags, flnum))
+		    if (getmatch(pp, s, flags, flnum, replstr))
 			pp++;
 		}
 		copied = 1;
 	    } else {
 		if (vunset)
 		    val = dupstring("");
-		getmatch(&val, s, flags, flnum);
+		getmatch(&val, s, flags, flnum, replstr);
 		copied = 1;
 	    }
 	    break;
diff --git a/Src/text.c b/Src/text.c
index 836a6a0a8..ec724f27d 100644
--- a/Src/text.c
+++ b/Src/text.c
@@ -410,6 +410,27 @@ getcond(Cond nm, int addpar)
 	taddstr(" || ");
 	getcond(nm->right, _Cond(nm->right)->type == COND_AND);
 	break;
+    case COND_MOD:
+	{
+	    /* Module defined prefix condition. */
+	    char **p = (char **) nm->right;
+
+	    taddstr("-");
+	    taddstr(nm->left);
+	    for (; *p; p++) {
+		taddstr(" ");
+		taddstr(*p);
+	    }
+	}
+	break;
+    case COND_MODI:
+	/* Module defined infix condition. */
+	taddstr(((char **) nm->right)[0]);
+	taddstr(" -");
+	taddstr(nm->left);
+	taddstr(" ");
+	taddstr(((char **) nm->right)[1]);
+	break;
     default:
 	if (nm->type <= COND_GE) {
 	    /* Binary test: `a = b' etc. */
diff --git a/Src/utils.c b/Src/utils.c
index 44223867f..af0247ebf 100644
--- a/Src/utils.c
+++ b/Src/utils.c
@@ -634,7 +634,7 @@ preprompt(void)
     /* If a shell function named "precmd" exists, *
      * then execute it.                           */
     if ((list = getshfunc("precmd")) != &dummy_list)
-	doshfunc(list, NULL, 0, 1);
+	doshfunc("precmd", list, NULL, 0, 1);
     if (errflag)
 	return;
 
@@ -643,7 +643,7 @@ preprompt(void)
      * executed "periodic", then execute it now.                    */
     if (period && (time(NULL) > lastperiodic + period) &&
 	(list = getshfunc("periodic")) != &dummy_list) {
-	doshfunc(list, NULL, 0, 1);
+	doshfunc("periodic", list, NULL, 0, 1);
 	lastperiodic = time(NULL);
     }
     if (errflag)
@@ -732,7 +732,7 @@ checkmailpath(char **s)
 	    }
 	} else {
 	    if (st.st_size && st.st_atime <= st.st_mtime &&
-		st.st_mtime > lastmailcheck)
+		st.st_mtime > lastmailcheck) {
 		if (!u) {
 		    fprintf(shout, "You have new mail.\n");
 		    fflush(shout);
@@ -751,6 +751,7 @@ checkmailpath(char **s)
 			underscore = usav;
 		    } LASTALLOC;
 		}
+	    }
 	    if (isset(MAILWARNING) && st.st_atime > st.st_mtime &&
 		st.st_atime > lastmailcheck && st.st_size) {
 		fprintf(shout, "The mail in %s has been read.\n", unmeta(*s));
@@ -1066,14 +1067,14 @@ zstrtol(const char *s, char **t, int base)
     else if (*s == '+')
 	s++;
 
-    if (!base)
+    if (!base) {
 	if (*s != '0')
 	    base = 10;
 	else if (*++s == 'x' || *s == 'X')
 	    base = 16, s++;
 	else
 	    base = 8;
- 
+    }
     if (base <= 10)
 	for (; *s >= '0' && *s < ('0' + base); s++)
 	    ret = ret * base + *s - '0';
@@ -2137,22 +2138,24 @@ dupstruct2(void *a)
 		n = dupstring(on);
 		break;
 	    case NT_LIST | NT_NODE:
-		if (heap)
+		if (heap) {
 		    if (useheap)
 			n =  duplist(on, (VFunc) dupstruct2);
 		    else
 			n = list2arr(on, (VFunc) dupstruct2);
+		}
 		else if (useheap)
 		    n = arr2list(on, (VFunc) dupstruct2);
 		else
 		    n = duparray(on, (VFunc) dupstruct2);
 		break;
 	    case NT_LIST | NT_STR:
-		if (heap)
+		if (heap) {
 		    if (useheap)
 			n = duplist(on, (VFunc) dupstring);
 		    else
 			n = list2arr(on, (VFunc) ztrdup);
+		}
 		else if (useheap)
 		    n = arr2list(on, (VFunc) dupstring);
 		else
@@ -2378,11 +2381,12 @@ inittyptab(void)
     for (t0 = (int)STOUC(Pound); t0 <= (int)STOUC(Nularg); t0++)
 	typtab[t0] |= ITOK | IMETA;
     for (s = ifs ? ifs : DEFAULT_IFS; *s; s++) {
-	if (inblank(*s))
+	if (inblank(*s)) {
 	    if (s[1] == *s)
 		s++;
 	    else
 		typtab[STOUC(*s)] |= IWSEP;
+	}
 	typtab[STOUC(*s == Meta ? *++s ^ 32 : *s)] |= ISEP;
     }
     for (s = wordchars ? wordchars : DEFAULT_WORDCHARS; *s; s++)
@@ -2406,6 +2410,21 @@ arrdup(char **s)
 }
 
 /**/
+char **
+listarr(LinkList l)
+{
+    char **x, **y;
+    LinkNode n;
+
+    x = y = (char **) ncalloc((countlinknodes(l) + 1) * sizeof(char *));
+
+    for (n = firstnode(l); n; incnode(n))
+	*x++ = dupstring((char *) getdata(n));
+    *x = NULL;
+    return y;
+}
+
+/**/
 static char *
 spname(char *oldname)
 {
@@ -3009,11 +3028,12 @@ niceztrdup(char const *s)
     char *p = buf, *n, *ret;
 
     while ((c = *s++)) {
-	if (itok(c))
+	if (itok(c)) {
 	    if (c <= Comma)
 		c = ztokens[c - Pound];
 	    else 
 		continue;
+	}
 	if (c == Meta)
 	    c = *s++ ^ 32;
 	n = nicechar(c);
@@ -3034,11 +3054,12 @@ nicezputs(char const *s, FILE *stream)
     int c;
 
     while ((c = *s++)) {
-	if (itok(c))
+	if (itok(c)) {
 	    if (c <= Comma)
 		c = ztokens[c - Pound];
 	    else 
 		continue;
+	}
 	if (c == Meta)
 	    c = *s++ ^ 32;
 	if(fputs(nicechar(c), stream) < 0)
@@ -3057,11 +3078,12 @@ niceztrlen(char const *s)
     int c;
 
     while ((c = *s++)) {
-	if (itok(c))
+	if (itok(c)) {
 	    if (c <= Comma)
 		c = ztokens[c - Pound];
 	    else 
 		continue;
+	}
 	if (c == Meta)
 	    c = *s++ ^ 32;
 	l += strlen(nicechar(STOUC(c)));
@@ -3328,13 +3350,14 @@ getkeystring(char *s, int *len, int fromwhere, int *misc)
 		}
 	    default:
 		if ((idigit(*s) && *s < '8') || *s == 'x') {
-		    if (!fromwhere)
+		    if (!fromwhere) {
 			if (*s == '0')
 			    s++;
 			else if (*s != 'x') {
 			    *t++ = '\\', s--;
 			    continue;
 			}
+		    }
 		    if (s[1] && s[2] && s[3]) {
 			svchar = s[3];
 			s[3] = '\0';
diff --git a/Src/zsh.export b/Src/zsh.export
index 701aeb990..c51699269 100644
--- a/Src/zsh.export
+++ b/Src/zsh.export
@@ -1,8 +1,10 @@
 #!
 SHTTY
 addbuiltins
+addconddefs
 addedx
 addhashnode
+addwrapper
 aliastab
 alloc_stackp
 appstr
@@ -30,7 +32,9 @@ ctxtlex
 curhist
 current_limits
 deletebuiltins
+deleteconddefs
 deletehashtable
+deletewrapper
 domatch
 doshfunc
 dputs
@@ -65,6 +69,7 @@ getkeystring
 getlinknode
 getshfunc
 getsparam
+gettempname
 glob_pre
 glob_suf
 global_heapalloc
@@ -82,6 +87,7 @@ hgetc
 hgetline
 histentarr
 histentct
+holdintr
 hptr
 hrealloc
 inbufct
@@ -94,6 +100,7 @@ inpop
 inpush
 inredir
 insertlinknode
+install_handler
 intr
 inwhat
 isfirstln
@@ -108,6 +115,7 @@ limits
 line
 lines
 locallevel
+matheval
 metadiffer
 metafy
 metalen
@@ -124,6 +132,7 @@ niceztrdup
 niceztrlen
 noaliases
 noerrs
+noholdintr
 noop_function
 noop_function_int
 optiontab
@@ -157,8 +166,10 @@ resetneeded
 restoredir
 reswdtab
 retflag
+runshfunc
 scanhashtable
 setaparam
+sethparam
 setlimits
 setsparam
 settyinfo
@@ -166,6 +177,8 @@ shfunctab
 shingetline
 shout
 shttyinfo
+sigfuncs
+sigtrapped
 singsub
 skipparens
 spaceinlineptr
@@ -184,6 +197,7 @@ struncpy
 tclen
 tcstr
 termflags
+thisjob
 tgoto
 tok
 tokenize
@@ -201,6 +215,7 @@ ugetnode
 uinsertlinknode
 unmeta
 unmetafy
+unsetparam_pm
 untokenize
 uremnode
 useheap
@@ -210,6 +225,7 @@ zalloc
 zbeep
 zcalloc
 zchdir
+zclose
 zerr
 zerrnam
 zexit
@@ -222,6 +238,7 @@ zleparse
 zlereadptr
 zputs
 zreaddir
+zrealloc
 zsetlimit
 zsfree
 zshcs
diff --git a/Src/zsh.h b/Src/zsh.h
index 837a76e88..6a962d8bf 100644
--- a/Src/zsh.h
+++ b/Src/zsh.h
@@ -233,6 +233,7 @@ typedef struct alias     *Alias;
 typedef struct param     *Param;
 typedef struct cmdnam    *Cmdnam;
 typedef struct shfunc    *Shfunc;
+typedef struct funcwrap  *FuncWrap;
 typedef struct builtin   *Builtin;
 typedef struct nameddir  *Nameddir;
 typedef struct module    *Module;
@@ -242,6 +243,7 @@ typedef struct job       *Job;
 typedef struct value     *Value;
 typedef struct varasg    *Varasg;
 typedef struct cond      *Cond;
+typedef struct conddef   *Conddef;
 typedef struct cmd       *Cmd;
 typedef struct pline     *Pline;
 typedef struct sublist   *Sublist;
@@ -455,6 +457,26 @@ struct cond {
 #define COND_GT    13
 #define COND_LE    14
 #define COND_GE    15
+#define COND_MOD   16
+#define COND_MODI  17
+
+typedef int (*CondHandler) _((Conddef, char **));
+
+struct conddef {
+    Conddef next;		/* next in list                       */
+    char *name;			/* the condition name                 */
+    int flags;			/* see CONDF_* below                  */
+    int min;			/* minimum number of strings          */
+    int max;			/* maximum number of strings          */
+    CondHandler handler;	/* handler function                   */
+    char *module;		/* module to autoload                 */
+};
+
+#define CONDF_INFIX  1
+#define CONDF_ADDED  2
+
+#define CONDDEF(name, flags, min, max, handler) \
+    { NULL, name, flags, min, max, handler, NULL }
 
 struct forcmd {			/* for/select */
 /* Cmd->args contains list of words to loop thru */
@@ -750,6 +772,23 @@ struct shfunc {
     List funcdef;		/* function definition    */
 };
 
+/* node in list of function call wrappers */
+
+typedef int (*WrapFunc) _((List, FuncWrap, char *));
+
+struct funcwrap {
+    FuncWrap next;
+    int flags;
+    WrapFunc handler;
+    Module module;
+    int count;
+};
+
+#define WRAPF_ADDED 1
+
+#define WRAPDEF(func) \
+    { NULL, 0, func, NULL, 0 }
+
 /* node in builtin command hash table (builtintab) */
 
 typedef int (*HandlerFunc) _((char *, char **, char *, int));
@@ -800,6 +839,8 @@ struct module {
 };
 
 #define MOD_BUSY    (1<<0)
+#define MOD_WRAPPER (1<<1)
+#define MOD_UNLOAD  (1<<2)
 
 /* node used in parameter hash table (paramtab) */
 
@@ -872,6 +913,23 @@ struct param {
 #define PM_RESTRICTED	(1<<13) /* cannot be changed in restricted mode       */
 #define PM_UNSET	(1<<14)	/* has null value                             */
 
+/*
+ * Flags for doing matches inside parameter substitutions, i.e.
+ * ${...#...} and friends.  This could be an enum, but so
+ * could a lot of other things.
+ */
+
+#define SUB_END		0x0001	/* match end instead of begining, % or %%  */
+#define SUB_LONG	0x0002	/* % or # doubled, get longest match */
+#define SUB_SUBSTR	0x0004	/* match a substring */
+#define SUB_MATCH	0x0008	/* include the matched portion */
+#define SUB_REST	0x0010	/* include the unmatched portion */
+#define SUB_BIND	0x0020	/* index of beginning of string */
+#define SUB_EIND	0x0040	/* index of end of string */
+#define SUB_LEN		0x0080	/* length of match */
+#define SUB_ALL		0x0100	/* match complete string */
+#define SUB_GLOBAL	0x0200	/* global substitution ${..//all/these} */
+
 /* node for named directory hash table (nameddirtab) */
 
 struct nameddir {
@@ -891,13 +949,14 @@ struct nameddir {
 #define PRINT_NAMEONLY		(1<<0)
 #define PRINT_TYPE		(1<<1)
 #define PRINT_LIST		(1<<2)
+#define PRINT_KV_PAIR		(1<<3)
 
 /* flags for printing for the whence builtin */
-#define PRINT_WHENCE_CSH	(1<<3)
-#define PRINT_WHENCE_VERBOSE	(1<<4)
-#define PRINT_WHENCE_SIMPLE	(1<<5)
-#define PRINT_WHENCE_FUNCDEF	(1<<6)
-#define PRINT_WHENCE_WORD	(1<<7)
+#define PRINT_WHENCE_CSH	(1<<4)
+#define PRINT_WHENCE_VERBOSE	(1<<5)
+#define PRINT_WHENCE_SIMPLE	(1<<6)
+#define PRINT_WHENCE_FUNCDEF	(1<<7)
+#define PRINT_WHENCE_WORD	(1<<8)
 
 /***********************************/
 /* Definitions for history control */
diff --git a/configure.in b/configure.in
index 822a7f477..6985f24e4 100644
--- a/configure.in
+++ b/configure.in
@@ -428,6 +428,8 @@ fi
 
 AC_CHECK_LIB(cap, cap_get_proc)
 
+AC_CHECK_LIB(socket, socket)
+
 dnl ---------------------
 dnl CHECK TERMCAP LIBRARY
 dnl ---------------------
diff --git a/patchlist.txt b/patchlist.txt
index 9c98bd6e3..923919098 100644
--- a/patchlist.txt
+++ b/patchlist.txt
@@ -47,7 +47,8 @@ Bart's chpwd() fix 4589
   Second edition
 
 Added line in zle_tricky.c missed when patching by hand, spotted by
-Bart.  (Whitespace is still non-canonical.)
+Bart.  (Whitespace is still non-canonical in the completion code where
+I have merged patches by hand.)
 
 Fixed up my compctl widgets patch for use with Sven's rewrite, which I
 hadn't done properly before.
@@ -58,7 +59,9 @@ Bart's doc fixes, 4472
 
 Bart's PWD and OLDPWD reshuffle, 4589
 
-My test-line-length patch for prompts, 4591
+My test-line-length patch for prompts, 4591 (`%(40l.yes.no)' outputs
+`yes' if at least 40 characters have already appeared on the line,
+`no' otherwise.)
 
 Configure patch from Wilfredo Sanchez in 4594, with some extra
 tabbification and without the setterm() hunk, since I've already renamed
@@ -84,12 +87,16 @@ one two
 % print ${(kv)hash}                  # flag to get keys and values (**)
 one eins two zwei
 Comparison of (*) and (**) will reveal how to copy an associative
-array, but you always need to declare it with typeset -A or an
-ordinary array will appear.  There is a predefined special associative
-array $testhash, for testing purposes only, which will eventually
-disappear.
+array, `hash2=(${(kv}hash})', but you always need to `typeset -A
+hash2' first or an ordinary array will appear.  There is a predefined
+special associative array $testhash, for testing purposes only, which
+will eventually disappear.
 
-My rewrite of prompt truncation, 4601
+My rewrite of prompt truncation, 4601 --- note this introduces a
+slight incompatibility in that the string to be truncated now runs by
+default to the end of the string, instead of only covering individual
+%-substitutions.  If necessary, stick in an extra '%>>' to turn
+truncation off at the point you want.
 
 Bart's params error message fix, 4606
 
@@ -100,3 +107,65 @@ Bart's version of the *** fix, 4624
 Bart's parameter substitution flag delimiter fix, 4644
 
 My special parameter unset fix, 4662
+
+  Third edition
+
+I've taken the plunge and changed $ZSH_VERSION, the current one is now
+3.1.5.pws-1 .  It seemed rational to have something incremental at the
+end for testing, so I abandoned using the date.
+
+4482 (cdmatch2)and 4641 (${assoc[0]}) now applied; 4641 was supposed
+to be there before.
+
+nroff manual pages deleted, you now need yodl.
+
+deleted modules-bltin by hand, which `make distclean' somehow missed.
+Caused problems when building a statically linked shell.
+
+Bart's scanmatchtable fix, 4674
+
+Commented out vifirstnonblank() in vioperswapcase(), pending any
+better patch for it.
+
+Bart's viforwardword fix, 4678
+
+My case-independent globbing fix, 4693
+
+Sven's zle_tricky.c, 4697
+
+Sven's patch to ignore completions if the cursor is not in a part to
+be completed, 4698, plus addition, 4707
+
+I have not added Sven's zerr() patch, 4699, in case it has side
+effects, but I haven't heard anything on the subject and I haven't
+looked at it.
+
+Sven's pennockite heap memory patch, 4700
+
+Sven's condition module patch, 4716, and addition, 4732, and the
+function wrapper patch, 4734, and additions, 4742, 4769: the module.c
+bits of these have been moved around a little to avoid clashes with
+the AIXDYNAMIC stuff.  The wrapper stuff is still not finished, but
+doesn't currently impinge on use of the shell.
+
+Phil Pennock's patch to use associative arrays in stat, 4727
+
+Unposted fix for use of printcompctlptr in completion widgets:
+printcompctl() had acquired another argument.
+
+My bash-like ${foo/orig/new} patch, 4736, and the version to do
+shortest matching together with optimizations of all pattern matching
+in variable strings, 4754.
+
+Phil's patch for typeset -a docs, 4737
+
+Nobody wanted my fix for `FOO=x eval external', so it's not there.
+
+zftp, 4761
+
+Bart's fix for conddef without dynamical modules, 4762
+
+Bart's associative array patches for implentation of subscripting flags,
+4763, plus fix 4766; typeset output 4764
+
+Sven's completion listing fix, 4767