about summary refs log tree commit diff
path: root/Src
diff options
context:
space:
mode:
Diffstat (limited to 'Src')
-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
27 files changed, 3856 insertions, 358 deletions
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 */